@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,137 @@
|
|
|
1
|
+
import { 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 { isTriggerLink, placedIn } from '@iyulab/u-board/picture';
|
|
5
|
+
import { partNamer } from './part-names.js';
|
|
6
|
+
import { stateNames } from './StatesEditor.js';
|
|
7
|
+
import { removeTrigger, replaceTrigger, withTrigger } from '../editing/trigger-editing.js';
|
|
8
|
+
import { ERROR_STYLE, GROUP_STYLE } from '@iyulab/u-board/toolkit';
|
|
9
|
+
const ACTION_FIELD = { 'open-link': 'url', focus: 'node', emit: 'name', 'go-to-board': 'board' };
|
|
10
|
+
function draftOf(trigger, group) {
|
|
11
|
+
const on = trigger.on;
|
|
12
|
+
return {
|
|
13
|
+
...(group !== undefined && 'node' in on && on.node === group ? { every: true } : {}),
|
|
14
|
+
on: { event: on.event, name: on.event === 'element' ? on.name : '', state: on.event === 'enter' || on.event === 'leave' ? on.state : '' },
|
|
15
|
+
...(trigger.when && { when: { node: trigger.when.node, state: [].concat(trigger.when.state).join(', ') } }),
|
|
16
|
+
do: trigger.do.map(action => ({ action: action.action, value: action[ACTION_FIELD[action.action]] })),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** What is wrong with a draft — `incomplete` (a field left empty or too long), `link` (a link a board may not open) —
|
|
20
|
+
* or the trigger it describes. The same rules saving checks, so the editor never writes what would be refused. */
|
|
21
|
+
function triggerOf(draft, node) {
|
|
22
|
+
const name = (text) => text.length >= 1 && text.length <= 64;
|
|
23
|
+
const { event, name: eventName, state } = draft.on;
|
|
24
|
+
let on;
|
|
25
|
+
if (event === 'open' || node === undefined)
|
|
26
|
+
on = { event: 'open' };
|
|
27
|
+
else if (event === 'click')
|
|
28
|
+
on = { event, node };
|
|
29
|
+
else if (event === 'element') {
|
|
30
|
+
if (!name(eventName.trim()))
|
|
31
|
+
return { problem: 'incomplete' };
|
|
32
|
+
on = { event, node, name: eventName.trim() };
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
if (state.trim() === '')
|
|
36
|
+
return { problem: 'incomplete' };
|
|
37
|
+
on = { event, node, state: state.trim() };
|
|
38
|
+
}
|
|
39
|
+
let when;
|
|
40
|
+
if (draft.when) {
|
|
41
|
+
const states = draft.when.state.split(',').map(s => s.trim()).filter(Boolean);
|
|
42
|
+
if (draft.when.node === '' || states.length === 0)
|
|
43
|
+
return { problem: 'incomplete' };
|
|
44
|
+
when = { node: draft.when.node, state: states.length === 1 ? states[0] : states };
|
|
45
|
+
}
|
|
46
|
+
if (draft.do.length === 0)
|
|
47
|
+
return { problem: 'incomplete' };
|
|
48
|
+
const actions = [];
|
|
49
|
+
for (const { action, value } of draft.do) {
|
|
50
|
+
const text = value.trim();
|
|
51
|
+
if (text === '')
|
|
52
|
+
return { problem: 'incomplete' };
|
|
53
|
+
if (action === 'open-link') {
|
|
54
|
+
if (!isTriggerLink(text))
|
|
55
|
+
return { problem: 'link' };
|
|
56
|
+
actions.push({ action, url: text });
|
|
57
|
+
}
|
|
58
|
+
else if (action === 'emit') {
|
|
59
|
+
if (!name(text))
|
|
60
|
+
return { problem: 'incomplete' };
|
|
61
|
+
actions.push({ action, name: text });
|
|
62
|
+
}
|
|
63
|
+
else if (action === 'focus')
|
|
64
|
+
actions.push({ action, node: text });
|
|
65
|
+
else {
|
|
66
|
+
if (text.length > 100)
|
|
67
|
+
return { problem: 'incomplete' };
|
|
68
|
+
actions.push({ action, board: text });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { trigger: { on, ...(when && { when }), do: actions } };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A board's triggers, edited where an author thinks of them: on a part — clicked, an event of its element, a state
|
|
75
|
+
* entered or left — or, without a part, when the board opens. Each trigger is written as soon as it is complete and
|
|
76
|
+
* one a board may hold; until then the form says what is missing, and the document keeps what it had.
|
|
77
|
+
*/
|
|
78
|
+
export function TriggersEditor({ node, triggers, parts, stateNamesOf, nameOf, eachOf, onAdd, onReplace, onRemove, labels }) {
|
|
79
|
+
const group = node === undefined ? undefined : eachOf?.(node);
|
|
80
|
+
const shown = (triggers ?? []).flatMap((trigger, index) => node === undefined
|
|
81
|
+
? (trigger.on.event === 'open' ? [{ trigger, index }] : [])
|
|
82
|
+
: 'node' in trigger.on && (trigger.on.node === node || (group !== undefined && trigger.on.node === group)) ? [{ trigger, index }] : []);
|
|
83
|
+
const [adding, setAdding] = useState(false);
|
|
84
|
+
const fresh = { on: { event: node === undefined ? 'open' : 'click', name: '', state: '' }, do: [{ action: 'emit', value: '' }] };
|
|
85
|
+
return (_jsxs("section", { className: "ub-panel__section", children: [_jsx("h3", { className: "ub-panel__subheading", children: node === undefined ? labels.triggersOnOpenHeading : labels.triggersHeading }), shown.map(({ trigger, index }, n) => (_jsx(TriggerRow, { n: n + 1, current: trigger, fresh: fresh, node, group, parts, stateNamesOf, nameOf, labels, onWrite: next => onReplace(index, next), onRemove: () => onRemove(index) }, `row-${n}`))), adding && (_jsx(TriggerRow, { n: shown.length + 1, current: undefined, fresh: fresh, node, group, parts, stateNamesOf, nameOf, labels, onWrite: next => {
|
|
86
|
+
onAdd(next);
|
|
87
|
+
setAdding(false);
|
|
88
|
+
}, onRemove: () => setAdding(false) }, `row-${shown.length}`)), !adding && (_jsx("button", { type: "button", className: "ub-action", onClick: () => setAdding(true), children: labels.addTrigger }))] }));
|
|
89
|
+
}
|
|
90
|
+
function TriggerRow({ n, current, fresh, node, group, parts, stateNamesOf, nameOf, labels, onWrite, onRemove, }) {
|
|
91
|
+
const [draft, setDraft] = useState(() => (current ? draftOf(current, group) : fresh));
|
|
92
|
+
// A trigger arriving that this row did not write (Undo, a row removed above it) replaces the draft.
|
|
93
|
+
const [seen, setSeen] = useState(current);
|
|
94
|
+
const [written, setWritten] = useState(current);
|
|
95
|
+
if (current !== seen) {
|
|
96
|
+
setSeen(current);
|
|
97
|
+
if (current !== written)
|
|
98
|
+
setDraft(current ? draftOf(current, group) : fresh);
|
|
99
|
+
}
|
|
100
|
+
const statesId = useId();
|
|
101
|
+
/** The node the trigger is about: the one being edited, or — for every item — the part they belong to. */
|
|
102
|
+
const target = (d) => (d.every && group !== undefined ? group : node);
|
|
103
|
+
const result = triggerOf(draft, target(draft));
|
|
104
|
+
const change = (next) => {
|
|
105
|
+
setDraft(next);
|
|
106
|
+
const made = triggerOf(next, target(next));
|
|
107
|
+
if ('trigger' in made) {
|
|
108
|
+
setWritten(made.trigger);
|
|
109
|
+
onWrite(made.trigger);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const label = (text) => text.replace('{n}', String(n));
|
|
113
|
+
const ownStates = node === undefined ? [] : stateNamesOf(node);
|
|
114
|
+
return (_jsxs("fieldset", { className: "ub-panel__trigger", style: { minWidth: 0 }, children: [_jsx("legend", { children: label(labels.triggerHeading) }), node !== undefined && (_jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.triggerEvent, _jsxs("select", { value: draft.on.event, onChange: e => change({ ...draft, on: { ...draft.on, event: e.target.value } }), children: [_jsx("option", { value: "click", children: labels.triggerOnClick }), _jsx("option", { value: "element", children: labels.triggerOnElement }), _jsx("option", { value: "enter", children: labels.triggerOnEnter }), _jsx("option", { value: "leave", children: labels.triggerOnLeave })] })] })), group !== undefined && (_jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: !!draft.every, onChange: e => change({ ...draft, every: e.target.checked }) }), labels.triggerEveryItem.replace('{part}', group)] })), draft.on.event === 'element' && (_jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.triggerEventName, _jsx("input", { value: draft.on.name, onChange: e => change({ ...draft, on: { ...draft.on, name: e.target.value } }), placeholder: "open-valve" })] })), (draft.on.event === 'enter' || draft.on.event === 'leave') && (_jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.triggerState, _jsx("input", { list: `${statesId}-own`, value: draft.on.state, onChange: e => change({ ...draft, on: { ...draft.on, state: e.target.value } }), placeholder: "fault" })] })), (draft.on.event === 'enter' || draft.on.event === 'leave') && (_jsx("datalist", { id: `${statesId}-own`, children: ownStates.map(s => (_jsx("option", { value: s, children: s }, s))) })), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: !!draft.when, onChange: e => change({ ...draft, when: e.target.checked ? { node: node ?? parts[0] ?? '', state: '' } : undefined }) }), labels.triggerOnlyWhen] }), draft.when && (_jsxs("div", { style: GROUP_STYLE, children: [_jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.triggerWhenPart, _jsx("select", { value: draft.when.node, onChange: e => change({ ...draft, when: { ...draft.when, node: e.target.value } }), children: parts.map(p => (_jsx("option", { value: p, children: optionName(p, nameOf) }, p))) })] }), _jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.triggerWhenState, _jsx("input", { list: `${statesId}-when`, value: draft.when.state, onChange: e => change({ ...draft, when: { ...draft.when, state: e.target.value } }), placeholder: "fault, hot" })] }), _jsx("datalist", { id: `${statesId}-when`, children: stateNamesOf(draft.when.node).map(s => (_jsx("option", { value: s, children: s }, s))) })] })), draft.do.map((action, i) => {
|
|
115
|
+
const setAction = (next) => change({ ...draft, do: draft.do.map((a, j) => (j === i ? next : a)) });
|
|
116
|
+
const k = String(i + 1);
|
|
117
|
+
return (_jsxs("div", { className: "ub-panel__trigger-action", style: GROUP_STYLE, children: [_jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.triggerAction.replace('{n}', k), _jsxs("select", { value: action.action, onChange: e => setAction({ action: e.target.value, value: '' }), children: [_jsx("option", { value: "open-link", children: labels.triggerOpenLink }), _jsx("option", { value: "focus", children: labels.triggerFocus }), _jsx("option", { value: "emit", children: labels.triggerEmit }), _jsx("option", { value: "go-to-board", children: labels.triggerGoToBoard })] })] }), action.action === 'focus' ? (_jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.triggerFocusPart.replace('{n}', k), _jsxs("select", { value: action.value, onChange: e => setAction({ ...action, value: e.target.value }), children: [_jsx("option", { value: "", children: "\u2014" }), parts.map(p => (_jsx("option", { value: p, children: optionName(p, nameOf) }, p)))] })] })) : (_jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [(action.action === 'open-link' ? labels.triggerLinkUrl : action.action === 'emit' ? labels.triggerEmitName : labels.triggerBoardId).replace('{n}', k), _jsx("input", { value: action.value, onChange: e => setAction({ ...action, value: e.target.value }), placeholder: action.action === 'open-link' ? 'https://example.com/asset/{{node}}' : action.action === 'emit' ? 'details' : '' })] })), _jsx("button", { type: "button", className: "ub-action ub-action--icon", "aria-label": labels.removeTriggerAction.replace('{n}', k), onClick: () => change({ ...draft, do: draft.do.filter((_, j) => j !== i) }), children: "\u00D7" })] }, i));
|
|
118
|
+
}), _jsxs("div", { style: GROUP_STYLE, children: [_jsx("button", { type: "button", className: "ub-action", onClick: () => change({ ...draft, do: [...draft.do, { action: 'emit', value: '' }] }), children: labels.addTriggerAction }), _jsx("button", { type: "button", className: "ub-action", onClick: onRemove, children: label(labels.removeTrigger) })] }), 'problem' in result && (_jsx("p", { className: "ub-panel__hint", style: result.problem === 'link' ? ERROR_STYLE : undefined, children: result.problem === 'link' ? labels.triggerLinkRefused : labels.triggerIncomplete }))] }));
|
|
119
|
+
}
|
|
120
|
+
/** A part as a choice reads: its name and, when that is not its id, the id a trigger keeps. */
|
|
121
|
+
function optionName(part, nameOf) {
|
|
122
|
+
const name = nameOf?.(part) ?? part;
|
|
123
|
+
return name === part ? part : `${name} (${part})`;
|
|
124
|
+
}
|
|
125
|
+
/** The parts a trigger may name are the parts the board places. */
|
|
126
|
+
export function triggerEditing(doc, updateDoc, locale) {
|
|
127
|
+
const placed = placedIn(doc);
|
|
128
|
+
return {
|
|
129
|
+
triggers: doc.triggers,
|
|
130
|
+
parts: placed.map(part => part.id),
|
|
131
|
+
stateNamesOf: id => stateNames(placed.find(part => part.id === id)?.states?.map),
|
|
132
|
+
...(doc.kind === 'canvas' && { nameOf: partNamer(doc, locale) }),
|
|
133
|
+
onAdd: trigger => updateDoc(current => withTrigger(current, trigger)),
|
|
134
|
+
onReplace: (index, trigger) => updateDoc(current => replaceTrigger(current, index, trigger)),
|
|
135
|
+
onRemove: index => updateDoc(current => removeTrigger(current, index)),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type DesignerLabels } from '../labels.js';
|
|
2
|
+
import type { ValueMap } from '@iyulab/u-board/picture';
|
|
3
|
+
/** A value map as a form edits it — rows of source value → mapped value, numeric bands, and the value for anything
|
|
4
|
+
* else (empty: none). Written as text: a map's usual job is a status word, a level or a state's name. */
|
|
5
|
+
export interface MapDraft {
|
|
6
|
+
mappings: MappingRow[];
|
|
7
|
+
/** Numeric bands → mapped value (`ValueMap.ranges`), tried after `mappings`. */
|
|
8
|
+
ranges: RangeRow[];
|
|
9
|
+
otherwise: string;
|
|
10
|
+
/** The map's `otherwise` as loaded, kept as it was unless its text is edited (see `MappingRow.loaded`). */
|
|
11
|
+
otherwiseLoaded?: {
|
|
12
|
+
value: unknown;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** A row of the value map as the form edits it. `loaded` is the shown value the row was loaded with:
|
|
16
|
+
* while its text is left as shown, that value is saved back as it was — a number stays a number —
|
|
17
|
+
* rather than as the text the form displays it with. */
|
|
18
|
+
export interface MappingRow {
|
|
19
|
+
from: string;
|
|
20
|
+
to: string;
|
|
21
|
+
loaded?: {
|
|
22
|
+
value: unknown;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** A numeric range row as the form edits it: its bounds as typed (empty: that end is open) and
|
|
26
|
+
* its shown value, kept as loaded like `MappingRow.to`. */
|
|
27
|
+
export interface RangeRow {
|
|
28
|
+
min: string;
|
|
29
|
+
max: string;
|
|
30
|
+
to: string;
|
|
31
|
+
loaded?: {
|
|
32
|
+
value: unknown;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** The form's text for a mapped value — itself when it is text, its JSON otherwise. */
|
|
36
|
+
export declare const asText: (value: unknown) => string;
|
|
37
|
+
export declare function draftMap(map: ValueMap | undefined): MapDraft;
|
|
38
|
+
/** The rows (1-based) whose range is not a range — a shown value with neither end, an upper end not
|
|
39
|
+
* above the lower one, or a bound that is not a number. Such a binding is not saved: a row with no
|
|
40
|
+
* end would be dropped silently, the others refused with the document. */
|
|
41
|
+
export declare function invalidRanges(draft: MapDraft): number[];
|
|
42
|
+
/** The `Binding.map` the form describes, or `undefined` when it describes none. A row without a
|
|
43
|
+
* source value, and a range row with neither end, is left out. */
|
|
44
|
+
export declare function mapFromDraft(draft: MapDraft): ValueMap | undefined;
|
|
45
|
+
export interface ValueMapFieldsProps {
|
|
46
|
+
draft: MapDraft;
|
|
47
|
+
/** Applies a change to the map's part of the draft. */
|
|
48
|
+
onChange: (update: (draft: MapDraft) => MapDraft) => void;
|
|
49
|
+
labels: DesignerLabels;
|
|
50
|
+
/** The heading — `labels.valueMapHeading` by default. */
|
|
51
|
+
heading?: string;
|
|
52
|
+
/** What a row maps to, as its field is named — `labels.mapTo` and `labels.rangeTo` by default ({n} is the row). */
|
|
53
|
+
toLabel?: {
|
|
54
|
+
mapping: string;
|
|
55
|
+
range: string;
|
|
56
|
+
};
|
|
57
|
+
/** Examples shown in a row's two sides while they are empty. */
|
|
58
|
+
placeholders?: {
|
|
59
|
+
from: string;
|
|
60
|
+
to: string;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** The rows of a value map — source value → mapped value, numeric bands, the value for anything else — as one form. */
|
|
64
|
+
export declare function ValueMapFields({ draft, onChange: setDraft, labels, heading, toLabel, placeholders, }: ValueMapFieldsProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { FIELD_STYLE } from './field-style.js';
|
|
3
|
+
import { ERROR_STYLE } from '@iyulab/u-board/toolkit';
|
|
4
|
+
/** The value a form field stands for: what it was loaded with if its text was not edited, else its text. */
|
|
5
|
+
function fieldValue(text, loaded) {
|
|
6
|
+
return loaded && text === asText(loaded.value) ? loaded.value : text;
|
|
7
|
+
}
|
|
8
|
+
/** A row of the value map: its inputs share the line. */
|
|
9
|
+
const ROW_STYLE = { display: 'flex', gap: 'var(--ub-space-1, 4px)', alignItems: 'center' };
|
|
10
|
+
const ROW_INPUT_STYLE = { flex: 1, minWidth: 0 };
|
|
11
|
+
/** The form's text for a mapped value — itself when it is text, its JSON otherwise. */
|
|
12
|
+
export const asText = (value) => (typeof value === 'string' ? value : JSON.stringify(value));
|
|
13
|
+
const boundText = (bound) => (bound === undefined ? '' : String(bound));
|
|
14
|
+
export function draftMap(map) {
|
|
15
|
+
if (!map)
|
|
16
|
+
return { mappings: [], ranges: [], otherwise: '' };
|
|
17
|
+
return {
|
|
18
|
+
mappings: Object.entries(map.values ?? {}).map(([from, to]) => ({ from, to: asText(to), loaded: { value: to } })),
|
|
19
|
+
ranges: (map.ranges ?? []).map(r => ({ min: boundText(r.min), max: boundText(r.max), to: asText(r.value), loaded: { value: r.value } })),
|
|
20
|
+
otherwise: 'otherwise' in map ? asText(map.otherwise) : '',
|
|
21
|
+
...('otherwise' in map && { otherwiseLoaded: { value: map.otherwise } }),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** A range bound as typed: `undefined` when left empty (an open end), else the number it reads as. */
|
|
25
|
+
const bound = (text) => (text.trim() === '' ? undefined : Number(text));
|
|
26
|
+
/** The rows (1-based) whose range is not a range — a shown value with neither end, an upper end not
|
|
27
|
+
* above the lower one, or a bound that is not a number. Such a binding is not saved: a row with no
|
|
28
|
+
* end would be dropped silently, the others refused with the document. */
|
|
29
|
+
export function invalidRanges(draft) {
|
|
30
|
+
return draft.ranges.flatMap((row, i) => {
|
|
31
|
+
const [min, max] = [bound(row.min), bound(row.max)];
|
|
32
|
+
const broken = (min === undefined && max === undefined && row.to.trim() !== '') ||
|
|
33
|
+
[min, max].some(b => b !== undefined && !Number.isFinite(b)) ||
|
|
34
|
+
(min !== undefined && max !== undefined && !(min < max));
|
|
35
|
+
return broken ? [i + 1] : [];
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** The `Binding.map` the form describes, or `undefined` when it describes none. A row without a
|
|
39
|
+
* source value, and a range row with neither end, is left out. */
|
|
40
|
+
export function mapFromDraft(draft) {
|
|
41
|
+
const rows = draft.mappings.filter(row => row.from !== '');
|
|
42
|
+
const ranges = draft.ranges.filter(row => bound(row.min) !== undefined || bound(row.max) !== undefined);
|
|
43
|
+
if (rows.length === 0 && ranges.length === 0 && draft.otherwise === '')
|
|
44
|
+
return undefined;
|
|
45
|
+
return {
|
|
46
|
+
...(rows.length > 0 || ranges.length === 0 ? { values: Object.fromEntries(rows.map(row => [row.from, fieldValue(row.to, row.loaded)])) } : {}),
|
|
47
|
+
...(ranges.length > 0 && {
|
|
48
|
+
ranges: ranges.map(row => {
|
|
49
|
+
const [min, max] = [bound(row.min), bound(row.max)];
|
|
50
|
+
return { ...(min !== undefined && { min }), ...(max !== undefined && { max }), value: fieldValue(row.to, row.loaded) };
|
|
51
|
+
}),
|
|
52
|
+
}),
|
|
53
|
+
...(draft.otherwise !== '' && { otherwise: fieldValue(draft.otherwise, draft.otherwiseLoaded) }),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** The rows of a value map — source value → mapped value, numeric bands, the value for anything else — as one form. */
|
|
57
|
+
export function ValueMapFields({ draft, onChange: setDraft, labels, heading = labels.valueMapHeading, toLabel = { mapping: labels.mapTo, range: labels.rangeTo }, placeholders = { from: 'Fault', to: 'error' }, }) {
|
|
58
|
+
const rangeIssues = invalidRanges(draft);
|
|
59
|
+
return (_jsxs("fieldset", { className: "ub-panel__value-map", style: { minWidth: 0 }, children: [_jsx("legend", { children: heading }), draft.mappings.map((row, i) => {
|
|
60
|
+
const n = String(i + 1);
|
|
61
|
+
const setRow = (change) => setDraft(d => ({ ...d, mappings: d.mappings.map((r, j) => (j === i ? { ...r, ...change } : r)) }));
|
|
62
|
+
return (_jsxs("div", { className: "ub-panel__map-row", style: ROW_STYLE, children: [_jsx("input", { style: ROW_INPUT_STYLE, "aria-label": labels.mapFrom.replace('{n}', n), value: row.from, onChange: e => setRow({ from: e.target.value }), placeholder: placeholders.from }), _jsx("span", { "aria-hidden": "true", children: "\u2192" }), _jsx("input", { style: ROW_INPUT_STYLE, "aria-label": toLabel.mapping.replace('{n}', n), value: row.to, onChange: e => setRow({ to: e.target.value }), placeholder: placeholders.to }), _jsx("button", { type: "button", className: "ub-action ub-action--icon", "aria-label": labels.removeMapping.replace('{n}', n), onClick: () => setDraft(d => ({ ...d, mappings: d.mappings.filter((_, j) => j !== i) })), children: "\u00D7" })] }, i));
|
|
63
|
+
}), _jsx("button", { type: "button", className: "ub-action", onClick: () => setDraft(d => ({ ...d, mappings: [...d.mappings, { from: '', to: '' }] })), children: labels.addMapping }), draft.ranges.map((row, i) => {
|
|
64
|
+
const n = String(i + 1);
|
|
65
|
+
const setRow = (change) => setDraft(d => ({ ...d, ranges: d.ranges.map((r, j) => (j === i ? { ...r, ...change } : r)) }));
|
|
66
|
+
return (_jsxs("div", { className: "ub-panel__map-row", style: ROW_STYLE, children: [_jsx("input", { type: "number", style: ROW_INPUT_STYLE, "aria-label": labels.rangeMin.replace('{n}', n), value: row.min, onChange: e => setRow({ min: e.target.value }), placeholder: "70" }), _jsx("span", { "aria-hidden": "true", children: "\u2264 x <" }), _jsx("input", { type: "number", style: ROW_INPUT_STYLE, "aria-label": labels.rangeMax.replace('{n}', n), value: row.max, onChange: e => setRow({ max: e.target.value }), placeholder: "80" }), _jsx("span", { "aria-hidden": "true", children: "\u2192" }), _jsx("input", { style: ROW_INPUT_STYLE, "aria-label": toLabel.range.replace('{n}', n), value: row.to, onChange: e => setRow({ to: e.target.value }), placeholder: "warning" }), _jsx("button", { type: "button", className: "ub-action ub-action--icon", "aria-label": labels.removeRange.replace('{n}', n), onClick: () => setDraft(d => ({ ...d, ranges: d.ranges.filter((_, j) => j !== i) })), children: "\u00D7" })] }, i));
|
|
67
|
+
}), _jsx("button", { type: "button", className: "ub-action", onClick: () => setDraft(d => ({ ...d, ranges: [...d.ranges, { min: '', max: '', to: '' }] })), children: labels.addRange }), rangeIssues.map(n => (_jsx("p", { role: "alert", className: "ub-panel__error", style: ERROR_STYLE, children: labels.rangeInvalid.replace('{n}', String(n)) }, n))), _jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.mapOtherwise, _jsx("input", { value: draft.otherwise, onChange: e => setDraft(d => ({ ...d, otherwise: e.target.value })), placeholder: labels.mapOtherwisePlaceholder })] })] }));
|
|
68
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type DesignerLabels } from '../labels.js';
|
|
2
|
+
import type { ElementDefinition, Widget } from '@iyulab/u-board/picture';
|
|
3
|
+
import { type WidgetField } from './widget-fields.js';
|
|
4
|
+
export interface WidgetPropsFormProps {
|
|
5
|
+
widget: Widget;
|
|
6
|
+
onChange: (widget: Widget) => void;
|
|
7
|
+
labels?: DesignerLabels;
|
|
8
|
+
/** The board's elements, which a placed element's fields are read from. */
|
|
9
|
+
elements?: Readonly<Record<string, ElementDefinition>>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A control for each value of the widget the widget library describes — its data fields, and the
|
|
13
|
+
* options whose type is known — or, for an element, each prop its definition's schema lists
|
|
14
|
+
* (`widgetFields`). A typed value is written when the control is left (or a choice is made), not on
|
|
15
|
+
* every keystroke: each change re-resolves the preview's bindings.
|
|
16
|
+
*/
|
|
17
|
+
export declare function WidgetPropsForm({ widget, onChange, labels, elements }: WidgetPropsFormProps): import("react").JSX.Element | null;
|
|
18
|
+
/** The control for one field: `value` is its value now, `write` sets it (`undefined` takes it out), `bound` marks a value
|
|
19
|
+
* a binding fills. A typed value is written when the control is left (or a choice is made), not on every keystroke. */
|
|
20
|
+
/** The control of one field: a text, a number, a check box or a choice, named as the widget library names it. */
|
|
21
|
+
export declare function FieldControl({ field, value, write, bound, labels }: {
|
|
22
|
+
field: WidgetField;
|
|
23
|
+
value: unknown;
|
|
24
|
+
write: (value: unknown) => void;
|
|
25
|
+
bound?: boolean;
|
|
26
|
+
labels: DesignerLabels;
|
|
27
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { DEFAULT_DESIGNER_LABELS } from '../labels.js';
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { readField, widgetFields, withField } from './widget-fields.js';
|
|
5
|
+
const FIELD_STYLE = { display: 'flex', flexDirection: 'column' };
|
|
6
|
+
const CHECK_STYLE = { display: 'flex', alignItems: 'center', gap: 'var(--ub-space-2, 8px)' };
|
|
7
|
+
const text = (value) => (value === undefined || value === null ? '' : typeof value === 'string' ? value : JSON.stringify(value));
|
|
8
|
+
/**
|
|
9
|
+
* A control for each value of the widget the widget library describes — its data fields, and the
|
|
10
|
+
* options whose type is known — or, for an element, each prop its definition's schema lists
|
|
11
|
+
* (`widgetFields`). A typed value is written when the control is left (or a choice is made), not on
|
|
12
|
+
* every keystroke: each change re-resolves the preview's bindings.
|
|
13
|
+
*/
|
|
14
|
+
export function WidgetPropsForm({ widget, onChange, labels = DEFAULT_DESIGNER_LABELS, elements }) {
|
|
15
|
+
const { fields, otherOptions } = useMemo(() => widgetFields(widget, labels.locale, elements), [widget, labels.locale, elements]);
|
|
16
|
+
if (fields.length === 0 && otherOptions === 0)
|
|
17
|
+
return null;
|
|
18
|
+
const set = (field, value) => {
|
|
19
|
+
if (value === readField(widget, field))
|
|
20
|
+
return;
|
|
21
|
+
onChange(withField(widget, field, value));
|
|
22
|
+
};
|
|
23
|
+
const control = (field) => (_jsx(FieldControl, { field: field, value: readField(widget, field), write: value => set(field, value), bound: field.section !== 'options' && widget.bindings?.[field.path.join('.')] !== undefined, labels: labels }, field.path.join('.')));
|
|
24
|
+
const data = fields.filter(f => f.section === 'data');
|
|
25
|
+
const options = fields.filter(f => f.section === 'options');
|
|
26
|
+
const element = fields.filter(f => f.section === 'element');
|
|
27
|
+
return (_jsxs("div", { className: "ub-panel__widget-form", children: [element.length > 0 && (_jsxs("fieldset", { className: "ub-panel__group", style: { minWidth: 0 }, children: [_jsx("legend", { children: labels.elementProps }), element.map(control)] })), data.length > 0 && (_jsxs("fieldset", { className: "ub-panel__group", style: { minWidth: 0 }, children: [_jsx("legend", { children: labels.widgetData }), data.map(control)] })), options.length > 0 && (_jsxs("fieldset", { className: "ub-panel__group", style: { minWidth: 0 }, children: [_jsx("legend", { children: labels.widgetOptions }), options.map(control)] })), otherOptions > 0 && _jsx("p", { className: "ub-panel__hint", children: labels.moreInAdvanced.replace('{count}', String(otherOptions)) })] }));
|
|
28
|
+
}
|
|
29
|
+
/** The control for one field: `value` is its value now, `write` sets it (`undefined` takes it out), `bound` marks a value
|
|
30
|
+
* a binding fills. A typed value is written when the control is left (or a choice is made), not on every keystroke. */
|
|
31
|
+
/** The control of one field: a text, a number, a check box or a choice, named as the widget library names it. */
|
|
32
|
+
export function FieldControl({ field, value, write, bound = false, labels }) {
|
|
33
|
+
const name = (_jsxs("span", { className: "ub-panel__field-name", title: field.description, children: [field.label, " ", _jsx("code", { children: field.key }), bound && _jsxs("span", { className: "ub-panel__binding-tag", children: [" \u00B7 ", labels.boundField] })] }));
|
|
34
|
+
const id = field.path.join('.');
|
|
35
|
+
switch (field.kind) {
|
|
36
|
+
case 'boolean':
|
|
37
|
+
return (_jsxs("label", { className: "ub-panel__field ub-panel__field--check", style: CHECK_STYLE, children: [_jsx("input", { type: "checkbox", checked: value === undefined ? field.defaultValue === true : value === true, onChange: e => write(e.target.checked) }), name] }, id));
|
|
38
|
+
case 'choice':
|
|
39
|
+
return (_jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [name, _jsxs("select", { value: typeof value === 'string' ? value : '', onChange: e => write(e.target.value === '' ? undefined : e.target.value), children: [_jsx("option", { value: "", children: field.defaultValue === undefined ? labels.choiceDefault : `${labels.choiceDefault} (${text(field.defaultValue)})` }), field.choices.map(choice => (_jsx("option", { value: choice, children: choice }, choice)))] })] }, id));
|
|
40
|
+
case 'number':
|
|
41
|
+
return (_jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [name, _jsx("input", { type: "number", min: field.min, max: field.max, step: field.integer ? 1 : undefined, defaultValue: typeof value === 'number' ? value : '',
|
|
42
|
+
// Left empty, the widget's own default applies — shown so the empty field does not read as nothing.
|
|
43
|
+
placeholder: field.defaultValue === undefined ? undefined : text(field.defaultValue), onBlur: e => {
|
|
44
|
+
const raw = e.currentTarget.value.trim();
|
|
45
|
+
// Cleared: the widget's own default applies again. A whole-number field keeps whole numbers.
|
|
46
|
+
const number = raw === '' || !Number.isFinite(Number(raw)) ? undefined : Number(raw);
|
|
47
|
+
write(number !== undefined && field.integer ? Math.round(number) : number);
|
|
48
|
+
} }, `${id}=${text(value)}`)] }, id));
|
|
49
|
+
case 'text':
|
|
50
|
+
return (_jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [name, _jsx("input", { defaultValue: text(value), placeholder: field.defaultValue === undefined ? undefined : text(field.defaultValue), onBlur: e => {
|
|
51
|
+
// Left as shown: nothing to write (an absent value stays absent).
|
|
52
|
+
if (e.currentTarget.value !== text(value))
|
|
53
|
+
write(e.currentTarget.value);
|
|
54
|
+
} }, `${id}=${text(value)}`)] }, id));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { BackgroundImage } from '@iyulab/u-board/picture';
|
|
2
|
+
/** The image types a background may be — what every browser draws. */
|
|
3
|
+
export declare const BACKGROUND_IMAGE_TYPES: string[];
|
|
4
|
+
/** The largest background file accepted. The image is kept inside the document — a board needs no
|
|
5
|
+
* other store, opens the same on a closed network, and is in every backup and exported file — so
|
|
6
|
+
* it is bounded: every save and every open of the board carries it. */
|
|
7
|
+
export declare const MAX_BACKGROUND_BYTES: number;
|
|
8
|
+
export type BackgroundImageProblem = 'type' | 'size' | 'unreadable';
|
|
9
|
+
export declare class BackgroundImageError extends Error {
|
|
10
|
+
readonly problem: BackgroundImageProblem;
|
|
11
|
+
constructor(problem: BackgroundImageProblem);
|
|
12
|
+
}
|
|
13
|
+
/** Reads a chosen file into a background: the image as a `data:` URL, and its own size in pixels —
|
|
14
|
+
* the scene units the board is laid out in. Refuses another type, a file over the limit, and an
|
|
15
|
+
* image the browser cannot draw or that has no size of its own. */
|
|
16
|
+
export declare function readBackgroundImage(file: File, measure?: (src: string) => Promise<{
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
}>): Promise<BackgroundImage>;
|
|
20
|
+
/** An SVG drawing's size from its root element: `width`/`height` in plain numbers or pixels, else
|
|
21
|
+
* its `viewBox`. Zero when it states neither. */
|
|
22
|
+
export declare function svgSize(text: string): {
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** The image types a background may be — what every browser draws. */
|
|
2
|
+
export const BACKGROUND_IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/webp', 'image/gif', 'image/svg+xml'];
|
|
3
|
+
/** The largest background file accepted. The image is kept inside the document — a board needs no
|
|
4
|
+
* other store, opens the same on a closed network, and is in every backup and exported file — so
|
|
5
|
+
* it is bounded: every save and every open of the board carries it. */
|
|
6
|
+
export const MAX_BACKGROUND_BYTES = 4 * 1024 * 1024;
|
|
7
|
+
export class BackgroundImageError extends Error {
|
|
8
|
+
problem;
|
|
9
|
+
constructor(problem) {
|
|
10
|
+
super(`background image refused: ${problem}`);
|
|
11
|
+
this.problem = problem;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Reads a chosen file into a background: the image as a `data:` URL, and its own size in pixels —
|
|
15
|
+
* the scene units the board is laid out in. Refuses another type, a file over the limit, and an
|
|
16
|
+
* image the browser cannot draw or that has no size of its own. */
|
|
17
|
+
export async function readBackgroundImage(file, measure = naturalSize) {
|
|
18
|
+
// Some systems give an SVG file no type; its name still says what it is.
|
|
19
|
+
const type = file.type || (/\.svg$/i.test(file.name) ? 'image/svg+xml' : '');
|
|
20
|
+
if (!BACKGROUND_IMAGE_TYPES.includes(type))
|
|
21
|
+
throw new BackgroundImageError('type');
|
|
22
|
+
if (file.size > MAX_BACKGROUND_BYTES)
|
|
23
|
+
throw new BackgroundImageError('size');
|
|
24
|
+
const src = await readAsDataUrl(type === file.type ? file : new Blob([file], { type }));
|
|
25
|
+
let { width, height } = await measure(src).catch(() => ({ width: 0, height: 0 }));
|
|
26
|
+
// An SVG sized only by its viewBox has no size the browser reports; read it from the drawing.
|
|
27
|
+
if (!(width > 0 && height > 0) && type === 'image/svg+xml')
|
|
28
|
+
({ width, height } = svgSize(await file.text()));
|
|
29
|
+
if (!(width > 0 && height > 0))
|
|
30
|
+
throw new BackgroundImageError('unreadable');
|
|
31
|
+
return { src, width, height };
|
|
32
|
+
}
|
|
33
|
+
/** An SVG drawing's size from its root element: `width`/`height` in plain numbers or pixels, else
|
|
34
|
+
* its `viewBox`. Zero when it states neither. */
|
|
35
|
+
export function svgSize(text) {
|
|
36
|
+
const root = /<svg\b[^>]*>/i.exec(text)?.[0] ?? '';
|
|
37
|
+
const attr = (name) => new RegExp(`\\s${name}\\s*=\\s*["']([^"']*)["']`, 'i').exec(root)?.[1];
|
|
38
|
+
const pixels = (value) => (value && /^\s*[\d.]+\s*(px)?\s*$/i.test(value) ? parseFloat(value) : NaN);
|
|
39
|
+
const width = pixels(attr('width'));
|
|
40
|
+
const height = pixels(attr('height'));
|
|
41
|
+
if (width > 0 && height > 0)
|
|
42
|
+
return { width, height };
|
|
43
|
+
const box = attr('viewBox')?.trim().split(/[\s,]+/).map(Number);
|
|
44
|
+
if (box?.length === 4 && box[2] > 0 && box[3] > 0)
|
|
45
|
+
return { width: box[2], height: box[3] };
|
|
46
|
+
return { width: 0, height: 0 };
|
|
47
|
+
}
|
|
48
|
+
function readAsDataUrl(file) {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const reader = new FileReader();
|
|
51
|
+
reader.onload = () => resolve(reader.result);
|
|
52
|
+
reader.onerror = () => reject(new BackgroundImageError('unreadable'));
|
|
53
|
+
reader.readAsDataURL(file);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function naturalSize(src) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const image = new Image();
|
|
59
|
+
image.onload = () => resolve({ width: image.naturalWidth, height: image.naturalHeight });
|
|
60
|
+
image.onerror = () => reject(new Error('the image could not be drawn'));
|
|
61
|
+
image.src = src;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type ViewDocument } from '@iyulab/u-board/picture';
|
|
2
|
+
import type { Board, Shape } from '@iyulab/u-board/toolkit';
|
|
3
|
+
/**
|
|
4
|
+
* Whether the editor lays this board out: a canvas, or a grid — of parts, canvases, and grids of parts and canvases in
|
|
5
|
+
* its cells (one level, as the picture draws them). A grid in a cell is laid out on its own (`cellBoard`): the picture
|
|
6
|
+
* flattens it into the outer grid's rows, which the editor does not say back item by item.
|
|
7
|
+
*/
|
|
8
|
+
export declare function layoutEditable(board: Board): boolean;
|
|
9
|
+
/** The id the picture draws a grid's canvas cell — entry `i` of its items — with; what it draws in the cell is named
|
|
10
|
+
* past `<id>:`. */
|
|
11
|
+
export declare function cellId(i: number): string;
|
|
12
|
+
/** The grid entry a canvas cell's id names, if the board has a canvas there. */
|
|
13
|
+
export declare function cellIndex(board: Board, id: string): number | undefined;
|
|
14
|
+
/** The grid entry holding a grid that the picture's item `id` is drawn from — one of its parts (or their items), or a
|
|
15
|
+
* canvas in it (`cell-<i>-<j>`) — if it is one. */
|
|
16
|
+
export declare function gridCellOf(board: Board, id: string): number | undefined;
|
|
17
|
+
/** The cell of grid entry `i` — a canvas, or a grid — as a board of its own: the board, that cell its layout. */
|
|
18
|
+
export declare function cellBoard(board: Board, i: number): Board | undefined;
|
|
19
|
+
/** The board with grid entry `i`'s cell — and its parts, triggers and all the rest — as `cell` (one `cellBoard` gave,
|
|
20
|
+
* edited) has them. */
|
|
21
|
+
export declare function withCell(board: Board, i: number, cell: Board): Board;
|
|
22
|
+
/** The id the picture draws a board's own link with — `link.<id>`. */
|
|
23
|
+
export declare const LINK = "link.";
|
|
24
|
+
/** The link of a canvas board a picture connector `id` draws, if it is one of the board's own (not a group's). */
|
|
25
|
+
export declare function linkOf(board: Board, id: string): string | undefined;
|
|
26
|
+
/** The id the picture draws a board's own shape with — `shape.<id>`. */
|
|
27
|
+
export declare const SHAPE = "shape.";
|
|
28
|
+
/** The shape of a canvas board a picture piece `id` draws, if it is one of the board's own — a box or a text (drawn as a
|
|
29
|
+
* decoration), or a line or a path (drawn as a node of its own). */
|
|
30
|
+
export declare function shapeOf(board: Board, id: string): string | undefined;
|
|
31
|
+
/** The line or path of a canvas board a picture node `id` draws, if it is one. */
|
|
32
|
+
export declare function lineOf(board: Board, id: string): string | undefined;
|
|
33
|
+
/** A line or path moved and sized as its node was, from box `from` to box `to`: each point where it stands in the box. */
|
|
34
|
+
export declare function shapeIn(shape: Shape, from: {
|
|
35
|
+
x: number;
|
|
36
|
+
y: number;
|
|
37
|
+
width: number;
|
|
38
|
+
height: number;
|
|
39
|
+
}, to: {
|
|
40
|
+
x: number;
|
|
41
|
+
y: number;
|
|
42
|
+
width: number;
|
|
43
|
+
height: number;
|
|
44
|
+
}): Shape;
|
|
45
|
+
/** The board with a new shape — a box, a text, or a line — at the canvas's top left, stepped from the shapes it has, and its
|
|
46
|
+
* id. */
|
|
47
|
+
export declare function withNewShape(board: Board, kind: 'box' | 'text' | 'line', text: string): {
|
|
48
|
+
board: Board;
|
|
49
|
+
id: string;
|
|
50
|
+
};
|
|
51
|
+
/** The board with a new link from part `from` to part `to` — drawn with an arrowhead, as a flow is — and its id. */
|
|
52
|
+
export declare function withNewLink(board: Board, from: string, to: string): {
|
|
53
|
+
board: Board;
|
|
54
|
+
id: string;
|
|
55
|
+
};
|
|
56
|
+
/** Whether the picture node or item `id` is one the board places itself: a part, or an item of an `each` part — not a
|
|
57
|
+
* part inside a group, which moves with its group. */
|
|
58
|
+
export declare function placesItself(board: Board, id: string): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* The board with its canvas layout as the picture `after` shows it, the picture having been `before`: each part (or
|
|
61
|
+
* item) that moved or was sized placed where it is now, each part deleted gone — with the links and triggers that
|
|
62
|
+
* named it. An item of an `each` part is there while its data has it, so deleting one deletes nothing.
|
|
63
|
+
*/
|
|
64
|
+
export declare function withPictureLayout(board: Board, before: ViewDocument, after: ViewDocument): Board;
|