@glimstone/core 4.0.0 → 4.1.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.js CHANGED
@@ -3,6 +3,8 @@ export * from "./appMarks.js";
3
3
  export * from "./badge.js";
4
4
  export * from "./bubblePosition.js";
5
5
  export * from "./button.js";
6
+ export * from "./clipboard.js";
7
+ export * from "./colorNames.js";
6
8
  export * from "./colorPicker.js";
7
9
  export * from "./controls.js";
8
10
  export * from "./dialog.js";
@@ -13,9 +15,12 @@ export * from "./infoTile.js";
13
15
  export * from "./inputModality.js";
14
16
  export * from "./marks.js";
15
17
  export * from "./media.js";
18
+ export * from "./menu.js";
19
+ export * from "./meter.js";
16
20
  export * from "./motionNative.js";
17
21
  export * from "./numberField.js";
18
22
  export * from "./paypal.js";
23
+ export * from "./pill.js";
19
24
  export * from "./portalHue.js";
20
25
  export * from "./readmeButton.js";
21
26
  export * from "./segmentLayout.js";
@@ -29,3 +34,4 @@ export * from "./tipBubble.js";
29
34
  export * from "./toastEngine.js";
30
35
  export * from "./tooltip.js";
31
36
  export * from "./version.js";
37
+ export * from "./window.js";
package/dist/marks.d.ts CHANGED
@@ -41,6 +41,30 @@ export declare const MARKS: {
41
41
  size: number;
42
42
  body: string;
43
43
  };
44
+ /** `IconPencil`: the edit button at the end of a menu's heading. */
45
+ pencil: {
46
+ viewBox: string;
47
+ size: number;
48
+ body: string;
49
+ };
50
+ /** `IconEye`: the reveal eye of a PasswordField while the secret is hidden. */
51
+ eye: {
52
+ viewBox: string;
53
+ size: number;
54
+ body: string;
55
+ };
56
+ /** `IconEyeOff`: the same eye once the secret shows, to hide it again. */
57
+ eyeOff: {
58
+ viewBox: string;
59
+ size: number;
60
+ body: string;
61
+ };
62
+ /** `IconCopy`: a CopyField's button. */
63
+ copy: {
64
+ viewBox: string;
65
+ size: number;
66
+ body: string;
67
+ };
44
68
  /** The small cross that dismisses a toast. */
45
69
  dismiss: {
46
70
  viewBox: string;
package/dist/marks.js CHANGED
@@ -48,6 +48,30 @@ export const MARKS = {
48
48
  size: 16,
49
49
  body: '<path fill-rule="evenodd" clip-rule="evenodd" d="M2.5 3h6A2.5 2.5 0 0 1 11 5.5v6A2.5 2.5 0 0 1 8.5 14h-6A2.5 2.5 0 0 1 0 11.5v-6A2.5 2.5 0 0 1 2.5 3Zm0 2a.5.5 0 0 0-.5.5v6a.5.5 0 0 0 .5.5h6a.5.5 0 0 0 .5-.5v-6a.5.5 0 0 0-.5-.5Z"/><path d="M5 0h6.5A2.5 2.5 0 0 1 14 2.5V9h-2V2.5a.5.5 0 0 0-.5-.5H5Z"/>',
50
50
  },
51
+ /** `IconPencil`: the edit button at the end of a menu's heading. */
52
+ pencil: {
53
+ viewBox: "-0.002 -0.001 14.001 14.001",
54
+ size: 14,
55
+ body: '<path d="M10.715 -0.000976562c-0.1998 0 -0.3976 0.039922962 -0.5818 0.117424562 -0.18301 0.07702 -0.34892 0.18961 -0.48808 0.331228L1.40732 8.64549c-0.06116 0.06086 -0.10553 0.13651 -0.12879 0.21959L0.0185299 13.3651c-0.0487051 0.1739 0.0001992 0.3606 0.1279281 0.4884 0.12773 0.1277 0.314422 0.1766 0.488369 0.1279l4.500003 -1.26c0.08308 -0.0233 0.15873 -0.0677 0.21959 -0.1288l8.19768 -8.2377 0.0014 -0.00135c0.1399 -0.13917 0.251 -0.30459 0.3269 -0.48679 0.0762 -0.18278 0.1154 -0.37884 0.1154 -0.57686 0 -0.19802 -0.0392 -0.39408 -0.1154 -0.57687 -0.0759 -0.1822 -0.187 -0.34763 -0.3269 -0.48679l-0.0014 -0.00134L11.7859 0.448721c-0.1393 -0.14211 -0.3056 -0.255064 -0.4891 -0.332273 -0.1842 -0.0775016 -0.382 -0.117424562 -0.5818 -0.117424562Z"/>',
56
+ },
57
+ /** `IconEye`: the reveal eye of a PasswordField while the secret is hidden. */
58
+ eye: {
59
+ viewBox: "0 0 14 14",
60
+ size: 16,
61
+ body: '<g><path fill-rule="evenodd" d="M2.9327 3.49099C4.0559 2.68177 5.4556 2 7 2c1.54441 0 2.9441 0.68177 4.0673 1.49099 1.1273 0.81215 2.0197 1.78397 2.5599 2.4369l0.0045 0.00553c0.2413 0.3002 0.3683 0.68062 0.3683 1.06664 0 0.38601 -0.127 0.76644 -0.3683 1.06664l-0.0045 0.00553c-0.5402 0.65292 -1.4326 1.62475 -2.5599 2.43687 -1.1232 0.8092 -2.52289 1.491 -4.0673 1.491 -1.5444 0 -2.9441 -0.6818 -4.0673 -1.491C1.80544 9.69698 0.913028 8.72515 0.37279 8.07223L0.36828 8.0667C0.127025 7.7665 0 7.38607 0 7.00006c0 -0.38602 0.127025 -0.76644 0.36828 -1.06664l0.00451 -0.00553c0.540238 -0.65293 1.43265 -1.62475 2.55991 -2.4369ZM7 9.25c1.24264 0 2.25 -1.00736 2.25 -2.25S8.24264 4.75 7 4.75 4.75 5.75736 4.75 7 5.75736 9.25 7 9.25Z" clip-rule="evenodd"></path></g>',
62
+ },
63
+ /** `IconEyeOff`: the same eye once the secret shows, to hide it again. */
64
+ eyeOff: {
65
+ viewBox: "0.044 0.044 13.912 13.912",
66
+ size: 16,
67
+ body: '<g><path fill-rule="evenodd" d="M0.263638 1.32434c-0.2928929 -0.2929 -0.2928929 -0.767771 0 -1.060664 0.292894 -0.2928929 0.767772 -0.2928929 1.060662 0L3.95291 2.89228c0.90784 -0.49884 1.93998 -0.85203 3.04708 -0.85203 1.53199 0 2.92043 0.67629 4.03461 1.479 1.1182 0.80562 2.0034 1.76963 2.5393 2.41731l0.0045 0.00548c0.2393 0.29779 0.3653 0.67515 0.3653 1.05806s-0.126 0.76028 -0.3653 1.05806l-0.0045 0.00549c-0.4886 0.59054 -1.2677 1.44407 -2.2489 2.20075l2.4114 2.4113c0.2929 0.2929 0.2929 0.7678 0 1.0607 -0.2929 0.2929 -0.7678 0.2929 -1.0607 0L0.263638 1.32434ZM9.01713 7.95651c0.13771 -0.2899 0.21477 -0.61419 0.21477 -0.95649 0 -1.23264 -0.99926 -2.2319 -2.23191 -2.2319 -0.34229 0 -0.66659 0.07705 -0.95649 0.21476l2.97363 2.97363ZM0.426074 5.93656c0.319448 -0.38609 0.763036 -0.88458 1.306096 -1.39491l7.10402 7.10405c-0.58062 0.1961 -1.19587 0.3143 -1.8362 0.3143 -1.53198 0 -2.92042 -0.6763 -4.03459 -1.479C1.8472 9.67534 0.961967 8.71133 0.426074 8.06365L0.4216 8.05816C0.182285 7.76038 0.0562814 7.38301 0.0562814 7.0001S0.182285 6.23983 0.4216 5.94204l0.004474 -0.00548Z" clip-rule="evenodd"></path></g>',
68
+ },
69
+ /** `IconCopy`: a CopyField's button. */
70
+ copy: {
71
+ viewBox: "2 2 20 20",
72
+ size: 16,
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
+ },
51
75
  /** The small cross that dismisses a toast. */
52
76
  dismiss: {
53
77
  viewBox: "0 0 16 16",
package/dist/menu.d.ts ADDED
@@ -0,0 +1,107 @@
1
+ export interface MenuItem<G> {
2
+ label: string;
3
+ glyph?: G;
4
+ /** A colour dot where the glyph would stand, such as a label's colour. */
5
+ color?: string;
6
+ /** The key that runs the same thing, shown as a key cap at the row's end. */
7
+ shortcut?: string;
8
+ /** A figure at the row's end, such as how many files a filter would show. */
9
+ count?: number;
10
+ disabled?: boolean;
11
+ /** Set for an item that is on or off. A checkbox item leaves the menu open,
12
+ * so several can be ticked in a row. */
13
+ checked?: boolean;
14
+ /** With `checked`: one choice among several. It keeps its glyph, ticks the
15
+ * current choice at the end and closes the menu like a plain item. */
16
+ radio?: boolean;
17
+ /** Entries that open beside this row. */
18
+ sub?: MenuEntry<G>[];
19
+ onSelect?: () => void;
20
+ }
21
+ export interface MenuHead<G> {
22
+ head: string;
23
+ /** A pencil at the heading's end, for a group the person keeps themselves,
24
+ * such as bookmarks. */
25
+ edit?: {
26
+ tip: string;
27
+ glyph?: G;
28
+ onSelect: () => void;
29
+ };
30
+ }
31
+ /** An item, a heading, or "-" for a separator. */
32
+ export type MenuEntry<G> = MenuItem<G> | MenuHead<G> | "-";
33
+ export declare function isMenuHead<G>(entry: MenuEntry<G>): entry is MenuHead<G>;
34
+ /**
35
+ * Drops the separators a list built from parts ends up with: at either end,
36
+ * twice in a row and right under a heading. A menu assembled from whatever
37
+ * applies to the selection then needs no care at each call site.
38
+ */
39
+ export declare function tidyMenu<G>(entries: readonly MenuEntry<G>[]): MenuEntry<G>[];
40
+ export interface MenuRowView {
41
+ role: "menuitem" | "menuitemcheckbox" | "menuitemradio";
42
+ /** What stands before the label. */
43
+ lead: "box" | "dot" | "glyph" | "blank";
44
+ /** What stands at the row's end, one thing at most. */
45
+ end: "submenu" | "tick" | "shortcut" | "count" | null;
46
+ /** The radio item that is the current choice, washed with the accent. */
47
+ current: boolean;
48
+ /** A press leaves the menu open. */
49
+ staysOpen: boolean;
50
+ }
51
+ /** What one row shows. A row without a glyph keeps the glyph's room, so every label starts at one edge. */
52
+ export declare function menuRowView<G>(item: MenuItem<G>): MenuRowView;
53
+ interface Size {
54
+ width: number;
55
+ height: number;
56
+ }
57
+ /**
58
+ * Where a menu opened at a point goes: at the point, pushed back inside the
59
+ * window by its own size. `above` stands it on the point instead, for a menu
60
+ * opened from a bar at the bottom.
61
+ */
62
+ export declare function placeMenu(x: number, y: number, menu: Size, view: Size, above?: boolean): {
63
+ left: number;
64
+ top: number;
65
+ };
66
+ /**
67
+ * Where a submenu goes: beside its row on the reading side, on the other side
68
+ * when that has no room, and level with the row as far as the window allows.
69
+ */
70
+ export declare function placeSubmenu(row: {
71
+ left: number;
72
+ right: number;
73
+ top: number;
74
+ }, menu: Size, view: Size, rtl?: boolean): {
75
+ left: number;
76
+ top: number;
77
+ };
78
+ export type MenuMove = "next" | "prev" | "first" | "last" | "in" | "out";
79
+ /** What a key does inside an open menu. Right and left swap under a right-to-left page. */
80
+ export declare function menuMove(key: string, rtl?: boolean): MenuMove | null;
81
+ /** The rows a key can reach, in order: every enabled button of the menu. */
82
+ export declare function menuRows(menu: HTMLElement): HTMLElement[];
83
+ /**
84
+ * Puts the focus on a menu's first row as it opens. A menu the pointer opened
85
+ * carries `data-pointer` until a key is pressed in it, and the row shows no
86
+ * ring or wash meanwhile: the mouse never asked for that focus, and a lit
87
+ * first row reads as a choice already made.
88
+ */
89
+ export declare function focusFirstMenuRow(menu: HTMLElement): void;
90
+ /** Moves the focus along a menu's rows, wrapping at both ends. */
91
+ export declare function focusMenuRow(menu: HTMLElement, move: "next" | "prev" | "first" | "last"): void;
92
+ /**
93
+ * Registers a menu as the open one and closes any other. `close` hears
94
+ * whether Escape closed it, which hands focus back, or another menu took its
95
+ * place. Returns the release to call when it closes.
96
+ */
97
+ export declare function claimMenu(close: (byEscape: boolean) => void): () => void;
98
+ /** Whether a menu is open, for a key handler of the app's that should leave it alone. */
99
+ export declare function menuIsOpen(): boolean;
100
+ /**
101
+ * Closes a menu on a click anywhere outside the given elements, such as the
102
+ * menu, its submenu and the control that opened it. A click rather than a
103
+ * press: the window under a menu reads the press, and while the menu is still
104
+ * open it knows to stay. Returns the cleanup.
105
+ */
106
+ export declare function closeMenuOutside(inside: () => (Element | null | undefined)[], close: () => void): () => void;
107
+ export {};
package/dist/menu.js ADDED
@@ -0,0 +1,163 @@
1
+ // The model behind ContextMenu and MenuBar: what an entry is, how a list is
2
+ // tidied, what each row shows, where a menu goes on the screen and which key
3
+ // does what. The frameworks only draw it; the glyph type is theirs.
4
+ import { pointerWasLast, trackInputModality } from "./inputModality.js";
5
+ // Listening starts with the module rather than with the first menu, which
6
+ // mounts after the press that opened it and would miss it.
7
+ trackInputModality();
8
+ export function isMenuHead(entry) {
9
+ return typeof entry === "object" && "head" in entry;
10
+ }
11
+ /**
12
+ * Drops the separators a list built from parts ends up with: at either end,
13
+ * twice in a row and right under a heading. A menu assembled from whatever
14
+ * applies to the selection then needs no care at each call site.
15
+ */
16
+ export function tidyMenu(entries) {
17
+ return entries.filter((e, i, all) => {
18
+ if (e !== "-")
19
+ return true;
20
+ const before = all[i - 1];
21
+ return i > 0 && i < all.length - 1 && before !== "-" && !isMenuHead(before);
22
+ });
23
+ }
24
+ /** What one row shows. A row without a glyph keeps the glyph's room, so every label starts at one edge. */
25
+ export function menuRowView(item) {
26
+ const check = item.checked !== undefined;
27
+ const box = check && !item.radio;
28
+ const on = item.checked === true;
29
+ return {
30
+ role: !check ? "menuitem" : item.radio ? "menuitemradio" : "menuitemcheckbox",
31
+ lead: box ? "box" : item.color ? "dot" : item.glyph ? "glyph" : "blank",
32
+ end: item.sub
33
+ ? "submenu"
34
+ : item.radio && on
35
+ ? "tick"
36
+ : item.shortcut
37
+ ? "shortcut"
38
+ : item.count !== undefined
39
+ ? "count"
40
+ : null,
41
+ current: !!item.radio && on,
42
+ staysOpen: box,
43
+ };
44
+ }
45
+ const EDGE = 8;
46
+ /**
47
+ * Where a menu opened at a point goes: at the point, pushed back inside the
48
+ * window by its own size. `above` stands it on the point instead, for a menu
49
+ * opened from a bar at the bottom.
50
+ */
51
+ export function placeMenu(x, y, menu, view, above = false) {
52
+ return {
53
+ left: Math.max(EDGE, Math.min(x, view.width - menu.width - EDGE)),
54
+ top: above ? Math.max(EDGE, y - menu.height - EDGE) : Math.max(EDGE, Math.min(y, view.height - menu.height - EDGE)),
55
+ };
56
+ }
57
+ /**
58
+ * Where a submenu goes: beside its row on the reading side, on the other side
59
+ * when that has no room, and level with the row as far as the window allows.
60
+ */
61
+ export function placeSubmenu(row, menu, view, rtl = false) {
62
+ const after = row.right + 6;
63
+ const before = row.left - menu.width - 6;
64
+ const fitsAfter = after + menu.width <= view.width;
65
+ const fitsBefore = before >= 0;
66
+ const left = rtl ? (fitsBefore ? before : after) : fitsAfter ? after : before;
67
+ return { left, top: Math.max(EDGE, Math.min(row.top - 6, view.height - menu.height - EDGE)) };
68
+ }
69
+ /** What a key does inside an open menu. Right and left swap under a right-to-left page. */
70
+ export function menuMove(key, rtl = false) {
71
+ switch (key) {
72
+ case "ArrowDown":
73
+ return "next";
74
+ case "ArrowUp":
75
+ return "prev";
76
+ case "Home":
77
+ return "first";
78
+ case "End":
79
+ return "last";
80
+ case "ArrowRight":
81
+ return rtl ? "out" : "in";
82
+ case "ArrowLeft":
83
+ return rtl ? "in" : "out";
84
+ }
85
+ return null;
86
+ }
87
+ /** The rows a key can reach, in order: every enabled button of the menu. */
88
+ export function menuRows(menu) {
89
+ return Array.from(menu.querySelectorAll("button:not([disabled])"));
90
+ }
91
+ /**
92
+ * Puts the focus on a menu's first row as it opens. A menu the pointer opened
93
+ * carries `data-pointer` until a key is pressed in it, and the row shows no
94
+ * ring or wash meanwhile: the mouse never asked for that focus, and a lit
95
+ * first row reads as a choice already made.
96
+ */
97
+ export function focusFirstMenuRow(menu) {
98
+ if (pointerWasLast()) {
99
+ menu.dataset.pointer = "";
100
+ menu.addEventListener("keydown", () => delete menu.dataset.pointer, { once: true });
101
+ }
102
+ menuRows(menu)[0]?.focus();
103
+ }
104
+ /** Moves the focus along a menu's rows, wrapping at both ends. */
105
+ export function focusMenuRow(menu, move) {
106
+ const rows = menuRows(menu);
107
+ if (rows.length === 0)
108
+ return;
109
+ const last = rows.length - 1;
110
+ const at = rows.indexOf(document.activeElement);
111
+ let next = move === "first" ? 0 : move === "last" ? last : at + (move === "next" ? 1 : -1);
112
+ if (at < 0 && move === "prev")
113
+ next = last;
114
+ rows[(next + rows.length) % rows.length].focus();
115
+ }
116
+ // One menu is open at a time in the whole document. Opening one closes the
117
+ // last, and Escape reaches the open one from anywhere, so a window under it
118
+ // keeps its own Escape for later.
119
+ let openMenu = null;
120
+ function onMenuKey(e) {
121
+ if (e.key !== "Escape" || !openMenu)
122
+ return;
123
+ e.preventDefault();
124
+ openMenu(true);
125
+ }
126
+ /**
127
+ * Registers a menu as the open one and closes any other. `close` hears
128
+ * whether Escape closed it, which hands focus back, or another menu took its
129
+ * place. Returns the release to call when it closes.
130
+ */
131
+ export function claimMenu(close) {
132
+ const previous = openMenu;
133
+ openMenu = close;
134
+ if (previous && previous !== close)
135
+ previous(false);
136
+ else if (!previous)
137
+ document.addEventListener("keydown", onMenuKey);
138
+ return () => {
139
+ if (openMenu !== close)
140
+ return;
141
+ openMenu = null;
142
+ document.removeEventListener("keydown", onMenuKey);
143
+ };
144
+ }
145
+ /** Whether a menu is open, for a key handler of the app's that should leave it alone. */
146
+ export function menuIsOpen() {
147
+ return openMenu !== null;
148
+ }
149
+ /**
150
+ * Closes a menu on a click anywhere outside the given elements, such as the
151
+ * menu, its submenu and the control that opened it. A click rather than a
152
+ * press: the window under a menu reads the press, and while the menu is still
153
+ * open it knows to stay. Returns the cleanup.
154
+ */
155
+ export function closeMenuOutside(inside, close) {
156
+ function onClick(e) {
157
+ const target = e.target;
158
+ if (!inside().some((el) => el?.contains(target)))
159
+ close();
160
+ }
161
+ document.addEventListener("click", onClick, true);
162
+ return () => document.removeEventListener("click", onClick, true);
163
+ }
@@ -0,0 +1,32 @@
1
+ export type MeterSize = "small" | "medium";
2
+ export interface BarView {
3
+ /** The track's class list. */
4
+ className: string;
5
+ /** The fill's width as a CSS percentage, or null while indeterminate. */
6
+ width: string | null;
7
+ /** The value in whole percent for aria-valuenow, or null while indeterminate. */
8
+ percent: number | null;
9
+ }
10
+ export interface MeterOptions {
11
+ size?: MeterSize;
12
+ /** From here on the fill is the fault colour. */
13
+ threshold?: number;
14
+ /** Slim and grey, for a footer line or a dense list where an accent bar on
15
+ * every row would shout. */
16
+ quiet?: boolean;
17
+ }
18
+ /**
19
+ * A meter's look. At or past `threshold` the fill turns to the fault colour,
20
+ * quiet or not, since a disk that is nearly full is a problem to see from
21
+ * across the room.
22
+ */
23
+ export declare function meterView(value: number | null | undefined, { size, threshold, quiet }?: MeterOptions): BarView;
24
+ /**
25
+ * A progress bar's look. The sheen runs only while the work does: it stops
26
+ * when the bar is paused, finished or failed, since a light still travelling a
27
+ * finished bar contradicts the number beside it.
28
+ */
29
+ export declare function progressView(value: number | null | undefined, state?: {
30
+ paused?: boolean;
31
+ failed?: boolean;
32
+ }): BarView;
package/dist/meter.js ADDED
@@ -0,0 +1,44 @@
1
+ // What a Meter and a ProgressBar show for a value. A meter is a level that
2
+ // stands, such as how full a disk is; a progress bar is work that moves toward
3
+ // its end. Both take a value from 0 to 1, and without one they are busy: the
4
+ // level is still being read, or the work has no measure yet.
5
+ function measure(value) {
6
+ if (value == null || Number.isNaN(value))
7
+ return { v: null, width: null, percent: null };
8
+ const v = Math.min(1, Math.max(0, value));
9
+ return { v, width: `${Math.round(v * 1000) / 10}%`, percent: Math.round(v * 100) };
10
+ }
11
+ /**
12
+ * A meter's look. At or past `threshold` the fill turns to the fault colour,
13
+ * quiet or not, since a disk that is nearly full is a problem to see from
14
+ * across the room.
15
+ */
16
+ export function meterView(value, { size = "medium", threshold = 0.9, quiet = false } = {}) {
17
+ const { v, width, percent } = measure(value);
18
+ const classes = ["glim-meter", "glim-meter-track", `glim-meter-track--${size}`];
19
+ if (quiet)
20
+ classes.push("glim-meter-track--quiet");
21
+ if (v === null)
22
+ classes.push("glim-meter-track--busy");
23
+ else if (v >= threshold)
24
+ classes.push("glim-meter-track--full");
25
+ return { className: classes.join(" "), width, percent };
26
+ }
27
+ /**
28
+ * A progress bar's look. The sheen runs only while the work does: it stops
29
+ * when the bar is paused, finished or failed, since a light still travelling a
30
+ * finished bar contradicts the number beside it.
31
+ */
32
+ export function progressView(value, state = {}) {
33
+ const { v, width, percent } = measure(value);
34
+ const classes = ["glim-meter", "glim-progress"];
35
+ if (v === null)
36
+ classes.push("glim-progress--busy");
37
+ if (state.failed)
38
+ classes.push("glim-progress--failed");
39
+ else if (state.paused)
40
+ classes.push("glim-progress--paused");
41
+ else if (v === 1)
42
+ classes.push("glim-progress--done");
43
+ return { className: classes.join(" "), width, percent };
44
+ }
package/dist/pill.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export type PillTone = "neutral" | "state" | "ok" | "warn" | "fail";
2
+ /** The class list of a pill. */
3
+ export declare function pillClassName(tone?: PillTone): string;
package/dist/pill.js ADDED
@@ -0,0 +1,7 @@
1
+ // The small data pill of a list row: a size, a date, a state. A Badge says
2
+ // something about the row; a pill is one of its values, so it stays quiet and
3
+ // takes a state colour only as a wash.
4
+ /** The class list of a pill. */
5
+ export function pillClassName(tone = "neutral") {
6
+ return tone === "neutral" ? "glim-pill" : `glim-pill glim-pill--${tone}`;
7
+ }
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.0.0";
5
+ export declare const GLIMSTONE_VERSION = "4.1.0";
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.0.0";
5
+ export const GLIMSTONE_VERSION = "4.1.0";
@@ -0,0 +1,29 @@
1
+ export type WindowSize = "normal" | "wide" | "full";
2
+ export interface WindowActionBase {
3
+ /** The forward action, filled with the accent. Its presence makes the way out Cancel rather than Close. */
4
+ primary?: boolean;
5
+ /** This action already leaves the window, such as Done, so no Cancel or Close is added. */
6
+ wayOut?: boolean;
7
+ }
8
+ /**
9
+ * The foot's buttons in order. A window whose foot has anything in it can be
10
+ * left from there: without a way out among its actions it gets Cancel when
11
+ * one of them goes forward and Close otherwise, before the others, so the
12
+ * forward action stays last.
13
+ */
14
+ export declare function windowFoot<A extends WindowActionBase>(actions: readonly A[], hasStart: boolean, wayOut: {
15
+ cancel: A;
16
+ close: A;
17
+ }): A[];
18
+ /**
19
+ * Puts a window on the stack. Escape closes the one in front and Tab stays
20
+ * inside it. Returns the removal, to call however the window closes.
21
+ */
22
+ export declare function stackWindow(card: () => HTMLElement | null, close: () => void): () => void;
23
+ /** Whether a window is open, for a key handler of the app's that should leave the page alone. */
24
+ export declare function windowIsOpen(): boolean;
25
+ /**
26
+ * Where focus goes as a window opens: its first field, so typing can start at
27
+ * once, or else the last button of its foot, the forward action.
28
+ */
29
+ export declare function windowFocusTarget(card: HTMLElement): HTMLElement | null;
package/dist/window.js ADDED
@@ -0,0 +1,62 @@
1
+ // What every Window shares, whatever it holds ("Windows" in the design
2
+ // language): a foot that always offers a way out, a stack in which Escape and
3
+ // Tab belong to the window in front, and where focus lands when one opens.
4
+ import { keepTabInside } from "./dialog.js";
5
+ import { menuIsOpen } from "./menu.js";
6
+ /**
7
+ * The foot's buttons in order. A window whose foot has anything in it can be
8
+ * left from there: without a way out among its actions it gets Cancel when
9
+ * one of them goes forward and Close otherwise, before the others, so the
10
+ * forward action stays last.
11
+ */
12
+ export function windowFoot(actions, hasStart, wayOut) {
13
+ if ((actions.length === 0 && !hasStart) || actions.some((a) => a.wayOut))
14
+ return [...actions];
15
+ return [actions.some((a) => a.primary) ? wayOut.cancel : wayOut.close, ...actions];
16
+ }
17
+ const stack = [];
18
+ // A key that an open menu or a Select inside the window already handled is
19
+ // left alone, so Escape closes one thing at a time from the front.
20
+ function onKey(e) {
21
+ const top = stack[stack.length - 1];
22
+ if (!top || e.defaultPrevented)
23
+ return;
24
+ if (e.key === "Escape") {
25
+ if (menuIsOpen())
26
+ return;
27
+ e.preventDefault();
28
+ top.close();
29
+ }
30
+ else if (e.key === "Tab")
31
+ keepTabInside(top.card(), e);
32
+ }
33
+ /**
34
+ * Puts a window on the stack. Escape closes the one in front and Tab stays
35
+ * inside it. Returns the removal, to call however the window closes.
36
+ */
37
+ export function stackWindow(card, close) {
38
+ const entry = { card, close };
39
+ stack.push(entry);
40
+ if (stack.length === 1)
41
+ document.addEventListener("keydown", onKey);
42
+ return () => {
43
+ const at = stack.indexOf(entry);
44
+ if (at >= 0)
45
+ stack.splice(at, 1);
46
+ if (stack.length === 0)
47
+ document.removeEventListener("keydown", onKey);
48
+ };
49
+ }
50
+ /** Whether a window is open, for a key handler of the app's that should leave the page alone. */
51
+ export function windowIsOpen() {
52
+ return stack.length > 0;
53
+ }
54
+ const FIELD = 'input:not([type="file"]):not([type="hidden"]):not([disabled]), textarea:not([disabled])';
55
+ /**
56
+ * Where focus goes as a window opens: its first field, so typing can start at
57
+ * once, or else the last button of its foot, the forward action.
58
+ */
59
+ export function windowFocusTarget(card) {
60
+ const foot = card.querySelectorAll(".glim-modal-foot button:not([disabled])");
61
+ return card.querySelector(FIELD) ?? foot[foot.length - 1] ?? null;
62
+ }
package/glyph-map.md CHANGED
@@ -13,6 +13,7 @@
13
13
  | Show, reveal | `IconEye` | `IconEye`, standard | `IconEye`, standard |
14
14
  | Hide again | `IconEyeOff` | `IconEyeOff`, standard | `IconEyeOff`, standard |
15
15
  | Reset to the default | `IconRefresh` | `IconClearSelection`, switch | `IconRefresh`, standard |
16
+ | Reset one row of a colour card to its defaults | `IconResetArrow` | `IconResetArrow` in `AccentCard.tsx`, switch | none |
16
17
  | Retry, reload, refresh | `IconRetry` | `IconRefresh`, switch | `IconRetry`, standard |
17
18
  | Upload | `IconUpload` | `IconUpload`, standard | `IconUpload`, standard |
18
19
  | Import, read in | `IconImport` | `IconImport`, standard | `IconImport`, standard |
@@ -82,6 +83,8 @@
82
83
  | Write to us | `IconMail` | `IconMail`, standard | `IconMail`, standard |
83
84
  | Send a test | `IconSendTest` | `IconCheck`, switch | `IconSendTest`, standard |
84
85
  | Buy the author a coffee | `IconCoffee` | `IconCoffee`, standard | none |
86
+ | Give through PayPal | `IconPayPal` | `IconPayPal` in `donateMarks.tsx`, switch | `IconPayPal` in `donateMarks.tsx`, switch |
87
+ | Give from a crypto wallet | `IconBitcoin` | `IconBitcoin` in `donateMarks.tsx`, switch | `IconBitcoin` in `donateMarks.tsx`, switch |
85
88
  | A protection is on, turn it on | `IconShieldOn` | `IconShieldOn`, standard | `IconShieldOn`, standard |
86
89
  | A protection is off, turn it off | `IconShieldOff` | `IconShieldOff`, standard | `IconShieldOff`, standard |
87
90
  | Store uncompressed | `IconCompressOff` | `IconCompressOff`, standard | none |
@@ -125,6 +128,22 @@
125
128
  | A subtitle file | `IconFileSubtitles` | none | `IconFileSubtitles` before a subtitle file, standard |
126
129
  | A description or another text file | `IconFileText` | none | `IconFileText` before a description or a text file, standard |
127
130
  | A file, an attachment | `IconFile` | none | `IconFile` before any other file, standard |
131
+ | A RAW photo straight from the camera | `IconFileRaw` | none | none |
132
+ | A design or vector graphic | `IconFileDesign` | none | none |
133
+ | A playlist | `IconFilePlaylist` | none | none |
134
+ | A PDF document | `IconFilePdf` | none | none |
135
+ | A text document from an office suite | `IconFileDoc` | none | none |
136
+ | A spreadsheet | `IconFileSheet` | none | none |
137
+ | A presentation | `IconFileSlides` | none | none |
138
+ | An e-book | `IconBook` | none | none |
139
+ | A disk image or virtual disk | `IconDisc` | none | none |
140
+ | A program or installable package | `IconFileProgram` | none | none |
141
+ | A font | `IconFont` | none | none |
142
+ | A checksum or signature | `IconHash` | none | none |
143
+ | A torrent or magnet link | `IconMagnet` | none | none |
144
+ | A 3D model or CAD drawing | `IconCube` | none | none |
145
+ | A calendar or contact card | `IconCalendar` | none | none |
146
+ | A backup, temporary or unfinished file | `IconHourglass` | none | none |
128
147
  | Instances, other boxes | `IconFleet` | `IconFleet`, standard | `IconFleet`, standard |
129
148
  | Collector, taking links in | `IconCollector` | none | `IconCollector`, standard |
130
149
  | Accounts | `IconAccounts` | none | `IconAccounts`, standard |
@@ -365,10 +384,39 @@ FireSquire wears seven glyphs on the buttons of its pre-reboot check and on the
365
384
 
366
385
  <br>
367
386
 
387
+ ## CastleCommander
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.
390
+
391
+ | Meaning | GlimStone | CastleCommander |
392
+ | --- | --- | --- |
393
+ | Reset one row of a colour card to its defaults | `IconResetArrow` | `IconResetArrow`, standard |
394
+ | Give through PayPal | `IconPayPal` | `IconPayPal`, standard |
395
+ | Give from a crypto wallet | `IconBitcoin` | `IconBitcoin`, standard |
396
+ | A RAW photo straight from the camera | `IconFileRaw` | `IconFileRaw`, standard |
397
+ | A design or vector graphic | `IconFileDesign` | `IconFileDesign`, standard |
398
+ | A playlist | `IconFilePlaylist` | `IconFilePlaylist`, standard |
399
+ | A PDF document | `IconFilePdf` | `IconFilePdf`, standard |
400
+ | A text document from an office suite | `IconFileDoc` | `IconFileDoc`, standard |
401
+ | A spreadsheet | `IconFileSheet` | `IconFileSheet`, standard |
402
+ | A presentation | `IconFileSlides` | `IconFileSlides`, standard |
403
+ | An e-book | `IconBook` | `IconBook`, standard |
404
+ | A disk image or virtual disk | `IconDisc` | `IconDisc`, standard |
405
+ | A program or installable package | `IconFileProgram` | `IconFileProgram`, standard |
406
+ | A font | `IconFont` | `IconFont`, standard |
407
+ | A checksum or signature | `IconHash` | `IconHash`, standard |
408
+ | A torrent or magnet link | `IconMagnet` | `IconMagnet`, standard |
409
+ | A 3D model or CAD drawing | `IconCube` | `IconCube`, standard |
410
+ | A calendar or contact card | `IconCalendar` | `IconCalendar`, standard |
411
+ | A backup, temporary or unfinished file | `IconHourglass` | `IconHourglass`, standard |
412
+
413
+ <br>
414
+
368
415
  ## Where each app keeps its glyphs
369
416
 
370
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.
371
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`.
372
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.
373
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/`.
374
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.