@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.
- package/LICENSE +661 -0
- package/README.md +127 -2
- package/dist/lib/authoring/AnchorFields.d.ts +12 -0
- package/dist/lib/authoring/AnchorFields.js +64 -0
- package/dist/lib/authoring/BackgroundField.d.ts +13 -0
- package/dist/lib/authoring/BackgroundField.js +38 -0
- package/dist/lib/authoring/BoardAuthoring.d.ts +37 -0
- package/dist/lib/authoring/BoardAuthoring.js +288 -0
- package/dist/lib/authoring/BoardElementsPanel.d.ts +14 -0
- package/dist/lib/authoring/BoardElementsPanel.js +49 -0
- package/dist/lib/authoring/BoardPanel.d.ts +40 -0
- package/dist/lib/authoring/BoardPanel.js +22 -0
- package/dist/lib/authoring/CanvasAuthoring.d.ts +13 -0
- package/dist/lib/authoring/CanvasAuthoring.js +161 -0
- package/dist/lib/authoring/DataPanel.d.ts +29 -0
- package/dist/lib/authoring/DataPanel.js +98 -0
- package/dist/lib/authoring/GridAuthoring.d.ts +13 -0
- package/dist/lib/authoring/GridAuthoring.js +135 -0
- package/dist/lib/authoring/GridItemPanel.d.ts +22 -0
- package/dist/lib/authoring/GridItemPanel.js +21 -0
- package/dist/lib/authoring/LinkPanel.d.ts +19 -0
- package/dist/lib/authoring/LinkPanel.js +24 -0
- package/dist/lib/authoring/MappingEditor.d.ts +23 -0
- package/dist/lib/authoring/MappingEditor.js +83 -0
- package/dist/lib/authoring/PartPanel.d.ts +24 -0
- package/dist/lib/authoring/PartPanel.js +78 -0
- package/dist/lib/authoring/ShapePanel.d.ts +20 -0
- package/dist/lib/authoring/ShapePanel.js +54 -0
- package/dist/lib/authoring/StatesEditor.d.ts +18 -0
- package/dist/lib/authoring/StatesEditor.js +79 -0
- package/dist/lib/authoring/StatusesPanel.d.ts +14 -0
- package/dist/lib/authoring/StatusesPanel.js +32 -0
- package/dist/lib/authoring/TriggersEditor.d.ts +30 -0
- package/dist/lib/authoring/TriggersEditor.js +137 -0
- package/dist/lib/authoring/ValueMapFields.d.ts +64 -0
- package/dist/lib/authoring/ValueMapFields.js +68 -0
- package/dist/lib/authoring/WidgetPropsForm.d.ts +27 -0
- package/dist/lib/authoring/WidgetPropsForm.js +56 -0
- package/dist/lib/authoring/background-image.d.ts +25 -0
- package/dist/lib/authoring/background-image.js +63 -0
- package/dist/lib/authoring/board-layout.d.ts +64 -0
- package/dist/lib/authoring/board-layout.js +326 -0
- package/dist/lib/authoring/board-triggers.d.ts +5 -0
- package/dist/lib/authoring/board-triggers.js +55 -0
- package/dist/lib/authoring/document-history.d.ts +23 -0
- package/dist/lib/authoring/document-history.js +37 -0
- package/dist/lib/authoring/editor-parts.d.ts +46 -0
- package/dist/lib/authoring/editor-parts.js +6 -0
- package/dist/lib/authoring/field-style.d.ts +3 -0
- package/dist/lib/authoring/field-style.js +2 -0
- package/dist/lib/authoring/part-names.d.ts +6 -0
- package/dist/lib/authoring/part-names.js +9 -0
- package/dist/lib/authoring/scene-mapping.d.ts +22 -0
- package/dist/lib/authoring/scene-mapping.js +124 -0
- package/dist/lib/authoring/use-edit-keys.d.ts +13 -0
- package/dist/lib/authoring/use-edit-keys.js +57 -0
- package/dist/lib/authoring/widget-catalog.d.ts +4 -0
- package/dist/lib/authoring/widget-catalog.js +9 -0
- package/dist/lib/authoring/widget-fields.d.ts +45 -0
- package/dist/lib/authoring/widget-fields.js +129 -0
- package/dist/lib/board/check.d.ts +27 -0
- package/dist/lib/board/check.js +48 -0
- package/dist/lib/board/rulebook.d.ts +7 -0
- package/dist/lib/board/rulebook.js +148 -0
- package/dist/lib/check-entry.d.ts +3 -0
- package/dist/lib/check-entry.js +4 -0
- package/dist/lib/editing/grid-editing.d.ts +19 -0
- package/dist/lib/editing/grid-editing.js +68 -0
- package/dist/lib/editing/part-removal.d.ts +4 -0
- package/dist/lib/editing/part-removal.js +11 -0
- package/dist/lib/editing/trigger-editing.d.ts +16 -0
- package/dist/lib/editing/trigger-editing.js +36 -0
- package/dist/lib/index.d.ts +7 -0
- package/dist/lib/index.js +5 -0
- package/dist/lib/labels-ko.d.ts +4 -0
- package/dist/lib/labels-ko.js +270 -0
- package/dist/lib/labels.d.ts +368 -0
- package/dist/lib/labels.js +273 -0
- package/dist/lib/use-labels.d.ts +7 -0
- package/dist/lib/use-labels.js +12 -0
- package/package.json +75 -4
- package/styles.css +277 -0
package/README.md
CHANGED
|
@@ -1,3 +1,128 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @iyulab/u-board-designer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The authoring tool for [U-Board](https://github.com/iyulab/U-Board) boards — `BoardAuthoring`, a board edited in place —
|
|
4
|
+
and the board check and the rulebook an assistant writes by. It builds on
|
|
5
|
+
[`@iyulab/u-board`](https://www.npmjs.com/package/@iyulab/u-board), which holds the board language and the viewer.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @iyulab/u-board-designer @iyulab/u-board react react-dom
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`react` and `react-dom` (19.2 or later) are peer dependencies. The package is ESM-only and needs Node 22.12 or later.
|
|
14
|
+
|
|
15
|
+
## Entry points
|
|
16
|
+
|
|
17
|
+
| Import | Contents | Depends on React / canvas |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| `@iyulab/u-board-designer` | `BoardAuthoring`, `DesignerLabels`, `KO_DESIGNER_LABELS` | Yes |
|
|
20
|
+
| `@iyulab/u-board-designer/check` | `checkBoard`, `emptyBoard`, `sourceAdvice`, `boardRulebook`, `RULEBOOK_EXAMPLE` | No |
|
|
21
|
+
|
|
22
|
+
## Authoring a board
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { BoardAuthoring } from '@iyulab/u-board-designer';
|
|
26
|
+
|
|
27
|
+
<div style={{ height: '80vh' }}>
|
|
28
|
+
<BoardAuthoring initialBoard={board} adapters={adapters} onSave={next => save(next)} />
|
|
29
|
+
</div>;
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`BoardAuthoring` sizes the same way: without `width`/`height` the board fills the parent's width beside the panel, and
|
|
33
|
+
its height. The board opens fitted into view and is shown in one of two modes, switched from the toolbar with pan and
|
|
34
|
+
zoom kept. **Edit**: the board drawn as its viewers see it, each part's widget in place over its footprint, its kind
|
|
35
|
+
named above it; drag a part to move it, its handles to size it; drag across empty space to select several; select two
|
|
36
|
+
parts (Shift + click the second) to join them with a link; Delete removes the selection; on a grid, drag an item onto
|
|
37
|
+
another to move it there, and lay the grid out at a wide, medium or narrow width. **View**: the board exactly as a
|
|
38
|
+
shared link shows it, where a drag pans. The wheel zooms in both.
|
|
39
|
+
|
|
40
|
+
The panel edits what is selected — a part's title, kind, what it reads (a data read, and one item of it by key), the
|
|
41
|
+
fields it shows and its kind's options, with the controls the widget library describes; a link; a shape; a grid item's
|
|
42
|
+
span and height — and, with nothing selected, the board: its background, theme and accent, its data reads (reading a
|
|
43
|
+
source to offer the fields its items have), its statuses, its elements and its triggers. What the board's check finds is
|
|
44
|
+
listed where it applies, and a setting the board cannot be drawn with keeps the last picture it could, saying why. A
|
|
45
|
+
grid's cell — a canvas, or a grid of its own — opens in its own editor. Undo and Redo walk the edits; Save calls
|
|
46
|
+
`onSave` with the board (without it there is no Save), and Export downloads the board as a file. `sourceLabels` names
|
|
47
|
+
each adapter's source in the data panel.
|
|
48
|
+
|
|
49
|
+
### Text in another language
|
|
50
|
+
|
|
51
|
+
The tool shows English text by default. Pass `labels` — any subset of `DesignerLabels`, the rest stays English — to
|
|
52
|
+
show your own; `DEFAULT_DESIGNER_LABELS` lists every key with its English text. `DesignerLabels` is the viewer's
|
|
53
|
+
`UBoardLabels` plus the authoring texts, and `labels.locale` is the language of both: the panel names part kinds, data
|
|
54
|
+
fields and options in it, as the widget library names them. Korean ships as `KO_DESIGNER_LABELS`:
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import { BoardAuthoring, KO_DESIGNER_LABELS } from '@iyulab/u-board-designer';
|
|
58
|
+
|
|
59
|
+
<BoardAuthoring initialBoard={board} adapters={adapters} labels={KO_DESIGNER_LABELS} />;
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Checking a board
|
|
63
|
+
|
|
64
|
+
`checkBoard` is what a board meets before it is stored: the language, and then that it draws. `sourceAdvice` and the
|
|
65
|
+
rulebook (`boardRulebook`) are what an assistant reads to write a board well. They load without React, so a server or an
|
|
66
|
+
MCP process imports them from the `check` entry point:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { checkBoard, emptyBoard } from '@iyulab/u-board-designer/check';
|
|
70
|
+
|
|
71
|
+
const { issues } = checkBoard(emptyBoard());
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Styling
|
|
75
|
+
|
|
76
|
+
The components carry only the layout they need to work. Import the default stylesheet once, after the viewer's:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import '@iyulab/u-board/styles.css';
|
|
80
|
+
import '@iyulab/u-board-designer/styles.css';
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
It reads the same `--ub-*` custom properties as `@iyulab/u-board/styles.css` (listed in that package's README) and falls
|
|
84
|
+
back to its own values for any you leave out. These are the ones only the authoring view reads:
|
|
85
|
+
|
|
86
|
+
| Token | Used for |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `--ub-panel-width` | Width of the authoring view's panel (320 px by default) |
|
|
89
|
+
| `--ub-space-4` | A gap of 16 px by default |
|
|
90
|
+
|
|
91
|
+
The sheet's rules select these class names, mostly one class each: they hold against a host's element styles, and a host
|
|
92
|
+
rule on the same class, loaded after the sheet, overrides them.
|
|
93
|
+
|
|
94
|
+
| Class | Element |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `ub-action--primary` | The primary button (Save); with `ub-action`, which the viewer package styles |
|
|
97
|
+
| `ub-authoring` | The authoring view |
|
|
98
|
+
| `ub-authoring__toolbar`, `ub-authoring__group` | Its toolbar, and each group of related actions in it |
|
|
99
|
+
| `ub-authoring__appearance` | The board tone picker (light or dark board), with the board's settings in the panel |
|
|
100
|
+
| `ub-authoring__mode` | The Edit / View switch in the toolbar (the chosen button has `aria-pressed="true"`) |
|
|
101
|
+
| `ub-authoring__panes`, `ub-authoring__pane` | The row holding the board and the panel; the board's column |
|
|
102
|
+
| `ub-authoring__surface` | The frame the board draws in — the editor, or the board in view mode |
|
|
103
|
+
| `ub-authoring__node`, `ub-authoring__node-tag` | A part's widget drawn in place on the editor; its kind named above it |
|
|
104
|
+
| `ub-authoring__pick`, `ub-authoring__layout-width` | On a grid view's editor: the button over each item that selects it (`aria-pressed` when selected); the Wide / Medium / Narrow layout width switch |
|
|
105
|
+
| `ub-authoring__panel` | The column holding the panel |
|
|
106
|
+
| `ub-authoring__error`, `ub-authoring__status` | A setting the board cannot be drawn with; "resolving" |
|
|
107
|
+
| `ub-panel` (`--board`, `--item`, `--elements`, `--part`, `--link`, `--shape`, `--data`, `--statuses`) | The board's settings (while nothing is selected), a grid item's layout (its widths, height and place in the order), the board's elements, a part, a link or a shape, or the board's data or statuses |
|
|
108
|
+
| `ub-panel__heading`, `ub-panel__subheading`, `ub-panel__field`, `ub-panel__hint`, `ub-panel__error` | Its heading, section heading, a labelled field, hint text and an error |
|
|
109
|
+
| `ub-panel__findings` | What a board's check finds at the selected part, listed under its settings |
|
|
110
|
+
| `ub-panel__widget-form`, `ub-panel__group` | The widget's own fields, and each group of them (data, display options) |
|
|
111
|
+
| `ub-panel__details` | A fold for the finer settings of a data read (which items, their time, the request time) |
|
|
112
|
+
| `ub-panel__field-name`, `ub-panel__field--check` | A field's name (with its key); a checkbox field |
|
|
113
|
+
| `ub-panel__binding-tag` | A field's "mapped" tag |
|
|
114
|
+
| `ub-panel__value-map`, `ub-panel__map-row`, `ub-panel__actions` | A value map, a row of the map, and its buttons |
|
|
115
|
+
| `ub-panel__section` | A section of a panel under its own subheading — a part’s states, its triggers |
|
|
116
|
+
| `ub-panel__state-looks`, `ub-panel__state-look` | How each state is shown, and one state’s row |
|
|
117
|
+
| `ub-panel__trigger`, `ub-panel__trigger-action` | One trigger, and one of its actions |
|
|
118
|
+
|
|
119
|
+
## Versioning
|
|
120
|
+
|
|
121
|
+
Pre-1.0: a minor version may change the public API.
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
Copyright (c) 2026 iyulab.
|
|
126
|
+
|
|
127
|
+
AGPL-3.0-or-later. A commercial license is available for organizations that cannot adopt AGPL-3.0
|
|
128
|
+
terms.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type DesignerLabels } from '../labels.js';
|
|
2
|
+
import type { ReferencePoint } from '@iyulab/u-board/picture';
|
|
3
|
+
/**
|
|
4
|
+
* The background image's reference points (`BackgroundImage.referencePoints`): two points of the image, in
|
|
5
|
+
* the board's units, and the coordinate each stands for. Saved once all eight numbers are in and the points
|
|
6
|
+
* are apart on both axes; cleared together.
|
|
7
|
+
*/
|
|
8
|
+
export declare function ReferencePointsFields({ points, onChange, labels, }: {
|
|
9
|
+
points: readonly ReferencePoint[] | undefined;
|
|
10
|
+
onChange: (points: [ReferencePoint, ReferencePoint] | undefined) => void;
|
|
11
|
+
labels: DesignerLabels;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { ERROR_STYLE } from '@iyulab/u-board/toolkit';
|
|
4
|
+
const FIELD_STYLE = { display: 'flex', flexDirection: 'column' };
|
|
5
|
+
const ROW_STYLE = { display: 'flex', gap: 'var(--ub-space-2, 8px)', alignItems: 'end', flexWrap: 'wrap' };
|
|
6
|
+
const NUMBER_STYLE = { width: '6.5em', minWidth: 0 };
|
|
7
|
+
/** A number as typed, or `null` when it is not one. */
|
|
8
|
+
function parsed(text) {
|
|
9
|
+
const value = Number(text);
|
|
10
|
+
return text.trim() !== '' && Number.isFinite(value) ? value : null;
|
|
11
|
+
}
|
|
12
|
+
/** A coordinate as the field shows it — enough digits for a longitude to a metre, without a float's tail. */
|
|
13
|
+
const shown = (value) => String(Number(value.toPrecision(10)));
|
|
14
|
+
const draftOf = (points) => [0, 1].map(i => {
|
|
15
|
+
const point = points?.[i];
|
|
16
|
+
return point ? [shown(point.x), shown(point.y), shown(point.coordinate.x), shown(point.coordinate.y)] : ['', '', '', ''];
|
|
17
|
+
});
|
|
18
|
+
/** The two points typed in, when every field is a number and the points are apart on both axes in the image
|
|
19
|
+
* and in their coordinates — or `null` with why not: `incomplete` or `degenerate`. */
|
|
20
|
+
function pointsOf(draft) {
|
|
21
|
+
const values = draft.map(row => row.map(parsed));
|
|
22
|
+
if (values.some(row => row.some(v => v === null)))
|
|
23
|
+
return 'incomplete';
|
|
24
|
+
const [a, b] = values.map(([x, y, cx, cy]) => ({ x: x, y: y, coordinate: { x: cx, y: cy } }));
|
|
25
|
+
if (a.x === b.x || a.y === b.y || a.coordinate.x === b.coordinate.x || a.coordinate.y === b.coordinate.y)
|
|
26
|
+
return 'degenerate';
|
|
27
|
+
return [a, b];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The background image's reference points (`BackgroundImage.referencePoints`): two points of the image, in
|
|
31
|
+
* the board's units, and the coordinate each stands for. Saved once all eight numbers are in and the points
|
|
32
|
+
* are apart on both axes; cleared together.
|
|
33
|
+
*/
|
|
34
|
+
export function ReferencePointsFields({ points, onChange, labels, }) {
|
|
35
|
+
const [draft, setDraft] = useState(() => draftOf(points));
|
|
36
|
+
// The points this form last saved. When they come back as `points`, the fields already say them, as the
|
|
37
|
+
// author typed them — rewriting them in normal form would turn "35.0" into "35" mid-keystroke. Points
|
|
38
|
+
// from elsewhere (another background, an undo) replace the fields.
|
|
39
|
+
const saved = useRef(points);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (points === saved.current)
|
|
42
|
+
return;
|
|
43
|
+
saved.current = points;
|
|
44
|
+
setDraft(draftOf(points));
|
|
45
|
+
}, [points]);
|
|
46
|
+
const result = pointsOf(draft);
|
|
47
|
+
const blank = draft.every(row => row.every(field => field.trim() === ''));
|
|
48
|
+
const set = (i, j, text) => {
|
|
49
|
+
const next = draft.map((row, r) => (r === i ? row.map((field, f) => (f === j ? text : field)) : row));
|
|
50
|
+
setDraft(next);
|
|
51
|
+
const changed = pointsOf(next);
|
|
52
|
+
if (typeof changed !== 'string') {
|
|
53
|
+
saved.current = changed;
|
|
54
|
+
onChange(changed);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const clear = () => {
|
|
58
|
+
saved.current = undefined;
|
|
59
|
+
setDraft(draftOf(undefined));
|
|
60
|
+
onChange(undefined);
|
|
61
|
+
};
|
|
62
|
+
const names = [labels.imageX, labels.imageY, labels.coordinateX, labels.coordinateY];
|
|
63
|
+
return (_jsxs("fieldset", { className: "ub-panel__value-map", style: { minWidth: 0 }, children: [_jsx("legend", { children: labels.referencePoints }), _jsx("p", { className: "ub-panel__hint", children: labels.referencePointsHint }), draft.map((row, i) => (_jsx("div", { className: "ub-panel__map-row", style: ROW_STYLE, role: "group", "aria-label": labels.referencePoint.replace('{n}', String(i + 1)), children: row.map((field, j) => (_jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [names[j], _jsx("input", { type: "number", step: "any", style: NUMBER_STYLE, value: field, onChange: e => set(i, j, e.target.value) })] }, j))) }, i))), result === 'degenerate' && (_jsx("p", { role: "alert", className: "ub-panel__error", style: ERROR_STYLE, children: labels.referencePointsApart })), !blank && result === 'incomplete' && _jsx("p", { className: "ub-panel__hint", children: labels.referencePointsIncomplete }), points && (_jsx("button", { type: "button", className: "ub-action", onClick: clear, children: labels.clearReferencePoints }))] }));
|
|
64
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type DesignerLabels } from '../labels.js';
|
|
2
|
+
import type { CanvasLayout } from '@iyulab/u-board/toolkit';
|
|
3
|
+
export interface BackgroundFieldProps {
|
|
4
|
+
layout: CanvasLayout;
|
|
5
|
+
/** The canvas with its background as chosen — or taken away. */
|
|
6
|
+
onChange: (layout: CanvasLayout) => void;
|
|
7
|
+
labels?: DesignerLabels;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The background of a canvas board: an image file chosen, which the canvas takes the
|
|
11
|
+
* size of — its parts keep their places in the canvas's units — or taken away, the canvas keeping its size.
|
|
12
|
+
*/
|
|
13
|
+
export declare function BackgroundField({ layout, onChange, labels }: BackgroundFieldProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { DEFAULT_DESIGNER_LABELS } from '../labels.js';
|
|
3
|
+
import { useRef, useState } from 'react';
|
|
4
|
+
import { GROUP_STYLE } from '@iyulab/u-board/toolkit';
|
|
5
|
+
import { readBackgroundImage, BackgroundImageError, BACKGROUND_IMAGE_TYPES, MAX_BACKGROUND_BYTES } from './background-image.js';
|
|
6
|
+
/**
|
|
7
|
+
* The background of a canvas board: an image file chosen, which the canvas takes the
|
|
8
|
+
* size of — its parts keep their places in the canvas's units — or taken away, the canvas keeping its size.
|
|
9
|
+
*/
|
|
10
|
+
export function BackgroundField({ layout, onChange, labels = DEFAULT_DESIGNER_LABELS }) {
|
|
11
|
+
const input = useRef(null);
|
|
12
|
+
const [error, setError] = useState(null);
|
|
13
|
+
const choose = async (e) => {
|
|
14
|
+
const file = e.target.files?.[0];
|
|
15
|
+
e.target.value = '';
|
|
16
|
+
if (!file)
|
|
17
|
+
return;
|
|
18
|
+
try {
|
|
19
|
+
const image = await readBackgroundImage(file);
|
|
20
|
+
const size = image.width > 0 && image.height > 0 ? [image.width, image.height] : layout.size;
|
|
21
|
+
onChange({ ...layout, background: image.src, size });
|
|
22
|
+
setError(null);
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
const problem = err instanceof BackgroundImageError ? err.problem : 'unreadable';
|
|
26
|
+
setError(problem === 'type'
|
|
27
|
+
? labels.backgroundType
|
|
28
|
+
: problem === 'size'
|
|
29
|
+
? labels.backgroundTooLarge.replace('{max}', `${MAX_BACKGROUND_BYTES / (1024 * 1024)} MB`)
|
|
30
|
+
: labels.backgroundUnreadable);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const remove = () => {
|
|
34
|
+
const { background: _gone, ...rest } = layout;
|
|
35
|
+
onChange(rest);
|
|
36
|
+
};
|
|
37
|
+
return (_jsxs("div", { className: "ub-panel__actions", style: GROUP_STYLE, children: [_jsx("button", { type: "button", className: "ub-action", onClick: () => input.current?.click(), children: labels.setBackground }), layout.background !== undefined && (_jsx("button", { type: "button", className: "ub-action", onClick: remove, children: labels.removeBackground })), _jsx("input", { ref: input, type: "file", accept: BACKGROUND_IMAGE_TYPES.join(','), onChange: choose, style: { display: 'none' }, "data-testid": "background-file-input" }), error && (_jsx("p", { className: "ub-panel__error", role: "alert", children: error }))] }));
|
|
38
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type DesignerLabels } from '../labels.js';
|
|
2
|
+
import type { Adapter } from '@iyulab/u-board/toolkit';
|
|
3
|
+
import { type ViewerPageProps } from '@iyulab/u-board/toolkit';
|
|
4
|
+
import type { Board } from '@iyulab/u-board/toolkit';
|
|
5
|
+
import type { AuthoringMode } from './editor-parts.js';
|
|
6
|
+
export interface BoardAuthoringProps {
|
|
7
|
+
/** The board to edit. */
|
|
8
|
+
initialBoard: Board;
|
|
9
|
+
/** Where the board's values are read — also which items a grid laying out a part per item of a list lays out, read
|
|
10
|
+
* when the board opens and whenever what the board reads them by changes. */
|
|
11
|
+
adapters: readonly Adapter[];
|
|
12
|
+
/** What each adapter's source is called, by adapter id — the names the data panel offers sources by. Omitted, an id. */
|
|
13
|
+
sourceLabels?: Readonly<Record<string, string>>;
|
|
14
|
+
width?: number;
|
|
15
|
+
height?: number;
|
|
16
|
+
/** What Save does. Omitted, there is no Save — Export downloads the board. */
|
|
17
|
+
onSave?: (board: Board) => void | Promise<void>;
|
|
18
|
+
onDirtyChange?: (isDirty: boolean) => void;
|
|
19
|
+
labels?: Partial<DesignerLabels>;
|
|
20
|
+
clock?: () => number;
|
|
21
|
+
elementOrigins?: readonly string[];
|
|
22
|
+
elementFrames?: string;
|
|
23
|
+
boardId?: string;
|
|
24
|
+
onTrigger?: ViewerPageProps['onTrigger'];
|
|
25
|
+
onNavigate?: ViewerPageProps['onNavigate'];
|
|
26
|
+
linkOrigins?: readonly string[];
|
|
27
|
+
initialMode?: AuthoringMode;
|
|
28
|
+
onModeChange?: (mode: AuthoringMode) => void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The authoring surface of a board: the board drawn from its picture, where its parts
|
|
32
|
+
* are moved, sized, ordered and deleted — each edit said again in the board's layout (`withPictureLayout`) — and a
|
|
33
|
+
* selected part's title, kind, data and fields set in its panel, with what the board's check finds there. A setting the
|
|
34
|
+
* board cannot be drawn with keeps the last picture it could, and says why. Undo, Redo, Save and Export are the
|
|
35
|
+
* board's. A grid's cell — a canvas, or a grid of its own — opens in its own editor, what changes there written back.
|
|
36
|
+
*/
|
|
37
|
+
export declare function BoardAuthoring({ initialBoard, adapters, sourceLabels, width, height, onSave, onDirtyChange, labels: labelsProp, clock, elementOrigins, elementFrames, boardId, onTrigger, onNavigate, linkOrigins, initialMode, onModeChange }: BoardAuthoringProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useDesignerLabels } from '../use-labels.js';
|
|
3
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { TONES } from '@iyulab/u-board/toolkit';
|
|
5
|
+
import { GROUP_STYLE } from '@iyulab/u-board/toolkit';
|
|
6
|
+
import { placedIn } from '@iyulab/u-board/picture';
|
|
7
|
+
import { ElementHost } from '@iyulab/u-board/toolkit';
|
|
8
|
+
import { ViewerPage } from '@iyulab/u-board/toolkit';
|
|
9
|
+
import { PictureError, dataReadOf, keyReadsOf, keysIn, toViewDocument } from '@iyulab/u-board/toolkit';
|
|
10
|
+
import { readLists, readerOf } from '@iyulab/u-board/toolkit';
|
|
11
|
+
import { checkBoard } from '../board/check.js';
|
|
12
|
+
import { CanvasAuthoring } from './CanvasAuthoring.js';
|
|
13
|
+
import { PartPanel } from './PartPanel.js';
|
|
14
|
+
import { StatusesPanel } from './StatusesPanel.js';
|
|
15
|
+
import { DataPanel } from './DataPanel.js';
|
|
16
|
+
import { LinkPanel } from './LinkPanel.js';
|
|
17
|
+
import { ShapePanel } from './ShapePanel.js';
|
|
18
|
+
import { BackgroundField } from './BackgroundField.js';
|
|
19
|
+
import { BoardElementsPanel } from './BoardElementsPanel.js';
|
|
20
|
+
import { BoardPanel } from './BoardPanel.js';
|
|
21
|
+
import { TriggersEditor } from './TriggersEditor.js';
|
|
22
|
+
import { boardTriggerEditing } from './board-triggers.js';
|
|
23
|
+
import { GridAuthoring } from './GridAuthoring.js';
|
|
24
|
+
import { LINK, cellBoard, cellIndex, gridCellOf, layoutEditable, linkOf, placesItself, shapeOf, withCell, withNewLink, withNewShape, withPictureLayout } from './board-layout.js';
|
|
25
|
+
import { recordEdit, redo, startHistory, undo } from './document-history.js';
|
|
26
|
+
import { useEditKeys } from './use-edit-keys.js';
|
|
27
|
+
/**
|
|
28
|
+
* The authoring surface of a board: the board drawn from its picture, where its parts
|
|
29
|
+
* are moved, sized, ordered and deleted — each edit said again in the board's layout (`withPictureLayout`) — and a
|
|
30
|
+
* selected part's title, kind, data and fields set in its panel, with what the board's check finds there. A setting the
|
|
31
|
+
* board cannot be drawn with keeps the last picture it could, and says why. Undo, Redo, Save and Export are the
|
|
32
|
+
* board's. A grid's cell — a canvas, or a grid of its own — opens in its own editor, what changes there written back.
|
|
33
|
+
*/
|
|
34
|
+
export function BoardAuthoring({ initialBoard, adapters, sourceLabels, width, height, onSave, onDirtyChange, labels: labelsProp, clock = Date.now, elementOrigins, elementFrames, boardId, onTrigger, onNavigate, linkOrigins, initialMode = 'edit', onModeChange }) {
|
|
35
|
+
const labels = useDesignerLabels(labelsProp);
|
|
36
|
+
const [history, setHistory] = useState(() => startHistory(initialBoard));
|
|
37
|
+
const board = history.present;
|
|
38
|
+
// A grid's cell open in its own editor — its entry — and the board as that editor sees it: the cell (a canvas, or a
|
|
39
|
+
// grid) as the layout, what it changes written back into the cell.
|
|
40
|
+
const [cell, setCell] = useState(undefined);
|
|
41
|
+
const inCell = cell !== undefined && cellBoard(board, cell) !== undefined ? cell : undefined;
|
|
42
|
+
const scope = (b) => (inCell !== undefined ? (cellBoard(b, inCell) ?? b) : b);
|
|
43
|
+
const unscope = (b, scoped) => (inCell !== undefined ? withCell(b, inCell, scoped) : scoped);
|
|
44
|
+
const scoped = scope(board);
|
|
45
|
+
/** Where the layout the canvas editor edits sits in the board — for the findings about it. */
|
|
46
|
+
const layoutAt = inCell !== undefined ? `/layout/items/${inCell}/${scoped.layout.kind}` : '/layout';
|
|
47
|
+
const [lastSaved, setLastSaved] = useState(initialBoard);
|
|
48
|
+
const isDirty = board !== lastSaved;
|
|
49
|
+
const [mode, setModeState] = useState(initialMode);
|
|
50
|
+
const setMode = (next) => {
|
|
51
|
+
setModeState(next);
|
|
52
|
+
onModeChange?.(next);
|
|
53
|
+
};
|
|
54
|
+
// A view shows the board as it is: the edits are walked back and forth only while editing — by the buttons, or the
|
|
55
|
+
// keys heard anywhere in the editor.
|
|
56
|
+
const canUndo = mode === 'edit' && history.past.length > 0;
|
|
57
|
+
const canRedo = mode === 'edit' && history.future.length > 0;
|
|
58
|
+
const { rootRef, typing } = useEditKeys(setHistory, canUndo, canRedo);
|
|
59
|
+
/** An edit — one with what was typed in the same field before it while it answers typing there (document-history.ts),
|
|
60
|
+
* until focus moves on. Which field is read when the edit is made, before its update runs. */
|
|
61
|
+
const setEdit = (update) => {
|
|
62
|
+
const typingIn = typing.current;
|
|
63
|
+
setHistory(h => update(h, typingIn));
|
|
64
|
+
};
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (!isDirty)
|
|
67
|
+
return;
|
|
68
|
+
const handler = (e) => {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
e.returnValue = '';
|
|
71
|
+
};
|
|
72
|
+
window.addEventListener('beforeunload', handler);
|
|
73
|
+
return () => window.removeEventListener('beforeunload', handler);
|
|
74
|
+
}, [isDirty]);
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
onDirtyChange?.(isDirty);
|
|
77
|
+
}, [isDirty, onDirtyChange]);
|
|
78
|
+
const items = useItemLists(board, adapters);
|
|
79
|
+
const drawOptions = useMemo(() => ({ keysOf: data => keysIn(items.board, data, items.lists[data] ?? []), rowsOf: data => items.lists[data] ?? [] }), [items]);
|
|
80
|
+
// What is drawn: the board, or — while a setting leaves it one that cannot be drawn — the newest board before it that
|
|
81
|
+
// could be, with why the board is not.
|
|
82
|
+
const pictureOf = (h) => drawnOf(h, drawOptions);
|
|
83
|
+
const drawing = useMemo(() => drawnOf(history, drawOptions), [history, drawOptions]);
|
|
84
|
+
const picture = drawing.picture;
|
|
85
|
+
/** What the editor shows: the board, or the cell open — drawn as a board of its own. */
|
|
86
|
+
const shown = inCell !== undefined ? drawCell(scoped, drawOptions) : picture;
|
|
87
|
+
/** What was drawn before an edit, as the editor shows it. */
|
|
88
|
+
const shownOf = (h) => (inCell !== undefined ? drawCell(scope(h.present), drawOptions) : pictureOf(h).picture);
|
|
89
|
+
const movable = (id) => placesItself(scoped, id);
|
|
90
|
+
const ownPiece = (id) => linkOf(scoped, id) !== undefined || shapeOf(scoped, id) !== undefined;
|
|
91
|
+
const findings = useMemo(() => {
|
|
92
|
+
const check = checkBoard(board);
|
|
93
|
+
return [...check.issues, ...check.advice];
|
|
94
|
+
}, [board]);
|
|
95
|
+
// What a trigger may name: a part — one made per item, by item — as the board is drawn now.
|
|
96
|
+
const targets = useMemo(() => placedIn(picture).map(placed => placed.id.slice(placed.id.lastIndexOf(':') + 1)).filter(id => {
|
|
97
|
+
const [part, key, ...rest] = id.split('/');
|
|
98
|
+
const each = board.parts[part]?.each !== undefined;
|
|
99
|
+
return rest.length === 0 && (key === undefined ? !each && part in board.parts : each);
|
|
100
|
+
}), [picture, board]);
|
|
101
|
+
const sources = useMemo(() => adapters.map(a => ({ id: a.id, ...(sourceLabels?.[a.id] !== undefined ? { label: sourceLabels[a.id] } : {}) })), [adapters, sourceLabels]);
|
|
102
|
+
const read = useMemo(() => readerOf(adapters), [adapters]);
|
|
103
|
+
/** The board's own settings (`board` — its title, theme, accent and look) changed — one left empty goes, and so does
|
|
104
|
+
* `board` when nothing is left in it. */
|
|
105
|
+
const setBoardHead = (change) => setEdit((h, typingIn) => {
|
|
106
|
+
const { board: head, ...rest } = h.present;
|
|
107
|
+
const next = { ...head, ...change };
|
|
108
|
+
for (const [key, value] of Object.entries(change))
|
|
109
|
+
if (value === undefined)
|
|
110
|
+
delete next[key];
|
|
111
|
+
return recordEdit(h, Object.keys(next).length > 0 ? { ...rest, board: next } : rest, typingIn);
|
|
112
|
+
});
|
|
113
|
+
/** One of the board's data read now, whole, as the board reads it. */
|
|
114
|
+
const look = async (name) => {
|
|
115
|
+
const { adapter, ref } = dataReadOf(board, name);
|
|
116
|
+
return (await read(adapter, ref)).value;
|
|
117
|
+
};
|
|
118
|
+
const triggerEditing = boardTriggerEditing(board, targets, update => setEdit((h, typingIn) => recordEdit(h, update(h.present), typingIn)));
|
|
119
|
+
/** The panel of the part a selected node or item draws — a part made per item, or one inside a group, is its part;
|
|
120
|
+
* with nothing selected, the board's own triggers (those that run when it opens). */
|
|
121
|
+
const panel = (selected) => {
|
|
122
|
+
if (selected === null) {
|
|
123
|
+
return (_jsxs(_Fragment, { children: [_jsx(BoardPanel, { appearance: board.board?.theme ?? 'light', onAppearanceChange: theme => setBoardHead({ theme }), accent: board.board?.accent, onAccentChange: accent => setBoardHead({ accent }), hint: labels.layoutOnlyHint, labels: labels }), scoped.layout.kind === 'canvas' && (_jsx(BackgroundField, { layout: scoped.layout, onChange: layout => setEdit((h, typingIn) => recordEdit(h, unscope(h.present, { ...scope(h.present), layout }), typingIn)), labels: labels })), _jsx(DataPanel, { data: board.data, sources: sources, look: look, statuses: [...new Set([...Object.keys(board.statuses ?? {}), ...TONES])], onChange: data => setEdit((h, typingIn) => {
|
|
124
|
+
const { data: _gone, ...rest } = h.present;
|
|
125
|
+
return recordEdit(h, data ? { ...rest, data } : rest, typingIn);
|
|
126
|
+
}), labels: labels }), _jsx(StatusesPanel, { statuses: board.statuses, onChange: statuses => setEdit((h, typingIn) => {
|
|
127
|
+
const { statuses: _gone, ...rest } = h.present;
|
|
128
|
+
return recordEdit(h, statuses ? { ...rest, statuses } : rest, typingIn);
|
|
129
|
+
}), labels: labels }), _jsx(TriggersEditor, { ...triggerEditing, labels: labels }), _jsx(BoardElementsPanel, { board: board, onChange: elements => setEdit((h, typingIn) => {
|
|
130
|
+
const { elements: _gone, ...rest } = h.present;
|
|
131
|
+
return recordEdit(h, elements ? { ...rest, elements } : rest, typingIn);
|
|
132
|
+
}), labels: labels })] }));
|
|
133
|
+
}
|
|
134
|
+
const shape = shapeOf(scoped, selected);
|
|
135
|
+
if (shape !== undefined) {
|
|
136
|
+
return (_jsx(ShapePanel, { id: shape, board: scoped, onChange: next => setEdit((h, typingIn) => {
|
|
137
|
+
const own = scope(h.present);
|
|
138
|
+
if (own.layout.kind !== 'canvas')
|
|
139
|
+
return h;
|
|
140
|
+
return recordEdit(h, unscope(h.present, { ...own, layout: { ...own.layout, shapes: { ...own.layout.shapes, [shape]: next } } }), typingIn);
|
|
141
|
+
}), findings: findings.filter(f => f.path === `${layoutAt}/shapes/${shape}` || f.path.startsWith(`${layoutAt}/shapes/${shape}/`)), labels: labels }, shape));
|
|
142
|
+
}
|
|
143
|
+
const link = linkOf(scoped, selected);
|
|
144
|
+
if (link !== undefined) {
|
|
145
|
+
return (_jsx(LinkPanel, { id: link, board: scoped, onChange: next => setEdit((h, typingIn) => {
|
|
146
|
+
const own = scope(h.present);
|
|
147
|
+
if (own.layout.kind !== 'canvas')
|
|
148
|
+
return h;
|
|
149
|
+
return recordEdit(h, unscope(h.present, { ...own, layout: { ...own.layout, links: { ...own.layout.links, [link]: next } } }), typingIn);
|
|
150
|
+
}), findings: findings.filter(f => f.path === `${layoutAt}/links/${link}` || f.path.startsWith(`${layoutAt}/links/${link}/`)), labels: labels }, link));
|
|
151
|
+
}
|
|
152
|
+
// An item drawn from a grid in a cell is laid out in that grid: it opens from here.
|
|
153
|
+
const gridCell = inCell === undefined ? gridCellOf(board, selected) : undefined;
|
|
154
|
+
const openGrid = gridCell === undefined ? null : (_jsx("button", { type: "button", className: "ub-action ub-action--primary", onClick: () => setCell(gridCell), children: labels.editGridCell }));
|
|
155
|
+
const id = selected.split('/')[0];
|
|
156
|
+
if (id === undefined || !(id in board.parts))
|
|
157
|
+
return openGrid;
|
|
158
|
+
return (_jsxs(_Fragment, { children: [openGrid, _jsx(PartPanel, { id: id, board: board, onChange: part => setEdit((h, typingIn) => recordEdit(h, { ...h.present, parts: { ...h.present.parts, [id]: part } }, typingIn)), findings: findings.filter(f => f.path === `/parts/${id}` || f.path.startsWith(`/parts/${id}/`)), triggers: targets.includes(selected) ? _jsx(TriggersEditor, { node: selected, ...triggerEditing, labels: labels }, `triggers:${selected}`) : undefined, labels: labels }, id)] }));
|
|
159
|
+
};
|
|
160
|
+
const handleSave = async () => {
|
|
161
|
+
if (!onSave)
|
|
162
|
+
return handleExport();
|
|
163
|
+
try {
|
|
164
|
+
await onSave(board);
|
|
165
|
+
setLastSaved(board);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// The host says why it failed; the board stays unsaved.
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const handleExport = () => {
|
|
172
|
+
const blob = new Blob([JSON.stringify(board, null, 2)], { type: 'application/json' });
|
|
173
|
+
const url = URL.createObjectURL(blob);
|
|
174
|
+
const link = document.createElement('a');
|
|
175
|
+
link.href = url;
|
|
176
|
+
link.download = 'board.json';
|
|
177
|
+
link.click();
|
|
178
|
+
URL.revokeObjectURL(url);
|
|
179
|
+
if (!onSave)
|
|
180
|
+
setLastSaved(board);
|
|
181
|
+
};
|
|
182
|
+
/** A new part, titled and of a kind that reads no data until its panel sets one — placed where a canvas (or the canvas
|
|
183
|
+
* cell open) has room at its top left, or last in a grid. */
|
|
184
|
+
const addPart = () => setEdit((h, typingIn) => {
|
|
185
|
+
const present = h.present;
|
|
186
|
+
let n = Object.keys(present.parts).length + 1;
|
|
187
|
+
while (`part-${n}` in present.parts)
|
|
188
|
+
n++;
|
|
189
|
+
const id = `part-${n}`;
|
|
190
|
+
const own = scope(present);
|
|
191
|
+
if (own.layout.kind === 'canvas') {
|
|
192
|
+
const layout = own.layout;
|
|
193
|
+
const step = 24 * (n - 1);
|
|
194
|
+
const x = Math.min(40 + step, Math.max(0, layout.size[0] - 160));
|
|
195
|
+
const y = Math.min(40 + step, Math.max(0, layout.size[1] - 60));
|
|
196
|
+
return recordEdit(h, unscope(present, { ...own, parts: { ...own.parts, [id]: { as: 'label', title: labels.newPartTitle } }, layout: { ...layout, place: { ...layout.place, [id]: [x, y, 160, 60] } } }), typingIn);
|
|
197
|
+
}
|
|
198
|
+
const layout = present.layout;
|
|
199
|
+
if (layout.kind !== 'grid')
|
|
200
|
+
return h;
|
|
201
|
+
return recordEdit(h, { ...present, parts: { ...present.parts, [id]: { as: 'header', title: labels.newPartTitle } }, layout: { ...layout, items: [...layout.items, id] } }, typingIn);
|
|
202
|
+
});
|
|
203
|
+
/** A frame or a text on a canvas board, at its top left. */
|
|
204
|
+
const addShape = (kind) => setEdit((h, typingIn) => recordEdit(h, unscope(h.present, withNewShape(scope(h.present), kind, labels.shapeNewText).board), typingIn));
|
|
205
|
+
const documentActions = (_jsxs(_Fragment, { children: [inCell !== undefined && (_jsx("div", { className: "ub-authoring__group", style: GROUP_STYLE, children: _jsx("button", { type: "button", className: "ub-action", onClick: () => setCell(undefined), children: labels.backToGrid }) })), mode === 'edit' && layoutEditable(board) && (_jsxs("div", { className: "ub-authoring__group", style: GROUP_STYLE, children: [_jsx("button", { type: "button", className: "ub-action", onClick: addPart, children: labels.partAdd }), scoped.layout.kind === 'canvas' && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "ub-action", onClick: () => addShape('box'), children: labels.shapeAddBox }), _jsx("button", { type: "button", className: "ub-action", onClick: () => addShape('text'), children: labels.shapeAddText }), _jsx("button", { type: "button", className: "ub-action", onClick: () => addShape('line'), children: labels.shapeAddLine })] }))] })), _jsxs("div", { className: "ub-authoring__group", style: GROUP_STYLE, children: [_jsx("button", { type: "button", className: "ub-action", onClick: () => setHistory(undo), disabled: !canUndo, "aria-keyshortcuts": "Control+Z Meta+Z", children: labels.undo }), _jsx("button", { type: "button", className: "ub-action", onClick: () => setHistory(redo), disabled: !canRedo, "aria-keyshortcuts": "Control+Shift+Z Meta+Shift+Z Control+Y", children: labels.redo })] }), _jsxs("div", { className: "ub-authoring__group", style: GROUP_STYLE, children: [onSave && (_jsx("button", { type: "button", className: "ub-action ub-action--primary", onClick: handleSave, children: labels.save })), _jsx("button", { type: "button", className: "ub-action", onClick: handleExport, children: labels.export })] })] }));
|
|
206
|
+
return (_jsx(ElementHost, { origins: elementOrigins, frames: elementFrames, children: _jsxs("div", { className: "ub-authoring", style: { display: 'flex', flexDirection: 'column', height: '100%' }, ref: rootRef, children: [drawing.error && (_jsx("p", { role: "alert", className: "ub-authoring__error", children: labels.boardNotDrawn.replace('{reason}', drawing.error) })), shown?.kind === 'canvas' && layoutEditable(board) ? (_jsx(CanvasAuthoring, { doc: shown, updateDoc: update => setEdit((h, typingIn) => {
|
|
207
|
+
const before = shownOf(h);
|
|
208
|
+
if (before?.kind !== 'canvas')
|
|
209
|
+
return h;
|
|
210
|
+
return recordEdit(h, unscope(h.present, withPictureLayout(scope(h.present), before, update(before))), typingIn);
|
|
211
|
+
}), mode: mode, onModeChange: setMode, documentActions: documentActions, adapters: adapters, width: width, height: height, labels: labels, clock: clock, triggerHosting: { boardId, onTrigger, onNavigate, linkOrigins }, parts: {
|
|
212
|
+
movable,
|
|
213
|
+
panel,
|
|
214
|
+
connect: (from, to) => {
|
|
215
|
+
const made = withNewLink(scoped, from, to);
|
|
216
|
+
setEdit((h, typingIn) => recordEdit(h, unscope(h.present, withNewLink(scope(h.present), from, to).board), typingIn));
|
|
217
|
+
return `${LINK}${made.id}`;
|
|
218
|
+
},
|
|
219
|
+
deletable: ownPiece,
|
|
220
|
+
} }, inCell === undefined ? 'board' : `cell-${inCell}`)) : shown?.kind === 'grid' && layoutEditable(board) ? (_jsx(GridAuthoring, { doc: shown, updateDoc: update => setEdit((h, typingIn) => {
|
|
221
|
+
const before = shownOf(h);
|
|
222
|
+
if (before?.kind !== 'grid')
|
|
223
|
+
return h;
|
|
224
|
+
return recordEdit(h, unscope(h.present, withPictureLayout(scope(h.present), before, update(before))), typingIn);
|
|
225
|
+
}), mode: mode, onModeChange: setMode, documentActions: documentActions, adapters: adapters, width: width, height: height, labels: labels, clock: clock, triggerHosting: { boardId, onTrigger, onNavigate, linkOrigins }, parts: { movable, panel }, onEditCanvas: inCell === undefined ? id => setCell(cellIndex(board, id)) : undefined }, inCell === undefined ? 'board' : `cell-${inCell}`)) : (_jsx(ViewerPage, { document: picture, adapters: adapters, width: width, height: height, labels: labels, clock: clock, boardId: boardId, onTrigger: onTrigger, onNavigate: onNavigate, linkOrigins: linkOrigins, elementOrigins: elementOrigins, elementFrames: elementFrames }))] }) }));
|
|
226
|
+
}
|
|
227
|
+
/** A board drawn — when its language takes it and it draws — or why not. */
|
|
228
|
+
function draw(board, options) {
|
|
229
|
+
const issue = checkBoard(board).issues[0];
|
|
230
|
+
if (issue)
|
|
231
|
+
return { picture: null, error: `${issue.path}: ${issue.message}` };
|
|
232
|
+
try {
|
|
233
|
+
return { picture: toViewDocument(board, options), error: null };
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
if (error instanceof PictureError)
|
|
237
|
+
return { picture: null, error: error.message };
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/** A grid's cell as `cellBoard` gives it, drawn for its editor: without the board's triggers — they name parts all over
|
|
242
|
+
* the board, most of them not in the cell — and without the board's check, which would find those parts placed nowhere
|
|
243
|
+
* in it. Null when it cannot be drawn. */
|
|
244
|
+
function drawCell(cell, options) {
|
|
245
|
+
const { triggers: _triggers, ...drawn } = cell;
|
|
246
|
+
try {
|
|
247
|
+
return toViewDocument(drawn, options);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
if (error instanceof PictureError)
|
|
251
|
+
return null;
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/** The keys of the items each data list a part is made per item of has now, read through `adapters` — again only when
|
|
256
|
+
* what they are read by changes (an edit elsewhere in the board reads nothing). While a setting leaves the board one
|
|
257
|
+
* that cannot be drawn, the keys read last stay. */
|
|
258
|
+
function useItemLists(board, adapters) {
|
|
259
|
+
const [items, setItems] = useState({ board, lists: {} });
|
|
260
|
+
const read = useMemo(() => readerOf(adapters), [adapters]);
|
|
261
|
+
const by = useMemo(() => (draw(board, { keysOf: () => [], rowsOf: () => [] }).picture === null ? null : JSON.stringify(keyReadsOf(board).map(r => [r, board.data?.[r.data]?.key]))), [board]);
|
|
262
|
+
// The board the items are read from: the newest one that draws whose reads differ from the last.
|
|
263
|
+
const [source, setSource] = useState({ by: '[]', board });
|
|
264
|
+
if (by !== null && by !== source.by)
|
|
265
|
+
setSource({ by, board });
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
if (source.by === '[]')
|
|
268
|
+
return;
|
|
269
|
+
let current = true;
|
|
270
|
+
void readLists(source.board, read).then(lists => current && setItems({ board: source.board, lists }));
|
|
271
|
+
return () => {
|
|
272
|
+
current = false;
|
|
273
|
+
};
|
|
274
|
+
}, [source, read]);
|
|
275
|
+
return items;
|
|
276
|
+
}
|
|
277
|
+
/** What the editor draws for a history: its board, or the newest one before it that draws, and why the board does not. */
|
|
278
|
+
function drawnOf(history, options) {
|
|
279
|
+
const now = draw(history.present, options);
|
|
280
|
+
if (now.picture)
|
|
281
|
+
return now;
|
|
282
|
+
for (const before of [...history.past].reverse()) {
|
|
283
|
+
const drawn = draw(before, options).picture;
|
|
284
|
+
if (drawn)
|
|
285
|
+
return { picture: drawn, error: now.error };
|
|
286
|
+
}
|
|
287
|
+
return { picture: toViewDocument(history.present, options), error: now.error };
|
|
288
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type DesignerLabels } from '../labels.js';
|
|
2
|
+
import type { Board, ElementDefinition } from '@iyulab/u-board/toolkit';
|
|
3
|
+
export interface BoardElementsPanelProps {
|
|
4
|
+
board: Board;
|
|
5
|
+
/** The board's elements as the panel made them — none left: `undefined`. */
|
|
6
|
+
onChange: (elements: Record<string, ElementDefinition> | undefined) => void;
|
|
7
|
+
labels?: DesignerLabels;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The board's elements — drawings no built-in makes, each a template of markup with `{{prop}}` holes (or a script run in a
|
|
11
|
+
* sandboxed frame), its CSS, and the props it takes by name and type. An element is added by name — a template — and
|
|
12
|
+
* removed when no part draws it.
|
|
13
|
+
*/
|
|
14
|
+
export declare function BoardElementsPanel({ board, onChange, labels }: BoardElementsPanelProps): import("react").JSX.Element;
|