@dumiflex/wp-kit 0.3.0 → 0.5.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/CHANGELOG.md +92 -51
- package/LICENSE +674 -674
- package/README.md +73 -76
- package/package.json +78 -77
- package/src/Avatar.vue +83 -83
- package/src/Badge.vue +97 -97
- package/src/Banner.vue +118 -118
- package/src/BookmarkButton.vue +27 -27
- package/src/BundleMeta.vue +51 -51
- package/src/Button.vue +103 -103
- package/src/Card.vue +68 -68
- package/src/Checkbox.vue +109 -107
- package/src/Chip.vue +163 -97
- package/src/ChoiceCards.vue +133 -133
- package/src/CloseButton.vue +69 -69
- package/src/CodeBlock.vue +146 -119
- package/src/ColorField.vue +188 -206
- package/src/ColorPicker.vue +201 -201
- package/src/Combobox.vue +331 -334
- package/src/ConfirmDialog.vue +165 -163
- package/src/ConfirmModal.vue +147 -147
- package/src/ConstraintMatrixPreview.vue +708 -708
- package/src/ContextMenu.vue +386 -379
- package/src/CopyButton.vue +156 -156
- package/src/DataTable.vue +293 -292
- package/src/DatePicker.vue +234 -234
- package/src/DescriptionList.vue +73 -73
- package/src/DiffView.vue +68 -68
- package/src/Disclosure.vue +166 -166
- package/src/DragHandle.vue +49 -49
- package/src/Drawer.vue +131 -131
- package/src/DropZone.vue +196 -196
- package/src/DropdownMenu.vue +72 -72
- package/src/EmptyState.vue +111 -111
- package/src/Field.vue +28 -28
- package/src/HsvPicker.vue +208 -208
- package/src/Icon.vue +39 -39
- package/src/IconPicker.vue +209 -209
- package/src/ImageGrid.vue +127 -125
- package/src/InlineEdit.vue +183 -183
- package/src/Input.vue +99 -99
- package/src/Kbd.vue +56 -56
- package/src/Lightbox.vue +176 -173
- package/src/ListFilter.vue +85 -85
- package/src/ListRow.vue +204 -204
- package/src/LoadingMark.vue +217 -161
- package/src/Logo.vue +60 -60
- package/src/Modal.vue +78 -76
- package/src/ModalShell.vue +109 -107
- package/src/Pagination.vue +106 -106
- package/src/Popover.vue +180 -171
- package/src/PostCard.vue +210 -211
- package/src/ProgressBar.vue +90 -90
- package/src/RadioGroup.vue +177 -177
- package/src/RangeSlider.vue +183 -183
- package/src/RelativeDate.vue +35 -35
- package/src/ResizeHandle.vue +143 -143
- package/src/SearchInput.vue +192 -199
- package/src/Select.vue +644 -641
- package/src/Separator.vue +54 -54
- package/src/Skeleton.vue +98 -82
- package/src/Slider.vue +198 -198
- package/src/SortableList.vue +260 -260
- package/src/SplitPane.vue +68 -68
- package/src/StarButton.vue +40 -40
- package/src/Stepper.vue +141 -141
- package/src/SupportLinks.vue +93 -93
- package/src/TagInput.vue +297 -300
- package/src/Textarea.vue +57 -57
- package/src/Timeline.vue +92 -92
- package/src/ToastCard.vue +119 -119
- package/src/ToastHost.vue +40 -40
- package/src/Toggle.vue +36 -36
- package/src/TokenizedValue.vue +310 -310
- package/src/Toolbar.vue +110 -110
- package/src/VirtualList.vue +87 -87
- package/src/VisuallyHidden.vue +29 -29
- package/src/WpButton.vue +75 -75
- package/src/WpCheck.vue +104 -102
- package/src/WpIconButton.vue +107 -107
- package/src/WpImageZoom.vue +544 -544
- package/src/WpNumber.vue +178 -183
- package/src/WpSegmented.vue +175 -175
- package/src/WpSelect.vue +93 -89
- package/src/WpSwitch.vue +163 -163
- package/src/WpTabs.vue +188 -188
- package/src/_modal-motion.css +33 -33
- package/src/_skeleton.css +50 -50
- package/src/axis-color.ts +42 -42
- package/src/button.css +169 -169
- package/src/calendar.ts +59 -59
- package/src/clipboard.ts +50 -50
- package/src/colour-presets.ts +12 -12
- package/src/diff.ts +51 -51
- package/src/drop-zone.ts +41 -41
- package/src/field.css +50 -0
- package/src/flip.ts +237 -237
- package/src/floating-layer.ts +48 -0
- package/src/floating.ts +99 -99
- package/src/highlight.ts +199 -0
- package/src/hsv.ts +100 -100
- package/src/icon-catalog.ts +52 -52
- package/src/image-zoom.ts +93 -93
- package/src/kind-colour.ts +28 -0
- package/src/loading-mark-motion.ts +249 -0
- package/src/logo-paths.ts +21 -21
- package/src/matrix-axis.ts +91 -91
- package/src/pagination.ts +27 -27
- package/src/platform-keys.ts +29 -29
- package/src/rich-tokenize.ts +357 -357
- package/src/select-types.ts +32 -32
- package/src/slider-math.ts +45 -45
- package/src/sortable.ts +22 -22
- package/src/theme/fonts/OFL-Fraunces.txt +93 -93
- package/src/theme/fonts/OFL-JetBrainsMono.txt +93 -93
- package/src/theme/fonts/OFL-Manrope.txt +93 -93
- package/src/theme/looks.css +428 -428
- package/src/theme/palettes.css +254 -254
- package/src/theme/tokens.css +2896 -2894
- package/src/tokens.ts +56 -56
- package/src/tooltip.css +43 -43
- package/src/tooltip.ts +146 -146
- package/src/useDialogFocus.ts +73 -73
- package/src/useGrowableField.ts +357 -357
- package/src/useModalKeyShield.ts +116 -116
- package/src/useToast.ts +48 -48
- package/src/wp-logo.svg +14 -14
- package/src/ChipToggle.vue +0 -92
- package/src/KindChip.vue +0 -53
- package/src/RatingBadge.vue +0 -42
package/src/matrix-axis.ts
CHANGED
|
@@ -1,91 +1,91 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Constraint-matrix axis grouping — the pure logic shared by both matrix
|
|
3
|
-
* surfaces (the SPA `ConstraintMatrix.vue` library editor and the canvas
|
|
4
|
-
* `MatrixSection.vue` instance modal). Keeping it here means the two grids
|
|
5
|
-
* order, group, label and colour their axes identically; only their template
|
|
6
|
-
* + CSS differ.
|
|
7
|
-
*
|
|
8
|
-
* A wildcard's `tag_groups` (axis name → member sub-categories) drives the
|
|
9
|
-
* layout: tags are ordered by axis, each axis becomes a coloured band
|
|
10
|
-
* (columns) or pinned header chip (rows), and the leftover run of tags with
|
|
11
|
-
* no axis becomes the labelled "uncategorized" bucket instead of a blank.
|
|
12
|
-
*/
|
|
13
|
-
import { axisHueAt } from "./axis-color";
|
|
14
|
-
|
|
15
|
-
/** One tag stamped with its axis + whether it opens a new group. Cell lookups
|
|
16
|
-
* stay keyed by tag name, so reordering for display is safe. */
|
|
17
|
-
export interface TagMeta {
|
|
18
|
-
tag: string;
|
|
19
|
-
axisIndex: number; // -1 = ungrouped
|
|
20
|
-
axisName: string;
|
|
21
|
-
groupStart: boolean; // first tag of its axis
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** A contiguous run of tags belonging to one axis (or the trailing ungrouped
|
|
25
|
-
* run). Columns render one band per group; row groups with >1 tag get a
|
|
26
|
-
* header chip, solo named groups fold the name into the tag. */
|
|
27
|
-
export interface AxisGroup {
|
|
28
|
-
axisIndex: number; // -1 = ungrouped
|
|
29
|
-
axisName: string;
|
|
30
|
-
tags: string[];
|
|
31
|
-
grouped: boolean;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Order `tags` by axis (grouped first, in axis order; ungrouped last) and
|
|
35
|
-
* stamp each with its axis index + whether it opens a new group. */
|
|
36
|
-
export function orderByGroups(tags: string[], groups: Record<string, string[]>): TagMeta[] {
|
|
37
|
-
const out: TagMeta[] = [];
|
|
38
|
-
const seen = new Set<string>();
|
|
39
|
-
Object.keys(groups).forEach((axis, ai) => {
|
|
40
|
-
let first = true;
|
|
41
|
-
for (const t of groups[axis] ?? []) {
|
|
42
|
-
if (seen.has(t) || !tags.includes(t)) continue;
|
|
43
|
-
seen.add(t);
|
|
44
|
-
out.push({ tag: t, axisIndex: ai, axisName: axis, groupStart: first });
|
|
45
|
-
first = false;
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
let firstUngrouped = true;
|
|
49
|
-
for (const t of tags) {
|
|
50
|
-
if (seen.has(t)) continue;
|
|
51
|
-
out.push({ tag: t, axisIndex: -1, axisName: "", groupStart: firstUngrouped });
|
|
52
|
-
firstUngrouped = false;
|
|
53
|
-
}
|
|
54
|
-
return out;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Collapse an ordered `TagMeta[]` into contiguous axis groups. */
|
|
58
|
-
export function toGroups(ordered: TagMeta[]): AxisGroup[] {
|
|
59
|
-
const groups: AxisGroup[] = [];
|
|
60
|
-
for (const m of ordered) {
|
|
61
|
-
if (m.groupStart || groups.length === 0) {
|
|
62
|
-
groups.push({ axisIndex: m.axisIndex, axisName: m.axisName, tags: [], grouped: m.axisIndex >= 0 });
|
|
63
|
-
}
|
|
64
|
-
groups[groups.length - 1].tags.push(m.tag);
|
|
65
|
-
}
|
|
66
|
-
return groups;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** A "bucket" is the catch-all uncategorized run: no axis, or a blank axis
|
|
70
|
-
* name. Buckets are labelled "uncategorized" + tinted neutral rather than
|
|
71
|
-
* rendered blank. */
|
|
72
|
-
export function isBucket(grp: AxisGroup): boolean {
|
|
73
|
-
return !grp.grouped || grp.axisName.trim().length === 0;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Display label for a group's band / chip. */
|
|
77
|
-
export function groupLabel(grp: AxisGroup): string {
|
|
78
|
-
return isBucket(grp) ? "uncategorized" : grp.axisName;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** Neutral grey for the uncategorized bucket — distinct from the per-axis
|
|
82
|
-
* palette so it doesn't read as a real named axis. */
|
|
83
|
-
export const UNGROUPED_HUE = "var(--wp-text-muted, #8a8a9a)";
|
|
84
|
-
|
|
85
|
-
/** Hue for a group's chips / band / tags. Named axes take the shared palette;
|
|
86
|
-
* the uncategorized bucket goes neutral grey when it's labelled (mixed
|
|
87
|
-
* layout), else keeps the legacy source/target tint (fully-flat matrix). */
|
|
88
|
-
export function groupHue(grp: AxisGroup, role: "source" | "target", labeled: boolean): string {
|
|
89
|
-
if (!isBucket(grp)) return axisHueAt(grp.axisIndex);
|
|
90
|
-
return labeled ? UNGROUPED_HUE : role === "source" ? "var(--wp-constraint-source)" : "var(--wp-constraint-target)";
|
|
91
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Constraint-matrix axis grouping — the pure logic shared by both matrix
|
|
3
|
+
* surfaces (the SPA `ConstraintMatrix.vue` library editor and the canvas
|
|
4
|
+
* `MatrixSection.vue` instance modal). Keeping it here means the two grids
|
|
5
|
+
* order, group, label and colour their axes identically; only their template
|
|
6
|
+
* + CSS differ.
|
|
7
|
+
*
|
|
8
|
+
* A wildcard's `tag_groups` (axis name → member sub-categories) drives the
|
|
9
|
+
* layout: tags are ordered by axis, each axis becomes a coloured band
|
|
10
|
+
* (columns) or pinned header chip (rows), and the leftover run of tags with
|
|
11
|
+
* no axis becomes the labelled "uncategorized" bucket instead of a blank.
|
|
12
|
+
*/
|
|
13
|
+
import { axisHueAt } from "./axis-color";
|
|
14
|
+
|
|
15
|
+
/** One tag stamped with its axis + whether it opens a new group. Cell lookups
|
|
16
|
+
* stay keyed by tag name, so reordering for display is safe. */
|
|
17
|
+
export interface TagMeta {
|
|
18
|
+
tag: string;
|
|
19
|
+
axisIndex: number; // -1 = ungrouped
|
|
20
|
+
axisName: string;
|
|
21
|
+
groupStart: boolean; // first tag of its axis
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A contiguous run of tags belonging to one axis (or the trailing ungrouped
|
|
25
|
+
* run). Columns render one band per group; row groups with >1 tag get a
|
|
26
|
+
* header chip, solo named groups fold the name into the tag. */
|
|
27
|
+
export interface AxisGroup {
|
|
28
|
+
axisIndex: number; // -1 = ungrouped
|
|
29
|
+
axisName: string;
|
|
30
|
+
tags: string[];
|
|
31
|
+
grouped: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Order `tags` by axis (grouped first, in axis order; ungrouped last) and
|
|
35
|
+
* stamp each with its axis index + whether it opens a new group. */
|
|
36
|
+
export function orderByGroups(tags: string[], groups: Record<string, string[]>): TagMeta[] {
|
|
37
|
+
const out: TagMeta[] = [];
|
|
38
|
+
const seen = new Set<string>();
|
|
39
|
+
Object.keys(groups).forEach((axis, ai) => {
|
|
40
|
+
let first = true;
|
|
41
|
+
for (const t of groups[axis] ?? []) {
|
|
42
|
+
if (seen.has(t) || !tags.includes(t)) continue;
|
|
43
|
+
seen.add(t);
|
|
44
|
+
out.push({ tag: t, axisIndex: ai, axisName: axis, groupStart: first });
|
|
45
|
+
first = false;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
let firstUngrouped = true;
|
|
49
|
+
for (const t of tags) {
|
|
50
|
+
if (seen.has(t)) continue;
|
|
51
|
+
out.push({ tag: t, axisIndex: -1, axisName: "", groupStart: firstUngrouped });
|
|
52
|
+
firstUngrouped = false;
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Collapse an ordered `TagMeta[]` into contiguous axis groups. */
|
|
58
|
+
export function toGroups(ordered: TagMeta[]): AxisGroup[] {
|
|
59
|
+
const groups: AxisGroup[] = [];
|
|
60
|
+
for (const m of ordered) {
|
|
61
|
+
if (m.groupStart || groups.length === 0) {
|
|
62
|
+
groups.push({ axisIndex: m.axisIndex, axisName: m.axisName, tags: [], grouped: m.axisIndex >= 0 });
|
|
63
|
+
}
|
|
64
|
+
groups[groups.length - 1].tags.push(m.tag);
|
|
65
|
+
}
|
|
66
|
+
return groups;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A "bucket" is the catch-all uncategorized run: no axis, or a blank axis
|
|
70
|
+
* name. Buckets are labelled "uncategorized" + tinted neutral rather than
|
|
71
|
+
* rendered blank. */
|
|
72
|
+
export function isBucket(grp: AxisGroup): boolean {
|
|
73
|
+
return !grp.grouped || grp.axisName.trim().length === 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Display label for a group's band / chip. */
|
|
77
|
+
export function groupLabel(grp: AxisGroup): string {
|
|
78
|
+
return isBucket(grp) ? "uncategorized" : grp.axisName;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Neutral grey for the uncategorized bucket — distinct from the per-axis
|
|
82
|
+
* palette so it doesn't read as a real named axis. */
|
|
83
|
+
export const UNGROUPED_HUE = "var(--wp-text-muted, #8a8a9a)";
|
|
84
|
+
|
|
85
|
+
/** Hue for a group's chips / band / tags. Named axes take the shared palette;
|
|
86
|
+
* the uncategorized bucket goes neutral grey when it's labelled (mixed
|
|
87
|
+
* layout), else keeps the legacy source/target tint (fully-flat matrix). */
|
|
88
|
+
export function groupHue(grp: AxisGroup, role: "source" | "target", labeled: boolean): string {
|
|
89
|
+
if (!isBucket(grp)) return axisHueAt(grp.axisIndex);
|
|
90
|
+
return labeled ? UNGROUPED_HUE : role === "source" ? "var(--wp-constraint-source)" : "var(--wp-constraint-target)";
|
|
91
|
+
}
|
package/src/pagination.ts
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The page numbers a Pagination shows: always the first and last page, the
|
|
3
|
-
* current page with `siblings` on each side, and "…" where pages are left
|
|
4
|
-
* out. A gap of exactly one page shows that page instead of "…", so the row
|
|
5
|
-
* keeps the same length as you move through it.
|
|
6
|
-
*/
|
|
7
|
-
export type PageItem = number | "gap-start" | "gap-end";
|
|
8
|
-
|
|
9
|
-
export function pageItems(page: number, pages: number, siblings = 1): PageItem[] {
|
|
10
|
-
if (pages <= 0) return [];
|
|
11
|
-
const slots = siblings * 2 + 5;
|
|
12
|
-
if (pages <= slots) return Array.from({ length: pages }, (_, i) => i + 1);
|
|
13
|
-
const cur = Math.min(Math.max(1, page), pages);
|
|
14
|
-
const left = Math.max(cur - siblings, 1);
|
|
15
|
-
const right = Math.min(cur + siblings, pages);
|
|
16
|
-
const showStart = left > 3;
|
|
17
|
-
const showEnd = right < pages - 2;
|
|
18
|
-
if (!showStart) {
|
|
19
|
-
const n = siblings * 2 + 3;
|
|
20
|
-
return [...Array.from({ length: n }, (_, i) => i + 1), "gap-end", pages];
|
|
21
|
-
}
|
|
22
|
-
if (!showEnd) {
|
|
23
|
-
const n = siblings * 2 + 3;
|
|
24
|
-
return [1, "gap-start", ...Array.from({ length: n }, (_, i) => pages - n + 1 + i)];
|
|
25
|
-
}
|
|
26
|
-
return [1, "gap-start", ...Array.from({ length: right - left + 1 }, (_, i) => left + i), "gap-end", pages];
|
|
27
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The page numbers a Pagination shows: always the first and last page, the
|
|
3
|
+
* current page with `siblings` on each side, and "…" where pages are left
|
|
4
|
+
* out. A gap of exactly one page shows that page instead of "…", so the row
|
|
5
|
+
* keeps the same length as you move through it.
|
|
6
|
+
*/
|
|
7
|
+
export type PageItem = number | "gap-start" | "gap-end";
|
|
8
|
+
|
|
9
|
+
export function pageItems(page: number, pages: number, siblings = 1): PageItem[] {
|
|
10
|
+
if (pages <= 0) return [];
|
|
11
|
+
const slots = siblings * 2 + 5;
|
|
12
|
+
if (pages <= slots) return Array.from({ length: pages }, (_, i) => i + 1);
|
|
13
|
+
const cur = Math.min(Math.max(1, page), pages);
|
|
14
|
+
const left = Math.max(cur - siblings, 1);
|
|
15
|
+
const right = Math.min(cur + siblings, pages);
|
|
16
|
+
const showStart = left > 3;
|
|
17
|
+
const showEnd = right < pages - 2;
|
|
18
|
+
if (!showStart) {
|
|
19
|
+
const n = siblings * 2 + 3;
|
|
20
|
+
return [...Array.from({ length: n }, (_, i) => i + 1), "gap-end", pages];
|
|
21
|
+
}
|
|
22
|
+
if (!showEnd) {
|
|
23
|
+
const n = siblings * 2 + 3;
|
|
24
|
+
return [1, "gap-start", ...Array.from({ length: n }, (_, i) => pages - n + 1 + i)];
|
|
25
|
+
}
|
|
26
|
+
return [1, "gap-start", ...Array.from({ length: right - left + 1 }, (_, i) => left + i), "gap-end", pages];
|
|
27
|
+
}
|
package/src/platform-keys.ts
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Platform-correct labels for the modifier we actually listen for.
|
|
3
|
-
*
|
|
4
|
-
* The save shortcut in our modals is `(ev.ctrlKey || ev.metaKey) && Enter`, so
|
|
5
|
-
* it has always worked on both platforms — but the hint rendered a hardcoded
|
|
6
|
-
* `⌘`, which reads as a Mac-only binding to the ~90% of ComfyUI users on
|
|
7
|
-
* Windows and Linux.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/** True on macOS/iPadOS, where the command key is the accelerator. */
|
|
11
|
-
export function isApplePlatform(): boolean {
|
|
12
|
-
if (typeof navigator === "undefined") return false;
|
|
13
|
-
const nav = navigator as Navigator & { userAgentData?: { platform?: string } };
|
|
14
|
-
// `navigator.platform` is deprecated; prefer the UA-CH hint and fall back to
|
|
15
|
-
// the user-agent string, which still carries "Macintosh" / "iPhone".
|
|
16
|
-
const hint = nav.userAgentData?.platform ?? "";
|
|
17
|
-
if (hint) return /mac/i.test(hint);
|
|
18
|
-
return /mac|iphone|ipad|ipod/i.test(nav.userAgent ?? "");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** The accelerator key's symbol: `⌘` on Apple platforms, `Ctrl` elsewhere. */
|
|
22
|
-
export function accelLabel(): string {
|
|
23
|
-
return isApplePlatform() ? "⌘" : "Ctrl";
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** The full "save" chord as shown in modal footers — `⌘↵` or `Ctrl+↵`. */
|
|
27
|
-
export function saveChordLabel(): string {
|
|
28
|
-
return isApplePlatform() ? "⌘↵" : "Ctrl+↵";
|
|
29
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Platform-correct labels for the modifier we actually listen for.
|
|
3
|
+
*
|
|
4
|
+
* The save shortcut in our modals is `(ev.ctrlKey || ev.metaKey) && Enter`, so
|
|
5
|
+
* it has always worked on both platforms — but the hint rendered a hardcoded
|
|
6
|
+
* `⌘`, which reads as a Mac-only binding to the ~90% of ComfyUI users on
|
|
7
|
+
* Windows and Linux.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** True on macOS/iPadOS, where the command key is the accelerator. */
|
|
11
|
+
export function isApplePlatform(): boolean {
|
|
12
|
+
if (typeof navigator === "undefined") return false;
|
|
13
|
+
const nav = navigator as Navigator & { userAgentData?: { platform?: string } };
|
|
14
|
+
// `navigator.platform` is deprecated; prefer the UA-CH hint and fall back to
|
|
15
|
+
// the user-agent string, which still carries "Macintosh" / "iPhone".
|
|
16
|
+
const hint = nav.userAgentData?.platform ?? "";
|
|
17
|
+
if (hint) return /mac/i.test(hint);
|
|
18
|
+
return /mac|iphone|ipad|ipod/i.test(nav.userAgent ?? "");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The accelerator key's symbol: `⌘` on Apple platforms, `Ctrl` elsewhere. */
|
|
22
|
+
export function accelLabel(): string {
|
|
23
|
+
return isApplePlatform() ? "⌘" : "Ctrl";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The full "save" chord as shown in modal footers — `⌘↵` or `Ctrl+↵`. */
|
|
27
|
+
export function saveChordLabel(): string {
|
|
28
|
+
return isApplePlatform() ? "⌘↵" : "Ctrl+↵";
|
|
29
|
+
}
|