@wildwinter/app-shell 0.56.0 → 0.57.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.
@@ -50,6 +50,41 @@ declare function foldCaption(name: string, set: number): string;
50
50
  */
51
51
  declare function extensionFold(): ExtensionFold;
52
52
 
53
+ /** A map whose values are records: edited in the grid, summarised in the inspector. */
54
+ declare function isRecordMap(decl: ValueDecl): boolean;
55
+ /** A map key's name: a node's from the host, else the key itself. */
56
+ declare function recordMapKeyLabel(decl: ValueDecl, key: string, adapter?: ValueAdapter): string;
57
+ /** A value as words, the way its control shows it: a pick's or a reference's name, flags joined, a boolean as
58
+ * the field's own name. Empty for a value that isn't set. */
59
+ declare function valueText(decl: ValueDecl, value: unknown, adapter?: ValueAdapter): string;
60
+ /** The inspector's summary of a map of records: a line per key, "TAM: smile, nod, WREN", the record's set
61
+ * fields in the manifest's order; a key with nothing set is its name alone. */
62
+ declare function recordMapSummary(decl: ValueDecl, value: unknown, adapter?: ValueAdapter): string[];
63
+ interface RecordMapGridOptions {
64
+ /** The map's declaration: `isRecordMap`. */
65
+ decl: ValueDecl;
66
+ /** The node's value, or undefined where it holds none. */
67
+ value: unknown;
68
+ /** The host's nodes, for a reference key and the cells' picks and references. */
69
+ adapter?: ValueAdapter;
70
+ /** Commit the whole map; undefined clears it. Absent: read-only. */
71
+ onCommit?: (value: unknown) => void;
72
+ disabled?: boolean;
73
+ /** The class on each cell's control, as the host's own rows carry. */
74
+ controlClassName?: string;
75
+ }
76
+ interface RecordMapGrid {
77
+ readonly el: HTMLElement;
78
+ update(opts: RecordMapGridOptions): void;
79
+ }
80
+ /**
81
+ * The grid: a row per key, a column per record field, each cell the row value-rows draws for that field (so
82
+ * picks, their checks and their problems are the same as in the inspector), a remove button per row, and the
83
+ * key adder under it. Keeps its cells across `update`, so a host that redraws on every change keeps the
84
+ * author's place.
85
+ */
86
+ declare function recordMapGrid(initial: RecordMapGridOptions): RecordMapGrid;
87
+
53
88
  /** A declaration of any type, as an extension's manifest writes it. */
54
89
  interface ValueDecl extends FieldDeclLike {
55
90
  /** The caption; the name when absent. */
@@ -135,4 +170,4 @@ interface ValueRows {
135
170
  }
136
171
  declare function valueRows(): ValueRows;
137
172
 
138
- export { type ExtensionFold, type FoldSectionOptions, type ValueAdapter, type ValueDecl, type ValueNode, type ValueRowHandle, type ValueRowOptions, type ValueRows, extensionFold, foldCaption, isSet, valueRow, valueRows };
173
+ export { type ExtensionFold, type FoldSectionOptions, type RecordMapGrid, type RecordMapGridOptions, type ValueAdapter, type ValueDecl, type ValueNode, type ValueRowHandle, type ValueRowOptions, type ValueRows, extensionFold, foldCaption, isRecordMap, isSet, recordMapGrid, recordMapKeyLabel, recordMapSummary, valueRow, valueRows, valueText };
@@ -50,6 +50,41 @@ declare function foldCaption(name: string, set: number): string;
50
50
  */
51
51
  declare function extensionFold(): ExtensionFold;
52
52
 
53
+ /** A map whose values are records: edited in the grid, summarised in the inspector. */
54
+ declare function isRecordMap(decl: ValueDecl): boolean;
55
+ /** A map key's name: a node's from the host, else the key itself. */
56
+ declare function recordMapKeyLabel(decl: ValueDecl, key: string, adapter?: ValueAdapter): string;
57
+ /** A value as words, the way its control shows it: a pick's or a reference's name, flags joined, a boolean as
58
+ * the field's own name. Empty for a value that isn't set. */
59
+ declare function valueText(decl: ValueDecl, value: unknown, adapter?: ValueAdapter): string;
60
+ /** The inspector's summary of a map of records: a line per key, "TAM: smile, nod, WREN", the record's set
61
+ * fields in the manifest's order; a key with nothing set is its name alone. */
62
+ declare function recordMapSummary(decl: ValueDecl, value: unknown, adapter?: ValueAdapter): string[];
63
+ interface RecordMapGridOptions {
64
+ /** The map's declaration: `isRecordMap`. */
65
+ decl: ValueDecl;
66
+ /** The node's value, or undefined where it holds none. */
67
+ value: unknown;
68
+ /** The host's nodes, for a reference key and the cells' picks and references. */
69
+ adapter?: ValueAdapter;
70
+ /** Commit the whole map; undefined clears it. Absent: read-only. */
71
+ onCommit?: (value: unknown) => void;
72
+ disabled?: boolean;
73
+ /** The class on each cell's control, as the host's own rows carry. */
74
+ controlClassName?: string;
75
+ }
76
+ interface RecordMapGrid {
77
+ readonly el: HTMLElement;
78
+ update(opts: RecordMapGridOptions): void;
79
+ }
80
+ /**
81
+ * The grid: a row per key, a column per record field, each cell the row value-rows draws for that field (so
82
+ * picks, their checks and their problems are the same as in the inspector), a remove button per row, and the
83
+ * key adder under it. Keeps its cells across `update`, so a host that redraws on every change keeps the
84
+ * author's place.
85
+ */
86
+ declare function recordMapGrid(initial: RecordMapGridOptions): RecordMapGrid;
87
+
53
88
  /** A declaration of any type, as an extension's manifest writes it. */
54
89
  interface ValueDecl extends FieldDeclLike {
55
90
  /** The caption; the name when absent. */
@@ -135,4 +170,4 @@ interface ValueRows {
135
170
  }
136
171
  declare function valueRows(): ValueRows;
137
172
 
138
- export { type ExtensionFold, type FoldSectionOptions, type ValueAdapter, type ValueDecl, type ValueNode, type ValueRowHandle, type ValueRowOptions, type ValueRows, extensionFold, foldCaption, isSet, valueRow, valueRows };
173
+ export { type ExtensionFold, type FoldSectionOptions, type RecordMapGrid, type RecordMapGridOptions, type ValueAdapter, type ValueDecl, type ValueNode, type ValueRowHandle, type ValueRowOptions, type ValueRows, extensionFold, foldCaption, isRecordMap, isSet, recordMapGrid, recordMapKeyLabel, recordMapSummary, valueRow, valueRows, valueText };
@@ -1,5 +1,5 @@
1
1
  import { fieldRow, fieldRowFocus } from './chunk-XJM733E6.js';
2
- import { el, iconNode, iconBtn, moveItem } from './chunk-W4WC4FO5.js';
2
+ import { el, iconBtn, iconNode, moveItem } from './chunk-W4WC4FO5.js';
3
3
  import { stableStringify, defaultOf, pickChoices, valueProblems } from '@wildwinter/toolkit/extensions';
4
4
 
5
5
  var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
@@ -78,13 +78,198 @@ function extensionFold() {
78
78
  };
79
79
  return fold;
80
80
  }
81
+ var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
82
+ function isRecordMap(decl) {
83
+ return decl.type === "map" && decl.of?.type === "record";
84
+ }
85
+ function recordMapKeyLabel(decl, key, adapter) {
86
+ const kind = decl.key?.type === "reference" ? decl.key.to ?? "" : void 0;
87
+ if (kind === void 0 || !adapter) return key;
88
+ return adapter.nodes(kind).find((n) => n.id === key)?.label ?? adapter.label?.(kind, key) ?? key;
89
+ }
90
+ function valueText(decl, value, adapter) {
91
+ if (!isSet(decl, value)) return "";
92
+ if (decl.type === "pick" && typeof value === "string") {
93
+ const choice = pickChoices(decl, {
94
+ ...adapter ? { nodes: (kind) => adapter.nodes(kind) } : {},
95
+ ...adapter?.list ? { list: (name) => adapter.list(name) } : {}
96
+ }).find((c) => c.value === value);
97
+ return choice?.label ?? value;
98
+ }
99
+ if (decl.type === "reference" && typeof value === "string" && decl.to !== void 0 && adapter) {
100
+ return adapter.nodes(decl.to).find((n) => n.id === value)?.label ?? adapter.label?.(decl.to, value) ?? value;
101
+ }
102
+ if (decl.type === "boolean") return value === true ? decl.label ?? decl.name : "";
103
+ if (Array.isArray(value)) return value.map(String).join(", ");
104
+ if (isObject2(value)) return JSON.stringify(value);
105
+ return String(value);
106
+ }
107
+ function recordMapSummary(decl, value, adapter) {
108
+ if (!isObject2(value)) return [];
109
+ const fields = decl.of?.fields ?? [];
110
+ return Object.entries(value).map(([key, record]) => {
111
+ const name = recordMapKeyLabel(decl, key, adapter);
112
+ const words = isObject2(record) ? fields.map((f) => valueText(f, record[f.name], adapter)).filter((w) => w !== "") : [];
113
+ return words.length > 0 ? `${name}: ${words.join(", ")}` : name;
114
+ });
115
+ }
116
+ function recordMapGrid(initial) {
117
+ let opts = initial;
118
+ const root = el("div", "shell-recordgrid");
119
+ const table = el("table", "shell-recordgrid-table");
120
+ const head = el("thead");
121
+ const body = el("tbody");
122
+ table.append(head, body);
123
+ const adder = el("div", "shell-recordgrid-add");
124
+ const empty = el("p", "shell-recordgrid-empty");
125
+ root.append(table, empty, adder);
126
+ const cells = /* @__PURE__ */ new Map();
127
+ let addKey = "";
128
+ const editable = () => opts.onCommit !== void 0 && opts.disabled !== true;
129
+ const held = () => isObject2(opts.value) ? opts.value : {};
130
+ const commit = (next) => {
131
+ const v = Object.keys(next).length > 0 ? next : void 0;
132
+ opts = { ...opts, value: v };
133
+ draw();
134
+ opts.onCommit?.(v);
135
+ };
136
+ const cell = (key, field, record) => {
137
+ const id = `${key}\0${field.name}`;
138
+ const o = {
139
+ decl: field,
140
+ value: record[field.name],
141
+ ...opts.adapter ? { adapter: opts.adapter } : {},
142
+ ...opts.disabled !== void 0 ? { disabled: opts.disabled } : {},
143
+ ...opts.controlClassName !== void 0 ? { controlClassName: opts.controlClassName } : {},
144
+ ...editable() ? {
145
+ onCommit: (v) => {
146
+ const map = held();
147
+ const rec = { ...isObject2(map[key]) ? map[key] : {} };
148
+ if (v === void 0) delete rec[field.name];
149
+ else rec[field.name] = v;
150
+ commit({ ...map, [key]: rec });
151
+ }
152
+ } : {},
153
+ className: "shell-recordgrid-cell"
154
+ };
155
+ const had = cells.get(id);
156
+ if (had && had.el.dataset["type"] === field.type) {
157
+ fieldRowFocus.settle(had.el);
158
+ had.update(o);
159
+ fieldRowFocus.restore(had.el);
160
+ return had.el;
161
+ }
162
+ const handle = valueRow(o);
163
+ handle.el.dataset["type"] = field.type;
164
+ cells.set(id, handle);
165
+ return handle.el;
166
+ };
167
+ const draw = () => {
168
+ const decl = opts.decl;
169
+ const fields = decl.of?.fields ?? [];
170
+ const map = held();
171
+ const keys = Object.keys(map);
172
+ const adapter = opts.adapter;
173
+ const keyKind = decl.key?.type === "reference" ? decl.key.to ?? "" : void 0;
174
+ const keyNodes = keyKind !== void 0 && adapter ? adapter.nodes(keyKind) : void 0;
175
+ const hr = el("tr");
176
+ hr.append(el("th", "shell-recordgrid-keyhead", recordMapKeyHeading(decl)));
177
+ for (const f of fields) {
178
+ const th = el("th", void 0, f.label ?? f.name);
179
+ const help = f.help ?? f.purpose;
180
+ if (help) th.dataset["tip"] = help;
181
+ hr.append(th);
182
+ }
183
+ hr.append(el("th", "shell-recordgrid-tools"));
184
+ head.replaceChildren(hr);
185
+ const used = /* @__PURE__ */ new Set();
186
+ const rows = [];
187
+ for (const key of keys) {
188
+ const record = isObject2(map[key]) ? map[key] : {};
189
+ const tr = el("tr");
190
+ tr.dataset["key"] = key;
191
+ const name = recordMapKeyLabel(decl, key, adapter);
192
+ const keyCell = el("th", "shell-recordgrid-key", name);
193
+ keyCell.setAttribute("scope", "row");
194
+ if (keyNodes !== void 0 && adapter && keyNodes.every((n) => n.id !== key) && adapter.label?.(keyKind, key) === void 0) {
195
+ keyCell.append(el("span", "shell-recordgrid-gone", " (not here any more)"));
196
+ }
197
+ tr.append(keyCell);
198
+ for (const f of fields) {
199
+ used.add(`${key}\0${f.name}`);
200
+ const td = el("td");
201
+ td.append(cell(key, f, record));
202
+ tr.append(td);
203
+ }
204
+ const tools = el("td", "shell-recordgrid-tools");
205
+ if (editable()) tools.append(iconBtn("close", `Remove ${name}`, () => {
206
+ const next = { ...held() };
207
+ delete next[key];
208
+ commit(next);
209
+ }));
210
+ tr.append(tools);
211
+ rows.push(tr);
212
+ }
213
+ for (const id of [...cells.keys()]) if (!used.has(id)) cells.delete(id);
214
+ body.replaceChildren(...rows);
215
+ table.hidden = keys.length === 0;
216
+ empty.textContent = keys.length === 0 ? "None yet." : "";
217
+ empty.hidden = keys.length !== 0;
218
+ adder.replaceChildren();
219
+ if (!editable()) return;
220
+ const add = (k) => {
221
+ if (k === "" || Object.prototype.hasOwnProperty.call(held(), k)) return;
222
+ commit({ ...held(), [k]: defaultOf(decl.of) });
223
+ };
224
+ if (keyNodes !== void 0) {
225
+ const pick = el("select", `field shell-valuerow-key${opts.controlClassName ? ` ${opts.controlClassName}` : ""}`);
226
+ const first = el("option", void 0, "Add...");
227
+ first.value = "";
228
+ pick.append(first);
229
+ for (const n of keyNodes.filter((n2) => !Object.prototype.hasOwnProperty.call(map, n2.id))) {
230
+ const opt = el("option", void 0, n.label);
231
+ opt.value = n.id;
232
+ pick.append(opt);
233
+ }
234
+ pick.disabled = pick.options.length === 1;
235
+ pick.addEventListener("change", () => add(pick.value));
236
+ adder.append(pick);
237
+ } else {
238
+ const input = el("input", "field shell-valuerow-key");
239
+ input.type = "text";
240
+ input.placeholder = "New key";
241
+ input.value = addKey;
242
+ input.addEventListener("input", () => {
243
+ addKey = input.value;
244
+ });
245
+ const go = () => {
246
+ const k = input.value.trim();
247
+ addKey = "";
248
+ add(k);
249
+ };
250
+ input.addEventListener("keydown", (e) => {
251
+ if (e.key === "Enter") go();
252
+ });
253
+ adder.append(input, iconBtn("add", "Add", go));
254
+ }
255
+ };
256
+ draw();
257
+ return { el: root, update: (o) => {
258
+ opts = o;
259
+ draw();
260
+ } };
261
+ }
262
+ function recordMapKeyHeading(decl) {
263
+ const to = decl.key?.type === "reference" ? decl.key.to : void 0;
264
+ return to ? to.charAt(0).toUpperCase() + to.slice(1) : "Key";
265
+ }
81
266
 
82
267
  // src/value-rows.ts
83
268
  function nodeLabel(adapter, kind, id) {
84
269
  return adapter.nodes(kind).find((n) => n.id === id)?.label ?? adapter.label?.(kind, id);
85
270
  }
86
271
  var SCALARS = /* @__PURE__ */ new Set(["text", "multiline", "number", "boolean", "enum", "flags", "quality", "string"]);
87
- var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
272
+ var isObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
88
273
  var nameOf = (decl) => decl.label ?? decl.name;
89
274
  var asDecl = (d, name) => ({ ...d, name: d.name ?? name });
90
275
  function ownProblems(decl, value, adapter) {
@@ -323,31 +508,31 @@ function structuredRow(initial) {
323
508
  }, controls));
324
509
  };
325
510
  if (decl.type === "record") {
326
- const value = isObject2(o.value) ? o.value : void 0;
511
+ const value = isObject3(o.value) ? o.value : void 0;
327
512
  const required = value === void 0 ? [] : valueProblems(decl, value).filter((p) => p.code === "required");
328
513
  for (const f of decl.fields ?? []) {
329
514
  const missing = required.find((p) => p.path === f.name);
330
515
  child(`f:${f.name}`, f, value?.[f.name], (v) => {
331
- const next = { ...isObject2(opts.value) ? opts.value : {} };
516
+ const next = { ...isObject3(opts.value) ? opts.value : {} };
332
517
  if (v === void 0) delete next[f.name];
333
518
  else next[f.name] = v;
334
519
  commit(Object.keys(next).length > 0 ? next : void 0);
335
520
  }, void 0, void 0, missing ? "Required." : void 0);
336
521
  }
337
522
  } else if (decl.type === "map" && decl.of) {
338
- const value = isObject2(o.value) ? o.value : {};
523
+ const value = isObject3(o.value) ? o.value : {};
339
524
  const keyNodes = decl.key?.type === "reference" && o.adapter ? o.adapter.nodes(decl.key.to ?? "") : void 0;
340
525
  const keyKind = decl.key?.to ?? "";
341
526
  const keyLabel = (k) => (o.adapter && keyNodes !== void 0 ? nodeLabel(o.adapter, keyKind, k) : void 0) ?? k;
342
527
  for (const k of Object.keys(value)) {
343
528
  const remove = editable() ? [iconBtn("close", `Remove ${keyLabel(k)}`, () => {
344
- const next = { ...isObject2(opts.value) ? opts.value : {} };
529
+ const next = { ...isObject3(opts.value) ? opts.value : {} };
345
530
  delete next[k];
346
531
  commit(Object.keys(next).length > 0 ? next : void 0);
347
532
  })] : [];
348
533
  const gone = keyNodes !== void 0 && o.adapter && nodeLabel(o.adapter, keyKind, k) === void 0 ? `${k} is not here any more.` : void 0;
349
534
  child(`k:${k}`, asDecl(decl.of, k), value[k], (v) => {
350
- const next = { ...isObject2(opts.value) ? opts.value : {} };
535
+ const next = { ...isObject3(opts.value) ? opts.value : {} };
351
536
  next[k] = v === void 0 ? defaultOf(decl.of) : v;
352
537
  commit(next);
353
538
  }, remove, keyLabel(k), gone);
@@ -391,7 +576,7 @@ function structuredRow(initial) {
391
576
  }
392
577
  } else if (decl.type === "list" && decl.of) {
393
578
  const value = Array.isArray(o.value) ? o.value : [];
394
- const idOf = (item, i) => decl.id !== void 0 && isObject2(item) && typeof item[decl.id] === "string" ? `id:${item[decl.id]}` : `i:${i}`;
579
+ const idOf = (item, i) => decl.id !== void 0 && isObject3(item) && typeof item[decl.id] === "string" ? `id:${item[decl.id]}` : `i:${i}`;
395
580
  value.forEach((item, i) => {
396
581
  const set = (next) => commit(next.length > 0 ? next : void 0);
397
582
  const controls = editable() ? [
@@ -405,7 +590,7 @@ function structuredRow(initial) {
405
590
  }, i === value.length - 1),
406
591
  iconBtn("close", "Remove", () => set(value.filter((_, j) => j !== i)), false, true)
407
592
  ] : [];
408
- const itemLabel = decl.id !== void 0 && isObject2(item) && typeof item[decl.id] === "string" ? item[decl.id] : `${i + 1}`;
593
+ const itemLabel = decl.id !== void 0 && isObject3(item) && typeof item[decl.id] === "string" ? item[decl.id] : `${i + 1}`;
409
594
  child(idOf(item, i), asDecl(decl.of, itemLabel), item, (v) => {
410
595
  const next = [...Array.isArray(opts.value) ? opts.value : []];
411
596
  next[i] = v === void 0 ? defaultOf(decl.of) : v;
@@ -416,8 +601,8 @@ function structuredRow(initial) {
416
601
  if (editable()) {
417
602
  adder.append(iconBtn("add", "Add", () => {
418
603
  const fresh = defaultOf(decl.of);
419
- if (decl.id !== void 0 && isObject2(fresh)) {
420
- const taken = new Set(value.map((x) => isObject2(x) ? x[decl.id] : void 0));
604
+ if (decl.id !== void 0 && isObject3(fresh)) {
605
+ const taken = new Set(value.map((x) => isObject3(x) ? x[decl.id] : void 0));
421
606
  let n = value.length + 1;
422
607
  while (taken.has(`item-${n}`)) n++;
423
608
  fresh[decl.id] = `item-${n}`;
@@ -466,6 +651,6 @@ function valueRows() {
466
651
  };
467
652
  }
468
653
 
469
- export { extensionFold, foldCaption, isSet, valueRow, valueRows };
654
+ export { extensionFold, foldCaption, isRecordMap, isSet, recordMapGrid, recordMapKeyLabel, recordMapSummary, valueRow, valueRows, valueText };
470
655
  //# sourceMappingURL=value-rows.js.map
471
656
  //# sourceMappingURL=value-rows.js.map