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.

<img width="454" alt="Screenshot 2023-05-09 at 15 47 26"
src="https://github.com/tldraw/tldraw/assets/15892272/f3be438e-aa0f-4dec-8f51-8dfd9f9d0ced">

## 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:

<img width="1511" alt="Screenshot 2023-05-12 at 10 57 40"
src="https://github.com/tldraw/tldraw/assets/15892272/f5e1cd33-017e-4aaf-bfee-4d85119e2974">

## 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`.

<img width="620" alt="Screenshot 2023-05-09 at 16 12 58"
src="https://github.com/tldraw/tldraw/assets/15892272/3b28da74-0a74-4063-8053-e59e47027caf">

## 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.

<img width="454" alt="Screenshot 2023-05-09 at 15 47 26"
src="https://github.com/tldraw/tldraw/assets/15892272/da9a4699-ac32-40d9-a97c-6c682acfac41">

### 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 <steveruizok@gmail.com>
This commit is contained in:
Lu Wilson 2023-06-01 19:46:26 +01:00 committed by GitHub
parent 941647fd1b
commit 3bc72cb822
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 233 additions and 19 deletions

View file

@ -0,0 +1,40 @@
import { Tldraw } from '@tldraw/tldraw'
import '@tldraw/tldraw/editor.css'
import '@tldraw/tldraw/ui.css'
export default function Example() {
return (
<div className="tldraw__editor">
<Tldraw shareZone={<CustomShareZone />} topZone={<CustomTopZone />} />
</div>
)
}
function CustomShareZone() {
return (
<div
style={{
backgroundColor: 'var(--palette-light-blue)',
width: '100%',
textAlign: 'center',
minWidth: '80px',
}}
>
<p>Share Zone</p>
</div>
)
}
function CustomTopZone() {
return (
<div
style={{
width: '100%',
backgroundColor: 'var(--palette-light-green)',
textAlign: 'center',
}}
>
<p>Top Zone</p>
</div>
)
}

View file

@ -14,6 +14,7 @@ import UserPresenceExample from './11-user-presence/UserPresenceExample'
import UiEventsExample from './12-ui-events/UiEventsExample' import UiEventsExample from './12-ui-events/UiEventsExample'
import StoreEventsExample from './13-store-events/StoreEventsExample' import StoreEventsExample from './13-store-events/StoreEventsExample'
import PersistenceExample from './14-persistence/PersistenceExample' import PersistenceExample from './14-persistence/PersistenceExample'
import ZonesExample from './15-custom-zones/ZonesExample'
import ExampleApi from './2-api/APIExample' import ExampleApi from './2-api/APIExample'
import CustomConfigExample from './3-custom-config/CustomConfigExample' import CustomConfigExample from './3-custom-config/CustomConfigExample'
import CustomUiExample from './4-custom-ui/CustomUiExample' import CustomUiExample from './4-custom-ui/CustomUiExample'
@ -90,6 +91,10 @@ export const allExamples: Example[] = [
path: '/user-presence', path: '/user-presence',
element: <UserPresenceExample />, element: <UserPresenceExample />,
}, },
{
path: '/zones',
element: <ZonesExample />,
},
{ {
path: '/persistence', path: '/persistence',
element: <PersistenceExample />, element: <PersistenceExample />,

View file

@ -229,6 +229,7 @@
"share-menu.save-note": "Download this project to your computer as a .tldr file.", "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.fork-note": "Create a new shared project based on this snapshot.",
"share-menu.share-project": "Share this project", "share-menu.share-project": "Share this project",
"share-menu.default-project-name": "Shared Project",
"share-menu.copy-link": "Copy share link", "share-menu.copy-link": "Copy share link",
"share-menu.readonly-link": "Read-only", "share-menu.readonly-link": "Read-only",
"share-menu.create-snapshot-link": "Copy snapshot link", "share-menu.create-snapshot-link": "Copy snapshot link",
@ -277,6 +278,12 @@
"shortcuts-dialog.tools": "Tools", "shortcuts-dialog.tools": "Tools",
"shortcuts-dialog.transform": "Transform", "shortcuts-dialog.transform": "Transform",
"shortcuts-dialog.view": "View", "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.title": "Styles",
"style-panel.align": "Align", "style-panel.align": "Align",
"style-panel.vertical-align": "Vertical align", "style-panel.vertical-align": "Vertical align",

View file

@ -390,6 +390,8 @@ export class App extends EventEmitter<TLEventMap> {
panZoomIntoView(ids: TLShapeId[], opts?: AnimationOptions): this; panZoomIntoView(ids: TLShapeId[], opts?: AnimationOptions): this;
// (undocumented) // (undocumented)
popFocusLayer(): this; popFocusLayer(): this;
// @internal (undocumented)
get projectName(): string;
// @internal // @internal
get props(): null | TLNullableShapeProps; get props(): null | TLNullableShapeProps;
// (undocumented) // (undocumented)
@ -471,6 +473,8 @@ export class App extends EventEmitter<TLEventMap> {
setLocale(locale: string): void; setLocale(locale: string): void;
// (undocumented) // (undocumented)
setPenMode(isPenMode: boolean): this; setPenMode(isPenMode: boolean): this;
// @internal (undocumented)
setProjectName(name: string): void;
setProp(key: TLShapeProp, value: any, ephemeral?: boolean, squashing?: boolean): this; setProp(key: TLShapeProp, value: any, ephemeral?: boolean, squashing?: boolean): this;
// @internal (undocumented) // @internal (undocumented)
setReadOnly(isReadOnly: boolean): this; setReadOnly(isReadOnly: boolean): this;
@ -512,6 +516,8 @@ export class App extends EventEmitter<TLEventMap> {
updateAssets(assets: TLAssetPartial[]): this; updateAssets(assets: TLAssetPartial[]): this;
// @internal // @internal
updateCullingBounds(): this; updateCullingBounds(): this;
// @internal (undocumented)
updateDocumentSettings(settings: Partial<TLDocument>): void;
updateInstanceState(partial: Partial<Omit<TLInstance, 'currentPageId' | 'documentId' | 'userId'>>, ephemeral?: boolean, squashing?: boolean): this; updateInstanceState(partial: Partial<Omit<TLInstance, 'currentPageId' | 'documentId' | 'userId'>>, ephemeral?: boolean, squashing?: boolean): this;
updatePage(partial: RequiredKeys<TLPage, 'id'>, squashing?: boolean): this; updatePage(partial: RequiredKeys<TLPage, 'id'>, squashing?: boolean): this;
updateShapes(partials: (null | TLShapePartial | undefined)[], squashing?: boolean): this; updateShapes(partials: (null | TLShapePartial | undefined)[], squashing?: boolean): this;
@ -1829,6 +1835,7 @@ export type TldrawEditorProps = {
initialData?: StoreSnapshot<TLRecord>; initialData?: StoreSnapshot<TLRecord>;
instanceId?: TLInstanceId; instanceId?: TLInstanceId;
persistenceKey?: string; persistenceKey?: string;
defaultName?: string;
}); });
// @public (undocumented) // @public (undocumented)

View file

@ -119,6 +119,10 @@ export type TldrawEditorProps = {
* The id under which to sync and persist the editor's data. * The id under which to sync and persist the editor's data.
*/ */
persistenceKey?: string 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 }) { 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({ const syncedStore = useLocalStore({
customShapes: shapes, customShapes: shapes,
instanceId, instanceId,
initialData, initialData,
persistenceKey, persistenceKey,
defaultName,
}) })
return <TldrawEditorWithLoadingStore {...props} store={syncedStore} /> return <TldrawEditorWithLoadingStore {...props} store={syncedStore} />

View file

@ -35,6 +35,7 @@ import {
TLCursor, TLCursor,
TLCursorType, TLCursorType,
TLDOCUMENT_ID, TLDOCUMENT_ID,
TLDocument,
TLFrameShape, TLFrameShape,
TLGroupShape, TLGroupShape,
TLImageAsset, TLImageAsset,
@ -1515,10 +1516,25 @@ export class App extends EventEmitter<TLEventMap> {
return this.store.get(TLDOCUMENT_ID)! return this.store.get(TLDOCUMENT_ID)!
} }
/** @internal */
updateDocumentSettings(settings: Partial<TLDocument>) {
this.store.put([{ ...this.documentSettings, ...settings }])
}
get gridSize() { get gridSize() {
return this.documentSettings.gridSize return this.documentSettings.gridSize
} }
/** @internal */
get projectName() {
return this.documentSettings.name
}
/** @internal */
setProjectName(name: string) {
this.updateDocumentSettings({ name })
}
get isSnapMode() { get isSnapMode() {
return this.userDocumentSettings.isSnapMode return this.userDocumentSettings.isSnapMode
} }

View file

@ -21,6 +21,7 @@ export type StoreOptions = {
customShapes?: Record<string, ShapeInfo> customShapes?: Record<string, ShapeInfo>
instanceId?: TLInstanceId instanceId?: TLInstanceId
initialData?: StoreSnapshot<TLRecord> initialData?: StoreSnapshot<TLRecord>
defaultName?: string
} }
/** /**
@ -30,7 +31,12 @@ export type StoreOptions = {
* *
* @public */ * @public */
export function createTLStore(opts = {} as StoreOptions): TLStore { 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({ return new Store({
schema: createTLSchema({ customShapes }), schema: createTLSchema({ customShapes }),
@ -38,6 +44,7 @@ export function createTLStore(opts = {} as StoreOptions): TLStore {
props: { props: {
instanceId, instanceId,
documentId: TLDOCUMENT_ID, documentId: TLDOCUMENT_ID,
defaultName,
}, },
}) })
} }

View file

@ -5,11 +5,10 @@ import { usePrevious } from './usePrevious'
/** @public */ /** @public */
export function useTLStore(opts: StoreOptions) { export function useTLStore(opts: StoreOptions) {
const [store, setStore] = useState(() => createTLStore(opts)) const [store, setStore] = useState(() => createTLStore(opts))
const previousOpts = usePrevious(opts) const prev = usePrevious(opts)
if ( if (
previousOpts.customShapes !== opts.customShapes || // shallow equality check
previousOpts.initialData !== opts.initialData || (Object.keys(prev) as (keyof StoreOptions)[]).some((key) => prev[key] !== opts[key])
previousOpts.instanceId !== opts.instanceId
) { ) {
const newStore = createTLStore(opts) const newStore = createTLStore(opts)
setStore(newStore) setStore(newStore)

View file

@ -780,6 +780,8 @@ export type TLDefaultShape = TLArrowShape | TLBookmarkShape | TLDrawShape | TLEm
export interface TLDocument extends BaseRecord<'document', ID<TLDocument>> { export interface TLDocument extends BaseRecord<'document', ID<TLDocument>> {
// (undocumented) // (undocumented)
gridSize: number; gridSize: number;
// (undocumented)
name: string;
} }
// @public (undocumented) // @public (undocumented)
@ -1250,6 +1252,7 @@ export type TLStore = Store<TLRecord, TLStoreProps>;
export type TLStoreProps = { export type TLStoreProps = {
instanceId: TLInstanceId; instanceId: TLInstanceId;
documentId: typeof TLDOCUMENT_ID; documentId: typeof TLDOCUMENT_ID;
defaultName: string;
}; };
// @public (undocumented) // @public (undocumented)

View file

@ -40,6 +40,7 @@ export type TLStoreSnapshot = StoreSnapshot<TLRecord>
export type TLStoreProps = { export type TLStoreProps = {
instanceId: TLInstanceId instanceId: TLInstanceId
documentId: typeof TLDOCUMENT_ID documentId: typeof TLDOCUMENT_ID
defaultName: string
} }
/** @public */ /** @public */
@ -91,7 +92,7 @@ export function createIntegrityChecker(store: TLStore): () => void {
const { instanceId: tabId } = store.props const { instanceId: tabId } = store.props
// make sure we have exactly one document // make sure we have exactly one document
if (!store.has(TLDOCUMENT_ID)) { 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() return ensureStoreIsUsable()
} }

View file

@ -3,6 +3,7 @@ import { structuredClone } from '@tldraw/utils'
import fs from 'fs' import fs from 'fs'
import { imageAssetMigrations } from './assets/TLImageAsset' import { imageAssetMigrations } from './assets/TLImageAsset'
import { videoAssetMigrations } from './assets/TLVideoAsset' import { videoAssetMigrations } from './assets/TLVideoAsset'
import { documentTypeMigrations } from './records/TLDocument'
import { instanceTypeMigrations, instanceTypeVersions } from './records/TLInstance' import { instanceTypeMigrations, instanceTypeVersions } from './records/TLInstance'
import { instancePageStateMigrations } from './records/TLInstancePageState' import { instancePageStateMigrations } from './records/TLInstancePageState'
import { instancePresenceTypeMigrations } from './records/TLInstancePresence' 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', () => { describe('Adding check-box to geo shape', () => {
const { up, down } = geoShapeTypeMigrations.migrators[4] const { up, down } = geoShapeTypeMigrations.migrators[4]

View file

@ -8,6 +8,7 @@ import { T } from '@tldraw/tlvalidate'
*/ */
export interface TLDocument extends BaseRecord<'document', ID<TLDocument>> { export interface TLDocument extends BaseRecord<'document', ID<TLDocument>> {
gridSize: number gridSize: number
name: string
} }
/** @public */ /** @public */
@ -17,22 +18,46 @@ export const documentTypeValidator: T.Validator<TLDocument> = T.model(
typeName: T.literal('document'), typeName: T.literal('document'),
id: T.literal('document:document' as ID<TLDocument>), id: T.literal('document:document' as ID<TLDocument>),
gridSize: T.number, 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 */ /** @public */
export const DocumentRecordType = createRecordType<TLDocument>('document', { export const DocumentRecordType = createRecordType<TLDocument>('document', {
migrations: documentTypeMigrations,
validator: documentTypeValidator, validator: documentTypeValidator,
scope: 'document', scope: 'document',
}).withDefaultProperties( }).withDefaultProperties(
(): Omit<TLDocument, 'id' | 'typeName'> => ({ (): Omit<TLDocument, 'id' | 'typeName'> => ({
gridSize: 10, gridSize: 10,
name: '',
}) })
) )
// all document records have the same ID: 'document:document' // all document records have the same ID: 'document:document'
/** @public */ /** @public */
export const TLDOCUMENT_ID: ID<TLDocument> = DocumentRecordType.createCustomId('document') export const TLDOCUMENT_ID: ID<TLDocument> = DocumentRecordType.createCustomId('document')
/** @public */
export const documentTypeMigrations = defineMigrations({})

File diff suppressed because one or more lines are too long

View file

@ -31,6 +31,7 @@ export type TldrawUiProps = {
hideUi?: boolean hideUi?: boolean
/** A component to use for the share zone (will be deprecated) */ /** A component to use for the share zone (will be deprecated) */
shareZone?: ReactNode shareZone?: ReactNode
topZone?: ReactNode
/** Additional items to add to the debug menu (will be deprecated)*/ /** Additional items to add to the debug menu (will be deprecated)*/
renderDebugMenuItems?: () => React.ReactNode renderDebugMenuItems?: () => React.ReactNode
} & TldrawUiContextProviderProps } & TldrawUiContextProviderProps
@ -40,6 +41,7 @@ export type TldrawUiProps = {
*/ */
export const TldrawUi = React.memo(function TldrawUi({ export const TldrawUi = React.memo(function TldrawUi({
shareZone, shareZone,
topZone,
renderDebugMenuItems, renderDebugMenuItems,
children, children,
hideUi, hideUi,
@ -50,6 +52,7 @@ export const TldrawUi = React.memo(function TldrawUi({
<TldrawUiInner <TldrawUiInner
hideUi={hideUi} hideUi={hideUi}
shareZone={shareZone} shareZone={shareZone}
topZone={topZone}
renderDebugMenuItems={renderDebugMenuItems} renderDebugMenuItems={renderDebugMenuItems}
> >
{children} {children}
@ -61,6 +64,7 @@ export const TldrawUi = React.memo(function TldrawUi({
type TldrawUiContentProps = { type TldrawUiContentProps = {
hideUi?: boolean hideUi?: boolean
shareZone?: ReactNode shareZone?: ReactNode
topZone?: ReactNode
renderDebugMenuItems?: () => React.ReactNode renderDebugMenuItems?: () => React.ReactNode
} }
@ -84,6 +88,7 @@ const TldrawUiInner = React.memo(function TldrawUiInner({
/** @public */ /** @public */
export const TldrawUiContent = React.memo(function TldrawUI({ export const TldrawUiContent = React.memo(function TldrawUI({
shareZone, shareZone,
topZone,
renderDebugMenuItems, renderDebugMenuItems,
}: TldrawUiContentProps) { }: TldrawUiContentProps) {
const app = useApp() const app = useApp()
@ -127,12 +132,9 @@ export const TldrawUiContent = React.memo(function TldrawUI({
<StopFollowing /> <StopFollowing />
</div> </div>
</div> </div>
<div className="tlui-layout__top__center">{topZone}</div>
<div className="tlui-layout__top__right"> <div className="tlui-layout__top__right">
{shareZone && (
<div className="tlui-share-zone" draggable={false}>
{shareZone} {shareZone}
</div>
)}
{breakpoint >= 5 && !isReadonlyMode && ( {breakpoint >= 5 && !isReadonlyMode && (
<div className="tlui-style-panel__wrapper"> <div className="tlui-style-panel__wrapper">
<StylePanel /> <StylePanel />

View file

@ -24,7 +24,7 @@ export const MenuZone = track(function MenuZone() {
<Menu /> <Menu />
<div className="tlui-menu-zone__divider" /> <div className="tlui-menu-zone__divider" />
<PageMenu /> <PageMenu />
{breakpoint >= 5 && showQuickActions && ( {breakpoint >= 6 && showQuickActions && (
<> <>
<div className="tlui-menu-zone__divider" /> <div className="tlui-menu-zone__divider" />
<UndoButton /> <UndoButton />

View file

@ -115,7 +115,7 @@ export const Toolbar = function Toolbar() {
'tlui-toolbar__extras__hidden': !showExtraActions, 'tlui-toolbar__extras__hidden': !showExtraActions,
})} })}
> >
{breakpoint < 5 && ( {breakpoint < 6 && (
<div className="tlui-toolbar__extras__controls"> <div className="tlui-toolbar__extras__controls">
<UndoButton /> <UndoButton />
<RedoButton /> <RedoButton />

View file

@ -19,6 +19,7 @@ export interface InputProps {
onComplete?: (value: string) => void onComplete?: (value: string) => void
onValueChange?: (value: string) => void onValueChange?: (value: string) => void
onCancel?: (value: string) => void onCancel?: (value: string) => void
onBlur?: (value: string) => void
className?: string className?: string
/** /**
* Usually on iOS when you focus an input, the browser will adjust the viewport to bring the input * 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<HTMLInputElement, InputProps>(function Inp
onComplete, onComplete,
onValueChange, onValueChange,
onCancel, onCancel,
onBlur,
shouldManuallyMaintainScrollPositionWhenFocused = false, shouldManuallyMaintainScrollPositionWhenFocused = false,
children, children,
value, value,
@ -106,7 +108,14 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(function Inp
[onComplete, onCancel] [onComplete, onCancel]
) )
const handleBlur = React.useCallback(() => setIsFocused(false), []) const handleBlur = React.useCallback(
(e: React.FocusEvent<HTMLInputElement>) => {
setIsFocused(false)
const value = e.currentTarget.value
onBlur?.(value)
},
[onBlur]
)
React.useEffect(() => { React.useEffect(() => {
const visualViewport = window.visualViewport const visualViewport = window.visualViewport

View file

@ -233,6 +233,7 @@ export type TLTranslationKey =
| 'share-menu.save-note' | 'share-menu.save-note'
| 'share-menu.fork-note' | 'share-menu.fork-note'
| 'share-menu.share-project' | 'share-menu.share-project'
| 'share-menu.default-project-name'
| 'share-menu.copy-link' | 'share-menu.copy-link'
| 'share-menu.readonly-link' | 'share-menu.readonly-link'
| 'share-menu.create-snapshot-link' | 'share-menu.create-snapshot-link'
@ -281,6 +282,12 @@ export type TLTranslationKey =
| 'shortcuts-dialog.tools' | 'shortcuts-dialog.tools'
| 'shortcuts-dialog.transform' | 'shortcuts-dialog.transform'
| 'shortcuts-dialog.view' | '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.title'
| 'style-panel.align' | 'style-panel.align'
| 'style-panel.vertical-align' | 'style-panel.vertical-align'

View file

@ -233,6 +233,7 @@ export const DEFAULT_TRANSLATION = {
'share-menu.save-note': 'Download this project to your computer as a .tldr file.', '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.fork-note': 'Create a new shared project based on this snapshot.',
'share-menu.share-project': 'Share this project', 'share-menu.share-project': 'Share this project',
'share-menu.default-project-name': 'Shared Project',
'share-menu.copy-link': 'Copy share link', 'share-menu.copy-link': 'Copy share link',
'share-menu.readonly-link': 'Read-only', 'share-menu.readonly-link': 'Read-only',
'share-menu.create-snapshot-link': 'Copy snapshot link', 'share-menu.create-snapshot-link': 'Copy snapshot link',
@ -284,6 +285,12 @@ export const DEFAULT_TRANSLATION = {
'shortcuts-dialog.tools': 'Tools', 'shortcuts-dialog.tools': 'Tools',
'shortcuts-dialog.transform': 'Transform', 'shortcuts-dialog.transform': 'Transform',
'shortcuts-dialog.view': 'View', '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.title': 'Styles',
'style-panel.align': 'Align', 'style-panel.align': 'Align',
'style-panel.vertical-align': 'Vertical align', 'style-panel.vertical-align': 'Vertical align',

View file

@ -28,6 +28,7 @@
grid-column: 1; grid-column: 1;
grid-row: 1; grid-row: 1;
display: flex; display: flex;
min-width: 0px;
} }
.tlui-layout__top__left { .tlui-layout__top__left {
@ -37,6 +38,19 @@
justify-content: flex-start; justify-content: flex-start;
width: 100%; width: 100%;
height: 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 { .tlui-layout__top__right {
@ -46,6 +60,8 @@
justify-content: flex-start; justify-content: flex-start;
width: 100%; width: 100%;
height: 100%; height: 100%;
flex-shrink: 1;
min-width: 0px;
} }
.scrollable, .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 ------------------- */ /* ------------------- Navigation ------------------- */
.tlui-navigation-zone { .tlui-navigation-zone {