@quillmark/svelte 0.16.0 → 0.18.0
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/README.md +4 -2
- package/dist/core/codec/index.d.ts +0 -3
- package/dist/core/rebind.svelte.js +5 -8
- package/dist/core/theme.css +12 -1
- package/dist/preset/index.d.ts +1 -0
- package/dist/preview/Preview.svelte +14 -19
- package/dist/preview/Preview.svelte.d.ts +3 -3
- package/dist/preview/index.d.ts +0 -1
- package/dist/visual/ArrayField.svelte +44 -41
- package/dist/visual/ArrayField.svelte.d.ts +0 -1
- package/dist/visual/BooleanField.svelte +16 -14
- package/dist/visual/BooleanField.svelte.d.ts +0 -1
- package/dist/visual/Card.svelte +65 -35
- package/dist/visual/CardControls.svelte.d.ts +0 -1
- package/dist/visual/DateField.svelte +14 -17
- package/dist/visual/DateField.svelte.d.ts +0 -1
- package/dist/visual/EnumField.svelte +9 -12
- package/dist/visual/EnumField.svelte.d.ts +0 -1
- package/dist/visual/Field.svelte +17 -10
- package/dist/visual/Field.svelte.d.ts +3 -0
- package/dist/visual/FieldHint.svelte.d.ts +0 -1
- package/dist/visual/FieldLabel.svelte +1 -2
- package/dist/visual/FormatPopover.svelte +22 -27
- package/dist/visual/FormatPopover.svelte.d.ts +0 -1
- package/dist/visual/MatrixAdd.svelte +210 -0
- package/dist/visual/MatrixAdd.svelte.d.ts +15 -0
- package/dist/visual/MatrixField.svelte +329 -84
- package/dist/visual/MatrixField.svelte.d.ts +8 -6
- package/dist/visual/NumberField.svelte +6 -10
- package/dist/visual/NumberField.svelte.d.ts +0 -1
- package/dist/visual/ObjectField.svelte +30 -17
- package/dist/visual/ObjectField.svelte.d.ts +3 -1
- package/dist/visual/ProseField.svelte +42 -41
- package/dist/visual/ProseField.svelte.d.ts +0 -1
- package/dist/visual/ProseValue.svelte +56 -56
- package/dist/visual/ProseValue.svelte.d.ts +0 -1
- package/dist/visual/SlashMenu.svelte +5 -15
- package/dist/visual/SlashMenu.svelte.d.ts +0 -1
- package/dist/visual/TextField.svelte +5 -9
- package/dist/visual/TextField.svelte.d.ts +0 -1
- package/dist/visual/TipsCard.svelte +10 -11
- package/dist/visual/TipsCard.svelte.d.ts +0 -1
- package/dist/visual/VisualEditorInner.svelte +21 -14
- package/dist/visual/VisualEditorInner.svelte.d.ts +0 -1
- package/dist/visual/checklists.d.ts +82 -0
- package/dist/visual/checklists.js +171 -0
- package/dist/visual/index.d.ts +0 -1
- package/dist/visual/strings.d.ts +15 -1
- package/dist/visual/strings.js +6 -0
- package/dist/visual/structure.d.ts +63 -31
- package/dist/visual/structure.js +122 -44
- package/package.json +20 -16
- package/dist/visual/synced.svelte.d.ts +0 -15
- package/dist/visual/synced.svelte.js +0 -34
package/dist/visual/structure.js
CHANGED
|
@@ -11,6 +11,11 @@ import { core } from '../core/lifecycle.js';
|
|
|
11
11
|
export function isContainer(kind) {
|
|
12
12
|
return kind === 'array' || kind === 'object' || kind === 'variant' || kind === 'matrix';
|
|
13
13
|
}
|
|
14
|
+
/** `ui.layout: flat` on a typed dictionary: no frame of its own, its properties
|
|
15
|
+
* standing at the depth of the fields around it rather than a rung in. */
|
|
16
|
+
export function flatObject(schema) {
|
|
17
|
+
return controlKind(schema) === 'object' && schema.ui?.layout === 'flat';
|
|
18
|
+
}
|
|
14
19
|
/**
|
|
15
20
|
* A card's resolved rows keyed by field name: the provenance channel parallel to
|
|
16
21
|
* {@link CardModel.values}. `resolve` returns rows in declaration order; the
|
|
@@ -40,6 +45,18 @@ export function resolvedByCardIndex(resolved) {
|
|
|
40
45
|
export function ghostDefault(row) {
|
|
41
46
|
return row?.source === 'default' ? row.value : undefined;
|
|
42
47
|
}
|
|
48
|
+
/** A stored `boolean` cell as the render reads it: a boolean, `true` or `false` in any
|
|
49
|
+
* case, or a number by whether it is non-zero; undefined where the render refuses the
|
|
50
|
+
* value. The engine's coercion, restated, so the suite holds it to `reader.resolve()`. */
|
|
51
|
+
export function renderedBoolean(value) {
|
|
52
|
+
if (typeof value === 'boolean')
|
|
53
|
+
return value;
|
|
54
|
+
if (typeof value === 'number')
|
|
55
|
+
return value !== 0;
|
|
56
|
+
if (typeof value === 'string' && /^(true|false)$/i.test(value))
|
|
57
|
+
return value.toLowerCase() === 'true';
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
43
60
|
/** A ghost value's string form, or undefined for null/object (only text ghosts
|
|
44
61
|
* render a placeholder). A scalar's resolved default reads through it; a content
|
|
45
62
|
* field's resolves as `Content`, which a prose leaf reads by its text instead
|
|
@@ -142,10 +159,10 @@ export function controlKind(f) {
|
|
|
142
159
|
* crosses inside untyped container data with no type to read it off, so a literal here
|
|
143
160
|
* would be a second spelling of a reserved key. No variant may declare it. */
|
|
144
161
|
export const VARIANT_DISCRIMINANT = VARIANT_DISCRIMINANT_KEY;
|
|
145
|
-
/** The
|
|
146
|
-
* The boundary
|
|
147
|
-
*
|
|
148
|
-
export const
|
|
162
|
+
/** The label key of an open matrix's added item, the one cell a roster member never
|
|
163
|
+
* stores. The boundary exports no constant for it, so the package spells it once
|
|
164
|
+
* (canon `SCHEMAS.md` §Matrix). */
|
|
165
|
+
export const MATRIX_TITLE = 'title';
|
|
149
166
|
/** The roster's members, in declaration order. Own keys only: a schema map is indexed
|
|
150
167
|
* by a document-supplied id, and `'toString' in members` is true of every roster. */
|
|
151
168
|
export function matrixMembers(roster) {
|
|
@@ -157,17 +174,14 @@ export function memberValue(map, id) {
|
|
|
157
174
|
return map && Object.hasOwn(map, id) ? map[id] : undefined;
|
|
158
175
|
}
|
|
159
176
|
/**
|
|
160
|
-
* Whether a stored member is held
|
|
161
|
-
*
|
|
162
|
-
* `false`
|
|
163
|
-
*
|
|
164
|
-
* engine coerces to false — `false`, `0`, `"false"`, `null` — read false here; every other
|
|
165
|
-
* present scalar reads held, so a document the engine would refuse still draws the tick
|
|
166
|
-
* its spelling claims.
|
|
177
|
+
* Whether a stored member is held: by being present. A mapping is held whatever it
|
|
178
|
+
* carries, `{}` included, and a bare scalar is the tick itself, read as the engine's
|
|
179
|
+
* coercion reads it: `false`, `0`, `"false"` and `null` unheld, every other present
|
|
180
|
+
* scalar held, so a spelling the engine would refuse still draws the tick it claims.
|
|
167
181
|
*/
|
|
168
182
|
export function matrixHeld(stored) {
|
|
169
183
|
if (typeof stored === 'object' && stored !== null)
|
|
170
|
-
return
|
|
184
|
+
return true;
|
|
171
185
|
return tickOf(stored);
|
|
172
186
|
}
|
|
173
187
|
/** A tick's scalar reading: absent and the engine's false spellings unheld, the rest held. */
|
|
@@ -182,20 +196,29 @@ function tickOf(v) {
|
|
|
182
196
|
return v !== 'false';
|
|
183
197
|
return true;
|
|
184
198
|
}
|
|
185
|
-
/**
|
|
186
|
-
*
|
|
199
|
+
/** The keys no column may take (`quill::matrix_reserved_column`): an added item's
|
|
200
|
+
* label, and the `held` a member never stores. */
|
|
201
|
+
const MATRIX_RESERVED = [MATRIX_TITLE, 'held'];
|
|
202
|
+
/** A member's columns: its mapping without the reserved keys, `{}` for a bare tick or an
|
|
203
|
+
* absent member. What the columns subform takes as its value, so an edit writes back
|
|
204
|
+
* no `held` a document stored (`validation::held_stored`). */
|
|
187
205
|
export function matrixColumns(stored) {
|
|
188
206
|
if (typeof stored !== 'object' || stored === null || Array.isArray(stored))
|
|
189
207
|
return {};
|
|
190
208
|
const rest = { ...stored };
|
|
191
|
-
|
|
209
|
+
for (const key of MATRIX_RESERVED)
|
|
210
|
+
delete rest[key];
|
|
192
211
|
return rest;
|
|
193
212
|
}
|
|
213
|
+
/** What a matrix holding nothing commits: the unset field where unset prints nothing
|
|
214
|
+
* held, and the authored `{}` where it would print a `default:` or `none` instead. */
|
|
215
|
+
export function matrixEmpty(matrix) {
|
|
216
|
+
return matrix.default !== undefined || optionalCell(matrix) ? {} : undefined;
|
|
217
|
+
}
|
|
194
218
|
/**
|
|
195
219
|
* The map after one member moves: `next` written under `id`, or the key dropped where
|
|
196
220
|
* `next` is `undefined`; a map left holding nothing is an unset field. Every other
|
|
197
|
-
* member rides through in the spelling the document had
|
|
198
|
-
* stays `true` while its neighbour is edited.
|
|
221
|
+
* member rides through in the spelling the document had.
|
|
199
222
|
*/
|
|
200
223
|
export function commitMember(map, id, next) {
|
|
201
224
|
const out = { ...(map ?? {}) };
|
|
@@ -206,33 +229,86 @@ export function commitMember(map, id, next) {
|
|
|
206
229
|
return Object.keys(out).length ? out : undefined;
|
|
207
230
|
}
|
|
208
231
|
/**
|
|
209
|
-
* The member a tick or an edit writes
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
* Which columns are blank is the engine's rule and not restated here: a column control
|
|
213
|
-
* that clears drops its key (`ObjectField`), so a member emptied by hand arrives with no
|
|
214
|
-
* columns and only then leaves the map.
|
|
232
|
+
* The held member a tick or an edit writes: its columns, the bare tick `true` where
|
|
233
|
+
* it answers none, and an added item's `title` beside them. Unticking is removal
|
|
234
|
+
* ({@link commitMember} with `undefined`), never a write.
|
|
215
235
|
*/
|
|
216
|
-
export function memberWrite(
|
|
217
|
-
if (
|
|
218
|
-
return
|
|
219
|
-
return
|
|
236
|
+
export function memberWrite(columns, title) {
|
|
237
|
+
if (title !== undefined)
|
|
238
|
+
return { [MATRIX_TITLE]: title, ...columns };
|
|
239
|
+
return Object.keys(columns).length ? columns : true;
|
|
220
240
|
}
|
|
221
|
-
/**
|
|
222
|
-
*
|
|
223
|
-
export function
|
|
224
|
-
return
|
|
225
|
-
type: 'object',
|
|
226
|
-
properties: {
|
|
227
|
-
[MATRIX_HELD]: { type: 'boolean', default: false },
|
|
228
|
-
...(matrix.properties ?? {})
|
|
229
|
-
}
|
|
230
|
-
};
|
|
241
|
+
/** Whether `id` is spelled as a matrix id: snake_case, the rule the roster's ids and
|
|
242
|
+
* an added item's key share. */
|
|
243
|
+
export function matrixId(id) {
|
|
244
|
+
return /^[a-z][a-z0-9_]*$/.test(id);
|
|
231
245
|
}
|
|
232
246
|
/** Whether `id` is on the roster. */
|
|
233
|
-
export function
|
|
247
|
+
export function onRoster(matrix, id) {
|
|
234
248
|
return matrix.members != null && Object.hasOwn(matrix.members, id);
|
|
235
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* Whether a step under the matrix names a member, judged on the schema alone as an
|
|
252
|
+
* index step is: a roster id, or on an open matrix any key spelled as an id, the added
|
|
253
|
+
* item it would be. So an unheld member's address resolves.
|
|
254
|
+
*/
|
|
255
|
+
export function matrixDeclares(matrix, id) {
|
|
256
|
+
return onRoster(matrix, id) || (!!matrix.open && matrixId(id));
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* The added items a stored map holds, sorted by id as the plate orders them: on an
|
|
260
|
+
* open matrix, each key off the roster spelled as an id whose mapping carries a
|
|
261
|
+
* `title`. Any other key off the roster is the engine's refusal, drawn nowhere and
|
|
262
|
+
* ridden through.
|
|
263
|
+
*/
|
|
264
|
+
export function addedItems(matrix, map) {
|
|
265
|
+
if (!matrix.open || !map)
|
|
266
|
+
return [];
|
|
267
|
+
const out = [];
|
|
268
|
+
for (const id of Object.keys(map)) {
|
|
269
|
+
if (onRoster(matrix, id) || !matrixId(id))
|
|
270
|
+
continue;
|
|
271
|
+
const stored = map[id];
|
|
272
|
+
if (typeof stored !== 'object' || stored === null || Array.isArray(stored))
|
|
273
|
+
continue;
|
|
274
|
+
const title = stored[MATRIX_TITLE];
|
|
275
|
+
if (title == null)
|
|
276
|
+
continue;
|
|
277
|
+
out.push({ id, title: String(title) });
|
|
278
|
+
}
|
|
279
|
+
return out.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* The id an item added under `title` takes: the title folded to snake_case, prefixed
|
|
283
|
+
* where it opens on no letter, and suffixed past every id `taken` names, the roster's
|
|
284
|
+
* and the document's alike. The id is the wire's and never drawn, so a later retitle
|
|
285
|
+
* leaves it where it is.
|
|
286
|
+
*/
|
|
287
|
+
export function addedId(title, taken) {
|
|
288
|
+
const folded = title
|
|
289
|
+
.normalize('NFKD')
|
|
290
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
291
|
+
.toLowerCase()
|
|
292
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
293
|
+
.replace(/^_+|_+$/g, '');
|
|
294
|
+
const base = /^[a-z]/.test(folded) ? folded : `item${folded ? `_${folded}` : ''}`;
|
|
295
|
+
if (!taken(base))
|
|
296
|
+
return base;
|
|
297
|
+
let n = 2;
|
|
298
|
+
while (taken(`${base}_${n}`))
|
|
299
|
+
n++;
|
|
300
|
+
return `${base}_${n}`;
|
|
301
|
+
}
|
|
302
|
+
/** The `object` a matrix member is, for the schema walk: the declared columns, and an
|
|
303
|
+
* added item's `title` beside them. Composed here because the boundary serializes the
|
|
304
|
+
* roster and the columns alone. */
|
|
305
|
+
export function matrixMemberSchema(matrix, id) {
|
|
306
|
+
const columns = matrix.properties ?? {};
|
|
307
|
+
return {
|
|
308
|
+
type: 'object',
|
|
309
|
+
properties: onRoster(matrix, id) ? columns : { [MATRIX_TITLE]: { type: 'string' }, ...columns }
|
|
310
|
+
};
|
|
311
|
+
}
|
|
236
312
|
// ── The schema walk ──────────────────────────────────────────────────────────
|
|
237
313
|
/**
|
|
238
314
|
* The schema at `steps` under `field`, or `undefined` where the schema declares no such
|
|
@@ -265,7 +341,7 @@ function stepInto(schema, step) {
|
|
|
265
341
|
: undefined;
|
|
266
342
|
case 'matrix':
|
|
267
343
|
return typeof step === 'string' && matrixDeclares(schema, step)
|
|
268
|
-
? matrixMemberSchema(schema)
|
|
344
|
+
? matrixMemberSchema(schema, step)
|
|
269
345
|
: undefined;
|
|
270
346
|
case 'variant': {
|
|
271
347
|
if (typeof step !== 'string')
|
|
@@ -318,7 +394,7 @@ export function rowSummary(items, row) {
|
|
|
318
394
|
* The layout an array takes: `'table'` where the array's own `ui.layout` asks for it
|
|
319
395
|
* and every cell of the row is short (a block prose cell declines it, at load width and
|
|
320
396
|
* every width after), else `'list'`. That every column is a leaf is the loader's
|
|
321
|
-
* contract (`quill::
|
|
397
|
+
* contract (`quill::table_column_not_leaf`); whether a leaf fits a cell is the surface's
|
|
322
398
|
* answer (canon `SCHEMAS.md`): a table composes by position, so a row that would stack
|
|
323
399
|
* inside a cell is not one.
|
|
324
400
|
*/
|
|
@@ -362,9 +438,10 @@ export function commitDiscriminant(value, member) {
|
|
|
362
438
|
/**
|
|
363
439
|
* Whether the schema obliges a cell: `default:`'s absence, which is the whole of the
|
|
364
440
|
* obligation (DOCUMENT_MODEL). Exempt are the cells whose absence of one says nothing:
|
|
365
|
-
* a typed dictionary
|
|
366
|
-
*
|
|
367
|
-
* declare none, its unanswered render
|
|
441
|
+
* a typed dictionary, since a namespace declares no `default:` at all and `validate`
|
|
442
|
+
* anchors obligation on the leaves under it; a matrix, whose blank is nothing held, a
|
|
443
|
+
* whole answer; and an optional cell, which can declare none, its unanswered render
|
|
444
|
+
* being `none`.
|
|
368
445
|
*/
|
|
369
446
|
export function obliged(schema) {
|
|
370
447
|
return (schema.type !== 'object' &&
|
|
@@ -431,7 +508,8 @@ export function groupSections(fields, order, labelFor) {
|
|
|
431
508
|
for (const g of order) {
|
|
432
509
|
const fs = fields.filter((f) => f.group === g);
|
|
433
510
|
if (fs.length) {
|
|
434
|
-
|
|
511
|
+
const head = fs.length === 1 && flatObject(fs[0].schema) ? fs[0] : undefined;
|
|
512
|
+
sections.push({ group: g, label: labelFor(g), fields: fs, head });
|
|
435
513
|
emitted.add(g);
|
|
436
514
|
}
|
|
437
515
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quillmark/svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Editor, live-preview and source surfaces over a Quillmark WASM session.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -48,7 +48,10 @@
|
|
|
48
48
|
"svelte": "./dist/visual/index.js",
|
|
49
49
|
"default": "./dist/visual/index.js"
|
|
50
50
|
},
|
|
51
|
-
"./preset":
|
|
51
|
+
"./preset": {
|
|
52
|
+
"types": "./dist/preset/index.d.ts",
|
|
53
|
+
"default": "./dist/preset/index.css"
|
|
54
|
+
}
|
|
52
55
|
},
|
|
53
56
|
"files": [
|
|
54
57
|
"dist",
|
|
@@ -57,21 +60,22 @@
|
|
|
57
60
|
],
|
|
58
61
|
"sideEffects": true,
|
|
59
62
|
"scripts": {
|
|
60
|
-
"build": "svelte-package",
|
|
63
|
+
"build": "svelte-package && node scripts/strip-dts-css.mjs",
|
|
61
64
|
"prepare": "npm run build",
|
|
62
65
|
"prepack": "rm -rf dist && npm run build",
|
|
63
66
|
"check:pack": "publint",
|
|
64
67
|
"test": "vitest run",
|
|
65
68
|
"test:watch": "vitest",
|
|
66
|
-
"check": "svelte-check --tsconfig ./tsconfig.json"
|
|
69
|
+
"check": "svelte-check --tsconfig ./tsconfig.json --fail-on-warnings"
|
|
67
70
|
},
|
|
68
71
|
"peerDependencies": {
|
|
69
|
-
"@quillmark/wasm": ">=0.
|
|
70
|
-
"svelte": "^5.
|
|
72
|
+
"@quillmark/wasm": ">=0.122.0-0",
|
|
73
|
+
"svelte": "^5.33.0"
|
|
71
74
|
},
|
|
72
75
|
"dependencies": {
|
|
73
76
|
"@internationalized/date": "^3.12.2",
|
|
74
|
-
"bits-ui": "^2.
|
|
77
|
+
"bits-ui": "^2.19.0",
|
|
78
|
+
"esm-env": "^1.2.2",
|
|
75
79
|
"prosemirror-commands": "1.7.1",
|
|
76
80
|
"prosemirror-gapcursor": "1.3.2",
|
|
77
81
|
"prosemirror-history": "1.5.0",
|
|
@@ -84,15 +88,15 @@
|
|
|
84
88
|
"prosemirror-view": "1.42.1"
|
|
85
89
|
},
|
|
86
90
|
"devDependencies": {
|
|
87
|
-
"@sveltejs/package": "^
|
|
88
|
-
"@sveltejs/vite-plugin-svelte": "^
|
|
89
|
-
"@types/node": "^
|
|
90
|
-
"jsdom": "^
|
|
91
|
-
"publint": "^0.3.
|
|
92
|
-
"svelte": "^5.
|
|
93
|
-
"svelte-check": "^4.
|
|
94
|
-
"typescript": "^
|
|
95
|
-
"vitest": "^
|
|
91
|
+
"@sveltejs/package": "^3.0.0",
|
|
92
|
+
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
93
|
+
"@types/node": "^24.19.1",
|
|
94
|
+
"jsdom": "^30.1.0",
|
|
95
|
+
"publint": "^0.3.25",
|
|
96
|
+
"svelte": "^5.54.0",
|
|
97
|
+
"svelte-check": "^4.7.6",
|
|
98
|
+
"typescript": "^6.0.0",
|
|
99
|
+
"vitest": "^5.0.0"
|
|
96
100
|
},
|
|
97
101
|
"publishConfig": {
|
|
98
102
|
"access": "public"
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/** A settable local synced to an external projection. Bind `.value` in the
|
|
2
|
-
* template; assign `.value` on input. */
|
|
3
|
-
export interface SyncedLocal<T> {
|
|
4
|
-
value: T;
|
|
5
|
-
}
|
|
6
|
-
/**
|
|
7
|
-
* Seed a local from `project` and reconcile only an external change into it.
|
|
8
|
-
* `project` reads the reactive source (a prop) and maps it to the local's type:
|
|
9
|
-
* ` => value ?? ''`, ` => (value != null ? String(value) : '')`, etc. The
|
|
10
|
-
* seed is `untrack`ed (the ongoing sync is the `$effect`, not the initializer),
|
|
11
|
-
* and each reconcile is `untrack`ed so an own-edit write never re-triggers it.
|
|
12
|
-
* Reassignment through the `value` setter is a plain local write; only a changed
|
|
13
|
-
* `project` result flows the other way.
|
|
14
|
-
*/
|
|
15
|
-
export declare function syncedLocal<T>(project: () => T): SyncedLocal<T>;
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
// The controlled-local reconcile shared by the scalar controls (TextField /
|
|
2
|
-
// NumberField / DateField / BooleanField / EnumField). Each seeds a local from a
|
|
3
|
-
// projection of its `value` prop and reconciles only an external change back into
|
|
4
|
-
// the local: own edits stay `untrack`ed, so typing never re-runs the sync and
|
|
5
|
-
// resets the caret. One copy for all five, rather than a character-identical block
|
|
6
|
-
// in each (VISUAL_EDITOR §Surface).
|
|
7
|
-
import { untrack } from 'svelte';
|
|
8
|
-
/**
|
|
9
|
-
* Seed a local from `project` and reconcile only an external change into it.
|
|
10
|
-
* `project` reads the reactive source (a prop) and maps it to the local's type:
|
|
11
|
-
* ` => value ?? ''`, ` => (value != null ? String(value) : '')`, etc. The
|
|
12
|
-
* seed is `untrack`ed (the ongoing sync is the `$effect`, not the initializer),
|
|
13
|
-
* and each reconcile is `untrack`ed so an own-edit write never re-triggers it.
|
|
14
|
-
* Reassignment through the `value` setter is a plain local write; only a changed
|
|
15
|
-
* `project` result flows the other way.
|
|
16
|
-
*/
|
|
17
|
-
export function syncedLocal(project) {
|
|
18
|
-
let local = $state(untrack(project));
|
|
19
|
-
$effect(() => {
|
|
20
|
-
const incoming = project();
|
|
21
|
-
untrack(() => {
|
|
22
|
-
if (incoming !== local)
|
|
23
|
-
local = incoming;
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
return {
|
|
27
|
-
get value() {
|
|
28
|
-
return local;
|
|
29
|
-
},
|
|
30
|
-
set value(v) {
|
|
31
|
-
local = v;
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
}
|