@tldraw/tlschema 4.6.0-next.1f489710ee41 → 4.6.0-next.4dde09fa17ab

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist-cjs/TLStore.js +13 -0
  2. package/dist-cjs/TLStore.js.map +2 -2
  3. package/dist-cjs/createPresenceStateDerivation.js +6 -4
  4. package/dist-cjs/createPresenceStateDerivation.js.map +2 -2
  5. package/dist-cjs/createTLSchema.js +8 -1
  6. package/dist-cjs/createTLSchema.js.map +2 -2
  7. package/dist-cjs/index.d.ts +211 -28
  8. package/dist-cjs/index.js +9 -1
  9. package/dist-cjs/index.js.map +2 -2
  10. package/dist-cjs/records/TLRecord.js.map +1 -1
  11. package/dist-cjs/records/TLUser.js +101 -0
  12. package/dist-cjs/records/TLUser.js.map +7 -0
  13. package/dist-cjs/shapes/TLNoteShape.js +13 -2
  14. package/dist-cjs/shapes/TLNoteShape.js.map +2 -2
  15. package/dist-esm/TLStore.mjs +13 -0
  16. package/dist-esm/TLStore.mjs.map +2 -2
  17. package/dist-esm/createPresenceStateDerivation.mjs +6 -4
  18. package/dist-esm/createPresenceStateDerivation.mjs.map +2 -2
  19. package/dist-esm/createTLSchema.mjs +8 -1
  20. package/dist-esm/createTLSchema.mjs.map +2 -2
  21. package/dist-esm/index.d.mts +211 -28
  22. package/dist-esm/index.mjs +17 -1
  23. package/dist-esm/index.mjs.map +2 -2
  24. package/dist-esm/records/TLUser.mjs +85 -0
  25. package/dist-esm/records/TLUser.mjs.map +7 -0
  26. package/dist-esm/shapes/TLNoteShape.mjs +13 -2
  27. package/dist-esm/shapes/TLNoteShape.mjs.map +2 -2
  28. package/package.json +6 -6
  29. package/src/TLStore.test.ts +5 -0
  30. package/src/TLStore.ts +95 -1
  31. package/src/createPresenceStateDerivation.test.ts +33 -20
  32. package/src/createPresenceStateDerivation.ts +20 -25
  33. package/src/createTLSchema.ts +52 -0
  34. package/src/index.ts +13 -1
  35. package/src/migrations.test.ts +22 -0
  36. package/src/records/TLRecord.ts +2 -0
  37. package/src/records/TLUser.ts +148 -0
  38. package/src/shapes/TLNoteShape.ts +13 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/shapes/TLNoteShape.ts"],
4
- "sourcesContent": ["import { T } from '@tldraw/validate'\nimport { TLRichText, richTextValidator, toRichText } from '../misc/TLRichText'\nimport { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'\nimport { RecordProps } from '../recordsWithProps'\nimport {\n\tDefaultColorStyle,\n\tDefaultLabelColorStyle,\n\tTLDefaultColorStyle,\n} from '../styles/TLColorStyle'\nimport { DefaultFontStyle, TLDefaultFontStyle } from '../styles/TLFontStyle'\nimport {\n\tDefaultHorizontalAlignStyle,\n\tTLDefaultHorizontalAlignStyle,\n} from '../styles/TLHorizontalAlignStyle'\nimport { DefaultSizeStyle, TLDefaultSizeStyle } from '../styles/TLSizeStyle'\nimport {\n\tDefaultVerticalAlignStyle,\n\tTLDefaultVerticalAlignStyle,\n} from '../styles/TLVerticalAlignStyle'\nimport { TLBaseShape } from './TLBaseShape'\n\n/**\n * Properties for a note shape. Note shapes represent sticky notes or text annotations\n * with rich formatting capabilities and various styling options.\n *\n * @public\n * @example\n * ```ts\n * const noteProps: TLNoteShapeProps = {\n * color: 'yellow',\n * labelColor: 'black',\n * size: 'm',\n * font: 'draw',\n * fontSizeAdjustment: 0,\n * align: 'middle',\n * verticalAlign: 'middle',\n * growY: 0,\n * url: '',\n * richText: toRichText('Hello **world**!'),\n * scale: 1\n * }\n * ```\n */\nexport interface TLNoteShapeProps {\n\t/** Background color style of the note */\n\tcolor: TLDefaultColorStyle\n\t/** Text color style for the note content */\n\tlabelColor: TLDefaultColorStyle\n\t/** Size style determining the font size and note dimensions */\n\tsize: TLDefaultSizeStyle\n\t/** Font family style for the note text */\n\tfont: TLDefaultFontStyle\n\t/** Adjustment to the base font size (positive increases, negative decreases) */\n\tfontSizeAdjustment: number\n\t/** Horizontal alignment of text within the note */\n\talign: TLDefaultHorizontalAlignStyle\n\t/** Vertical alignment of text within the note */\n\tverticalAlign: TLDefaultVerticalAlignStyle\n\t/** Additional height growth for the note beyond its base size */\n\tgrowY: number\n\t/** Optional URL associated with the note for linking */\n\turl: string\n\t/** Rich text content with formatting like bold, italic, etc. */\n\trichText: TLRichText\n\t/** Scale factor applied to the note shape for display */\n\tscale: number\n}\n\n/**\n * A note shape representing a sticky note or text annotation on the canvas.\n * Note shapes support rich text formatting, various styling options, and can\n * be used for annotations, reminders, or general text content.\n *\n * @public\n * @example\n * ```ts\n * const noteShape: TLNoteShape = {\n * id: 'shape:note1',\n * type: 'note',\n * x: 100,\n * y: 100,\n * rotation: 0,\n * index: 'a1',\n * parentId: 'page:main',\n * isLocked: false,\n * opacity: 1,\n * props: {\n * color: 'light-blue',\n * labelColor: 'black',\n * size: 's',\n * font: 'sans',\n * fontSizeAdjustment: 2,\n * align: 'start',\n * verticalAlign: 'start',\n * growY: 50,\n * url: 'https://example.com',\n * richText: toRichText('Important **note**!'),\n * scale: 1\n * },\n * meta: {},\n * typeName: 'shape'\n * }\n * ```\n */\nexport type TLNoteShape = TLBaseShape<'note', TLNoteShapeProps>\n\n/**\n * Validation schema for note shape properties. Defines the runtime validation rules\n * for all properties of note shapes, ensuring data integrity and type safety.\n *\n * @public\n * @example\n * ```ts\n * import { noteShapeProps } from '@tldraw/tlschema'\n *\n * // Used internally by the validation system\n * const validator = T.object(noteShapeProps)\n * const validatedProps = validator.validate(someNoteProps)\n * ```\n */\nexport const noteShapeProps: RecordProps<TLNoteShape> = {\n\tcolor: DefaultColorStyle,\n\tlabelColor: DefaultLabelColorStyle,\n\tsize: DefaultSizeStyle,\n\tfont: DefaultFontStyle,\n\tfontSizeAdjustment: T.positiveNumber,\n\talign: DefaultHorizontalAlignStyle,\n\tverticalAlign: DefaultVerticalAlignStyle,\n\tgrowY: T.positiveNumber,\n\turl: T.linkUrl,\n\trichText: richTextValidator,\n\tscale: T.nonZeroNumber,\n}\n\nconst Versions = createShapePropsMigrationIds('note', {\n\tAddUrlProp: 1,\n\tRemoveJustify: 2,\n\tMigrateLegacyAlign: 3,\n\tAddVerticalAlign: 4,\n\tMakeUrlsValid: 5,\n\tAddFontSizeAdjustment: 6,\n\tAddScale: 7,\n\tAddLabelColor: 8,\n\tAddRichText: 9,\n\tAddRichTextAttrs: 10,\n})\n\n/**\n * Version identifiers for note shape migrations. These version numbers track\n * significant schema changes over time, enabling proper data migration between versions.\n *\n * @public\n */\nexport { Versions as noteShapeVersions }\n\n/**\n * Migration sequence for note shapes. Handles schema evolution over time by defining\n * how to upgrade and downgrade note shape data between different versions. Includes\n * migrations for URL properties, text alignment changes, vertical alignment addition,\n * font size adjustments, scaling support, label color, the transition from plain text to rich text,\n * and support for attrs property on richText.\n *\n * @public\n */\nexport const noteShapeMigrations = createShapePropsMigrationSequence({\n\tsequence: [\n\t\t{\n\t\t\tid: Versions.AddUrlProp,\n\t\t\tup: (props) => {\n\t\t\t\tprops.url = ''\n\t\t\t},\n\t\t\tdown: 'retired',\n\t\t},\n\t\t{\n\t\t\tid: Versions.RemoveJustify,\n\t\t\tup: (props) => {\n\t\t\t\tif (props.align === 'justify') {\n\t\t\t\t\tprops.align = 'start'\n\t\t\t\t}\n\t\t\t},\n\t\t\tdown: 'retired',\n\t\t},\n\t\t{\n\t\t\tid: Versions.MigrateLegacyAlign,\n\t\t\tup: (props) => {\n\t\t\t\tswitch (props.align) {\n\t\t\t\t\tcase 'start':\n\t\t\t\t\t\tprops.align = 'start-legacy'\n\t\t\t\t\t\treturn\n\t\t\t\t\tcase 'end':\n\t\t\t\t\t\tprops.align = 'end-legacy'\n\t\t\t\t\t\treturn\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tprops.align = 'middle-legacy'\n\t\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t},\n\t\t\tdown: 'retired',\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddVerticalAlign,\n\t\t\tup: (props) => {\n\t\t\t\tprops.verticalAlign = 'middle'\n\t\t\t},\n\t\t\tdown: 'retired',\n\t\t},\n\t\t{\n\t\t\tid: Versions.MakeUrlsValid,\n\t\t\tup: (props) => {\n\t\t\t\tif (!T.linkUrl.isValid(props.url)) {\n\t\t\t\t\tprops.url = ''\n\t\t\t\t}\n\t\t\t},\n\t\t\tdown: (_props) => {\n\t\t\t\t// noop\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddFontSizeAdjustment,\n\t\t\tup: (props) => {\n\t\t\t\tprops.fontSizeAdjustment = 0\n\t\t\t},\n\t\t\tdown: (props) => {\n\t\t\t\tdelete props.fontSizeAdjustment\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddScale,\n\t\t\tup: (props) => {\n\t\t\t\tprops.scale = 1\n\t\t\t},\n\t\t\tdown: (props) => {\n\t\t\t\tdelete props.scale\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddLabelColor,\n\t\t\tup: (props) => {\n\t\t\t\tprops.labelColor = 'black'\n\t\t\t},\n\t\t\tdown: (props) => {\n\t\t\t\tdelete props.labelColor\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddRichText,\n\t\t\tup: (props) => {\n\t\t\t\tprops.richText = toRichText(props.text)\n\t\t\t\tdelete props.text\n\t\t\t},\n\t\t\t// N.B. Explicitly no down state so that we force clients to update.\n\t\t\t// down: (props) => {\n\t\t\t// \tdelete props.richText\n\t\t\t// },\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddRichTextAttrs,\n\t\t\tup: (_props) => {\n\t\t\t\t// noop - attrs is optional so old records are valid\n\t\t\t},\n\t\t\tdown: (props) => {\n\t\t\t\t// Remove attrs from richText when migrating down\n\t\t\t\tif (props.richText && 'attrs' in props.richText) {\n\t\t\t\t\tdelete props.richText.attrs\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t],\n})\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { T } from '@tldraw/validate'\nimport { TLRichText, richTextValidator, toRichText } from '../misc/TLRichText'\nimport { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'\nimport { RecordProps } from '../recordsWithProps'\nimport {\n\tDefaultColorStyle,\n\tDefaultLabelColorStyle,\n\tTLDefaultColorStyle,\n} from '../styles/TLColorStyle'\nimport { DefaultFontStyle, TLDefaultFontStyle } from '../styles/TLFontStyle'\nimport {\n\tDefaultHorizontalAlignStyle,\n\tTLDefaultHorizontalAlignStyle,\n} from '../styles/TLHorizontalAlignStyle'\nimport { DefaultSizeStyle, TLDefaultSizeStyle } from '../styles/TLSizeStyle'\nimport {\n\tDefaultVerticalAlignStyle,\n\tTLDefaultVerticalAlignStyle,\n} from '../styles/TLVerticalAlignStyle'\nimport { TLBaseShape } from './TLBaseShape'\n\n/**\n * Properties for a note shape. Note shapes represent sticky notes or text annotations\n * with rich formatting capabilities and various styling options.\n *\n * @public\n * @example\n * ```ts\n * const noteProps: TLNoteShapeProps = {\n * color: 'yellow',\n * labelColor: 'black',\n * size: 'm',\n * font: 'draw',\n * fontSizeAdjustment: 0,\n * align: 'middle',\n * verticalAlign: 'middle',\n * growY: 0,\n * url: '',\n * richText: toRichText('Hello **world**!'),\n * scale: 1\n * }\n * ```\n */\nexport interface TLNoteShapeProps {\n\t/** Background color style of the note */\n\tcolor: TLDefaultColorStyle\n\t/** Text color style for the note content */\n\tlabelColor: TLDefaultColorStyle\n\t/** Size style determining the font size and note dimensions */\n\tsize: TLDefaultSizeStyle\n\t/** Font family style for the note text */\n\tfont: TLDefaultFontStyle\n\t/** Adjustment to the base font size (positive increases, negative decreases) */\n\tfontSizeAdjustment: number\n\t/** Horizontal alignment of text within the note */\n\talign: TLDefaultHorizontalAlignStyle\n\t/** Vertical alignment of text within the note */\n\tverticalAlign: TLDefaultVerticalAlignStyle\n\t/** Additional height growth for the note beyond its base size */\n\tgrowY: number\n\t/** Optional URL associated with the note for linking */\n\turl: string\n\t/** Rich text content with formatting like bold, italic, etc. */\n\trichText: TLRichText\n\t/** Scale factor applied to the note shape for display */\n\tscale: number\n\t/** User ID of the person who first edited the note text */\n\ttextFirstEditedBy: string | null\n}\n\n/**\n * A note shape representing a sticky note or text annotation on the canvas.\n * Note shapes support rich text formatting, various styling options, and can\n * be used for annotations, reminders, or general text content.\n *\n * @public\n * @example\n * ```ts\n * const noteShape: TLNoteShape = {\n * id: 'shape:note1',\n * type: 'note',\n * x: 100,\n * y: 100,\n * rotation: 0,\n * index: 'a1',\n * parentId: 'page:main',\n * isLocked: false,\n * opacity: 1,\n * props: {\n * color: 'light-blue',\n * labelColor: 'black',\n * size: 's',\n * font: 'sans',\n * fontSizeAdjustment: 2,\n * align: 'start',\n * verticalAlign: 'start',\n * growY: 50,\n * url: 'https://example.com',\n * richText: toRichText('Important **note**!'),\n * scale: 1\n * },\n * meta: {},\n * typeName: 'shape'\n * }\n * ```\n */\nexport type TLNoteShape = TLBaseShape<'note', TLNoteShapeProps>\n\n/**\n * Validation schema for note shape properties. Defines the runtime validation rules\n * for all properties of note shapes, ensuring data integrity and type safety.\n *\n * @public\n * @example\n * ```ts\n * import { noteShapeProps } from '@tldraw/tlschema'\n *\n * // Used internally by the validation system\n * const validator = T.object(noteShapeProps)\n * const validatedProps = validator.validate(someNoteProps)\n * ```\n */\nexport const noteShapeProps: RecordProps<TLNoteShape> = {\n\tcolor: DefaultColorStyle,\n\tlabelColor: DefaultLabelColorStyle,\n\tsize: DefaultSizeStyle,\n\tfont: DefaultFontStyle,\n\tfontSizeAdjustment: T.positiveNumber,\n\talign: DefaultHorizontalAlignStyle,\n\tverticalAlign: DefaultVerticalAlignStyle,\n\tgrowY: T.positiveNumber,\n\turl: T.linkUrl,\n\trichText: richTextValidator,\n\tscale: T.nonZeroNumber,\n\ttextFirstEditedBy: T.string.nullable(),\n}\n\nconst Versions = createShapePropsMigrationIds('note', {\n\tAddUrlProp: 1,\n\tRemoveJustify: 2,\n\tMigrateLegacyAlign: 3,\n\tAddVerticalAlign: 4,\n\tMakeUrlsValid: 5,\n\tAddFontSizeAdjustment: 6,\n\tAddScale: 7,\n\tAddLabelColor: 8,\n\tAddRichText: 9,\n\tAddRichTextAttrs: 10,\n\tAddFirstEditedBy: 11,\n})\n\n/**\n * Version identifiers for note shape migrations. These version numbers track\n * significant schema changes over time, enabling proper data migration between versions.\n *\n * @public\n */\nexport { Versions as noteShapeVersions }\n\n/**\n * Migration sequence for note shapes. Handles schema evolution over time by defining\n * how to upgrade and downgrade note shape data between different versions. Includes\n * migrations for URL properties, text alignment changes, vertical alignment addition,\n * font size adjustments, scaling support, label color, the transition from plain text to rich text,\n * and support for attrs property on richText.\n *\n * @public\n */\nexport const noteShapeMigrations = createShapePropsMigrationSequence({\n\tsequence: [\n\t\t{\n\t\t\tid: Versions.AddUrlProp,\n\t\t\tup: (props) => {\n\t\t\t\tprops.url = ''\n\t\t\t},\n\t\t\tdown: 'retired',\n\t\t},\n\t\t{\n\t\t\tid: Versions.RemoveJustify,\n\t\t\tup: (props) => {\n\t\t\t\tif (props.align === 'justify') {\n\t\t\t\t\tprops.align = 'start'\n\t\t\t\t}\n\t\t\t},\n\t\t\tdown: 'retired',\n\t\t},\n\t\t{\n\t\t\tid: Versions.MigrateLegacyAlign,\n\t\t\tup: (props) => {\n\t\t\t\tswitch (props.align) {\n\t\t\t\t\tcase 'start':\n\t\t\t\t\t\tprops.align = 'start-legacy'\n\t\t\t\t\t\treturn\n\t\t\t\t\tcase 'end':\n\t\t\t\t\t\tprops.align = 'end-legacy'\n\t\t\t\t\t\treturn\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tprops.align = 'middle-legacy'\n\t\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t},\n\t\t\tdown: 'retired',\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddVerticalAlign,\n\t\t\tup: (props) => {\n\t\t\t\tprops.verticalAlign = 'middle'\n\t\t\t},\n\t\t\tdown: 'retired',\n\t\t},\n\t\t{\n\t\t\tid: Versions.MakeUrlsValid,\n\t\t\tup: (props) => {\n\t\t\t\tif (!T.linkUrl.isValid(props.url)) {\n\t\t\t\t\tprops.url = ''\n\t\t\t\t}\n\t\t\t},\n\t\t\tdown: (_props) => {\n\t\t\t\t// noop\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddFontSizeAdjustment,\n\t\t\tup: (props) => {\n\t\t\t\tprops.fontSizeAdjustment = 0\n\t\t\t},\n\t\t\tdown: (props) => {\n\t\t\t\tdelete props.fontSizeAdjustment\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddScale,\n\t\t\tup: (props) => {\n\t\t\t\tprops.scale = 1\n\t\t\t},\n\t\t\tdown: (props) => {\n\t\t\t\tdelete props.scale\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddLabelColor,\n\t\t\tup: (props) => {\n\t\t\t\tprops.labelColor = 'black'\n\t\t\t},\n\t\t\tdown: (props) => {\n\t\t\t\tdelete props.labelColor\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddRichText,\n\t\t\tup: (props) => {\n\t\t\t\tprops.richText = toRichText(props.text)\n\t\t\t\tdelete props.text\n\t\t\t},\n\t\t\t// N.B. Explicitly no down state so that we force clients to update.\n\t\t\t// down: (props) => {\n\t\t\t// \tdelete props.richText\n\t\t\t// },\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddRichTextAttrs,\n\t\t\tup: (_props) => {\n\t\t\t\t// noop - attrs is optional so old records are valid\n\t\t\t},\n\t\t\tdown: (props) => {\n\t\t\t\t// Remove attrs from richText when migrating down\n\t\t\t\tif (props.richText && 'attrs' in props.richText) {\n\t\t\t\t\tdelete props.richText.attrs\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: Versions.AddFirstEditedBy,\n\t\t\tup: (props) => {\n\t\t\t\tprops.textFirstEditedBy = null\n\t\t\t},\n\t\t\tdown: (props) => {\n\t\t\t\tdelete props.textFirstEditedBy\n\t\t\t},\n\t\t},\n\t],\n})\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/tlschema",
3
3
  "description": "tldraw infinite canvas SDK (schema).",
4
- "version": "4.6.0-next.1f489710ee41",
4
+ "version": "4.6.0-next.4dde09fa17ab",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -41,7 +41,7 @@
41
41
  "prepack": "yarn run -T tsx ../../internal/scripts/prepack.ts",
42
42
  "postpack": "../../internal/scripts/postpack.sh",
43
43
  "pack-tarball": "yarn pack",
44
- "lint": "cd ../.. && yarn run -T oxlint packages/tlschema"
44
+ "lint": "yarn run -T tsx ../../internal/scripts/lint.ts"
45
45
  },
46
46
  "devDependencies": {
47
47
  "kleur": "^4.1.5",
@@ -50,10 +50,10 @@
50
50
  "vitest": "^3.2.4"
51
51
  },
52
52
  "dependencies": {
53
- "@tldraw/state": "4.6.0-next.1f489710ee41",
54
- "@tldraw/store": "4.6.0-next.1f489710ee41",
55
- "@tldraw/utils": "4.6.0-next.1f489710ee41",
56
- "@tldraw/validate": "4.6.0-next.1f489710ee41"
53
+ "@tldraw/state": "4.6.0-next.4dde09fa17ab",
54
+ "@tldraw/store": "4.6.0-next.4dde09fa17ab",
55
+ "@tldraw/utils": "4.6.0-next.4dde09fa17ab",
56
+ "@tldraw/validate": "4.6.0-next.4dde09fa17ab"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "react": "^18.2.0 || ^19.2.1",
@@ -1,3 +1,4 @@
1
+ import { computed } from '@tldraw/state'
1
2
  import { Store } from '@tldraw/store'
2
3
  import { annotateError, IndexKey, structuredClone } from '@tldraw/utils'
3
4
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
@@ -330,6 +331,10 @@ describe('createIntegrityChecker', () => {
330
331
  props: {
331
332
  defaultName: 'Test Store',
332
333
  assets: mockAssetStore,
334
+ users: {
335
+ currentUser: computed('currentUser', () => null),
336
+ resolve: () => computed('resolve', () => null),
337
+ },
333
338
  onMount: vi.fn(),
334
339
  },
335
340
  })
package/src/TLStore.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Signal } from '@tldraw/state'
1
+ import { Signal, computed } from '@tldraw/state'
2
2
  import {
3
3
  SerializedStore,
4
4
  Store,
@@ -15,6 +15,7 @@ import { PageRecordType, TLPageId } from './records/TLPage'
15
15
  import { InstancePageStateRecordType, TLInstancePageStateId } from './records/TLPageState'
16
16
  import { PointerRecordType, TLPOINTER_ID } from './records/TLPointer'
17
17
  import { TLRecord } from './records/TLRecord'
18
+ import { TLUser } from './records/TLUser'
18
19
 
19
20
  /**
20
21
  * Redacts the source of an asset record for error reporting.
@@ -200,6 +201,97 @@ export interface TLAssetStore {
200
201
  remove?(assetIds: TLAssetId[]): Promise<void>
201
202
  }
202
203
 
204
+ /**
205
+ * Interface for resolving user information in tldraw.
206
+ *
207
+ * A `TLUserStore` sits alongside the main {@link TLStore} and provides user
208
+ * resolution for attribution labels and display names. Implement this interface
209
+ * to connect tldraw to your auth/user system.
210
+ *
211
+ * `currentUser` and `resolve` are reactive {@link @tldraw/state#Signal | Signals}
212
+ * so that the editor can automatically track changes to user data and
213
+ * re-render when a user's name, color, or avatar updates.
214
+ *
215
+ * Implementations should cache signals returned by `resolve` — e.g. return the
216
+ * same `Signal` for repeated calls with the same `userId` — to avoid
217
+ * unnecessary re-computation.
218
+ *
219
+ * @public
220
+ * @example
221
+ * ```ts
222
+ * const currentUser = computed('currentUser', () =>
223
+ * UserRecordType.create({
224
+ * id: createUserId(myAuth.userId),
225
+ * name: myAuth.displayName,
226
+ * color: myAuth.color,
227
+ * })
228
+ * )
229
+ *
230
+ * const userStore: TLUserStore = {
231
+ * currentUser,
232
+ * resolve(userId) {
233
+ * return computed('resolve-' + userId, () =>
234
+ * myUserCache.get(userId) ?? null
235
+ * )
236
+ * },
237
+ * }
238
+ * ```
239
+ */
240
+ export interface TLUserStore {
241
+ /**
242
+ * A signal resolving to the currently authenticated user,
243
+ * or `null` for anonymous / unknown.
244
+ * Read when stamping attribution on shape create/update.
245
+ */
246
+ currentUser: Signal<TLUser | null>
247
+
248
+ /**
249
+ * Return a signal resolving an arbitrary user ID to display info.
250
+ * Called when rendering attribution labels for shapes that may have been
251
+ * created or edited by someone else.
252
+ * The signal's value should be `null` if the user cannot be resolved.
253
+ */
254
+ resolve?(userId: string): Signal<TLUser | null>
255
+ }
256
+
257
+ /**
258
+ * Create a cached {@link TLUserStore.resolve} implementation.
259
+ *
260
+ * Wraps a reactive lookup function so that each `userId` gets a single
261
+ * stable {@link @tldraw/state#Signal | Signal} that is reused across calls.
262
+ * The `resolveFn` is evaluated inside a `computed`, so any `.get()` calls
263
+ * it makes are automatically tracked.
264
+ *
265
+ * @param resolveFn - A function that resolves a raw user-ID string to a
266
+ * {@link TLUser} or `null`. Called reactively inside a `computed`.
267
+ * @returns A function suitable for use as `TLUserStore.resolve`.
268
+ *
269
+ * @example
270
+ * ```ts
271
+ * const users: TLUserStore = {
272
+ * currentUser: currentUserSignal,
273
+ * resolve: createCachedUserResolve(
274
+ * (userId) => usersAtom.get()[createUserId(userId)] ?? null
275
+ * ),
276
+ * }
277
+ * ```
278
+ *
279
+ * @public
280
+ */
281
+ export function createCachedUserResolve(
282
+ resolveFn: (userId: string) => TLUser | null
283
+ ): (userId: string) => Signal<TLUser | null> {
284
+ const cache = new Map<string, Signal<TLUser | null>>()
285
+ return (userId: string) => {
286
+ let signal = cache.get(userId)
287
+ if (!signal) {
288
+ signal = computed('resolve-user-' + userId, () => resolveFn(userId))
289
+ cache.set(userId, signal)
290
+ }
291
+ return signal
292
+ }
293
+ }
294
+
203
295
  /**
204
296
  * Configuration properties for a tldraw store, defining its behavior and integrations.
205
297
  * These props are passed when creating a new store instance.
@@ -228,6 +320,8 @@ export interface TLStoreProps {
228
320
  defaultName: string
229
321
  /** Asset store implementation for handling file uploads and storage */
230
322
  assets: Required<TLAssetStore>
323
+ /** User store implementation for user resolution and attribution */
324
+ users: Required<TLUserStore>
231
325
  /**
232
326
  * Called when an {@link @tldraw/editor#Editor} connected to this store is mounted.
233
327
  * Can optionally return a cleanup function that will be called when unmounted.
@@ -1,21 +1,21 @@
1
- import { atom } from '@tldraw/state'
1
+ import { atom, computed } from '@tldraw/state'
2
2
  import { Store } from '@tldraw/store'
3
3
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4
4
  import {
5
5
  createPresenceStateDerivation,
6
6
  getDefaultUserPresence,
7
- TLPresenceUserInfo,
8
7
  } from './createPresenceStateDerivation'
9
8
  import { createTLSchema } from './createTLSchema'
10
9
  import { TLINSTANCE_ID } from './records/TLInstance'
11
10
  import { TLPageId } from './records/TLPage'
12
11
  import { InstancePageStateRecordType } from './records/TLPageState'
13
12
  import { TLRecord } from './records/TLRecord'
13
+ import { createUserId, TLUser, UserRecordType } from './records/TLUser'
14
14
  import { createIntegrityChecker, TLStoreProps } from './TLStore'
15
15
 
16
16
  describe('createPresenceStateDerivation', () => {
17
17
  let store: Store<TLRecord, TLStoreProps>
18
- let userSignal: ReturnType<typeof atom<TLPresenceUserInfo>>
18
+ let userSignal: ReturnType<typeof atom<TLUser>>
19
19
 
20
20
  beforeEach(() => {
21
21
  const schema = createTLSchema()
@@ -28,6 +28,10 @@ describe('createPresenceStateDerivation', () => {
28
28
  resolve: vi.fn().mockResolvedValue('resolved-url'),
29
29
  remove: vi.fn().mockResolvedValue(undefined),
30
30
  },
31
+ users: {
32
+ currentUser: computed('currentUser', () => null),
33
+ resolve: () => computed('resolve', () => null),
34
+ },
31
35
  onMount: vi.fn(),
32
36
  },
33
37
  })
@@ -36,11 +40,14 @@ describe('createPresenceStateDerivation', () => {
36
40
  const checker = createIntegrityChecker(store)
37
41
  checker()
38
42
 
39
- userSignal = atom('user', {
40
- id: 'user123',
41
- name: 'Test User',
42
- color: '#007AFF',
43
- })
43
+ userSignal = atom(
44
+ 'user',
45
+ UserRecordType.create({
46
+ id: createUserId('user123'),
47
+ name: 'Test User',
48
+ color: '#007AFF',
49
+ })
50
+ )
44
51
  })
45
52
 
46
53
  afterEach(() => {
@@ -53,16 +60,18 @@ describe('createPresenceStateDerivation', () => {
53
60
  const presenceSignal = derivation(store)
54
61
  const presence = presenceSignal.get()
55
62
 
56
- expect(presence!.userId).toBe('user123')
63
+ expect(presence!.userId).toBe('user:user123')
57
64
  expect(presence!.userName).toBe('Test User')
58
65
  expect(presence!.color).toBe('#007AFF')
59
66
 
60
67
  // Update user signal
61
- userSignal.set({
62
- id: 'user456',
63
- name: 'Updated User',
64
- color: '#FF6B6B',
65
- })
68
+ userSignal.set(
69
+ UserRecordType.create({
70
+ id: createUserId('user456'),
71
+ name: 'Updated User',
72
+ color: '#FF6B6B',
73
+ })
74
+ )
66
75
 
67
76
  const updatedPresence = presenceSignal.get()
68
77
  expect(updatedPresence!.userName).toBe('Updated User')
@@ -111,6 +120,10 @@ describe('getDefaultUserPresence', () => {
111
120
  resolve: vi.fn().mockResolvedValue('resolved-url'),
112
121
  remove: vi.fn().mockResolvedValue(undefined),
113
122
  },
123
+ users: {
124
+ currentUser: computed('currentUser', () => null),
125
+ resolve: () => computed('resolve', () => null),
126
+ },
114
127
  onMount: vi.fn(),
115
128
  },
116
129
  })
@@ -126,21 +139,21 @@ describe('getDefaultUserPresence', () => {
126
139
 
127
140
  describe('basic functionality', () => {
128
141
  it('should return presence state with user info and default values', () => {
129
- const user: TLPresenceUserInfo = {
130
- id: 'test-user',
142
+ const user = UserRecordType.create({
143
+ id: createUserId('test-user'),
131
144
  name: 'Test User',
132
145
  color: '#00FF00',
133
- }
146
+ })
134
147
 
135
148
  const presence = getDefaultUserPresence(store, user)
136
149
 
137
- expect(presence!.userId).toBe('test-user')
150
+ expect(presence!.userId).toBe('user:test-user')
138
151
  expect(presence!.userName).toBe('Test User')
139
152
  expect(presence!.color).toBe('#00FF00')
140
153
  })
141
154
 
142
155
  it('should use defaults for missing user fields', () => {
143
- const minimalUser: TLPresenceUserInfo = { id: 'minimal' }
156
+ const minimalUser = UserRecordType.create({ id: createUserId('minimal') })
144
157
  const presence = getDefaultUserPresence(store, minimalUser)
145
158
 
146
159
  expect(presence!.userName).toBe('')
@@ -151,7 +164,7 @@ describe('getDefaultUserPresence', () => {
151
164
  describe('error conditions', () => {
152
165
  it('should return null when required records are missing', () => {
153
166
  store.remove([TLINSTANCE_ID])
154
- const user: TLPresenceUserInfo = { id: 'test' }
167
+ const user = UserRecordType.create({ id: createUserId('test') })
155
168
  expect(getDefaultUserPresence(store, user)).toBe(null)
156
169
  })
157
170
  })
@@ -4,25 +4,18 @@ import { TLINSTANCE_ID } from './records/TLInstance'
4
4
  import { InstancePageStateRecordType } from './records/TLPageState'
5
5
  import { TLPOINTER_ID } from './records/TLPointer'
6
6
  import { InstancePresenceRecordType, TLInstancePresence } from './records/TLPresence'
7
+ import { TLUser } from './records/TLUser'
7
8
  import { TLStore } from './TLStore'
8
9
 
9
- /**
10
- * The information about a user which is used for multiplayer features.
11
- * @public
12
- */
13
- export interface TLPresenceUserInfo {
14
- /**
15
- * id - A unique identifier for the user. This should be the same across all devices and sessions.
16
- */
17
- id: string
18
- /**
19
- * The user's display name.
20
- */
21
- name?: string | null
10
+ /** @public */
11
+ export interface CreatePresenceStateDerivationOpts {
12
+ /** Custom instance ID. If not provided, one is generated from the store ID. */
13
+ instanceId?: TLInstancePresence['id']
22
14
  /**
23
- * The user's color. If not given, a random color will be assigned.
15
+ * Override how presence state is built from the store and current user.
16
+ * Defaults to {@link getDefaultUserPresence}.
24
17
  */
25
- color?: string | null
18
+ getUserPresence?(store: TLStore, user: TLUser): TLPresenceStateInfo | null
26
19
  }
27
20
 
28
21
  /**
@@ -33,8 +26,8 @@ export interface TLPresenceUserInfo {
33
26
  * position, selected shapes, camera position, and user metadata that gets synchronized in
34
27
  * multiplayer scenarios.
35
28
  *
36
- * @param $user - A reactive signal containing the user information
37
- * @param instanceId - Optional custom instance ID. If not provided, one will be generated based on the store ID
29
+ * @param $user - A reactive signal containing the user information, or `null` when anonymous
30
+ * @param opts - Optional configuration for instance ID and presence derivation
38
31
  * @returns A function that takes a store and returns a computed signal of the user's presence state
39
32
  *
40
33
  * @example
@@ -42,7 +35,7 @@ export interface TLPresenceUserInfo {
42
35
  * import { createPresenceStateDerivation } from '@tldraw/tlschema'
43
36
  * import { atom } from '@tldraw/state'
44
37
  *
45
- * const userSignal = atom('user', { id: 'user-123', name: 'Alice', color: '#ff0000' })
38
+ * const userSignal = atom('user', { id: 'user-123', name: 'Alice', color: '#ff0000', meta: {} })
46
39
  * const presenceDerivation = createPresenceStateDerivation(userSignal)
47
40
  *
48
41
  * // Use with a store to get reactive presence state
@@ -53,15 +46,17 @@ export interface TLPresenceUserInfo {
53
46
  * @public
54
47
  */
55
48
  export function createPresenceStateDerivation(
56
- $user: Signal<TLPresenceUserInfo>,
57
- instanceId?: TLInstancePresence['id']
49
+ $user: Signal<TLUser | null>,
50
+ opts?: CreatePresenceStateDerivationOpts
58
51
  ) {
52
+ const { instanceId, getUserPresence: _getUserPresence } = opts ?? {}
53
+ const getUserPresence = _getUserPresence ?? getDefaultUserPresence
59
54
  return (store: TLStore): Signal<TLInstancePresence | null> => {
60
55
  return computed('instancePresence', () => {
61
56
  const user = $user.get()
62
57
  if (!user) return null
63
58
 
64
- const state = getDefaultUserPresence(store, user)
59
+ const state = getUserPresence(store, user)
65
60
  if (!state) return null
66
61
 
67
62
  return InstancePresenceRecordType.create({
@@ -98,7 +93,7 @@ export type TLPresenceStateInfo = Parameters<(typeof InstancePresenceRecordType)
98
93
  * ```ts
99
94
  * import { getDefaultUserPresence } from '@tldraw/tlschema'
100
95
  *
101
- * const user = { id: 'user-123', name: 'Alice', color: '#ff0000' }
96
+ * const user = { id: 'user-123', name: 'Alice', color: '#ff0000', meta: {} }
102
97
  * const presenceInfo = getDefaultUserPresence(store, user)
103
98
  *
104
99
  * if (presenceInfo) {
@@ -122,7 +117,7 @@ export type TLPresenceStateInfo = Parameters<(typeof InstancePresenceRecordType)
122
117
  *
123
118
  * @public
124
119
  */
125
- export function getDefaultUserPresence(store: TLStore, user: TLPresenceUserInfo) {
120
+ export function getDefaultUserPresence(store: TLStore, user: TLUser) {
126
121
  const instance = store.get(TLINSTANCE_ID)
127
122
  const pageState = store.get(InstancePageStateRecordType.createId(instance?.currentPageId))
128
123
  const camera = store.get(CameraRecordType.createId(instance?.currentPageId))
@@ -136,14 +131,14 @@ export function getDefaultUserPresence(store: TLStore, user: TLPresenceUserInfo)
136
131
  brush: instance.brush,
137
132
  scribbles: instance.scribbles,
138
133
  userId: user.id,
139
- userName: user.name ?? '',
134
+ userName: user.name,
140
135
  followingUserId: instance.followingUserId,
141
136
  camera: {
142
137
  x: camera.x,
143
138
  y: camera.y,
144
139
  z: camera.z,
145
140
  },
146
- color: user.color ?? '#FF0000',
141
+ color: user.color || '#FF0000',
147
142
  currentPageId: instance.currentPageId,
148
143
  cursor: {
149
144
  x: pointer.x,
@@ -1,5 +1,6 @@
1
1
  import { LegacyMigrations, MigrationSequence, StoreSchema, StoreValidator } from '@tldraw/store'
2
2
  import { objectMapValues } from '@tldraw/utils'
3
+ import { T } from '@tldraw/validate'
3
4
  import { bookmarkAssetMigrations } from './assets/TLBookmarkAsset'
4
5
  import { imageAssetMigrations } from './assets/TLImageAsset'
5
6
  import { videoAssetMigrations } from './assets/TLVideoAsset'
@@ -26,6 +27,7 @@ import {
26
27
  getShapePropKeysByStyle,
27
28
  rootShapeMigrations,
28
29
  } from './records/TLShape'
30
+ import { UserRecordType, createUserRecordType, userMigrations } from './records/TLUser'
29
31
  import { RecordProps, TLPropsMigrations, processPropsMigrations } from './recordsWithProps'
30
32
  import { arrowShapeMigrations, arrowShapeProps } from './shapes/TLArrowShape'
31
33
  import { TLBaseShape } from './shapes/TLBaseShape'
@@ -193,6 +195,38 @@ export const defaultBindingSchemas = {
193
195
  arrow: { migrations: arrowBindingMigrations, props: arrowBindingProps },
194
196
  } satisfies { [T in TLDefaultBinding['type']]: SchemaPropsInfo }
195
197
 
198
+ /**
199
+ * Configuration for extending the user record type with custom metadata
200
+ * validators and migration sequences.
201
+ *
202
+ * @example
203
+ * ```ts
204
+ * import { T } from '@tldraw/validate'
205
+ *
206
+ * const userSchema: UserSchemaInfo = {
207
+ * meta: {
208
+ * isAdmin: T.boolean,
209
+ * department: T.string,
210
+ * },
211
+ * }
212
+ * ```
213
+ *
214
+ * @public
215
+ */
216
+ export interface UserSchemaInfo {
217
+ /**
218
+ * Validators for custom metadata fields on user records. Each field is
219
+ * treated as optional — user records without these fields remain valid,
220
+ * but when present, values are validated against the provided validators.
221
+ */
222
+ meta?: Record<string, T.Validatable<any>>
223
+
224
+ /**
225
+ * Additional migration sequences for evolving custom user data over time.
226
+ */
227
+ migrations?: readonly MigrationSequence[]
228
+ }
229
+
196
230
  /**
197
231
  * Creates a complete TLSchema for use with tldraw stores. This schema defines the structure,
198
232
  * validation, and migration sequences for all record types in a tldraw application.
@@ -204,6 +238,7 @@ export const defaultBindingSchemas = {
204
238
  * @param options - Configuration options for the schema
205
239
  * - shapes - Shape schema configurations. Defaults to defaultShapeSchemas if not provided
206
240
  * - bindings - Binding schema configurations. Defaults to defaultBindingSchemas if not provided
241
+ * - user - Custom user record configuration with meta validators and migrations
207
242
  * - records - Custom record type configurations. These are additional record types beyond
208
243
  * the built-in shapes, bindings, assets, etc.
209
244
  * - migrations - Additional migration sequences to include in the schema
@@ -229,6 +264,16 @@ export const defaultBindingSchemas = {
229
264
  * },
230
265
  * })
231
266
  *
267
+ * // Create schema with custom user metadata
268
+ * const schemaWithCustomUser = createTLSchema({
269
+ * user: {
270
+ * meta: {
271
+ * isAdmin: T.boolean,
272
+ * department: T.string,
273
+ * },
274
+ * },
275
+ * })
276
+ *
232
277
  * // Create schema with custom record types
233
278
  * const schemaWithCustomRecords = createTLSchema({
234
279
  * records: {
@@ -256,11 +301,13 @@ export const defaultBindingSchemas = {
256
301
  export function createTLSchema({
257
302
  shapes = defaultShapeSchemas,
258
303
  bindings = defaultBindingSchemas,
304
+ user,
259
305
  records = {},
260
306
  migrations,
261
307
  }: {
262
308
  shapes?: Record<string, SchemaPropsInfo>
263
309
  bindings?: Record<string, SchemaPropsInfo>
310
+ user?: UserSchemaInfo
264
311
  records?: Record<string, CustomRecordInfo>
265
312
  migrations?: readonly MigrationSequence[]
266
313
  } = {}): TLSchema {
@@ -277,6 +324,7 @@ export function createTLSchema({
277
324
  const ShapeRecordType = createShapeRecordType(shapes)
278
325
  const BindingRecordType = createBindingRecordType(bindings)
279
326
  const InstanceRecordType = createInstanceRecordType(stylesById)
327
+ const CustomUserRecordType = user ? createUserRecordType(user) : UserRecordType
280
328
 
281
329
  // Create RecordTypes for custom records
282
330
  const builtInTypeNames = new Set([
@@ -291,6 +339,7 @@ export function createTLSchema({
291
339
  'pointer',
292
340
  'shape',
293
341
  'store',
342
+ 'user',
294
343
  ])
295
344
  const customRecordTypes: Record<string, { createId: any }> = {}
296
345
  for (const [typeName, config] of Object.entries(records)) {
@@ -314,6 +363,7 @@ export function createTLSchema({
314
363
  instance_presence: InstancePresenceRecordType,
315
364
  pointer: PointerRecordType,
316
365
  shape: ShapeRecordType,
366
+ user: CustomUserRecordType,
317
367
  ...customRecordTypes,
318
368
  },
319
369
  {
@@ -329,6 +379,7 @@ export function createTLSchema({
329
379
  pointerMigrations,
330
380
  rootShapeMigrations,
331
381
 
382
+ userMigrations,
332
383
  bookmarkAssetMigrations,
333
384
  imageAssetMigrations,
334
385
  videoAssetMigrations,
@@ -337,6 +388,7 @@ export function createTLSchema({
337
388
  ...processPropsMigrations<TLBinding>('binding', bindings),
338
389
  ...processCustomRecordMigrations(records),
339
390
 
391
+ ...(user?.migrations ?? []),
340
392
  ...(migrations ?? []),
341
393
  ],
342
394
  onValidationFailure,
package/src/index.ts CHANGED
@@ -51,8 +51,8 @@ export {
51
51
  export {
52
52
  createPresenceStateDerivation,
53
53
  getDefaultUserPresence,
54
+ type CreatePresenceStateDerivationOpts,
54
55
  type TLPresenceStateInfo,
55
- type TLPresenceUserInfo,
56
56
  } from './createPresenceStateDerivation'
57
57
  export {
58
58
  createTLSchema,
@@ -60,6 +60,7 @@ export {
60
60
  defaultShapeSchemas,
61
61
  type SchemaPropsInfo,
62
62
  type TLSchema,
63
+ type UserSchemaInfo,
63
64
  } from './createTLSchema'
64
65
  export {
65
66
  boxModelValidator,
@@ -173,6 +174,15 @@ export {
173
174
  type TLShapePartial,
174
175
  type TLUnknownShape,
175
176
  } from './records/TLShape'
177
+ export {
178
+ createUserId,
179
+ createUserRecordType,
180
+ isUserId,
181
+ userIdValidator,
182
+ UserRecordType,
183
+ type TLUser,
184
+ type TLUserId,
185
+ } from './records/TLUser'
176
186
  export {
177
187
  type RecordProps,
178
188
  type RecordPropsType,
@@ -308,6 +318,7 @@ export {
308
318
  type TLDefaultVerticalAlignStyle,
309
319
  } from './styles/TLVerticalAlignStyle'
310
320
  export {
321
+ createCachedUserResolve,
311
322
  type TLAssetContext,
312
323
  type TLAssetStore,
313
324
  type TLSerializedStore,
@@ -315,6 +326,7 @@ export {
315
326
  type TLStoreProps,
316
327
  type TLStoreSchema,
317
328
  type TLStoreSnapshot,
329
+ type TLUserStore,
318
330
  } from './TLStore'
319
331
  export {
320
332
  getDefaultTranslationLocale,