@iyulab/u-board-designer 0.1.0 → 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.
@@ -12,9 +12,17 @@ import { documentExtent } from '@iyulab/u-board/toolkit';
12
12
  import { sizedNodes } from '@iyulab/u-board/toolkit';
13
13
  import { useFittedView } from '@iyulab/u-board/toolkit';
14
14
  import { ViewControls } from '@iyulab/u-board/toolkit';
15
- import { TOOLBAR_STYLE, GROUP_STYLE } from '@iyulab/u-board/toolkit';
15
+ import { TOOLBAR_STYLE, GROUP_STYLE, mayLoadBackground } from '@iyulab/u-board/toolkit';
16
16
  import { ModeSwitch } from './editor-parts.js';
17
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
+ };
18
26
  /**
19
27
  * The canvas of a board being edited, drawn from its picture: in Edit, a canvas-kit `KonvaDesigner` moves, sizes,
20
28
  * selects and deletes the parts — each widget, rendered through the same path a viewer uses (`resolveDocument` +
@@ -141,7 +149,7 @@ export function CanvasAuthoring({ doc, updateDoc, mode, onModeChange, documentAc
141
149
  const paneStyle = followsContainer
142
150
  ? { flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column' }
143
151
  : { display: 'flex', flexDirection: 'column' };
144
- 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 }) })] }), _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 }))) })] })] }));
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 }))) })] })] }));
145
153
  }
146
154
  const NODE_IN_PLACE_STYLE = { position: 'relative', width: '100%', height: '100%' };
147
155
  // Above the node's box, so it never covers the widget; the tag is a hint, not part of the board.
@@ -2,7 +2,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
2
2
  import { FIELD_STYLE } from './field-style.js';
3
3
  import { DEFAULT_DESIGNER_LABELS } from '../labels.js';
4
4
  import { useState } from 'react';
5
- import { ERROR_STYLE, GROUP_STYLE } from '@iyulab/u-board/toolkit';
5
+ import { ERROR_STYLE, GROUP_STYLE, isRelativeRule } from '@iyulab/u-board/toolkit';
6
6
  import { MappingEditor } from './MappingEditor.js';
7
7
  /** Whether new data may take `name`: a name parts can address (`<data>[<key>]`), not one taken. */
8
8
  const usable = (name, taken) => /^[\w-]+$/.test(name.trim()) && !Object.hasOwn(taken, name.trim());
@@ -87,7 +87,7 @@ function DataRead({ name, query, sources, look, statuses, labels, set, setField,
87
87
  const fields = query.fields ?? {};
88
88
  const known = sources.some(s => s.id === query.from) ? sources : [...sources, { id: query.from }];
89
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 })), _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: () => {
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
91
  setField(field.trim(), {});
92
92
  setFieldName('');
93
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: () => {
@@ -4,7 +4,7 @@
4
4
  // else of the board changes.
5
5
  import { DEFAULT_NODE_HEIGHT, DEFAULT_NODE_WIDTH } from '@iyulab/u-board/toolkit';
6
6
  import { GRID_DEFAULT_SPAN } from '@iyulab/u-board/picture';
7
- import { isDataPlace } from '@iyulab/u-board/toolkit';
7
+ import { isDataPlace, isFlowPlace } from '@iyulab/u-board/toolkit';
8
8
  /**
9
9
  * Whether the editor lays this board out: a canvas, or a grid — of parts, canvases, and grids of parts and canvases in
10
10
  * its cells (one level, as the picture draws them). A grid in a cell is laid out on its own (`cellBoard`): the picture
@@ -155,8 +155,8 @@ export function placesItself(board, id) {
155
155
  const placed = board.layout.place[part];
156
156
  if (placed === undefined || rest.length > 0)
157
157
  return false;
158
- // Items placed by their own fields are not dragged: their fields say where they are.
159
- return key === undefined ? Array.isArray(placed) : board.parts[part]?.each !== undefined && !Array.isArray(placed) && !isDataPlace(placed);
158
+ // Items placed by their own fields, or by a rule, are not dragged: the fields and the rule say where they are.
159
+ return key === undefined ? Array.isArray(placed) : board.parts[part]?.each !== undefined && !Array.isArray(placed) && !isDataPlace(placed) && !isFlowPlace(placed);
160
160
  }
161
161
  /**
162
162
  * The board with its canvas layout as the picture `after` shows it, the picture having been `before`: each part (or
@@ -1,5 +1,5 @@
1
1
  import { Scene } from '@canvas-kit/core';
2
- import { sizedNodes } from '@iyulab/u-board/toolkit';
2
+ import { mayLoadBackground, sizedNodes } from '@iyulab/u-board/toolkit';
3
3
  import { decorationObjects } from '@iyulab/u-board/toolkit';
4
4
  import { captionObjects, connectorObjects } from '@iyulab/u-board/toolkit';
5
5
  /** The stroke a rect decoration is drawn with in the designer when the document does not set its own — distinct
@@ -37,7 +37,8 @@ export function documentToScene(doc, movable, selectable) {
37
37
  : [...doc.nodes, ...doc.connectors, ...(doc.decorations ?? [])]).map(part => part.id));
38
38
  // A part not edited here, and every piece of it: drawn, never selected or moved.
39
39
  const add = (object) => scene.add(object.id && !own.has(partOf(object.id)) ? { ...object, locked: true } : object);
40
- if (doc.background.image) {
40
+ // A picture outside the origins the host allowed is not requested here either: the board is drawn on an empty background.
41
+ if (doc.background.image && mayLoadBackground(doc.background.image.src)) {
41
42
  add({
42
43
  type: 'image',
43
44
  x: 0,
@@ -3,3 +3,5 @@ import { type Board } from '@iyulab/u-board/domain';
3
3
  export declare const RULEBOOK_EXAMPLE: Board;
4
4
  /** The rulebook, as Markdown. */
5
5
  export declare function boardRulebook(): string;
6
+ /** The options every `chart.*` kind takes — the data columns and the like — said once under the table, not on each chart's row. */
7
+ export declare function sharedChartOptions(): string[];
@@ -49,14 +49,15 @@ ${['board', 'statuses', 'symbols', 'elements', 'data', 'presets', 'parts', 'layo
49
49
  A data read asks an HTTP connector a \`path\` — the usual read. Only an adapter the host names itself is given its own
50
50
  \`ref\` instead (one item, its \`fields\` only, never with \`path\`).
51
51
  Parts name a read (\`"pumps"\`, the whole list) or one item of it (\`"pumps[P1]"\`, by its \`key\`). Data stays raw —
52
- names, units, places and statuses live in \`fields\`, never in the data.
52
+ names, units, places and statuses live in \`fields\`, never in the data. A field's \`at\` points into one item, so with a \`list\` what sits beside it (a count, a total) is out of reach: read that
53
+ with a read of its own and no \`list\` — \`{ from: 'plant', path: '/plant/pumps?limit=1', fields: { total: { at: '/total' } } }\`. With a \`list\`, \`time\` is a field of each item (an item lacking it has no value); the answer's own is \`answerTime\`.
53
54
 
54
55
  ${keys('query')}
55
56
 
56
57
  A field is \`{ at?, label?, unit?, digits?, format?, names?, state? }\`; it reads \`/<its name>\` of the item unless \`at\`
57
58
  says where. \`state\` says how the value is doing, as a status: \`"raw"\` (the value is a status name), a map
58
59
  (\`{ "trip": "down", "*": "off" }\`, \`*\` for the rest) or bands (\`["ok", 1, "warn", 5, "alarm"]\` — the status below the
59
- first number, then from each number up). \`names\` takes the same two forms for the words a value is shown as.`,
60
+ first number, then from each number up); a time's \`state\` can follow the clock: \`{ "relative": ["past", -2700, "now", 1, "next", 2700, "later"] }\` — bands over the seconds from now to it (negative: gone by). \`names\` takes the same two forms for what a value is shown as (a word, a count of marks). A field can also be computed from the items of the list: \`derive: { op: count | sum | min | max | avg, field?, where? }\` — a part that reads the list (no item) shows it, a \`state\` on it can raise a status. A field that holds a list of objects says what its items' fields mean with \`items\`.`,
60
61
  `## statuses — how each status looks
61
62
 
62
63
  Tones (always there): ${code(TONES)}. A board names its own statuses, each with a \`tone\`, a \`label\`, and optionally a
@@ -65,30 +66,32 @@ label, glyph. Never write colours for statuses.`,
65
66
  `## parts — what, of what, which fields
66
67
 
67
68
  \`{ "as": <kind>, "of": "<data>" | "<data>[<key>]", "show": "<field>" | ["<field>", …], "title"?, …options }\`. \`{{field}}\`
68
- in a title or a label reads a field of \`of\`; \`{{key}}\` is the item's key. \`presets\` hold shared settings a part takes
69
+ in a title or a label reads a field of \`of\` (a status card's \`subtitle\` · \`note\` too; header \`kicker\` · \`subtitle\` are fixed text, \`clock\` is \`true\`); \`{{key}}\` is the item's key. \`presets\` hold shared settings a part takes
69
70
  with \`use\`; \`each: "<data>"\` makes one part per item (ids \`<part>/<key>\`) — it takes no \`of\`, and is placed once. A \`group\` is several parts on a small
70
71
  canvas of their own — its parts and links read the group's \`of\` unless they name their own.
71
72
 
72
73
  The board's own options take fields of what the part reads: a \`status-card\`'s \`meter\` is \`"<field>"\` or
73
- \`{ "field", "min"?, "max"?, "form"?: "bar" | "ring" }\`, its \`state\` (and a \`stat-list\`'s) the field whose status colours
74
+ \`{ "field", "min"?, "max"? (a number, or a field of the item), "form"?: "bar" | "ring" }\`, its \`state\` (and a \`stat-list\`'s) the field whose status colours
74
75
  it, a \`stat\`'s \`trend\` a field holding its recent values, a \`list\`'s \`primary\` · \`secondary\` · \`trailing\` ·
75
- \`badge\` · \`time\` fields of each row. Any other option is the widget's own — only those listed.
76
+ \`badge\` · \`time\` fields of each row. A part that reads one item takes \`highlight: "<boolean field>"\`: a ring while it is true, apart from the status. Any other option is the widget's own — only those listed.
76
77
 
77
78
  | kind | shows | reads data | options |
78
79
  |---|---|---|---|
79
- ${PART_KINDS.map(kindRow).join('\n')}`,
80
+ ${PART_KINDS.map(kindRow).join('\n')}
81
+
82
+ Every \`chart.*\` also takes ${code(sharedChartOptions())}.`,
80
83
  `## layout — where, per view kind
81
84
 
82
85
  - canvas: \`{ "kind": "canvas", "size": [w, h], "background"?, "place": { "<part>": [x, y, w?, h?] }, "shapes"?, "links"? }\` —
83
86
  an \`each\` part is placed item by item inside its own entry (\`{ "P1": [x, y, w, h], … }\`), or — when the items change and cannot be
84
87
  named — by their own fields: \`"quake": { "at": ["lon", "lat"], "frame": [-180, 90, 180, -90], "size": [14, 14] }\` puts each item
85
88
  at the point its two fields say; \`frame\` is the values at the canvas's left, top, right and bottom edges (the canvas's own
86
- coordinates when left out), \`size\` the box centred on the point (give one for a marker). An item with no number in a field, or
87
- outside the frame, is not drawn — \`look\` counts them. The data needs a \`key\`; the projection is linear (right for a plan or an
88
- equirectangular map). A part is placed one way, not both. A shape is a \`box\` (with \`label\`), a \`text\` at a point,
89
+ coordinates when left out), \`size\` the box centred on the point (give one for a marker: a dot alone is [14, 14]; one with a name or note draws
90
+ it right of the dot — about [150, 40] — and it is the box, not the dot, that is centred). An item with no number in a field, or
91
+ outside the frame, is not drawn — \`look\` counts them. The data needs a \`key\`; the projection is linear (right for a plan or a flat map). A part is placed one way, not both. Or by a rule: \`"tile": { "flow": { "cell": [w, h], "columns": n, "gap"?, "at"? }, "by"?: "<field>", "boxes"?: { "columns": n, "label"?: "{{key}}" } }\` — items flow in columns, or with \`by\` are grouped into labelled boxes (\`flow.pad\` [sides, top] kept clear inside). A shape is a \`box\` (with \`label\`), a \`text\` at a point,
89
92
  a \`line\` through points or a \`path\` (absolute M · L · C · Q · Z).
90
93
  - grid: \`{ "kind": "grid", "items": ["<part>", ["<part>", span], { "grid": [ … ], "span": n }, { "canvas": { … }, "span": n }] }\` —
91
- 12 columns; a grid inside an item stacks what it holds.
94
+ 12 columns; an \`each\` part's items are cells (\`["pumps", 2]\`: six to a row); a grid in an item stacks what it holds.
92
95
 
93
96
  A link joins two parts:
94
97
 
@@ -101,8 +104,10 @@ Use a built-in whenever one shows the thing — a title, a status and a few valu
101
104
  — every \`{{hole}}\` is a declared prop (\`number\` · \`string\` · \`boolean\` · \`list\` · \`object\`). A part places it with
102
105
  \`"as": "element", "element": "tank"\`, fixed \`props\` and \`bind\` (\`{ "level": "<field>", "tone": "<field>.tone" }\`). What markup cannot draw runs as a
103
106
  \`"script"\` in place of the template — in a sandboxed frame with no network, its props through \`ub.onProps\`, \`"resources"\` the
104
- https:// origins it loads from. The look is \`board.skin\` — fonts, a
105
- palette and a frame (${code(FRAMES)}) — once for the board; parts never set fonts or colours.`,
107
+ https:// origins it loads from. The look is \`board.skin\`, once for the board; parts never set fonts or colours:
108
+ \`{ "palette"?: { "paper", "ink", "muted", "rule" }, "font"? · "display"? · "mono"? (font stacks), "frame"?, "radius"? (px), "shadow"?: "none" | "soft" | "hard" }\`
109
+ — a frame is ${code(FRAMES)}; paper is the ground the widgets sit on, ink their text, muted the quieter text, rule their lines; an element's CSS reads them as
110
+ \`var(--u-widget-bg)\` · \`var(--u-widget-text)\` · \`var(--u-widget-text-secondary)\` · \`var(--u-widget-border)\`; a background picture follows the skin when it writes a colour as \`var(--u-widget-bg, #f6f4ef)\` (its own colour is the fallback).`,
106
111
  `## triggers — what the board does
107
112
 
108
113
  A trigger is \`{ "on", "when"?, "do": [ … ] }\`. \`on\` a part clicked (\`{ "event": "click", "part" }\`), an event its element
@@ -128,9 +133,16 @@ ${JSON.stringify(RULEBOOK_EXAMPLE, null, 1)}
128
133
  \`\`\``,
129
134
  ].join('\n\n');
130
135
  }
136
+ const optionsOf = (k) => [...k.own, ...k.options];
137
+ /** The options every `chart.*` kind takes — the data columns and the like — said once under the table, not on each chart's row. */
138
+ export function sharedChartOptions() {
139
+ const charts = PART_KINDS.filter(k => k.kind.startsWith('chart.'));
140
+ return charts.length === 0 ? [] : optionsOf(charts[0]).filter(option => charts.every(k => optionsOf(k).includes(option)));
141
+ }
131
142
  /** A kind's row: its name, what it shows, whether it reads data, and its options — the board's own, then the widget's. */
132
143
  function kindRow(k) {
133
144
  const reads = { needs: 'yes', may: 'may', no: 'no' }[k.of];
134
- const options = [...k.own, ...k.options];
145
+ const shared = k.kind.startsWith('chart.') ? sharedChartOptions() : [];
146
+ const options = optionsOf(k).filter(option => !shared.includes(option));
135
147
  return `| \`${k.kind}\` | ${k.shows.replace(/\|/g, '/')} | ${reads} | ${options.length > 0 ? options.map(o => `\`${o}\``).join(' ') : '—'} |`;
136
148
  }
@@ -131,6 +131,7 @@ export const KO_AUTHORING_LABELS = {
131
131
  dataFieldFormat: '서식',
132
132
  dataFieldRemove: '필드 {name} 지우기',
133
133
  dataFieldState: '상태',
134
+ dataFieldStateRelative: '상태가 시계를 따름(지금부터 그 시각까지의 초에 대한 구간) — 보드 JSON에서 고친다',
134
135
  dataFieldNames: '낱말',
135
136
  mappingNone: '(없음)',
136
137
  mappingRaw: '값이 곧 상태 이름',
@@ -184,6 +184,8 @@ export interface AuthoringLabels {
184
184
  dataFieldRemove: string;
185
185
  /** A field's status — how its value is doing — and the words it is shown as, as the data panel sets them. */
186
186
  dataFieldState: string;
187
+ /** A status that follows the clock (`{ relative: bands }`) — shown as it is; it is written in the board's JSON. */
188
+ dataFieldStateRelative: string;
187
189
  dataFieldNames: string;
188
190
  mappingNone: string;
189
191
  mappingRaw: string;
@@ -131,6 +131,7 @@ export const DEFAULT_AUTHORING_LABELS = {
131
131
  dataFieldFormat: 'Format',
132
132
  dataFieldRemove: 'Remove field {name}',
133
133
  dataFieldState: 'Status',
134
+ dataFieldStateRelative: 'Status follows the clock (bands over the seconds from now) — written in the board’s JSON',
134
135
  dataFieldNames: 'Words',
135
136
  mappingNone: '(none)',
136
137
  mappingRaw: 'The value is a status name',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iyulab/u-board-designer",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "The authoring tool for U-Board boards, and the check and rulebook an assistant writes by",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-or-later",
@@ -47,8 +47,8 @@
47
47
  "@canvas-kit/core": "^0.8.0",
48
48
  "@canvas-kit/designer": "^0.10.0",
49
49
  "@canvas-kit/viewer": "^0.6.1",
50
- "@iyulab/u-board": "^0.36.0",
51
- "@iyulab/u-widgets": "^0.42.0"
50
+ "@iyulab/u-board": "^0.39.0",
51
+ "@iyulab/u-widgets": "^0.45.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@playwright/test": "^1.62.1",