@tldraw/tlschema 5.3.0-internal.fba91ed55f6c → 5.3.0-next.2fa9c61a8de6
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.
- package/dist-cjs/index.d.ts +115 -11
- package/dist-cjs/index.js +5 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/misc/TLCursor.js +1 -0
- package/dist-cjs/misc/TLCursor.js.map +2 -2
- package/dist-cjs/records/TLComment.js +68 -8
- package/dist-cjs/records/TLComment.js.map +2 -2
- package/dist-cjs/shapes/TLGeoShape.js +15 -1
- package/dist-cjs/shapes/TLGeoShape.js.map +2 -2
- package/dist-esm/index.d.mts +115 -11
- package/dist-esm/index.mjs +9 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/misc/TLCursor.mjs +1 -0
- package/dist-esm/misc/TLCursor.mjs.map +2 -2
- package/dist-esm/records/TLComment.mjs +68 -8
- package/dist-esm/records/TLComment.mjs.map +2 -2
- package/dist-esm/shapes/TLGeoShape.mjs +15 -1
- package/dist-esm/shapes/TLGeoShape.mjs.map +2 -2
- package/package.json +5 -5
- package/src/index.ts +6 -0
- package/src/migrations.test.ts +12 -0
- package/src/misc/TLCursor.ts +1 -0
- package/src/records/TLComment.test.ts +74 -1
- package/src/records/TLComment.ts +182 -14
- package/src/shapes/TLGeoShape.ts +21 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/records/TLComment.ts"],
|
|
4
|
-
"sourcesContent": ["import { BaseRecord, RecordId, createRecordMigrationSequence } from '@tldraw/store'\nimport { JsonObject } from '@tldraw/utils'\nimport { T } from '@tldraw/validate'\nimport { idValidator } from '../misc/id-validator'\nimport { richTextValidator, TLRichText } from '../misc/TLRichText'\nimport { CustomRecordInfo, createCustomRecordId, isCustomRecordId } from './TLCustomRecord'\nimport { TLPageId } from './TLPage'\nimport { TLShapeId } from './TLShape'\n\n/**\n * Where a comment thread is anchored on the canvas. Modeled as a discriminated union so new\n * anchor kinds can be added without breaking existing threads:\n *\n * - `shape` \u2014 pinned to a shape. `x`/`y` are normalized (0\u20131) within the shape's page bounds, so\n * the pin keeps its spot as the shape moves and resizes. `isPrecise` mirrors arrow bindings: when\n * true the pin sits at exactly `x`/`y`; when false (the default) it sits at a consumer-defined\n * spot (top-right out of the box), and `x`/`y` are the remembered precise position\n * - `point` \u2014 pinned to a fixed point on the page, in page coordinates\n * - `region` \u2014 pinned to a rectangular area of the page, in page coordinates\n * - `page` \u2014 a page-level thread with no spatial anchor\n * - `text-range` \u2014 pinned to a character range inside a shape's text\n *\n * @public\n */\nexport type TLCommentAnchor =\n\t| { type: 'shape'; shapeId: TLShapeId; x: number; y: number; isPrecise: boolean }\n\t| { type: 'point'; x: number; y: number }\n\t| { type: 'region'; x: number; y: number; w: number; h: number }\n\t| { type: 'page' }\n\t| { type: 'text-range'; shapeId: TLShapeId; from: number; to: number }\n\n/**\n * A comment thread. The thread owns the anchor (where the conversation lives on the canvas) and\n * the resolution state; the messages themselves are `TLComment` records pointing at the thread\n * via `threadId`, ordered by `createdAt`. v1 threads are flat \u2014 no nested replies.\n *\n * Threads and comments are document records, but they're intended to be served through the sync\n * server's object-store lane: gated by the session's `objectAccess` rather than `isReadonly`\n * (so \"can comment but not edit\" is expressible), excluded from document snapshots and `.tldr`\n * exports server-side, and persisted in a separate lane from the main document.\n *\n * Opt-in: register with `createTLSchema({ records: commentSchemaRecords })` on the server and\n * the matching `records` option on the client \u2014 neither type is part of the default schema, and\n * both sides must register them identically.\n *\n * @public\n */\nexport interface TLCommentThread extends BaseRecord<'comment-thread', TLCommentThreadId> {\n\t/** The page the thread lives on. */\n\tpageId: TLPageId\n\t/** Where the thread is anchored on that page. */\n\tanchor: TLCommentAnchor\n\t/**\n\t * Who started the thread. Client-supplied for now; the sync server should stamp/verify this\n\t * from the session's authenticated identity once record-level authorization lands (the same\n\t * model presence already uses for self-ownership).\n\t */\n\tcreatedBy: string\n\tcreatedAt: number\n\t/** Resolution state: when and by whom the thread was resolved, or null while open. */\n\tresolved: { at: number; by: string } | null\n\tmeta: JsonObject\n}\n\n/** @public */\nexport type TLCommentThreadId = RecordId<TLCommentThread>\n\n/**\n * A single comment message within a thread. See `TLCommentThread` for the overall model and\n * sync/registration notes.\n *\n * Comment mutations are deliberately not undoable \u2014 create/edit them with\n * `{ history: 'ignore' }` to avoid multiplayer surprises like a comment reappearing after\n * someone else deleted it.\n *\n * @public\n */\nexport interface TLComment extends BaseRecord<'comment', TLCommentId> {\n\t/** The thread this comment belongs to. */\n\tthreadId: TLCommentThreadId\n\t/** Denormalized from the thread so per-page queries don't need a join. */\n\tpageId: TLPageId\n\t/** See `TLCommentThread.createdBy` \u2014 same server-stamping caveat applies. */\n\tauthorId: string\n\tcreatedAt: number\n\t/** Null until the comment is first edited. */\n\teditedAt: number | null\n\t/** Rich text body. Use `toRichText(...)` for plaintext input. */\n\tbody: TLRichText\n\tmeta: JsonObject\n}\n\n/** @public */\nexport type TLCommentId = RecordId<TLComment>\n\nconst commentAnchorValidator: T.Validator<TLCommentAnchor> = T.union('type', {\n\tshape: T.object({\n\t\ttype: T.literal('shape'),\n\t\tshapeId: idValidator<TLShapeId>('shape'),\n\t\tx: T.number,\n\t\ty: T.number,\n\t\tisPrecise: T.boolean,\n\t}),\n\tpoint: T.object({\n\t\ttype: T.literal('point'),\n\t\tx: T.number,\n\t\ty: T.number,\n\t}),\n\tregion: T.object({\n\t\ttype: T.literal('region'),\n\t\tx: T.number,\n\t\ty: T.number,\n\t\tw: T.number,\n\t\th: T.number,\n\t}),\n\tpage: T.object({\n\t\ttype: T.literal('page'),\n\t}),\n\t'text-range': T.object({\n\t\ttype: T.literal('text-range'),\n\t\tshapeId: idValidator<TLShapeId>('shape'),\n\t\tfrom: T.number,\n\t\tto: T.number,\n\t}),\n})\n\n/**\n * Guard migrations for the comment record types. Each sequence is retroactive and starts with an\n * identity migration that has no `down`: a sync server whose schema registers the comment types\n * cannot down-migrate records for a session whose schema predates them, so such sessions are\n * rejected with CLIENT_TOO_OLD (prompting a refresh) instead of being sent record types their\n * store cannot represent.\n */\nfunction createCommentGuardMigrations(\n\ttypeName: 'comment' | 'comment-thread',\n\textra: Parameters<typeof createRecordMigrationSequence>[0]['sequence'] = []\n) {\n\treturn createRecordMigrationSequence({\n\t\tsequenceId: `com.tldraw.${typeName}`,\n\t\trecordType: typeName,\n\t\tretroactive: true,\n\t\tsequence: [\n\t\t\t{\n\t\t\t\tid: `com.tldraw.${typeName}/1`,\n\t\t\t\tup: (record) => record,\n\t\t\t},\n\t\t\t...extra,\n\t\t],\n\t})\n}\n\n/**\n * Config for registering the `comment-thread` record type in a tldraw schema. Pass via\n * `commentSchemaRecords`; see `TLCommentThread`.\n *\n * @public\n */\nexport const commentThreadRecordConfig: CustomRecordInfo = {\n\tscope: 'document',\n\tmigrations: createCommentGuardMigrations('comment-thread', [\n\t\t{\n\t\t\t// Shape anchors gained normalized x/y + isPrecise; existing ones were imprecise (top-right).\n\t\t\tid: 'com.tldraw.comment-thread/2',\n\t\t\tup: (record) => {\n\t\t\t\tconst anchor = (record as any).anchor\n\t\t\t\tif (anchor?.type === 'shape' && anchor.x === undefined) {\n\t\t\t\t\t;(record as any).anchor = { ...anchor, x: 1, y: 0, isPrecise: false }\n\t\t\t\t}\n\t\t\t\treturn record\n\t\t\t},\n\t\t\tdown: (record) => {\n\t\t\t\tconst anchor = (record as any).anchor\n\t\t\t\tif (anchor?.type === 'shape') {\n\t\t\t\t\t;(record as any).anchor = { type: 'shape', shapeId: anchor.shapeId }\n\t\t\t\t}\n\t\t\t\treturn record\n\t\t\t},\n\t\t},\n\t]),\n\tvalidator: T.object({\n\t\tid: idValidator<TLCommentThreadId>('comment-thread'),\n\t\ttypeName: T.literal('comment-thread'),\n\t\tpageId: idValidator<TLPageId>('page'),\n\t\tanchor: commentAnchorValidator,\n\t\tcreatedBy: T.string,\n\t\tcreatedAt: T.number,\n\t\tresolved: T.object({ at: T.number, by: T.string }).nullable(),\n\t\tmeta: T.jsonValue,\n\t}),\n}\n\n/**\n * Config for registering the `comment` record type in a tldraw schema. Pass via\n * `commentSchemaRecords`; see `TLComment`.\n *\n * @public\n */\nexport const commentRecordConfig: CustomRecordInfo = {\n\tscope: 'document',\n\tmigrations: createCommentGuardMigrations('comment'),\n\tvalidator: T.object({\n\t\tid: idValidator<TLCommentId>('comment'),\n\t\ttypeName: T.literal('comment'),\n\t\tthreadId: idValidator<TLCommentThreadId>('comment-thread'),\n\t\tpageId: idValidator<TLPageId>('page'),\n\t\tauthorId: T.string,\n\t\tcreatedAt: T.number,\n\t\teditedAt: T.number.nullable(),\n\t\tbody: richTextValidator,\n\t\tmeta: T.jsonValue,\n\t}),\n}\n\n/**\n * The `records` map to pass to `createTLSchema` / the client `records` option so comment\n * threads and comments sync. Register both types together \u2014 a comment without its thread type\n * (or vice versa) will fail schema validation on one side of the connection.\n *\n * @public\n */\nexport const commentSchemaRecords = {\n\t'comment-thread': commentThreadRecordConfig,\n\tcomment: commentRecordConfig,\n}\n\n/** @public */\nexport function createCommentThreadId(id?: string): TLCommentThreadId {\n\treturn createCustomRecordId('comment-thread', id) as TLCommentThreadId\n}\n\n/** @public */\nexport function createCommentId(id?: string): TLCommentId {\n\treturn createCustomRecordId('comment', id) as TLCommentId\n}\n\n/**\n * Type guard for `TLCommentThreadId`. Note `isCommentId` does not accept these: the prefixes\n * are `comment-thread:` vs `comment:`, and the character after `comment` differs (`-` vs `:`),\n * so the two never overlap.\n *\n * @public\n */\nexport function isCommentThreadId(id: string): id is TLCommentThreadId {\n\treturn isCustomRecordId('comment-thread', id)\n}\n\n/**\n * Type guard for `TLCommentId`. See `isCommentThreadId` for why `comment-thread:...` ids are\n * correctly rejected here.\n *\n * @public\n */\nexport function isCommentId(id: string): id is TLCommentId {\n\treturn isCustomRecordId('comment', id)\n}\n\n/**\n * Create a new comment thread record. Pair with `createComment` for the thread's first message.\n *\n * @public\n */\nexport function createCommentThread(props: {\n\tpageId: TLPageId\n\tanchor: TLCommentAnchor\n\tcreatedBy: string\n\tnow?: number\n\tmeta?: JsonObject\n}): TLCommentThread {\n\treturn {\n\t\tid: createCommentThreadId(),\n\t\ttypeName: 'comment-thread',\n\t\tpageId: props.pageId,\n\t\tanchor: props.anchor,\n\t\tcreatedBy: props.createdBy,\n\t\tcreatedAt: props.now ?? Date.now(),\n\t\tresolved: null,\n\t\tmeta: props.meta ?? {},\n\t}\n}\n\n/**\n * Create a new comment record within a thread.\n *\n * @public\n */\nexport function createComment(props: {\n\tthreadId: TLCommentThreadId\n\tpageId: TLPageId\n\tauthorId: string\n\tbody: TLRichText\n\tnow?: number\n\tmeta?: JsonObject\n}): TLComment {\n\treturn {\n\t\tid: createCommentId(),\n\t\ttypeName: 'comment',\n\t\tthreadId: props.threadId,\n\t\tpageId: props.pageId,\n\t\tauthorId: props.authorId,\n\t\tcreatedAt: props.now ?? Date.now(),\n\t\teditedAt: null,\n\t\tbody: props.body,\n\t\tmeta: props.meta ?? {},\n\t}\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAA+B,qCAAqC;AAEpE,SAAS,SAAS;AAClB,SAAS,mBAAmB;AAC5B,SAAS,yBAAqC;AAC9C,SAA2B,sBAAsB,wBAAwB;
|
|
4
|
+
"sourcesContent": ["import { BaseRecord, RecordId, createRecordMigrationSequence } from '@tldraw/store'\nimport { JsonObject } from '@tldraw/utils'\nimport { T } from '@tldraw/validate'\nimport { idValidator } from '../misc/id-validator'\nimport { richTextValidator, TLRichText } from '../misc/TLRichText'\nimport { CustomRecordInfo, createCustomRecordId, isCustomRecordId } from './TLCustomRecord'\nimport { TLPageId } from './TLPage'\nimport { TLShapeId } from './TLShape'\n\n/**\n * Where a comment thread is anchored on the canvas. Modeled as a discriminated union so new\n * anchor kinds can be added without breaking existing threads:\n *\n * - `shape` \u2014 pinned to a shape. `x`/`y` are normalized (0\u20131) within the shape's own bounds and\n * resolved through the shape's page transform, so the pin keeps its spot as the shape moves,\n * resizes, and rotates. `isPrecise` mirrors arrow bindings: when\n * true the pin sits at exactly `x`/`y`; when false (the default) it sits at a consumer-defined\n * spot (top-right out of the box), and `x`/`y` are the remembered precise position\n * - `point` \u2014 pinned to a fixed point on the page, in page coordinates\n * - `region` \u2014 pinned to a rectangular area of the page, in page coordinates\n * - `page` \u2014 a page-level thread with no spatial anchor\n *\n * @public\n */\nexport type TLCommentAnchor =\n\t| { type: 'shape'; shapeId: TLShapeId; x: number; y: number; isPrecise: boolean }\n\t| { type: 'point'; x: number; y: number }\n\t| {\n\t\t\ttype: 'region'\n\t\t\tx: number\n\t\t\ty: number\n\t\t\tw: number\n\t\t\th: number\n\t\t\t/** The normalized (0\u20131) corner the pin sits on \u2014 where the creating drag was released.\n\t\t\t * Absent on older records; consumers fall back to a corner of their own choosing. */\n\t\t\tpinX?: number\n\t\t\tpinY?: number\n\t }\n\t| { type: 'page' }\n\n/**\n * A comment thread. The thread owns the anchor (where the conversation lives on the canvas) and\n * the resolution state; the messages themselves are `TLComment` records pointing at the thread\n * via `threadId`, ordered by `createdAt`. v1 threads are flat \u2014 no nested replies.\n *\n * Threads and comments are document records, but they're intended to be served through the sync\n * server's object-store lane: gated by the session's `objectAccess` rather than `isReadonly`\n * (so \"can comment but not edit\" is expressible), excluded from document snapshots and `.tldr`\n * exports server-side, and persisted in a separate lane from the main document.\n *\n * Opt-in: register with `createTLSchema({ records: commentSchemaRecords })` on the server and\n * the matching `records` option on the client \u2014 neither type is part of the default schema, and\n * both sides must register them identically.\n *\n * @public\n */\nexport interface TLCommentThread extends BaseRecord<'comment-thread', TLCommentThreadId> {\n\t/** The page the thread lives on. */\n\tpageId: TLPageId\n\t/** Where the thread is anchored on that page. */\n\tanchor: TLCommentAnchor\n\t/**\n\t * Who started the thread. Client-supplied for now; the sync server should stamp/verify this\n\t * from the session's authenticated identity once record-level authorization lands (the same\n\t * model presence already uses for self-ownership).\n\t */\n\tcreatedBy: string\n\tcreatedAt: number\n\t/** Resolution state: when and by whom the thread was resolved, or null while open. */\n\tresolved: { at: number; by: string } | null\n\t/**\n\t * Whether the thread is soft-deleted. Clients delete a thread by setting this flag and\n\t * leaving the record (and its comments) in place, hidden from rendering. Sync servers are\n\t * expected to enforce the flag as write-once and creator-only, reject hard deletes from\n\t * clients, and drop flagged threads from future room loads.\n\t */\n\tisDeleted: boolean\n\tmeta: JsonObject\n}\n\n/** @public */\nexport type TLCommentThreadId = RecordId<TLCommentThread>\n\n/**\n * A single comment message within a thread. See `TLCommentThread` for the overall model and\n * sync/registration notes.\n *\n * Comment mutations are deliberately not undoable \u2014 create/edit them with\n * `{ history: 'ignore' }` to avoid multiplayer surprises like a comment reappearing after\n * someone else deleted it.\n *\n * @public\n */\nexport interface TLComment extends BaseRecord<'comment', TLCommentId> {\n\t/** The thread this comment belongs to. */\n\tthreadId: TLCommentThreadId\n\t/** Denormalized from the thread so per-page queries don't need a join. */\n\tpageId: TLPageId\n\t/** See `TLCommentThread.createdBy` \u2014 same server-stamping caveat applies. */\n\tauthorId: string\n\tcreatedAt: number\n\t/** Null until the comment is first edited. */\n\teditedAt: number | null\n\t/** Rich text body. Use `toRichText(...)` for plaintext input. */\n\tbody: TLRichText\n\t/**\n\t * Whether the comment is soft-deleted. Same model as `TLCommentThread.isDeleted` \u2014 clients\n\t * delete a comment by setting this flag and leaving the record in place, hidden from\n\t * rendering. Sync servers are expected to enforce the flag as write-once and author-only,\n\t * reject hard deletes from clients, and drop flagged comments from future room loads.\n\t */\n\tisDeleted: boolean\n\tmeta: JsonObject\n}\n\n/** @public */\nexport type TLCommentId = RecordId<TLComment>\n\n/**\n * One person's emoji reaction to one comment.\n *\n * A reaction is its own record rather than a field on the comment, for the same reason read\n * receipts are their own row: it's one person's data about someone else's comment. Comment records\n * are owner-only for updates, so a reaction could never be written onto them; and holding\n * everyone's reactions in one shared field would mean each write carried the whole set, so two\n * people reacting at once would race to overwrite each other. A record per person keeps every\n * write to its own record, which is what lets concurrent reactions coexist.\n *\n * A user holds at most one record per (comment, emoji) pair \u2014 so one user can react with several\n * different emoji to the same comment. That's enforced by the id: see `createCommentReactionId`.\n * Restricting a user to a single reaction overall is a client-side policy layered on top (see the\n * commenting package's `reactionMode`), not a property of this record.\n *\n * @public\n */\nexport interface TLCommentReaction extends BaseRecord<'comment-reaction', TLCommentReactionId> {\n\t/** The comment being reacted to. */\n\tcommentId: TLCommentId\n\t/** Denormalized from the comment, so a thread's reactions can be found without a join. */\n\tthreadId: TLCommentThreadId\n\t/** Denormalized from the comment \u2014 see `TLComment.pageId`. */\n\tpageId: TLPageId\n\t/** Who reacted. See `TLCommentThread.createdBy` \u2014 same server-stamping caveat applies. */\n\tuserId: string\n\t/** The emoji itself, as a string (e.g. `'\uD83D\uDC4D'`), not a shortcode. */\n\temoji: string\n\tcreatedAt: number\n\tmeta: JsonObject\n}\n\n/** @public */\nexport type TLCommentReactionId = RecordId<TLCommentReaction>\n\n/**\n * An emoji shortcode or literal. Bounded because reaction ids embed the emoji verbatim\n * (see {@link createCommentReactionId}), so an unbounded value means an unbounded record id.\n */\nconst emojiValidator = T.string.check((value) => {\n\tif (value.length === 0 || value.length > 64) {\n\t\tthrow new T.ValidationError(`Expected an emoji of 1-64 characters, got ${value.length}`)\n\t}\n})\n\nconst commentAnchorValidator: T.Validator<TLCommentAnchor> = T.union('type', {\n\tshape: T.object({\n\t\ttype: T.literal('shape'),\n\t\tshapeId: idValidator<TLShapeId>('shape'),\n\t\tx: T.number,\n\t\ty: T.number,\n\t\tisPrecise: T.boolean,\n\t}),\n\tpoint: T.object({\n\t\ttype: T.literal('point'),\n\t\tx: T.number,\n\t\ty: T.number,\n\t}),\n\tregion: T.object({\n\t\ttype: T.literal('region'),\n\t\tx: T.number,\n\t\ty: T.number,\n\t\tw: T.number,\n\t\th: T.number,\n\t\tpinX: T.number.optional(),\n\t\tpinY: T.number.optional(),\n\t}),\n\tpage: T.object({\n\t\ttype: T.literal('page'),\n\t}),\n})\n\n/**\n * Guard migrations for the comment record types. Each sequence is retroactive and starts with an\n * identity migration that has no `down`: a sync server whose schema registers the comment types\n * cannot down-migrate records for a session whose schema predates them, so such sessions are\n * rejected with CLIENT_TOO_OLD (prompting a refresh) instead of being sent record types their\n * store cannot represent.\n */\nfunction createCommentGuardMigrations(\n\ttypeName: 'comment' | 'comment-thread' | 'comment-reaction',\n\textra: Parameters<typeof createRecordMigrationSequence>[0]['sequence'] = []\n) {\n\treturn createRecordMigrationSequence({\n\t\tsequenceId: `com.tldraw.${typeName}`,\n\t\trecordType: typeName,\n\t\tretroactive: true,\n\t\tsequence: [\n\t\t\t{\n\t\t\t\tid: `com.tldraw.${typeName}/1`,\n\t\t\t\tup: (record) => record,\n\t\t\t},\n\t\t\t...extra,\n\t\t],\n\t})\n}\n\n/**\n * Config for registering the `comment-thread` record type in a tldraw schema. Pass via\n * `commentSchemaRecords`; see `TLCommentThread`.\n *\n * @public\n */\nexport const commentThreadRecordConfig: CustomRecordInfo = {\n\tscope: 'document',\n\tmigrations: createCommentGuardMigrations('comment-thread', [\n\t\t{\n\t\t\t// Shape anchors gained normalized x/y + isPrecise; existing ones were imprecise (top-right).\n\t\t\tid: 'com.tldraw.comment-thread/2',\n\t\t\tup: (record) => {\n\t\t\t\tconst anchor = (record as any).anchor\n\t\t\t\tif (anchor?.type === 'shape' && anchor.x === undefined) {\n\t\t\t\t\t;(record as any).anchor = { ...anchor, x: 1, y: 0, isPrecise: false }\n\t\t\t\t}\n\t\t\t\treturn record\n\t\t\t},\n\t\t\tdown: (record) => {\n\t\t\t\tconst anchor = (record as any).anchor\n\t\t\t\tif (anchor?.type === 'shape') {\n\t\t\t\t\t;(record as any).anchor = { type: 'shape', shapeId: anchor.shapeId }\n\t\t\t\t}\n\t\t\t\treturn record\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t// Region anchors gained an optional pin corner (where the creating drag released).\n\t\t\tid: 'com.tldraw.comment-thread/3',\n\t\t\tup: (record) => record,\n\t\t\tdown: (record) => {\n\t\t\t\tconst anchor = (record as any).anchor\n\t\t\t\tif (anchor?.type === 'region') {\n\t\t\t\t\tconst { pinX: _pinX, pinY: _pinY, ...rest } = anchor\n\t\t\t\t\t;(record as any).anchor = rest\n\t\t\t\t}\n\t\t\t\treturn record\n\t\t\t},\n\t\t},\n\t]),\n\tvalidator: T.object({\n\t\tid: idValidator<TLCommentThreadId>('comment-thread'),\n\t\ttypeName: T.literal('comment-thread'),\n\t\tpageId: idValidator<TLPageId>('page'),\n\t\tanchor: commentAnchorValidator,\n\t\tcreatedBy: T.string,\n\t\tcreatedAt: T.number,\n\t\tresolved: T.object({ at: T.number, by: T.string }).nullable(),\n\t\tisDeleted: T.boolean,\n\t\tmeta: T.jsonValue,\n\t}),\n}\n\n/**\n * Config for registering the `comment` record type in a tldraw schema. Pass via\n * `commentSchemaRecords`; see `TLComment`.\n *\n * @public\n */\nexport const commentRecordConfig: CustomRecordInfo = {\n\tscope: 'document',\n\tmigrations: createCommentGuardMigrations('comment'),\n\tvalidator: T.object({\n\t\tid: idValidator<TLCommentId>('comment'),\n\t\ttypeName: T.literal('comment'),\n\t\tthreadId: idValidator<TLCommentThreadId>('comment-thread'),\n\t\tpageId: idValidator<TLPageId>('page'),\n\t\tauthorId: T.string,\n\t\tcreatedAt: T.number,\n\t\teditedAt: T.number.nullable(),\n\t\tbody: richTextValidator,\n\t\tisDeleted: T.boolean,\n\t\tmeta: T.jsonValue,\n\t}),\n}\n\n/**\n * Config for registering the `comment-reaction` record type in a tldraw schema. Pass via\n * `commentSchemaRecords`; see `TLCommentReaction`.\n *\n * @public\n */\nexport const commentReactionRecordConfig: CustomRecordInfo = {\n\tscope: 'document',\n\tmigrations: createCommentGuardMigrations('comment-reaction'),\n\tvalidator: T.object({\n\t\tid: idValidator<TLCommentReactionId>('comment-reaction'),\n\t\ttypeName: T.literal('comment-reaction'),\n\t\tcommentId: idValidator<TLCommentId>('comment'),\n\t\tthreadId: idValidator<TLCommentThreadId>('comment-thread'),\n\t\tpageId: idValidator<TLPageId>('page'),\n\t\tuserId: T.string,\n\t\temoji: emojiValidator,\n\t\tcreatedAt: T.number,\n\t\tmeta: T.jsonValue,\n\t}),\n}\n\n/**\n * The `records` map to pass to `createTLSchema` / the client `records` option so comment\n * threads, comments, and reactions sync. Register the types together \u2014 one without the others\n * will fail schema validation on one side of the connection.\n *\n * @public\n */\nexport const commentSchemaRecords = {\n\t'comment-thread': commentThreadRecordConfig,\n\tcomment: commentRecordConfig,\n\t'comment-reaction': commentReactionRecordConfig,\n}\n\n/** @public */\nexport function createCommentThreadId(id?: string): TLCommentThreadId {\n\treturn createCustomRecordId('comment-thread', id) as TLCommentThreadId\n}\n\n/** @public */\nexport function createCommentId(id?: string): TLCommentId {\n\treturn createCustomRecordId('comment', id) as TLCommentId\n}\n\n/**\n * The id of one user's reaction to one comment, derived from the (comment, user, emoji) triple\n * rather than random. This is what makes reaction identity structural: the same triple always\n * addresses the same record, so re-picking an emoji toggles that one record and two tabs converge\n * instead of racing to create duplicates. A user can hold one record per emoji on a comment\n * (multiple reactions); enforcing \"at most one reaction total\" is a client concern layered on top,\n * not a property of the id. The sync authorizer leans on this derivation, so it must be injective.\n *\n * `emoji` here is the reaction's token \u2014 the emoji glyph for the default palette, or whatever\n * opaque string a consumer's palette uses. All three parts are URI-encoded before joining, so a\n * `:` in any of them can't shift the boundary and collapse two different triples onto one id. The\n * id is only ever recomputed for comparison, never parsed back, so the encoding needs no inverse.\n *\n * @public\n */\nexport function createCommentReactionId(\n\tcommentId: TLCommentId,\n\tuserId: string,\n\temoji: string\n): TLCommentReactionId {\n\treturn createCustomRecordId(\n\t\t'comment-reaction',\n\t\t`${encodeURIComponent(commentId)}:${encodeURIComponent(userId)}:${encodeURIComponent(emoji)}`\n\t) as TLCommentReactionId\n}\n\n/**\n * Type guard for `TLCommentThreadId`. Note `isCommentId` does not accept these: the prefixes\n * are `comment-thread:` vs `comment:`, and the character after `comment` differs (`-` vs `:`),\n * so the two never overlap.\n *\n * @public\n */\nexport function isCommentThreadId(id: string): id is TLCommentThreadId {\n\treturn isCustomRecordId('comment-thread', id)\n}\n\n/**\n * Type guard for `TLCommentId`. See `isCommentThreadId` for why `comment-thread:...` ids are\n * correctly rejected here.\n *\n * @public\n */\nexport function isCommentId(id: string): id is TLCommentId {\n\treturn isCustomRecordId('comment', id)\n}\n\n/**\n * Type guard for `TLCommentReactionId`. As with `isCommentThreadId`, the `comment-reaction:`\n * prefix never overlaps `comment:` \u2014 the character after `comment` differs (`-` vs `:`).\n *\n * @public\n */\nexport function isCommentReactionId(id: string): id is TLCommentReactionId {\n\treturn isCustomRecordId('comment-reaction', id)\n}\n\n/**\n * Create a new comment thread record. Pair with `createComment` for the thread's first message.\n *\n * @public\n */\nexport function createCommentThread(props: {\n\tpageId: TLPageId\n\tanchor: TLCommentAnchor\n\tcreatedBy: string\n\tnow?: number\n\tmeta?: JsonObject\n}): TLCommentThread {\n\treturn {\n\t\tid: createCommentThreadId(),\n\t\ttypeName: 'comment-thread',\n\t\tpageId: props.pageId,\n\t\tanchor: props.anchor,\n\t\tcreatedBy: props.createdBy,\n\t\tcreatedAt: props.now ?? Date.now(),\n\t\tresolved: null,\n\t\tisDeleted: false,\n\t\tmeta: props.meta ?? {},\n\t}\n}\n\n/**\n * Create a new comment record within a thread.\n *\n * @public\n */\nexport function createComment(props: {\n\tthreadId: TLCommentThreadId\n\tpageId: TLPageId\n\tauthorId: string\n\tbody: TLRichText\n\tnow?: number\n\tmeta?: JsonObject\n}): TLComment {\n\treturn {\n\t\tid: createCommentId(),\n\t\ttypeName: 'comment',\n\t\tthreadId: props.threadId,\n\t\tpageId: props.pageId,\n\t\tauthorId: props.authorId,\n\t\tcreatedAt: props.now ?? Date.now(),\n\t\teditedAt: null,\n\t\tbody: props.body,\n\t\tisDeleted: false,\n\t\tmeta: props.meta ?? {},\n\t}\n}\n\n/**\n * Create a reaction record for one user's reaction to one comment. The id is derived from the\n * comment and user (see `createCommentReactionId`), so re-reacting overwrites rather than adding.\n *\n * @public\n */\nexport function createCommentReaction(props: {\n\tcommentId: TLCommentId\n\tthreadId: TLCommentThreadId\n\tpageId: TLPageId\n\tuserId: string\n\temoji: string\n\tnow?: number\n\tmeta?: JsonObject\n}): TLCommentReaction {\n\treturn {\n\t\tid: createCommentReactionId(props.commentId, props.userId, props.emoji),\n\t\ttypeName: 'comment-reaction',\n\t\tcommentId: props.commentId,\n\t\tthreadId: props.threadId,\n\t\tpageId: props.pageId,\n\t\tuserId: props.userId,\n\t\temoji: props.emoji,\n\t\tcreatedAt: props.now ?? Date.now(),\n\t\tmeta: props.meta ?? {},\n\t}\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -49,6 +49,8 @@ const geoShapeProps = {
|
|
|
49
49
|
h: T.nonZeroNumber,
|
|
50
50
|
growY: T.positiveNumber,
|
|
51
51
|
scale: T.nonZeroNumber,
|
|
52
|
+
flipX: T.boolean,
|
|
53
|
+
flipY: T.boolean,
|
|
52
54
|
// Text properties
|
|
53
55
|
labelColor: DefaultLabelColorStyle,
|
|
54
56
|
color: DefaultColorStyle,
|
|
@@ -70,7 +72,8 @@ const geoShapeVersions = createShapePropsMigrationIds("geo", {
|
|
|
70
72
|
MakeUrlsValid: 8,
|
|
71
73
|
AddScale: 9,
|
|
72
74
|
AddRichText: 10,
|
|
73
|
-
AddRichTextAttrs: 11
|
|
75
|
+
AddRichTextAttrs: 11,
|
|
76
|
+
AddFlipProps: 12
|
|
74
77
|
});
|
|
75
78
|
const geoShapeMigrations = createShapePropsMigrationSequence({
|
|
76
79
|
sequence: [
|
|
@@ -174,6 +177,17 @@ const geoShapeMigrations = createShapePropsMigrationSequence({
|
|
|
174
177
|
delete props.richText.attrs;
|
|
175
178
|
}
|
|
176
179
|
}
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: geoShapeVersions.AddFlipProps,
|
|
183
|
+
up: (props) => {
|
|
184
|
+
props.flipX = false;
|
|
185
|
+
props.flipY = false;
|
|
186
|
+
},
|
|
187
|
+
down: (props) => {
|
|
188
|
+
delete props.flipX;
|
|
189
|
+
delete props.flipY;
|
|
190
|
+
}
|
|
177
191
|
}
|
|
178
192
|
]
|
|
179
193
|
});
|
|
@@ -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": "AAAA,SAAS,SAAS;AAClB,SAAqB,mBAAmB,kBAAkB;AAC1D,SAAS,8BAA8B,yCAAyC;AAEhF,SAAS,iBAAiB;AAC1B;AAAA,EACC;AAAA,EACA;AAAA,OAEM;AACP,SAAS,wBAA4C;AACrD,SAAS,wBAA4C;AACrD,SAAS,wBAA4C;AACrD;AAAA,EACC;AAAA,OAEM;AACP,SAAS,wBAA4C;AACrD;AAAA,EACC;AAAA,OAEM;AAkBA,MAAM,mBAAmB,UAAU,WAAW,cAAc;AAAA,EAClE,cAAc;AAAA,EACd,QAAQ;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACD;AACD,CAAC;
|
|
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-
|
|
4
|
+
"version": "5.3.0-next.2fa9c61a8de6",
|
|
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-
|
|
55
|
-
"@tldraw/store": "5.3.0-
|
|
56
|
-
"@tldraw/utils": "5.3.0-
|
|
57
|
-
"@tldraw/validate": "5.3.0-
|
|
54
|
+
"@tldraw/state": "5.3.0-next.2fa9c61a8de6",
|
|
55
|
+
"@tldraw/store": "5.3.0-next.2fa9c61a8de6",
|
|
56
|
+
"@tldraw/utils": "5.3.0-next.2fa9c61a8de6",
|
|
57
|
+
"@tldraw/validate": "5.3.0-next.2fa9c61a8de6"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"react": "^18.2.0 || ^19.2.1",
|
package/src/index.ts
CHANGED
|
@@ -117,18 +117,24 @@ export {
|
|
|
117
117
|
} from './records/TLBinding'
|
|
118
118
|
export { CameraRecordType, type TLCamera, type TLCameraId } from './records/TLCamera'
|
|
119
119
|
export {
|
|
120
|
+
commentReactionRecordConfig,
|
|
120
121
|
commentRecordConfig,
|
|
121
122
|
commentSchemaRecords,
|
|
122
123
|
commentThreadRecordConfig,
|
|
123
124
|
createComment,
|
|
124
125
|
createCommentId,
|
|
126
|
+
createCommentReaction,
|
|
127
|
+
createCommentReactionId,
|
|
125
128
|
createCommentThread,
|
|
126
129
|
createCommentThreadId,
|
|
127
130
|
isCommentId,
|
|
131
|
+
isCommentReactionId,
|
|
128
132
|
isCommentThreadId,
|
|
129
133
|
type TLComment,
|
|
130
134
|
type TLCommentAnchor,
|
|
131
135
|
type TLCommentId,
|
|
136
|
+
type TLCommentReaction,
|
|
137
|
+
type TLCommentReactionId,
|
|
132
138
|
type TLCommentThread,
|
|
133
139
|
type TLCommentThreadId,
|
|
134
140
|
} from './records/TLComment'
|
package/src/migrations.test.ts
CHANGED
|
@@ -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
|
|
package/src/misc/TLCursor.ts
CHANGED
|
@@ -2,14 +2,18 @@ import { describe, expect, it } from 'vitest'
|
|
|
2
2
|
import { createTLSchema } from '../createTLSchema'
|
|
3
3
|
import { toRichText } from '../misc/TLRichText'
|
|
4
4
|
import {
|
|
5
|
+
commentReactionRecordConfig,
|
|
5
6
|
commentRecordConfig,
|
|
6
7
|
commentSchemaRecords,
|
|
7
8
|
commentThreadRecordConfig,
|
|
8
9
|
createComment,
|
|
9
10
|
createCommentId,
|
|
11
|
+
createCommentReaction,
|
|
12
|
+
createCommentReactionId,
|
|
10
13
|
createCommentThread,
|
|
11
14
|
createCommentThreadId,
|
|
12
15
|
isCommentId,
|
|
16
|
+
isCommentReactionId,
|
|
13
17
|
isCommentThreadId,
|
|
14
18
|
TLCommentAnchor,
|
|
15
19
|
} from './TLComment'
|
|
@@ -24,7 +28,6 @@ const anchors: TLCommentAnchor[] = [
|
|
|
24
28
|
{ type: 'point', x: 100, y: 200 },
|
|
25
29
|
{ type: 'region', x: 0, y: 0, w: 300, h: 150 },
|
|
26
30
|
{ type: 'page' },
|
|
27
|
-
{ type: 'text-range', shapeId, from: 3, to: 12 },
|
|
28
31
|
]
|
|
29
32
|
|
|
30
33
|
describe('TLCommentThread', () => {
|
|
@@ -96,9 +99,23 @@ describe('TLComment', () => {
|
|
|
96
99
|
pageId,
|
|
97
100
|
createdAt: 1000,
|
|
98
101
|
editedAt: null,
|
|
102
|
+
isDeleted: false,
|
|
99
103
|
})
|
|
100
104
|
})
|
|
101
105
|
|
|
106
|
+
it('validates soft-deletion state', () => {
|
|
107
|
+
const comment = createComment({
|
|
108
|
+
threadId,
|
|
109
|
+
pageId,
|
|
110
|
+
authorId: 'user1',
|
|
111
|
+
body: toRichText('hello'),
|
|
112
|
+
now: 1000,
|
|
113
|
+
})
|
|
114
|
+
const deleted = { ...comment, isDeleted: true }
|
|
115
|
+
expect(commentRecordConfig.validator.validate(deleted)).toEqual(deleted)
|
|
116
|
+
expect(() => commentRecordConfig.validator.validate({ ...comment, isDeleted: 'yes' })).toThrow()
|
|
117
|
+
})
|
|
118
|
+
|
|
102
119
|
it('accepts an editedAt timestamp after edits', () => {
|
|
103
120
|
const comment = createComment({
|
|
104
121
|
threadId,
|
|
@@ -180,3 +197,59 @@ describe('comment-thread migrations', () => {
|
|
|
180
197
|
})
|
|
181
198
|
})
|
|
182
199
|
})
|
|
200
|
+
|
|
201
|
+
describe('comment-reaction record', () => {
|
|
202
|
+
it('createCommentReaction derives its id from the comment, user, and emoji', () => {
|
|
203
|
+
const commentId = createCommentId('c1')
|
|
204
|
+
const make = (emoji: string) =>
|
|
205
|
+
createCommentReaction({
|
|
206
|
+
commentId,
|
|
207
|
+
threadId: createCommentThreadId('t1'),
|
|
208
|
+
pageId,
|
|
209
|
+
userId: 'user1',
|
|
210
|
+
emoji,
|
|
211
|
+
})
|
|
212
|
+
// same emoji → same record (re-picking toggles it); different emoji → its own record (multi)
|
|
213
|
+
expect(make('👍').id).toBe(make('👍').id)
|
|
214
|
+
expect(make('👍').id).not.toBe(make('🎉').id)
|
|
215
|
+
expect(isCommentReactionId(make('👍').id)).toBe(true)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('validates a well-formed reaction record', () => {
|
|
219
|
+
const reaction = createCommentReaction({
|
|
220
|
+
commentId: createCommentId('c1'),
|
|
221
|
+
threadId: createCommentThreadId('t1'),
|
|
222
|
+
pageId,
|
|
223
|
+
userId: 'user1',
|
|
224
|
+
emoji: '👍',
|
|
225
|
+
})
|
|
226
|
+
expect(commentReactionRecordConfig.validator.validate(reaction)).toEqual(reaction)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('rejects an empty or oversized emoji token', () => {
|
|
230
|
+
const reaction = createCommentReaction({
|
|
231
|
+
commentId: createCommentId('c1'),
|
|
232
|
+
threadId: createCommentThreadId('t1'),
|
|
233
|
+
pageId,
|
|
234
|
+
userId: 'user1',
|
|
235
|
+
emoji: '👍',
|
|
236
|
+
})
|
|
237
|
+
// the reaction id embeds the emoji verbatim, so an unbounded token means an unbounded id
|
|
238
|
+
expect(() =>
|
|
239
|
+
commentReactionRecordConfig.validator.validate({ ...reaction, emoji: '' })
|
|
240
|
+
).toThrow()
|
|
241
|
+
expect(() =>
|
|
242
|
+
commentReactionRecordConfig.validator.validate({ ...reaction, emoji: 'x'.repeat(65) })
|
|
243
|
+
).toThrow()
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('createCommentReactionId is injective when a part contains a colon', () => {
|
|
247
|
+
// two different triples that a naive slice+join would collapse onto one id:
|
|
248
|
+
// comment "foo" + user "bar:baz" + 👍 vs comment "foo:bar" + user "baz" + 👍
|
|
249
|
+
const a = createCommentReactionId(createCommentId('foo'), 'bar:baz', '👍')
|
|
250
|
+
const b = createCommentReactionId(createCommentId('foo:bar'), 'baz', '👍')
|
|
251
|
+
expect(a).not.toBe(b)
|
|
252
|
+
expect(isCommentReactionId(a)).toBe(true)
|
|
253
|
+
expect(isCommentReactionId(b)).toBe(true)
|
|
254
|
+
})
|
|
255
|
+
})
|