@glimstone/core 4.1.0 → 4.1.2

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/README.md CHANGED
@@ -22,6 +22,10 @@ npm install @glimstone/core
22
22
  select's list model, the Info tile's version links and update check, the
23
23
  README button's classes, where a bubble goes, a dialog's keys and the toast
24
24
  queue.
25
+ - What a text field and its row share: `textFieldClass` and `fieldWidthClass`
26
+ say how a field looks and how wide it is, `pressRowControl` does what a click
27
+ on a row's label does, `treeRows` and `treeStep` hold a tree's rows and keys, and
28
+ `tableCellClass` says what a table's cell wears.
25
29
  - What windows and menus share: `windowFoot` adds the way out, `stackWindow`
26
30
  gives Escape and Tab to the window in front, and `menu.ts` holds the menu
27
31
  model with `tidyMenu`, `menuRowView`, `placeMenu`, `menuMove`,
package/dist/button.d.ts CHANGED
@@ -18,6 +18,8 @@ export type ButtonTone = "accent" | "neutral" | "subtle" | "ok" | "danger" | "wa
18
18
  * ordinary reactive button, because it grows on hover.
19
19
  */
20
20
  export type ButtonVariant = "default" | "chip" | "icon";
21
+ /** `key` is the second height, for the button a surface exists for (rule 19). */
22
+ export type ButtonSize = "normal" | "key";
21
23
  export interface ButtonInput {
22
24
  /** The current mode of the "buttons" label axis. */
23
25
  mode: LabelMode;
@@ -29,6 +31,9 @@ export interface ButtonInput {
29
31
  variant: ButtonVariant;
30
32
  disabled: boolean;
31
33
  keepLabel: boolean;
34
+ /** Its glyph alone in a square in every label mode, where the words have no room. */
35
+ compact?: boolean;
36
+ size?: ButtonSize;
32
37
  title?: string;
33
38
  hueIndex?: number;
34
39
  stage?: WidthStage;
package/dist/button.js CHANGED
@@ -27,10 +27,11 @@ const TONE_CLASS = {
27
27
  };
28
28
  export function buttonView(b) {
29
29
  const chip = b.variant === "chip";
30
- const iconOnly = b.variant === "icon";
30
+ const compact = !!b.compact && b.hasGlyph;
31
+ const iconOnly = b.variant === "icon" || compact;
31
32
  // A glyphless button shows its text in both hiding modes, and keepLabel
32
33
  // keeps the glyph beside the text when there is one.
33
- const effective = chip
34
+ const effective = chip || compact
34
35
  ? "glyph"
35
36
  : b.keepLabel
36
37
  ? b.hasGlyph
@@ -73,7 +74,7 @@ export function buttonView(b) {
73
74
  return {
74
75
  showGlyph: effective !== "text" && b.hasGlyph,
75
76
  labelClass: showText ? "glim-btn-label" : reactive ? "glim-label-reactive" : "glim-sr-only",
76
- className: `glim-btn ${stage} ${chip ? "" : TONE_CLASS[b.tone]}${hueOn ? " glim-hue" : ""}${reactive ? " glim-reactive" : ""}`,
77
+ className: `glim-btn ${stage} ${chip ? "" : TONE_CLASS[b.tone]}${b.size === "key" ? " glim-btn-key" : ""}${hueOn ? " glim-hue" : ""}${reactive ? " glim-reactive" : ""}`,
77
78
  style: Object.keys(style).length ? style : undefined,
78
79
  tip: reactive && !b.disabled ? undefined : tip,
79
80
  // The surface tones carry rule 40's tint; a button with its own palette
@@ -0,0 +1,21 @@
1
+ /**
2
+ * How wide a field is. `fill` takes its column. `short` holds a port or a time,
3
+ * `medium` a word, `wide` a line of text, and `grow` takes what its row has left.
4
+ */
5
+ export type FieldWidth = "fill" | "short" | "medium" | "wide" | "grow";
6
+ /** The class of a width, for a text field or any wrapper that carries one. */
7
+ export declare function fieldWidthClass(width?: FieldWidth): string;
8
+ /** The classes of a text field or a text area. */
9
+ export declare function textFieldClass({ width, mono, area }: {
10
+ width?: FieldWidth;
11
+ mono?: boolean;
12
+ area?: boolean;
13
+ }): string;
14
+ /** Below this width a PathField's button shows its glyph alone and leaves the room to the path. */
15
+ export declare const PATH_ROOM_PX = 320;
16
+ /**
17
+ * What a click on a row's label does, as a real label would: a field takes the
18
+ * focus and a switch flips. A selector stays as it is, since no one segment is
19
+ * the row's.
20
+ */
21
+ export declare function pressRowControl(row: HTMLElement): void;
package/dist/field.js ADDED
@@ -0,0 +1,26 @@
1
+ // The text field and the row a control stands in ("A dropdown or a one-line
2
+ // field stands beside its label", rule 35).
3
+ /** The class of a width, for a text field or any wrapper that carries one. */
4
+ export function fieldWidthClass(width = "fill") {
5
+ return width === "fill" ? "" : `glim-field--${width}`;
6
+ }
7
+ /** The classes of a text field or a text area. */
8
+ export function textFieldClass({ width, mono, area }) {
9
+ return ["glim-input", area && "glim-input--area", mono && "glim-input--mono", fieldWidthClass(width)].filter(Boolean).join(" ");
10
+ }
11
+ /** Below this width a PathField's button shows its glyph alone and leaves the room to the path. */
12
+ export const PATH_ROOM_PX = 320;
13
+ const FIELD = 'input:not([type="hidden"]), textarea';
14
+ const PRESSABLE = '[role="switch"], [role="checkbox"], .glim-select-trigger';
15
+ /**
16
+ * What a click on a row's label does, as a real label would: a field takes the
17
+ * focus and a switch flips. A selector stays as it is, since no one segment is
18
+ * the row's.
19
+ */
20
+ export function pressRowControl(row) {
21
+ const control = row.querySelector(`${FIELD}, ${PRESSABLE}`);
22
+ if (control?.matches(FIELD))
23
+ control.focus();
24
+ else
25
+ control?.click();
26
+ }
package/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export * from "./controls.ts";
10
10
  export * from "./dialog.ts";
11
11
  export * from "./disco.ts";
12
12
  export * from "./discoLoop.ts";
13
+ export * from "./field.ts";
13
14
  export * from "./flagEmoji.ts";
14
15
  export * from "./infoTile.ts";
15
16
  export * from "./inputModality.ts";
@@ -31,7 +32,9 @@ export * from "./selectScroll.ts";
31
32
  export * from "./sidebarRow.ts";
32
33
  export * from "./storage.ts";
33
34
  export * from "./tipBubble.ts";
35
+ export * from "./table.ts";
34
36
  export * from "./toastEngine.ts";
35
37
  export * from "./tooltip.ts";
38
+ export * from "./tree.ts";
36
39
  export * from "./version.ts";
37
40
  export * from "./window.ts";
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ export * from "./controls.js";
10
10
  export * from "./dialog.js";
11
11
  export * from "./disco.js";
12
12
  export * from "./discoLoop.js";
13
+ export * from "./field.js";
13
14
  export * from "./flagEmoji.js";
14
15
  export * from "./infoTile.js";
15
16
  export * from "./inputModality.js";
@@ -31,7 +32,9 @@ export * from "./selectScroll.js";
31
32
  export * from "./sidebarRow.js";
32
33
  export * from "./storage.js";
33
34
  export * from "./tipBubble.js";
35
+ export * from "./table.js";
34
36
  export * from "./toastEngine.js";
35
37
  export * from "./tooltip.js";
38
+ export * from "./tree.js";
36
39
  export * from "./version.js";
37
40
  export * from "./window.js";
package/dist/marks.d.ts CHANGED
@@ -65,6 +65,12 @@ export declare const MARKS: {
65
65
  size: number;
66
66
  body: string;
67
67
  };
68
+ /** `IconFolderOpen`: a PathField's button. */
69
+ folderOpen: {
70
+ viewBox: string;
71
+ size: number;
72
+ body: string;
73
+ };
68
74
  /** The small cross that dismisses a toast. */
69
75
  dismiss: {
70
76
  viewBox: string;
package/dist/marks.js CHANGED
@@ -72,6 +72,12 @@ export const MARKS = {
72
72
  size: 16,
73
73
  body: '<path d="M20.926 7.074a3.67 3.67 0 0 1 1.074 2.593v8.666a3.667 3.667 0 0 1 -3.667 3.667h-8.666a3.667 3.667 0 0 1 -3.667 -3.667v-8.666q 0 -.053 .005 -.102a3.66 3.66 0 0 1 3.662 -3.565h8.666c.973 0 1.905 .386 2.593 1.074" /><path d="M17.374 3.514a1 1 0 1 1 -1.748 .972c-.221 -.398 -.342 -.486 -.626 -.486h-10c-.548 0 -1 .452 -1 1v9.998c0 .36 .194 .692 .507 .87a1 1 0 1 1 -.99 1.738a3 3 0 0 1 -1.517 -2.606v-10c0 -1.652 1.348 -3 3 -3h10c1.094 0 1.828 .533 2.374 1.514" />',
74
74
  },
75
+ /** `IconFolderOpen`: a PathField's button. */
76
+ folderOpen: {
77
+ viewBox: "0 -31.981 575.963 575.963",
78
+ size: 16,
79
+ body: '<path d="M88.7 223.8L0 375.8 0 96C0 60.7 28.7 32 64 32l117.5 0c17 0 33.3 6.7 45.3 18.7l26.5 26.5c12 12 28.3 18.7 45.3 18.7L416 96c35.3 0 64 28.7 64 64l0 32-336 0c-22.8 0-43.8 12.1-55.3 31.8zm27.6 16.1C122.1 230 132.6 224 144 224l400 0c11.5 0 22 6.1 27.7 16.1s5.7 22.2-.1 32.1l-112 192C453.9 474 443.4 480 432 480L32 480c-11.5 0-22-6.1-27.7-16.1s-5.7-22.2 .1-32.1l112-192z"/>',
80
+ },
75
81
  /** The small cross that dismisses a toast. */
76
82
  dismiss: {
77
83
  viewBox: "0 0 16 16",
package/dist/media.d.ts CHANGED
@@ -2,6 +2,8 @@
2
2
  export declare const DESKTOP_QUERY = "(min-width: 48rem)";
3
3
  /** At least this wide, the column of tiles shows words; below it glyphs alone. */
4
4
  export declare const WIDE_QUERY = "(min-width: 64rem)";
5
+ /** The query for a window narrower than this many pixels. */
6
+ export declare function narrowerThan(px: number): string;
5
7
  export declare function mediaMatches(query: string): boolean;
6
8
  /** Calls `fn` whenever the query flips, and returns the unsubscribe. */
7
9
  export declare function subscribeMedia(query: string, fn: () => void): () => void;
package/dist/media.js CHANGED
@@ -4,6 +4,10 @@
4
4
  export const DESKTOP_QUERY = "(min-width: 48rem)";
5
5
  /** At least this wide, the column of tiles shows words; below it glyphs alone. */
6
6
  export const WIDE_QUERY = "(min-width: 64rem)";
7
+ /** The query for a window narrower than this many pixels. */
8
+ export function narrowerThan(px) {
9
+ return `(max-width: ${px - 0.02}px)`;
10
+ }
7
11
  export function mediaMatches(query) {
8
12
  return typeof matchMedia === "function" && matchMedia(query).matches;
9
13
  }
package/dist/menu.d.ts CHANGED
@@ -16,6 +16,8 @@ export interface MenuItem<G> {
16
16
  radio?: boolean;
17
17
  /** Entries that open beside this row. */
18
18
  sub?: MenuEntry<G>[];
19
+ /** The id of the menu `sub` opens, for a caller or a script that looks for it. */
20
+ subId?: string;
19
21
  onSelect?: () => void;
20
22
  }
21
23
  export interface MenuHead<G> {
@@ -38,6 +38,23 @@ export declare function selectRootClass(width: SelectWidth): string;
38
38
  /** The trigger's classes. `wrap` lets a long value run onto a second line, for one whose end says as much as its start, such as a path. */
39
39
  export declare function selectTriggerClass(size: SelectSize, wrap: boolean): string;
40
40
  export declare function selectContentClass(glyphs: boolean): string;
41
+ /**
42
+ * Which rows an open list draws. `menu` takes a context menu's, so an app with
43
+ * both shows one kind of menu row.
44
+ */
45
+ export type SelectRows = "list" | "menu";
46
+ export interface SelectRowParts {
47
+ content: string;
48
+ item: string;
49
+ /** The glyph's room, or null where no row of a list has one. A menu row keeps it always. */
50
+ glyph: string | null;
51
+ label: string;
52
+ check: string;
53
+ caret: string;
54
+ heading: string;
55
+ }
56
+ /** The classes of an open list and its rows. */
57
+ export declare function selectRowParts(rows: SelectRows, glyphs: boolean): SelectRowParts;
41
58
  /** A field defaults to `fill`, the smaller sizes to `steady`. */
42
59
  export declare function defaultWidth(size: SelectSize): SelectWidth;
43
60
  export declare const SELECT_POSITIONING: {
@@ -24,6 +24,29 @@ export function selectTriggerClass(size, wrap) {
24
24
  export function selectContentClass(glyphs) {
25
25
  return `glim-select-content glim-fade${glyphs ? " glim-select-content--glyphs" : ""}`;
26
26
  }
27
+ /** The classes of an open list and its rows. */
28
+ export function selectRowParts(rows, glyphs) {
29
+ const content = selectContentClass(glyphs);
30
+ return rows === "menu"
31
+ ? {
32
+ content: `${content} glim-select-content--menu`,
33
+ item: "glim-menu-item",
34
+ glyph: "glim-menu-glyph",
35
+ label: "glim-menu-label",
36
+ check: "glim-menu-end glim-menu-tick",
37
+ caret: "glim-menu-end glim-menu-caret",
38
+ heading: "glim-menu-head",
39
+ }
40
+ : {
41
+ content,
42
+ item: "glim-select-item",
43
+ glyph: glyphs ? "glim-select-glyph" : null,
44
+ label: "glim-select-label",
45
+ check: "glim-select-check",
46
+ caret: "glim-select-caret",
47
+ heading: "glim-select-heading",
48
+ };
49
+ }
27
50
  /** A field defaults to `fill`, the smaller sizes to `steady`. */
28
51
  export function defaultWidth(size) {
29
52
  return size === "field" ? "fill" : "steady";
@@ -34,7 +34,7 @@ export declare const MAX_PINNED_WIDTH = 352;
34
34
  */
35
35
  export declare function pinnedWidth(widest: number, count: number, room: number, gap: number): number;
36
36
  /** The id of a tab in a strip that switches `panelId`; the panel names itself after it. */
37
- export declare function selectorTabId(panelId: string, itemId: string): string;
37
+ export declare function selectorTabId(panelId: string, itemId: string | number): string;
38
38
  /**
39
39
  * The label axis a selector follows. The page-level strips are tabs, the
40
40
  * smaller ones stand in form rows beside buttons, and the column of tiles
@@ -56,7 +56,7 @@ export declare function nextFocusIndex(key: SelectorNavKey, current: number, cou
56
56
  */
57
57
  export declare function rovedIndex(disabled: boolean[], activeIndex: number): number;
58
58
  /** The value of a "many" selector once `id` is toggled, in the items' order. */
59
- export declare function toggleSelection(value: readonly string[], id: string, order: readonly string[]): string[];
59
+ export declare function toggleSelection<V>(value: readonly V[], id: V, order: readonly V[]): V[];
60
60
  /**
61
61
  * Moves the focus for a key press inside `track` and returns the segment it
62
62
  * landed on, or null when the key is not one the strip answers. Disabled
@@ -9,6 +9,10 @@ export interface SidebarRowInput {
9
9
  hueIndex?: number;
10
10
  /** The figure on the row; nothing shows at zero. */
11
11
  count?: number;
12
+ /** Something stands beside the link in the row, so it is never centred. */
13
+ framed?: boolean;
14
+ /** A tree's row: stepped in by its level, with a smaller glyph and gap. */
15
+ depth?: number;
12
16
  }
13
17
  export interface SidebarRowView {
14
18
  className: string;
@@ -15,14 +15,19 @@ export function sidebarRowView(r) {
15
15
  const showLabel = !hidesLabel(mode);
16
16
  const reactive = mode === "reactive";
17
17
  const hued = r.hueIndex !== undefined;
18
+ const tree = r.depth !== undefined;
19
+ // A tree's depth and a slot's control mean nothing in the middle of the row.
20
+ const centred = !showLabel && !r.framed && !tree;
18
21
  const style = {
19
22
  ...(hued ? hueVars(r.hueIndex) : {}),
23
+ ...(tree ? { "--nav-depth": String(r.depth) } : {}),
20
24
  ...(reactive ? { "--reactive-chars": String(labelWidth(r.label)) } : {}),
21
25
  };
22
26
  return {
23
27
  className: [
24
28
  "glim-nav-row",
25
- !showLabel && "glim-nav-row--centred",
29
+ centred && "glim-nav-row--centred",
30
+ tree && "glim-nav-row--tree",
26
31
  reactive && "glim-reactive",
27
32
  hued && "glim-hue glim-hue-icon",
28
33
  r.active && "glim-active",
@@ -0,0 +1,12 @@
1
+ export interface TableColumn {
2
+ /** Which field of a row the column shows. */
3
+ key: string;
4
+ /** The column's heading. A table with no heading at all draws no header row. */
5
+ label?: string;
6
+ /** Sets the column at the end, in figures that line up (rule 7). */
7
+ end?: boolean;
8
+ /** `muted` for a quiet column, `accent` for what changes. */
9
+ tone?: "muted" | "accent";
10
+ }
11
+ /** The classes of a cell. */
12
+ export declare function tableCellClass(column: TableColumn): string;
package/dist/table.js ADDED
@@ -0,0 +1,5 @@
1
+ // What a table is handed, and the classes its cells wear.
2
+ /** The classes of a cell. */
3
+ export function tableCellClass(column) {
4
+ return [column.end && "glim-table-end glim-num", column.tone && `glim-table-${column.tone}`].filter(Boolean).join(" ");
5
+ }
@@ -5,10 +5,16 @@ export declare const TOAST_DURATION_MS = 4000;
5
5
  * viewport. Without a cap, a held Enter key grows the stack off-screen, where
6
6
  * its cards still block clicks on the page. */
7
7
  export declare const MAX_VISIBLE_TOASTS = 4;
8
+ /** A button on a toast, such as Undo. A press runs it and dismisses the toast. */
9
+ export interface ToastAction {
10
+ label: string;
11
+ onClick: () => void;
12
+ }
8
13
  export interface ToastEntry {
9
14
  id: string;
10
15
  message: string;
11
16
  severity: ToastSeverity;
17
+ action?: ToastAction;
12
18
  /** What is left of the duration, authoritative only while paused
13
19
  * (expiresAt === null). */
14
20
  remainingMs: number;
@@ -26,6 +32,7 @@ export declare function addToast(list: ToastEntry[], toast: {
26
32
  id: string;
27
33
  message: string;
28
34
  severity: ToastSeverity;
35
+ action?: ToastAction;
29
36
  }, now: number, durationMs?: number, maxVisible?: number): ToastEntry[];
30
37
  /** Removes one toast and leaves the others alone. Unknown ids are ignored. */
31
38
  export declare function removeToast(list: ToastEntry[], id: string): ToastEntry[];
@@ -54,6 +61,11 @@ export declare function applyEngagement(prev: Readonly<ToastEngagement>, kind: T
54
61
  next: ToastEngagement;
55
62
  engaged: boolean;
56
63
  };
64
+ /**
65
+ * A message cut after each slash, for a <wbr> between the parts, so a path breaks
66
+ * at a folder rather than inside a name.
67
+ */
68
+ export declare function softBreaks(message: string): string[];
57
69
  /** Quiet mode filters by severity: it hides routine successes, never warnings
58
70
  * or failures. */
59
71
  export declare function shouldShowToast(severity: ToastSeverity, quiet: boolean): boolean;
@@ -24,6 +24,7 @@ export function addToast(list, toast, now, durationMs = TOAST_DURATION_MS, maxVi
24
24
  id: toast.id,
25
25
  message: toast.message,
26
26
  severity: toast.severity,
27
+ action: toast.action,
27
28
  remainingMs: durationMs,
28
29
  expiresAt: now + durationMs,
29
30
  };
@@ -76,6 +77,13 @@ export function applyEngagement(prev, kind, active) {
76
77
  const next = { ...prev, [kind]: active };
77
78
  return { next, engaged: next.hover || next.focus };
78
79
  }
80
+ /**
81
+ * A message cut after each slash, for a <wbr> between the parts, so a path breaks
82
+ * at a folder rather than inside a name.
83
+ */
84
+ export function softBreaks(message) {
85
+ return message.split(/(?<=[/\\])/);
86
+ }
79
87
  /** Quiet mode filters by severity: it hides routine successes, never warnings
80
88
  * or failures. */
81
89
  export function shouldShowToast(severity, quiet) {
package/dist/tree.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ export interface TreeNode<G = unknown> {
2
+ /** Unique in the tree, such as a path. */
3
+ id: string;
4
+ label: string;
5
+ /** Whether the row can open. Its children are asked for the first time it does. */
6
+ hasChildren?: boolean;
7
+ glyph?: G;
8
+ /** Takes the glyph's place while the row is open. */
9
+ openGlyph?: G;
10
+ /** Attributes for the row, such as a data hook. */
11
+ attrs?: Record<string, string>;
12
+ }
13
+ export interface TreeRow<G = unknown> {
14
+ node: TreeNode<G>;
15
+ /** From 1, as aria-level counts. */
16
+ level: number;
17
+ position: number;
18
+ siblings: number;
19
+ parent: string | null;
20
+ open: boolean;
21
+ }
22
+ /** The rows on show, top to bottom. The top level's children are filed under null. */
23
+ export declare function treeRows<G>(children: ReadonlyMap<string | null, readonly TreeNode<G>[]>, open: readonly string[]): TreeRow<G>[];
24
+ export type TreeStep = {
25
+ focus: string;
26
+ } | {
27
+ open: string;
28
+ } | {
29
+ close: string;
30
+ };
31
+ /**
32
+ * What a key does on the row at `at`, or null when the tree does not answer it.
33
+ * Right and left swap under a right-to-left page. A letter goes to the next row
34
+ * starting with it, wrapping at the end.
35
+ */
36
+ export declare function treeStep<G>(rows: readonly TreeRow<G>[], at: number, key: string, rtl?: boolean): TreeStep | null;
37
+ /** Puts the focus on a row. */
38
+ export declare function focusTreeRow(tree: HTMLElement, id: string): void;
package/dist/tree.js ADDED
@@ -0,0 +1,57 @@
1
+ // The model behind Tree: which rows are on show and what a key does there. The
2
+ // frameworks hold the children and draw the rows; the glyph type is theirs.
3
+ /** The rows on show, top to bottom. The top level's children are filed under null. */
4
+ export function treeRows(children, open) {
5
+ const rows = [];
6
+ const walk = (parent, level) => {
7
+ const nodes = children.get(parent) ?? [];
8
+ nodes.forEach((node, i) => {
9
+ const isOpen = !!node.hasChildren && open.includes(node.id);
10
+ rows.push({ node, level, position: i + 1, siblings: nodes.length, parent, open: isOpen });
11
+ if (isOpen)
12
+ walk(node.id, level + 1);
13
+ });
14
+ };
15
+ walk(null, 1);
16
+ return rows;
17
+ }
18
+ /**
19
+ * What a key does on the row at `at`, or null when the tree does not answer it.
20
+ * Right and left swap under a right-to-left page. A letter goes to the next row
21
+ * starting with it, wrapping at the end.
22
+ */
23
+ export function treeStep(rows, at, key, rtl = false) {
24
+ const row = rows[at];
25
+ const focus = (target) => (target ? { focus: target.node.id } : null);
26
+ switch (key) {
27
+ case "ArrowDown":
28
+ return focus(rows[at + 1]);
29
+ case "ArrowUp":
30
+ return focus(rows[at - 1]);
31
+ case "Home":
32
+ return focus(rows[0]);
33
+ case "End":
34
+ return focus(rows[rows.length - 1]);
35
+ case "ArrowRight":
36
+ case "ArrowLeft": {
37
+ const inward = key === (rtl ? "ArrowLeft" : "ArrowRight");
38
+ if (inward) {
39
+ if (!row.node.hasChildren)
40
+ return null;
41
+ return row.open ? focus(rows[at + 1]?.parent === row.node.id ? rows[at + 1] : undefined) : { open: row.node.id };
42
+ }
43
+ if (row.open)
44
+ return { close: row.node.id };
45
+ return row.parent === null ? null : { focus: row.parent };
46
+ }
47
+ }
48
+ if (key.length !== 1 || key === " ")
49
+ return null;
50
+ const letter = key.toLowerCase();
51
+ const after = [...rows.slice(at + 1), ...rows.slice(0, at)];
52
+ return focus(after.find((r) => r.node.label.toLowerCase().startsWith(letter)));
53
+ }
54
+ /** Puts the focus on a row. */
55
+ export function focusTreeRow(tree, id) {
56
+ tree.querySelector(`[data-tree-id="${CSS.escape(id)}"]`)?.focus();
57
+ }
package/dist/version.d.ts CHANGED
@@ -2,4 +2,4 @@
2
2
  * The version every GlimStone package carries, so an app's Version card names
3
3
  * the one it runs. All packages share it and move together.
4
4
  */
5
- export declare const GLIMSTONE_VERSION = "4.1.0";
5
+ export declare const GLIMSTONE_VERSION = "4.1.2";
package/dist/version.js CHANGED
@@ -2,4 +2,4 @@
2
2
  * The version every GlimStone package carries, so an app's Version card names
3
3
  * the one it runs. All packages share it and move together.
4
4
  */
5
- export const GLIMSTONE_VERSION = "4.1.0";
5
+ export const GLIMSTONE_VERSION = "4.1.2";
package/glyph-map.md CHANGED
@@ -384,11 +384,11 @@ FireSquire wears seven glyphs on the buttons of its pre-reboot check and on the
384
384
 
385
385
  <br>
386
386
 
387
- ## CastleCommander
387
+ ## CannonCommander
388
388
 
389
- CastleCommander's new interface draws every glyph from the assortment. The rows below are the meanings that came into it with the file manager, and the others follow once its screens are built.
389
+ CannonCommander's new interface draws every glyph from the assortment. The rows below are the meanings that came into it with the file manager, and the others follow once its screens are built.
390
390
 
391
- | Meaning | GlimStone | CastleCommander |
391
+ | Meaning | GlimStone | CannonCommander |
392
392
  | --- | --- | --- |
393
393
  | Reset one row of a colour card to its defaults | `IconResetArrow` | `IconResetArrow`, standard |
394
394
  | Give through PayPal | `IconPayPal` | `IconPayPal`, standard |
@@ -416,7 +416,7 @@ CastleCommander's new interface draws every glyph from the assortment. The rows
416
416
 
417
417
  - **ArrowLoop:** `scripts/gen_glyphs.py` writes the app's glyphs, and the Look settings glyphs come from it, for the web and, through `scripts/gen_glyph_data.py`, for the phone. `web/src/lib/glyphName.ts` maps a translation key to a glyph name, and a key it does not know keeps its text.
418
418
  - **BombVault:** `scripts/gen_glyphs.py` writes `web/src/components/glyphs.tsx` and `navGlyphs.tsx` from a copy of `glyphs.json` at 16px; the phone app shows the web launcher and uses the same files. `glyphFor.tsx` maps a translation key to a glyph when a button passes none; a button that passes one wins.
419
- - **CastleCommander:** the frontend maps a file's kind to a glyph name through `prototype/src/formats.json` and draws the entry from `glyphs.json` through `@glimstone/vue`.
419
+ - **CannonCommander:** the frontend maps a file's kind to a glyph name through `prototype/src/formats.json` and draws the entry from `glyphs.json` through `@glimstone/vue`.
420
420
  - **FireSquire:** `firesquire-glyphs.js` beside its scripts holds a copy of every entry it draws, the square viewBox and the drawing, and names the GlimStone release it came from. `plugin/test/glyphs.test.js` checks that every glyph a button asks for is in the copy.
421
421
  - **KnightLoader:** `web/scripts/gen-glyphs.mjs` writes `web/src/lib/glyphs.tsx` at 22px with the ink at three quarters of the box; the Android app has no SVG and shows tinted bitmaps rendered from the same list into `mobile/assets/`. Every call site passes its glyph; Settings page glyphs are mapped in `pages/settings/pageIcons.tsx`. The browser extension copies the eye and a few others in `extension/src/`.
422
422
  - **ShipLog:** `include/glyphs.php` holds a copy of every entry it draws. `shiplog.page` uses them on the settings page, and `shiplog.Docker.page` hands four of them to `scripts/docker.js` for the changelog window.