From 3bc72cb822d3a081df3e0d7c1df5bd83750c9d26 Mon Sep 17 00:00:00 2001 From: Lu Wilson Date: Thu, 1 Jun 2023 19:46:26 +0100 Subject: [PATCH] Add support for project names (#1340) This PR adds some things that we need for the Project Name feature on tldraw.com. It should be reviewed alongside https://github.com/tldraw/tldraw-lite/pull/1814 ## Name Property This PR adds a `name` property to `TLDocument`. We use this to store a project's name. Screenshot 2023-05-09 at 15 47 26 ## Top Zone This PR adds a `topZone` area of the UI that we can add stuff to, similar to how `shareZone` works. It also adds an example to show where the `topZone` and `shareZone` are: Screenshot 2023-05-12 at 10 57 40 ## Breakpoints This PR change's the UI's breakpoints a little bit. It moves the action bar to the bottom a little bit earlier. (This gives us more space at the top for the project name). ![2023-05-12 at 11 08 26 - Fuchsia Bison](https://github.com/tldraw/tldraw/assets/15892272/34563cea-b1d1-47be-ac5e-5650ee0ba02d) ![2023-05-12 at 13 45 04 - Tan Mole](https://github.com/tldraw/tldraw/assets/15892272/ab190bd3-51d4-4a8b-88de-c72ab14bcba6) ## Input Blur This PR adds an `onBlur` parameter to `Input`. This was needed because 'clicking off' the input wasn't firing `onComplete` or `onCancel`. Screenshot 2023-05-09 at 16 12 58 ## Create Project Name This PR adds an internal `createProjectName` property to `TldrawEditorConfig`. Similar to `derivePresenceState`, you can pass a custom function to it. It lets you control what gets used as the default project name. We use it to set different names in our local projects compared to shared projects. In the future, when we add more advanced project features, we could handle this better within the UI. Screenshot 2023-05-09 at 15 47 26 ### Test Plan 1. Gradually reduce the width of the browser window. 2. Check that the actions menu jumps to the bottom before the style panel moves to the bottom. --- 1. In the examples app, open the `/zones` example. 2. Check that there's a 'top zone' at the top. - [ ] Unit Tests - [ ] Webdriver tests ### Release Note - [dev] Added a `topZone` area where you can put stuff. - [dev] Added a `name` property to `TLDocument` - and `app` methods for it. - [dev] Added an internal `createProjectName` config property for controlling the default project name. - [dev] Added an `onBlur` parameter to `Input`. - Moved the actions bar to the bottom on medium-sized screens. --------- Co-authored-by: Steve Ruiz --- .../src/15-custom-zones/ZonesExample.tsx | 40 +++++++++++++ apps/examples/src/index.tsx | 5 ++ assets/translations/main.json | 7 +++ packages/editor/api-report.md | 7 +++ packages/editor/src/lib/TldrawEditor.tsx | 7 ++- packages/editor/src/lib/app/App.ts | 16 ++++++ .../editor/src/lib/config/createTLStore.ts | 9 ++- packages/editor/src/lib/hooks/useTLStore.ts | 7 +-- packages/tlschema/api-report.md | 3 + packages/tlschema/src/TLStore.ts | 3 +- packages/tlschema/src/migrations.test.ts | 13 +++++ packages/tlschema/src/records/TLDocument.ts | 31 +++++++++- packages/ui/api-report.md | 6 +- packages/ui/src/lib/TldrawUi.tsx | 12 ++-- packages/ui/src/lib/components/MenuZone.tsx | 2 +- .../ui/src/lib/components/Toolbar/Toolbar.tsx | 2 +- .../src/lib/components/primitives/Input.tsx | 11 +++- .../hooks/useTranslation/TLTranslationKey.ts | 7 +++ .../useTranslation/defaultTranslation.ts | 7 +++ packages/ui/ui.css | 57 +++++++++++++++++++ 20 files changed, 233 insertions(+), 19 deletions(-) create mode 100644 apps/examples/src/15-custom-zones/ZonesExample.tsx 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 {