diff --git a/apps/examples/src/15-custom-zones/ZonesExample.tsx b/apps/examples/src/15-custom-zones/ZonesExample.tsx new file mode 100644 index 000000000..fae8ed977 --- /dev/null +++ b/apps/examples/src/15-custom-zones/ZonesExample.tsx @@ -0,0 +1,40 @@ +import { Tldraw } from '@tldraw/tldraw' +import '@tldraw/tldraw/editor.css' +import '@tldraw/tldraw/ui.css' + +export default function Example() { + return ( +
+ } topZone={} /> +
+ ) +} + +function CustomShareZone() { + return ( +
+

Share Zone

+
+ ) +} + +function CustomTopZone() { + return ( +
+

Top Zone

+
+ ) +} diff --git a/apps/examples/src/index.tsx b/apps/examples/src/index.tsx index a73552916..c0d8115f2 100644 --- a/apps/examples/src/index.tsx +++ b/apps/examples/src/index.tsx @@ -14,6 +14,7 @@ import UserPresenceExample from './11-user-presence/UserPresenceExample' import UiEventsExample from './12-ui-events/UiEventsExample' import StoreEventsExample from './13-store-events/StoreEventsExample' import PersistenceExample from './14-persistence/PersistenceExample' +import ZonesExample from './15-custom-zones/ZonesExample' import ExampleApi from './2-api/APIExample' import CustomConfigExample from './3-custom-config/CustomConfigExample' import CustomUiExample from './4-custom-ui/CustomUiExample' @@ -90,6 +91,10 @@ export const allExamples: Example[] = [ path: '/user-presence', element: , }, + { + path: '/zones', + element: , + }, { path: '/persistence', element: , diff --git a/assets/translations/main.json b/assets/translations/main.json index 8177d47a3..089e99e54 100644 --- a/assets/translations/main.json +++ b/assets/translations/main.json @@ -229,6 +229,7 @@ "share-menu.save-note": "Download this project to your computer as a .tldr file.", "share-menu.fork-note": "Create a new shared project based on this snapshot.", "share-menu.share-project": "Share this project", + "share-menu.default-project-name": "Shared Project", "share-menu.copy-link": "Copy share link", "share-menu.readonly-link": "Read-only", "share-menu.create-snapshot-link": "Copy snapshot link", @@ -277,6 +278,12 @@ "shortcuts-dialog.tools": "Tools", "shortcuts-dialog.transform": "Transform", "shortcuts-dialog.view": "View", + "home-project-dialog.title": "Home project", + "home-project-dialog.description": "This is your local home project. It's just for you!", + "rename-project-dialog.title": "Rename project", + "rename-project-dialog.cancel": "Cancel", + "rename-project-dialog.rename": "Rename", + "home-project-dialog.ok": "Ok", "style-panel.title": "Styles", "style-panel.align": "Align", "style-panel.vertical-align": "Vertical align", diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 93d96f26d..8a863097b 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -390,6 +390,8 @@ export class App extends EventEmitter { panZoomIntoView(ids: TLShapeId[], opts?: AnimationOptions): this; // (undocumented) popFocusLayer(): this; + // @internal (undocumented) + get projectName(): string; // @internal get props(): null | TLNullableShapeProps; // (undocumented) @@ -471,6 +473,8 @@ export class App extends EventEmitter { setLocale(locale: string): void; // (undocumented) setPenMode(isPenMode: boolean): this; + // @internal (undocumented) + setProjectName(name: string): void; setProp(key: TLShapeProp, value: any, ephemeral?: boolean, squashing?: boolean): this; // @internal (undocumented) setReadOnly(isReadOnly: boolean): this; @@ -512,6 +516,8 @@ export class App extends EventEmitter { updateAssets(assets: TLAssetPartial[]): this; // @internal updateCullingBounds(): this; + // @internal (undocumented) + updateDocumentSettings(settings: Partial): void; updateInstanceState(partial: Partial>, ephemeral?: boolean, squashing?: boolean): this; updatePage(partial: RequiredKeys, squashing?: boolean): this; updateShapes(partials: (null | TLShapePartial | undefined)[], squashing?: boolean): this; @@ -1829,6 +1835,7 @@ export type TldrawEditorProps = { initialData?: StoreSnapshot; instanceId?: TLInstanceId; persistenceKey?: string; + defaultName?: string; }); // @public (undocumented) diff --git a/packages/editor/src/lib/TldrawEditor.tsx b/packages/editor/src/lib/TldrawEditor.tsx index 2f83c7b65..e16dbe364 100644 --- a/packages/editor/src/lib/TldrawEditor.tsx +++ b/packages/editor/src/lib/TldrawEditor.tsx @@ -119,6 +119,10 @@ export type TldrawEditorProps = { * The id under which to sync and persist the editor's data. */ persistenceKey?: string + /** + * The initial document name to use for the new store. + */ + defaultName?: string } ) @@ -169,13 +173,14 @@ export const TldrawEditor = memo(function TldrawEditor(props: TldrawEditorProps) }) function TldrawEditorWithOwnStore(props: TldrawEditorProps & { store: undefined }) { - const { initialData, instanceId = TAB_ID, shapes, persistenceKey } = props + const { defaultName, initialData, instanceId = TAB_ID, shapes, persistenceKey } = props const syncedStore = useLocalStore({ customShapes: shapes, instanceId, initialData, persistenceKey, + defaultName, }) return diff --git a/packages/editor/src/lib/app/App.ts b/packages/editor/src/lib/app/App.ts index 8fbd0ecd6..934f950e3 100644 --- a/packages/editor/src/lib/app/App.ts +++ b/packages/editor/src/lib/app/App.ts @@ -35,6 +35,7 @@ import { TLCursor, TLCursorType, TLDOCUMENT_ID, + TLDocument, TLFrameShape, TLGroupShape, TLImageAsset, @@ -1515,10 +1516,25 @@ export class App extends EventEmitter { return this.store.get(TLDOCUMENT_ID)! } + /** @internal */ + updateDocumentSettings(settings: Partial) { + this.store.put([{ ...this.documentSettings, ...settings }]) + } + get gridSize() { return this.documentSettings.gridSize } + /** @internal */ + get projectName() { + return this.documentSettings.name + } + + /** @internal */ + setProjectName(name: string) { + this.updateDocumentSettings({ name }) + } + get isSnapMode() { return this.userDocumentSettings.isSnapMode } diff --git a/packages/editor/src/lib/config/createTLStore.ts b/packages/editor/src/lib/config/createTLStore.ts index c41d6c373..64c57567a 100644 --- a/packages/editor/src/lib/config/createTLStore.ts +++ b/packages/editor/src/lib/config/createTLStore.ts @@ -21,6 +21,7 @@ export type StoreOptions = { customShapes?: Record instanceId?: TLInstanceId initialData?: StoreSnapshot + defaultName?: string } /** @@ -30,7 +31,12 @@ export type StoreOptions = { * * @public */ export function createTLStore(opts = {} as StoreOptions): TLStore { - const { customShapes = {}, instanceId = InstanceRecordType.createId(), initialData } = opts + const { + customShapes = {}, + instanceId = InstanceRecordType.createId(), + initialData, + defaultName = '', + } = opts return new Store({ schema: createTLSchema({ customShapes }), @@ -38,6 +44,7 @@ export function createTLStore(opts = {} as StoreOptions): TLStore { props: { instanceId, documentId: TLDOCUMENT_ID, + defaultName, }, }) } diff --git a/packages/editor/src/lib/hooks/useTLStore.ts b/packages/editor/src/lib/hooks/useTLStore.ts index 4cb515960..4e1a1ba27 100644 --- a/packages/editor/src/lib/hooks/useTLStore.ts +++ b/packages/editor/src/lib/hooks/useTLStore.ts @@ -5,11 +5,10 @@ import { usePrevious } from './usePrevious' /** @public */ export function useTLStore(opts: StoreOptions) { const [store, setStore] = useState(() => createTLStore(opts)) - const previousOpts = usePrevious(opts) + const prev = usePrevious(opts) if ( - previousOpts.customShapes !== opts.customShapes || - previousOpts.initialData !== opts.initialData || - previousOpts.instanceId !== opts.instanceId + // shallow equality check + (Object.keys(prev) as (keyof StoreOptions)[]).some((key) => prev[key] !== opts[key]) ) { const newStore = createTLStore(opts) setStore(newStore) diff --git a/packages/tlschema/api-report.md b/packages/tlschema/api-report.md index 522a943c7..e0ec40084 100644 --- a/packages/tlschema/api-report.md +++ b/packages/tlschema/api-report.md @@ -780,6 +780,8 @@ export type TLDefaultShape = TLArrowShape | TLBookmarkShape | TLDrawShape | TLEm export interface TLDocument extends BaseRecord<'document', ID> { // (undocumented) gridSize: number; + // (undocumented) + name: string; } // @public (undocumented) @@ -1250,6 +1252,7 @@ export type TLStore = Store; export type TLStoreProps = { instanceId: TLInstanceId; documentId: typeof TLDOCUMENT_ID; + defaultName: string; }; // @public (undocumented) diff --git a/packages/tlschema/src/TLStore.ts b/packages/tlschema/src/TLStore.ts index 97c8ea527..0bb3f6b77 100644 --- a/packages/tlschema/src/TLStore.ts +++ b/packages/tlschema/src/TLStore.ts @@ -40,6 +40,7 @@ export type TLStoreSnapshot = StoreSnapshot export type TLStoreProps = { instanceId: TLInstanceId documentId: typeof TLDOCUMENT_ID + defaultName: string } /** @public */ @@ -91,7 +92,7 @@ export function createIntegrityChecker(store: TLStore): () => void { const { instanceId: tabId } = store.props // make sure we have exactly one document if (!store.has(TLDOCUMENT_ID)) { - store.put([DocumentRecordType.create({ id: TLDOCUMENT_ID })]) + store.put([DocumentRecordType.create({ id: TLDOCUMENT_ID, name: store.props.defaultName })]) return ensureStoreIsUsable() } diff --git a/packages/tlschema/src/migrations.test.ts b/packages/tlschema/src/migrations.test.ts index bd540a627..4e1b802a4 100644 --- a/packages/tlschema/src/migrations.test.ts +++ b/packages/tlschema/src/migrations.test.ts @@ -3,6 +3,7 @@ import { structuredClone } from '@tldraw/utils' import fs from 'fs' import { imageAssetMigrations } from './assets/TLImageAsset' import { videoAssetMigrations } from './assets/TLVideoAsset' +import { documentTypeMigrations } from './records/TLDocument' import { instanceTypeMigrations, instanceTypeVersions } from './records/TLInstance' import { instancePageStateMigrations } from './records/TLInstancePageState' import { instancePresenceTypeMigrations } from './records/TLInstancePresence' @@ -644,6 +645,18 @@ describe('Adding instance_presence to the schema', () => { }) }) +describe('Adding name to document', () => { + const { up, down } = documentTypeMigrations.migrators[1] + + test('up works as expected', () => { + expect(up({})).toEqual({ name: '' }) + }) + + test('down works as expected', () => { + expect(down({ name: '' })).toEqual({}) + }) +}) + describe('Adding check-box to geo shape', () => { const { up, down } = geoShapeTypeMigrations.migrators[4] diff --git a/packages/tlschema/src/records/TLDocument.ts b/packages/tlschema/src/records/TLDocument.ts index 26a27d854..2e4f7d8fb 100644 --- a/packages/tlschema/src/records/TLDocument.ts +++ b/packages/tlschema/src/records/TLDocument.ts @@ -8,6 +8,7 @@ import { T } from '@tldraw/tlvalidate' */ export interface TLDocument extends BaseRecord<'document', ID> { gridSize: number + name: string } /** @public */ @@ -17,22 +18,46 @@ export const documentTypeValidator: T.Validator = T.model( typeName: T.literal('document'), id: T.literal('document:document' as ID), gridSize: T.number, + name: T.string, }) ) +// --- MIGRATIONS --- +// STEP 1: Add a new version number here, give it a meaningful name. +// It should be 1 higher than the current version +const Versions = { + AddName: 1, +} as const + +/** @public */ +export const documentTypeMigrations = defineMigrations({ + // STEP 2: Update the current version to point to your latest version + currentVersion: Versions.AddName, + // STEP 3: Add an up+down migration for the new version here + migrators: { + [Versions.AddName]: { + up: (document: TLDocument) => { + return { ...document, name: '' } + }, + down: ({ name: _, ...document }: TLDocument) => { + return document + }, + }, + }, +}) + /** @public */ export const DocumentRecordType = createRecordType('document', { + migrations: documentTypeMigrations, validator: documentTypeValidator, scope: 'document', }).withDefaultProperties( (): Omit => ({ gridSize: 10, + name: '', }) ) // all document records have the same ID: 'document:document' /** @public */ export const TLDOCUMENT_ID: ID = DocumentRecordType.createCustomId('document') - -/** @public */ -export const documentTypeMigrations = defineMigrations({}) diff --git a/packages/ui/api-report.md b/packages/ui/api-report.md index 208ce111c..a51dbebf9 100644 --- a/packages/ui/api-report.md +++ b/packages/ui/api-report.md @@ -417,6 +417,8 @@ export interface InputProps { // (undocumented) label?: TLTranslationKey; // (undocumented) + onBlur?: (value: string) => void; + // (undocumented) onCancel?: (value: string) => void; // (undocumented) onComplete?: (value: string) => void; @@ -624,6 +626,7 @@ export const TldrawUi: React_2.NamedExoticComponent<{ children?: ReactNode; hideUi?: boolean | undefined; shareZone?: ReactNode; + topZone?: ReactNode; renderDebugMenuItems?: (() => React_2.ReactNode) | undefined; } & TldrawUiContextProviderProps>; @@ -672,6 +675,7 @@ export type TldrawUiProps = { children?: ReactNode; hideUi?: boolean; shareZone?: ReactNode; + topZone?: ReactNode; renderDebugMenuItems?: () => React_2.ReactNode; } & TldrawUiContextProviderProps; @@ -720,7 +724,7 @@ export type TLTranslation = { }; // @public (undocumented) -export type TLTranslationKey = 'action.align-bottom' | 'action.align-center-horizontal.short' | 'action.align-center-horizontal' | 'action.align-center-vertical.short' | 'action.align-center-vertical' | 'action.align-left' | 'action.align-right' | 'action.align-top' | 'action.back-to-content' | 'action.bring-forward' | 'action.bring-to-front' | 'action.convert-to-bookmark' | 'action.convert-to-embed' | 'action.copy-as-json.short' | 'action.copy-as-json' | 'action.copy-as-png.short' | 'action.copy-as-png' | 'action.copy-as-svg.short' | 'action.copy-as-svg' | 'action.copy' | 'action.cut' | 'action.delete' | 'action.distribute-horizontal.short' | 'action.distribute-horizontal' | 'action.distribute-vertical.short' | 'action.distribute-vertical' | 'action.duplicate' | 'action.edit-link' | 'action.exit-pen-mode' | 'action.export-as-json.short' | 'action.export-as-json' | 'action.export-as-png.short' | 'action.export-as-png' | 'action.export-as-svg.short' | 'action.export-as-svg' | 'action.flip-horizontal.short' | 'action.flip-horizontal' | 'action.flip-vertical.short' | 'action.flip-vertical' | 'action.fork-project' | 'action.group' | 'action.insert-embed' | 'action.insert-media' | 'action.leave-shared-project' | 'action.new-project' | 'action.new-shared-project' | 'action.open-embed-link' | 'action.open-file' | 'action.pack' | 'action.paste' | 'action.print' | 'action.redo' | 'action.rotate-ccw' | 'action.rotate-cw' | 'action.save-copy' | 'action.select-all' | 'action.select-none' | 'action.send-backward' | 'action.send-to-back' | 'action.share-project' | 'action.stack-horizontal.short' | 'action.stack-horizontal' | 'action.stack-vertical.short' | 'action.stack-vertical' | 'action.stop-following' | 'action.stretch-horizontal.short' | 'action.stretch-horizontal' | 'action.stretch-vertical.short' | 'action.stretch-vertical' | 'action.toggle-auto-size' | 'action.toggle-dark-mode.menu' | 'action.toggle-dark-mode' | 'action.toggle-debug-mode.menu' | 'action.toggle-debug-mode' | 'action.toggle-focus-mode.menu' | 'action.toggle-focus-mode' | 'action.toggle-grid.menu' | 'action.toggle-grid' | 'action.toggle-lock' | 'action.toggle-reduce-motion.menu' | 'action.toggle-reduce-motion' | 'action.toggle-snap-mode.menu' | 'action.toggle-snap-mode' | 'action.toggle-tool-lock.menu' | 'action.toggle-tool-lock' | 'action.toggle-transparent.context-menu' | 'action.toggle-transparent.menu' | 'action.toggle-transparent' | 'action.undo' | 'action.ungroup' | 'action.zoom-in' | 'action.zoom-out' | 'action.zoom-to-100' | 'action.zoom-to-fit' | 'action.zoom-to-selection' | 'actions-menu.title' | 'align-style.end' | 'align-style.justify' | 'align-style.middle' | 'align-style.start' | 'arrowheadEnd-style.arrow' | 'arrowheadEnd-style.bar' | 'arrowheadEnd-style.diamond' | 'arrowheadEnd-style.dot' | 'arrowheadEnd-style.inverted' | 'arrowheadEnd-style.none' | 'arrowheadEnd-style.pipe' | 'arrowheadEnd-style.square' | 'arrowheadEnd-style.triangle' | 'arrowheadStart-style.arrow' | 'arrowheadStart-style.bar' | 'arrowheadStart-style.diamond' | 'arrowheadStart-style.dot' | 'arrowheadStart-style.inverted' | 'arrowheadStart-style.none' | 'arrowheadStart-style.pipe' | 'arrowheadStart-style.square' | 'arrowheadStart-style.triangle' | 'color-style.black' | 'color-style.blue' | 'color-style.green' | 'color-style.grey' | 'color-style.light-blue' | 'color-style.light-green' | 'color-style.light-red' | 'color-style.light-violet' | 'color-style.orange' | 'color-style.red' | 'color-style.violet' | 'color-style.yellow' | 'context-menu.arrange' | 'context-menu.copy-as' | 'context-menu.export-as' | 'context-menu.move-to-page' | 'context-menu.reorder' | 'context.pages.new-page' | 'dash-style.dashed' | 'dash-style.dotted' | 'dash-style.draw' | 'dash-style.solid' | 'debug-panel.more' | 'edit-link-dialog.cancel' | 'edit-link-dialog.clear' | 'edit-link-dialog.detail' | 'edit-link-dialog.invalid-url' | 'edit-link-dialog.save' | 'edit-link-dialog.title' | 'edit-link-dialog.url' | 'edit-pages-dialog.move-down' | 'edit-pages-dialog.move-up' | 'embed-dialog.back' | 'embed-dialog.cancel' | 'embed-dialog.create' | 'embed-dialog.instruction' | 'embed-dialog.invalid-url' | 'embed-dialog.title' | 'embed-dialog.url' | 'file-system.confirm-clear.cancel' | 'file-system.confirm-clear.continue' | 'file-system.confirm-clear.description' | 'file-system.confirm-clear.dont-show-again' | 'file-system.confirm-clear.title' | 'file-system.confirm-open.cancel' | 'file-system.confirm-open.description' | 'file-system.confirm-open.dont-show-again' | 'file-system.confirm-open.open' | 'file-system.confirm-open.title' | 'file-system.file-open-error.file-format-version-too-new' | 'file-system.file-open-error.generic-corrupted-file' | 'file-system.file-open-error.not-a-tldraw-file' | 'file-system.file-open-error.title' | 'file-system.shared-document-file-open-error.description' | 'file-system.shared-document-file-open-error.title' | 'fill-style.none' | 'fill-style.pattern' | 'fill-style.semi' | 'fill-style.solid' | 'focus-mode.toggle-focus-mode' | 'font-style.draw' | 'font-style.mono' | 'font-style.sans' | 'font-style.serif' | 'geo-style.arrow-down' | 'geo-style.arrow-left' | 'geo-style.arrow-right' | 'geo-style.arrow-up' | 'geo-style.check-box' | 'geo-style.diamond' | 'geo-style.ellipse' | 'geo-style.hexagon' | 'geo-style.octagon' | 'geo-style.oval' | 'geo-style.pentagon' | 'geo-style.rectangle' | 'geo-style.rhombus-2' | 'geo-style.rhombus' | 'geo-style.star' | 'geo-style.trapezoid' | 'geo-style.triangle' | 'geo-style.x-box' | 'help-menu.about' | 'help-menu.discord' | 'help-menu.github' | 'help-menu.keyboard-shortcuts' | 'help-menu.title' | 'help-menu.twitter' | 'menu.copy-as' | 'menu.edit' | 'menu.export-as' | 'menu.file' | 'menu.language' | 'menu.preferences' | 'menu.title' | 'menu.view' | 'navigation-zone.toggle-minimap' | 'navigation-zone.zoom' | 'opacity-style.0.1' | 'opacity-style.0.25' | 'opacity-style.0.5' | 'opacity-style.0.75' | 'opacity-style.1' | 'page-menu.create-new-page' | 'page-menu.edit-done' | 'page-menu.edit-start' | 'page-menu.go-to-page' | 'page-menu.max-page-count-reached' | 'page-menu.new-page-initial-name' | 'page-menu.submenu.delete' | 'page-menu.submenu.duplicate-page' | 'page-menu.submenu.move-down' | 'page-menu.submenu.move-up' | 'page-menu.submenu.rename' | 'page-menu.submenu.title' | 'page-menu.title' | 'people-menu.change-color' | 'people-menu.change-name' | 'people-menu.follow' | 'people-menu.following' | 'people-menu.invite' | 'people-menu.leading' | 'people-menu.title' | 'people-menu.user' | 'share-menu.copy-link-note' | 'share-menu.copy-link' | 'share-menu.copy-readonly-link-note' | 'share-menu.copy-readonly-link' | 'share-menu.create-snapshot-link' | 'share-menu.fork-note' | 'share-menu.offline-note' | 'share-menu.project-too-large' | 'share-menu.readonly-link' | 'share-menu.save-note' | 'share-menu.share-project' | 'share-menu.snapshot-link-note' | 'share-menu.title' | 'share-menu.upload-failed' | 'sharing.confirm-leave.cancel' | 'sharing.confirm-leave.description' | 'sharing.confirm-leave.dont-show-again' | 'sharing.confirm-leave.leave' | 'sharing.confirm-leave.title' | 'shortcuts-dialog.edit' | 'shortcuts-dialog.file' | 'shortcuts-dialog.preferences' | 'shortcuts-dialog.title' | 'shortcuts-dialog.tools' | 'shortcuts-dialog.transform' | 'shortcuts-dialog.view' | 'size-style.l' | 'size-style.m' | 'size-style.s' | 'size-style.xl' | 'spline-style.cubic' | 'spline-style.line' | 'style-panel.align' | 'style-panel.arrowhead-end' | 'style-panel.arrowhead-start' | 'style-panel.arrowheads' | 'style-panel.color' | 'style-panel.dash' | 'style-panel.fill' | 'style-panel.font' | 'style-panel.geo' | 'style-panel.mixed' | 'style-panel.opacity' | 'style-panel.position' | 'style-panel.size' | 'style-panel.spline' | 'style-panel.title' | 'style-panel.vertical-align' | 'toast.close' | 'toast.error.copy-fail.desc' | 'toast.error.copy-fail.title' | 'toast.error.export-fail.desc' | 'toast.error.export-fail.title' | 'tool-panel.drawing' | 'tool-panel.more' | 'tool-panel.shapes' | 'tool.arrow-down' | 'tool.arrow-left' | 'tool.arrow-right' | 'tool.arrow-up' | 'tool.arrow' | 'tool.asset' | 'tool.check-box' | 'tool.diamond' | 'tool.draw' | 'tool.ellipse' | 'tool.embed' | 'tool.eraser' | 'tool.frame' | 'tool.hand' | 'tool.hexagon' | 'tool.highlight' | 'tool.laser' | 'tool.line' | 'tool.note' | 'tool.octagon' | 'tool.oval' | 'tool.pentagon' | 'tool.rectangle' | 'tool.rhombus' | 'tool.select' | 'tool.star' | 'tool.text' | 'tool.trapezoid' | 'tool.triangle' | 'tool.x-box' | 'vscode.file-open.backup-failed' | 'vscode.file-open.backup-saved' | 'vscode.file-open.backup' | 'vscode.file-open.desc' | 'vscode.file-open.dont-show-again' | 'vscode.file-open.open'; +export type TLTranslationKey = 'action.align-bottom' | 'action.align-center-horizontal.short' | 'action.align-center-horizontal' | 'action.align-center-vertical.short' | 'action.align-center-vertical' | 'action.align-left' | 'action.align-right' | 'action.align-top' | 'action.back-to-content' | 'action.bring-forward' | 'action.bring-to-front' | 'action.convert-to-bookmark' | 'action.convert-to-embed' | 'action.copy-as-json.short' | 'action.copy-as-json' | 'action.copy-as-png.short' | 'action.copy-as-png' | 'action.copy-as-svg.short' | 'action.copy-as-svg' | 'action.copy' | 'action.cut' | 'action.delete' | 'action.distribute-horizontal.short' | 'action.distribute-horizontal' | 'action.distribute-vertical.short' | 'action.distribute-vertical' | 'action.duplicate' | 'action.edit-link' | 'action.exit-pen-mode' | 'action.export-as-json.short' | 'action.export-as-json' | 'action.export-as-png.short' | 'action.export-as-png' | 'action.export-as-svg.short' | 'action.export-as-svg' | 'action.flip-horizontal.short' | 'action.flip-horizontal' | 'action.flip-vertical.short' | 'action.flip-vertical' | 'action.fork-project' | 'action.group' | 'action.insert-embed' | 'action.insert-media' | 'action.leave-shared-project' | 'action.new-project' | 'action.new-shared-project' | 'action.open-embed-link' | 'action.open-file' | 'action.pack' | 'action.paste' | 'action.print' | 'action.redo' | 'action.rotate-ccw' | 'action.rotate-cw' | 'action.save-copy' | 'action.select-all' | 'action.select-none' | 'action.send-backward' | 'action.send-to-back' | 'action.share-project' | 'action.stack-horizontal.short' | 'action.stack-horizontal' | 'action.stack-vertical.short' | 'action.stack-vertical' | 'action.stop-following' | 'action.stretch-horizontal.short' | 'action.stretch-horizontal' | 'action.stretch-vertical.short' | 'action.stretch-vertical' | 'action.toggle-auto-size' | 'action.toggle-dark-mode.menu' | 'action.toggle-dark-mode' | 'action.toggle-debug-mode.menu' | 'action.toggle-debug-mode' | 'action.toggle-focus-mode.menu' | 'action.toggle-focus-mode' | 'action.toggle-grid.menu' | 'action.toggle-grid' | 'action.toggle-lock' | 'action.toggle-reduce-motion.menu' | 'action.toggle-reduce-motion' | 'action.toggle-snap-mode.menu' | 'action.toggle-snap-mode' | 'action.toggle-tool-lock.menu' | 'action.toggle-tool-lock' | 'action.toggle-transparent.context-menu' | 'action.toggle-transparent.menu' | 'action.toggle-transparent' | 'action.undo' | 'action.ungroup' | 'action.zoom-in' | 'action.zoom-out' | 'action.zoom-to-100' | 'action.zoom-to-fit' | 'action.zoom-to-selection' | 'actions-menu.title' | 'align-style.end' | 'align-style.justify' | 'align-style.middle' | 'align-style.start' | 'arrowheadEnd-style.arrow' | 'arrowheadEnd-style.bar' | 'arrowheadEnd-style.diamond' | 'arrowheadEnd-style.dot' | 'arrowheadEnd-style.inverted' | 'arrowheadEnd-style.none' | 'arrowheadEnd-style.pipe' | 'arrowheadEnd-style.square' | 'arrowheadEnd-style.triangle' | 'arrowheadStart-style.arrow' | 'arrowheadStart-style.bar' | 'arrowheadStart-style.diamond' | 'arrowheadStart-style.dot' | 'arrowheadStart-style.inverted' | 'arrowheadStart-style.none' | 'arrowheadStart-style.pipe' | 'arrowheadStart-style.square' | 'arrowheadStart-style.triangle' | 'color-style.black' | 'color-style.blue' | 'color-style.green' | 'color-style.grey' | 'color-style.light-blue' | 'color-style.light-green' | 'color-style.light-red' | 'color-style.light-violet' | 'color-style.orange' | 'color-style.red' | 'color-style.violet' | 'color-style.yellow' | 'context-menu.arrange' | 'context-menu.copy-as' | 'context-menu.export-as' | 'context-menu.move-to-page' | 'context-menu.reorder' | 'context.pages.new-page' | 'dash-style.dashed' | 'dash-style.dotted' | 'dash-style.draw' | 'dash-style.solid' | 'debug-panel.more' | 'edit-link-dialog.cancel' | 'edit-link-dialog.clear' | 'edit-link-dialog.detail' | 'edit-link-dialog.invalid-url' | 'edit-link-dialog.save' | 'edit-link-dialog.title' | 'edit-link-dialog.url' | 'edit-pages-dialog.move-down' | 'edit-pages-dialog.move-up' | 'embed-dialog.back' | 'embed-dialog.cancel' | 'embed-dialog.create' | 'embed-dialog.instruction' | 'embed-dialog.invalid-url' | 'embed-dialog.title' | 'embed-dialog.url' | 'file-system.confirm-clear.cancel' | 'file-system.confirm-clear.continue' | 'file-system.confirm-clear.description' | 'file-system.confirm-clear.dont-show-again' | 'file-system.confirm-clear.title' | 'file-system.confirm-open.cancel' | 'file-system.confirm-open.description' | 'file-system.confirm-open.dont-show-again' | 'file-system.confirm-open.open' | 'file-system.confirm-open.title' | 'file-system.file-open-error.file-format-version-too-new' | 'file-system.file-open-error.generic-corrupted-file' | 'file-system.file-open-error.not-a-tldraw-file' | 'file-system.file-open-error.title' | 'file-system.shared-document-file-open-error.description' | 'file-system.shared-document-file-open-error.title' | 'fill-style.none' | 'fill-style.pattern' | 'fill-style.semi' | 'fill-style.solid' | 'focus-mode.toggle-focus-mode' | 'font-style.draw' | 'font-style.mono' | 'font-style.sans' | 'font-style.serif' | 'geo-style.arrow-down' | 'geo-style.arrow-left' | 'geo-style.arrow-right' | 'geo-style.arrow-up' | 'geo-style.check-box' | 'geo-style.diamond' | 'geo-style.ellipse' | 'geo-style.hexagon' | 'geo-style.octagon' | 'geo-style.oval' | 'geo-style.pentagon' | 'geo-style.rectangle' | 'geo-style.rhombus-2' | 'geo-style.rhombus' | 'geo-style.star' | 'geo-style.trapezoid' | 'geo-style.triangle' | 'geo-style.x-box' | 'help-menu.about' | 'help-menu.discord' | 'help-menu.github' | 'help-menu.keyboard-shortcuts' | 'help-menu.title' | 'help-menu.twitter' | 'home-project-dialog.description' | 'home-project-dialog.ok' | 'home-project-dialog.title' | 'menu.copy-as' | 'menu.edit' | 'menu.export-as' | 'menu.file' | 'menu.language' | 'menu.preferences' | 'menu.title' | 'menu.view' | 'navigation-zone.toggle-minimap' | 'navigation-zone.zoom' | 'opacity-style.0.1' | 'opacity-style.0.25' | 'opacity-style.0.5' | 'opacity-style.0.75' | 'opacity-style.1' | 'page-menu.create-new-page' | 'page-menu.edit-done' | 'page-menu.edit-start' | 'page-menu.go-to-page' | 'page-menu.max-page-count-reached' | 'page-menu.new-page-initial-name' | 'page-menu.submenu.delete' | 'page-menu.submenu.duplicate-page' | 'page-menu.submenu.move-down' | 'page-menu.submenu.move-up' | 'page-menu.submenu.rename' | 'page-menu.submenu.title' | 'page-menu.title' | 'people-menu.change-color' | 'people-menu.change-name' | 'people-menu.follow' | 'people-menu.following' | 'people-menu.invite' | 'people-menu.leading' | 'people-menu.title' | 'people-menu.user' | 'rename-project-dialog.cancel' | 'rename-project-dialog.rename' | 'rename-project-dialog.title' | 'share-menu.copy-link-note' | 'share-menu.copy-link' | 'share-menu.copy-readonly-link-note' | 'share-menu.copy-readonly-link' | 'share-menu.create-snapshot-link' | 'share-menu.default-project-name' | 'share-menu.fork-note' | 'share-menu.offline-note' | 'share-menu.project-too-large' | 'share-menu.readonly-link' | 'share-menu.save-note' | 'share-menu.share-project' | 'share-menu.snapshot-link-note' | 'share-menu.title' | 'share-menu.upload-failed' | 'sharing.confirm-leave.cancel' | 'sharing.confirm-leave.description' | 'sharing.confirm-leave.dont-show-again' | 'sharing.confirm-leave.leave' | 'sharing.confirm-leave.title' | 'shortcuts-dialog.edit' | 'shortcuts-dialog.file' | 'shortcuts-dialog.preferences' | 'shortcuts-dialog.title' | 'shortcuts-dialog.tools' | 'shortcuts-dialog.transform' | 'shortcuts-dialog.view' | 'size-style.l' | 'size-style.m' | 'size-style.s' | 'size-style.xl' | 'spline-style.cubic' | 'spline-style.line' | 'style-panel.align' | 'style-panel.arrowhead-end' | 'style-panel.arrowhead-start' | 'style-panel.arrowheads' | 'style-panel.color' | 'style-panel.dash' | 'style-panel.fill' | 'style-panel.font' | 'style-panel.geo' | 'style-panel.mixed' | 'style-panel.opacity' | 'style-panel.position' | 'style-panel.size' | 'style-panel.spline' | 'style-panel.title' | 'style-panel.vertical-align' | 'toast.close' | 'toast.error.copy-fail.desc' | 'toast.error.copy-fail.title' | 'toast.error.export-fail.desc' | 'toast.error.export-fail.title' | 'tool-panel.drawing' | 'tool-panel.more' | 'tool-panel.shapes' | 'tool.arrow-down' | 'tool.arrow-left' | 'tool.arrow-right' | 'tool.arrow-up' | 'tool.arrow' | 'tool.asset' | 'tool.check-box' | 'tool.diamond' | 'tool.draw' | 'tool.ellipse' | 'tool.embed' | 'tool.eraser' | 'tool.frame' | 'tool.hand' | 'tool.hexagon' | 'tool.highlight' | 'tool.laser' | 'tool.line' | 'tool.note' | 'tool.octagon' | 'tool.oval' | 'tool.pentagon' | 'tool.rectangle' | 'tool.rhombus' | 'tool.select' | 'tool.star' | 'tool.text' | 'tool.trapezoid' | 'tool.triangle' | 'tool.x-box' | 'vscode.file-open.backup-failed' | 'vscode.file-open.backup-saved' | 'vscode.file-open.backup' | 'vscode.file-open.desc' | 'vscode.file-open.dont-show-again' | 'vscode.file-open.open'; // @public (undocumented) export type TLTranslationLocale = TLTranslations[number]['locale']; diff --git a/packages/ui/src/lib/TldrawUi.tsx b/packages/ui/src/lib/TldrawUi.tsx index ae4b0500d..2d47d322b 100644 --- a/packages/ui/src/lib/TldrawUi.tsx +++ b/packages/ui/src/lib/TldrawUi.tsx @@ -31,6 +31,7 @@ export type TldrawUiProps = { hideUi?: boolean /** A component to use for the share zone (will be deprecated) */ shareZone?: ReactNode + topZone?: ReactNode /** Additional items to add to the debug menu (will be deprecated)*/ renderDebugMenuItems?: () => React.ReactNode } & TldrawUiContextProviderProps @@ -40,6 +41,7 @@ export type TldrawUiProps = { */ export const TldrawUi = React.memo(function TldrawUi({ shareZone, + topZone, renderDebugMenuItems, children, hideUi, @@ -50,6 +52,7 @@ export const TldrawUi = React.memo(function TldrawUi({ {children} @@ -61,6 +64,7 @@ export const TldrawUi = React.memo(function TldrawUi({ type TldrawUiContentProps = { hideUi?: boolean shareZone?: ReactNode + topZone?: ReactNode renderDebugMenuItems?: () => React.ReactNode } @@ -84,6 +88,7 @@ const TldrawUiInner = React.memo(function TldrawUiInner({ /** @public */ export const TldrawUiContent = React.memo(function TldrawUI({ shareZone, + topZone, renderDebugMenuItems, }: TldrawUiContentProps) { const app = useApp() @@ -127,12 +132,9 @@ export const TldrawUiContent = React.memo(function TldrawUI({ +
{topZone}
- {shareZone && ( -
- {shareZone} -
- )} + {shareZone} {breakpoint >= 5 && !isReadonlyMode && (
diff --git a/packages/ui/src/lib/components/MenuZone.tsx b/packages/ui/src/lib/components/MenuZone.tsx index 8b6e2713d..600a9c621 100644 --- a/packages/ui/src/lib/components/MenuZone.tsx +++ b/packages/ui/src/lib/components/MenuZone.tsx @@ -24,7 +24,7 @@ export const MenuZone = track(function MenuZone() {
- {breakpoint >= 5 && showQuickActions && ( + {breakpoint >= 6 && showQuickActions && ( <>
diff --git a/packages/ui/src/lib/components/Toolbar/Toolbar.tsx b/packages/ui/src/lib/components/Toolbar/Toolbar.tsx index b63681558..c8b816edd 100644 --- a/packages/ui/src/lib/components/Toolbar/Toolbar.tsx +++ b/packages/ui/src/lib/components/Toolbar/Toolbar.tsx @@ -115,7 +115,7 @@ export const Toolbar = function Toolbar() { 'tlui-toolbar__extras__hidden': !showExtraActions, })} > - {breakpoint < 5 && ( + {breakpoint < 6 && (
diff --git a/packages/ui/src/lib/components/primitives/Input.tsx b/packages/ui/src/lib/components/primitives/Input.tsx index 360a64a3f..9f53aa63c 100644 --- a/packages/ui/src/lib/components/primitives/Input.tsx +++ b/packages/ui/src/lib/components/primitives/Input.tsx @@ -19,6 +19,7 @@ export interface InputProps { onComplete?: (value: string) => void onValueChange?: (value: string) => void onCancel?: (value: string) => void + onBlur?: (value: string) => void className?: string /** * Usually on iOS when you focus an input, the browser will adjust the viewport to bring the input @@ -46,6 +47,7 @@ export const Input = React.forwardRef(function Inp onComplete, onValueChange, onCancel, + onBlur, shouldManuallyMaintainScrollPositionWhenFocused = false, children, value, @@ -106,7 +108,14 @@ export const Input = React.forwardRef(function Inp [onComplete, onCancel] ) - const handleBlur = React.useCallback(() => setIsFocused(false), []) + const handleBlur = React.useCallback( + (e: React.FocusEvent) => { + setIsFocused(false) + const value = e.currentTarget.value + onBlur?.(value) + }, + [onBlur] + ) React.useEffect(() => { const visualViewport = window.visualViewport diff --git a/packages/ui/src/lib/hooks/useTranslation/TLTranslationKey.ts b/packages/ui/src/lib/hooks/useTranslation/TLTranslationKey.ts index 6f956baac..e923dc4f2 100644 --- a/packages/ui/src/lib/hooks/useTranslation/TLTranslationKey.ts +++ b/packages/ui/src/lib/hooks/useTranslation/TLTranslationKey.ts @@ -233,6 +233,7 @@ export type TLTranslationKey = | 'share-menu.save-note' | 'share-menu.fork-note' | 'share-menu.share-project' + | 'share-menu.default-project-name' | 'share-menu.copy-link' | 'share-menu.readonly-link' | 'share-menu.create-snapshot-link' @@ -281,6 +282,12 @@ export type TLTranslationKey = | 'shortcuts-dialog.tools' | 'shortcuts-dialog.transform' | 'shortcuts-dialog.view' + | 'home-project-dialog.title' + | 'home-project-dialog.description' + | 'rename-project-dialog.title' + | 'rename-project-dialog.cancel' + | 'rename-project-dialog.rename' + | 'home-project-dialog.ok' | 'style-panel.title' | 'style-panel.align' | 'style-panel.vertical-align' diff --git a/packages/ui/src/lib/hooks/useTranslation/defaultTranslation.ts b/packages/ui/src/lib/hooks/useTranslation/defaultTranslation.ts index aaf8d99d3..aca2e8fc9 100644 --- a/packages/ui/src/lib/hooks/useTranslation/defaultTranslation.ts +++ b/packages/ui/src/lib/hooks/useTranslation/defaultTranslation.ts @@ -233,6 +233,7 @@ export const DEFAULT_TRANSLATION = { 'share-menu.save-note': 'Download this project to your computer as a .tldr file.', 'share-menu.fork-note': 'Create a new shared project based on this snapshot.', 'share-menu.share-project': 'Share this project', + 'share-menu.default-project-name': 'Shared Project', 'share-menu.copy-link': 'Copy share link', 'share-menu.readonly-link': 'Read-only', 'share-menu.create-snapshot-link': 'Copy snapshot link', @@ -284,6 +285,12 @@ export const DEFAULT_TRANSLATION = { 'shortcuts-dialog.tools': 'Tools', 'shortcuts-dialog.transform': 'Transform', 'shortcuts-dialog.view': 'View', + 'home-project-dialog.title': 'Home project', + 'home-project-dialog.description': "This is your local home project. It's just for you!", + 'rename-project-dialog.title': 'Rename project', + 'rename-project-dialog.cancel': 'Cancel', + 'rename-project-dialog.rename': 'Rename', + 'home-project-dialog.ok': 'Ok', 'style-panel.title': 'Styles', 'style-panel.align': 'Align', 'style-panel.vertical-align': 'Vertical align', diff --git a/packages/ui/ui.css b/packages/ui/ui.css index c8887200c..b34b4439b 100644 --- a/packages/ui/ui.css +++ b/packages/ui/ui.css @@ -28,6 +28,7 @@ grid-column: 1; grid-row: 1; display: flex; + min-width: 0px; } .tlui-layout__top__left { @@ -37,6 +38,19 @@ justify-content: flex-start; width: 100%; height: 100%; + flex-shrink: 1; +} + +.tlui-layout__top__center { + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + width: 100%; + height: 100%; + margin-left: var(--space-2); + flex-grow: 1; + min-width: 0px; } .tlui-layout__top__right { @@ -46,6 +60,8 @@ justify-content: flex-start; width: 100%; height: 100%; + flex-shrink: 1; + min-width: 0px; } .scrollable, @@ -1627,6 +1643,47 @@ } } +/* ------------------ Project Menu ------------------ */ +.tlui-project-menu__wrapper { + display: flex; + width: 100%; + align-items: center; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + min-width: 0px; +} + +.tlui-project-menu__button { + display: flex; + gap: var(--space-4); + pointer-events: all; +} + +.tlui-project-menu__button__name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0px; +} + +.tlui-project-menu__input { + min-width: 0px; + text-align: center; + pointer-events: all; + + /* Position slightly to the right so that it doesn't jump around */ + /* 40px is the width of the icon */ + margin-left: 40px; +} + +.tlui-rename-project-dialog__input { + background-color: var(--color-muted-2); + flex-grow: 2; + border-radius: var(--radius-2); + padding: 0px var(--space-4); +} + /* ------------------- Navigation ------------------- */ .tlui-navigation-zone {