@tldraw/tlschema 5.3.0-canary.fceaae5e9feb → 5.3.0-internal.1640468db8fd

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/shapes/TLGeoShape.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 { StyleProp } from '../styles/StyleProp'\nimport {\n\tDefaultColorStyle,\n\tDefaultLabelColorStyle,\n\tTLDefaultColorStyle,\n} from '../styles/TLColorStyle'\nimport { DefaultDashStyle, TLDefaultDashStyle } from '../styles/TLDashStyle'\nimport { DefaultFillStyle, TLDefaultFillStyle } from '../styles/TLFillStyle'\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 * Style property defining the geometric shape type for geo shapes.\n * Provides a variety of built-in geometric forms including basic shapes,\n * polygons, arrows, and special shapes.\n *\n * @public\n * @example\n * ```ts\n * // Use in shape props\n * const props = {\n * geo: 'rectangle', // or 'ellipse', 'triangle', etc.\n * // other properties...\n * }\n * ```\n */\nexport const GeoShapeGeoStyle = StyleProp.defineEnum('tldraw:geo', {\n\tdefaultValue: 'rectangle',\n\tvalues: [\n\t\t'cloud',\n\t\t'rectangle',\n\t\t'ellipse',\n\t\t'triangle',\n\t\t'diamond',\n\t\t'pentagon',\n\t\t'hexagon',\n\t\t'octagon',\n\t\t'star',\n\t\t'rhombus',\n\t\t'rhombus-2',\n\t\t'oval',\n\t\t'trapezoid',\n\t\t'arrow-right',\n\t\t'arrow-left',\n\t\t'arrow-up',\n\t\t'arrow-down',\n\t\t'x-box',\n\t\t'check-box',\n\t\t'heart',\n\t],\n})\n\n/**\n * Type representing valid geometric shape styles for geo shapes.\n *\n * @public\n */\nexport type TLGeoShapeGeoStyle = T.TypeOf<typeof GeoShapeGeoStyle>\n\n/**\n * Properties for the geo shape, which renders various geometric forms with styling and text.\n *\n * @public\n */\nexport interface TLGeoShapeProps {\n\t/** Geometric shape type (rectangle, ellipse, triangle, etc.) */\n\tgeo: TLGeoShapeGeoStyle\n\t/** Dash pattern style for the shape outline */\n\tdash: TLDefaultDashStyle\n\t/** URL link associated with the shape */\n\turl: string\n\t/** Width of the shape in pixels */\n\tw: number\n\t/** Height of the shape in pixels */\n\th: number\n\t/** Additional vertical growth for text content */\n\tgrowY: number\n\t/** Scale factor applied to the shape */\n\tscale: number\n\n\t/** Color style for text label */\n\tlabelColor: TLDefaultColorStyle\n\t/** Color style for the shape outline */\n\tcolor: TLDefaultColorStyle\n\t/** Fill style for the shape interior */\n\tfill: TLDefaultFillStyle\n\t/** Size/thickness style for outline and text */\n\tsize: TLDefaultSizeStyle\n\t/** Font style for text content */\n\tfont: TLDefaultFontStyle\n\t/** Horizontal alignment for text content */\n\talign: TLDefaultHorizontalAlignStyle\n\t/** Vertical alignment for text content */\n\tverticalAlign: TLDefaultVerticalAlignStyle\n\t/** Rich text content displayed within the shape */\n\trichText: TLRichText\n}\n\n/**\n * A geo shape represents geometric forms like rectangles, ellipses, triangles, and other\n * predefined shapes. Geo shapes support styling, text content, and can act as containers.\n *\n * @public\n * @example\n * ```ts\n * const geoShape: TLGeoShape = {\n * id: createShapeId(),\n * typeName: 'shape',\n * type: 'geo',\n * x: 100,\n * y: 100,\n * rotation: 0,\n * index: 'a1',\n * parentId: 'page:page1',\n * isLocked: false,\n * opacity: 1,\n * props: {\n * geo: 'rectangle',\n * w: 200,\n * h: 100,\n * color: 'black',\n * fill: 'solid',\n * dash: 'solid',\n * size: 'm',\n * font: 'draw',\n * align: 'middle',\n * verticalAlign: 'middle',\n * richText: toRichText('Hello World'),\n * labelColor: 'black',\n * url: '',\n * growY: 0,\n * scale: 1\n * },\n * meta: {}\n * }\n * ```\n */\nexport type TLGeoShape = TLBaseShape<'geo', TLGeoShapeProps>\n\n/**\n * Validation schema for geo shape properties.\n *\n * @public\n * @example\n * ```ts\n * // Validate geo shape properties\n * const isValidGeo = geoShapeProps.geo.isValid('rectangle')\n * const isValidSize = geoShapeProps.w.isValid(100)\n * const isValidText = geoShapeProps.richText.isValid(toRichText('Hello'))\n * ```\n */\nexport const geoShapeProps: RecordProps<TLGeoShape> = {\n\tgeo: GeoShapeGeoStyle,\n\tdash: DefaultDashStyle,\n\turl: T.linkUrl,\n\tw: T.nonZeroNumber,\n\th: T.nonZeroNumber,\n\tgrowY: T.positiveNumber,\n\tscale: T.nonZeroNumber,\n\n\t// Text properties\n\tlabelColor: DefaultLabelColorStyle,\n\tcolor: DefaultColorStyle,\n\tfill: DefaultFillStyle,\n\tsize: DefaultSizeStyle,\n\tfont: DefaultFontStyle,\n\talign: DefaultHorizontalAlignStyle,\n\tverticalAlign: DefaultVerticalAlignStyle,\n\trichText: richTextValidator,\n}\n\nconst geoShapeVersions = createShapePropsMigrationIds('geo', {\n\tAddUrlProp: 1,\n\tAddLabelColor: 2,\n\tRemoveJustify: 3,\n\tAddCheckBox: 4,\n\tAddVerticalAlign: 5,\n\tMigrateLegacyAlign: 6,\n\tAddCloud: 7,\n\tMakeUrlsValid: 8,\n\tAddScale: 9,\n\tAddRichText: 10,\n\tAddRichTextAttrs: 11,\n})\n\n/**\n * Version identifiers for geo shape migrations.\n *\n * @public\n */\nexport { geoShapeVersions }\n\n/**\n * Migration sequence for geo shape properties across different schema versions.\n * Handles evolution of geo shapes including URL support, label colors, alignment changes,\n * the transition from plain text to rich text, and support for attrs property on richText.\n *\n * @public\n */\nexport const geoShapeMigrations = createShapePropsMigrationSequence({\n\tsequence: [\n\t\t{\n\t\t\tid: geoShapeVersions.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: geoShapeVersions.AddLabelColor,\n\t\t\tup: (props) => {\n\t\t\t\tprops.labelColor = 'black'\n\t\t\t},\n\t\t\tdown: 'retired',\n\t\t},\n\t\t{\n\t\t\tid: geoShapeVersions.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: geoShapeVersions.AddCheckBox,\n\t\t\tup: (_props) => {\n\t\t\t\t// noop\n\t\t\t},\n\t\t\tdown: 'retired',\n\t\t},\n\t\t{\n\t\t\tid: geoShapeVersions.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: geoShapeVersions.MigrateLegacyAlign,\n\t\t\tup: (props) => {\n\t\t\t\tlet newAlign: TLDefaultHorizontalAlignStyle\n\t\t\t\tswitch (props.align) {\n\t\t\t\t\tcase 'start':\n\t\t\t\t\t\tnewAlign = 'start-legacy'\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase 'end':\n\t\t\t\t\t\tnewAlign = 'end-legacy'\n\t\t\t\t\t\tbreak\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tnewAlign = 'middle-legacy'\n\t\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tprops.align = newAlign\n\t\t\t},\n\t\t\tdown: 'retired',\n\t\t},\n\t\t{\n\t\t\tid: geoShapeVersions.AddCloud,\n\t\t\tup: (_props) => {\n\t\t\t\t// noop\n\t\t\t},\n\t\t\tdown: 'retired',\n\t\t},\n\t\t{\n\t\t\tid: geoShapeVersions.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: geoShapeVersions.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: geoShapeVersions.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: geoShapeVersions.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 { StyleProp } from '../styles/StyleProp'\nimport {\n\tDefaultColorStyle,\n\tDefaultLabelColorStyle,\n\tTLDefaultColorStyle,\n} from '../styles/TLColorStyle'\nimport { DefaultDashStyle, TLDefaultDashStyle } from '../styles/TLDashStyle'\nimport { DefaultFillStyle, TLDefaultFillStyle } from '../styles/TLFillStyle'\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 * Style property defining the geometric shape type for geo shapes.\n * Provides a variety of built-in geometric forms including basic shapes,\n * polygons, arrows, and special shapes.\n *\n * @public\n * @example\n * ```ts\n * // Use in shape props\n * const props = {\n * geo: 'rectangle', // or 'ellipse', 'triangle', etc.\n * // other properties...\n * }\n * ```\n */\nexport const GeoShapeGeoStyle = StyleProp.defineEnum('tldraw:geo', {\n\tdefaultValue: 'rectangle',\n\tvalues: [\n\t\t'cloud',\n\t\t'rectangle',\n\t\t'ellipse',\n\t\t'triangle',\n\t\t'diamond',\n\t\t'pentagon',\n\t\t'hexagon',\n\t\t'octagon',\n\t\t'star',\n\t\t'rhombus',\n\t\t'rhombus-2',\n\t\t'oval',\n\t\t'trapezoid',\n\t\t'arrow-right',\n\t\t'arrow-left',\n\t\t'arrow-up',\n\t\t'arrow-down',\n\t\t'x-box',\n\t\t'check-box',\n\t\t'heart',\n\t],\n})\n\n/**\n * Type representing valid geometric shape styles for geo shapes.\n *\n * @public\n */\nexport type TLGeoShapeGeoStyle = T.TypeOf<typeof GeoShapeGeoStyle>\n\n/**\n * Properties for the geo shape, which renders various geometric forms with styling and text.\n *\n * @public\n */\nexport interface TLGeoShapeProps {\n\t/** Geometric shape type (rectangle, ellipse, triangle, etc.) */\n\tgeo: TLGeoShapeGeoStyle\n\t/** Dash pattern style for the shape outline */\n\tdash: TLDefaultDashStyle\n\t/** URL link associated with the shape */\n\turl: string\n\t/** Width of the shape in pixels */\n\tw: number\n\t/** Height of the shape in pixels */\n\th: number\n\t/** Additional vertical growth for text content */\n\tgrowY: number\n\t/** Scale factor applied to the shape */\n\tscale: number\n\t/** Whether to mirror the shape horizontally */\n\tflipX: boolean\n\t/** Whether to mirror the shape vertically */\n\tflipY: boolean\n\n\t/** Color style for text label */\n\tlabelColor: TLDefaultColorStyle\n\t/** Color style for the shape outline */\n\tcolor: TLDefaultColorStyle\n\t/** Fill style for the shape interior */\n\tfill: TLDefaultFillStyle\n\t/** Size/thickness style for outline and text */\n\tsize: TLDefaultSizeStyle\n\t/** Font style for text content */\n\tfont: TLDefaultFontStyle\n\t/** Horizontal alignment for text content */\n\talign: TLDefaultHorizontalAlignStyle\n\t/** Vertical alignment for text content */\n\tverticalAlign: TLDefaultVerticalAlignStyle\n\t/** Rich text content displayed within the shape */\n\trichText: TLRichText\n}\n\n/**\n * A geo shape represents geometric forms like rectangles, ellipses, triangles, and other\n * predefined shapes. Geo shapes support styling, text content, and can act as containers.\n *\n * @public\n * @example\n * ```ts\n * const geoShape: TLGeoShape = {\n * id: createShapeId(),\n * typeName: 'shape',\n * type: 'geo',\n * x: 100,\n * y: 100,\n * rotation: 0,\n * index: 'a1',\n * parentId: 'page:page1',\n * isLocked: false,\n * opacity: 1,\n * props: {\n * geo: 'rectangle',\n * w: 200,\n * h: 100,\n * color: 'black',\n * fill: 'solid',\n * dash: 'solid',\n * size: 'm',\n * font: 'draw',\n * align: 'middle',\n * verticalAlign: 'middle',\n * richText: toRichText('Hello World'),\n * labelColor: 'black',\n * url: '',\n * growY: 0,\n * scale: 1,\n * flipX: false,\n * flipY: false\n * },\n * meta: {}\n * }\n * ```\n */\nexport type TLGeoShape = TLBaseShape<'geo', TLGeoShapeProps>\n\n/**\n * Validation schema for geo shape properties.\n *\n * @public\n * @example\n * ```ts\n * // Validate geo shape properties\n * const isValidGeo = geoShapeProps.geo.isValid('rectangle')\n * const isValidSize = geoShapeProps.w.isValid(100)\n * const isValidText = geoShapeProps.richText.isValid(toRichText('Hello'))\n * ```\n */\nexport const geoShapeProps: RecordProps<TLGeoShape> = {\n\tgeo: GeoShapeGeoStyle,\n\tdash: DefaultDashStyle,\n\turl: T.linkUrl,\n\tw: T.nonZeroNumber,\n\th: T.nonZeroNumber,\n\tgrowY: T.positiveNumber,\n\tscale: T.nonZeroNumber,\n\tflipX: T.boolean,\n\tflipY: T.boolean,\n\n\t// Text properties\n\tlabelColor: DefaultLabelColorStyle,\n\tcolor: DefaultColorStyle,\n\tfill: DefaultFillStyle,\n\tsize: DefaultSizeStyle,\n\tfont: DefaultFontStyle,\n\talign: DefaultHorizontalAlignStyle,\n\tverticalAlign: DefaultVerticalAlignStyle,\n\trichText: richTextValidator,\n}\n\nconst geoShapeVersions = createShapePropsMigrationIds('geo', {\n\tAddUrlProp: 1,\n\tAddLabelColor: 2,\n\tRemoveJustify: 3,\n\tAddCheckBox: 4,\n\tAddVerticalAlign: 5,\n\tMigrateLegacyAlign: 6,\n\tAddCloud: 7,\n\tMakeUrlsValid: 8,\n\tAddScale: 9,\n\tAddRichText: 10,\n\tAddRichTextAttrs: 11,\n\tAddFlipProps: 12,\n})\n\n/**\n * Version identifiers for geo shape migrations.\n *\n * @public\n */\nexport { geoShapeVersions }\n\n/**\n * Migration sequence for geo shape properties across different schema versions.\n * Handles evolution of geo shapes including URL support, label colors, alignment changes,\n * the transition from plain text to rich text, and support for attrs property on richText.\n *\n * @public\n */\nexport const geoShapeMigrations = createShapePropsMigrationSequence({\n\tsequence: [\n\t\t{\n\t\t\tid: geoShapeVersions.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: geoShapeVersions.AddLabelColor,\n\t\t\tup: (props) => {\n\t\t\t\tprops.labelColor = 'black'\n\t\t\t},\n\t\t\tdown: 'retired',\n\t\t},\n\t\t{\n\t\t\tid: geoShapeVersions.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: geoShapeVersions.AddCheckBox,\n\t\t\tup: (_props) => {\n\t\t\t\t// noop\n\t\t\t},\n\t\t\tdown: 'retired',\n\t\t},\n\t\t{\n\t\t\tid: geoShapeVersions.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: geoShapeVersions.MigrateLegacyAlign,\n\t\t\tup: (props) => {\n\t\t\t\tlet newAlign: TLDefaultHorizontalAlignStyle\n\t\t\t\tswitch (props.align) {\n\t\t\t\t\tcase 'start':\n\t\t\t\t\t\tnewAlign = 'start-legacy'\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase 'end':\n\t\t\t\t\t\tnewAlign = 'end-legacy'\n\t\t\t\t\t\tbreak\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tnewAlign = 'middle-legacy'\n\t\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tprops.align = newAlign\n\t\t\t},\n\t\t\tdown: 'retired',\n\t\t},\n\t\t{\n\t\t\tid: geoShapeVersions.AddCloud,\n\t\t\tup: (_props) => {\n\t\t\t\t// noop\n\t\t\t},\n\t\t\tdown: 'retired',\n\t\t},\n\t\t{\n\t\t\tid: geoShapeVersions.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: geoShapeVersions.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: geoShapeVersions.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: geoShapeVersions.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: geoShapeVersions.AddFlipProps,\n\t\t\tup: (props) => {\n\t\t\t\tprops.flipX = false\n\t\t\t\tprops.flipY = false\n\t\t\t},\n\t\t\tdown: (props) => {\n\t\t\t\tdelete props.flipX\n\t\t\t\tdelete props.flipY\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": "5.3.0-canary.fceaae5e9feb",
4
+ "version": "5.3.0-internal.1640468db8fd",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -51,10 +51,10 @@
51
51
  "vitest": "^4.1.7"
52
52
  },
53
53
  "dependencies": {
54
- "@tldraw/state": "5.3.0-canary.fceaae5e9feb",
55
- "@tldraw/store": "5.3.0-canary.fceaae5e9feb",
56
- "@tldraw/utils": "5.3.0-canary.fceaae5e9feb",
57
- "@tldraw/validate": "5.3.0-canary.fceaae5e9feb"
54
+ "@tldraw/state": "5.3.0-internal.1640468db8fd",
55
+ "@tldraw/store": "5.3.0-internal.1640468db8fd",
56
+ "@tldraw/utils": "5.3.0-internal.1640468db8fd",
57
+ "@tldraw/validate": "5.3.0-internal.1640468db8fd"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "react": "^18.2.0 || ^19.2.1",
package/src/TLStore.ts CHANGED
@@ -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 { isShapeId } from './records/TLShape'
18
19
  import { TLUser } from './records/TLUser'
19
20
 
20
21
  /**
@@ -462,6 +463,7 @@ function getDefaultPages() {
462
463
  export function createIntegrityChecker(store: Store<TLRecord, TLStoreProps>): () => void {
463
464
  const $pageIds = store.query.ids('page')
464
465
  const $pageStates = store.query.records('instance_page_state')
466
+ const $shapes = store.query.records('shape')
465
467
 
466
468
  const ensureStoreIsUsable = (): void => {
467
469
  // make sure we have exactly one document
@@ -531,6 +533,20 @@ export function createIntegrityChecker(store: Store<TLRecord, TLStoreProps>): ()
531
533
  store.put([...missingCameraIds].map((id) => CameraRecordType.create({ id })))
532
534
  }
533
535
 
536
+ // remove any shape whose parent is another shape that no longer exists. This can happen
537
+ // when a change is undone that references a shape (e.g. a frame) which has since been
538
+ // deleted by another collaborator: the undo would otherwise leave a shape parented to a
539
+ // record that doesn't exist. Such a shape has no valid place in the hierarchy, so we
540
+ // discard it. Removing a shape can orphan its own children, which the re-run then handles.
541
+ const orphanedShapeIds = $shapes
542
+ .get()
543
+ .filter((shape) => isShapeId(shape.parentId) && !store.has(shape.parentId))
544
+ .map((shape) => shape.id)
545
+ if (orphanedShapeIds.length > 0) {
546
+ store.remove(orphanedShapeIds)
547
+ return ensureStoreIsUsable()
548
+ }
549
+
534
550
  const pageStates = $pageStates.get()
535
551
  for (const pageState of pageStates) {
536
552
  if (!pageIds.has(pageState.pageId)) {
package/src/index.ts CHANGED
@@ -116,6 +116,22 @@ export {
116
116
  type TLUnknownBinding,
117
117
  } from './records/TLBinding'
118
118
  export { CameraRecordType, type TLCamera, type TLCameraId } from './records/TLCamera'
119
+ export {
120
+ commentRecordConfig,
121
+ commentSchemaRecords,
122
+ commentThreadRecordConfig,
123
+ createComment,
124
+ createCommentId,
125
+ createCommentThread,
126
+ createCommentThreadId,
127
+ isCommentId,
128
+ isCommentThreadId,
129
+ type TLComment,
130
+ type TLCommentAnchor,
131
+ type TLCommentId,
132
+ type TLCommentThread,
133
+ type TLCommentThreadId,
134
+ } from './records/TLComment'
119
135
  export {
120
136
  createCustomRecordId,
121
137
  createCustomRecordMigrationIds,
@@ -2152,6 +2152,18 @@ describe('Add flipX, flipY to image shape', () => {
2152
2152
  })
2153
2153
  })
2154
2154
 
2155
+ describe('Add flipX, flipY to geo shape', () => {
2156
+ const { up, down } = getTestMigration(geoShapeVersions.AddFlipProps)
2157
+
2158
+ test('up works as expected', () => {
2159
+ expect(up({ props: {} })).toEqual({ props: { flipX: false, flipY: false } })
2160
+ })
2161
+
2162
+ test('down works as expected', () => {
2163
+ expect(down({ props: { flipX: false, flipY: false } })).toEqual({ props: {} })
2164
+ })
2165
+ })
2166
+
2155
2167
  describe('Add alt text to image shape', () => {
2156
2168
  const { up, down } = getTestMigration(imageShapeVersions.AddAltText)
2157
2169
 
@@ -27,6 +27,7 @@ export const TL_CURSOR_TYPES = new Set([
27
27
  'default',
28
28
  'pointer',
29
29
  'cross',
30
+ 'comment',
30
31
  'grab',
31
32
  'rotate',
32
33
  'grabbing',
@@ -0,0 +1,182 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createTLSchema } from '../createTLSchema'
3
+ import { toRichText } from '../misc/TLRichText'
4
+ import {
5
+ commentRecordConfig,
6
+ commentSchemaRecords,
7
+ commentThreadRecordConfig,
8
+ createComment,
9
+ createCommentId,
10
+ createCommentThread,
11
+ createCommentThreadId,
12
+ isCommentId,
13
+ isCommentThreadId,
14
+ TLCommentAnchor,
15
+ } from './TLComment'
16
+ import { TLPageId } from './TLPage'
17
+ import { TLShapeId } from './TLShape'
18
+
19
+ const pageId = 'page:page1' as TLPageId
20
+ const shapeId = 'shape:box1' as TLShapeId
21
+
22
+ const anchors: TLCommentAnchor[] = [
23
+ { type: 'shape', shapeId, x: 1, y: 0, isPrecise: false },
24
+ { type: 'point', x: 100, y: 200 },
25
+ { type: 'region', x: 0, y: 0, w: 300, h: 150 },
26
+ { type: 'page' },
27
+ { type: 'text-range', shapeId, from: 3, to: 12 },
28
+ ]
29
+
30
+ describe('TLCommentThread', () => {
31
+ it.each(anchors)('creates a valid thread anchored to %j', (anchor) => {
32
+ const thread = createCommentThread({ pageId, anchor, createdBy: 'user1', now: 1000 })
33
+ expect(commentThreadRecordConfig.validator.validate(thread)).toEqual(thread)
34
+ expect(thread).toMatchObject({
35
+ typeName: 'comment-thread',
36
+ pageId,
37
+ anchor,
38
+ createdAt: 1000,
39
+ resolved: null,
40
+ meta: {},
41
+ })
42
+ })
43
+
44
+ it('validates resolution state', () => {
45
+ const thread = createCommentThread({
46
+ pageId,
47
+ anchor: { type: 'page' },
48
+ createdBy: 'user1',
49
+ now: 1000,
50
+ })
51
+ const resolved = { ...thread, resolved: { at: 2000, by: 'user2' } }
52
+ expect(commentThreadRecordConfig.validator.validate(resolved)).toEqual(resolved)
53
+ expect(() =>
54
+ commentThreadRecordConfig.validator.validate({ ...thread, resolved: { at: 2000 } })
55
+ ).toThrow()
56
+ })
57
+
58
+ it('rejects unknown anchor kinds and malformed anchors', () => {
59
+ const thread = createCommentThread({
60
+ pageId,
61
+ anchor: { type: 'shape', shapeId, x: 1, y: 0, isPrecise: false },
62
+ createdBy: 'user1',
63
+ now: 1000,
64
+ })
65
+ expect(() =>
66
+ commentThreadRecordConfig.validator.validate({
67
+ ...thread,
68
+ anchor: { type: 'volume', level: 11 },
69
+ })
70
+ ).toThrow()
71
+ expect(() =>
72
+ commentThreadRecordConfig.validator.validate({
73
+ ...thread,
74
+ // shape anchors must reference a shape id
75
+ anchor: { type: 'shape', shapeId: 'page:nope', x: 1, y: 0, isPrecise: false },
76
+ })
77
+ ).toThrow()
78
+ })
79
+ })
80
+
81
+ describe('TLComment', () => {
82
+ const threadId = createCommentThreadId('thread1')
83
+
84
+ it('creates a valid comment with a rich text body', () => {
85
+ const comment = createComment({
86
+ threadId,
87
+ pageId,
88
+ authorId: 'user1',
89
+ body: toRichText('hello'),
90
+ now: 1000,
91
+ })
92
+ expect(commentRecordConfig.validator.validate(comment)).toEqual(comment)
93
+ expect(comment).toMatchObject({
94
+ typeName: 'comment',
95
+ threadId,
96
+ pageId,
97
+ createdAt: 1000,
98
+ editedAt: null,
99
+ })
100
+ })
101
+
102
+ it('accepts an editedAt timestamp after edits', () => {
103
+ const comment = createComment({
104
+ threadId,
105
+ pageId,
106
+ authorId: 'user1',
107
+ body: toRichText('hello'),
108
+ now: 1000,
109
+ })
110
+ const edited = { ...comment, editedAt: 2000, body: toRichText('hello, edited') }
111
+ expect(commentRecordConfig.validator.validate(edited)).toEqual(edited)
112
+ })
113
+
114
+ it('rejects a threadId that is not a comment-thread id', () => {
115
+ const comment = createComment({
116
+ threadId,
117
+ pageId,
118
+ authorId: 'user1',
119
+ body: toRichText('hello'),
120
+ now: 1000,
121
+ })
122
+ expect(() =>
123
+ commentRecordConfig.validator.validate({ ...comment, threadId: 'comment:not-a-thread' })
124
+ ).toThrow()
125
+ })
126
+ })
127
+
128
+ describe('schema registration', () => {
129
+ it('registers both record types via commentSchemaRecords', () => {
130
+ const schema = createTLSchema({ records: commentSchemaRecords })
131
+ // custom record types aren't part of the default TLRecord union, so index untyped
132
+ const types = schema.types as Record<string, { scope: string } | undefined>
133
+ expect(types['comment-thread']?.scope).toBe('document')
134
+ expect(types['comment']?.scope).toBe('document')
135
+ })
136
+ })
137
+
138
+ describe('isCommentThreadId / isCommentId', () => {
139
+ it('isCommentThreadId accepts comment-thread ids and rejects everything else', () => {
140
+ expect(isCommentThreadId(createCommentThreadId())).toBe(true)
141
+ expect(isCommentThreadId(createCommentId())).toBe(false)
142
+ expect(isCommentThreadId('shape:box1')).toBe(false)
143
+ expect(isCommentThreadId('not-an-id')).toBe(false)
144
+ })
145
+
146
+ it('isCommentId accepts comment ids and rejects everything else, including comment-thread ids', () => {
147
+ expect(isCommentId(createCommentId())).toBe(true)
148
+ expect(isCommentId(createCommentThreadId())).toBe(false)
149
+ expect(isCommentId('shape:box1')).toBe(false)
150
+ expect(isCommentId('not-an-id')).toBe(false)
151
+ })
152
+ })
153
+
154
+ describe('comment-thread migrations', () => {
155
+ const migration = (commentThreadRecordConfig.migrations as any).sequence.find(
156
+ (m: any) => m.id === 'com.tldraw.comment-thread/2'
157
+ ) as { up(r: any): any; down(r: any): any }
158
+
159
+ it('stamps existing shape anchors with the top-right position (x:1, y:0)', () => {
160
+ expect(migration.up({ anchor: { type: 'shape', shapeId } })).toEqual({
161
+ anchor: { type: 'shape', shapeId, x: 1, y: 0, isPrecise: false },
162
+ })
163
+ })
164
+
165
+ it('leaves shape anchors that already have x/y unchanged', () => {
166
+ const anchor = { type: 'shape', shapeId, x: 0.25, y: 0.75, isPrecise: true }
167
+ expect(migration.up({ anchor })).toEqual({ anchor })
168
+ })
169
+
170
+ it('leaves non-shape anchors untouched', () => {
171
+ const anchor = { type: 'point', x: 5, y: 6 }
172
+ expect(migration.up({ anchor })).toEqual({ anchor })
173
+ })
174
+
175
+ it('down-migrates by dropping x/y', () => {
176
+ expect(
177
+ migration.down({ anchor: { type: 'shape', shapeId, x: 0.3, y: 0.4, isPrecise: true } })
178
+ ).toEqual({
179
+ anchor: { type: 'shape', shapeId },
180
+ })
181
+ })
182
+ })