@tldraw/tlschema 5.3.0-canary.fa3355c81e86 → 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.
- package/dist-cjs/TLStore.js +7 -0
- package/dist-cjs/TLStore.js.map +2 -2
- package/dist-cjs/index.d.ts +196 -1
- package/dist-cjs/index.js +11 -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 +187 -0
- package/dist-cjs/records/TLComment.js.map +7 -0
- package/dist-cjs/shapes/TLGeoShape.js +15 -1
- package/dist-cjs/shapes/TLGeoShape.js.map +2 -2
- package/dist-esm/TLStore.mjs +7 -0
- package/dist-esm/TLStore.mjs.map +2 -2
- package/dist-esm/index.d.mts +196 -1
- package/dist-esm/index.mjs +21 -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 +167 -0
- package/dist-esm/records/TLComment.mjs.map +7 -0
- package/dist-esm/shapes/TLGeoShape.mjs +15 -1
- package/dist-esm/shapes/TLGeoShape.mjs.map +2 -2
- package/package.json +5 -5
- package/src/TLStore.ts +16 -0
- package/src/index.ts +16 -0
- package/src/migrations.test.ts +12 -0
- package/src/misc/TLCursor.ts +1 -0
- package/src/records/TLComment.test.ts +182 -0
- package/src/records/TLComment.ts +330 -0
- package/src/shapes/TLGeoShape.ts +21 -1
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { BaseRecord, RecordId, createRecordMigrationSequence } from '@tldraw/store'
|
|
2
|
+
import { JsonObject } from '@tldraw/utils'
|
|
3
|
+
import { T } from '@tldraw/validate'
|
|
4
|
+
import { idValidator } from '../misc/id-validator'
|
|
5
|
+
import { richTextValidator, TLRichText } from '../misc/TLRichText'
|
|
6
|
+
import { CustomRecordInfo, createCustomRecordId, isCustomRecordId } from './TLCustomRecord'
|
|
7
|
+
import { TLPageId } from './TLPage'
|
|
8
|
+
import { TLShapeId } from './TLShape'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Where a comment thread is anchored on the canvas. Modeled as a discriminated union so new
|
|
12
|
+
* anchor kinds can be added without breaking existing threads:
|
|
13
|
+
*
|
|
14
|
+
* - `shape` — pinned to a shape. `x`/`y` are normalized (0–1) within the shape's page bounds, so
|
|
15
|
+
* the pin keeps its spot as the shape moves and resizes. `isPrecise` mirrors arrow bindings: when
|
|
16
|
+
* true the pin sits at exactly `x`/`y`; when false (the default) it sits at a consumer-defined
|
|
17
|
+
* spot (top-right out of the box), and `x`/`y` are the remembered precise position
|
|
18
|
+
* - `point` — pinned to a fixed point on the page, in page coordinates
|
|
19
|
+
* - `region` — pinned to a rectangular area of the page, in page coordinates
|
|
20
|
+
* - `page` — a page-level thread with no spatial anchor
|
|
21
|
+
* - `text-range` — pinned to a character range inside a shape's text
|
|
22
|
+
*
|
|
23
|
+
* @public
|
|
24
|
+
*/
|
|
25
|
+
export type TLCommentAnchor =
|
|
26
|
+
| { type: 'shape'; shapeId: TLShapeId; x: number; y: number; isPrecise: boolean }
|
|
27
|
+
| { type: 'point'; x: number; y: number }
|
|
28
|
+
| {
|
|
29
|
+
type: 'region'
|
|
30
|
+
x: number
|
|
31
|
+
y: number
|
|
32
|
+
w: number
|
|
33
|
+
h: number
|
|
34
|
+
/** The normalized (0–1) corner the pin sits on — where the creating drag was released.
|
|
35
|
+
* Absent on older records; consumers fall back to their configured corner. */
|
|
36
|
+
pinX?: number
|
|
37
|
+
pinY?: number
|
|
38
|
+
}
|
|
39
|
+
| { type: 'page' }
|
|
40
|
+
| { type: 'text-range'; shapeId: TLShapeId; from: number; to: number }
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A comment thread. The thread owns the anchor (where the conversation lives on the canvas) and
|
|
44
|
+
* the resolution state; the messages themselves are `TLComment` records pointing at the thread
|
|
45
|
+
* via `threadId`, ordered by `createdAt`. v1 threads are flat — no nested replies.
|
|
46
|
+
*
|
|
47
|
+
* Threads and comments are document records, but they're intended to be served through the sync
|
|
48
|
+
* server's object-store lane: gated by the session's `objectAccess` rather than `isReadonly`
|
|
49
|
+
* (so "can comment but not edit" is expressible), excluded from document snapshots and `.tldr`
|
|
50
|
+
* exports server-side, and persisted in a separate lane from the main document.
|
|
51
|
+
*
|
|
52
|
+
* Opt-in: register with `createTLSchema({ records: commentSchemaRecords })` on the server and
|
|
53
|
+
* the matching `records` option on the client — neither type is part of the default schema, and
|
|
54
|
+
* both sides must register them identically.
|
|
55
|
+
*
|
|
56
|
+
* @public
|
|
57
|
+
*/
|
|
58
|
+
export interface TLCommentThread extends BaseRecord<'comment-thread', TLCommentThreadId> {
|
|
59
|
+
/** The page the thread lives on. */
|
|
60
|
+
pageId: TLPageId
|
|
61
|
+
/** Where the thread is anchored on that page. */
|
|
62
|
+
anchor: TLCommentAnchor
|
|
63
|
+
/**
|
|
64
|
+
* Who started the thread. Client-supplied for now; the sync server should stamp/verify this
|
|
65
|
+
* from the session's authenticated identity once record-level authorization lands (the same
|
|
66
|
+
* model presence already uses for self-ownership).
|
|
67
|
+
*/
|
|
68
|
+
createdBy: string
|
|
69
|
+
createdAt: number
|
|
70
|
+
/** Resolution state: when and by whom the thread was resolved, or null while open. */
|
|
71
|
+
resolved: { at: number; by: string } | null
|
|
72
|
+
meta: JsonObject
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** @public */
|
|
76
|
+
export type TLCommentThreadId = RecordId<TLCommentThread>
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A single comment message within a thread. See `TLCommentThread` for the overall model and
|
|
80
|
+
* sync/registration notes.
|
|
81
|
+
*
|
|
82
|
+
* Comment mutations are deliberately not undoable — create/edit them with
|
|
83
|
+
* `{ history: 'ignore' }` to avoid multiplayer surprises like a comment reappearing after
|
|
84
|
+
* someone else deleted it.
|
|
85
|
+
*
|
|
86
|
+
* @public
|
|
87
|
+
*/
|
|
88
|
+
export interface TLComment extends BaseRecord<'comment', TLCommentId> {
|
|
89
|
+
/** The thread this comment belongs to. */
|
|
90
|
+
threadId: TLCommentThreadId
|
|
91
|
+
/** Denormalized from the thread so per-page queries don't need a join. */
|
|
92
|
+
pageId: TLPageId
|
|
93
|
+
/** See `TLCommentThread.createdBy` — same server-stamping caveat applies. */
|
|
94
|
+
authorId: string
|
|
95
|
+
createdAt: number
|
|
96
|
+
/** Null until the comment is first edited. */
|
|
97
|
+
editedAt: number | null
|
|
98
|
+
/** Rich text body. Use `toRichText(...)` for plaintext input. */
|
|
99
|
+
body: TLRichText
|
|
100
|
+
meta: JsonObject
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** @public */
|
|
104
|
+
export type TLCommentId = RecordId<TLComment>
|
|
105
|
+
|
|
106
|
+
const commentAnchorValidator: T.Validator<TLCommentAnchor> = T.union('type', {
|
|
107
|
+
shape: T.object({
|
|
108
|
+
type: T.literal('shape'),
|
|
109
|
+
shapeId: idValidator<TLShapeId>('shape'),
|
|
110
|
+
x: T.number,
|
|
111
|
+
y: T.number,
|
|
112
|
+
isPrecise: T.boolean,
|
|
113
|
+
}),
|
|
114
|
+
point: T.object({
|
|
115
|
+
type: T.literal('point'),
|
|
116
|
+
x: T.number,
|
|
117
|
+
y: T.number,
|
|
118
|
+
}),
|
|
119
|
+
region: T.object({
|
|
120
|
+
type: T.literal('region'),
|
|
121
|
+
x: T.number,
|
|
122
|
+
y: T.number,
|
|
123
|
+
w: T.number,
|
|
124
|
+
h: T.number,
|
|
125
|
+
pinX: T.number.optional(),
|
|
126
|
+
pinY: T.number.optional(),
|
|
127
|
+
}),
|
|
128
|
+
page: T.object({
|
|
129
|
+
type: T.literal('page'),
|
|
130
|
+
}),
|
|
131
|
+
'text-range': T.object({
|
|
132
|
+
type: T.literal('text-range'),
|
|
133
|
+
shapeId: idValidator<TLShapeId>('shape'),
|
|
134
|
+
from: T.number,
|
|
135
|
+
to: T.number,
|
|
136
|
+
}),
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Guard migrations for the comment record types. Each sequence is retroactive and starts with an
|
|
141
|
+
* identity migration that has no `down`: a sync server whose schema registers the comment types
|
|
142
|
+
* cannot down-migrate records for a session whose schema predates them, so such sessions are
|
|
143
|
+
* rejected with CLIENT_TOO_OLD (prompting a refresh) instead of being sent record types their
|
|
144
|
+
* store cannot represent.
|
|
145
|
+
*/
|
|
146
|
+
function createCommentGuardMigrations(
|
|
147
|
+
typeName: 'comment' | 'comment-thread',
|
|
148
|
+
extra: Parameters<typeof createRecordMigrationSequence>[0]['sequence'] = []
|
|
149
|
+
) {
|
|
150
|
+
return createRecordMigrationSequence({
|
|
151
|
+
sequenceId: `com.tldraw.${typeName}`,
|
|
152
|
+
recordType: typeName,
|
|
153
|
+
retroactive: true,
|
|
154
|
+
sequence: [
|
|
155
|
+
{
|
|
156
|
+
id: `com.tldraw.${typeName}/1`,
|
|
157
|
+
up: (record) => record,
|
|
158
|
+
},
|
|
159
|
+
...extra,
|
|
160
|
+
],
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Config for registering the `comment-thread` record type in a tldraw schema. Pass via
|
|
166
|
+
* `commentSchemaRecords`; see `TLCommentThread`.
|
|
167
|
+
*
|
|
168
|
+
* @public
|
|
169
|
+
*/
|
|
170
|
+
export const commentThreadRecordConfig: CustomRecordInfo = {
|
|
171
|
+
scope: 'document',
|
|
172
|
+
migrations: createCommentGuardMigrations('comment-thread', [
|
|
173
|
+
{
|
|
174
|
+
// Shape anchors gained normalized x/y + isPrecise; existing ones were imprecise (top-right).
|
|
175
|
+
id: 'com.tldraw.comment-thread/2',
|
|
176
|
+
up: (record) => {
|
|
177
|
+
const anchor = (record as any).anchor
|
|
178
|
+
if (anchor?.type === 'shape' && anchor.x === undefined) {
|
|
179
|
+
;(record as any).anchor = { ...anchor, x: 1, y: 0, isPrecise: false }
|
|
180
|
+
}
|
|
181
|
+
return record
|
|
182
|
+
},
|
|
183
|
+
down: (record) => {
|
|
184
|
+
const anchor = (record as any).anchor
|
|
185
|
+
if (anchor?.type === 'shape') {
|
|
186
|
+
;(record as any).anchor = { type: 'shape', shapeId: anchor.shapeId }
|
|
187
|
+
}
|
|
188
|
+
return record
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
// Region anchors gained an optional pin corner (where the creating drag released).
|
|
193
|
+
id: 'com.tldraw.comment-thread/3',
|
|
194
|
+
up: (record) => record,
|
|
195
|
+
down: (record) => {
|
|
196
|
+
const anchor = (record as any).anchor
|
|
197
|
+
if (anchor?.type === 'region') {
|
|
198
|
+
const { pinX: _pinX, pinY: _pinY, ...rest } = anchor
|
|
199
|
+
;(record as any).anchor = rest
|
|
200
|
+
}
|
|
201
|
+
return record
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
]),
|
|
205
|
+
validator: T.object({
|
|
206
|
+
id: idValidator<TLCommentThreadId>('comment-thread'),
|
|
207
|
+
typeName: T.literal('comment-thread'),
|
|
208
|
+
pageId: idValidator<TLPageId>('page'),
|
|
209
|
+
anchor: commentAnchorValidator,
|
|
210
|
+
createdBy: T.string,
|
|
211
|
+
createdAt: T.number,
|
|
212
|
+
resolved: T.object({ at: T.number, by: T.string }).nullable(),
|
|
213
|
+
meta: T.jsonValue,
|
|
214
|
+
}),
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Config for registering the `comment` record type in a tldraw schema. Pass via
|
|
219
|
+
* `commentSchemaRecords`; see `TLComment`.
|
|
220
|
+
*
|
|
221
|
+
* @public
|
|
222
|
+
*/
|
|
223
|
+
export const commentRecordConfig: CustomRecordInfo = {
|
|
224
|
+
scope: 'document',
|
|
225
|
+
migrations: createCommentGuardMigrations('comment'),
|
|
226
|
+
validator: T.object({
|
|
227
|
+
id: idValidator<TLCommentId>('comment'),
|
|
228
|
+
typeName: T.literal('comment'),
|
|
229
|
+
threadId: idValidator<TLCommentThreadId>('comment-thread'),
|
|
230
|
+
pageId: idValidator<TLPageId>('page'),
|
|
231
|
+
authorId: T.string,
|
|
232
|
+
createdAt: T.number,
|
|
233
|
+
editedAt: T.number.nullable(),
|
|
234
|
+
body: richTextValidator,
|
|
235
|
+
meta: T.jsonValue,
|
|
236
|
+
}),
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The `records` map to pass to `createTLSchema` / the client `records` option so comment
|
|
241
|
+
* threads and comments sync. Register both types together — a comment without its thread type
|
|
242
|
+
* (or vice versa) will fail schema validation on one side of the connection.
|
|
243
|
+
*
|
|
244
|
+
* @public
|
|
245
|
+
*/
|
|
246
|
+
export const commentSchemaRecords = {
|
|
247
|
+
'comment-thread': commentThreadRecordConfig,
|
|
248
|
+
comment: commentRecordConfig,
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** @public */
|
|
252
|
+
export function createCommentThreadId(id?: string): TLCommentThreadId {
|
|
253
|
+
return createCustomRecordId('comment-thread', id) as TLCommentThreadId
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** @public */
|
|
257
|
+
export function createCommentId(id?: string): TLCommentId {
|
|
258
|
+
return createCustomRecordId('comment', id) as TLCommentId
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Type guard for `TLCommentThreadId`. Note `isCommentId` does not accept these: the prefixes
|
|
263
|
+
* are `comment-thread:` vs `comment:`, and the character after `comment` differs (`-` vs `:`),
|
|
264
|
+
* so the two never overlap.
|
|
265
|
+
*
|
|
266
|
+
* @public
|
|
267
|
+
*/
|
|
268
|
+
export function isCommentThreadId(id: string): id is TLCommentThreadId {
|
|
269
|
+
return isCustomRecordId('comment-thread', id)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Type guard for `TLCommentId`. See `isCommentThreadId` for why `comment-thread:...` ids are
|
|
274
|
+
* correctly rejected here.
|
|
275
|
+
*
|
|
276
|
+
* @public
|
|
277
|
+
*/
|
|
278
|
+
export function isCommentId(id: string): id is TLCommentId {
|
|
279
|
+
return isCustomRecordId('comment', id)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Create a new comment thread record. Pair with `createComment` for the thread's first message.
|
|
284
|
+
*
|
|
285
|
+
* @public
|
|
286
|
+
*/
|
|
287
|
+
export function createCommentThread(props: {
|
|
288
|
+
pageId: TLPageId
|
|
289
|
+
anchor: TLCommentAnchor
|
|
290
|
+
createdBy: string
|
|
291
|
+
now?: number
|
|
292
|
+
meta?: JsonObject
|
|
293
|
+
}): TLCommentThread {
|
|
294
|
+
return {
|
|
295
|
+
id: createCommentThreadId(),
|
|
296
|
+
typeName: 'comment-thread',
|
|
297
|
+
pageId: props.pageId,
|
|
298
|
+
anchor: props.anchor,
|
|
299
|
+
createdBy: props.createdBy,
|
|
300
|
+
createdAt: props.now ?? Date.now(),
|
|
301
|
+
resolved: null,
|
|
302
|
+
meta: props.meta ?? {},
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Create a new comment record within a thread.
|
|
308
|
+
*
|
|
309
|
+
* @public
|
|
310
|
+
*/
|
|
311
|
+
export function createComment(props: {
|
|
312
|
+
threadId: TLCommentThreadId
|
|
313
|
+
pageId: TLPageId
|
|
314
|
+
authorId: string
|
|
315
|
+
body: TLRichText
|
|
316
|
+
now?: number
|
|
317
|
+
meta?: JsonObject
|
|
318
|
+
}): TLComment {
|
|
319
|
+
return {
|
|
320
|
+
id: createCommentId(),
|
|
321
|
+
typeName: 'comment',
|
|
322
|
+
threadId: props.threadId,
|
|
323
|
+
pageId: props.pageId,
|
|
324
|
+
authorId: props.authorId,
|
|
325
|
+
createdAt: props.now ?? Date.now(),
|
|
326
|
+
editedAt: null,
|
|
327
|
+
body: props.body,
|
|
328
|
+
meta: props.meta ?? {},
|
|
329
|
+
}
|
|
330
|
+
}
|
package/src/shapes/TLGeoShape.ts
CHANGED
|
@@ -90,6 +90,10 @@ export interface TLGeoShapeProps {
|
|
|
90
90
|
growY: number
|
|
91
91
|
/** Scale factor applied to the shape */
|
|
92
92
|
scale: number
|
|
93
|
+
/** Whether to mirror the shape horizontally */
|
|
94
|
+
flipX: boolean
|
|
95
|
+
/** Whether to mirror the shape vertically */
|
|
96
|
+
flipY: boolean
|
|
93
97
|
|
|
94
98
|
/** Color style for text label */
|
|
95
99
|
labelColor: TLDefaultColorStyle
|
|
@@ -142,7 +146,9 @@ export interface TLGeoShapeProps {
|
|
|
142
146
|
* labelColor: 'black',
|
|
143
147
|
* url: '',
|
|
144
148
|
* growY: 0,
|
|
145
|
-
* scale: 1
|
|
149
|
+
* scale: 1,
|
|
150
|
+
* flipX: false,
|
|
151
|
+
* flipY: false
|
|
146
152
|
* },
|
|
147
153
|
* meta: {}
|
|
148
154
|
* }
|
|
@@ -170,6 +176,8 @@ export const geoShapeProps: RecordProps<TLGeoShape> = {
|
|
|
170
176
|
h: T.nonZeroNumber,
|
|
171
177
|
growY: T.positiveNumber,
|
|
172
178
|
scale: T.nonZeroNumber,
|
|
179
|
+
flipX: T.boolean,
|
|
180
|
+
flipY: T.boolean,
|
|
173
181
|
|
|
174
182
|
// Text properties
|
|
175
183
|
labelColor: DefaultLabelColorStyle,
|
|
@@ -194,6 +202,7 @@ const geoShapeVersions = createShapePropsMigrationIds('geo', {
|
|
|
194
202
|
AddScale: 9,
|
|
195
203
|
AddRichText: 10,
|
|
196
204
|
AddRichTextAttrs: 11,
|
|
205
|
+
AddFlipProps: 12,
|
|
197
206
|
})
|
|
198
207
|
|
|
199
208
|
/**
|
|
@@ -318,5 +327,16 @@ export const geoShapeMigrations = createShapePropsMigrationSequence({
|
|
|
318
327
|
}
|
|
319
328
|
},
|
|
320
329
|
},
|
|
330
|
+
{
|
|
331
|
+
id: geoShapeVersions.AddFlipProps,
|
|
332
|
+
up: (props) => {
|
|
333
|
+
props.flipX = false
|
|
334
|
+
props.flipY = false
|
|
335
|
+
},
|
|
336
|
+
down: (props) => {
|
|
337
|
+
delete props.flipX
|
|
338
|
+
delete props.flipY
|
|
339
|
+
},
|
|
340
|
+
},
|
|
321
341
|
],
|
|
322
342
|
})
|