Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/ui/src/ui-desks/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { ActionButton } from './action-button';
export { ControlButton, IconControlButton } from './control-button';
export { LoadingPlaceholder } from './loading-placeholder';
export { List, ListItem } from './list';
export { Divider, Surface } from './surface';
export * as Menu from './menu';
17 changes: 17 additions & 0 deletions apps/ui/src/ui-desks/components/loading-placeholder/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { clsx } from 'clsx';
import styles from './style.module.css';
import type { ComponentPropsWithoutRef } from 'react';

type LoadingPlaceholderProps = ComponentPropsWithoutRef< 'div' > & {
text?: string;
};

export function LoadingPlaceholder( { className, text, ...props }: LoadingPlaceholderProps ) {
return (
<div { ...props } className={ clsx( styles.placeholder, className ) }>
{ text && <div className={ styles.title }>{ text }</div> }
<div className={ styles.line } />
<div className={ styles.shortLine } />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
.placeholder {
display: flex;
flex-direction: column;
gap: 8px;
}

.title {
font-size: 13px;
line-height: 18px;
font-weight: 500;
color: var(--loading-placeholder-text-color, #50575e);
}

.line {
width: 68%;
height: 8px;
border-radius: 999px;
background: linear-gradient(
90deg,
var(--loading-placeholder-line-color, #f0f0f0) 0%,
var(--loading-placeholder-line-highlight-color, #fff) 45%,
var(--loading-placeholder-line-color, #f0f0f0) 90%
);
background-size: 220% 100%;
animation: shimmer 1.4s ease-in-out infinite;
}

.shortLine {
width: 44%;
height: 8px;
border-radius: 999px;
background: var(--loading-placeholder-line-color, #f0f0f0);
}

@keyframes shimmer {
0% {
background-position: 120% 0;
}

100% {
background-position: -120% 0;
}
}
220 changes: 214 additions & 6 deletions apps/ui/src/ui-desks/desk/provider/editor-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ import {
stackSelectedWidgetsInEditor as stackSelectionInEditor,
unstackSelectedWidgetsInEditor as unstackSelectionInEditor,
} from '@/ui-desks/stacks/editor-commands';
import { getStackId } from '@/ui-desks/stacks/utils';
import {
getStackAnchorFromMember,
getStackHome,
getStackId,
getStackOrder,
getStackZIndexFromMember,
} from '@/ui-desks/stacks/utils';
import { createDeskWidget } from '@/ui-desks/widgets/create-widget';
import { getSelectedWidgetToolbarItem } from '@/ui-desks/widgets/toolbar-selection';
import {
Expand All @@ -16,6 +22,10 @@ import {
canvasShapesToDeskStacks,
deskConfigToCanvasShapes,
deskWidgetToCanvasShape,
getDerivedDeskCanvasRecordSourceId,
hasOnlyDeskCanvasRecordResolutionStateChange,
isDerivedDeskCanvasRecord,
isPersistentDeskCanvasShape,
} from '../tldraw-adapter';
import { DESK_CONFIG_VERSION, type DeskConfig } from '../types';
import type { SelectedWidgetToolbarItem, AddDeskWidgetOptions } from './context';
Expand All @@ -27,6 +37,12 @@ interface CanvasStoreChanges {
removed: Record< string, unknown >;
}

interface DerivedWidgetAnchor {
x: number;
y: number;
zIndex?: string;
}

export function hydrateEditorFromDesk( editor: Editor, desk: DeskConfig ) {
const existingShapes = editor.getCurrentPageShapes();
if ( existingShapes.length > 0 ) {
Expand Down Expand Up @@ -133,7 +149,7 @@ export function updateSelectedWidgetPropsInEditor(

export function removeSelectedWidgetFromEditor( editor: Editor ) {
const selection = getCurrentSelectedWidgetSelection( editor );
if ( ! selection ) {
if ( ! selection || ! selection.item.canRemove ) {
return false;
}

Expand Down Expand Up @@ -167,6 +183,37 @@ export function hasCameraChange( changes: CanvasStoreChanges ) {
return records.some( isCameraRecord );
}

export function hasPersistentDocumentChange( changes: CanvasStoreChanges ) {
const addedOrRemovedRecords = [
...Object.values( changes.added ),
...Object.values( changes.removed ),
];
if ( addedOrRemovedRecords.some( isPersistentDocumentRecord ) ) {
return true;
}

return Object.values( changes.updated ).some( ( [ previousRecord, nextRecord ] ) => {
if ( isShapeRecord( previousRecord ) || isShapeRecord( nextRecord ) ) {
if ( hasOnlyDeskCanvasRecordResolutionStateChange( previousRecord, nextRecord ) ) {
return false;
}

if (
isDerivedDeskCanvasRecord( previousRecord ) ||
isDerivedDeskCanvasRecord( nextRecord )
) {
return hasDerivedShapePersistenceChange( previousRecord, nextRecord );
}

return (
isPersistentDocumentRecord( previousRecord ) || isPersistentDocumentRecord( nextRecord )
);
}

return true;
} );
}

function getCurrentSelectedWidgetSelection( editor: Editor ) {
const selectedShapeIds = editor.getSelectedShapeIds();
if ( selectedShapeIds.length === 0 ) {
Expand All @@ -178,6 +225,11 @@ function getCurrentSelectedWidgetSelection( editor: Editor ) {
return null;
}

const derivedSourceSelection = getDerivedSourceWidgetSelection( editor, shapes as TLShape[] );
if ( derivedSourceSelection ) {
return derivedSourceSelection;
}

const widgets = shapes.map( ( shape ) => canvasShapeToDeskWidget( shape as TLShape ) );
if ( widgets.some( ( widget ) => ! widget ) ) {
return null;
Expand All @@ -201,11 +253,18 @@ function getCurrentSelectedWidgetSelection( editor: Editor ) {
};
}

function getCurrentDeskWidgets( editor: Editor ) {
return editor
.getCurrentPageShapes()
export function getCurrentDeskWidgets( editor: Editor ) {
const shapes = editor.getCurrentPageShapes();
const derivedAnchors = getDerivedWidgetAnchorsBySourceId( shapes );

return shapes
.filter( isPersistentDeskCanvasShape )
.map( canvasShapeToDeskWidget )
.filter( ( widget ) => widget !== null );
.filter( ( widget ): widget is DeskWidget => widget !== null )
.map( ( widget ) => {
const anchor = derivedAnchors.get( widget.id );
return anchor ? { ...widget, ...anchor } : widget;
} );
}

function getCurrentDeskStacks( editor: Editor ) {
Expand All @@ -220,6 +279,155 @@ function isCameraRecord( value: unknown ) {
);
}

function isShapeRecord( value: unknown ) {
return (
Boolean( value ) &&
typeof value === 'object' &&
( value as { typeName?: unknown } ).typeName === 'shape'
);
}

function isPersistentDocumentRecord( value: unknown ) {
if ( isShapeRecord( value ) ) {
return ! isDerivedDeskCanvasRecord( value );
}

return true;
}

function hasDerivedShapePersistenceChange( previousRecord: unknown, nextRecord: unknown ) {
if (
! isDerivedDeskCanvasRecord( previousRecord ) ||
! isDerivedDeskCanvasRecord( nextRecord )
) {
return false;
}

return (
getDerivedShapePersistenceSignature( previousRecord ) !==
getDerivedShapePersistenceSignature( nextRecord )
);
}

function getDerivedShapePersistenceSignature( value: unknown ) {
const shape = value as Partial< TLShape >;
const meta = ( shape.meta ?? {} ) as Record< string, unknown >;

return JSON.stringify( {
x: shape.x,
y: shape.y,
rotation: shape.rotation,
index: shape.index,
deskStackExpanded: meta.deskStackExpanded,
deskStackHomeX: meta.deskStackHomeX,
deskStackHomeY: meta.deskStackHomeY,
deskStackHomeZIndex: meta.deskStackHomeZIndex,
} );
}

function getDerivedSourceWidgetSelection( editor: Editor, shapes: TLShape[] ) {
const sourceWidgetId = getDerivedSelectionSourceWidgetId( shapes );
if ( ! sourceWidgetId ) {
return null;
}

const sourceShape = editor
.getCurrentPageShapes()
.find( ( shape ) => canvasShapeToDeskWidget( shape )?.id === sourceWidgetId );
if ( ! sourceShape ) {
return null;
}

const sourceWidget = canvasShapeToDeskWidget( sourceShape );
if ( ! sourceWidget ) {
return null;
}

const item = getSelectedWidgetToolbarItem( [ sourceWidget ], {
stackIds: [],
canRemove: false,
} );
if ( ! item ) {
return null;
}

return {
item,
shapes: [ sourceShape ],
};
}

function getDerivedSelectionSourceWidgetId( shapes: TLShape[] ) {
let sourceWidgetId: string | null = null;
for ( const shape of shapes ) {
const nextSourceWidgetId = getDerivedDeskCanvasRecordSourceId( shape );
if ( ! nextSourceWidgetId ) {
return null;
}

if ( sourceWidgetId === null ) {
sourceWidgetId = nextSourceWidgetId;
} else if ( sourceWidgetId !== nextSourceWidgetId ) {
return null;
}
}

return sourceWidgetId;
}

function getDerivedWidgetAnchorsBySourceId( shapes: TLShape[] ) {
const shapesBySourceId = new Map< string, TLShape[] >();
for ( const shape of shapes ) {
const sourceWidgetId = getDerivedDeskCanvasRecordSourceId( shape );
if ( ! sourceWidgetId ) {
continue;
}

shapesBySourceId.set( sourceWidgetId, [
...( shapesBySourceId.get( sourceWidgetId ) ?? [] ),
shape,
] );
}

return new Map(
Array.from( shapesBySourceId, ( [ sourceWidgetId, sourceShapes ] ) => [
sourceWidgetId,
getDerivedWidgetAnchor( sourceShapes ),
] ).filter( ( entry ): entry is [ string, DerivedWidgetAnchor ] => entry[ 1 ] !== null )
);
}

function getDerivedWidgetAnchor( shapes: TLShape[] ): DerivedWidgetAnchor | null {
const firstShape = [ ...shapes ].sort( ( first, second ) => {
const firstStackOrder = getStackOrder( first );
const secondStackOrder = getStackOrder( second );
return firstStackOrder - secondStackOrder || sortByIndex( second, first );
} )[ 0 ];
if ( ! firstShape ) {
return null;
}

const stackId = getStackId( firstShape );
if ( ! stackId ) {
return {
x: firstShape.x,
y: firstShape.y,
zIndex: firstShape.index,
};
}

const order = getStackOrder( firstShape );
const home = getStackHome( firstShape );
if ( home ) {
return home;
}

return {
...getStackAnchorFromMember( firstShape, order ),
zIndex: getStackZIndexFromMember( firstShape.index, order ),
};
}

function ensureContentVisible( editor: Editor ) {
const shapes = editor.getCurrentPageShapes();
if ( shapes.length === 0 ) {
Expand Down
13 changes: 11 additions & 2 deletions apps/ui/src/ui-desks/desk/provider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ import {
createDeskConfigFromEditor,
getCurrentSelectedWidgetToolbarItem,
hasCameraChange,
hasPersistentDocumentChange,
hydrateEditorFromDesk,
removeSelectedWidgetFromEditor,
stackSelectedWidgetsInEditor,
unstackSelectedWidgetsInEditor,
updateSelectedWidgetPropsInEditor,
} from './editor-state';
import { useDeskPersistence } from './persistence';
import { useDeskWidgetResolvers } from './resolvers';
import type { Editor } from 'tldraw';

export { useDesk, useRegisterDeskEditor } from './context';
Expand Down Expand Up @@ -72,8 +74,10 @@ export function DeskProvider( { siteId, children }: DeskProviderProps ) {
};

const unsubscribeDocument = editor.store.listen(
() => {
queueSave();
( { changes } ) => {
if ( hasPersistentDocumentChange( changes ) ) {
queueSave();
}
syncSelectedWidgetToolbarItem();
},
{ scope: 'document' }
Expand Down Expand Up @@ -205,5 +209,10 @@ export function DeskProvider( { siteId, children }: DeskProviderProps ) {
]
);

useDeskWidgetResolvers( {
editor,
isEnabled: Boolean( siteId && isHydrated ),
} );

return <DeskContext.Provider value={ value }>{ children }</DeskContext.Provider>;
}
Loading
Loading