flexdesk 0.4.7 → 0.6.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 (76) hide show
  1. package/README.md +1036 -301
  2. package/css/base.css +2958 -2
  3. package/css/flexdesk.css +2962 -4
  4. package/css/overrides.css +4 -2
  5. package/css/tokens.css +86 -0
  6. package/dist/canvas.js +390 -0
  7. package/dist/canvas.js.map +7 -0
  8. package/dist/charts.js +782 -782
  9. package/dist/{chunk-LH5TSOZW.js → chunk-6JSOVNID.js} +42 -2
  10. package/dist/{chunk-LH5TSOZW.js.map → chunk-6JSOVNID.js.map} +3 -3
  11. package/dist/{chunk-ELXVW542.js → chunk-6NG7YVBG.js} +3 -471
  12. package/dist/chunk-6NG7YVBG.js.map +7 -0
  13. package/dist/{chunk-F5FYDCOA.js → chunk-CNXP3SIN.js} +2373 -2373
  14. package/dist/{chunk-F5FYDCOA.js.map → chunk-CNXP3SIN.js.map} +7 -7
  15. package/dist/chunk-H4LT43RS.js +475 -0
  16. package/dist/chunk-H4LT43RS.js.map +7 -0
  17. package/dist/{chunk-WMGOJ3I6.js → chunk-KH2GJTKR.js} +3702 -2628
  18. package/dist/chunk-KH2GJTKR.js.map +7 -0
  19. package/dist/chunk-SN4LSOAR.js +532 -0
  20. package/dist/chunk-SN4LSOAR.js.map +7 -0
  21. package/dist/flexdesk.css +2962 -4
  22. package/dist/flow.js +10030 -0
  23. package/dist/flow.js.map +7 -0
  24. package/dist/tiles.js +2195 -2195
  25. package/dist/tokens.css +86 -0
  26. package/dist/widgets.js +5824 -5796
  27. package/dist/widgets.js.map +1 -1
  28. package/dist/wm.js +8077 -8074
  29. package/dist/wm.js.map +7 -7
  30. package/package.json +89 -81
  31. package/src/canvas/canvas_adapter.js +523 -0
  32. package/src/canvas/edge_router.js +392 -0
  33. package/src/canvas/graph/connector_router.js +189 -0
  34. package/src/canvas/graph/history_service.js +170 -0
  35. package/src/canvas/graph/node_platform.js +116 -0
  36. package/src/canvas/index.js +16 -0
  37. package/src/flow/canvas_editor/editor.js +1081 -0
  38. package/src/flow/canvas_editor/index.js +16 -0
  39. package/src/flow/canvas_editor/layout.js +118 -0
  40. package/src/flow/canvas_editor/strings.js +37 -0
  41. package/src/flow/kit/always_before.js +325 -0
  42. package/src/flow/kit/catalogue.js +170 -0
  43. package/src/flow/kit/chip_input.js +461 -0
  44. package/src/flow/kit/dom.js +205 -0
  45. package/src/flow/kit/findings.js +129 -0
  46. package/src/flow/kit/history.js +173 -0
  47. package/src/flow/kit/index.js +38 -0
  48. package/src/flow/kit/keys.js +127 -0
  49. package/src/flow/kit/logic_graph.js +210 -0
  50. package/src/flow/kit/references.js +199 -0
  51. package/src/flow/kit/settings_panel.js +357 -0
  52. package/src/flow/kit/settings_schema.js +175 -0
  53. package/src/flow/kit/step_picker.js +209 -0
  54. package/src/flow/kit/strings.js +102 -0
  55. package/src/flow/kit/value_picker.js +238 -0
  56. package/src/flow/kit/widgets.js +763 -0
  57. package/src/flow/lanes/editor.js +1469 -0
  58. package/src/flow/lanes/index.js +26 -0
  59. package/src/flow/lanes/layout.js +525 -0
  60. package/src/flow/lanes/pipeline.js +327 -0
  61. package/src/flow/lanes/preview.js +109 -0
  62. package/src/flow/lanes/strings.js +98 -0
  63. package/src/flow/outline/editor.js +1511 -0
  64. package/src/flow/outline/index.js +20 -0
  65. package/src/flow/outline/mapping.js +286 -0
  66. package/src/flow/outline/menu.js +154 -0
  67. package/src/flow/outline/operations.js +738 -0
  68. package/src/flow/outline/recognise.js +755 -0
  69. package/src/flow/outline/strings.js +135 -0
  70. package/src/tiling/landing_table.js +13 -5
  71. package/src/tiling/wm.js +3916 -3916
  72. package/src/ui/components/data_table.js +4205 -3294
  73. package/src/ui/components/row_activation.js +70 -0
  74. package/src/ui/components/table_values.js +423 -0
  75. package/dist/chunk-ELXVW542.js.map +0 -7
  76. package/dist/chunk-WMGOJ3I6.js.map +0 -7
@@ -0,0 +1,175 @@
1
+ /**
2
+ * THE SETTINGS-SCHEMA SUBSET a step type's `config_schema` may use (36 §3.2).
3
+ *
4
+ * The subset is the one the consumer's SERVER enforces — the keyword set, the
5
+ * type names and every refusal sentence are a port of a Python module
6
+ * (`check_schema` in Tables' workflow engine; the data-flow core's
7
+ * `settings_schema` ports the same one), and a fixture of schemas with both
8
+ * verdicts (`tests/fixtures/flow_kit_settings_schema.json`) is read by both
9
+ * sides so they cannot drift:
10
+ *
11
+ * type (one name or a list), enum, const, required, properties,
12
+ * additionalProperties (boolean only), items, minItems/maxItems,
13
+ * minimum/maximum, minLength/maxLength, format: "uuid";
14
+ * annotations title, description, default, examples; and any `x-` hint.
15
+ *
16
+ * `oneOf`, `anyOf`, `$ref` and `pattern` are refused for reasons that hold in a
17
+ * browser too: a regular expression is read three ways by three engines (`$`
18
+ * also matches before a final newline in Python and .NET, not in JavaScript),
19
+ * and a union is exactly the construct a generated form cannot draw honestly.
20
+ *
21
+ * THE KIT DRAWS FROM A SCHEMA AND NEVER VALIDATES A VALUE AGAINST ONE: whether
22
+ * a setting is valid is the server's answer, drawn as a finding.
23
+ *
24
+ * ══ SENTENCE FOR SENTENCE ══════════════════════════════════════════════
25
+ *
26
+ * `checkSettingsSchema` returns the FIRST refusal the Python raises, in the
27
+ * Python's words, or `null`. Two quirks are kept on purpose, because "the same
28
+ * sentence" is the contract and a tidier one here would be a drift there:
29
+ *
30
+ * - four refusals (`properties`, `additionalProperties`, `format`,
31
+ * `required`) name the path WITHOUT the `<root>` fallback, so at the root
32
+ * they read `": properties must be an object."`;
33
+ * - names are quoted the way Python's `repr` quotes them (`'oneOf'`, and
34
+ * `"it's"` for a name holding an apostrophe).
35
+ *
36
+ * One difference cannot be closed from here: a JavaScript object lists keys
37
+ * that look like integers ("1", "2") first, where a Python dict keeps the
38
+ * document's order. Which refusal is FIRST can then differ for a schema with
39
+ * integer-like property names; the fixture holds none.
40
+ */
41
+
42
+ export const SETTINGS_SCHEMA_ANNOTATIONS = Object.freeze(['title', 'description', 'default', 'examples']);
43
+ export const SETTINGS_SCHEMA_KEYWORDS = Object.freeze([
44
+ 'type', 'enum', 'const', 'required', 'properties', 'additionalProperties', 'items',
45
+ 'minItems', 'maxItems', 'minimum', 'maximum', 'minLength', 'maxLength', 'format',
46
+ ]);
47
+ export const SETTINGS_SCHEMA_TYPES = Object.freeze(['object', 'array', 'string', 'integer', 'number',
48
+ 'boolean', 'null']);
49
+
50
+ /** The `x-ui-` hints the kit reads (36 §3.2); the server ignores every `x-` key. */
51
+ export const UI_HINTS = Object.freeze(['x-ui-widget', 'x-ui-references', 'x-ui-when', 'x-ui-placeholder',
52
+ 'x-ui-enum-labels', 'x-ui-multiline', 'x-ui-fold']);
53
+
54
+ const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
55
+
56
+ /** Python's `repr` of the values a schema refusal quotes. */
57
+ export function pyRepr(value) {
58
+ if (value === null || value === undefined) return 'None';
59
+ if (value === true) return 'True';
60
+ if (value === false) return 'False';
61
+ if (typeof value === 'number') return String(value);
62
+ if (typeof value === 'string') {
63
+ const q = value.includes("'") && !value.includes('"') ? '"' : "'";
64
+ let out = '';
65
+ for (const ch of value) {
66
+ const code = ch.codePointAt(0);
67
+ if (ch === '\\') out += '\\\\';
68
+ else if (ch === q) out += `\\${q}`;
69
+ else if (ch === '\n') out += '\\n';
70
+ else if (ch === '\r') out += '\\r';
71
+ else if (ch === '\t') out += '\\t';
72
+ else if (code < 0x20 || code === 0x7f) out += `\\x${code.toString(16).padStart(2, '0')}`;
73
+ else out += ch;
74
+ }
75
+ return q + out + q;
76
+ }
77
+ return JSON.stringify(value);
78
+ }
79
+
80
+ /**
81
+ * The first way `schema` leaves the subset, as the server's sentence, or
82
+ * `null` when it is inside it.
83
+ */
84
+ export function checkSettingsSchema(schema, path = '') {
85
+ const where = path || '<root>';
86
+ if (!isPlainObject(schema)) return `${where}: a schema must be an object.`;
87
+ for (const [key, value] of Object.entries(schema)) {
88
+ if (SETTINGS_SCHEMA_ANNOTATIONS.includes(key) || key.startsWith('x-')) continue;
89
+ if (!SETTINGS_SCHEMA_KEYWORDS.includes(key)) return `${where}: the keyword ${pyRepr(key)} is not supported.`;
90
+ if (key === 'type') {
91
+ for (const n of (Array.isArray(value) ? value : [value])) {
92
+ if (!SETTINGS_SCHEMA_TYPES.includes(n)) return `${where}: unknown type ${pyRepr(n)}.`;
93
+ }
94
+ } else if (key === 'properties') {
95
+ if (!isPlainObject(value)) return `${path}: properties must be an object.`;
96
+ for (const [name, sub] of Object.entries(value)) {
97
+ const refused = checkSettingsSchema(sub, path ? `${path}.${name}` : name);
98
+ if (refused) return refused;
99
+ }
100
+ } else if (key === 'items') {
101
+ const refused = checkSettingsSchema(value, `${path}[]`);
102
+ if (refused) return refused;
103
+ } else if (key === 'additionalProperties' && typeof value !== 'boolean') {
104
+ return `${path}: additionalProperties must be true or false.`;
105
+ } else if (key === 'format' && value !== 'uuid') {
106
+ return `${path}: only format 'uuid' is supported.`;
107
+ } else if (key === 'required' && !(Array.isArray(value) && value.every((v) => typeof v === 'string'))) {
108
+ return `${path}: required must be a list of names.`;
109
+ }
110
+ }
111
+ return null;
112
+ }
113
+
114
+ /** The settings a schema declares, in the schema's order: `[{key, spec, required}]`. */
115
+ export function fieldsFromSchema(schema) {
116
+ const props = isPlainObject(schema?.properties) ? schema.properties : {};
117
+ const required = new Set(Array.isArray(schema?.required) ? schema.required : []);
118
+ return Object.entries(props).map(([key, spec]) => ({
119
+ key, spec: isPlainObject(spec) ? spec : {}, required: required.has(key),
120
+ }));
121
+ }
122
+
123
+ /** A setting's label: its `title`, else its key in words (`max_attempts` → "Max attempts"). */
124
+ export function titleOf(key, spec = {}) {
125
+ if (spec?.title) return String(spec.title);
126
+ const words = String(key).replace(/_/g, ' ');
127
+ return words.charAt(0).toUpperCase() + words.slice(1);
128
+ }
129
+
130
+ /** The first JSON-Schema type a spec names (a list's first), or undefined. */
131
+ export function primaryType(spec) {
132
+ return Array.isArray(spec?.type) ? spec.type[0] : spec?.type;
133
+ }
134
+
135
+ /** Every type a spec admits, as a list. */
136
+ export function admittedTypes(spec) {
137
+ return [].concat(spec?.type ?? []);
138
+ }
139
+
140
+ /**
141
+ * Is `field` drawn while the step's settings are `value`? Its `x-ui-when`
142
+ * (`{field, in}`) names a sibling; the sibling's stored value — or, absent,
143
+ * its schema `default`, which is what the server will apply — must be one of
144
+ * `in`. A hint that names no sibling, or is malformed, never hides a field: a
145
+ * setting nobody can reach is worse than one drawn too often.
146
+ */
147
+ export function fieldVisible(field, value, fields = []) {
148
+ const when = field?.spec?.['x-ui-when'];
149
+ if (!isPlainObject(when) || typeof when.field !== 'string' || !Array.isArray(when.in)) return true;
150
+ const sibling = fields.find((f) => f.key === when.field);
151
+ const stored = value && typeof value === 'object' ? value[when.field] : undefined;
152
+ const current = stored !== undefined ? stored : sibling?.spec?.default;
153
+ return when.in.some((v) => v === current);
154
+ }
155
+
156
+ /** The fields drawn for `value`, honouring every `x-ui-when`. */
157
+ export function visibleFields(fields, value) {
158
+ return fields.filter((f) => fieldVisible(f, value, fields));
159
+ }
160
+
161
+ /** The keys some field's `x-ui-when` reads — a change to one of them repaints the panel. */
162
+ export function whenKeys(fields) {
163
+ const keys = new Set();
164
+ for (const f of fields) {
165
+ const when = f?.spec?.['x-ui-when'];
166
+ if (isPlainObject(when) && typeof when.field === 'string') keys.add(when.field);
167
+ }
168
+ return keys;
169
+ }
170
+
171
+ /** A spec's `x-ui-enum-labels` word for `v`, or `v` as text. */
172
+ export function enumLabel(spec, v) {
173
+ const words = spec?.['x-ui-enum-labels'];
174
+ return isPlainObject(words) && words[v] !== undefined ? String(words[v]) : String(v);
175
+ }
@@ -0,0 +1,209 @@
1
+ /**
2
+ * THE STEP PICKER — what "+" opens (36 §3.5).
3
+ *
4
+ * const picked = await openStepPicker({anchor, entries, categories, where, paste, strings});
5
+ * // → {entry} | {paste: true} | null
6
+ *
7
+ * Grouped by category, searchable over each entry's label, sub, description
8
+ * and id (the box has focus on open); ↑ ↓ choose, stepping over a refused row;
9
+ * Enter adds; Esc closes; *"Paste a copied step · Ctrl+V"* is its footer when
10
+ * `paste` is set — and Ctrl+V in the EMPTY search box pastes the step (with
11
+ * text in the box it pastes text, as a search box should).
12
+ *
13
+ * An entry with a `refusal` is drawn greyed WITH its sentence under it and
14
+ * cannot be chosen — never hidden, because "why can I not add it here" is the
15
+ * question the next screen would otherwise have to answer.
16
+ *
17
+ * ENTRIES ARE THE EDITOR'S, NOT THE CATALOGUE'S: an outline offers blocks
18
+ * ("If … otherwise" is a condition and its two arms) beside plain steps, and
19
+ * lanes offer sources only after "Add a source". The picker draws what it is
20
+ * handed.
21
+ *
22
+ * It is a FlexDesk popover (`dom.js`): `role="dialog"` holding a `listbox`
23
+ * whose active option is the search box's `aria-activedescendant`; it returns
24
+ * focus to its anchor when it closes. The promise carries `close()`.
25
+ */
26
+
27
+ import { el, icon, nextOption, openPopover, toneClass, uid } from './dom.js';
28
+ import { createStrings, say } from './strings.js';
29
+
30
+ const words = (s) => String(s ?? '').toLowerCase();
31
+
32
+ /**
33
+ * @param {object} o
34
+ * @param {HTMLElement} o.anchor
35
+ * @param {{id: string, label: string, sub?: string, description?: string, icon?: string, tone?: string,
36
+ * category?: string, refusal?: string}[]} o.entries
37
+ * @param {{id: string, label: string, layout?: 'list'|'grid'}[]} [o.categories] groups, in order
38
+ * @param {string} [o.where] "inside For each row, after Check eligibility"
39
+ * @param {{label?: string}|null} [o.paste] a copied step can be pasted here
40
+ * @param {object} [o.strings]
41
+ * @param {string} [o.title] the heading; `Add a step` by default
42
+ */
43
+ export function openStepPicker({ anchor, entries = [], categories = [], where = '', paste = null, strings = null,
44
+ title = null } = {}) {
45
+ const S = createStrings(strings);
46
+ let settle;
47
+ const done = new Promise((resolve) => { settle = resolve; });
48
+ let result = null;
49
+
50
+ const root = el('div', 'twm-flow-picker');
51
+ const headingId = uid('picker-title');
52
+ const head = el('div', 'twm-flow-picker__head');
53
+ const heading = el('span', 'twm-flow-picker__title', title ?? say(S, 'addStep'));
54
+ heading.id = headingId;
55
+ head.appendChild(heading);
56
+ if (where) head.appendChild(el('span', 'twm-flow-picker__where', where));
57
+ root.appendChild(head);
58
+
59
+ const listId = uid('picker-list');
60
+ const searchRow = el('div', 'twm-flow-picker__search');
61
+ searchRow.appendChild(icon('search', 'twm-flow-picker__search-icon'));
62
+ const search = el('input', 'twm-flow-picker__input');
63
+ search.type = 'search';
64
+ search.placeholder = say(S, 'searchSteps');
65
+ search.setAttribute('aria-label', say(S, 'searchSteps'));
66
+ search.setAttribute('role', 'combobox');
67
+ search.setAttribute('aria-controls', listId);
68
+ search.setAttribute('aria-expanded', 'true');
69
+ search.setAttribute('aria-autocomplete', 'list');
70
+ search.autocomplete = 'off';
71
+ searchRow.appendChild(search);
72
+ root.appendChild(searchRow);
73
+
74
+ const list = el('div', 'twm-flow-picker__list');
75
+ list.id = listId;
76
+ list.setAttribute('role', 'listbox');
77
+ list.setAttribute('aria-labelledby', headingId);
78
+ root.appendChild(list);
79
+ const empty = el('p', 'twm-flow-picker__empty');
80
+ empty.hidden = true;
81
+ root.appendChild(empty);
82
+
83
+ // Groups in the categories' order; an entry whose category is not listed
84
+ // goes in a group of its own after them.
85
+ const order = (Array.isArray(categories) ? categories : []).map((c) => ({ ...c }));
86
+ for (const e of entries) {
87
+ const cat = e.category ?? '';
88
+ if (!order.some((c) => String(c.id) === String(cat))) order.push({ id: cat, label: cat, layout: 'list' });
89
+ }
90
+ const options = [];
91
+ for (const cat of order) {
92
+ const mine = entries.filter((e) => String(e.category ?? '') === String(cat.id));
93
+ if (!mine.length) continue;
94
+ const group = el('div', 'twm-flow-picker__group');
95
+ group.setAttribute('role', 'group');
96
+ const gid = uid('picker-group');
97
+ const glabel = el('div', 'twm-flow-picker__group-label', cat.label ?? '');
98
+ glabel.id = gid;
99
+ group.setAttribute('aria-labelledby', gid);
100
+ const items = el('div', `twm-flow-picker__items${cat.layout === 'grid' ? ' twm-flow-picker__items--grid' : ''}`);
101
+ for (const entry of mine) {
102
+ const opt = el('div', `twm-flow-picker__option${entry.refusal ? ' twm-flow-picker__option--refused' : ''}`);
103
+ opt.id = uid('picker-option');
104
+ opt.setAttribute('role', 'option');
105
+ opt.setAttribute('aria-selected', 'false');
106
+ if (entry.refusal) opt.setAttribute('aria-disabled', 'true');
107
+ opt.dataset.entry = String(entry.id);
108
+ const chip = el('span', `twm-flow-picker__chip ${toneClass('twm-flow-picker__chip', entry.tone)}`);
109
+ chip.appendChild(icon(entry.icon || ''));
110
+ chip.setAttribute('aria-hidden', 'true');
111
+ const text = el('span', 'twm-flow-picker__words');
112
+ const line = el('span', 'twm-flow-picker__label', entry.label ?? entry.id);
113
+ if (entry.sub) line.appendChild(el('span', 'twm-flow-picker__sub', ` ${entry.sub}`));
114
+ text.appendChild(line);
115
+ if (entry.description && cat.layout !== 'grid') {
116
+ text.appendChild(el('span', 'twm-flow-picker__description', entry.description));
117
+ }
118
+ if (entry.refusal) text.appendChild(el('span', 'twm-flow-picker__refusal', entry.refusal));
119
+ opt.append(chip, text);
120
+ // The search box keeps focus: an option is chosen by a click, not focused by a press.
121
+ opt.addEventListener('mousedown', (ev) => ev.preventDefault());
122
+ opt.addEventListener('click', () => {
123
+ if (entry.refusal) return;
124
+ choose({ entry });
125
+ });
126
+ opt.addEventListener('mousemove', () => { if (!entry.refusal) activate(opt); });
127
+ options.push({ opt, entry, group, haystack: [entry.label, entry.sub, entry.description, entry.id].map(words).join(' ') });
128
+ items.appendChild(opt);
129
+ }
130
+ group.append(glabel, items);
131
+ list.appendChild(group);
132
+ }
133
+
134
+ const foot = el('div', 'twm-flow-picker__foot');
135
+ foot.appendChild(el('span', 'twm-flow-picker__keys', say(S, 'pickerKeys')));
136
+ if (paste) {
137
+ const p = el('button', 'twm-flow-picker__paste', paste.label ?? say(S, 'pasteStep'));
138
+ p.type = 'button';
139
+ p.addEventListener('click', () => choose({ paste: true }));
140
+ foot.appendChild(p);
141
+ }
142
+ root.appendChild(foot);
143
+
144
+ let active = null;
145
+ function activate(opt) {
146
+ if (active === opt) return;
147
+ active?.setAttribute('aria-selected', 'false');
148
+ active?.classList.remove('twm-flow-picker__option--active');
149
+ active = opt;
150
+ if (opt) {
151
+ opt.setAttribute('aria-selected', 'true');
152
+ opt.classList.add('twm-flow-picker__option--active');
153
+ search.setAttribute('aria-activedescendant', opt.id);
154
+ opt.scrollIntoView?.({ block: 'nearest' });
155
+ } else {
156
+ search.removeAttribute('aria-activedescendant');
157
+ }
158
+ }
159
+
160
+ function filter() {
161
+ const terms = words(search.value).split(/\s+/).filter(Boolean);
162
+ let shown = 0;
163
+ for (const o of options) {
164
+ const hit = terms.every((t) => o.haystack.includes(t));
165
+ o.opt.hidden = !hit;
166
+ if (hit) shown += 1;
167
+ }
168
+ for (const g of list.querySelectorAll('.twm-flow-picker__group')) {
169
+ g.hidden = ![...g.querySelectorAll('[role="option"]')].some((o) => !o.hidden);
170
+ }
171
+ empty.hidden = shown > 0;
172
+ empty.textContent = shown > 0 ? '' : say(S, 'nothingMatches', search.value.trim());
173
+ if (!active || active.hidden) activate(nextOption(list, null, 1));
174
+ }
175
+
176
+ function choose(value) {
177
+ result = value;
178
+ handle.close('chosen');
179
+ }
180
+
181
+ search.addEventListener('input', filter);
182
+ search.addEventListener('keydown', (ev) => {
183
+ if (ev.isComposing) return;
184
+ if (ev.key === 'ArrowDown' || ev.key === 'ArrowUp') {
185
+ ev.preventDefault();
186
+ activate(nextOption(list, active, ev.key === 'ArrowDown' ? 1 : -1) || active);
187
+ } else if (ev.key === 'Enter') {
188
+ ev.preventDefault();
189
+ const hit = options.find((o) => o.opt === active);
190
+ if (hit && !hit.entry.refusal) choose({ entry: hit.entry });
191
+ } else if ((ev.ctrlKey || ev.metaKey) && String(ev.key).toLowerCase() === 'v' && paste && !search.value) {
192
+ ev.preventDefault();
193
+ choose({ paste: true });
194
+ }
195
+ });
196
+ root.addEventListener('focusout', (ev) => {
197
+ const to = ev.relatedTarget;
198
+ if (to && !root.contains(to) && to !== anchor) handle.close('blur');
199
+ });
200
+
201
+ const handle = openPopover({
202
+ anchor, content: root, label: title ?? say(S, 'addStep'), className: 'twm-flow-popover--picker',
203
+ focus: search, onClose: () => settle(result),
204
+ });
205
+ if (handle.toggled) settle(null);
206
+ else filter();
207
+ done.close = () => handle.close('closed');
208
+ return done;
209
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * EVERY WORD A FLOW EDITOR DRAWS (36 §3.13).
3
+ *
4
+ * The defaults are English and name no domain: a step, a source, a value. What
5
+ * a consumer calls its parts — what an error port means, what a loop's foot
6
+ * says — comes from the consumer's `strings`, or from an editor's block
7
+ * mapping, and is never guessed here.
8
+ *
9
+ * A string is text, or a function of its arguments for the ones that count or
10
+ * quote something (`stepsInside(3)`). `createStrings(...layers)` lays the
11
+ * layers over the defaults left to right, so an editor lays its own defaults
12
+ * over the kit's and the consumer's over both:
13
+ *
14
+ * const strings = createStrings(OUTLINE_STRINGS, options.strings);
15
+ *
16
+ * `say(strings, key, ...args)` reads one either way, and falls back to the key
17
+ * itself — a missing string shows as its name on the screen, which a reader
18
+ * reports, rather than as an empty label nobody notices.
19
+ */
20
+
21
+ const plural = (n, one, many) => (n === 1 ? one : many.replace('{n}', String(n)));
22
+
23
+ export const FLOW_STRINGS = Object.freeze({
24
+ // ── the words every editor shares ──────────────────────────────────
25
+ step: 'step',
26
+ steps: 'steps',
27
+ addStep: 'Add a step',
28
+ removeStep: 'Remove step',
29
+ duplicate: 'Duplicate',
30
+ rename: 'Rename',
31
+ undo: 'Undo',
32
+ redo: 'Redo',
33
+ undoTitle: 'Undo (Ctrl+Z)',
34
+ redoTitle: 'Redo (Ctrl+Y)',
35
+ foldAll: 'Fold all',
36
+ unfoldAll: 'Unfold all',
37
+ stepsInside: (n) => plural(n, '1 step inside', '{n} steps inside'),
38
+ stepsShow: 'Steps show',
39
+ thePreview: 'The preview',
40
+ theLastRun: 'The last run',
41
+ addSource: 'Add a source — starts a new lane',
42
+ settings: 'Settings',
43
+ readOnly: 'Read only',
44
+
45
+ // ── the settings panel and its fields ──────────────────────────────
46
+ required: ' *',
47
+ requiredLabel: 'required',
48
+ choose: 'Choose…',
49
+ defaultOption: (v) => `Default (${v})`,
50
+ notFound: (v) => `${v} (not found)`,
51
+ noControl: 'This setting has no control in this editor; its value is kept as it is.',
52
+ unknownWidget: (name) => `This setting needs the "${name}" control, which this editor does not have. `
53
+ + 'Its value is kept as it is.',
54
+ foldDefaults: 'Defaults',
55
+ add: 'Add',
56
+ remove: 'Remove',
57
+ removeNamed: (what) => `Remove ${what}`,
58
+ addAssignment: 'Add assignment',
59
+ name: 'Name',
60
+ value: 'Value',
61
+ formula: 'Formula',
62
+ textOrValue: 'Text, or insert a value',
63
+ noColumns: 'There are no columns to choose from yet: connect a step before this one.',
64
+ noColumnsChosen: 'None chosen',
65
+ fieldFinding: 'Fix this before publishing',
66
+
67
+ // ── reference chips and Insert a value ─────────────────────────────
68
+ insertValue: 'Insert a value',
69
+ insertValueGlyph: '{ }',
70
+ searchValues: 'Search values',
71
+ wholeValue: (label) => `All of ${label}`,
72
+ back: 'Back',
73
+ insertsLine: (ref) => `Inserts ${ref} — shown as a chip, kept as text.`,
74
+ noValues: 'Nothing to insert here yet.',
75
+ valueKeys: '↑ ↓ choose · Enter inserts · Esc closes',
76
+
77
+ // ── the step picker ────────────────────────────────────────────────
78
+ searchSteps: 'Search steps',
79
+ pickerKeys: '↑ ↓ choose · Enter adds · Esc closes',
80
+ pasteStep: 'Paste a copied step · Ctrl+V',
81
+ nothingMatches: (q) => `Nothing matches “${q}”.`,
82
+
83
+ // ── findings ───────────────────────────────────────────────────────
84
+ toFix: (n) => plural(n, '1 thing to fix before this can be published.',
85
+ '{n} things to fix before this can be published.'),
86
+ toLookAt: (n) => plural(n, '1 thing to look at.', '{n} things to look at.'),
87
+ goToIt: 'Go to it',
88
+ theFlow: 'The flow',
89
+ findingLine: (where, message) => (where ? `${where} — ${message}` : message),
90
+ });
91
+
92
+ /** The defaults with each layer laid over them, left to right; frozen. */
93
+ export function createStrings(...layers) {
94
+ return Object.freeze(Object.assign({}, FLOW_STRINGS, ...layers.filter((l) => l && typeof l === 'object')));
95
+ }
96
+
97
+ /** One string: text as it is, a function called with `args`, the key when absent. */
98
+ export function say(strings, key, ...args) {
99
+ const s = strings?.[key] ?? FLOW_STRINGS[key];
100
+ if (typeof s === 'function') return s(...args);
101
+ return s === undefined || s === null ? String(key) : String(s);
102
+ }