@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
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { DEFAULT_DESIGNER_LABELS } from '../labels.js';
|
|
3
|
+
import { GRID_COLUMNS, GRID_COLUMNS_MEDIUM, GRID_DEFAULT_SPAN, GRID_MAX_ROW_SPAN, isCanvasItem } from '@iyulab/u-board/picture';
|
|
4
|
+
import { GROUP_STYLE } from '@iyulab/u-board/toolkit';
|
|
5
|
+
const ACTIONS_STYLE = { ...GROUP_STYLE, marginBottom: 'var(--ub-space-3, 12px)' };
|
|
6
|
+
const columns = (count) => Array.from({ length: count }, (_, i) => i + 1);
|
|
7
|
+
/**
|
|
8
|
+
* A grid item's room and place: how many columns it spans at full and at medium width, its height, and where it
|
|
9
|
+
* is in the order. Moving and removing are buttons, so they are a key press as well as a click. A choice left at
|
|
10
|
+
* its default leaves its field out of the document.
|
|
11
|
+
*/
|
|
12
|
+
export function GridItemPanel({ item, isFirst, isLast, onChange, onMove, onDuplicate, onRemove, spanOnly = false, labels = DEFAULT_DESIGNER_LABELS }) {
|
|
13
|
+
const span = item.span ?? GRID_DEFAULT_SPAN;
|
|
14
|
+
return (_jsxs("div", { className: "ub-panel ub-panel--item", children: [_jsx("h2", { className: "ub-panel__heading", children: labels.itemLayout }), _jsxs("div", { className: "ub-panel__actions", style: ACTIONS_STYLE, children: [_jsx("button", { type: "button", className: "ub-action", disabled: isFirst, onClick: () => onMove(-1), children: labels.moveEarlier }), _jsx("button", { type: "button", className: "ub-action", disabled: isLast, onClick: () => onMove(1), children: labels.moveLater }), onDuplicate && (_jsx("button", { type: "button", className: "ub-action", onClick: onDuplicate, children: labels.duplicateItem })), _jsx("button", { type: "button", className: "ub-action", onClick: onRemove, children: labels.removeItem })] }), _jsxs("label", { className: "ub-panel__field", children: [labels.itemSpan.replace('{columns}', String(GRID_COLUMNS)), _jsx("select", { value: span, onChange: e => {
|
|
15
|
+
const value = Number(e.target.value);
|
|
16
|
+
onChange({ span: value === GRID_DEFAULT_SPAN ? undefined : value });
|
|
17
|
+
}, children: columns(GRID_COLUMNS).map(n => (_jsx("option", { value: n, children: n }, n))) })] }), !spanOnly && (_jsxs(_Fragment, { children: [_jsxs("label", { className: "ub-panel__field", children: [labels.itemSpanMedium.replace('{columns}', String(GRID_COLUMNS_MEDIUM)), _jsxs("select", { value: item.spanMedium ?? '', onChange: e => onChange({ spanMedium: e.target.value === '' ? undefined : Number(e.target.value) }), children: [_jsx("option", { value: "", children: labels.itemSpanMediumAuto }), columns(GRID_COLUMNS_MEDIUM).map(n => (_jsx("option", { value: n, children: n }, n)))] })] }), _jsxs("label", { className: "ub-panel__field", children: [labels.itemRowSpan, _jsx("select", { value: item.rowSpan ?? 1, onChange: e => onChange({ rowSpan: Number(e.target.value) === 1 ? undefined : Number(e.target.value) }), children: columns(GRID_MAX_ROW_SPAN).map(n => (_jsx("option", { value: n, children: n }, n))) })] }), _jsxs("label", { className: "ub-panel__field", children: [labels.itemHeight, _jsx("input", { type: "number", min: 1, step: 1, inputMode: "numeric", placeholder: isCanvasItem(item) ? labels.canvasHeightAuto : labels.itemHeightAuto, value: item.height ?? '', onChange: e => {
|
|
18
|
+
const value = e.target.valueAsNumber;
|
|
19
|
+
onChange({ height: Number.isFinite(value) && value > 0 ? value : undefined });
|
|
20
|
+
} })] })] }))] }));
|
|
21
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type DesignerLabels } from '../labels.js';
|
|
2
|
+
import { type Board, type Link } from '@iyulab/u-board/toolkit';
|
|
3
|
+
import type { BoardFinding } from '../board/check.js';
|
|
4
|
+
export interface LinkPanelProps {
|
|
5
|
+
id: string;
|
|
6
|
+
board: Board;
|
|
7
|
+
/** The link as the panel made it. */
|
|
8
|
+
onChange: (link: Link) => void;
|
|
9
|
+
findings?: readonly BoardFinding[];
|
|
10
|
+
labels?: DesignerLabels;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The panel of a link of a canvas board: the parts it joins (set by joining them on
|
|
14
|
+
* the canvas), its label, the item it reads and the field whose status it takes — its colour the status's tone, its line
|
|
15
|
+
* the status's emphasis — and, fixed, its tone without a status, its route, arrowheads and dash. A setting left empty
|
|
16
|
+
* goes from the link. Arrowheads read from a field (a direction that reverses) are written in the board's source; the
|
|
17
|
+
* panel names the field and replaces them only when a fixed choice is made.
|
|
18
|
+
*/
|
|
19
|
+
export declare function LinkPanel({ id, board, onChange, findings, labels }: LinkPanelProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { FIELD_STYLE } from './field-style.js';
|
|
3
|
+
import { DEFAULT_DESIGNER_LABELS } from '../labels.js';
|
|
4
|
+
import { TONES } from '@iyulab/u-board/toolkit';
|
|
5
|
+
const ROUTES = ['straight', 'orthogonal', 'curved'];
|
|
6
|
+
/**
|
|
7
|
+
* The panel of a link of a canvas board: the parts it joins (set by joining them on
|
|
8
|
+
* the canvas), its label, the item it reads and the field whose status it takes — its colour the status's tone, its line
|
|
9
|
+
* the status's emphasis — and, fixed, its tone without a status, its route, arrowheads and dash. A setting left empty
|
|
10
|
+
* goes from the link. Arrowheads read from a field (a direction that reverses) are written in the board's source; the
|
|
11
|
+
* panel names the field and replaces them only when a fixed choice is made.
|
|
12
|
+
*/
|
|
13
|
+
export function LinkPanel({ id, board, onChange, findings = [], labels = DEFAULT_DESIGNER_LABELS }) {
|
|
14
|
+
const link = (board.layout.kind === 'canvas' ? board.layout.links?.[id] : undefined) ?? { from: '', to: '' };
|
|
15
|
+
const set = (change) => {
|
|
16
|
+
const next = { ...link, ...change };
|
|
17
|
+
for (const [key, value] of Object.entries(change))
|
|
18
|
+
if (value === undefined || value === '' || value === false)
|
|
19
|
+
delete next[key];
|
|
20
|
+
onChange(next);
|
|
21
|
+
};
|
|
22
|
+
const text = (label, value, change) => (_jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [label, _jsx("input", { type: "text", value: value ?? '', onChange: e => change(e.target.value) })] }));
|
|
23
|
+
return (_jsxs("div", { className: "ub-panel ub-panel--link", children: [_jsx("h2", { className: "ub-panel__heading", children: labels.linkHeading.replace('{from}', link.from).replace('{to}', link.to) }), text(labels.linkLabel, link.label, label => set({ label })), text(labels.linkReads, link.of, of => set({ of })), text(labels.linkShows, link.show, show => set({ show })), _jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.linkTone, _jsxs("select", { value: link.tone ?? '', onChange: e => set({ tone: (e.target.value || undefined) }), children: [_jsx("option", { value: "", children: labels.linkToneNone }), TONES.map(tone => (_jsx("option", { value: tone, children: tone }, tone)))] })] }), _jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.linkRoute, _jsx("select", { value: link.route ?? 'straight', onChange: e => set({ route: e.target.value === 'straight' ? undefined : e.target.value }), children: ROUTES.map(route => (_jsx("option", { value: route, children: route }, route))) })] }), _jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.linkArrow, _jsxs("select", { value: typeof link.arrow === 'object' ? 'field' : (link.arrow ?? ''), onChange: e => e.target.value !== 'field' && set({ arrow: (e.target.value || undefined) }), children: [_jsx("option", { value: "", children: labels.arrowNone }), _jsx("option", { value: "end", children: labels.arrowEnd.replace('{to}', link.to) }), _jsx("option", { value: "start", children: labels.arrowStart.replace('{from}', link.from) }), _jsx("option", { value: "both", children: labels.arrowBoth }), typeof link.arrow === 'object' && _jsx("option", { value: "field", children: labels.arrowFromField.replace('{field}', link.arrow.field) })] })] }), _jsxs("label", { className: "ub-panel__field ub-panel__field--check", children: [_jsx("input", { type: "checkbox", checked: link.dash === true, onChange: e => set({ dash: e.target.checked }) }), labels.linkDash] }), findings.length > 0 && (_jsx("ul", { className: "ub-panel__findings", role: "alert", children: findings.map(finding => (_jsx("li", { children: finding.message }, `${finding.path}:${finding.message}`))) }))] }));
|
|
24
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type DesignerLabels } from '../labels.js';
|
|
2
|
+
import type { ValueMapping } from '@iyulab/u-board/toolkit';
|
|
3
|
+
/** Bands as written — `ok, 1, warn, 5, alarm`: names and rising numbers in turn, from a name — or `undefined` when they
|
|
4
|
+
* are not. */
|
|
5
|
+
export declare function parseBands(text: string): (string | number)[] | undefined;
|
|
6
|
+
export interface MappingEditorProps {
|
|
7
|
+
/** What the mapping is for, as its legend says it: a field's status, or its words. */
|
|
8
|
+
legend: string;
|
|
9
|
+
value: ValueMapping | 'raw' | undefined;
|
|
10
|
+
/** The mapping as the editor made it — none: `undefined`. */
|
|
11
|
+
onChange: (value: ValueMapping | 'raw' | undefined) => void;
|
|
12
|
+
/** Offer «the value already is a status name» (`raw`) — a status, not words. */
|
|
13
|
+
raw?: boolean;
|
|
14
|
+
/** The names a mapping may map to, to choose from — the board's statuses. */
|
|
15
|
+
names?: readonly string[];
|
|
16
|
+
labels?: DesignerLabels;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A mapping of a field's raw value to a name — its status, or the words it is shown as — in the language's two forms: by
|
|
20
|
+
* value (`{ "down": "alarm", "*": "off" }`) or by number bands (`["ok", 1, "warn", 5, "alarm"]`); for a status, also
|
|
21
|
+
* «the value already is one» (`raw`). A row with no value, and bands that do not read, are not written.
|
|
22
|
+
*/
|
|
23
|
+
export declare function MappingEditor({ legend, value, onChange, raw, names, labels }: MappingEditorProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { FIELD_STYLE } from './field-style.js';
|
|
3
|
+
import { DEFAULT_DESIGNER_LABELS } from '../labels.js';
|
|
4
|
+
import { useId, useState } from 'react';
|
|
5
|
+
import { ERROR_STYLE, GROUP_STYLE } from '@iyulab/u-board/toolkit';
|
|
6
|
+
const formOf = (value) => (Array.isArray(value) ? 'bands' : 'map');
|
|
7
|
+
const rowsOf = (value) => value && !Array.isArray(value) ? Object.entries(value).filter(([from]) => from !== '*').map(([from, to]) => ({ from, to })) : [];
|
|
8
|
+
const bandsText = (value) => (Array.isArray(value) ? value.join(', ') : '');
|
|
9
|
+
/** Bands as written — `ok, 1, warn, 5, alarm`: names and rising numbers in turn, from a name — or `undefined` when they
|
|
10
|
+
* are not. */
|
|
11
|
+
export function parseBands(text) {
|
|
12
|
+
const parts = text.split(',').map(part => part.trim());
|
|
13
|
+
if (parts.length < 3 || parts.length % 2 === 0)
|
|
14
|
+
return undefined;
|
|
15
|
+
const bands = [];
|
|
16
|
+
for (const [i, part] of parts.entries()) {
|
|
17
|
+
if (i % 2 === 0) {
|
|
18
|
+
if (part === '')
|
|
19
|
+
return undefined;
|
|
20
|
+
bands.push(part);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
const bound = Number(part);
|
|
24
|
+
if (part === '' || !Number.isFinite(bound) || (i > 1 && bound <= bands[i - 2]))
|
|
25
|
+
return undefined;
|
|
26
|
+
bands.push(bound);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return bands;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A mapping of a field's raw value to a name — its status, or the words it is shown as — in the language's two forms: by
|
|
33
|
+
* value (`{ "down": "alarm", "*": "off" }`) or by number bands (`["ok", 1, "warn", 5, "alarm"]`); for a status, also
|
|
34
|
+
* «the value already is one» (`raw`). A row with no value, and bands that do not read, are not written.
|
|
35
|
+
*/
|
|
36
|
+
export function MappingEditor({ legend, value, onChange, raw = false, names = [], labels = DEFAULT_DESIGNER_LABELS }) {
|
|
37
|
+
const list = useId();
|
|
38
|
+
// A form chosen before it holds anything to write: shown while the mapping is still none.
|
|
39
|
+
const [drafting, setDrafting] = useState(null);
|
|
40
|
+
const kind = value === undefined ? (drafting ?? 'none') : value === 'raw' ? 'raw' : formOf(value);
|
|
41
|
+
const [rows, setRows] = useState(() => rowsOf(value === 'raw' ? undefined : value));
|
|
42
|
+
const [otherwise, setOtherwise] = useState(() => (value && value !== 'raw' && !Array.isArray(value) ? (value['*'] ?? '') : ''));
|
|
43
|
+
const [bands, setBands] = useState(() => bandsText(value === 'raw' ? undefined : value));
|
|
44
|
+
const writeMap = (nextRows, nextOtherwise) => {
|
|
45
|
+
const map = {};
|
|
46
|
+
for (const row of nextRows)
|
|
47
|
+
if (row.from.trim() !== '' && row.to.trim() !== '')
|
|
48
|
+
map[row.from.trim()] = row.to.trim();
|
|
49
|
+
if (nextOtherwise.trim() !== '')
|
|
50
|
+
map['*'] = nextOtherwise.trim();
|
|
51
|
+
setDrafting('map');
|
|
52
|
+
onChange(Object.keys(map).length > 0 ? map : undefined);
|
|
53
|
+
};
|
|
54
|
+
const setRow = (i, change) => {
|
|
55
|
+
const next = rows.map((row, j) => (j === i ? { ...row, ...change } : row));
|
|
56
|
+
setRows(next);
|
|
57
|
+
writeMap(next, otherwise);
|
|
58
|
+
};
|
|
59
|
+
const choose = (next) => {
|
|
60
|
+
setDrafting(null);
|
|
61
|
+
if (next === 'none')
|
|
62
|
+
onChange(undefined);
|
|
63
|
+
else if (next === 'raw')
|
|
64
|
+
onChange('raw');
|
|
65
|
+
else if (next === 'map')
|
|
66
|
+
writeMap(rows, otherwise);
|
|
67
|
+
else {
|
|
68
|
+
setDrafting('bands');
|
|
69
|
+
onChange(parseBands(bands));
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const nameInput = (text, label, change) => (_jsx("input", { type: "text", "aria-label": label, list: names.length > 0 ? list : undefined, value: text, onChange: e => change(e.target.value) }));
|
|
73
|
+
const bandsRead = parseBands(bands);
|
|
74
|
+
return (_jsxs("fieldset", { className: "ub-panel__group", style: { minWidth: 0 }, children: [_jsx("legend", { children: legend }), _jsxs("select", { "aria-label": legend, value: kind, onChange: e => choose(e.target.value), children: [_jsx("option", { value: "none", children: labels.mappingNone }), raw && _jsx("option", { value: "raw", children: labels.mappingRaw }), _jsx("option", { value: "map", children: labels.mappingMap }), _jsx("option", { value: "bands", children: labels.mappingBands })] }), names.length > 0 && (_jsx("datalist", { id: list, children: names.map(name => (_jsx("option", { value: name, children: name }, name))) })), kind === 'map' && (_jsxs(_Fragment, { children: [rows.map((row, i) => (_jsxs("div", { className: "ub-panel__field", style: GROUP_STYLE, children: [_jsx("input", { type: "text", "aria-label": labels.mappingFrom.replace('{n}', String(i + 1)), value: row.from, onChange: e => setRow(i, { from: e.target.value }) }), nameInput(row.to, labels.mappingTo.replace('{n}', String(i + 1)), to => setRow(i, { to }))] }, i))), _jsx("button", { type: "button", className: "ub-action", onClick: () => setRows([...rows, { from: '', to: '' }]), children: labels.mappingAddRow }), _jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.mappingOtherwise, nameInput(otherwise, labels.mappingOtherwise, text => {
|
|
75
|
+
setOtherwise(text);
|
|
76
|
+
writeMap(rows, text);
|
|
77
|
+
})] })] })), kind === 'bands' && (_jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.mappingBandsText, _jsx("input", { type: "text", value: bands, "aria-invalid": bands.trim() !== '' && !bandsRead, onChange: e => {
|
|
78
|
+
setBands(e.target.value);
|
|
79
|
+
const read = parseBands(e.target.value);
|
|
80
|
+
if (read)
|
|
81
|
+
onChange(read);
|
|
82
|
+
} }), bands.trim() !== '' && !bandsRead && (_jsx("span", { role: "alert", style: ERROR_STYLE, children: labels.mappingBandsInvalid }))] }))] }));
|
|
83
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type DesignerLabels } from '../labels.js';
|
|
2
|
+
import { type ReactNode } from 'react';
|
|
3
|
+
import type { BoardFinding } from '../board/check.js';
|
|
4
|
+
import type { Board, Part } from '@iyulab/u-board/toolkit';
|
|
5
|
+
export interface PartPanelProps {
|
|
6
|
+
/** The part's id in the board's `parts`. */
|
|
7
|
+
id: string;
|
|
8
|
+
board: Board;
|
|
9
|
+
/** Replaces the part with what the panel made of it. */
|
|
10
|
+
onChange: (part: Part) => void;
|
|
11
|
+
/** What the board's check finds at this part — shown under it, where it is fixed. */
|
|
12
|
+
findings?: readonly BoardFinding[];
|
|
13
|
+
/** The part's triggers' editor, shown under its settings. */
|
|
14
|
+
triggers?: ReactNode;
|
|
15
|
+
labels?: DesignerLabels;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The panel of a part of a board: its title, its kind, what it reads (a data read,
|
|
19
|
+
* and one item of it by key), which fields it shows, then its kind's options — those the board reads itself (a field's
|
|
20
|
+
* name, offered from what the part reads; one written as a structure is shown, and set by the assistant or the JSON),
|
|
21
|
+
* and the widget's, with the controls the widget library describes. A setting left empty goes from the part, so the
|
|
22
|
+
* board keeps only what was set. A part made per item reads the items its `each` names, shown and not changed here.
|
|
23
|
+
*/
|
|
24
|
+
export declare function PartPanel({ id, board, onChange, findings, triggers, labels }: PartPanelProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { FIELD_STYLE } from './field-style.js';
|
|
3
|
+
import { DEFAULT_DESIGNER_LABELS } from '../labels.js';
|
|
4
|
+
import { useId, useMemo } from 'react';
|
|
5
|
+
import { PART_KINDS, partKind } from '@iyulab/u-board/toolkit';
|
|
6
|
+
import { widgetFields } from './widget-fields.js';
|
|
7
|
+
import { FieldControl } from './WidgetPropsForm.js';
|
|
8
|
+
/** `<data>` or `<data>[<key>]`, as a part names what it reads. */
|
|
9
|
+
const OF = /^([\w-]+)(?:\[([^\]]+)\])?$/;
|
|
10
|
+
/**
|
|
11
|
+
* The panel of a part of a board: its title, its kind, what it reads (a data read,
|
|
12
|
+
* and one item of it by key), which fields it shows, then its kind's options — those the board reads itself (a field's
|
|
13
|
+
* name, offered from what the part reads; one written as a structure is shown, and set by the assistant or the JSON),
|
|
14
|
+
* and the widget's, with the controls the widget library describes. A setting left empty goes from the part, so the
|
|
15
|
+
* board keeps only what was set. A part made per item reads the items its `each` names, shown and not changed here.
|
|
16
|
+
*/
|
|
17
|
+
export function PartPanel({ id, board, onChange, findings = [], triggers, labels = DEFAULT_DESIGNER_LABELS }) {
|
|
18
|
+
const part = board.parts[id] ?? {};
|
|
19
|
+
const fieldList = useId();
|
|
20
|
+
const set = (key, value) => {
|
|
21
|
+
const { [key]: _gone, ...rest } = part;
|
|
22
|
+
onChange(value === undefined || value === '' ? rest : { ...rest, [key]: value });
|
|
23
|
+
};
|
|
24
|
+
const of = typeof part.of === 'string' ? OF.exec(part.of) : null;
|
|
25
|
+
const data = of?.[1] ?? '';
|
|
26
|
+
const key = of?.[2] ?? '';
|
|
27
|
+
const reads = typeof part.each === 'string' ? part.each : data;
|
|
28
|
+
const fields = Object.keys(board.data?.[reads]?.fields ?? {});
|
|
29
|
+
const show = part.show === undefined ? '' : Array.isArray(part.show) ? part.show.join(', ') : part.show;
|
|
30
|
+
const kind = partKind(part.as ?? (part.use !== undefined ? board.presets?.[part.use]?.as : undefined) ?? '');
|
|
31
|
+
const widgetOptions = useMemo(() => (kind?.widget === undefined ? [] : widgetFields({ type: kind.widget, props: {} }, labels.locale).fields.filter(f => f.section === 'options' && kind.options.includes(f.key))), [kind, labels.locale]);
|
|
32
|
+
const own = (kind?.own ?? []).filter(key => !['size', 'parts', 'place'].includes(key) && !(kind?.kind === 'element' && ['element', 'props', 'bind'].includes(key)));
|
|
33
|
+
const writeOf = (nextData, nextKey) => set('of', nextData === '' ? undefined : nextKey.trim() === '' ? nextData : `${nextData}[${nextKey.trim()}]`);
|
|
34
|
+
return (_jsxs("div", { className: "ub-panel ub-panel--part", children: [_jsx("h2", { className: "ub-panel__heading", children: labels.partHeading.replace('{id}', id) }), _jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.partTitle, _jsx("input", { type: "text", value: part.title ?? '', onChange: e => set('title', e.target.value) })] }), _jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.partKind, _jsxs("select", { value: part.as ?? '', onChange: e => set('as', e.target.value), children: [part.as === undefined && _jsx("option", { value: "", children: labels.partFromPreset }), PART_KINDS.map(kind => (_jsx("option", { value: kind.kind, children: kind.kind }, kind.kind)))] })] }), typeof part.each === 'string' ? (_jsx("p", { className: "ub-panel__hint", children: labels.partEach.replace('{data}', part.each) })) : (_jsxs(_Fragment, { children: [_jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.partOf, _jsxs("select", { value: data, onChange: e => writeOf(e.target.value, key), children: [_jsx("option", { value: "", children: labels.partOfNone }), Object.keys(board.data ?? {}).map(name => (_jsx("option", { value: name, children: name }, name)))] })] }), data !== '' && board.data?.[data]?.list !== undefined && (_jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.partItem, _jsx("input", { type: "text", value: key, placeholder: labels.partItemAll, onChange: e => writeOf(data, e.target.value) })] }))] })), _jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.partShow, _jsx("input", { type: "text", list: fields.length > 0 ? fieldList : undefined, value: show, onChange: e => {
|
|
35
|
+
const names = e.target.value.split(',').map(name => name.trim()).filter(name => name !== '');
|
|
36
|
+
set('show', names.length === 0 ? undefined : names.length === 1 && !e.target.value.includes(',') ? names[0] : names);
|
|
37
|
+
} })] }), fields.length > 0 && (_jsx("datalist", { id: fieldList, children: fields.map(name => (_jsx("option", { value: name, children: name }, name))) })), kind?.kind === 'element' && _jsx(ElementPartFields, { part: part, board: board, onChange: onChange, labels: labels, fieldList: fields.length > 0 ? fieldList : undefined }), own.length > 0 && (_jsxs("fieldset", { className: "ub-panel__group", style: { minWidth: 0 }, children: [_jsx("legend", { children: labels.partOwnOptions }), own.map(key => {
|
|
38
|
+
const value = part[key];
|
|
39
|
+
const structured = value !== undefined && typeof value !== 'string' && typeof value !== 'number';
|
|
40
|
+
return (_jsxs("div", { className: "ub-panel__field", style: FIELD_STYLE, children: [_jsx("code", { "aria-hidden": "true", children: key }), _jsx("input", { type: "text", "aria-label": key, list: fields.length > 0 ? fieldList : undefined, value: structured ? JSON.stringify(value) : value === undefined ? '' : String(value), readOnly: structured, title: structured ? labels.partStructured : undefined, onChange: e => {
|
|
41
|
+
const text = e.target.value.trim();
|
|
42
|
+
set(key, text !== '' && /^-?\d+(\.\d+)?$/.test(text) && key === 'max' ? Number(text) : text);
|
|
43
|
+
} })] }, key));
|
|
44
|
+
})] })), widgetOptions.length > 0 && (_jsxs("fieldset", { className: "ub-panel__group", style: { minWidth: 0 }, children: [_jsx("legend", { children: labels.partWidgetOptions }), widgetOptions.map(field => (_jsx(FieldControl, { field: field, value: part[field.key], write: value => set(field.key, value), labels: labels }, field.key)))] })), triggers, findings.length > 0 && (_jsx("ul", { className: "ub-panel__findings", role: "alert", children: findings.map(finding => (_jsx("li", { children: finding.message }, `${finding.path}:${finding.message}`))) }))] }));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* An element part's drawing: which of the board's elements, and each prop it takes — bound to a field of what the
|
|
48
|
+
* part reads (`<field>`, or its status `<field>.status` or tone `<field>.tone`), or fixed. A prop that takes a list or
|
|
49
|
+
* a record is fixed only in the board's JSON.
|
|
50
|
+
*/
|
|
51
|
+
function ElementPartFields({ part, board, onChange, labels, fieldList }) {
|
|
52
|
+
const name = typeof part.element === 'string' ? part.element : '';
|
|
53
|
+
const props = board.elements?.[name]?.props ?? {};
|
|
54
|
+
const bind = (part.bind ?? {});
|
|
55
|
+
const fixed = (part.props ?? {});
|
|
56
|
+
/** The part with its bound and fixed props as given — an empty set of either left out. */
|
|
57
|
+
const write = (nextBind, nextFixed) => {
|
|
58
|
+
const { bind: _b, props: _p, ...rest } = part;
|
|
59
|
+
onChange({ ...rest, ...(Object.keys(nextFixed).length > 0 ? { props: nextFixed } : {}), ...(Object.keys(nextBind).length > 0 ? { bind: nextBind } : {}) });
|
|
60
|
+
};
|
|
61
|
+
return (_jsxs("fieldset", { className: "ub-panel__group", style: { minWidth: 0 }, children: [_jsx("legend", { children: labels.elementProps }), _jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.boardElements, _jsxs("select", { value: name, onChange: e => onChange({ ...part, element: e.target.value }), children: [name === '' && _jsx("option", { value: "", children: "\u2014" }), Object.keys(board.elements ?? {}).map(element => (_jsx("option", { value: element, children: element }, element)))] })] }), Object.entries(props).map(([prop, type]) => {
|
|
62
|
+
const structured = type === 'list' || type === 'object';
|
|
63
|
+
const value = bind[prop] ?? (fixed[prop] === undefined ? '' : String(fixed[prop]));
|
|
64
|
+
return (_jsxs("div", { className: "ub-panel__field", style: FIELD_STYLE, children: [_jsxs("code", { "aria-hidden": "true", children: [prop, " \u00B7 ", type] }), _jsx("input", { type: "text", "aria-label": prop, list: fieldList, value: structured && prop in fixed ? JSON.stringify(fixed[prop]) : value, readOnly: structured && prop in fixed, onChange: e => {
|
|
65
|
+
const text = e.target.value.trim();
|
|
66
|
+
const { [prop]: _b, ...restBind } = bind;
|
|
67
|
+
const { [prop]: _f, ...restFixed } = fixed;
|
|
68
|
+
// A field's name (or its status, its tone) binds the prop; a value of the prop's type fixes it.
|
|
69
|
+
const asFixed = type === 'number' && /^-?\d+(\.\d+)?$/.test(text) ? Number(text) : type === 'boolean' && /^(true|false)$/.test(text) ? text === 'true' : undefined;
|
|
70
|
+
if (text === '')
|
|
71
|
+
write(restBind, restFixed);
|
|
72
|
+
else if (asFixed !== undefined || (type === 'string' && text.startsWith('"')))
|
|
73
|
+
write(restBind, { ...restFixed, [prop]: asFixed ?? text.replace(/^"|"$/g, '') });
|
|
74
|
+
else
|
|
75
|
+
write({ ...restBind, [prop]: text }, restFixed);
|
|
76
|
+
} })] }, prop));
|
|
77
|
+
})] }));
|
|
78
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type DesignerLabels } from '../labels.js';
|
|
2
|
+
import { type Board, type Shape } from '@iyulab/u-board/toolkit';
|
|
3
|
+
import type { BoardFinding } from '../board/check.js';
|
|
4
|
+
export interface ShapePanelProps {
|
|
5
|
+
id: string;
|
|
6
|
+
board: Board;
|
|
7
|
+
/** The shape as the panel made it. */
|
|
8
|
+
onChange: (shape: Shape) => void;
|
|
9
|
+
findings?: readonly BoardFinding[];
|
|
10
|
+
labels?: DesignerLabels;
|
|
11
|
+
}
|
|
12
|
+
/** A line's points as written — `20,20 220,20`: two or more `x,y` by spaces — or `undefined` when they are not. */
|
|
13
|
+
export declare function parsePoints(text: string): [number, number][] | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* The panel of a shape of a canvas board — a frame (a box, with a label at its top),
|
|
16
|
+
* a text, or a line or a path: its words (a line's points, a path's drawing), its tone, and a frame's or a line's line or
|
|
17
|
+
* a text's size. Where it is and how large is set on the canvas. A
|
|
18
|
+
* setting left empty goes from the shape.
|
|
19
|
+
*/
|
|
20
|
+
export declare function ShapePanel({ id, board, onChange, findings, labels }: ShapePanelProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { FIELD_STYLE } from './field-style.js';
|
|
3
|
+
import { DEFAULT_DESIGNER_LABELS } from '../labels.js';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { ERROR_STYLE } from '@iyulab/u-board/toolkit';
|
|
6
|
+
import { TONES } from '@iyulab/u-board/toolkit';
|
|
7
|
+
/** A line's points as written — `20,20 220,20`: two or more `x,y` by spaces — or `undefined` when they are not. */
|
|
8
|
+
export function parsePoints(text) {
|
|
9
|
+
const pairs = text.trim().split(/\s+/).filter(pair => pair !== '');
|
|
10
|
+
if (pairs.length < 2)
|
|
11
|
+
return undefined;
|
|
12
|
+
const points = [];
|
|
13
|
+
for (const pair of pairs) {
|
|
14
|
+
const [x, y, ...rest] = pair.split(',').map(Number);
|
|
15
|
+
if (rest.length > 0 || !Number.isFinite(x) || !Number.isFinite(y))
|
|
16
|
+
return undefined;
|
|
17
|
+
points.push([x, y]);
|
|
18
|
+
}
|
|
19
|
+
return points;
|
|
20
|
+
}
|
|
21
|
+
/** A line's points, or a path's drawing, as its panel edits them — a line's written only once they read. */
|
|
22
|
+
function LineFields({ shape, onChange, labels }) {
|
|
23
|
+
const [points, setPoints] = useState(() => (shape.line ?? []).map(([x, y]) => `${x},${y}`).join(' '));
|
|
24
|
+
if (shape.path !== undefined) {
|
|
25
|
+
return (_jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.shapePath, _jsx("textarea", { rows: 3, spellCheck: false, value: shape.path, onChange: e => onChange({ ...shape, path: e.target.value }) })] }));
|
|
26
|
+
}
|
|
27
|
+
const read = parsePoints(points);
|
|
28
|
+
return (_jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.shapePoints, _jsx("input", { type: "text", value: points, "aria-invalid": !read, onChange: e => {
|
|
29
|
+
setPoints(e.target.value);
|
|
30
|
+
const next = parsePoints(e.target.value);
|
|
31
|
+
if (next)
|
|
32
|
+
onChange({ ...shape, line: next });
|
|
33
|
+
} }), !read && (_jsx("span", { role: "alert", style: ERROR_STYLE, children: labels.shapePointsInvalid }))] }));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The panel of a shape of a canvas board — a frame (a box, with a label at its top),
|
|
37
|
+
* a text, or a line or a path: its words (a line's points, a path's drawing), its tone, and a frame's or a line's line or
|
|
38
|
+
* a text's size. Where it is and how large is set on the canvas. A
|
|
39
|
+
* setting left empty goes from the shape.
|
|
40
|
+
*/
|
|
41
|
+
export function ShapePanel({ id, board, onChange, findings = [], labels = DEFAULT_DESIGNER_LABELS }) {
|
|
42
|
+
const shape = (board.layout.kind === 'canvas' ? board.layout.shapes?.[id] : undefined) ?? {};
|
|
43
|
+
const set = (change) => {
|
|
44
|
+
const next = { ...shape, ...change };
|
|
45
|
+
for (const [key, value] of Object.entries(change))
|
|
46
|
+
if (value === undefined || value === '' || value === false)
|
|
47
|
+
delete next[key];
|
|
48
|
+
onChange(next);
|
|
49
|
+
};
|
|
50
|
+
const number = (label, value, change) => (_jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [label, _jsx("input", { type: "number", min: 1, value: value ?? '', onChange: e => change(e.target.value === '' ? undefined : Number(e.target.value)) })] }));
|
|
51
|
+
const box = shape.box !== undefined;
|
|
52
|
+
const drawn = shape.line !== undefined || shape.path !== undefined;
|
|
53
|
+
return (_jsxs("div", { className: "ub-panel ub-panel--shape", children: [_jsx("h2", { className: "ub-panel__heading", children: labels.shapeHeading.replace('{id}', id) }), drawn ? (_jsx(LineFields, { shape: shape, onChange: onChange, labels: labels })) : (_jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [box ? labels.shapeLabel : labels.shapeText, _jsx("input", { type: "text", value: (box ? shape.label : shape.text) ?? '', onChange: e => (box ? set({ label: e.target.value }) : onChange({ ...shape, text: e.target.value })) })] })), _jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.shapeTone, _jsxs("select", { value: shape.tone ?? '', onChange: e => set({ tone: (e.target.value || undefined) }), children: [_jsx("option", { value: "", children: "\u2014" }), TONES.map(tone => (_jsx("option", { value: tone, children: tone }, tone)))] })] }), box || drawn ? (_jsxs(_Fragment, { children: [number(labels.shapeWidth, shape.width, width => set({ width })), _jsxs("label", { className: "ub-panel__field ub-panel__field--check", children: [_jsx("input", { type: "checkbox", checked: shape.dash === true, onChange: e => set({ dash: e.target.checked }) }), labels.linkDash] })] })) : (number(labels.shapeSize, shape.size, size => set({ size }))), findings.length > 0 && (_jsx("ul", { className: "ub-panel__findings", role: "alert", children: findings.map(finding => (_jsx("li", { children: finding.message }, `${finding.path}:${finding.message}`))) }))] }));
|
|
54
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type DesignerLabels } from '../labels.js';
|
|
2
|
+
import type { StateRule, ValueMap, Widget } from '@iyulab/u-board/picture';
|
|
3
|
+
export interface StatesEditorProps {
|
|
4
|
+
/** The part's widget — its bindings are what a state can be read from. */
|
|
5
|
+
widget: Widget;
|
|
6
|
+
states: StateRule | undefined;
|
|
7
|
+
/** A new rule, or `undefined` to take the part's states away. */
|
|
8
|
+
onChange: (states: StateRule | undefined) => void;
|
|
9
|
+
labels: DesignerLabels;
|
|
10
|
+
}
|
|
11
|
+
/** The state names a map gives, each once, in the order the map tries them: values, ranges, then anything else. */
|
|
12
|
+
export declare function stateNames(map: ValueMap<unknown> | undefined): string[];
|
|
13
|
+
/**
|
|
14
|
+
* A part's states: which of its widget's bindings they are read from, the map from that value to a state's name — the
|
|
15
|
+
* same rows a binding's value map has — and how each named state is shown (a ring, blinking, hidden). Written as the
|
|
16
|
+
* author edits; a draft that names no state yet, or has a range that is not one, is held here until it does.
|
|
17
|
+
*/
|
|
18
|
+
export declare function StatesEditor({ widget, states, onChange, labels }: StatesEditorProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { FIELD_STYLE } from './field-style.js';
|
|
3
|
+
import { useId, useState } from 'react';
|
|
4
|
+
import { GROUP_STYLE } from '@iyulab/u-board/toolkit';
|
|
5
|
+
import { ValueMapFields, draftMap, invalidRanges, mapFromDraft } from './ValueMapFields.js';
|
|
6
|
+
import { THEME_COLORS } from '@iyulab/u-board/toolkit';
|
|
7
|
+
/** The state names a map gives, each once, in the order the map tries them: values, ranges, then anything else. */
|
|
8
|
+
export function stateNames(map) {
|
|
9
|
+
if (!map)
|
|
10
|
+
return [];
|
|
11
|
+
const names = [...Object.values(map.values ?? {}), ...(map.ranges ?? []).map(r => r.value), ...('otherwise' in map ? [map.otherwise] : [])];
|
|
12
|
+
return [...new Set(names.filter((name) => typeof name === 'string' && name !== ''))];
|
|
13
|
+
}
|
|
14
|
+
/** `looks` kept to the names the map still gives — and, when one name was replaced by one other, carried to it. */
|
|
15
|
+
function followNames(looks, before, after) {
|
|
16
|
+
const gone = before.filter(name => !after.includes(name));
|
|
17
|
+
const added = after.filter(name => !before.includes(name));
|
|
18
|
+
const kept = Object.fromEntries(Object.entries(looks).filter(([name]) => after.includes(name)));
|
|
19
|
+
if (gone.length === 1 && added.length === 1 && looks[gone[0]] && !kept[added[0]])
|
|
20
|
+
kept[added[0]] = looks[gone[0]];
|
|
21
|
+
return kept;
|
|
22
|
+
}
|
|
23
|
+
/** A look with nothing in it is no look. */
|
|
24
|
+
const isEmptyLook = (look) => !look.outline && !look.blink && !look.hidden;
|
|
25
|
+
/**
|
|
26
|
+
* A part's states: which of its widget's bindings they are read from, the map from that value to a state's name — the
|
|
27
|
+
* same rows a binding's value map has — and how each named state is shown (a ring, blinking, hidden). Written as the
|
|
28
|
+
* author edits; a draft that names no state yet, or has a range that is not one, is held here until it does.
|
|
29
|
+
*/
|
|
30
|
+
export function StatesEditor({ widget, states, onChange, labels }) {
|
|
31
|
+
const keys = Object.keys(widget.bindings ?? {});
|
|
32
|
+
const themeColorsId = useId();
|
|
33
|
+
const [from, setFrom] = useState(states?.from ?? '');
|
|
34
|
+
const [draft, setDraft] = useState(() => draftMap(states?.map));
|
|
35
|
+
const [looks, setLooks] = useState(states?.looks ?? {});
|
|
36
|
+
// The rule last seen, and the one this editor last wrote: a rule arriving that is not what it wrote came from
|
|
37
|
+
// elsewhere (Undo, another panel) and replaces the draft; its own leaves the draft alone, empty rows included.
|
|
38
|
+
const [seen, setSeen] = useState(states);
|
|
39
|
+
const [written, setWritten] = useState(states);
|
|
40
|
+
if (states !== seen) {
|
|
41
|
+
setSeen(states);
|
|
42
|
+
if (states !== written) {
|
|
43
|
+
setFrom(states?.from ?? '');
|
|
44
|
+
setDraft(draftMap(states?.map));
|
|
45
|
+
setLooks(states?.looks ?? {});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const write = (nextFrom, nextDraft, nextLooks) => {
|
|
49
|
+
const map = invalidRanges(nextDraft).length === 0 ? mapFromDraft(nextDraft) : undefined;
|
|
50
|
+
const names = stateNames(map);
|
|
51
|
+
if (!nextFrom || !map || names.length === 0)
|
|
52
|
+
return;
|
|
53
|
+
const kept = Object.fromEntries(Object.entries(nextLooks).filter(([name, look]) => names.includes(name) && !isEmptyLook(look)));
|
|
54
|
+
const rule = { from: nextFrom, map: map, ...(Object.keys(kept).length > 0 && { looks: kept }) };
|
|
55
|
+
setWritten(rule);
|
|
56
|
+
onChange(rule);
|
|
57
|
+
};
|
|
58
|
+
if (keys.length === 0 && !states) {
|
|
59
|
+
return (_jsxs("section", { className: "ub-panel__section", children: [_jsx("h3", { className: "ub-panel__subheading", children: labels.statesHeading }), _jsx("p", { className: "ub-panel__hint", children: labels.statesNeedBinding })] }));
|
|
60
|
+
}
|
|
61
|
+
const names = stateNames(invalidRanges(draft).length === 0 ? mapFromDraft(draft) : undefined);
|
|
62
|
+
const setLook = (name, change) => {
|
|
63
|
+
const next = { ...looks, [name]: { ...looks[name], ...change } };
|
|
64
|
+
setLooks(next);
|
|
65
|
+
write(from, draft, next);
|
|
66
|
+
};
|
|
67
|
+
return (_jsxs("section", { className: "ub-panel__section", children: [_jsx("h3", { className: "ub-panel__subheading", children: labels.statesHeading }), _jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.statesFrom, _jsxs("select", { value: from, onChange: e => {
|
|
68
|
+
setFrom(e.target.value);
|
|
69
|
+
write(e.target.value, draft, looks);
|
|
70
|
+
}, children: [_jsx("option", { value: "", children: labels.statesNone }), [...new Set([...keys, ...(from ? [from] : [])])].map(key => (_jsx("option", { value: key, children: key }, key)))] })] }), from !== '' && (_jsxs(_Fragment, { children: [_jsx(ValueMapFields, { draft: draft, onChange: update => {
|
|
71
|
+
const next = update(draft);
|
|
72
|
+
const before = stateNames(invalidRanges(draft).length === 0 ? mapFromDraft(draft) : undefined);
|
|
73
|
+
const after = stateNames(invalidRanges(next).length === 0 ? mapFromDraft(next) : undefined);
|
|
74
|
+
const nextLooks = followNames(looks, before, after);
|
|
75
|
+
setDraft(next);
|
|
76
|
+
setLooks(nextLooks);
|
|
77
|
+
write(from, next, nextLooks);
|
|
78
|
+
}, labels: labels, heading: labels.statesMapHeading, toLabel: { mapping: labels.stateForValue, range: labels.stateForRange }, placeholders: { from: 'Fault', to: 'fault' } }), names.length > 0 && (_jsxs("fieldset", { className: "ub-panel__state-looks", style: { minWidth: 0 }, children: [_jsx("legend", { children: labels.statesLooks }), _jsx("datalist", { id: themeColorsId, children: THEME_COLORS.map(name => (_jsx("option", { value: name, children: name }, name))) }), names.map(name => (_jsxs("div", { className: "ub-panel__state-look", style: GROUP_STYLE, children: [_jsxs("label", { style: FIELD_STYLE, children: [labels.stateOutline.replace('{state}', name), _jsx("input", { value: looks[name]?.outline ?? '', onChange: e => setLook(name, { outline: e.target.value || undefined }), placeholder: "negative, #dc2626", list: themeColorsId })] }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: !!looks[name]?.blink, onChange: e => setLook(name, { blink: e.target.checked || undefined }) }), labels.stateBlink.replace('{state}', name)] }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: !!looks[name]?.hidden, onChange: e => setLook(name, { hidden: e.target.checked || undefined }) }), labels.stateHidden.replace('{state}', name)] })] }, name)))] }))] })), states && (_jsx("button", { type: "button", className: "ub-action", onClick: () => onChange(undefined), children: labels.removeStates }))] }));
|
|
79
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type DesignerLabels } from '../labels.js';
|
|
2
|
+
import { type Status } from '@iyulab/u-board/toolkit';
|
|
3
|
+
export interface StatusesPanelProps {
|
|
4
|
+
statuses: Readonly<Record<string, Status>> | undefined;
|
|
5
|
+
/** The board's statuses as the panel made them — none left: `undefined`. */
|
|
6
|
+
onChange: (statuses: Record<string, Status> | undefined) => void;
|
|
7
|
+
labels?: DesignerLabels;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The board's statuses — each name a field's state gives, with the tone it is drawn in, its words, its mark and a second
|
|
11
|
+
* channel besides colour. A status is added by name and removed; its name is not changed here, since the states of
|
|
12
|
+
* the board's fields name it.
|
|
13
|
+
*/
|
|
14
|
+
export declare function StatusesPanel({ statuses, onChange, labels }: StatusesPanelProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { FIELD_STYLE } from './field-style.js';
|
|
3
|
+
import { DEFAULT_DESIGNER_LABELS } from '../labels.js';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { GROUP_STYLE } from '@iyulab/u-board/toolkit';
|
|
6
|
+
import { EMPHASES, TONES } from '@iyulab/u-board/toolkit';
|
|
7
|
+
/** Whether a new status may take `name`: any word a field's state may give, but not an empty one or one taken. */
|
|
8
|
+
const usable = (name, taken) => name.trim() !== '' && !Object.hasOwn(taken, name.trim());
|
|
9
|
+
/**
|
|
10
|
+
* The board's statuses — each name a field's state gives, with the tone it is drawn in, its words, its mark and a second
|
|
11
|
+
* channel besides colour. A status is added by name and removed; its name is not changed here, since the states of
|
|
12
|
+
* the board's fields name it.
|
|
13
|
+
*/
|
|
14
|
+
export function StatusesPanel({ statuses = {}, onChange, labels = DEFAULT_DESIGNER_LABELS }) {
|
|
15
|
+
const [name, setName] = useState('');
|
|
16
|
+
const write = (next) => onChange(Object.keys(next).length > 0 ? next : undefined);
|
|
17
|
+
const set = (key, change) => {
|
|
18
|
+
const status = { ...statuses[key], ...change };
|
|
19
|
+
for (const field of ['label', 'glyph', 'emphasis'])
|
|
20
|
+
if (status[field] === undefined || status[field] === '')
|
|
21
|
+
delete status[field];
|
|
22
|
+
write({ ...statuses, [key]: status });
|
|
23
|
+
};
|
|
24
|
+
const remove = (key) => {
|
|
25
|
+
const { [key]: _gone, ...rest } = statuses;
|
|
26
|
+
write(rest);
|
|
27
|
+
};
|
|
28
|
+
return (_jsxs("div", { className: "ub-panel ub-panel--statuses", children: [_jsx("h2", { className: "ub-panel__heading", children: labels.statusesHeading }), Object.entries(statuses).map(([key, status]) => (_jsxs("fieldset", { className: "ub-panel__group", style: { minWidth: 0 }, children: [_jsx("legend", { children: _jsx("code", { children: key }) }), _jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.statusTone, _jsx("select", { value: status.tone, onChange: e => set(key, { tone: e.target.value }), children: TONES.map(tone => (_jsx("option", { value: tone, children: tone }, tone))) })] }), _jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.statusLabel, _jsx("input", { type: "text", value: status.label ?? '', onChange: e => set(key, { label: e.target.value }) })] }), _jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.statusGlyph, _jsx("input", { type: "text", value: status.glyph ?? '', onChange: e => set(key, { glyph: e.target.value }) })] }), _jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.statusEmphasis, _jsxs("select", { value: status.emphasis ?? '', onChange: e => set(key, { emphasis: (e.target.value || undefined) }), children: [_jsx("option", { value: "", children: labels.statusEmphasisNone }), EMPHASES.map(emphasis => (_jsx("option", { value: emphasis, children: emphasis }, emphasis)))] })] }), _jsx("button", { type: "button", className: "ub-action", onClick: () => remove(key), children: labels.statusRemove.replace('{name}', key) })] }, key))), _jsxs("div", { className: "ub-panel__actions", style: GROUP_STYLE, children: [_jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.statusNew, _jsx("input", { type: "text", value: name, onChange: e => setName(e.target.value) })] }), _jsx("button", { type: "button", className: "ub-action", disabled: !usable(name, statuses), onClick: () => {
|
|
29
|
+
write({ ...statuses, [name.trim()]: { tone: 'info' } });
|
|
30
|
+
setName('');
|
|
31
|
+
}, children: labels.statusAdd })] })] }));
|
|
32
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type DesignerLabels } from '../labels.js';
|
|
2
|
+
import { type Trigger, type ViewDocument } from '@iyulab/u-board/picture';
|
|
3
|
+
export interface TriggersEditorProps {
|
|
4
|
+
/** The part whose triggers are edited — or none, for the board's own: those that run when it opens. */
|
|
5
|
+
node?: string;
|
|
6
|
+
/** The board's whole list; the editor shows the ones on `node` (or the `open` ones) and changes them by index. */
|
|
7
|
+
triggers: readonly Trigger[] | undefined;
|
|
8
|
+
/** The board's parts, by id — what a trigger may wait on or bring into view. */
|
|
9
|
+
parts: readonly string[];
|
|
10
|
+
/** The state names a part's states give — offered for «enters», «leaves» and «only when». */
|
|
11
|
+
stateNamesOf: (part: string) => readonly string[];
|
|
12
|
+
/** How a part is named to the author; its id when left out. */
|
|
13
|
+
nameOf?: (part: string) => string;
|
|
14
|
+
/** The `each` part an item node belongs to — a trigger may then be for every item of it. */
|
|
15
|
+
eachOf?: (node: string) => string | undefined;
|
|
16
|
+
onAdd: (trigger: Trigger) => void;
|
|
17
|
+
onReplace: (index: number, trigger: Trigger) => void;
|
|
18
|
+
onRemove: (index: number) => void;
|
|
19
|
+
labels: DesignerLabels;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A board's triggers, edited where an author thinks of them: on a part — clicked, an event of its element, a state
|
|
23
|
+
* entered or left — or, without a part, when the board opens. Each trigger is written as soon as it is complete and
|
|
24
|
+
* one a board may hold; until then the form says what is missing, and the document keeps what it had.
|
|
25
|
+
*/
|
|
26
|
+
export declare function TriggersEditor({ node, triggers, parts, stateNamesOf, nameOf, eachOf, onAdd, onReplace, onRemove, labels }: TriggersEditorProps): import("react").JSX.Element;
|
|
27
|
+
/** What a panel needs to edit `doc`'s triggers — its list, its parts and their states, and edits through `updateDoc`. */
|
|
28
|
+
export type TriggerEditing = Omit<TriggersEditorProps, 'node' | 'labels'>;
|
|
29
|
+
/** The parts a trigger may name are the parts the board places. */
|
|
30
|
+
export declare function triggerEditing<D extends ViewDocument>(doc: D, updateDoc: (update: (current: D) => D) => void, locale?: string): TriggerEditing;
|