@iyulab/u-board-designer 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +127 -2
  3. package/dist/lib/authoring/AnchorFields.d.ts +12 -0
  4. package/dist/lib/authoring/AnchorFields.js +64 -0
  5. package/dist/lib/authoring/BackgroundField.d.ts +13 -0
  6. package/dist/lib/authoring/BackgroundField.js +38 -0
  7. package/dist/lib/authoring/BoardAuthoring.d.ts +37 -0
  8. package/dist/lib/authoring/BoardAuthoring.js +288 -0
  9. package/dist/lib/authoring/BoardElementsPanel.d.ts +14 -0
  10. package/dist/lib/authoring/BoardElementsPanel.js +49 -0
  11. package/dist/lib/authoring/BoardPanel.d.ts +40 -0
  12. package/dist/lib/authoring/BoardPanel.js +22 -0
  13. package/dist/lib/authoring/CanvasAuthoring.d.ts +13 -0
  14. package/dist/lib/authoring/CanvasAuthoring.js +161 -0
  15. package/dist/lib/authoring/DataPanel.d.ts +29 -0
  16. package/dist/lib/authoring/DataPanel.js +98 -0
  17. package/dist/lib/authoring/GridAuthoring.d.ts +13 -0
  18. package/dist/lib/authoring/GridAuthoring.js +135 -0
  19. package/dist/lib/authoring/GridItemPanel.d.ts +22 -0
  20. package/dist/lib/authoring/GridItemPanel.js +21 -0
  21. package/dist/lib/authoring/LinkPanel.d.ts +19 -0
  22. package/dist/lib/authoring/LinkPanel.js +24 -0
  23. package/dist/lib/authoring/MappingEditor.d.ts +23 -0
  24. package/dist/lib/authoring/MappingEditor.js +83 -0
  25. package/dist/lib/authoring/PartPanel.d.ts +24 -0
  26. package/dist/lib/authoring/PartPanel.js +78 -0
  27. package/dist/lib/authoring/ShapePanel.d.ts +20 -0
  28. package/dist/lib/authoring/ShapePanel.js +54 -0
  29. package/dist/lib/authoring/StatesEditor.d.ts +18 -0
  30. package/dist/lib/authoring/StatesEditor.js +79 -0
  31. package/dist/lib/authoring/StatusesPanel.d.ts +14 -0
  32. package/dist/lib/authoring/StatusesPanel.js +32 -0
  33. package/dist/lib/authoring/TriggersEditor.d.ts +30 -0
  34. package/dist/lib/authoring/TriggersEditor.js +137 -0
  35. package/dist/lib/authoring/ValueMapFields.d.ts +64 -0
  36. package/dist/lib/authoring/ValueMapFields.js +68 -0
  37. package/dist/lib/authoring/WidgetPropsForm.d.ts +27 -0
  38. package/dist/lib/authoring/WidgetPropsForm.js +56 -0
  39. package/dist/lib/authoring/background-image.d.ts +25 -0
  40. package/dist/lib/authoring/background-image.js +63 -0
  41. package/dist/lib/authoring/board-layout.d.ts +64 -0
  42. package/dist/lib/authoring/board-layout.js +326 -0
  43. package/dist/lib/authoring/board-triggers.d.ts +5 -0
  44. package/dist/lib/authoring/board-triggers.js +55 -0
  45. package/dist/lib/authoring/document-history.d.ts +23 -0
  46. package/dist/lib/authoring/document-history.js +37 -0
  47. package/dist/lib/authoring/editor-parts.d.ts +46 -0
  48. package/dist/lib/authoring/editor-parts.js +6 -0
  49. package/dist/lib/authoring/field-style.d.ts +3 -0
  50. package/dist/lib/authoring/field-style.js +2 -0
  51. package/dist/lib/authoring/part-names.d.ts +6 -0
  52. package/dist/lib/authoring/part-names.js +9 -0
  53. package/dist/lib/authoring/scene-mapping.d.ts +22 -0
  54. package/dist/lib/authoring/scene-mapping.js +124 -0
  55. package/dist/lib/authoring/use-edit-keys.d.ts +13 -0
  56. package/dist/lib/authoring/use-edit-keys.js +57 -0
  57. package/dist/lib/authoring/widget-catalog.d.ts +4 -0
  58. package/dist/lib/authoring/widget-catalog.js +9 -0
  59. package/dist/lib/authoring/widget-fields.d.ts +45 -0
  60. package/dist/lib/authoring/widget-fields.js +129 -0
  61. package/dist/lib/board/check.d.ts +27 -0
  62. package/dist/lib/board/check.js +48 -0
  63. package/dist/lib/board/rulebook.d.ts +7 -0
  64. package/dist/lib/board/rulebook.js +148 -0
  65. package/dist/lib/check-entry.d.ts +3 -0
  66. package/dist/lib/check-entry.js +4 -0
  67. package/dist/lib/editing/grid-editing.d.ts +19 -0
  68. package/dist/lib/editing/grid-editing.js +68 -0
  69. package/dist/lib/editing/part-removal.d.ts +4 -0
  70. package/dist/lib/editing/part-removal.js +11 -0
  71. package/dist/lib/editing/trigger-editing.d.ts +16 -0
  72. package/dist/lib/editing/trigger-editing.js +36 -0
  73. package/dist/lib/index.d.ts +7 -0
  74. package/dist/lib/index.js +5 -0
  75. package/dist/lib/labels-ko.d.ts +4 -0
  76. package/dist/lib/labels-ko.js +270 -0
  77. package/dist/lib/labels.d.ts +368 -0
  78. package/dist/lib/labels.js +273 -0
  79. package/dist/lib/use-labels.d.ts +7 -0
  80. package/dist/lib/use-labels.js +12 -0
  81. package/package.json +75 -4
  82. package/styles.css +277 -0
@@ -0,0 +1,49 @@
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 { GROUP_STYLE } from '@iyulab/u-board/toolkit';
6
+ const PROP_TYPES = ['number', 'string', 'boolean', 'list', 'object'];
7
+ /** Whether a new element may take `name`: lowercase letters, digits and `-`, from a letter, not U-Board's, not taken. */
8
+ const usable = (name, taken) => /^[a-z][a-z0-9-]*$/.test(name) && !name.startsWith('u-board-') && !Object.hasOwn(taken, name);
9
+ /** The parts that draw element `name`. */
10
+ const usesOf = (board, name) => Object.entries(board.parts).filter(([, part]) => part.as === 'element' && part.element === name).map(([id]) => id);
11
+ /**
12
+ * The board's elements — drawings no built-in makes, each a template of markup with `{{prop}}` holes (or a script run in a
13
+ * sandboxed frame), its CSS, and the props it takes by name and type. An element is added by name — a template — and
14
+ * removed when no part draws it.
15
+ */
16
+ export function BoardElementsPanel({ board, onChange, labels = DEFAULT_DESIGNER_LABELS }) {
17
+ const elements = board.elements ?? {};
18
+ const [name, setName] = useState('');
19
+ const write = (next) => onChange(Object.keys(next).length > 0 ? next : undefined);
20
+ const set = (key, change) => {
21
+ const next = { ...elements[key], ...change };
22
+ for (const [k, v] of Object.entries(change))
23
+ if (v === undefined || (v === '' && k !== 'script'))
24
+ delete next[k];
25
+ // A template element keeps its template while it is emptied; a script element has none.
26
+ if (typeof next.template !== 'string' && typeof next.script !== 'string')
27
+ next.template = '';
28
+ write({ ...elements, [key]: next });
29
+ };
30
+ return (_jsxs("div", { className: "ub-panel ub-panel--elements", children: [_jsx("h2", { className: "ub-panel__heading", children: labels.elementsHeading }), Object.entries(elements).map(([key, element]) => (_jsx(BoardElement, { name: key, element: element, uses: usesOf(board, key), labels: labels, set: change => set(key, change), remove: () => {
31
+ const { [key]: _gone, ...rest } = elements;
32
+ write(rest);
33
+ } }, key))), _jsxs("div", { className: "ub-panel__actions", style: GROUP_STYLE, children: [_jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.elementName, _jsx("input", { type: "text", value: name, onChange: e => setName(e.target.value) })] }), _jsx("button", { type: "button", className: "ub-action", disabled: !usable(name.trim(), elements), onClick: () => {
34
+ write({ ...elements, [name.trim()]: { template: '<div>{{value}}</div>', props: { value: 'string' } } });
35
+ setName('');
36
+ }, children: labels.addElement })] })] }));
37
+ }
38
+ function BoardElement({ name, element, uses, labels, set, remove }) {
39
+ const [prop, setProp] = useState('');
40
+ const props = element.props ?? {};
41
+ const setProps = (next) => set({ props: Object.keys(next).length > 0 ? next : undefined });
42
+ return (_jsxs("fieldset", { className: "ub-panel__group", style: { minWidth: 0 }, children: [_jsx("legend", { children: _jsx("code", { children: name }) }), element.script !== undefined ? (_jsxs(_Fragment, { children: [_jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.elementScript, _jsx("textarea", { rows: 8, spellCheck: false, value: element.script, onChange: e => set({ script: e.target.value }) })] }), _jsx("p", { className: "ub-panel__hint", children: labels.elementScriptPreview })] })) : (_jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.elementTemplate, _jsx("textarea", { rows: 5, spellCheck: false, value: element.template ?? '', onChange: e => set({ template: e.target.value }) })] })), _jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.elementStyle, _jsx("textarea", { rows: 3, spellCheck: false, value: element.style ?? '', onChange: e => set({ style: e.target.value }) })] }), _jsxs("fieldset", { className: "ub-panel__group", style: { minWidth: 0 }, children: [_jsx("legend", { children: labels.elementProps }), Object.entries(props).map(([key, type]) => (_jsxs("div", { className: "ub-panel__field", style: FIELD_STYLE, children: [_jsx("code", { "aria-hidden": "true", children: key }), _jsx("select", { "aria-label": key, value: type, onChange: e => setProps({ ...props, [key]: e.target.value }), children: PROP_TYPES.map(t => (_jsx("option", { value: t, children: t }, t))) }), _jsx("button", { type: "button", className: "ub-action", onClick: () => {
43
+ const { [key]: _gone, ...rest } = props;
44
+ setProps(rest);
45
+ }, children: labels.elementPropRemove.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.elementPropNew, _jsx("input", { type: "text", value: prop, onChange: e => setProp(e.target.value) })] }), _jsx("button", { type: "button", className: "ub-action", disabled: !/^[\w-]+$/.test(prop.trim()) || Object.hasOwn(props, prop.trim()), onClick: () => {
46
+ setProps({ ...props, [prop.trim()]: 'string' });
47
+ setProp('');
48
+ }, children: labels.elementPropAdd })] })] }), uses.length > 0 ? (_jsx("p", { className: "ub-panel__hint", children: labels.removeElementPlaced.replace('{count}', String(uses.length)) })) : (_jsx("button", { type: "button", className: "ub-action", onClick: remove, children: labels.removeElement }))] }));
49
+ }
@@ -0,0 +1,40 @@
1
+ import { type DesignerLabels } from '../labels.js';
2
+ import type { BoardAppearance, ReferencePoint } from '@iyulab/u-board/picture';
3
+ import { type TriggerEditing } from './TriggersEditor.js';
4
+ export interface BoardPanelProps {
5
+ /** Whether the board has a background image (offers to remove it). */
6
+ hasBackground?: boolean;
7
+ appearance: BoardAppearance;
8
+ /** Where choosing and removing a background goes — a board that has one (a canvas view). Without them the panel
9
+ * offers no background. */
10
+ onChooseBackground?: () => void;
11
+ onRemoveBackground?: () => void;
12
+ onAppearanceChange: (appearance: BoardAppearance) => void;
13
+ /** The board's accent colour (absent: the theme's), and where a change to it goes. */
14
+ accent?: string;
15
+ onAccentChange?: (accent: string | undefined) => void;
16
+ /** The background image's reference points, and where a change to them goes — offered while there is a
17
+ * background. */
18
+ referencePoints?: [ReferencePoint, ReferencePoint];
19
+ onReferencePointsChange?: (points: [ReferencePoint, ReferencePoint] | undefined) => void;
20
+ /** Anchored nodes that the points' change left off their coordinates: whether the points are still to be
21
+ * given again, how many nodes moved, and the two answers. */
22
+ anchors?: {
23
+ awaitingPoints: boolean;
24
+ moved: number;
25
+ onRelocate: () => void;
26
+ onKeep: () => void;
27
+ };
28
+ /** What to select to edit something — `labels.selectNode` by default. */
29
+ hint?: string;
30
+ /** The board's triggers, to edit those that run when it opens. */
31
+ triggers?: TriggerEditing;
32
+ labels?: DesignerLabels;
33
+ }
34
+ /**
35
+ * The panel shown while nothing is selected: the board's own settings — its background, its tone and its accent.
36
+ * They describe the whole document rather than an action on it, so they sit where a selection's
37
+ * properties do, as editors conventionally show the document's properties when nothing is selected,
38
+ * and leave the toolbar to adding, saving, the mode and the view.
39
+ */
40
+ export declare function BoardPanel({ hasBackground, appearance, onChooseBackground, onRemoveBackground, onAppearanceChange, accent, onAccentChange, referencePoints, onReferencePointsChange, anchors, hint, triggers, labels, }: BoardPanelProps): import("react").JSX.Element;
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { DEFAULT_DESIGNER_LABELS } from '../labels.js';
3
+ import { ReferencePointsFields } from './AnchorFields.js';
4
+ import { GROUP_STYLE } from '@iyulab/u-board/toolkit';
5
+ import { TriggersEditor } from './TriggersEditor.js';
6
+ // The button row is spaced like the property panel's, and apart from the tone picker below it.
7
+ const ACTIONS_STYLE = { ...GROUP_STYLE, marginBottom: 'var(--ub-space-3, 12px)' };
8
+ /**
9
+ * The panel shown while nothing is selected: the board's own settings — its background, its tone and its accent.
10
+ * They describe the whole document rather than an action on it, so they sit where a selection's
11
+ * properties do, as editors conventionally show the document's properties when nothing is selected,
12
+ * and leave the toolbar to adding, saving, the mode and the view.
13
+ */
14
+ export function BoardPanel({ hasBackground = false, appearance, onChooseBackground, onRemoveBackground, onAppearanceChange, accent, onAccentChange, referencePoints, onReferencePointsChange, anchors, hint, triggers, labels = DEFAULT_DESIGNER_LABELS, }) {
15
+ return (_jsxs("div", { className: "ub-panel ub-panel--board", children: [_jsx("h2", { className: "ub-panel__heading", children: labels.boardHeading }), onChooseBackground && (_jsxs("div", { className: "ub-panel__actions", style: ACTIONS_STYLE, children: [_jsx("button", { type: "button", className: "ub-action", onClick: onChooseBackground, children: labels.setBackground }), hasBackground && onRemoveBackground && (_jsx("button", { type: "button", className: "ub-action", onClick: onRemoveBackground, children: labels.removeBackground }))] })), hasBackground && onReferencePointsChange && (_jsx(ReferencePointsFields, { points: referencePoints, onChange: onReferencePointsChange, labels: labels })), hasBackground && anchors?.awaitingPoints && (_jsx("p", { className: "ub-panel__hint", role: "status", children: labels.referencePointsNeeded })), hasBackground && anchors && !anchors.awaitingPoints && anchors.moved > 0 && (_jsxs("div", { role: "status", className: "ub-panel__actions", style: ACTIONS_STYLE, children: [_jsx("p", { className: "ub-panel__hint", children: labels.anchorsMoved.replace('{count}', String(anchors.moved)) }), _jsx("button", { type: "button", className: "ub-action", onClick: anchors.onRelocate, children: labels.relocateAnchors }), _jsx("button", { type: "button", className: "ub-action", onClick: anchors.onKeep, children: labels.keepAnchors })] })), _jsxs("label", { className: "ub-panel__field ub-authoring__appearance", children: [labels.appearance, _jsxs("select", { value: appearance, onChange: e => onAppearanceChange(e.target.value), children: [_jsx("option", { value: "light", children: labels.appearanceLight }), _jsx("option", { value: "dark", children: labels.appearanceDark })] })] }), onAccentChange && (_jsxs("label", { className: "ub-panel__field", children: [labels.accent, _jsx("input", { defaultValue: accent ?? '', placeholder: labels.accentPlaceholder,
16
+ // Written when left, as the other fields are; cleared, the board takes the theme's again.
17
+ onBlur: e => {
18
+ const typed = e.currentTarget.value.trim();
19
+ if (typed !== (accent ?? ''))
20
+ onAccentChange(typed === '' ? undefined : typed);
21
+ } }, accent ?? '')] })), triggers && _jsx(TriggersEditor, { ...triggers, labels: labels }), _jsx("p", { className: "ub-panel__hint", children: hint ?? labels.selectNode })] }));
22
+ }
@@ -0,0 +1,13 @@
1
+ import type { CanvasView } from '@iyulab/u-board/picture';
2
+ import { type KindEditorProps } from './editor-parts.js';
3
+ /**
4
+ * The canvas of a board being edited, drawn from its picture: in Edit, a canvas-kit `KonvaDesigner` moves, sizes,
5
+ * selects and deletes the parts — each widget, rendered through the same path a viewer uses (`resolveDocument` +
6
+ * `toCanvasKit`), laid over its footprint, display-only — and in View, the canvas-kit `Viewer` shows the board as a
7
+ * shared link does (docs/principles.md — editor/renderer separation; the designer never renders a widget itself, it
8
+ * only owns a part's footprint and selection). What a selected part is set to is the panel `parts.panel` draws.
9
+ *
10
+ * Both modes share one pan/zoom. The board opens fitted into view (shrunk to fit, never magnified) and stays fitted as
11
+ * it resizes until the author pans or zooms; "Fit to view" restores that.
12
+ */
13
+ export declare function CanvasAuthoring({ doc, updateDoc, mode, onModeChange, documentActions, adapters, width, height, labels, clock, triggerHosting, parts, }: KindEditorProps<CanvasView>): import("react").JSX.Element;
@@ -0,0 +1,161 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useEffectEvent, useMemo, useRef, useState } from 'react';
3
+ import { KonvaDesigner } from '@canvas-kit/designer';
4
+ import { Viewer } from '@canvas-kit/viewer';
5
+ import { documentToScene, applySceneToDocument, partOf } from './scene-mapping.js';
6
+ import { removeParts } from '../editing/part-removal.js';
7
+ import { resolveDocument } from '@iyulab/u-board/toolkit';
8
+ import { toCanvasKit } from '@iyulab/u-board/toolkit';
9
+ import { focusPart, useTriggers } from '@iyulab/u-board/toolkit';
10
+ import { widgetTypeName } from './widget-catalog.js';
11
+ import { documentExtent } from '@iyulab/u-board/toolkit';
12
+ import { sizedNodes } from '@iyulab/u-board/toolkit';
13
+ import { useFittedView } from '@iyulab/u-board/toolkit';
14
+ import { ViewControls } from '@iyulab/u-board/toolkit';
15
+ import { TOOLBAR_STYLE, GROUP_STYLE, mayLoadBackground } from '@iyulab/u-board/toolkit';
16
+ import { ModeSwitch } from './editor-parts.js';
17
+ import { AttributionLine, boardAttributions, showsExampleData } from '@iyulab/u-board/toolkit';
18
+ /** The note that a background picture is left out — in the warning colours, with or without the stylesheet. */
19
+ const BACKGROUND_BLOCKED_STYLE = {
20
+ padding: '2px 8px',
21
+ borderRadius: 4,
22
+ background: 'var(--_ub-warning-bg, var(--ub-warning-bg, #fef3c7))',
23
+ color: 'var(--_ub-warning, var(--ub-warning, #92400e))',
24
+ fontWeight: 600,
25
+ };
26
+ /**
27
+ * The canvas of a board being edited, drawn from its picture: in Edit, a canvas-kit `KonvaDesigner` moves, sizes,
28
+ * selects and deletes the parts — each widget, rendered through the same path a viewer uses (`resolveDocument` +
29
+ * `toCanvasKit`), laid over its footprint, display-only — and in View, the canvas-kit `Viewer` shows the board as a
30
+ * shared link does (docs/principles.md — editor/renderer separation; the designer never renders a widget itself, it
31
+ * only owns a part's footprint and selection). What a selected part is set to is the panel `parts.panel` draws.
32
+ *
33
+ * Both modes share one pan/zoom. The board opens fitted into view (shrunk to fit, never magnified) and stays fitted as
34
+ * it resizes until the author pans or zooms; "Fit to view" restores that.
35
+ */
36
+ export function CanvasAuthoring({ doc, updateDoc, mode, onModeChange, documentActions, adapters, width, height, labels, clock, triggerHosting, parts, }) {
37
+ // Read when a result is drawn, not watched: a new function on every render must not re-resolve.
38
+ const readClock = useEffectEvent(() => clock());
39
+ // The board as its bindings last resolved, and when — drawn below as a viewer draws it.
40
+ const [resolved, setResolved] = useState(null);
41
+ // The parts the editor has selected, in the order they were picked. With several (a box selection), they can be
42
+ // deleted together, and two joined, but no single part's panel applies.
43
+ const [selectedIds, setSelectedIds] = useState([]);
44
+ // A part just added, to be selected once the designer draws it — so its panel opens.
45
+ const [pendingSelect, setPendingSelect] = useState(null);
46
+ const designerRef = useRef(null);
47
+ const view = useFittedView();
48
+ const { fitTo, focusOn } = view;
49
+ // The board's triggers run in View only, as a shared link runs them; in Edit a click selects.
50
+ const shownDoc = mode === 'view' ? (resolved?.doc ?? null) : null;
51
+ const focus = useCallback((node) => focusPart(shownDoc, node, focusOn, null), [shownDoc, focusOn]);
52
+ const { activatable, activate, elementEvent } = useTriggers(doc, shownDoc, { ...triggerHosting, focus });
53
+ const preview = useMemo(() => resolved &&
54
+ toCanvasKit(resolved.doc, {
55
+ qualityText: labels.qualityText,
56
+ now: resolved.now,
57
+ locale: labels.locale,
58
+ ...(mode === 'view' && { activatable, onNodeActivate: activate, onElementEvent: elementEvent }),
59
+ }), [resolved, labels, mode, activatable, activate, elementEvent]);
60
+ // The picture as it was opened — this editor is mounted afresh for each board opened — which is what fits into view;
61
+ // not each edit.
62
+ const [openedDoc] = useState(doc);
63
+ const { movable, deletable } = parts;
64
+ const scene = useMemo(() => documentToScene(doc, movable, deletable), [doc, movable, deletable]);
65
+ const extent = useMemo(() => documentExtent(doc), [doc]);
66
+ // The widgets the viewer would draw, laid over the parts being edited — display-only, so a press on one reaches the
67
+ // part beneath — each with its widget type named above it. Where and how big each is comes from the picture as edited
68
+ // now; only what it shows waits for the bindings to resolve, so a moved or resized part does not leave its widget
69
+ // behind while a slow source answers.
70
+ const editorOverlays = useMemo(() => {
71
+ const widgets = new Map((preview?.overlays ?? []).map(overlay => [overlay.id, overlay.content]));
72
+ return sizedNodes(doc).map(node => ({
73
+ id: node.id,
74
+ x: node.x,
75
+ y: node.y,
76
+ width: node.width,
77
+ height: node.height,
78
+ content: _jsx(NodeInPlace, { widget: widgets.get(node.id) ?? null, name: widgetTypeName(node.widget.type, labels.locale) }),
79
+ }));
80
+ }, [preview, doc, labels.locale]);
81
+ useEffect(() => {
82
+ fitTo(documentExtent(openedDoc));
83
+ }, [fitTo, openedDoc]);
84
+ useEffect(() => {
85
+ let cancelled = false;
86
+ resolveDocument(doc, adapters).then(result => {
87
+ if (!cancelled)
88
+ setResolved({ doc: result, now: readClock() });
89
+ });
90
+ return () => {
91
+ cancelled = true;
92
+ };
93
+ }, [doc, adapters]);
94
+ const attributions = useMemo(() => boardAttributions(doc, adapters), [doc, adapters]);
95
+ const example = useMemo(() => showsExampleData(doc, adapters), [doc, adapters]);
96
+ // A part that leaves the picture (undone) leaves the selection.
97
+ useEffect(() => {
98
+ const present = (id) => doc.nodes.some(n => n.id === id) || doc.connectors.some(c => c.id === id) || !!doc.decorations?.some(d => d.id === id);
99
+ setSelectedIds(ids => (ids.every(present) ? ids : ids.filter(present)));
100
+ }, [doc]);
101
+ useEffect(() => {
102
+ if (!pendingSelect || !scene.getObjects().some(o => o.id === pendingSelect))
103
+ return;
104
+ designerRef.current?.select([pendingSelect]);
105
+ setPendingSelect(null);
106
+ }, [scene, pendingSelect]);
107
+ const handleSceneChange = (newScene) => {
108
+ updateDoc(prev => applySceneToDocument(prev, newScene));
109
+ };
110
+ // Nothing is selected on a board being viewed — and the editor, mounted again on the way back, starts with no
111
+ // selection — so the panel lets go of the selected item rather than show one the board does not.
112
+ const handleViewMode = () => {
113
+ onModeChange('view');
114
+ setSelectedIds([]);
115
+ };
116
+ // A connector is drawn in pieces (its line, heads, label chip): any of them selects the connector.
117
+ const handleSelectionChange = (selection) => {
118
+ setSelectedIds([...new Set(selection.flatMap(object => (object.id ? [partOf(object.id)] : [])))]);
119
+ };
120
+ // Two of the board's own parts picked: they can be joined, first to second.
121
+ const pair = selectedIds.length === 2 && selectedIds.every(id => doc.nodes.some(n => n.id === id) && movable(id)) ? selectedIds : null;
122
+ const handleConnect = () => {
123
+ if (!pair || !parts.connect)
124
+ return;
125
+ setPendingSelect(parts.connect(pair[0], pair[1]));
126
+ };
127
+ const handleDelete = () => {
128
+ if (selectedIds.length === 0)
129
+ return;
130
+ const ids = selectedIds.filter(id => movable(id) || !!deletable?.(id));
131
+ updateDoc(prev => removeParts(prev, ids));
132
+ designerRef.current?.select([]);
133
+ setSelectedIds([]);
134
+ };
135
+ // Delete or Backspace on the editor deletes the selection — not while typing in a field.
136
+ const handleEditorKey = (e) => {
137
+ if (e.key !== 'Delete' && e.key !== 'Backspace')
138
+ return;
139
+ const target = e.target;
140
+ if (target.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName))
141
+ return;
142
+ if (selectedIds.length === 0)
143
+ return;
144
+ e.preventDefault();
145
+ handleDelete();
146
+ };
147
+ // Sized panes stay at their size; container-sized ones split the row and fill its height.
148
+ const followsContainer = width === undefined || height === undefined;
149
+ const paneStyle = followsContainer
150
+ ? { flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column' }
151
+ : { display: 'flex', flexDirection: 'column' };
152
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "ub-authoring__toolbar", style: TOOLBAR_STYLE, children: [_jsxs("div", { className: "ub-authoring__group", style: GROUP_STYLE, children: [parts.connect && (_jsx("button", { type: "button", className: "ub-action", onClick: handleConnect, disabled: !pair, title: pair ? undefined : labels.connectHint, children: labels.addConnector })), _jsx("button", { type: "button", className: "ub-action", onClick: handleDelete, disabled: selectedIds.length === 0, children: labels.deleteSelected })] }), documentActions, _jsx(ModeSwitch, { mode: mode, onEdit: () => onModeChange('edit'), onView: handleViewMode, labels: labels }), _jsx("div", { className: "ub-authoring__group", style: GROUP_STYLE, children: _jsx(ViewControls, { view: view, onFit: extent ? () => fitTo(extent) : undefined, labels: labels }) }), doc.background.image && !mayLoadBackground(doc.background.image.src) && (_jsx("span", { role: "status", style: BACKGROUND_BLOCKED_STYLE, children: labels.backgroundBlocked }))] }), _jsxs("div", { className: "ub-authoring__panes", style: { display: 'flex', gap: 'var(--ub-space-4, 16px)', flex: 1, minHeight: 0 }, children: [_jsxs("div", { className: "ub-authoring__pane", style: paneStyle, children: [mode === 'edit' ? (_jsx("div", { className: "ub-authoring__surface", "data-appearance": doc.appearance ?? 'light', style: { flex: 1, minHeight: 0 }, role: "presentation", onKeyDown: handleEditorKey, children: _jsx(KonvaDesigner, { ref: designerRef, width: width, height: height, scene: scene, transform: view.transform, onTransformChange: view.onUserTransform, onViewportResize: view.onViewportResize, onSceneChange: handleSceneChange, onSelectionChange: handleSelectionChange, overlays: editorOverlays, ariaLabel: labels.editorRegion }) })) : preview ? (_jsx("div", { className: "ub-authoring__surface", "data-appearance": doc.appearance ?? 'light', style: { flex: 1, minHeight: 0 }, children: _jsx(Viewer, { width: width, height: height, scene: preview.scene, overlays: preview.overlays, transform: view.transform, onTransformChange: view.onUserTransform, onViewportResize: view.onViewportResize, ariaLabel: labels.previewRegion }) })) : (_jsx("p", { className: "ub-authoring__status", children: labels.resolving })), mode === 'view' && preview && _jsx(AttributionLine, { attributions: attributions, example: example, labels: labels })] }), _jsx("div", { className: "ub-authoring__panel", style: { flex: '0 0 var(--ub-panel-width, 320px)', minWidth: 0, overflowY: 'auto' }, children: selectedIds.length > 1 ? (_jsx("p", { className: "ub-panel__hint", children: labels.multipleSelected.replace('{count}', String(selectedIds.length)) })) : ((parts.panel(selectedIds[0] ?? null) ?? _jsx("p", { className: "ub-panel__hint", children: labels.layoutOnlyHint }))) })] })] }));
153
+ }
154
+ const NODE_IN_PLACE_STYLE = { position: 'relative', width: '100%', height: '100%' };
155
+ // Above the node's box, so it never covers the widget; the tag is a hint, not part of the board.
156
+ const NODE_TAG_STYLE = { position: 'absolute', left: 0, bottom: '100%', whiteSpace: 'nowrap' };
157
+ /** A part's widget drawn where it is being edited, with the widget type named above it — what tells two parts apart,
158
+ * and what an unbound widget that draws nothing still shows. */
159
+ function NodeInPlace({ widget, name }) {
160
+ return (_jsxs("div", { className: "ub-authoring__node", style: NODE_IN_PLACE_STYLE, children: [widget, _jsx("span", { className: "ub-authoring__node-tag", style: NODE_TAG_STYLE, children: name })] }));
161
+ }
@@ -0,0 +1,29 @@
1
+ import { type DesignerLabels } from '../labels.js';
2
+ import type { Query } from '@iyulab/u-board/toolkit';
3
+ /** A data source a board may read from, as its host names it — one of the adapters the host gives. */
4
+ export interface DataSource {
5
+ id: string;
6
+ label?: string;
7
+ }
8
+ export interface DataPanelProps {
9
+ data: Readonly<Record<string, Query>> | undefined;
10
+ /** The board's data as the panel made them — none left: `undefined`. */
11
+ onChange: (data: Record<string, Query> | undefined) => void;
12
+ /** Where a read may come from — the host's sources. */
13
+ sources: readonly DataSource[];
14
+ /** Reads one of the board's data as the board reads it whole: the list, or the one item. Omitted, there is no looking. */
15
+ look?: (name: string) => Promise<unknown>;
16
+ /** The board's statuses, by name — what a field's status maps to. */
17
+ statuses?: readonly string[];
18
+ labels?: DesignerLabels;
19
+ }
20
+ /**
21
+ * The board's data — each read: where from (a source of the host's), what is asked, the list in the answer and the
22
+ * field naming its items, and the meaning of its fields (a name, a unit, decimal places, a format). Looking reads the
23
+ * data now and offers the fields it found. A field's value names and state are written as structures and shown, not
24
+ * edited, here.
25
+ */
26
+ export declare function DataPanel({ data, onChange, sources, look, statuses, labels }: DataPanelProps): import("react").JSX.Element;
27
+ /** Items picked by their fields, as written — `kind=PM10, site=north` — a number (not one with a leading zero, an id) or
28
+ * `true`/`false` as one, the rest as text; `null` when it is not pairs, `undefined` when empty. */
29
+ export declare function parseWhere(text: string): Record<string, string | number | boolean> | null | undefined;
@@ -0,0 +1,98 @@
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, GROUP_STYLE, isRelativeRule } from '@iyulab/u-board/toolkit';
6
+ import { MappingEditor } from './MappingEditor.js';
7
+ /** Whether new data may take `name`: a name parts can address (`<data>[<key>]`), not one taken. */
8
+ const usable = (name, taken) => /^[\w-]+$/.test(name.trim()) && !Object.hasOwn(taken, name.trim());
9
+ /** Whether a new field may take `name`: any name an item's field has, not an empty one or one taken. */
10
+ const usableField = (name, taken) => name.trim() !== '' && !Object.hasOwn(taken, name.trim());
11
+ /** The fields an item read has: an object's own names, or those of a list's first item. */
12
+ function fieldsIn(value) {
13
+ const item = Array.isArray(value) ? value[0] : value;
14
+ return typeof item === 'object' && item !== null && !Array.isArray(item) ? Object.keys(item) : [];
15
+ }
16
+ /** A query without the settings emptied in the panel. */
17
+ function tidy(value) {
18
+ return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined && v !== ''));
19
+ }
20
+ /**
21
+ * The board's data — each read: where from (a source of the host's), what is asked, the list in the answer and the
22
+ * field naming its items, and the meaning of its fields (a name, a unit, decimal places, a format). Looking reads the
23
+ * data now and offers the fields it found. A field's value names and state are written as structures and shown, not
24
+ * edited, here.
25
+ */
26
+ export function DataPanel({ data = {}, onChange, sources, look, statuses = [], labels = DEFAULT_DESIGNER_LABELS }) {
27
+ const [name, setName] = useState('');
28
+ const write = (next) => onChange(Object.keys(next).length > 0 ? next : undefined);
29
+ const set = (key, change) => write({ ...data, [key]: tidy({ ...data[key], ...change }) });
30
+ const setField = (key, field, change) => {
31
+ const { [field]: before, ...others } = data[key].fields ?? {};
32
+ const fields = change === null ? others : { ...data[key].fields, [field]: tidy({ ...before, ...change }) };
33
+ set(key, { fields: Object.keys(fields).length > 0 ? fields : undefined });
34
+ };
35
+ const remove = (key) => {
36
+ const { [key]: _gone, ...rest } = data;
37
+ write(rest);
38
+ };
39
+ return (_jsxs("div", { className: "ub-panel ub-panel--data", children: [_jsx("h2", { className: "ub-panel__heading", children: labels.dataHeading }), Object.entries(data).map(([key, query]) => (_jsx(DataRead, { name: key, query: query, sources: sources, look: look, statuses: statuses, labels: labels, set: change => set(key, change), setField: (field, change) => setField(key, field, change), remove: () => remove(key) }, key))), _jsxs("div", { className: "ub-panel__actions", style: GROUP_STYLE, children: [_jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.dataNew, _jsx("input", { type: "text", value: name, onChange: e => setName(e.target.value) })] }), _jsx("button", { type: "button", className: "ub-action", disabled: !usable(name, data) || sources.length === 0, onClick: () => {
40
+ write({ ...data, [name.trim()]: { from: sources[0].id, path: '/' } });
41
+ setName('');
42
+ }, children: labels.dataAdd })] }), sources.length === 0 && _jsx("p", { className: "ub-panel__hint", children: labels.noDataSources })] }));
43
+ }
44
+ /** Items picked by their fields, as written — `kind=PM10, site=north` — a number (not one with a leading zero, an id) or
45
+ * `true`/`false` as one, the rest as text; `null` when it is not pairs, `undefined` when empty. */
46
+ export function parseWhere(text) {
47
+ if (text.trim() === '')
48
+ return undefined;
49
+ const where = {};
50
+ for (const pair of text.split(',')) {
51
+ const at = pair.indexOf('=');
52
+ const field = pair.slice(0, at).trim();
53
+ const value = pair.slice(at + 1).trim();
54
+ if (at < 0 || field === '' || value === '')
55
+ return null;
56
+ where[field] = /^-?(0|[1-9]\d*)(\.\d+)?$/.test(value) ? Number(value) : value === 'true' ? true : value === 'false' ? false : value;
57
+ }
58
+ return where;
59
+ }
60
+ const whereText = (where) => (where ? Object.entries(where).map(([field, value]) => `${field}=${value}`).join(', ') : '');
61
+ /** The finer points of a request read, folded: which items, when they were observed, how long a value stays current, the
62
+ * source's time zone, the time its request asks for, and a long answer read as one item. */
63
+ function ReadDetails({ query, labels, set }) {
64
+ const [where, setWhere] = useState(() => whereText(query.where));
65
+ const whereRead = parseWhere(where);
66
+ 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) })] }));
67
+ const seconds = (label, value, change) => (_jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [label, _jsx("input", { type: "number", min: 0, value: value ?? '', onChange: e => change(e.target.value === '' ? undefined : Number(e.target.value)) })] }));
68
+ const setNow = (change) => {
69
+ const now = Object.fromEntries(Object.entries({ ...query.now, ...change }).filter(([, v]) => v !== undefined));
70
+ set({ now: Object.keys(now).length > 0 ? now : undefined });
71
+ };
72
+ const setPivot = (change) => {
73
+ const pivot = { key: query.pivot?.key ?? '', value: query.pivot?.value ?? '', ...change };
74
+ set({ pivot: pivot.key.trim() === '' && pivot.value.trim() === '' ? undefined : pivot });
75
+ };
76
+ const open = [query.where, query.time, query.answerTime, query.fresh, query.zone, query.now, query.pivot].some(v => v !== undefined);
77
+ return (_jsxs("details", { className: "ub-panel__details", open: open, children: [_jsx("summary", { children: labels.dataDetails }), _jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.dataWhere, _jsx("input", { type: "text", value: where, "aria-invalid": whereRead === null, onChange: e => {
78
+ setWhere(e.target.value);
79
+ const read = parseWhere(e.target.value);
80
+ if (read !== null)
81
+ set({ where: read });
82
+ } }), whereRead === null && (_jsx("span", { role: "alert", style: ERROR_STYLE, children: labels.dataWhereInvalid }))] }), text(labels.dataTime, query.time, time => set({ time })), text(labels.dataAnswerTime, query.answerTime, answerTime => set({ answerTime })), seconds(labels.dataFresh, query.fresh, fresh => set({ fresh })), text(labels.dataZone, query.zone, zone => set({ zone })), seconds(labels.dataNowDelay, query.now?.delay, delay => setNow({ delay })), seconds(labels.dataNowStep, query.now?.step, step => setNow({ step })), text(labels.dataPivotKey, query.pivot?.key, key => setPivot({ key })), text(labels.dataPivotValue, query.pivot?.value, value => setPivot({ value }))] }));
83
+ }
84
+ function DataRead({ name, query, sources, look, statuses, labels, set, setField, remove }) {
85
+ const [field, setFieldName] = useState('');
86
+ const [found, setFound] = useState(null);
87
+ const fields = query.fields ?? {};
88
+ const known = sources.some(s => s.id === query.from) ? sources : [...sources, { id: query.from }];
89
+ 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) })] }));
90
+ return (_jsxs("fieldset", { className: "ub-panel__group", style: { minWidth: 0 }, children: [_jsx("legend", { children: _jsx("code", { children: name }) }), _jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.dataFrom, _jsx("select", { value: query.from, onChange: e => set({ from: e.target.value }), children: known.map(source => (_jsx("option", { value: source.id, children: source.label ?? source.id }, source.id))) })] }), query.ref !== undefined ? (_jsx("p", { className: "ub-panel__hint", children: labels.dataByRef })) : (_jsxs(_Fragment, { children: [text(labels.dataPath, query.path, path => set({ path })), text(labels.dataList, query.list, list => set({ list })), text(labels.dataKey, query.key, key => set({ key })), _jsx(ReadDetails, { query: query, labels: labels, set: set })] })), _jsxs("fieldset", { className: "ub-panel__group", style: { minWidth: 0 }, children: [_jsx("legend", { children: labels.dataFields }), Object.entries(fields).map(([fieldName, f]) => (_jsxs("fieldset", { className: "ub-panel__group", style: { minWidth: 0 }, children: [_jsx("legend", { children: _jsx("code", { children: fieldName }) }), text(labels.dataFieldLabel, f.label, label => setField(fieldName, { label })), text(labels.dataFieldUnit, f.unit, unit => setField(fieldName, { unit })), _jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.dataFieldDigits, _jsx("input", { type: "number", min: 0, step: 1, value: f.digits ?? '', onChange: e => setField(fieldName, { digits: e.target.value === '' ? undefined : Number(e.target.value) }) })] }), text(labels.dataFieldFormat, f.format, format => setField(fieldName, { format })), isRelativeRule(f.state) ? (_jsxs("p", { className: "ub-panel__hint", children: [labels.dataFieldStateRelative, ": ", _jsx("code", { children: JSON.stringify(f.state.relative) })] })) : (_jsx(MappingEditor, { legend: labels.dataFieldState, value: f.state, raw: true, names: statuses, onChange: state => setField(fieldName, { state }), labels: labels })), _jsx(MappingEditor, { legend: labels.dataFieldNames, value: f.names, onChange: names => setField(fieldName, { names: names === 'raw' ? undefined : names }), labels: labels }), _jsx("button", { type: "button", className: "ub-action", onClick: () => setField(fieldName, null), children: labels.dataFieldRemove.replace('{name}', fieldName) })] }, fieldName))), _jsxs("div", { className: "ub-panel__actions", style: GROUP_STYLE, children: [_jsxs("label", { className: "ub-panel__field", style: FIELD_STYLE, children: [labels.dataFieldNew, _jsx("input", { type: "text", value: field, onChange: e => setFieldName(e.target.value) })] }), _jsx("button", { type: "button", className: "ub-action", disabled: !usableField(field, fields), onClick: () => {
91
+ setField(field.trim(), {});
92
+ setFieldName('');
93
+ }, children: labels.dataFieldAdd })] })] }), look && (_jsxs("div", { className: "ub-panel__actions", style: GROUP_STYLE, children: [_jsx("button", { type: "button", className: "ub-action", disabled: found === 'reading', onClick: () => {
94
+ setFound('reading');
95
+ look(name).then(value => setFound(fieldsIn(value)), () => setFound([]));
96
+ }, children: found === 'reading' ? labels.dataLooking : labels.dataLook }), Array.isArray(found) &&
97
+ (found.length === 0 ? (_jsx("p", { role: "status", children: labels.dataFoundNone })) : (_jsx("div", { role: "group", "aria-label": labels.dataFound, children: found.filter(f => !Object.hasOwn(fields, f)).map(f => (_jsx("button", { type: "button", className: "ub-action", onClick: () => setField(f, {}), children: labels.dataFoundAdd.replace('{name}', f) }, f))) })))] })), _jsx("button", { type: "button", className: "ub-action", onClick: remove, children: labels.dataRemove.replace('{name}', name) })] }));
98
+ }
@@ -0,0 +1,13 @@
1
+ import { type GridView } from '@iyulab/u-board/picture';
2
+ import { type KindEditorProps } from './editor-parts.js';
3
+ /**
4
+ * The grid of a board being edited, drawn from its picture: its items in order, laid out as the viewer lays them out,
5
+ * beside a panel for the selected item — its room and place, then what `parts.panel` draws for it — or, with nothing
6
+ * selected, what `parts.panel` draws for the board. The layout width lays the grid out as a medium or a narrow screen
7
+ * will, so the author sees each.
8
+ */
9
+ export interface GridAuthoringProps extends KindEditorProps<GridView> {
10
+ /** Opens a canvas cell's canvas in the canvas editor. */
11
+ onEditCanvas?: (id: string) => void;
12
+ }
13
+ export declare function GridAuthoring({ doc, updateDoc, mode, onModeChange, documentActions, adapters, width, height, labels, clock, triggerHosting, onEditCanvas, parts }: GridAuthoringProps): import("react").JSX.Element;
@@ -0,0 +1,135 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useEffectEvent, useMemo, useRef, useState } from 'react';
3
+ import { resolveDocument } from '@iyulab/u-board/toolkit';
4
+ import { isCanvasItem } from '@iyulab/u-board/picture';
5
+ import { GridBoard, GRID_MEDIUM_MIN_WIDTH, GRID_WIDE_MIN_WIDTH } from '@iyulab/u-board/toolkit';
6
+ import { AttributionLine, boardAttributions, showsExampleData } from '@iyulab/u-board/toolkit';
7
+ import { focusPart, useTriggers } from '@iyulab/u-board/toolkit';
8
+ import { TOOLBAR_STYLE, GROUP_STYLE } from '@iyulab/u-board/toolkit';
9
+ import { GridItemPanel } from './GridItemPanel.js';
10
+ import { widgetTypeName } from './widget-catalog.js';
11
+ import { gridItemIndex, moveGridItem, removeGridItem, updateGridItem } from '../editing/grid-editing.js';
12
+ import { ModeSwitch } from './editor-parts.js';
13
+ const LAYOUT_MAX_WIDTH = {
14
+ wide: undefined,
15
+ medium: GRID_WIDE_MIN_WIDTH - 1,
16
+ narrow: GRID_MEDIUM_MIN_WIDTH - 1,
17
+ };
18
+ const CELL_STYLE = { position: 'relative', width: '100%', height: '100%' };
19
+ // The item's widget is shown, not used, while editing: a press anywhere on the cell selects the item.
20
+ const FRAME_STYLE = { width: '100%', height: '100%', pointerEvents: 'none' };
21
+ const PICK_STYLE = {
22
+ position: 'absolute',
23
+ inset: -4,
24
+ borderWidth: 2,
25
+ borderStyle: 'solid',
26
+ borderColor: 'transparent',
27
+ borderRadius: 10,
28
+ background: 'transparent',
29
+ cursor: 'pointer',
30
+ padding: 0,
31
+ };
32
+ const PICKED_STYLE = { ...PICK_STYLE, borderColor: 'var(--_ub-brand-solid, var(--ub-brand-solid, #2563eb))' };
33
+ // Where a dragged item would go: the item it is held over, whose place it takes.
34
+ const DROP_STYLE = { ...PICK_STYLE, borderStyle: 'dashed', borderColor: 'var(--_ub-brand-solid, var(--ub-brand-solid, #2563eb))' };
35
+ // Above the cell's top-right corner — the quality mark takes the top-left one.
36
+ const TAG_STYLE = { position: 'absolute', right: 0, bottom: '100%', marginBottom: 6, whiteSpace: 'nowrap' };
37
+ /** A widget not resolved yet — its own props, no quality — so a new or changed item shows at once. */
38
+ const unresolved = (widget) => ({ type: widget.type, props: widget.props ?? {}, quality: {} });
39
+ /** A canvas cell as edited now, each node showing what its widget read when it was last resolved — as a widget item
40
+ * does — and its connectors as written. */
41
+ function shownCanvas(canvas, resolved) {
42
+ const read = new Map(resolved?.nodes.map(node => [node.id, node.widget]));
43
+ return {
44
+ ...canvas,
45
+ nodes: canvas.nodes.map(node => {
46
+ const widget = read.get(node.id);
47
+ return { ...node, widget: widget && widget.type === node.widget.type ? widget : unresolved(node.widget) };
48
+ }),
49
+ connectors: canvas.connectors.map(connector => ({ ...connector, quality: {} })),
50
+ };
51
+ }
52
+ export function GridAuthoring({ doc, updateDoc, mode, onModeChange, documentActions, adapters, width, height, labels, clock, triggerHosting, onEditCanvas, parts }) {
53
+ const { movable } = parts;
54
+ const readClock = useEffectEvent(() => clock());
55
+ const [preview, setPreview] = useState(null);
56
+ const [selectedId, setSelectedId] = useState(null);
57
+ // An item being dragged to another place, and the item it is held over.
58
+ const [dragged, setDragged] = useState(null);
59
+ const [dropOn, setDropOn] = useState(null);
60
+ const [layoutWidth, setLayoutWidth] = useState('wide');
61
+ useEffect(() => {
62
+ let cancelled = false;
63
+ resolveDocument(doc, adapters).then(resolved => {
64
+ if (!cancelled)
65
+ setPreview({ doc: resolved, now: readClock() });
66
+ });
67
+ return () => {
68
+ cancelled = true;
69
+ };
70
+ }, [doc, adapters]);
71
+ const attributions = useMemo(() => boardAttributions(doc, adapters), [doc, adapters]);
72
+ const example = useMemo(() => showsExampleData(doc, adapters), [doc, adapters]);
73
+ // The board's triggers run in View only, as a shared link runs them; in Edit a click selects.
74
+ const surface = useRef(null);
75
+ const shownDoc = mode === 'view' ? (preview?.doc ?? null) : null;
76
+ const focus = useCallback((node) => focusPart(shownDoc, node, () => { }, surface.current), [shownDoc]);
77
+ const { activatable, activate, elementEvent } = useTriggers(doc, shownDoc, { ...triggerHosting, focus });
78
+ // Where each item is, how much room it takes and which widget it is come from the document as edited now; only
79
+ // what a widget shows waits for its bindings, so a moved or resized item does not lag behind a slow source.
80
+ const shown = useMemo(() => {
81
+ const resolved = new Map(preview?.doc.items.map(item => [item.id, item]));
82
+ return {
83
+ ...doc,
84
+ items: doc.items.map(item => {
85
+ const before = resolved.get(item.id);
86
+ if (isCanvasItem(item))
87
+ return { ...item, canvas: shownCanvas(item.canvas, before && isCanvasItem(before) ? before.canvas : undefined) };
88
+ const widget = before && !isCanvasItem(before) ? before.widget : undefined;
89
+ return { ...item, widget: widget && widget.type === item.widget.type ? widget : unresolved(item.widget) };
90
+ }),
91
+ };
92
+ }, [doc, preview]);
93
+ const selectedIndex = doc.items.findIndex(item => item.id === selectedId);
94
+ const selected = selectedIndex >= 0 ? doc.items[selectedIndex] : null;
95
+ const handleRemove = () => {
96
+ if (!selected)
97
+ return;
98
+ updateDoc(current => removeGridItem(current, selected.id));
99
+ setSelectedId(null);
100
+ };
101
+ const handleViewMode = () => {
102
+ onModeChange('view');
103
+ setSelectedId(null);
104
+ };
105
+ const followsContainer = width === undefined || height === undefined;
106
+ const paneStyle = followsContainer ? { flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column' } : { display: 'flex', flexDirection: 'column' };
107
+ const surfaceStyle = { flex: 1, minHeight: 0, width, height, maxWidth: mode === 'edit' ? LAYOUT_MAX_WIDTH[layoutWidth] : undefined };
108
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "ub-authoring__toolbar", style: TOOLBAR_STYLE, children: [documentActions, _jsx(ModeSwitch, { mode: mode, onEdit: () => onModeChange('edit'), onView: handleViewMode, labels: labels }), mode === 'edit' && (_jsx("div", { className: "ub-authoring__group ub-authoring__layout-width", style: GROUP_STYLE, role: "group", "aria-label": labels.layoutWidth, children: ['wide', 'medium', 'narrow'].map(choice => (_jsx("button", { type: "button", className: "ub-action", "aria-pressed": layoutWidth === choice, onClick: () => setLayoutWidth(choice), children: choice === 'wide' ? labels.layoutWide : choice === 'medium' ? labels.layoutMedium : labels.layoutNarrow }, choice))) }))] }), _jsxs("div", { className: "ub-authoring__panes", style: { display: 'flex', gap: 'var(--ub-space-4, 16px)', flex: 1, minHeight: 0 }, children: [_jsxs("div", { className: "ub-authoring__pane", style: paneStyle, children: [mode === 'edit' ? (_jsx("div", { className: "ub-authoring__surface", "data-appearance": doc.appearance ?? 'light', style: surfaceStyle, children: _jsx(GridBoard, { doc: shown, ariaLabel: labels.editorRegion, qualityText: labels.qualityText, now: preview?.now ?? 0, locale: labels.locale, labels: labels, canvasControls: false, renderCell: (item, frame, index) => {
109
+ const name = isCanvasItem(item) ? labels.canvasCell : widgetTypeName(item.widget.type, labels.locale);
110
+ const picked = item.id === selectedId;
111
+ return (_jsxs("div", { className: "ub-authoring__node", style: CELL_STYLE, children: [_jsx("div", { style: FRAME_STYLE, children: frame }), _jsx("span", { className: "ub-authoring__node-tag", style: TAG_STYLE, children: name }), _jsx("button", { type: "button", className: "ub-authoring__pick", "aria-label": `${index + 1}. ${name}`, "aria-pressed": picked, style: dropOn === item.id ? DROP_STYLE : picked ? PICKED_STYLE : PICK_STYLE, onClick: () => setSelectedId(picked ? null : item.id), draggable: movable(item.id), onDragStart: e => {
112
+ e.dataTransfer.effectAllowed = 'move';
113
+ e.dataTransfer.setData('text/plain', item.id);
114
+ setDragged(item.id);
115
+ }, onDragOver: e => {
116
+ if (!dragged || dragged === item.id || !movable(item.id))
117
+ return;
118
+ e.preventDefault();
119
+ e.dataTransfer.dropEffect = 'move';
120
+ setDropOn(item.id);
121
+ }, onDragLeave: () => setDropOn(on => (on === item.id ? null : on)), onDrop: e => {
122
+ e.preventDefault();
123
+ if (dragged) {
124
+ const moving = dragged;
125
+ updateDoc(current => moveGridItem(current, moving, gridItemIndex(current, item.id)));
126
+ setSelectedId(moving);
127
+ }
128
+ setDragged(null);
129
+ setDropOn(null);
130
+ }, onDragEnd: () => {
131
+ setDragged(null);
132
+ setDropOn(null);
133
+ } })] }));
134
+ } }) })) : preview ? (_jsx("div", { ref: surface, className: "ub-authoring__surface", "data-appearance": doc.appearance ?? 'light', style: surfaceStyle, children: _jsx(GridBoard, { doc: preview.doc, ariaLabel: labels.previewRegion, qualityText: labels.qualityText, now: preview.now, locale: labels.locale, labels: labels, activatable: activatable, onItemActivate: activate, onElementEvent: elementEvent }) })) : (_jsx("p", { className: "ub-authoring__status", children: labels.resolving })), mode === 'view' && preview && _jsx(AttributionLine, { attributions: attributions, example: example, labels: labels })] }), _jsx("div", { className: "ub-authoring__panel", style: { flex: '0 0 var(--ub-panel-width, 320px)', minWidth: 0, overflowY: 'auto' }, children: selected ? (_jsxs(_Fragment, { children: [movable(selected.id) && (_jsx(GridItemPanel, { item: selected, isFirst: selectedIndex === 0, isLast: selectedIndex === doc.items.length - 1, onChange: change => updateDoc(current => updateGridItem(current, selected.id, change)), onMove: by => updateDoc(current => moveGridItem(current, selected.id, gridItemIndex(current, selected.id) + by)), onRemove: handleRemove, spanOnly: true, labels: labels })), (parts.panel(selected.id) ?? _jsx("p", { className: "ub-panel__hint", children: labels.layoutOnlyHint })), isCanvasItem(selected) && onEditCanvas && (_jsx("button", { type: "button", className: "ub-action ub-action--primary", onClick: () => onEditCanvas(selected.id), children: labels.editCanvas }))] })) : ((parts.panel(null) ?? _jsx("p", { className: "ub-panel__hint", children: labels.layoutOnlyHint }))) })] })] }));
135
+ }
@@ -0,0 +1,22 @@
1
+ import { type DesignerLabels } from '../labels.js';
2
+ import type { GridItem } from '@iyulab/u-board/picture';
3
+ export interface GridItemPanelProps {
4
+ item: GridItem;
5
+ /** Whether it can move earlier or later — not at the start or the end of the order. */
6
+ isFirst: boolean;
7
+ isLast: boolean;
8
+ onChange: (change: Partial<Pick<GridItem, 'span' | 'spanMedium' | 'rowSpan' | 'height'>>) => void;
9
+ onMove: (by: -1 | 1) => void;
10
+ /** Left out, there is no Duplicate (an editor that adds nothing). */
11
+ onDuplicate?: () => void;
12
+ onRemove: () => void;
13
+ /** Only the columns it spans — what a board lays out. */
14
+ spanOnly?: boolean;
15
+ labels?: DesignerLabels;
16
+ }
17
+ /**
18
+ * A grid item's room and place: how many columns it spans at full and at medium width, its height, and where it
19
+ * is in the order. Moving and removing are buttons, so they are a key press as well as a click. A choice left at
20
+ * its default leaves its field out of the document.
21
+ */
22
+ export declare function GridItemPanel({ item, isFirst, isLast, onChange, onMove, onDuplicate, onRemove, spanOnly, labels }: GridItemPanelProps): import("react").JSX.Element;