@wildwinter/app-shell 0.30.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -19,7 +19,20 @@ declare function el<K extends keyof HTMLElementTagNameMap>(tag: K, classNameOrPr
19
19
  * Its rollover is the THEMED one (`data-tip`), so an app that has called
20
20
  * `initTooltips()` gets our bubble rather than the platform's. */
21
21
  declare function iconBtn(glyph: string, title: string, onClick: () => void, disabled?: boolean, danger?: boolean): HTMLButtonElement;
22
- /** A captioned field: `<label class="shell-labelled"><span class="shell-fieldcap">…</span>control</label>`. */
22
+ /**
23
+ * A captioned field: `<label class="shell-labelled"><span class="shell-fieldcap">…</span>control</label>`.
24
+ *
25
+ * The label is pointed at the field it captions, EXPLICITLY, and never at a button. A `<label>` with no
26
+ * `for` forwards a click to its first labelable descendant, and buttons are labelable - so a caption
27
+ * wrapped around a control containing buttons (`tagChips`, whose every chip carries a remove button)
28
+ * turns every click on the row's dead space into a press of the FIRST button in it. That shipped:
29
+ * patterkit/patter#44, where clicking a Game Data list value anywhere but its ✕ deleted the first value
30
+ * in the list. Reported as "clicking a value removes the wrong one", which is not a wrong-index bug at
31
+ * all - the click never reached the chip.
32
+ *
33
+ * When the control holds no labelable field (a chips editor with the add input removed, say), this is a
34
+ * plain `<div>`: no caption behaviour is better than a caption that presses something.
35
+ */
23
36
  declare function labelled(label: string, control: HTMLElement): HTMLElement;
24
37
  /** Swap item `i` with its neighbour `i + delta` IN PLACE (the up/down reorder list editors share);
25
38
  * a no-op when the target is out of range. Returns whether anything moved. */
@@ -408,6 +421,35 @@ declare function bindPropertyName(input: HTMLInputElement, onCommit: (value: str
408
421
  * sets of words, one gate.
409
422
  */
410
423
  declare function firstIllegalPropertyName(host: ParentNode): HTMLInputElement | null;
424
+ interface PropertyRefOptions {
425
+ /** The names that exist, read fresh on every check so a declaration added in the
426
+ * same dialog clears the complaint. Compared case-insensitively, since the
427
+ * parser folds. */
428
+ known: () => string[];
429
+ /** The scope token this field addresses, for the wording ("no @world property..."). */
430
+ scope?: string;
431
+ /** Shown on the field when there is nothing to complain about. */
432
+ hint?: string;
433
+ }
434
+ /** Why this reference cannot be used, or undefined when it can. */
435
+ declare function propertyRefProblem(name: string, opts: PropertyRefOptions): string | undefined;
436
+ /**
437
+ * Wire a field that POINTS at a declared property: same manners as a declaration
438
+ * field (mark, do not rewrite, Tab coerces), plus the declared names offered as a
439
+ * datalist, because the rule is declare-then-reference and a picker makes that
440
+ * painless rather than pedantic.
441
+ *
442
+ * Marks `.illegal` for an undeclared name too, which gates Save wherever the host
443
+ * already asks `firstIllegalPropertyName`.
444
+ */
445
+ declare function bindPropertyRef(input: HTMLInputElement, onCommit: (value: string) => void, opts: PropertyRefOptions): void;
446
+ /**
447
+ * Re-check every bound reference field inside `host`. Hosts call this when the
448
+ * DECLARATIONS change, since a reference's validity depends on a list it does not
449
+ * own: rename a property and the driver pointing at it is wrong from that moment,
450
+ * with nothing typed into it.
451
+ */
452
+ declare function revalidatePropertyRefs(host: ParentNode): void;
411
453
 
412
454
  interface GameIdEditorOptions {
413
455
  anchor: HTMLElement;
@@ -789,4 +831,4 @@ interface StepperBarOptions {
789
831
  /** Draw (or hide) the bar in `host`. Idempotent: call it again with new state. */
790
832
  declare function renderStepperBar(host: HTMLElement, opts: StepperBarOptions): void;
791
833
 
792
- export { type AboutOptions, type AnchoredPanel, type AnchoredPanelOptions, type Child, type Comment, type CommentMessage, type CommentsOptions, type ConfirmOptions, type DocLine, type DupGuard, type ElProps, type GameIdEditorOptions, type IconName, type IconSvgName, type Identity, type IdentityOptions, type JobProgressOptions, type JobProgressView, type Notes, type NotesEditorOptions, PALETTE, PALETTE_SIZE, PROPERTY_NAME_HINT, type PaneShell, type PaneShellOptions, type PaneShellState, type PaneSide, type PaneSideConfig, type PinButton, type PinButtonOptions, RESERVED_PROPERTY_NAMES, type SaveController, type SaveControllerOptions, type SaveIndicator, type SaveStatus, type SettingsDialog, type SettingsDialogOptions, type SettingsSection, type SettingsSectionHandle, type ShardVc, type StaleBarOptions, type StepperBarOptions, type StepperItem, type StepperTone, type TipRect, type TooltipOptions, type VcMap, askIdentity, bindPropertyName, closeAnchoredPanel, closeWithExit, colourFor, colourIndex, confirmDialog, createSaveController, dupGuard, el, expandableRow, firstIllegalPropertyName, focusNewRow, foldVc, gameIdify, hideTip, icon, iconBtn, iconSvg, initTooltips, isCaseOnlyPropertyName, isValidGameId, isValidPropertyName, labelled, lockControls, lockNotice, mountJobProgress, mountPaneShell, mountSettingsDialog, moveItem, openAnchoredPanel, openComments, openGameIdEditor, openNotesEditor, paintVcBadges, pinButton, placeAnchored, propertyNameProblem, propertyNameify, renderStepperBar, saveIndicator, showAbout, staleBar, tagChips, tipAt, tipBold, vcBadgeFor };
834
+ export { type AboutOptions, type AnchoredPanel, type AnchoredPanelOptions, type Child, type Comment, type CommentMessage, type CommentsOptions, type ConfirmOptions, type DocLine, type DupGuard, type ElProps, type GameIdEditorOptions, type IconName, type IconSvgName, type Identity, type IdentityOptions, type JobProgressOptions, type JobProgressView, type Notes, type NotesEditorOptions, PALETTE, PALETTE_SIZE, PROPERTY_NAME_HINT, type PaneShell, type PaneShellOptions, type PaneShellState, type PaneSide, type PaneSideConfig, type PinButton, type PinButtonOptions, type PropertyRefOptions, RESERVED_PROPERTY_NAMES, type SaveController, type SaveControllerOptions, type SaveIndicator, type SaveStatus, type SettingsDialog, type SettingsDialogOptions, type SettingsSection, type SettingsSectionHandle, type ShardVc, type StaleBarOptions, type StepperBarOptions, type StepperItem, type StepperTone, type TipRect, type TooltipOptions, type VcMap, askIdentity, bindPropertyName, bindPropertyRef, closeAnchoredPanel, closeWithExit, colourFor, colourIndex, confirmDialog, createSaveController, dupGuard, el, expandableRow, firstIllegalPropertyName, focusNewRow, foldVc, gameIdify, hideTip, icon, iconBtn, iconSvg, initTooltips, isCaseOnlyPropertyName, isValidGameId, isValidPropertyName, labelled, lockControls, lockNotice, mountJobProgress, mountPaneShell, mountSettingsDialog, moveItem, openAnchoredPanel, openComments, openGameIdEditor, openNotesEditor, paintVcBadges, pinButton, placeAnchored, propertyNameProblem, propertyNameify, propertyRefProblem, renderStepperBar, revalidatePropertyRefs, saveIndicator, showAbout, staleBar, tagChips, tipAt, tipBold, vcBadgeFor };
package/dist/index.d.ts CHANGED
@@ -19,7 +19,20 @@ declare function el<K extends keyof HTMLElementTagNameMap>(tag: K, classNameOrPr
19
19
  * Its rollover is the THEMED one (`data-tip`), so an app that has called
20
20
  * `initTooltips()` gets our bubble rather than the platform's. */
21
21
  declare function iconBtn(glyph: string, title: string, onClick: () => void, disabled?: boolean, danger?: boolean): HTMLButtonElement;
22
- /** A captioned field: `<label class="shell-labelled"><span class="shell-fieldcap">…</span>control</label>`. */
22
+ /**
23
+ * A captioned field: `<label class="shell-labelled"><span class="shell-fieldcap">…</span>control</label>`.
24
+ *
25
+ * The label is pointed at the field it captions, EXPLICITLY, and never at a button. A `<label>` with no
26
+ * `for` forwards a click to its first labelable descendant, and buttons are labelable - so a caption
27
+ * wrapped around a control containing buttons (`tagChips`, whose every chip carries a remove button)
28
+ * turns every click on the row's dead space into a press of the FIRST button in it. That shipped:
29
+ * patterkit/patter#44, where clicking a Game Data list value anywhere but its ✕ deleted the first value
30
+ * in the list. Reported as "clicking a value removes the wrong one", which is not a wrong-index bug at
31
+ * all - the click never reached the chip.
32
+ *
33
+ * When the control holds no labelable field (a chips editor with the add input removed, say), this is a
34
+ * plain `<div>`: no caption behaviour is better than a caption that presses something.
35
+ */
23
36
  declare function labelled(label: string, control: HTMLElement): HTMLElement;
24
37
  /** Swap item `i` with its neighbour `i + delta` IN PLACE (the up/down reorder list editors share);
25
38
  * a no-op when the target is out of range. Returns whether anything moved. */
@@ -408,6 +421,35 @@ declare function bindPropertyName(input: HTMLInputElement, onCommit: (value: str
408
421
  * sets of words, one gate.
409
422
  */
410
423
  declare function firstIllegalPropertyName(host: ParentNode): HTMLInputElement | null;
424
+ interface PropertyRefOptions {
425
+ /** The names that exist, read fresh on every check so a declaration added in the
426
+ * same dialog clears the complaint. Compared case-insensitively, since the
427
+ * parser folds. */
428
+ known: () => string[];
429
+ /** The scope token this field addresses, for the wording ("no @world property..."). */
430
+ scope?: string;
431
+ /** Shown on the field when there is nothing to complain about. */
432
+ hint?: string;
433
+ }
434
+ /** Why this reference cannot be used, or undefined when it can. */
435
+ declare function propertyRefProblem(name: string, opts: PropertyRefOptions): string | undefined;
436
+ /**
437
+ * Wire a field that POINTS at a declared property: same manners as a declaration
438
+ * field (mark, do not rewrite, Tab coerces), plus the declared names offered as a
439
+ * datalist, because the rule is declare-then-reference and a picker makes that
440
+ * painless rather than pedantic.
441
+ *
442
+ * Marks `.illegal` for an undeclared name too, which gates Save wherever the host
443
+ * already asks `firstIllegalPropertyName`.
444
+ */
445
+ declare function bindPropertyRef(input: HTMLInputElement, onCommit: (value: string) => void, opts: PropertyRefOptions): void;
446
+ /**
447
+ * Re-check every bound reference field inside `host`. Hosts call this when the
448
+ * DECLARATIONS change, since a reference's validity depends on a list it does not
449
+ * own: rename a property and the driver pointing at it is wrong from that moment,
450
+ * with nothing typed into it.
451
+ */
452
+ declare function revalidatePropertyRefs(host: ParentNode): void;
411
453
 
412
454
  interface GameIdEditorOptions {
413
455
  anchor: HTMLElement;
@@ -789,4 +831,4 @@ interface StepperBarOptions {
789
831
  /** Draw (or hide) the bar in `host`. Idempotent: call it again with new state. */
790
832
  declare function renderStepperBar(host: HTMLElement, opts: StepperBarOptions): void;
791
833
 
792
- export { type AboutOptions, type AnchoredPanel, type AnchoredPanelOptions, type Child, type Comment, type CommentMessage, type CommentsOptions, type ConfirmOptions, type DocLine, type DupGuard, type ElProps, type GameIdEditorOptions, type IconName, type IconSvgName, type Identity, type IdentityOptions, type JobProgressOptions, type JobProgressView, type Notes, type NotesEditorOptions, PALETTE, PALETTE_SIZE, PROPERTY_NAME_HINT, type PaneShell, type PaneShellOptions, type PaneShellState, type PaneSide, type PaneSideConfig, type PinButton, type PinButtonOptions, RESERVED_PROPERTY_NAMES, type SaveController, type SaveControllerOptions, type SaveIndicator, type SaveStatus, type SettingsDialog, type SettingsDialogOptions, type SettingsSection, type SettingsSectionHandle, type ShardVc, type StaleBarOptions, type StepperBarOptions, type StepperItem, type StepperTone, type TipRect, type TooltipOptions, type VcMap, askIdentity, bindPropertyName, closeAnchoredPanel, closeWithExit, colourFor, colourIndex, confirmDialog, createSaveController, dupGuard, el, expandableRow, firstIllegalPropertyName, focusNewRow, foldVc, gameIdify, hideTip, icon, iconBtn, iconSvg, initTooltips, isCaseOnlyPropertyName, isValidGameId, isValidPropertyName, labelled, lockControls, lockNotice, mountJobProgress, mountPaneShell, mountSettingsDialog, moveItem, openAnchoredPanel, openComments, openGameIdEditor, openNotesEditor, paintVcBadges, pinButton, placeAnchored, propertyNameProblem, propertyNameify, renderStepperBar, saveIndicator, showAbout, staleBar, tagChips, tipAt, tipBold, vcBadgeFor };
834
+ export { type AboutOptions, type AnchoredPanel, type AnchoredPanelOptions, type Child, type Comment, type CommentMessage, type CommentsOptions, type ConfirmOptions, type DocLine, type DupGuard, type ElProps, type GameIdEditorOptions, type IconName, type IconSvgName, type Identity, type IdentityOptions, type JobProgressOptions, type JobProgressView, type Notes, type NotesEditorOptions, PALETTE, PALETTE_SIZE, PROPERTY_NAME_HINT, type PaneShell, type PaneShellOptions, type PaneShellState, type PaneSide, type PaneSideConfig, type PinButton, type PinButtonOptions, type PropertyRefOptions, RESERVED_PROPERTY_NAMES, type SaveController, type SaveControllerOptions, type SaveIndicator, type SaveStatus, type SettingsDialog, type SettingsDialogOptions, type SettingsSection, type SettingsSectionHandle, type ShardVc, type StaleBarOptions, type StepperBarOptions, type StepperItem, type StepperTone, type TipRect, type TooltipOptions, type VcMap, askIdentity, bindPropertyName, bindPropertyRef, closeAnchoredPanel, closeWithExit, colourFor, colourIndex, confirmDialog, createSaveController, dupGuard, el, expandableRow, firstIllegalPropertyName, focusNewRow, foldVc, gameIdify, hideTip, icon, iconBtn, iconSvg, initTooltips, isCaseOnlyPropertyName, isValidGameId, isValidPropertyName, labelled, lockControls, lockNotice, mountJobProgress, mountPaneShell, mountSettingsDialog, moveItem, openAnchoredPanel, openComments, openGameIdEditor, openNotesEditor, paintVcBadges, pinButton, placeAnchored, propertyNameProblem, propertyNameify, propertyRefProblem, renderStepperBar, revalidatePropertyRefs, saveIndicator, showAbout, staleBar, tagChips, tipAt, tipBold, vcBadgeFor };
package/dist/index.js CHANGED
@@ -181,8 +181,13 @@ function iconBtn(glyph, title, onClick, disabled = false, danger = false) {
181
181
  return b;
182
182
  }
183
183
  function labelled(label2, control) {
184
- const w = el("label", "shell-labelled");
184
+ const target = control.matches("input, select, textarea") ? control : control.querySelector("input:not([type=button]):not([type=submit]), select, textarea");
185
+ const w = el(target ? "label" : "div", "shell-labelled");
185
186
  w.append(el("span", "shell-fieldcap", label2), control);
187
+ if (target) {
188
+ if (!target.id) target.id = `shell-f-${Math.random().toString(36).slice(2, 9)}`;
189
+ w.htmlFor = target.id;
190
+ }
186
191
  return w;
187
192
  }
188
193
  function moveItem(arr, i, delta) {
@@ -880,6 +885,7 @@ function isCaseOnlyPropertyName(name) {
880
885
  }
881
886
 
882
887
  // src/property-name-field.ts
888
+ var refSyncs = /* @__PURE__ */ new WeakMap();
883
889
  function propertyNameProblem(name) {
884
890
  if (isValidPropertyName(name)) return void 0;
885
891
  const suggestion = propertyNameify(name);
@@ -926,6 +932,96 @@ function bindPropertyName(input, onCommit, opts = {}) {
926
932
  function firstIllegalPropertyName(host) {
927
933
  return host.querySelector("input.illegal");
928
934
  }
935
+ function nearest(name, known) {
936
+ const n = name.toLowerCase();
937
+ const byPrefix = known.filter((k) => k.toLowerCase().startsWith(n) || n.startsWith(k.toLowerCase()));
938
+ if (byPrefix.length === 1) return byPrefix[0];
939
+ let best;
940
+ let bestScore = 3;
941
+ for (const k of known) {
942
+ const a = k.toLowerCase();
943
+ if (Math.abs(a.length - n.length) > 2) continue;
944
+ let prev = Array.from({ length: n.length + 1 }, (_, i) => i);
945
+ for (let i = 1; i <= a.length; i++) {
946
+ const row = [i];
947
+ for (let j = 1; j <= n.length; j++) {
948
+ row[j] = Math.min(prev[j] + 1, row[j - 1] + 1, prev[j - 1] + (a[i - 1] === n[j - 1] ? 0 : 1));
949
+ }
950
+ prev = row;
951
+ }
952
+ const d = prev[n.length];
953
+ if (d < bestScore) {
954
+ bestScore = d;
955
+ best = k;
956
+ }
957
+ }
958
+ return best;
959
+ }
960
+ function propertyRefProblem(name, opts) {
961
+ const scope = opts.scope ? `@${opts.scope}` : "that scope";
962
+ const folded = name.toLowerCase();
963
+ if (!isValidPropertyName(folded)) {
964
+ const suggestion = propertyNameify(name);
965
+ const tail = suggestion ? ` Try "${suggestion}", or press Tab.` : "";
966
+ if (RESERVED_PROPERTY_NAMES.includes(folded)) return `Cannot be used: "${folded}" is a keyword in expressions.${tail}`;
967
+ if (name.includes("-")) return `Cannot be used: a hyphen reads as subtraction in an expression.${tail}`;
968
+ if (/^[0-9]/.test(name)) return `Cannot be used: a name cannot start with a digit.${tail}`;
969
+ return `Cannot be used: only lower case letters, digits and underscores.${tail}`;
970
+ }
971
+ const known = opts.known();
972
+ if (known.some((k) => k.toLowerCase() === folded)) return void 0;
973
+ const near = nearest(folded, known);
974
+ if (near) return `No ${scope} property is called "${name}". Did you mean "${near}"?`;
975
+ if (known.length === 0) return `No ${scope} properties are declared yet. Declare one above first.`;
976
+ const shown = known.slice(0, 3).map((k) => `"${k}"`).join(", ");
977
+ const more = known.length > 3 ? `, and ${known.length - 3} more` : "";
978
+ return `No ${scope} property is called "${name}". Declared: ${shown}${more}.`;
979
+ }
980
+ function bindPropertyRef(input, onCommit, opts) {
981
+ const list = document.createElement("datalist");
982
+ list.id = `shell-propref-${Math.random().toString(36).slice(2, 9)}`;
983
+ input.setAttribute("list", list.id);
984
+ const attach = () => {
985
+ if (!list.isConnected && input.parentNode) input.after(list);
986
+ };
987
+ const sync = () => {
988
+ attach();
989
+ const known = opts.known();
990
+ list.replaceChildren(...known.map((k) => {
991
+ const o = document.createElement("option");
992
+ o.value = k;
993
+ return o;
994
+ }));
995
+ const v = input.value.trim();
996
+ const problem = v === "" ? void 0 : propertyRefProblem(v, opts);
997
+ input.classList.toggle("illegal", !!problem);
998
+ if (problem) input.title = problem;
999
+ else if (input.title.startsWith("Cannot be used") || input.title.startsWith("No ")) {
1000
+ if (opts.hint) input.title = opts.hint;
1001
+ else input.removeAttribute("title");
1002
+ }
1003
+ };
1004
+ refSyncs.set(input, sync);
1005
+ input.addEventListener("input", () => {
1006
+ onCommit(input.value);
1007
+ sync();
1008
+ });
1009
+ input.addEventListener("keydown", (e) => {
1010
+ if (e.key !== "Tab" || e.shiftKey) return;
1011
+ const typed = input.value.trim();
1012
+ if (typed === "" || isValidPropertyName(typed.toLowerCase())) return;
1013
+ const coerced = propertyNameify(typed);
1014
+ if (!coerced || coerced === input.value) return;
1015
+ e.preventDefault();
1016
+ input.value = coerced;
1017
+ onCommit(coerced);
1018
+ sync();
1019
+ });
1020
+ sync();
1021
+ }
1022
+ function revalidatePropertyRefs(host) {
1023
+ for (const input of host.querySelectorAll("input")) refSyncs.get(input)?.();
1024
+ }
929
1025
 
930
1026
  // src/id-editor.ts
931
1027
  function openGameIdEditor(opts) {
@@ -1587,6 +1683,6 @@ function renderStepperBar(host, opts) {
1587
1683
  );
1588
1684
  }
1589
1685
 
1590
- export { PALETTE, PALETTE_SIZE, PROPERTY_NAME_HINT, RESERVED_PROPERTY_NAMES, askIdentity, bindPropertyName, closeAnchoredPanel, closeWithExit, colourFor, colourIndex, confirmDialog, createSaveController, dupGuard, el, expandableRow, firstIllegalPropertyName, focusNewRow, foldVc, gameIdify, hideTip, icon, iconBtn, iconSvg, initTooltips, isCaseOnlyPropertyName, isValidGameId, isValidPropertyName, labelled, lockControls, lockNotice, mountJobProgress, mountPaneShell, mountSettingsDialog, moveItem, openAnchoredPanel, openComments, openGameIdEditor, openNotesEditor, paintVcBadges, pinButton, placeAnchored, propertyNameProblem, propertyNameify, renderStepperBar, saveIndicator, showAbout, staleBar, tagChips, tipAt, tipBold, vcBadgeFor };
1686
+ export { PALETTE, PALETTE_SIZE, PROPERTY_NAME_HINT, RESERVED_PROPERTY_NAMES, askIdentity, bindPropertyName, bindPropertyRef, closeAnchoredPanel, closeWithExit, colourFor, colourIndex, confirmDialog, createSaveController, dupGuard, el, expandableRow, firstIllegalPropertyName, focusNewRow, foldVc, gameIdify, hideTip, icon, iconBtn, iconSvg, initTooltips, isCaseOnlyPropertyName, isValidGameId, isValidPropertyName, labelled, lockControls, lockNotice, mountJobProgress, mountPaneShell, mountSettingsDialog, moveItem, openAnchoredPanel, openComments, openGameIdEditor, openNotesEditor, paintVcBadges, pinButton, placeAnchored, propertyNameProblem, propertyNameify, propertyRefProblem, renderStepperBar, revalidatePropertyRefs, saveIndicator, showAbout, staleBar, tagChips, tipAt, tipBold, vcBadgeFor };
1591
1687
  //# sourceMappingURL=index.js.map
1592
1688
  //# sourceMappingURL=index.js.map