@iyulab/u-board-designer 0.0.0-stage → 0.2.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.
Files changed (82) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +127 -2
  3. package/dist/lib/authoring/AnchorFields.d.ts +12 -0
  4. package/dist/lib/authoring/AnchorFields.js +64 -0
  5. package/dist/lib/authoring/BackgroundField.d.ts +13 -0
  6. package/dist/lib/authoring/BackgroundField.js +38 -0
  7. package/dist/lib/authoring/BoardAuthoring.d.ts +37 -0
  8. package/dist/lib/authoring/BoardAuthoring.js +288 -0
  9. package/dist/lib/authoring/BoardElementsPanel.d.ts +14 -0
  10. package/dist/lib/authoring/BoardElementsPanel.js +49 -0
  11. package/dist/lib/authoring/BoardPanel.d.ts +40 -0
  12. package/dist/lib/authoring/BoardPanel.js +22 -0
  13. package/dist/lib/authoring/CanvasAuthoring.d.ts +13 -0
  14. package/dist/lib/authoring/CanvasAuthoring.js +161 -0
  15. package/dist/lib/authoring/DataPanel.d.ts +29 -0
  16. package/dist/lib/authoring/DataPanel.js +98 -0
  17. package/dist/lib/authoring/GridAuthoring.d.ts +13 -0
  18. package/dist/lib/authoring/GridAuthoring.js +135 -0
  19. package/dist/lib/authoring/GridItemPanel.d.ts +22 -0
  20. package/dist/lib/authoring/GridItemPanel.js +21 -0
  21. package/dist/lib/authoring/LinkPanel.d.ts +19 -0
  22. package/dist/lib/authoring/LinkPanel.js +24 -0
  23. package/dist/lib/authoring/MappingEditor.d.ts +23 -0
  24. package/dist/lib/authoring/MappingEditor.js +83 -0
  25. package/dist/lib/authoring/PartPanel.d.ts +24 -0
  26. package/dist/lib/authoring/PartPanel.js +78 -0
  27. package/dist/lib/authoring/ShapePanel.d.ts +20 -0
  28. package/dist/lib/authoring/ShapePanel.js +54 -0
  29. package/dist/lib/authoring/StatesEditor.d.ts +18 -0
  30. package/dist/lib/authoring/StatesEditor.js +79 -0
  31. package/dist/lib/authoring/StatusesPanel.d.ts +14 -0
  32. package/dist/lib/authoring/StatusesPanel.js +32 -0
  33. package/dist/lib/authoring/TriggersEditor.d.ts +30 -0
  34. package/dist/lib/authoring/TriggersEditor.js +137 -0
  35. package/dist/lib/authoring/ValueMapFields.d.ts +64 -0
  36. package/dist/lib/authoring/ValueMapFields.js +68 -0
  37. package/dist/lib/authoring/WidgetPropsForm.d.ts +27 -0
  38. package/dist/lib/authoring/WidgetPropsForm.js +56 -0
  39. package/dist/lib/authoring/background-image.d.ts +25 -0
  40. package/dist/lib/authoring/background-image.js +63 -0
  41. package/dist/lib/authoring/board-layout.d.ts +64 -0
  42. package/dist/lib/authoring/board-layout.js +326 -0
  43. package/dist/lib/authoring/board-triggers.d.ts +5 -0
  44. package/dist/lib/authoring/board-triggers.js +55 -0
  45. package/dist/lib/authoring/document-history.d.ts +23 -0
  46. package/dist/lib/authoring/document-history.js +37 -0
  47. package/dist/lib/authoring/editor-parts.d.ts +46 -0
  48. package/dist/lib/authoring/editor-parts.js +6 -0
  49. package/dist/lib/authoring/field-style.d.ts +3 -0
  50. package/dist/lib/authoring/field-style.js +2 -0
  51. package/dist/lib/authoring/part-names.d.ts +6 -0
  52. package/dist/lib/authoring/part-names.js +9 -0
  53. package/dist/lib/authoring/scene-mapping.d.ts +22 -0
  54. package/dist/lib/authoring/scene-mapping.js +124 -0
  55. package/dist/lib/authoring/use-edit-keys.d.ts +13 -0
  56. package/dist/lib/authoring/use-edit-keys.js +57 -0
  57. package/dist/lib/authoring/widget-catalog.d.ts +4 -0
  58. package/dist/lib/authoring/widget-catalog.js +9 -0
  59. package/dist/lib/authoring/widget-fields.d.ts +45 -0
  60. package/dist/lib/authoring/widget-fields.js +129 -0
  61. package/dist/lib/board/check.d.ts +27 -0
  62. package/dist/lib/board/check.js +48 -0
  63. package/dist/lib/board/rulebook.d.ts +7 -0
  64. package/dist/lib/board/rulebook.js +148 -0
  65. package/dist/lib/check-entry.d.ts +3 -0
  66. package/dist/lib/check-entry.js +4 -0
  67. package/dist/lib/editing/grid-editing.d.ts +19 -0
  68. package/dist/lib/editing/grid-editing.js +68 -0
  69. package/dist/lib/editing/part-removal.d.ts +4 -0
  70. package/dist/lib/editing/part-removal.js +11 -0
  71. package/dist/lib/editing/trigger-editing.d.ts +16 -0
  72. package/dist/lib/editing/trigger-editing.js +36 -0
  73. package/dist/lib/index.d.ts +7 -0
  74. package/dist/lib/index.js +5 -0
  75. package/dist/lib/labels-ko.d.ts +4 -0
  76. package/dist/lib/labels-ko.js +270 -0
  77. package/dist/lib/labels.d.ts +368 -0
  78. package/dist/lib/labels.js +273 -0
  79. package/dist/lib/use-labels.d.ts +7 -0
  80. package/dist/lib/use-labels.js +12 -0
  81. package/package.json +75 -4
  82. package/styles.css +277 -0
@@ -0,0 +1,326 @@
1
+ // Where the parts of a board are, written back from its picture. The editor draws the
2
+ // picture and the author moves, sizes, orders and deletes what it draws; a node's or an item's id is its part's
3
+ // (`<part>/<key>` for one of an `each` part's items), so what changed is said again in the board's layout — and nothing
4
+ // else of the board changes.
5
+ import { DEFAULT_NODE_HEIGHT, DEFAULT_NODE_WIDTH } from '@iyulab/u-board/toolkit';
6
+ import { GRID_DEFAULT_SPAN } from '@iyulab/u-board/picture';
7
+ import { isDataPlace, isFlowPlace } from '@iyulab/u-board/toolkit';
8
+ /**
9
+ * Whether the editor lays this board out: a canvas, or a grid — of parts, canvases, and grids of parts and canvases in
10
+ * its cells (one level, as the picture draws them). A grid in a cell is laid out on its own (`cellBoard`): the picture
11
+ * flattens it into the outer grid's rows, which the editor does not say back item by item.
12
+ */
13
+ export function layoutEditable(board) {
14
+ if (board.layout.kind === 'canvas')
15
+ return true;
16
+ return board.layout.items.every(entry => !isGridCell(entry) || entry.grid.every(inner => !isGridCell(inner)));
17
+ }
18
+ /** The id the picture draws a grid's canvas cell — entry `i` of its items — with; what it draws in the cell is named
19
+ * past `<id>:`. */
20
+ export function cellId(i) {
21
+ return `cell-${i}`;
22
+ }
23
+ /** The grid entry a canvas cell's id names, if the board has a canvas there. */
24
+ export function cellIndex(board, id) {
25
+ const match = /^cell-(\d+)$/.exec(id);
26
+ if (board.layout.kind !== 'grid' || match === null)
27
+ return undefined;
28
+ return isCanvasCell(board.layout.items[Number(match[1])]) ? Number(match[1]) : undefined;
29
+ }
30
+ /** The grid entry holding a grid that the picture's item `id` is drawn from — one of its parts (or their items), or a
31
+ * canvas in it (`cell-<i>-<j>`) — if it is one. */
32
+ export function gridCellOf(board, id) {
33
+ if (board.layout.kind !== 'grid')
34
+ return undefined;
35
+ const canvas = /^cell-(\d+)-\d+$/.exec(id);
36
+ if (canvas !== null)
37
+ return isGridCell(board.layout.items[Number(canvas[1])]) ? Number(canvas[1]) : undefined;
38
+ const part = id.split('/')[0];
39
+ const i = board.layout.items.findIndex(entry => isGridCell(entry) && entry.grid.some(inner => entryId(inner) === part));
40
+ return i === -1 ? undefined : i;
41
+ }
42
+ /** The cell of grid entry `i` — a canvas, or a grid — as a board of its own: the board, that cell its layout. */
43
+ export function cellBoard(board, i) {
44
+ if (board.layout.kind !== 'grid')
45
+ return undefined;
46
+ const entry = board.layout.items[i];
47
+ if (isCanvasCell(entry))
48
+ return { ...board, layout: { kind: 'canvas', ...entry.canvas } };
49
+ if (isGridCell(entry))
50
+ return { ...board, layout: { kind: 'grid', items: entry.grid } };
51
+ return undefined;
52
+ }
53
+ /** The board with grid entry `i`'s cell — and its parts, triggers and all the rest — as `cell` (one `cellBoard` gave,
54
+ * edited) has them. */
55
+ export function withCell(board, i, cell) {
56
+ if (board.layout.kind !== 'grid')
57
+ return board;
58
+ const entry = board.layout.items[i];
59
+ let next;
60
+ if (isCanvasCell(entry) && cell.layout.kind === 'canvas') {
61
+ const { kind: _kind, ...canvas } = cell.layout;
62
+ next = { ...entry, canvas };
63
+ }
64
+ else if (isGridCell(entry) && cell.layout.kind === 'grid')
65
+ next = { ...entry, grid: cell.layout.items };
66
+ else
67
+ return board;
68
+ return { ...cell, layout: { ...board.layout, items: board.layout.items.map((e, j) => (j === i ? next : e)) } };
69
+ }
70
+ function isCanvasCell(entry) {
71
+ return typeof entry === 'object' && !Array.isArray(entry) && entry.canvas !== undefined;
72
+ }
73
+ function isGridCell(entry) {
74
+ return typeof entry === 'object' && !Array.isArray(entry) && entry.grid !== undefined;
75
+ }
76
+ /** The id the picture draws a board's own link with — `link.<id>`. */
77
+ export const LINK = 'link.';
78
+ /** The link of a canvas board a picture connector `id` draws, if it is one of the board's own (not a group's). */
79
+ export function linkOf(board, id) {
80
+ if (board.layout.kind !== 'canvas' || !id.startsWith(LINK))
81
+ return undefined;
82
+ const link = id.slice(LINK.length);
83
+ return board.layout.links?.[link] !== undefined ? link : undefined;
84
+ }
85
+ /** The id the picture draws a board's own shape with — `shape.<id>`. */
86
+ export const SHAPE = 'shape.';
87
+ /** The shape of a canvas board a picture piece `id` draws, if it is one of the board's own — a box or a text (drawn as a
88
+ * decoration), or a line or a path (drawn as a node of its own). */
89
+ export function shapeOf(board, id) {
90
+ if (board.layout.kind !== 'canvas' || !id.startsWith(SHAPE))
91
+ return undefined;
92
+ return board.layout.shapes?.[id.slice(SHAPE.length)] !== undefined ? id.slice(SHAPE.length) : undefined;
93
+ }
94
+ /** The line or path of a canvas board a picture node `id` draws, if it is one. */
95
+ export function lineOf(board, id) {
96
+ const shape = shapeOf(board, id);
97
+ const drawn = shape !== undefined && board.layout.kind === 'canvas' ? board.layout.shapes[shape] : undefined;
98
+ return drawn && (drawn.line !== undefined || drawn.path !== undefined) ? shape : undefined;
99
+ }
100
+ /** A line or path moved and sized as its node was, from box `from` to box `to`: each point where it stands in the box. */
101
+ export function shapeIn(shape, from, to) {
102
+ const sx = from.width === 0 ? 1 : to.width / from.width;
103
+ const sy = from.height === 0 ? 1 : to.height / from.height;
104
+ const px = (x) => Math.round(to.x + (x - from.x) * sx);
105
+ const py = (y) => Math.round(to.y + (y - from.y) * sy);
106
+ let k = 0;
107
+ if (shape.line !== undefined)
108
+ return { ...shape, line: shape.line.map(([x, y]) => [px(x), py(y)]) };
109
+ if (shape.path !== undefined)
110
+ return { ...shape, path: shape.path.replace(/-?\d*\.?\d+/g, n => String(k++ % 2 === 0 ? px(Number(n)) : py(Number(n)))) };
111
+ return shape;
112
+ }
113
+ /** The board with a new shape — a box, a text, or a line — at the canvas's top left, stepped from the shapes it has, and its
114
+ * id. */
115
+ export function withNewShape(board, kind, text) {
116
+ if (board.layout.kind !== 'canvas')
117
+ return { board, id: '' };
118
+ const shapes = board.layout.shapes ?? {};
119
+ let n = Object.keys(shapes).length + 1;
120
+ while (`shape-${n}` in shapes)
121
+ n++;
122
+ const id = `shape-${n}`;
123
+ const step = 24 * (n - 1);
124
+ const shape = kind === 'box' ? { box: [20 + step, 20 + step, 240, 160] } : kind === 'line' ? { line: [[20 + step, 20 + step], [220 + step, 20 + step]] } : { text, at: [20 + step, 20 + step] };
125
+ return { board: { ...board, layout: { ...board.layout, shapes: { ...shapes, [id]: shape } } }, id };
126
+ }
127
+ /** The board with a new link from part `from` to part `to` — drawn with an arrowhead, as a flow is — and its id. */
128
+ export function withNewLink(board, from, to) {
129
+ if (board.layout.kind !== 'canvas')
130
+ return { board, id: '' };
131
+ const links = board.layout.links ?? {};
132
+ let n = Object.keys(links).length + 1;
133
+ while (`link-${n}` in links)
134
+ n++;
135
+ const id = `link-${n}`;
136
+ return { board: { ...board, layout: { ...board.layout, links: { ...links, [id]: { from, to, arrow: 'end' } } } }, id };
137
+ }
138
+ /** Whether the picture node or item `id` is one the board places itself: a part, or an item of an `each` part — not a
139
+ * part inside a group, which moves with its group. */
140
+ export function placesItself(board, id) {
141
+ if (!layoutEditable(board))
142
+ return false;
143
+ if (lineOf(board, id) !== undefined)
144
+ return true;
145
+ const [part, key, ...rest] = id.split('/');
146
+ if (board.layout.kind === 'grid') {
147
+ if (cellIndex(board, id) !== undefined)
148
+ return true;
149
+ // A part in a grid cell is laid out in that cell's own grid.
150
+ if (gridCellOf(board, id) !== undefined)
151
+ return false;
152
+ const each = board.parts[part]?.each !== undefined;
153
+ return rest.length === 0 && (key === undefined ? !each : each) && board.layout.items.some(entry => entryId(entry) === part);
154
+ }
155
+ const placed = board.layout.place[part];
156
+ if (placed === undefined || rest.length > 0)
157
+ return false;
158
+ // Items placed by their own fields, or by a rule, are not dragged: the fields and the rule say where they are.
159
+ return key === undefined ? Array.isArray(placed) : board.parts[part]?.each !== undefined && !Array.isArray(placed) && !isDataPlace(placed) && !isFlowPlace(placed);
160
+ }
161
+ /**
162
+ * The board with its canvas layout as the picture `after` shows it, the picture having been `before`: each part (or
163
+ * item) that moved or was sized placed where it is now, each part deleted gone — with the links and triggers that
164
+ * named it. An item of an `each` part is there while its data has it, so deleting one deletes nothing.
165
+ */
166
+ export function withPictureLayout(board, before, after) {
167
+ if (board.layout.kind === 'grid')
168
+ return before.kind === 'grid' && after.kind === 'grid' ? withGridLayout(board, before, after) : board;
169
+ if (before.kind !== 'canvas' || after.kind !== 'canvas')
170
+ return board;
171
+ const layout = { ...board.layout, place: { ...board.layout.place } };
172
+ const parts = { ...board.parts };
173
+ const was = new Map(before.nodes.map(node => [node.id, node]));
174
+ const now = new Map(after.nodes.map(node => [node.id, node]));
175
+ let changed = false;
176
+ for (const [id, node] of now) {
177
+ const old = was.get(id);
178
+ if (!old || !placesItself(board, id) || sameBox(old, node))
179
+ continue;
180
+ const line = lineOf(board, id);
181
+ if (line !== undefined) {
182
+ const box = (n) => ({ x: n.x, y: n.y, width: n.width ?? old.width ?? 0, height: n.height ?? old.height ?? 0 });
183
+ layout.shapes = { ...layout.shapes, [line]: shapeIn(layout.shapes[line], box(old), box(node)) };
184
+ changed = true;
185
+ continue;
186
+ }
187
+ const [part, key] = id.split('/');
188
+ const at = placeOf(old, node);
189
+ layout.place[part] = key === undefined ? at : { ...layout.place[part], [key]: at };
190
+ changed = true;
191
+ }
192
+ // A shape of the board's own moved, sized or deleted on the canvas: its decoration (`shape.<id>`).
193
+ const shapesNow = new Map((after.decorations ?? []).map(d => [d.id, d]));
194
+ for (const old of before.decorations ?? []) {
195
+ const id = shapeOf(board, old.id);
196
+ if (id === undefined)
197
+ continue;
198
+ const now = shapesNow.get(old.id);
199
+ const shapes = { ...layout.shapes };
200
+ const size = (d) => (d.type === 'rect' ? `${d.width}x${d.height}` : '');
201
+ if (now === undefined)
202
+ delete shapes[id];
203
+ else if (now.x === old.x && now.y === old.y && size(now) === size(old))
204
+ continue;
205
+ else if (now.type === 'rect' && shapes[id].box !== undefined)
206
+ shapes[id] = { ...shapes[id], box: [Math.round(now.x), Math.round(now.y), Math.round(now.width), Math.round(now.height)] };
207
+ else
208
+ shapes[id] = { ...shapes[id], at: [Math.round(now.x), Math.round(now.y)] };
209
+ layout.shapes = shapes;
210
+ changed = true;
211
+ }
212
+ // A link deleted on the canvas: its connector (`link.<id>`, the board's own links) gone from the picture.
213
+ const nowLinks = new Set(after.connectors.map(c => c.id));
214
+ const linksGone = before.connectors.map(c => c.id).filter(id => !nowLinks.has(id) && id.startsWith(LINK) && layout.links?.[id.slice(LINK.length)] !== undefined);
215
+ if (linksGone.length > 0) {
216
+ layout.links = Object.fromEntries(Object.entries(layout.links).filter(([id]) => !linksGone.includes(`${LINK}${id}`)));
217
+ changed = true;
218
+ }
219
+ const removed = [...was.keys()].filter(id => !now.has(id) && placesItself(board, id));
220
+ for (const line of removed.flatMap(id => lineOf(board, id) ?? [])) {
221
+ const { [line]: _gone, ...rest } = layout.shapes;
222
+ layout.shapes = rest;
223
+ changed = true;
224
+ }
225
+ const gone = removed.filter(id => !id.includes('/') && lineOf(board, id) === undefined);
226
+ for (const part of gone) {
227
+ delete parts[part];
228
+ delete layout.place[part];
229
+ changed = true;
230
+ }
231
+ if (!changed)
232
+ return board;
233
+ const names = named(gone);
234
+ if (gone.length > 0 && layout.links) {
235
+ layout.links = Object.fromEntries(Object.entries(layout.links).filter(([, link]) => !names(link.from) && !names(link.to)));
236
+ }
237
+ const triggers = gone.length > 0 ? board.triggers?.filter(t => !mentions(t, names)) : board.triggers;
238
+ return { ...board, parts, layout, ...(triggers !== undefined ? { triggers } : {}) };
239
+ }
240
+ /**
241
+ * A grid of parts and canvases as its picture `after` lays it out: the entries in the order they now come (an `each`
242
+ * part where its first item is), each taking the columns its item was given — one of an `each` part's items setting
243
+ * them all — and an entry deleted gone: a part with the triggers that named it, a canvas with the parts placed on it.
244
+ * A part the picture did not draw (an `each` part whose data has no items now) keeps its place.
245
+ */
246
+ function withGridLayout(board, before, after) {
247
+ if (board.layout.kind !== 'grid' || !layoutEditable(board))
248
+ return board;
249
+ // An entry by the id its item is drawn with: a part's (an `each` part's, its items'), a canvas's `cell-<i>`; a grid
250
+ // in a cell by `grid-<i>`, which every item drawn from it stands for — it keeps its own layout, coming where its first
251
+ // item does.
252
+ const entries = board.layout.items;
253
+ const keyOf = (entry, i) => (isCanvasCell(entry) ? cellId(i) : isGridCell(entry) ? `grid-${i}` : entryId(entry));
254
+ const byKey = new Map(entries.map((entry, i) => [keyOf(entry, i), entry]));
255
+ const partOf = (id) => {
256
+ const nested = gridCellOf(board, id);
257
+ return nested !== undefined ? `grid-${nested}` : id.split('/')[0];
258
+ };
259
+ const spanWas = new Map(before.items.map(item => [item.id, item.span ?? GRID_DEFAULT_SPAN]));
260
+ const span = new Map();
261
+ for (const item of after.items) {
262
+ const part = partOf(item.id);
263
+ const now = item.span ?? GRID_DEFAULT_SPAN;
264
+ if (!span.has(part) || now !== spanWas.get(item.id))
265
+ span.set(part, now);
266
+ }
267
+ const drawn = new Set(before.items.map(item => partOf(item.id)));
268
+ const order = [...new Set(after.items.map(item => partOf(item.id)))];
269
+ const gone = [...drawn].filter(part => !span.has(part));
270
+ // A part not drawn now keeps its place among the others: it goes back after the drawn part it followed, or first.
271
+ // An entry keeps its form — a part with a height, a canvas — taking only its columns from the picture.
272
+ const entryOf = (key) => {
273
+ const entry = byKey.get(key);
274
+ if (isGridCell(entry))
275
+ return entry;
276
+ const columns = span.get(key);
277
+ if (typeof entry === 'string' || Array.isArray(entry))
278
+ return columns === GRID_DEFAULT_SPAN ? key : [key, columns];
279
+ const { span: _span, ...rest } = entry;
280
+ return columns === GRID_DEFAULT_SPAN && entry.span === undefined ? rest : { ...rest, span: columns };
281
+ };
282
+ /** The entries of parts not drawn that come straight after entry `at` (-1: before the first). */
283
+ const following = (at) => {
284
+ const out = [];
285
+ for (let i = at + 1; i < entries.length && !drawn.has(keyOf(entries[i], i)); i++)
286
+ out.push(entries[i]);
287
+ return out;
288
+ };
289
+ const items = [...following(-1)];
290
+ for (const key of order)
291
+ items.push(entryOf(key), ...following(entries.findIndex((entry, i) => keyOf(entry, i) === key)));
292
+ if (gone.length === 0 && JSON.stringify(items) === JSON.stringify(board.layout.items))
293
+ return board;
294
+ // A canvas deleted takes the parts placed on it.
295
+ const goneParts = gone.flatMap(key => {
296
+ const entry = byKey.get(key);
297
+ return isCanvasCell(entry) ? Object.keys(entry.canvas.place) : [key];
298
+ });
299
+ const parts = { ...board.parts };
300
+ for (const part of goneParts)
301
+ delete parts[part];
302
+ const triggers = goneParts.length > 0 ? board.triggers?.filter(t => !mentions(t, named(goneParts))) : board.triggers;
303
+ return { ...board, parts, layout: { ...board.layout, items }, ...(triggers !== undefined ? { triggers } : {}) };
304
+ }
305
+ /** The part a grid entry places. */
306
+ function entryId(entry) {
307
+ return typeof entry === 'string' ? entry : Array.isArray(entry) ? entry[0] : (entry.id ?? '');
308
+ }
309
+ /** Whether an id names one of `gone` — the part, or something in it. */
310
+ function named(gone) {
311
+ return id => id !== undefined && gone.some(part => id === part || id.startsWith(`${part}/`));
312
+ }
313
+ /** Where a node is now, as a place: with its size when it was sized, or is now another size than it was drawn. */
314
+ function placeOf(old, node) {
315
+ const width = node.width ?? old.width ?? DEFAULT_NODE_WIDTH;
316
+ const height = node.height ?? old.height ?? DEFAULT_NODE_HEIGHT;
317
+ const sized = old.width !== undefined || old.height !== undefined || width !== DEFAULT_NODE_WIDTH || height !== DEFAULT_NODE_HEIGHT;
318
+ return sized ? [node.x, node.y, width, height] : [node.x, node.y];
319
+ }
320
+ function sameBox(a, b) {
321
+ return a.x === b.x && a.y === b.y && (b.width === undefined || b.width === (a.width ?? DEFAULT_NODE_WIDTH)) && (b.height === undefined || b.height === (a.height ?? DEFAULT_NODE_HEIGHT));
322
+ }
323
+ /** Whether a trigger names a part `names` says is gone — what it reacts to, waits on or acts on. */
324
+ function mentions(t, names) {
325
+ return ('part' in t.on && names(t.on.part)) || names(t.when?.part) || t.do.some(a => a.action === 'focus' && names(a.part));
326
+ }
@@ -0,0 +1,5 @@
1
+ import type { Board } from '@iyulab/u-board/toolkit';
2
+ import type { TriggerEditing } from './TriggersEditor.js';
3
+ /** What the triggers editor needs to edit `board`'s triggers: them in its words, the parts and statuses it offers, and
4
+ * how each change is written back (`edit`). `parts` are the ids a trigger may name — a part made per item by item. */
5
+ export declare function boardTriggerEditing(board: Board, parts: readonly string[], edit: (update: (current: Board) => Board) => void): TriggerEditing;
@@ -0,0 +1,55 @@
1
+ // The triggers of a board, edited with the triggers editor of its picture: each trigger
2
+ // shown in the picture's words (a part is a node, a status a state, a link fills in `{{node}}` and `{{state}}`) and
3
+ // written back in the board's — keeping the `field` a trigger reads a status of while it still names the same part.
4
+ /** What the triggers editor needs to edit `board`'s triggers: them in its words, the parts and statuses it offers, and
5
+ * how each change is written back (`edit`). `parts` are the ids a trigger may name — a part made per item by item. */
6
+ export function boardTriggerEditing(board, parts, edit) {
7
+ const statuses = Object.keys(board.statuses ?? {});
8
+ const write = (update) => edit(current => {
9
+ const triggers = update([...(current.triggers ?? [])]);
10
+ const { triggers: _gone, ...rest } = current;
11
+ return triggers.length > 0 ? { ...rest, triggers } : rest;
12
+ });
13
+ return {
14
+ triggers: board.triggers?.map(shown),
15
+ parts,
16
+ // An item of an `each` part is a node `<part>/<key>`: a trigger on it may be for every item of the part.
17
+ eachOf: node => (node.includes('/') && board.parts[node.split('/')[0]]?.each !== undefined ? node.split('/')[0] : undefined),
18
+ stateNamesOf: () => statuses,
19
+ onAdd: trigger => write(triggers => [...triggers, written(trigger)]),
20
+ onReplace: (index, trigger) => write(triggers => triggers.map((t, i) => (i === index ? written(trigger, t) : t))),
21
+ onRemove: index => write(triggers => triggers.filter((_, i) => i !== index)),
22
+ };
23
+ }
24
+ /** A board's trigger in its picture's words. */
25
+ function shown(t) {
26
+ const on = t.on.event === 'open' ? { event: 'open' }
27
+ : t.on.event === 'click' ? { event: 'click', node: t.on.part }
28
+ : t.on.event === 'element' ? { event: 'element', node: t.on.part, name: t.on.name }
29
+ : { event: t.on.event, node: t.on.part, state: t.on.status };
30
+ return {
31
+ on,
32
+ ...(t.when !== undefined ? { when: { node: t.when.part, state: t.when.status } } : {}),
33
+ do: t.do.map((a) => (a.action === 'focus' ? { action: 'focus', node: a.part }
34
+ : a.action === 'open-link' ? { action: 'open-link', url: a.url.replace(/\{\{\s*part\s*\}\}/g, '{{node}}').replace(/\{\{\s*status\s*\}\}/g, '{{state}}') }
35
+ : a)),
36
+ };
37
+ }
38
+ /** A trigger the editor wrote, in the board's words — the `field` of `was` kept where it still names the same part. */
39
+ function written(t, was) {
40
+ const field = (part, before) => before?.field !== undefined && before.part === part ? { field: before.field } : {};
41
+ const before = was && 'part' in was.on ? was.on : undefined;
42
+ const on = t.on.event === 'open' ? { event: 'open' }
43
+ : t.on.event === 'click' ? { event: 'click', part: t.on.node }
44
+ : t.on.event === 'element' ? { event: 'element', part: t.on.node, name: t.on.name }
45
+ : { event: t.on.event, part: t.on.node, ...field(t.on.node, before), status: t.on.state };
46
+ return {
47
+ on,
48
+ ...(t.when !== undefined
49
+ ? { when: { part: t.when.node, ...field(t.when.node, was?.when), status: Array.isArray(t.when.state) && t.when.state.length === 1 ? t.when.state[0] : t.when.state } }
50
+ : {}),
51
+ do: t.do.map((a) => (a.action === 'focus' ? { action: 'focus', part: a.node }
52
+ : a.action === 'open-link' ? { action: 'open-link', url: a.url.replace(/\{\{\s*node\s*\}\}/g, '{{part}}').replace(/\{\{\s*state\s*\}\}/g, '{{status}}') }
53
+ : a)),
54
+ };
55
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The document being edited with the steps before and after it — what Undo and Redo walk. Each change the editor
3
+ * makes is a step, except that typing on in the same field adds to the step the field's first keystroke began: a
4
+ * name typed is one step, not one per letter.
5
+ */
6
+ export interface DocumentHistory<D> {
7
+ readonly present: D;
8
+ readonly past: readonly D[];
9
+ readonly future: readonly D[];
10
+ /** The field the last step was typed into, while typing there goes on. */
11
+ readonly typingIn: object | null;
12
+ }
13
+ /** How many steps back Undo reaches. */
14
+ export declare const HISTORY_LIMIT = 100;
15
+ export declare function startHistory<D>(doc: D): DocumentHistory<D>;
16
+ /** `next` as the document now. `typingIn` is the text field the change was typed into, or null. */
17
+ export declare function recordEdit<D>(history: DocumentHistory<D>, next: D, typingIn: object | null): DocumentHistory<D>;
18
+ /** Focus has moved: what is typed next is a step of its own, even in the same field. */
19
+ export declare function endTyping<D>(history: DocumentHistory<D>): DocumentHistory<D>;
20
+ export declare function undo<D>(history: DocumentHistory<D>): DocumentHistory<D>;
21
+ export declare function redo<D>(history: DocumentHistory<D>): DocumentHistory<D>;
22
+ /** A field a person types into, one change per keystroke — not a checkbox, a button or a list to pick from. */
23
+ export declare function textEntry(element: Element | null): HTMLElement | null;
@@ -0,0 +1,37 @@
1
+ /** How many steps back Undo reaches. */
2
+ export const HISTORY_LIMIT = 100;
3
+ export function startHistory(doc) {
4
+ return { present: doc, past: [], future: [], typingIn: null };
5
+ }
6
+ /** `next` as the document now. `typingIn` is the text field the change was typed into, or null. */
7
+ export function recordEdit(history, next, typingIn) {
8
+ if (next === history.present)
9
+ return history;
10
+ if (typingIn && typingIn === history.typingIn)
11
+ return { ...history, present: next, future: [] };
12
+ return { present: next, past: [...history.past, history.present].slice(-HISTORY_LIMIT), future: [], typingIn };
13
+ }
14
+ /** Focus has moved: what is typed next is a step of its own, even in the same field. */
15
+ export function endTyping(history) {
16
+ return history.typingIn ? { ...history, typingIn: null } : history;
17
+ }
18
+ export function undo(history) {
19
+ if (history.past.length === 0)
20
+ return history;
21
+ return { present: history.past[history.past.length - 1], past: history.past.slice(0, -1), future: [history.present, ...history.future], typingIn: null };
22
+ }
23
+ export function redo(history) {
24
+ if (history.future.length === 0)
25
+ return history;
26
+ return { present: history.future[0], past: [...history.past, history.present], future: history.future.slice(1), typingIn: null };
27
+ }
28
+ /** A field a person types into, one change per keystroke — not a checkbox, a button or a list to pick from. */
29
+ export function textEntry(element) {
30
+ if (!(element instanceof HTMLElement))
31
+ return null;
32
+ if (element.isContentEditable || element instanceof HTMLTextAreaElement)
33
+ return element;
34
+ if (element instanceof HTMLInputElement && !['checkbox', 'radio', 'button', 'submit', 'reset', 'file', 'image'].includes(element.type))
35
+ return element;
36
+ return null;
37
+ }
@@ -0,0 +1,46 @@
1
+ import { type DesignerLabels } from '../labels.js';
2
+ import type { ReactNode } from 'react';
3
+ import type { Adapter } from '@iyulab/u-board/toolkit';
4
+ import type { ViewDocument } from '@iyulab/u-board/picture';
5
+ import type { TriggerHandlers } from '@iyulab/u-board/toolkit';
6
+ export type AuthoringMode = 'edit' | 'view';
7
+ /** What `BoardAuthoring` hands the editor of the picture's kind: the picture and the one way to change it, the mode,
8
+ * the toolbar's shared actions, and what the host gave the view. The editor owns its surface, its selection and its
9
+ * own toolbar actions. */
10
+ export interface KindEditorProps<D extends ViewDocument> {
11
+ doc: D;
12
+ /** Replaces the document with what `update` makes of the current one. */
13
+ updateDoc: (update: (current: D) => D) => void;
14
+ mode: AuthoringMode;
15
+ onModeChange: (mode: AuthoringMode) => void;
16
+ /** Undo and Redo, Save and Export — the same for every kind. */
17
+ documentActions: ReactNode;
18
+ adapters: readonly Adapter[];
19
+ width?: number;
20
+ height?: number;
21
+ labels: DesignerLabels;
22
+ clock: () => number;
23
+ /** What the board's triggers hand the host in View — Edit runs none (a click there selects). */
24
+ triggerHosting: TriggerHosting;
25
+ /**
26
+ * The board the picture is drawn from, which keeps everything but where its parts are: parts are moved, sized and
27
+ * deleted here, and `panel` draws the panel for what is selected (`null`: nothing, or more than one) — or nothing, for
28
+ * a hint. `movable` says which parts the board places itself; `connect`, when given, joins two of them, first to
29
+ * second, and answers the id the connector is drawn with — what may be deleted besides is `deletable`.
30
+ */
31
+ parts: {
32
+ movable: (id: string) => boolean;
33
+ panel: (selected: string | null) => ReactNode;
34
+ connect?: (from: string, to: string) => string;
35
+ deletable?: (id: string) => boolean;
36
+ };
37
+ }
38
+ /** The host's side of a board's triggers — `ViewerPage`'s props of the same names. */
39
+ export type TriggerHosting = Pick<TriggerHandlers, 'boardId' | 'onTrigger' | 'onNavigate' | 'linkOrigins'>;
40
+ /** The Edit / View switch. */
41
+ export declare function ModeSwitch({ mode, onEdit, onView, labels }: {
42
+ mode: AuthoringMode;
43
+ onEdit: () => void;
44
+ onView: () => void;
45
+ labels: DesignerLabels;
46
+ }): import("react").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { GROUP_STYLE } from '@iyulab/u-board/toolkit';
3
+ /** The Edit / View switch. */
4
+ export function ModeSwitch({ mode, onEdit, onView, labels }) {
5
+ return (_jsxs("div", { className: "ub-authoring__group ub-authoring__mode", style: GROUP_STYLE, role: "group", "aria-label": labels.mode, children: [_jsx("button", { type: "button", className: "ub-action", "aria-pressed": mode === 'edit', onClick: onEdit, children: labels.editMode }), _jsx("button", { type: "button", className: "ub-action", "aria-pressed": mode === 'view', onClick: onView, children: labels.viewMode })] }));
6
+ }
@@ -0,0 +1,3 @@
1
+ import type { CSSProperties } from 'react';
2
+ /** One field per line, its label above it — without the stylesheet as well. */
3
+ export declare const FIELD_STYLE: CSSProperties;
@@ -0,0 +1,2 @@
1
+ /** One field per line, its label above it — without the stylesheet as well. */
2
+ export const FIELD_STYLE = { display: 'flex', flexDirection: 'column' };
@@ -0,0 +1,6 @@
1
+ import type { CanvasView } from '@iyulab/u-board/picture';
2
+ /**
3
+ * How the editor names a node of `doc` to an author — by its caption, else by its widget's type and its place among the
4
+ * board's nodes ("Status 2"). Anything else by its id.
5
+ */
6
+ export declare function partNamer(doc: CanvasView, locale?: string): (id: string) => string;
@@ -0,0 +1,9 @@
1
+ import { widgetTypeName } from './widget-catalog.js';
2
+ /**
3
+ * How the editor names a node of `doc` to an author — by its caption, else by its widget's type and its place among the
4
+ * board's nodes ("Status 2"). Anything else by its id.
5
+ */
6
+ export function partNamer(doc, locale) {
7
+ const named = new Map(doc.nodes.map((node, i) => [node.id, node.caption || `${widgetTypeName(node.widget.type, locale)} ${i + 1}`]));
8
+ return id => named.get(id) ?? id;
9
+ }
@@ -0,0 +1,22 @@
1
+ import { Scene } from '@canvas-kit/core';
2
+ import type { CanvasView } from '@iyulab/u-board/picture';
3
+ /**
4
+ * Projects a ViewDocument into a canvas-kit Scene the designer can render and let the author
5
+ * drag: the background image (locked, drawn but not editable), the connectors (fixed: selected to edit
6
+ * them, never dragged — they follow their nodes), and one `rect` per node, id'd by node id, standing in
7
+ * for its widget's footprint. A scene object's id names its document part, after a `#` for a part's
8
+ * pieces (`partOf`).
9
+ */
10
+ export declare function documentToScene(doc: CanvasView, movable?: (id: string) => boolean, selectable?: (id: string) => boolean): Scene;
11
+ /**
12
+ * Folds a designer Scene's node-rect and decoration-shape positions back into a ViewDocument.
13
+ * Reads every scene object whose id matches a node or decoration — not just the one the author
14
+ * just dragged — so it stays correct regardless of how the designer reports the change (single
15
+ * move, batched drag, resize). A node and a decoration can both be scene `rect`s with the same
16
+ * shape; they're told apart by id membership in `doc.nodes`/`doc.decorations`, not by scanning
17
+ * order, so a decoration id never gets folded onto a node or vice versa.
18
+ */
19
+ export declare function applySceneToDocument(doc: CanvasView, scene: Scene): CanvasView;
20
+ /** The document part a scene object draws: its id, or a part's own id before the `#` of one of its pieces (a
21
+ * connector's `#chip`, `#arrow0`). */
22
+ export declare function partOf(objectId: string): string;