@glimstone/core 4.0.0 → 4.1.1
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 +13 -0
- package/dist/button.d.ts +5 -0
- package/dist/button.js +4 -3
- package/dist/clipboard.d.ts +4 -0
- package/dist/clipboard.js +30 -0
- package/dist/colorNames.d.ts +10 -0
- package/dist/colorNames.js +61 -0
- package/dist/colorPicker.d.ts +59 -3
- package/dist/colorPicker.js +181 -73
- package/dist/dialog.d.ts +5 -0
- package/dist/dialog.js +27 -22
- package/dist/field.d.ts +21 -0
- package/dist/field.js +26 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/marks.d.ts +30 -0
- package/dist/marks.js +30 -0
- package/dist/media.d.ts +2 -0
- package/dist/media.js +4 -0
- package/dist/menu.d.ts +109 -0
- package/dist/menu.js +163 -0
- package/dist/meter.d.ts +32 -0
- package/dist/meter.js +44 -0
- package/dist/pill.d.ts +3 -0
- package/dist/pill.js +7 -0
- package/dist/selectModel.d.ts +17 -0
- package/dist/selectModel.js +23 -0
- package/dist/selector.d.ts +2 -2
- package/dist/sidebarRow.d.ts +4 -0
- package/dist/sidebarRow.js +6 -1
- package/dist/table.d.ts +12 -0
- package/dist/table.js +5 -0
- package/dist/toastEngine.d.ts +12 -0
- package/dist/toastEngine.js +8 -0
- package/dist/tree.d.ts +38 -0
- package/dist/tree.js +57 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/window.d.ts +29 -0
- package/dist/window.js +62 -0
- package/glyph-map.md +48 -0
- package/glyphs.json +247 -0
- package/glyphs.md +28 -2
- package/package.json +1 -1
- package/tokens.css +1093 -12
package/dist/dialog.js
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
// "Behaviour contracts", the dialog rows): Escape cancels from anywhere, Tab
|
|
3
3
|
// stays inside, and focus goes back to the control that asked.
|
|
4
4
|
const FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
5
|
+
/**
|
|
6
|
+
* Turns a Tab that would leave the card back to its first or last control.
|
|
7
|
+
* Focus that sits outside the card, on <body> for one, is pulled back in.
|
|
8
|
+
*/
|
|
9
|
+
export function keepTabInside(card, e) {
|
|
10
|
+
if (!card)
|
|
11
|
+
return;
|
|
12
|
+
const focusables = Array.from(card.querySelectorAll(FOCUSABLE));
|
|
13
|
+
if (focusables.length === 0)
|
|
14
|
+
return;
|
|
15
|
+
const first = focusables[0];
|
|
16
|
+
const last = focusables[focusables.length - 1];
|
|
17
|
+
const active = document.activeElement;
|
|
18
|
+
const inside = active instanceof Node && card.contains(active);
|
|
19
|
+
if (e.shiftKey) {
|
|
20
|
+
if (!inside || active === first) {
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
last.focus();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
else if (!inside || active === last) {
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
first.focus();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
5
30
|
/**
|
|
6
31
|
* Escape from anywhere and a Tab trap over the dialog's own controls, so focus
|
|
7
32
|
* never reaches the page it covers. The listener sits on the document because
|
|
@@ -15,28 +40,8 @@ export function trapDialogKeys(dialog, onCancel) {
|
|
|
15
40
|
onCancel();
|
|
16
41
|
return;
|
|
17
42
|
}
|
|
18
|
-
if (e.key
|
|
19
|
-
|
|
20
|
-
const card = dialog();
|
|
21
|
-
if (!card)
|
|
22
|
-
return;
|
|
23
|
-
const focusables = Array.from(card.querySelectorAll(FOCUSABLE));
|
|
24
|
-
if (focusables.length === 0)
|
|
25
|
-
return;
|
|
26
|
-
const first = focusables[0];
|
|
27
|
-
const last = focusables[focusables.length - 1];
|
|
28
|
-
const active = document.activeElement;
|
|
29
|
-
const inside = active instanceof Node && card.contains(active);
|
|
30
|
-
if (e.shiftKey) {
|
|
31
|
-
if (!inside || active === first) {
|
|
32
|
-
e.preventDefault();
|
|
33
|
-
last.focus();
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
else if (!inside || active === last) {
|
|
37
|
-
e.preventDefault();
|
|
38
|
-
first.focus();
|
|
39
|
-
}
|
|
43
|
+
if (e.key === "Tab")
|
|
44
|
+
keepTabInside(dialog(), e);
|
|
40
45
|
}
|
|
41
46
|
document.addEventListener("keydown", onKeyDown);
|
|
42
47
|
return () => document.removeEventListener("keydown", onKeyDown);
|
package/dist/field.d.ts
ADDED
|
@@ -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
|
@@ -3,19 +3,25 @@ export * from "./appMarks.ts";
|
|
|
3
3
|
export * from "./badge.ts";
|
|
4
4
|
export * from "./bubblePosition.ts";
|
|
5
5
|
export * from "./button.ts";
|
|
6
|
+
export * from "./clipboard.ts";
|
|
7
|
+
export * from "./colorNames.ts";
|
|
6
8
|
export * from "./colorPicker.ts";
|
|
7
9
|
export * from "./controls.ts";
|
|
8
10
|
export * from "./dialog.ts";
|
|
9
11
|
export * from "./disco.ts";
|
|
10
12
|
export * from "./discoLoop.ts";
|
|
13
|
+
export * from "./field.ts";
|
|
11
14
|
export * from "./flagEmoji.ts";
|
|
12
15
|
export * from "./infoTile.ts";
|
|
13
16
|
export * from "./inputModality.ts";
|
|
14
17
|
export * from "./marks.ts";
|
|
15
18
|
export * from "./media.ts";
|
|
19
|
+
export * from "./menu.ts";
|
|
20
|
+
export * from "./meter.ts";
|
|
16
21
|
export * from "./motionNative.ts";
|
|
17
22
|
export * from "./numberField.ts";
|
|
18
23
|
export * from "./paypal.ts";
|
|
24
|
+
export * from "./pill.ts";
|
|
19
25
|
export * from "./portalHue.ts";
|
|
20
26
|
export * from "./readmeButton.ts";
|
|
21
27
|
export * from "./segmentLayout.ts";
|
|
@@ -26,6 +32,9 @@ export * from "./selectScroll.ts";
|
|
|
26
32
|
export * from "./sidebarRow.ts";
|
|
27
33
|
export * from "./storage.ts";
|
|
28
34
|
export * from "./tipBubble.ts";
|
|
35
|
+
export * from "./table.ts";
|
|
29
36
|
export * from "./toastEngine.ts";
|
|
30
37
|
export * from "./tooltip.ts";
|
|
38
|
+
export * from "./tree.ts";
|
|
31
39
|
export * from "./version.ts";
|
|
40
|
+
export * from "./window.ts";
|
package/dist/index.js
CHANGED
|
@@ -3,19 +3,25 @@ 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";
|
|
9
11
|
export * from "./disco.js";
|
|
10
12
|
export * from "./discoLoop.js";
|
|
13
|
+
export * from "./field.js";
|
|
11
14
|
export * from "./flagEmoji.js";
|
|
12
15
|
export * from "./infoTile.js";
|
|
13
16
|
export * from "./inputModality.js";
|
|
14
17
|
export * from "./marks.js";
|
|
15
18
|
export * from "./media.js";
|
|
19
|
+
export * from "./menu.js";
|
|
20
|
+
export * from "./meter.js";
|
|
16
21
|
export * from "./motionNative.js";
|
|
17
22
|
export * from "./numberField.js";
|
|
18
23
|
export * from "./paypal.js";
|
|
24
|
+
export * from "./pill.js";
|
|
19
25
|
export * from "./portalHue.js";
|
|
20
26
|
export * from "./readmeButton.js";
|
|
21
27
|
export * from "./segmentLayout.js";
|
|
@@ -26,6 +32,9 @@ export * from "./selectScroll.js";
|
|
|
26
32
|
export * from "./sidebarRow.js";
|
|
27
33
|
export * from "./storage.js";
|
|
28
34
|
export * from "./tipBubble.js";
|
|
35
|
+
export * from "./table.js";
|
|
29
36
|
export * from "./toastEngine.js";
|
|
30
37
|
export * from "./tooltip.js";
|
|
38
|
+
export * from "./tree.js";
|
|
31
39
|
export * from "./version.js";
|
|
40
|
+
export * from "./window.js";
|
package/dist/marks.d.ts
CHANGED
|
@@ -41,6 +41,36 @@ 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
|
+
};
|
|
68
|
+
/** `IconFolderOpen`: a PathField's button. */
|
|
69
|
+
folderOpen: {
|
|
70
|
+
viewBox: string;
|
|
71
|
+
size: number;
|
|
72
|
+
body: string;
|
|
73
|
+
};
|
|
44
74
|
/** The small cross that dismisses a toast. */
|
|
45
75
|
dismiss: {
|
|
46
76
|
viewBox: string;
|
package/dist/marks.js
CHANGED
|
@@ -48,6 +48,36 @@ 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
|
+
},
|
|
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
|
+
},
|
|
51
81
|
/** The small cross that dismisses a toast. */
|
|
52
82
|
dismiss: {
|
|
53
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
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
/** The id of the menu `sub` opens, for a caller or a script that looks for it. */
|
|
20
|
+
subId?: string;
|
|
21
|
+
onSelect?: () => void;
|
|
22
|
+
}
|
|
23
|
+
export interface MenuHead<G> {
|
|
24
|
+
head: string;
|
|
25
|
+
/** A pencil at the heading's end, for a group the person keeps themselves,
|
|
26
|
+
* such as bookmarks. */
|
|
27
|
+
edit?: {
|
|
28
|
+
tip: string;
|
|
29
|
+
glyph?: G;
|
|
30
|
+
onSelect: () => void;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** An item, a heading, or "-" for a separator. */
|
|
34
|
+
export type MenuEntry<G> = MenuItem<G> | MenuHead<G> | "-";
|
|
35
|
+
export declare function isMenuHead<G>(entry: MenuEntry<G>): entry is MenuHead<G>;
|
|
36
|
+
/**
|
|
37
|
+
* Drops the separators a list built from parts ends up with: at either end,
|
|
38
|
+
* twice in a row and right under a heading. A menu assembled from whatever
|
|
39
|
+
* applies to the selection then needs no care at each call site.
|
|
40
|
+
*/
|
|
41
|
+
export declare function tidyMenu<G>(entries: readonly MenuEntry<G>[]): MenuEntry<G>[];
|
|
42
|
+
export interface MenuRowView {
|
|
43
|
+
role: "menuitem" | "menuitemcheckbox" | "menuitemradio";
|
|
44
|
+
/** What stands before the label. */
|
|
45
|
+
lead: "box" | "dot" | "glyph" | "blank";
|
|
46
|
+
/** What stands at the row's end, one thing at most. */
|
|
47
|
+
end: "submenu" | "tick" | "shortcut" | "count" | null;
|
|
48
|
+
/** The radio item that is the current choice, washed with the accent. */
|
|
49
|
+
current: boolean;
|
|
50
|
+
/** A press leaves the menu open. */
|
|
51
|
+
staysOpen: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** What one row shows. A row without a glyph keeps the glyph's room, so every label starts at one edge. */
|
|
54
|
+
export declare function menuRowView<G>(item: MenuItem<G>): MenuRowView;
|
|
55
|
+
interface Size {
|
|
56
|
+
width: number;
|
|
57
|
+
height: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Where a menu opened at a point goes: at the point, pushed back inside the
|
|
61
|
+
* window by its own size. `above` stands it on the point instead, for a menu
|
|
62
|
+
* opened from a bar at the bottom.
|
|
63
|
+
*/
|
|
64
|
+
export declare function placeMenu(x: number, y: number, menu: Size, view: Size, above?: boolean): {
|
|
65
|
+
left: number;
|
|
66
|
+
top: number;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Where a submenu goes: beside its row on the reading side, on the other side
|
|
70
|
+
* when that has no room, and level with the row as far as the window allows.
|
|
71
|
+
*/
|
|
72
|
+
export declare function placeSubmenu(row: {
|
|
73
|
+
left: number;
|
|
74
|
+
right: number;
|
|
75
|
+
top: number;
|
|
76
|
+
}, menu: Size, view: Size, rtl?: boolean): {
|
|
77
|
+
left: number;
|
|
78
|
+
top: number;
|
|
79
|
+
};
|
|
80
|
+
export type MenuMove = "next" | "prev" | "first" | "last" | "in" | "out";
|
|
81
|
+
/** What a key does inside an open menu. Right and left swap under a right-to-left page. */
|
|
82
|
+
export declare function menuMove(key: string, rtl?: boolean): MenuMove | null;
|
|
83
|
+
/** The rows a key can reach, in order: every enabled button of the menu. */
|
|
84
|
+
export declare function menuRows(menu: HTMLElement): HTMLElement[];
|
|
85
|
+
/**
|
|
86
|
+
* Puts the focus on a menu's first row as it opens. A menu the pointer opened
|
|
87
|
+
* carries `data-pointer` until a key is pressed in it, and the row shows no
|
|
88
|
+
* ring or wash meanwhile: the mouse never asked for that focus, and a lit
|
|
89
|
+
* first row reads as a choice already made.
|
|
90
|
+
*/
|
|
91
|
+
export declare function focusFirstMenuRow(menu: HTMLElement): void;
|
|
92
|
+
/** Moves the focus along a menu's rows, wrapping at both ends. */
|
|
93
|
+
export declare function focusMenuRow(menu: HTMLElement, move: "next" | "prev" | "first" | "last"): void;
|
|
94
|
+
/**
|
|
95
|
+
* Registers a menu as the open one and closes any other. `close` hears
|
|
96
|
+
* whether Escape closed it, which hands focus back, or another menu took its
|
|
97
|
+
* place. Returns the release to call when it closes.
|
|
98
|
+
*/
|
|
99
|
+
export declare function claimMenu(close: (byEscape: boolean) => void): () => void;
|
|
100
|
+
/** Whether a menu is open, for a key handler of the app's that should leave it alone. */
|
|
101
|
+
export declare function menuIsOpen(): boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Closes a menu on a click anywhere outside the given elements, such as the
|
|
104
|
+
* menu, its submenu and the control that opened it. A click rather than a
|
|
105
|
+
* press: the window under a menu reads the press, and while the menu is still
|
|
106
|
+
* open it knows to stay. Returns the cleanup.
|
|
107
|
+
*/
|
|
108
|
+
export declare function closeMenuOutside(inside: () => (Element | null | undefined)[], close: () => void): () => void;
|
|
109
|
+
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
|
+
}
|
package/dist/meter.d.ts
ADDED
|
@@ -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
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
|
+
}
|