@dowel-ui/react 0.4.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.
Files changed (146) hide show
  1. package/README.md +9 -5
  2. package/dist/components/ai-extraction-review/ai-extraction-review.d.ts +52 -0
  3. package/dist/components/ai-extraction-review/ai-extraction-review.d.ts.map +1 -0
  4. package/dist/components/ai-extraction-review/ai-extraction-review.js +249 -0
  5. package/dist/components/ai-extraction-review/ai-extraction-review.js.map +1 -0
  6. package/dist/components/ai-extraction-review/extraction-model.d.ts +94 -0
  7. package/dist/components/ai-extraction-review/extraction-model.d.ts.map +1 -0
  8. package/dist/components/ai-extraction-review/extraction-model.js +91 -0
  9. package/dist/components/ai-extraction-review/extraction-model.js.map +1 -0
  10. package/dist/components/ai-extraction-review/index.d.ts +3 -0
  11. package/dist/components/ai-extraction-review/index.js +3 -0
  12. package/dist/components/ai-extraction-review/meta.js +17 -0
  13. package/dist/components/ai-extraction-review/meta.js.map +1 -0
  14. package/dist/components/ai-suggested-value/ai-suggested-value.d.ts +72 -0
  15. package/dist/components/ai-suggested-value/ai-suggested-value.d.ts.map +1 -0
  16. package/dist/components/ai-suggested-value/ai-suggested-value.js +126 -0
  17. package/dist/components/ai-suggested-value/ai-suggested-value.js.map +1 -0
  18. package/dist/components/ai-suggested-value/index.d.ts +2 -0
  19. package/dist/components/ai-suggested-value/index.js +2 -0
  20. package/dist/components/ai-suggested-value/meta.js +17 -0
  21. package/dist/components/ai-suggested-value/meta.js.map +1 -0
  22. package/dist/components/alert/alert.d.ts +1 -1
  23. package/dist/components/avatar/avatar.d.ts +1 -1
  24. package/dist/components/badge/badge.d.ts +1 -1
  25. package/dist/components/button/button.d.ts +1 -1
  26. package/dist/components/confirm-typed/confirm-typed.d.ts +47 -0
  27. package/dist/components/confirm-typed/confirm-typed.d.ts.map +1 -0
  28. package/dist/components/confirm-typed/confirm-typed.js +106 -0
  29. package/dist/components/confirm-typed/confirm-typed.js.map +1 -0
  30. package/dist/components/confirm-typed/index.d.ts +2 -0
  31. package/dist/components/confirm-typed/index.js +2 -0
  32. package/dist/components/confirm-typed/meta.js +17 -0
  33. package/dist/components/confirm-typed/meta.js.map +1 -0
  34. package/dist/components/cron-editor/cron-editor.d.ts +36 -0
  35. package/dist/components/cron-editor/cron-editor.d.ts.map +1 -0
  36. package/dist/components/cron-editor/cron-editor.js +493 -0
  37. package/dist/components/cron-editor/cron-editor.js.map +1 -0
  38. package/dist/components/cron-editor/cron-expression.d.ts +82 -0
  39. package/dist/components/cron-editor/cron-expression.d.ts.map +1 -0
  40. package/dist/components/cron-editor/cron-expression.js +352 -0
  41. package/dist/components/cron-editor/cron-expression.js.map +1 -0
  42. package/dist/components/cron-editor/index.d.ts +3 -0
  43. package/dist/components/cron-editor/index.js +3 -0
  44. package/dist/components/cron-editor/meta.js +17 -0
  45. package/dist/components/cron-editor/meta.js.map +1 -0
  46. package/dist/components/dns-record/dns-record.d.ts +61 -0
  47. package/dist/components/dns-record/dns-record.d.ts.map +1 -0
  48. package/dist/components/dns-record/dns-record.js +227 -0
  49. package/dist/components/dns-record/dns-record.js.map +1 -0
  50. package/dist/components/dns-record/index.d.ts +2 -0
  51. package/dist/components/dns-record/index.js +2 -0
  52. package/dist/components/dns-record/meta.js +17 -0
  53. package/dist/components/dns-record/meta.js.map +1 -0
  54. package/dist/components/permission-matrix/index.d.ts +3 -0
  55. package/dist/components/permission-matrix/index.js +3 -0
  56. package/dist/components/permission-matrix/meta.js +17 -0
  57. package/dist/components/permission-matrix/meta.js.map +1 -0
  58. package/dist/components/permission-matrix/permission-matrix.d.ts +46 -0
  59. package/dist/components/permission-matrix/permission-matrix.d.ts.map +1 -0
  60. package/dist/components/permission-matrix/permission-matrix.js +199 -0
  61. package/dist/components/permission-matrix/permission-matrix.js.map +1 -0
  62. package/dist/components/permission-matrix/permission-model.d.ts +53 -0
  63. package/dist/components/permission-matrix/permission-model.d.ts.map +1 -0
  64. package/dist/components/permission-matrix/permission-model.js +64 -0
  65. package/dist/components/permission-matrix/permission-model.js.map +1 -0
  66. package/dist/components/progress/progress.d.ts +1 -1
  67. package/dist/components/secret-field/index.d.ts +2 -0
  68. package/dist/components/secret-field/index.js +2 -0
  69. package/dist/components/secret-field/meta.js +17 -0
  70. package/dist/components/secret-field/meta.js.map +1 -0
  71. package/dist/components/secret-field/secret-field.d.ts +58 -0
  72. package/dist/components/secret-field/secret-field.d.ts.map +1 -0
  73. package/dist/components/secret-field/secret-field.js +168 -0
  74. package/dist/components/secret-field/secret-field.js.map +1 -0
  75. package/dist/components/session-expiry/index.d.ts +2 -0
  76. package/dist/components/session-expiry/index.js +2 -0
  77. package/dist/components/session-expiry/meta.js +17 -0
  78. package/dist/components/session-expiry/meta.js.map +1 -0
  79. package/dist/components/session-expiry/session-expiry.d.ts +64 -0
  80. package/dist/components/session-expiry/session-expiry.d.ts.map +1 -0
  81. package/dist/components/session-expiry/session-expiry.js +185 -0
  82. package/dist/components/session-expiry/session-expiry.js.map +1 -0
  83. package/dist/components/shortcut-recorder/index.d.ts +3 -0
  84. package/dist/components/shortcut-recorder/index.js +3 -0
  85. package/dist/components/shortcut-recorder/meta.js +17 -0
  86. package/dist/components/shortcut-recorder/meta.js.map +1 -0
  87. package/dist/components/shortcut-recorder/shortcut-model.d.ts +67 -0
  88. package/dist/components/shortcut-recorder/shortcut-model.d.ts.map +1 -0
  89. package/dist/components/shortcut-recorder/shortcut-model.js +168 -0
  90. package/dist/components/shortcut-recorder/shortcut-model.js.map +1 -0
  91. package/dist/components/shortcut-recorder/shortcut-recorder.d.ts +47 -0
  92. package/dist/components/shortcut-recorder/shortcut-recorder.d.ts.map +1 -0
  93. package/dist/components/shortcut-recorder/shortcut-recorder.js +212 -0
  94. package/dist/components/shortcut-recorder/shortcut-recorder.js.map +1 -0
  95. package/dist/components/spinner/spinner.d.ts +1 -1
  96. package/dist/components/sync-status/index.d.ts +2 -0
  97. package/dist/components/sync-status/index.js +2 -0
  98. package/dist/components/sync-status/meta.js +17 -0
  99. package/dist/components/sync-status/meta.js.map +1 -0
  100. package/dist/components/sync-status/sync-status.d.ts +65 -0
  101. package/dist/components/sync-status/sync-status.d.ts.map +1 -0
  102. package/dist/components/sync-status/sync-status.js +103 -0
  103. package/dist/components/sync-status/sync-status.js.map +1 -0
  104. package/dist/components/toast/toast.d.ts +1 -1
  105. package/dist/index.d.ts +25 -1
  106. package/dist/index.js +16 -2
  107. package/dist/registry/components.d.ts.map +1 -1
  108. package/dist/registry/components.js +73 -53
  109. package/dist/registry/components.js.map +1 -1
  110. package/package.json +4 -4
  111. package/src/components/ai-extraction-review/ai-extraction-review.tsx +492 -0
  112. package/src/components/ai-extraction-review/extraction-model.ts +159 -0
  113. package/src/components/ai-extraction-review/index.ts +13 -0
  114. package/src/components/ai-extraction-review/meta.ts +24 -0
  115. package/src/components/ai-suggested-value/ai-suggested-value.tsx +295 -0
  116. package/src/components/ai-suggested-value/index.ts +6 -0
  117. package/src/components/ai-suggested-value/meta.ts +24 -0
  118. package/src/components/confirm-typed/confirm-typed.tsx +191 -0
  119. package/src/components/confirm-typed/index.ts +1 -0
  120. package/src/components/confirm-typed/meta.ts +22 -0
  121. package/src/components/cron-editor/cron-editor.tsx +676 -0
  122. package/src/components/cron-editor/cron-expression.ts +552 -0
  123. package/src/components/cron-editor/index.ts +23 -0
  124. package/src/components/cron-editor/meta.ts +23 -0
  125. package/src/components/dns-record/dns-record.tsx +315 -0
  126. package/src/components/dns-record/index.ts +7 -0
  127. package/src/components/dns-record/meta.ts +21 -0
  128. package/src/components/permission-matrix/index.ts +14 -0
  129. package/src/components/permission-matrix/meta.ts +23 -0
  130. package/src/components/permission-matrix/permission-matrix.tsx +368 -0
  131. package/src/components/permission-matrix/permission-model.ts +120 -0
  132. package/src/components/secret-field/index.ts +6 -0
  133. package/src/components/secret-field/meta.ts +21 -0
  134. package/src/components/secret-field/secret-field.tsx +284 -0
  135. package/src/components/session-expiry/index.ts +9 -0
  136. package/src/components/session-expiry/meta.ts +23 -0
  137. package/src/components/session-expiry/session-expiry.tsx +321 -0
  138. package/src/components/shortcut-recorder/index.ts +19 -0
  139. package/src/components/shortcut-recorder/meta.ts +22 -0
  140. package/src/components/shortcut-recorder/shortcut-model.ts +204 -0
  141. package/src/components/shortcut-recorder/shortcut-recorder.tsx +336 -0
  142. package/src/components/sync-status/index.ts +9 -0
  143. package/src/components/sync-status/meta.ts +21 -0
  144. package/src/components/sync-status/sync-status.tsx +217 -0
  145. package/src/index.ts +10 -0
  146. package/src/registry/components.ts +20 -0
@@ -0,0 +1,185 @@
1
+ "use client";
2
+ import { cn } from "../../lib/utils.js";
3
+ import { Button } from "../button/button.js";
4
+ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../dialog/dialog.js";
5
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+ import { memo, useEffect, useId, useRef, useState, useSyncExternalStore } from "react";
7
+ //#region src/components/session-expiry/session-expiry.tsx
8
+ /** Where a session stands, given the clock. Pure, for tests and for the server. */
9
+ function sessionPhase(expiresAt, now, warnBefore) {
10
+ const remaining = expiresAt.getTime() - now.getTime();
11
+ if (remaining <= 0) return "expired";
12
+ if (remaining <= warnBefore) return "warning";
13
+ return "active";
14
+ }
15
+ /** Whole seconds left, never negative. */
16
+ function secondsLeft(expiresAt, now) {
17
+ return Math.max(0, Math.ceil((expiresAt.getTime() - now.getTime()) / 1e3));
18
+ }
19
+ /** "1:59" for the screen. */
20
+ function formatCountdown(seconds) {
21
+ const minutes = Math.floor(seconds / 60);
22
+ const rest = seconds % 60;
23
+ return `${String(minutes)}:${String(rest).padStart(2, "0")}`;
24
+ }
25
+ /** "1 minute 59 seconds" for the ear. */
26
+ function describeCountdown(seconds) {
27
+ const minutes = Math.floor(seconds / 60);
28
+ const rest = seconds % 60;
29
+ const parts = [];
30
+ if (minutes > 0) parts.push(`${String(minutes)} ${minutes === 1 ? "minute" : "minutes"}`);
31
+ if (rest > 0 || minutes === 0) parts.push(`${String(rest)} ${rest === 1 ? "second" : "seconds"}`);
32
+ return parts.join(" ");
33
+ }
34
+ /** The seconds-left values at which the countdown is said aloud. */
35
+ const ANNOUNCE_AT = [
36
+ 60,
37
+ 30,
38
+ 10
39
+ ];
40
+ function subscribeToClock(callback) {
41
+ const timer = setInterval(callback, 1e3);
42
+ return () => {
43
+ clearInterval(timer);
44
+ };
45
+ }
46
+ /** Whole seconds, so the snapshot is stable within a second and React does not re-render for nothing. */
47
+ const readClockSeconds = () => Math.floor(Date.now() / 1e3);
48
+ const readNoClock = () => null;
49
+ function SessionExpiry({ expiresAt, warnBefore = 12e4, now: nowProp, onExtend, onExpire, onSignOut, heading = "Your session is about to end", children, expiredAction, className }) {
50
+ const id = useId();
51
+ const tickSeconds = useSyncExternalStore(subscribeToClock, readClockSeconds, readNoClock);
52
+ const tick = tickSeconds === null ? null : /* @__PURE__ */ new Date(tickSeconds * 1e3);
53
+ const now = nowProp ?? tick;
54
+ const phase = now ? sessionPhase(expiresAt, now, warnBefore) : "active";
55
+ const seconds = now ? secondsLeft(expiresAt, now) : 0;
56
+ const [announcement, setAnnouncement] = useState("");
57
+ const [spoken, setSpoken] = useState(null);
58
+ const [endedFor, setEndedFor] = useState(null);
59
+ if (phase === "warning") {
60
+ if (spoken === null) {
61
+ setSpoken(Number.POSITIVE_INFINITY);
62
+ setAnnouncement(`${heading}. ${describeCountdown(seconds)} left.`);
63
+ } else {
64
+ const due = ANNOUNCE_AT.find((at) => seconds <= at && at < spoken);
65
+ if (due !== void 0) {
66
+ setSpoken(due);
67
+ setAnnouncement(`${describeCountdown(seconds)} left.`);
68
+ }
69
+ }
70
+ } else if (spoken !== null && phase === "active") {
71
+ setSpoken(null);
72
+ setAnnouncement("");
73
+ } else if (phase === "expired" && endedFor !== expiresAt.getTime()) {
74
+ setEndedFor(expiresAt.getTime());
75
+ setAnnouncement("Your session has ended.");
76
+ }
77
+ const firedFor = useRef(null);
78
+ useEffect(() => {
79
+ if (phase !== "expired" || firedFor.current === expiresAt.getTime()) return;
80
+ firedFor.current = expiresAt.getTime();
81
+ onExpire?.();
82
+ }, [
83
+ phase,
84
+ expiresAt,
85
+ onExpire
86
+ ]);
87
+ const open = phase !== "active";
88
+ const [shown, setShown] = useState(seconds);
89
+ if (open && shown !== seconds) setShown(seconds);
90
+ return /* @__PURE__ */ jsx(ExpiryDialog, {
91
+ id,
92
+ open,
93
+ phase,
94
+ seconds: shown,
95
+ heading,
96
+ announcement,
97
+ onExtend,
98
+ onSignOut,
99
+ expiredAction,
100
+ className,
101
+ children
102
+ });
103
+ }
104
+ const ExpiryDialog = memo(function ExpiryDialog({ id, open, phase, seconds, heading, announcement, onExtend, onSignOut, expiredAction, className, children }) {
105
+ const stayRef = useRef(null);
106
+ const [extending, setExtending] = useState(false);
107
+ const extend = () => {
108
+ const result = onExtend();
109
+ if (result instanceof Promise) {
110
+ setExtending(true);
111
+ result.finally(() => {
112
+ setExtending(false);
113
+ });
114
+ }
115
+ };
116
+ return /* @__PURE__ */ jsx(Dialog, {
117
+ open,
118
+ children: /* @__PURE__ */ jsxs(DialogContent, {
119
+ role: "alertdialog",
120
+ showCloseButton: false,
121
+ "aria-describedby": `${id}-description`,
122
+ "data-slot": "session-expiry",
123
+ "data-phase": phase,
124
+ className: cn("max-w-sm", className),
125
+ onEscapeKeyDown: (event) => {
126
+ event.preventDefault();
127
+ },
128
+ onPointerDownOutside: (event) => {
129
+ event.preventDefault();
130
+ },
131
+ onInteractOutside: (event) => {
132
+ event.preventDefault();
133
+ },
134
+ onOpenAutoFocus: (event) => {
135
+ event.preventDefault();
136
+ stayRef.current?.focus();
137
+ },
138
+ children: [
139
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsx(DialogTitle, { children: phase === "expired" ? "Your session has ended" : heading }), /* @__PURE__ */ jsx(DialogDescription, {
140
+ id: `${id}-description`,
141
+ children: phase === "expired" ? "You were signed out after a period of inactivity." : /* @__PURE__ */ jsxs(Fragment, { children: [
142
+ "You will be signed out in",
143
+ " ",
144
+ /* @__PURE__ */ jsxs("span", {
145
+ "data-slot": "session-expiry-countdown",
146
+ className: "font-medium tabular-nums",
147
+ children: [/* @__PURE__ */ jsx("span", {
148
+ "aria-hidden": "true",
149
+ children: formatCountdown(seconds)
150
+ }), /* @__PURE__ */ jsx("span", {
151
+ className: "sr-only",
152
+ children: describeCountdown(seconds)
153
+ })]
154
+ }),
155
+ "."
156
+ ] })
157
+ })] }),
158
+ children ? /* @__PURE__ */ jsx("div", {
159
+ className: "text-sm text-muted-foreground",
160
+ children
161
+ }) : null,
162
+ /* @__PURE__ */ jsx("span", {
163
+ role: "status",
164
+ "aria-live": "polite",
165
+ className: "sr-only",
166
+ children: announcement
167
+ }),
168
+ /* @__PURE__ */ jsx(DialogFooter, { children: phase === "expired" ? expiredAction : /* @__PURE__ */ jsxs(Fragment, { children: [onSignOut ? /* @__PURE__ */ jsx(Button, {
169
+ variant: "outline",
170
+ onClick: onSignOut,
171
+ children: "Sign out now"
172
+ }) : null, /* @__PURE__ */ jsx(Button, {
173
+ ref: stayRef,
174
+ loading: extending,
175
+ onClick: extend,
176
+ children: "Stay signed in"
177
+ })] }) })
178
+ ]
179
+ })
180
+ });
181
+ });
182
+ //#endregion
183
+ export { SessionExpiry, describeCountdown, formatCountdown, secondsLeft, sessionPhase };
184
+
185
+ //# sourceMappingURL=session-expiry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-expiry.js","names":[],"sources":["../../../src/components/session-expiry/session-expiry.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n memo,\n useEffect,\n useId,\n useRef,\n useState,\n useSyncExternalStore,\n type ReactNode,\n} from \"react\";\n\nimport { Button } from \"@/components/button\";\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from \"@/components/dialog\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * \"Your session ends in two minutes. Stay signed in?\"\n *\n * Every product with an idle timeout builds this, and WCAG 2.2.1 (Timing\n * Adjustable) says what it has to do: warn before the time runs out, and\n * give at least twenty seconds to extend it with a simple action. Most\n * implementations get the first half — a toast, a modal — and fail the\n * second in one of two ways: the warning can be dismissed without choosing,\n * so a reader who closed it to see the page underneath is signed out with\n * no further word; or the countdown is a live region, so a screen reader\n * user hears a number every second for two minutes and cannot hear the\n * choice they are being asked to make.\n *\n * So this is an alert dialog that cannot be waved away. Escape and the\n * backdrop do nothing, because dismissing a session warning without\n * choosing is choosing nothing, and the two buttons are the two things a\n * person can actually do. The countdown ticks on screen and is announced at\n * four moments — when the warning opens, at one minute, thirty seconds and\n * ten — which is enough to know how long is left and quiet enough to hear\n * the question.\n *\n * When time runs out, `onExpire` fires once and the dialog says so, with a\n * slot for whatever the application offers next. It does not sign anyone\n * out; the server did that, and this is the part that tells the person.\n */\n\nexport type SessionPhase = \"active\" | \"warning\" | \"expired\";\n\n/** Where a session stands, given the clock. Pure, for tests and for the server. */\nexport function sessionPhase(expiresAt: Date, now: Date, warnBefore: number): SessionPhase {\n const remaining = expiresAt.getTime() - now.getTime();\n if (remaining <= 0) return \"expired\";\n if (remaining <= warnBefore) return \"warning\";\n return \"active\";\n}\n\n/** Whole seconds left, never negative. */\nexport function secondsLeft(expiresAt: Date, now: Date): number {\n return Math.max(0, Math.ceil((expiresAt.getTime() - now.getTime()) / 1000));\n}\n\n/** \"1:59\" for the screen. */\nexport function formatCountdown(seconds: number): string {\n const minutes = Math.floor(seconds / 60);\n const rest = seconds % 60;\n return `${String(minutes)}:${String(rest).padStart(2, \"0\")}`;\n}\n\n/** \"1 minute 59 seconds\" for the ear. */\nexport function describeCountdown(seconds: number): string {\n const minutes = Math.floor(seconds / 60);\n const rest = seconds % 60;\n const parts: string[] = [];\n if (minutes > 0) parts.push(`${String(minutes)} ${minutes === 1 ? \"minute\" : \"minutes\"}`);\n if (rest > 0 || minutes === 0)\n parts.push(`${String(rest)} ${rest === 1 ? \"second\" : \"seconds\"}`);\n return parts.join(\" \");\n}\n\n/** The seconds-left values at which the countdown is said aloud. */\nconst ANNOUNCE_AT = [60, 30, 10];\n\nfunction subscribeToClock(callback: () => void) {\n const timer = setInterval(callback, 1000);\n return () => {\n clearInterval(timer);\n };\n}\n/** Whole seconds, so the snapshot is stable within a second and React does not re-render for nothing. */\nconst readClockSeconds = () => Math.floor(Date.now() / 1000);\nconst readNoClock = () => null;\n\nexport interface SessionExpiryProps {\n /** When the session ends. Move it forward after a successful extend. */\n expiresAt: Date;\n /** How long before the end to warn, in milliseconds. Two minutes by default. */\n warnBefore?: number;\n /**\n * The clock. Supply it to drive the countdown yourself, or in tests; omit\n * and the component ticks once a second while mounted.\n */\n now?: Date;\n /** Keep the session going. May return a promise; the button waits for it. */\n onExtend: () => void | Promise<void>;\n /** Called once when the time runs out. */\n onExpire?: () => void;\n /** Offered as the other choice. Omit to offer only staying signed in. */\n onSignOut?: () => void;\n /** The warning's heading. */\n heading?: string;\n /** What is at stake, in the reader's language: \"Unsaved edits will be lost.\" */\n children?: ReactNode;\n /** What to offer once the session has ended: a sign-in link, usually. */\n expiredAction?: ReactNode;\n className?: string;\n}\n\nexport function SessionExpiry({\n expiresAt,\n warnBefore = 120_000,\n now: nowProp,\n onExtend,\n onExpire,\n onSignOut,\n heading = \"Your session is about to end\",\n children,\n expiredAction,\n className,\n}: SessionExpiryProps) {\n const id = useId();\n\n // The clock as an external store that ticks once a second. The server\n // snapshot is null, so the server and the first client render agree on\n // showing nothing; the real instant arrives in the re-render after.\n const tickSeconds = useSyncExternalStore(subscribeToClock, readClockSeconds, readNoClock);\n const tick = tickSeconds === null ? null : new Date(tickSeconds * 1000);\n\n const now = nowProp ?? tick;\n const phase = now ? sessionPhase(expiresAt, now, warnBefore) : \"active\";\n const seconds = now ? secondsLeft(expiresAt, now) : 0;\n\n const [announcement, setAnnouncement] = useState(\"\");\n\n // Announced at the moments that matter, not every second. Tracked as the\n // last threshold spoken so a tick that skips a value still says it once.\n const [spoken, setSpoken] = useState<number | null>(null);\n const [endedFor, setEndedFor] = useState<number | null>(null);\n\n if (phase === \"warning\") {\n if (spoken === null) {\n setSpoken(Number.POSITIVE_INFINITY);\n setAnnouncement(`${heading}. ${describeCountdown(seconds)} left.`);\n } else {\n const due = ANNOUNCE_AT.find((at) => seconds <= at && at < spoken);\n if (due !== undefined) {\n setSpoken(due);\n setAnnouncement(`${describeCountdown(seconds)} left.`);\n }\n }\n } else if (spoken !== null && phase === \"active\") {\n // Extended. The next warning starts its announcements afresh.\n setSpoken(null);\n setAnnouncement(\"\");\n } else if (phase === \"expired\" && endedFor !== expiresAt.getTime()) {\n setEndedFor(expiresAt.getTime());\n setAnnouncement(\"Your session has ended.\");\n }\n\n // The one side effect: telling the application, once per expiry. A ref\n // rather than state, because nothing rendered depends on it.\n const firedFor = useRef<number | null>(null);\n useEffect(() => {\n if (phase !== \"expired\" || firedFor.current === expiresAt.getTime()) return;\n firedFor.current = expiresAt.getTime();\n onExpire?.();\n }, [phase, expiresAt, onExpire]);\n\n const open = phase !== \"active\";\n\n // Once told to close, the dialog is left alone: the countdown it shows is\n // frozen and its props stop changing, so the clock's ticks no longer\n // re-render a surface that is mid-exit. A dialog re-rendered every second\n // while closing never finishes closing.\n const [shown, setShown] = useState(seconds);\n if (open && shown !== seconds) setShown(seconds);\n\n return (\n <ExpiryDialog\n id={id}\n open={open}\n phase={phase}\n seconds={shown}\n heading={heading}\n announcement={announcement}\n onExtend={onExtend}\n onSignOut={onSignOut}\n expiredAction={expiredAction}\n className={className}\n >\n {children}\n </ExpiryDialog>\n );\n}\n\ninterface ExpiryDialogProps {\n id: string;\n open: boolean;\n phase: SessionPhase;\n seconds: number;\n heading: string;\n announcement: string;\n onExtend: () => void | Promise<void>;\n onSignOut?: () => void;\n expiredAction?: ReactNode;\n className?: string;\n children?: ReactNode;\n}\n\nconst ExpiryDialog = memo(function ExpiryDialog({\n id,\n open,\n phase,\n seconds,\n heading,\n announcement,\n onExtend,\n onSignOut,\n expiredAction,\n className,\n children,\n}: ExpiryDialogProps) {\n const stayRef = useRef<HTMLButtonElement>(null);\n const [extending, setExtending] = useState(false);\n\n const extend = () => {\n const result = onExtend();\n if (result instanceof Promise) {\n setExtending(true);\n void result.finally(() => {\n setExtending(false);\n });\n }\n };\n\n return (\n <Dialog open={open}>\n <DialogContent\n role=\"alertdialog\"\n showCloseButton={false}\n aria-describedby={`${id}-description`}\n data-slot=\"session-expiry\"\n data-phase={phase}\n className={cn(\"max-w-sm\", className)}\n // Dismissing a session warning without choosing is choosing nothing,\n // so the gestures that dismiss a dialog do nothing here. The buttons\n // are the way out, and there are two of them.\n onEscapeKeyDown={(event) => {\n event.preventDefault();\n }}\n onPointerDownOutside={(event) => {\n event.preventDefault();\n }}\n onInteractOutside={(event) => {\n event.preventDefault();\n }}\n // Focus lands on the safe choice. The first tabbable would be \"Sign\n // out now\", and a reflexive Enter on that is the outcome the whole\n // dialog exists to prevent.\n onOpenAutoFocus={(event) => {\n event.preventDefault();\n stayRef.current?.focus();\n }}\n >\n <DialogHeader>\n <DialogTitle>{phase === \"expired\" ? \"Your session has ended\" : heading}</DialogTitle>\n <DialogDescription id={`${id}-description`}>\n {phase === \"expired\" ? (\n \"You were signed out after a period of inactivity.\"\n ) : (\n <>\n You will be signed out in{\" \"}\n <span data-slot=\"session-expiry-countdown\" className=\"font-medium tabular-nums\">\n <span aria-hidden=\"true\">{formatCountdown(seconds)}</span>\n <span className=\"sr-only\">{describeCountdown(seconds)}</span>\n </span>\n .\n </>\n )}\n </DialogDescription>\n </DialogHeader>\n\n {children ? <div className=\"text-sm text-muted-foreground\">{children}</div> : null}\n\n {/* The countdown above is not live. This is, and it speaks four times. */}\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {announcement}\n </span>\n\n <DialogFooter>\n {phase === \"expired\" ? (\n expiredAction\n ) : (\n <>\n {onSignOut ? (\n <Button variant=\"outline\" onClick={onSignOut}>\n Sign out now\n </Button>\n ) : null}\n <Button ref={stayRef} loading={extending} onClick={extend}>\n Stay signed in\n </Button>\n </>\n )}\n </DialogFooter>\n </DialogContent>\n </Dialog>\n );\n});\n"],"mappings":";;;;;;;;AAoDA,SAAgB,aAAa,WAAiB,KAAW,YAAkC;CACzF,MAAM,YAAY,UAAU,QAAQ,IAAI,IAAI,QAAQ;CACpD,IAAI,aAAa,GAAG,OAAO;CAC3B,IAAI,aAAa,YAAY,OAAO;CACpC,OAAO;AACT;;AAGA,SAAgB,YAAY,WAAiB,KAAmB;CAC9D,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,UAAU,QAAQ,IAAI,IAAI,QAAQ,KAAK,GAAI,CAAC;AAC5E;;AAGA,SAAgB,gBAAgB,SAAyB;CACvD,MAAM,UAAU,KAAK,MAAM,UAAU,EAAE;CACvC,MAAM,OAAO,UAAU;CACvB,OAAO,GAAG,OAAO,OAAO,EAAE,GAAG,OAAO,IAAI,CAAC,CAAC,SAAS,GAAG,GAAG;AAC3D;;AAGA,SAAgB,kBAAkB,SAAyB;CACzD,MAAM,UAAU,KAAK,MAAM,UAAU,EAAE;CACvC,MAAM,OAAO,UAAU;CACvB,MAAM,QAAkB,CAAC;CACzB,IAAI,UAAU,GAAG,MAAM,KAAK,GAAG,OAAO,OAAO,EAAE,GAAG,YAAY,IAAI,WAAW,WAAW;CACxF,IAAI,OAAO,KAAK,YAAY,GAC1B,MAAM,KAAK,GAAG,OAAO,IAAI,EAAE,GAAG,SAAS,IAAI,WAAW,WAAW;CACnE,OAAO,MAAM,KAAK,GAAG;AACvB;;AAGA,MAAM,cAAc;CAAC;CAAI;CAAI;AAAE;AAE/B,SAAS,iBAAiB,UAAsB;CAC9C,MAAM,QAAQ,YAAY,UAAU,GAAI;CACxC,aAAa;EACX,cAAc,KAAK;CACrB;AACF;;AAEA,MAAM,yBAAyB,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAC3D,MAAM,oBAAoB;AA2B1B,SAAgB,cAAc,EAC5B,WACA,aAAa,MACb,KAAK,SACL,UACA,UACA,WACA,UAAU,gCACV,UACA,eACA,aACqB;CACrB,MAAM,KAAK,MAAM;CAKjB,MAAM,cAAc,qBAAqB,kBAAkB,kBAAkB,WAAW;CACxF,MAAM,OAAO,gBAAgB,OAAO,uBAAO,IAAI,KAAK,cAAc,GAAI;CAEtE,MAAM,MAAM,WAAW;CACvB,MAAM,QAAQ,MAAM,aAAa,WAAW,KAAK,UAAU,IAAI;CAC/D,MAAM,UAAU,MAAM,YAAY,WAAW,GAAG,IAAI;CAEpD,MAAM,CAAC,cAAc,mBAAmB,SAAS,EAAE;CAInD,MAAM,CAAC,QAAQ,aAAa,SAAwB,IAAI;CACxD,MAAM,CAAC,UAAU,eAAe,SAAwB,IAAI;CAE5D,IAAI,UAAU,WAAW;EACvB,IAAI,WAAW,MAAM;GACnB,UAAU,OAAO,iBAAiB;GAClC,gBAAgB,GAAG,QAAQ,IAAI,kBAAkB,OAAO,EAAE,OAAO;EACnE,OAAO;GACL,MAAM,MAAM,YAAY,MAAM,OAAO,WAAW,MAAM,KAAK,MAAM;GACjE,IAAI,QAAQ,KAAA,GAAW;IACrB,UAAU,GAAG;IACb,gBAAgB,GAAG,kBAAkB,OAAO,EAAE,OAAO;GACvD;EACF;CACF,OAAO,IAAI,WAAW,QAAQ,UAAU,UAAU;EAEhD,UAAU,IAAI;EACd,gBAAgB,EAAE;CACpB,OAAO,IAAI,UAAU,aAAa,aAAa,UAAU,QAAQ,GAAG;EAClE,YAAY,UAAU,QAAQ,CAAC;EAC/B,gBAAgB,yBAAyB;CAC3C;CAIA,MAAM,WAAW,OAAsB,IAAI;CAC3C,gBAAgB;EACd,IAAI,UAAU,aAAa,SAAS,YAAY,UAAU,QAAQ,GAAG;EACrE,SAAS,UAAU,UAAU,QAAQ;EACrC,WAAW;CACb,GAAG;EAAC;EAAO;EAAW;CAAQ,CAAC;CAE/B,MAAM,OAAO,UAAU;CAMvB,MAAM,CAAC,OAAO,YAAY,SAAS,OAAO;CAC1C,IAAI,QAAQ,UAAU,SAAS,SAAS,OAAO;CAE/C,OACE,oBAAC,cAAD;EACM;EACE;EACC;EACP,SAAS;EACA;EACK;EACJ;EACC;EACI;EACJ;EAEV;CACW,CAAA;AAElB;AAgBA,MAAM,eAAe,KAAK,SAAS,aAAa,EAC9C,IACA,MACA,OACA,SACA,SACA,cACA,UACA,WACA,eACA,WACA,YACoB;CACpB,MAAM,UAAU,OAA0B,IAAI;CAC9C,MAAM,CAAC,WAAW,gBAAgB,SAAS,KAAK;CAEhD,MAAM,eAAe;EACnB,MAAM,SAAS,SAAS;EACxB,IAAI,kBAAkB,SAAS;GAC7B,aAAa,IAAI;GACjB,OAAY,cAAc;IACxB,aAAa,KAAK;GACpB,CAAC;EACH;CACF;CAEA,OACE,oBAAC,QAAD;EAAc;EACZ,UAAA,qBAAC,eAAD;GACE,MAAK;GACL,iBAAiB;GACjB,oBAAkB,GAAG,GAAG;GACxB,aAAU;GACV,cAAY;GACZ,WAAW,GAAG,YAAY,SAAS;GAInC,kBAAkB,UAAU;IAC1B,MAAM,eAAe;GACvB;GACA,uBAAuB,UAAU;IAC/B,MAAM,eAAe;GACvB;GACA,oBAAoB,UAAU;IAC5B,MAAM,eAAe;GACvB;GAIA,kBAAkB,UAAU;IAC1B,MAAM,eAAe;IACrB,QAAQ,SAAS,MAAM;GACzB;GAzBF,UAAA;IA2BE,qBAAC,cAAD,EAAA,UAAA,CACE,oBAAC,aAAD,EAAA,UAAc,UAAU,YAAY,2BAA2B,QAAqB,CAAA,GACpF,oBAAC,mBAAD;KAAmB,IAAI,GAAG,GAAG;KAC1B,UAAA,UAAU,YACT,sDAEA,qBAAA,UAAA,EAAA,UAAA;MAAE;MAC0B;MAC1B,qBAAC,QAAD;OAAM,aAAU;OAA2B,WAAU;OAArD,UAAA,CACE,oBAAC,QAAD;QAAM,eAAY;QAAQ,UAAA,gBAAgB,OAAO;OAAQ,CAAA,GACzD,oBAAC,QAAD;QAAM,WAAU;QAAW,UAAA,kBAAkB,OAAO;OAAQ,CAAA,CACxD;;MAAC;KAEP,EAAA,CAAA;IAEa,CAAA,CACP,EAAA,CAAA;IAEb,WAAW,oBAAC,OAAD;KAAK,WAAU;KAAiC;IAAc,CAAA,IAAI;IAG9E,oBAAC,QAAD;KAAM,MAAK;KAAS,aAAU;KAAS,WAAU;KAC9C,UAAA;IACG,CAAA;IAEN,oBAAC,cAAD,EAAA,UACG,UAAU,YACT,gBAEA,qBAAA,UAAA,EAAA,UAAA,CACG,YACC,oBAAC,QAAD;KAAQ,SAAQ;KAAU,SAAS;KAAW,UAAA;IAEtC,CAAA,IACN,MACJ,oBAAC,QAAD;KAAQ,KAAK;KAAS,SAAS;KAAW,SAAS;KAAQ,UAAA;IAEnD,CAAA,CACR,EAAA,CAAA,EAEQ,CAAA;GACD;;CACT,CAAA;AAEZ,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { Platform, RESERVED_KEYS, Shortcut, describeShortcut, detectPlatform, formatShortcut, needsModifier, parseShortcut, serializeShortcut, shortcutFromKey, shortcutParts, shortcutsEqual } from "./shortcut-model.js";
2
+ import { ShortcutRecorder, ShortcutRecorderProps, TakenShortcut } from "./shortcut-recorder.js";
3
+ export { type Platform, RESERVED_KEYS, type Shortcut, ShortcutRecorder, type ShortcutRecorderProps, type TakenShortcut, describeShortcut, detectPlatform, formatShortcut, needsModifier, parseShortcut, serializeShortcut, shortcutFromKey, shortcutParts, shortcutsEqual };
@@ -0,0 +1,3 @@
1
+ import { RESERVED_KEYS, describeShortcut, detectPlatform, formatShortcut, needsModifier, parseShortcut, serializeShortcut, shortcutFromKey, shortcutParts, shortcutsEqual } from "./shortcut-model.js";
2
+ import { ShortcutRecorder } from "./shortcut-recorder.js";
3
+ export { RESERVED_KEYS, ShortcutRecorder, describeShortcut, detectPlatform, formatShortcut, needsModifier, parseShortcut, serializeShortcut, shortcutFromKey, shortcutParts, shortcutsEqual };
@@ -0,0 +1,17 @@
1
+ import { defineMeta } from "../../registry/schema.js";
2
+ //#region src/components/shortcut-recorder/meta.ts
3
+ const meta = defineMeta({
4
+ name: "shortcut-recorder",
5
+ title: "Shortcut Recorder",
6
+ description: "Press the keys you want: records a chord, platform-aware, and says when it clashes.",
7
+ category: "form",
8
+ status: "stable",
9
+ dependencies: [],
10
+ registryDependencies: [],
11
+ files: ["shortcut-model.ts", "shortcut-recorder.tsx"],
12
+ a11y: "The recorder is a button named by what the shortcut does and described by its value in words — Command Shift K — while the key caps on it are hidden from assistive technology, since ⌘⇧K read aloud is noise. While recording it is aria-pressed and its description says what to do; Tab always leaves and Escape always cancels, so it is never a keyboard trap. Every outcome is announced through a status region present from the start: recorded, refused with the reason, clashing with which command, cancelled, cleared. Symbols are shown on a Mac and words elsewhere, and the description uses the platform's own names for its keys."
13
+ });
14
+ //#endregion
15
+ export { meta };
16
+
17
+ //# sourceMappingURL=meta.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meta.js","names":[],"sources":["../../../src/components/shortcut-recorder/meta.ts"],"sourcesContent":["import { defineMeta } from \"@/registry/schema\";\n\nexport const meta = defineMeta({\n name: \"shortcut-recorder\",\n title: \"Shortcut Recorder\",\n description:\n \"Press the keys you want: records a chord, platform-aware, and says when it clashes.\",\n category: \"form\",\n status: \"stable\",\n dependencies: [],\n registryDependencies: [],\n files: [\"shortcut-model.ts\", \"shortcut-recorder.tsx\"],\n a11y:\n \"The recorder is a button named by what the shortcut does and described by its value in \" +\n \"words — Command Shift K — while the key caps on it are hidden from assistive technology, \" +\n \"since ⌘⇧K read aloud is noise. While recording it is aria-pressed and its description \" +\n \"says what to do; Tab always leaves and Escape always cancels, so it is never a keyboard \" +\n \"trap. Every outcome is announced through a status region present from the start: recorded, \" +\n \"refused with the reason, clashing with which command, cancelled, cleared. Symbols are shown \" +\n \"on a Mac and words elsewhere, and the description uses the platform's own names for its \" +\n \"keys.\",\n});\n"],"mappings":";;AAEA,MAAa,OAAO,WAAW;CAC7B,MAAM;CACN,OAAO;CACP,aACE;CACF,UAAU;CACV,QAAQ;CACR,cAAc,CAAC;CACf,sBAAsB,CAAC;CACvB,OAAO,CAAC,qBAAqB,uBAAuB;CACpD,MACE;AAQJ,CAAC"}
@@ -0,0 +1,67 @@
1
+ //#region src/components/shortcut-recorder/shortcut-model.d.ts
2
+ /**
3
+ * A keyboard shortcut as a value: parsed from the string an app stores,
4
+ * built from a keydown, and written back out for a Mac, for everyone else,
5
+ * and for the ear.
6
+ *
7
+ * Pure, so the modifier canon, the platform rendering and the conflict rule
8
+ * are tested without a keyboard, and so the same `parseShortcut` can run on
9
+ * the server that validates a saved binding.
10
+ *
11
+ * The stored form is platform-neutral: `Mod+Shift+K`, where `Mod` is Command
12
+ * on a Mac and Control elsewhere. That is the one decision every app makes
13
+ * and few make explicitly — a binding saved as `Ctrl+K` on Windows is wrong
14
+ * the moment the same account opens on a Mac.
15
+ */
16
+ type Platform = "mac" | "other";
17
+ interface Shortcut {
18
+ /** The non-modifier key, canonical: `K`, `1`, `ArrowUp`, `F5`, `Enter`, `Space`. */
19
+ key: string;
20
+ /** Command on a Mac, Control elsewhere. */
21
+ mod: boolean;
22
+ /** Control, held explicitly — on a Mac this is a different key from Mod. */
23
+ ctrl: boolean;
24
+ alt: boolean;
25
+ shift: boolean;
26
+ }
27
+ /** Keys that cannot be a shortcut, because they already mean something. */
28
+ declare const RESERVED_KEYS: Set<string>;
29
+ /**
30
+ * `Mod+Shift+K` to a shortcut. Accepts the spellings people use — `Cmd`,
31
+ * `Command`, `Ctrl`, `Option` — and throws on an empty or modifier-only one.
32
+ */
33
+ declare function parseShortcut(text: string): Shortcut;
34
+ /** The stored, platform-neutral form. Modifiers in a fixed order, so equal shortcuts are equal strings. */
35
+ declare function serializeShortcut(shortcut: Shortcut): string;
36
+ declare function shortcutsEqual(a: Shortcut, b: Shortcut): boolean;
37
+ interface KeyLike {
38
+ key: string;
39
+ code?: string;
40
+ metaKey: boolean;
41
+ ctrlKey: boolean;
42
+ altKey: boolean;
43
+ shiftKey: boolean;
44
+ }
45
+ /**
46
+ * The shortcut a keydown amounts to, or null while only modifiers are held.
47
+ *
48
+ * Letters and digits come from `code`, not `key`: with Shift or Option held,
49
+ * `key` is `K`, `˚` or `∆` depending on the layout and the platform, and a
50
+ * shortcut recorded as Option-∆ is one nobody can read back. `Mod` is Meta
51
+ * on a Mac and Control elsewhere; the other one, when held, is kept as
52
+ * itself.
53
+ */
54
+ declare function shortcutFromKey(event: KeyLike, platform: Platform): Shortcut | null;
55
+ /** Printable keys need a modifier: a bare K as a shortcut fires while someone types a sentence. */
56
+ declare function needsModifier(shortcut: Shortcut): boolean;
57
+ /** The parts to draw, in order: symbols on a Mac, words elsewhere. */
58
+ declare function shortcutParts(shortcut: Shortcut, platform: Platform): string[];
59
+ /** "⌘⇧K" on a Mac, "Ctrl+Shift+K" elsewhere. */
60
+ declare function formatShortcut(shortcut: Shortcut, platform: Platform): string;
61
+ /** "Command Shift K" — words, for the ear, whatever the screen shows. */
62
+ declare function describeShortcut(shortcut: Shortcut, platform: Platform): string;
63
+ /** Whether the browser is on a Mac, by the only signal it gives. */
64
+ declare function detectPlatform(): Platform;
65
+ //#endregion
66
+ export { Platform, RESERVED_KEYS, Shortcut, describeShortcut, detectPlatform, formatShortcut, needsModifier, parseShortcut, serializeShortcut, shortcutFromKey, shortcutParts, shortcutsEqual };
67
+ //# sourceMappingURL=shortcut-model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shortcut-model.d.ts","names":[],"sources":["../../../src/components/shortcut-recorder/shortcut-model.ts"],"mappings":";;;;;;;;;;;;;;;KAeY;UAEK;;EAEf;;EAEA;;EAEA;EACA;EACA;;;cAMW,eAAa;;;;;iBAgCV,cAAc,eAAe;;iBAsB7B,kBAAkB,UAAU;iBAU5B,eAAe,GAAG,UAAU,GAAG;UAIrC;EACR;EACA;EACA;EACA;EACA;EACA;;;;;;;;;;;iBAYc,gBAAgB,OAAO,SAAS,UAAU,WAAW;;iBAoBrD,cAAc,UAAU;;iBA2BxB,cAAc,UAAU,UAAU,UAAU;;iBAe5C,eAAe,UAAU,UAAU,UAAU;;iBAK7C,iBAAiB,UAAU,UAAU,UAAU;;iBAa/C,kBAAkB"}
@@ -0,0 +1,168 @@
1
+ //#region src/components/shortcut-recorder/shortcut-model.ts
2
+ const MODIFIER_KEYS = /* @__PURE__ */ new Set([
3
+ "Meta",
4
+ "Control",
5
+ "Alt",
6
+ "Shift",
7
+ "OS",
8
+ "AltGraph",
9
+ "Fn"
10
+ ]);
11
+ /** Keys that cannot be a shortcut, because they already mean something. */
12
+ const RESERVED_KEYS = /* @__PURE__ */ new Set(["Tab", "Escape"]);
13
+ const KEY_ALIASES = {
14
+ " ": "Space",
15
+ spacebar: "Space",
16
+ esc: "Escape",
17
+ return: "Enter",
18
+ del: "Delete",
19
+ up: "ArrowUp",
20
+ down: "ArrowDown",
21
+ left: "ArrowLeft",
22
+ right: "ArrowRight",
23
+ cmd: "Mod",
24
+ command: "Mod",
25
+ meta: "Mod",
26
+ control: "Ctrl",
27
+ option: "Alt"
28
+ };
29
+ /** The canonical key name for a token someone typed or stored. */
30
+ function canonicalKey(token) {
31
+ const alias = KEY_ALIASES[token.toLowerCase()];
32
+ if (alias) return alias;
33
+ if (token.length === 1) return token.toUpperCase();
34
+ return token.charAt(0).toUpperCase() + token.slice(1);
35
+ }
36
+ /**
37
+ * `Mod+Shift+K` to a shortcut. Accepts the spellings people use — `Cmd`,
38
+ * `Command`, `Ctrl`, `Option` — and throws on an empty or modifier-only one.
39
+ */
40
+ function parseShortcut(text) {
41
+ const tokens = text.split("+").map((token) => token.trim()).filter(Boolean);
42
+ if (tokens.length === 0) throw new Error("Enter a shortcut.");
43
+ const shortcut = {
44
+ key: "",
45
+ mod: false,
46
+ ctrl: false,
47
+ alt: false,
48
+ shift: false
49
+ };
50
+ for (const token of tokens) {
51
+ const canonical = canonicalKey(token);
52
+ if (canonical === "Mod") shortcut.mod = true;
53
+ else if (canonical === "Ctrl") shortcut.ctrl = true;
54
+ else if (canonical === "Alt") shortcut.alt = true;
55
+ else if (canonical === "Shift") shortcut.shift = true;
56
+ else if (shortcut.key) throw new Error(`"${text}" has two keys; a shortcut has one.`);
57
+ else shortcut.key = canonical;
58
+ }
59
+ if (!shortcut.key) throw new Error(`"${text}" is only modifiers; it needs a key.`);
60
+ return shortcut;
61
+ }
62
+ /** The stored, platform-neutral form. Modifiers in a fixed order, so equal shortcuts are equal strings. */
63
+ function serializeShortcut(shortcut) {
64
+ const parts = [];
65
+ if (shortcut.mod) parts.push("Mod");
66
+ if (shortcut.ctrl) parts.push("Ctrl");
67
+ if (shortcut.alt) parts.push("Alt");
68
+ if (shortcut.shift) parts.push("Shift");
69
+ parts.push(shortcut.key);
70
+ return parts.join("+");
71
+ }
72
+ function shortcutsEqual(a, b) {
73
+ return serializeShortcut(a) === serializeShortcut(b);
74
+ }
75
+ /**
76
+ * The shortcut a keydown amounts to, or null while only modifiers are held.
77
+ *
78
+ * Letters and digits come from `code`, not `key`: with Shift or Option held,
79
+ * `key` is `K`, `˚` or `∆` depending on the layout and the platform, and a
80
+ * shortcut recorded as Option-∆ is one nobody can read back. `Mod` is Meta
81
+ * on a Mac and Control elsewhere; the other one, when held, is kept as
82
+ * itself.
83
+ */
84
+ function shortcutFromKey(event, platform) {
85
+ if (MODIFIER_KEYS.has(event.key)) return null;
86
+ let key;
87
+ const code = event.code ?? "";
88
+ if (/^Key[A-Z]$/.test(code)) key = code.slice(3);
89
+ else if (/^Digit[0-9]$/.test(code)) key = code.slice(5);
90
+ else key = canonicalKey(event.key);
91
+ const mac = platform === "mac";
92
+ return {
93
+ key,
94
+ mod: mac ? event.metaKey : event.ctrlKey,
95
+ ctrl: mac ? event.ctrlKey : false,
96
+ alt: event.altKey,
97
+ shift: event.shiftKey
98
+ };
99
+ }
100
+ /** Printable keys need a modifier: a bare K as a shortcut fires while someone types a sentence. */
101
+ function needsModifier(shortcut) {
102
+ return (shortcut.key.length === 1 || shortcut.key === "Space") && !shortcut.mod && !shortcut.ctrl && !shortcut.alt;
103
+ }
104
+ const MAC_SYMBOLS = {
105
+ Mod: "⌘",
106
+ Ctrl: "⌃",
107
+ Alt: "⌥",
108
+ Shift: "⇧",
109
+ Enter: "↩",
110
+ Backspace: "⌫",
111
+ Delete: "⌦",
112
+ ArrowUp: "↑",
113
+ ArrowDown: "↓",
114
+ ArrowLeft: "←",
115
+ ArrowRight: "→",
116
+ Escape: "⎋",
117
+ Space: "Space"
118
+ };
119
+ const SPOKEN = {
120
+ mac: {
121
+ Mod: "Command",
122
+ Ctrl: "Control",
123
+ Alt: "Option",
124
+ Shift: "Shift"
125
+ },
126
+ other: {
127
+ Mod: "Control",
128
+ Ctrl: "Control",
129
+ Alt: "Alt",
130
+ Shift: "Shift"
131
+ }
132
+ };
133
+ /** The parts to draw, in order: symbols on a Mac, words elsewhere. */
134
+ function shortcutParts(shortcut, platform) {
135
+ const parts = [];
136
+ const name = (modifier) => platform === "mac" ? MAC_SYMBOLS[modifier] ?? modifier : SPOKEN.other[modifier] ?? modifier;
137
+ if (shortcut.mod) parts.push(name("Mod"));
138
+ if (shortcut.ctrl && !(platform === "other" && shortcut.mod)) parts.push(name("Ctrl"));
139
+ if (shortcut.alt) parts.push(name("Alt"));
140
+ if (shortcut.shift) parts.push(name("Shift"));
141
+ parts.push(platform === "mac" ? MAC_SYMBOLS[shortcut.key] ?? shortcut.key : shortcut.key);
142
+ return parts;
143
+ }
144
+ /** "⌘⇧K" on a Mac, "Ctrl+Shift+K" elsewhere. */
145
+ function formatShortcut(shortcut, platform) {
146
+ return shortcutParts(shortcut, platform).join(platform === "mac" ? "" : "+");
147
+ }
148
+ /** "Command Shift K" — words, for the ear, whatever the screen shows. */
149
+ function describeShortcut(shortcut, platform) {
150
+ const words = [];
151
+ const spoken = SPOKEN[platform];
152
+ if (shortcut.mod) words.push(spoken.Mod ?? "Mod");
153
+ if (shortcut.ctrl && !(platform === "other" && shortcut.mod)) words.push(spoken.Ctrl ?? "Ctrl");
154
+ if (shortcut.alt) words.push(spoken.Alt ?? "Alt");
155
+ if (shortcut.shift) words.push(spoken.Shift ?? "Shift");
156
+ words.push(shortcut.key.replace(/^Arrow/, "Arrow "));
157
+ return words.join(" ");
158
+ }
159
+ /** Whether the browser is on a Mac, by the only signal it gives. */
160
+ function detectPlatform() {
161
+ if (typeof navigator === "undefined") return "other";
162
+ const hint = navigator.userAgentData?.platform ?? navigator.platform;
163
+ return /mac|iphone|ipad|ipod/i.test(hint) ? "mac" : "other";
164
+ }
165
+ //#endregion
166
+ export { RESERVED_KEYS, describeShortcut, detectPlatform, formatShortcut, needsModifier, parseShortcut, serializeShortcut, shortcutFromKey, shortcutParts, shortcutsEqual };
167
+
168
+ //# sourceMappingURL=shortcut-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shortcut-model.js","names":[],"sources":["../../../src/components/shortcut-recorder/shortcut-model.ts"],"sourcesContent":["/**\n * A keyboard shortcut as a value: parsed from the string an app stores,\n * built from a keydown, and written back out for a Mac, for everyone else,\n * and for the ear.\n *\n * Pure, so the modifier canon, the platform rendering and the conflict rule\n * are tested without a keyboard, and so the same `parseShortcut` can run on\n * the server that validates a saved binding.\n *\n * The stored form is platform-neutral: `Mod+Shift+K`, where `Mod` is Command\n * on a Mac and Control elsewhere. That is the one decision every app makes\n * and few make explicitly — a binding saved as `Ctrl+K` on Windows is wrong\n * the moment the same account opens on a Mac.\n */\n\nexport type Platform = \"mac\" | \"other\";\n\nexport interface Shortcut {\n /** The non-modifier key, canonical: `K`, `1`, `ArrowUp`, `F5`, `Enter`, `Space`. */\n key: string;\n /** Command on a Mac, Control elsewhere. */\n mod: boolean;\n /** Control, held explicitly — on a Mac this is a different key from Mod. */\n ctrl: boolean;\n alt: boolean;\n shift: boolean;\n}\n\nconst MODIFIER_KEYS = new Set([\"Meta\", \"Control\", \"Alt\", \"Shift\", \"OS\", \"AltGraph\", \"Fn\"]);\n\n/** Keys that cannot be a shortcut, because they already mean something. */\nexport const RESERVED_KEYS = new Set([\"Tab\", \"Escape\"]);\n\nconst KEY_ALIASES: Record<string, string> = {\n \" \": \"Space\",\n spacebar: \"Space\",\n esc: \"Escape\",\n return: \"Enter\",\n del: \"Delete\",\n up: \"ArrowUp\",\n down: \"ArrowDown\",\n left: \"ArrowLeft\",\n right: \"ArrowRight\",\n cmd: \"Mod\",\n command: \"Mod\",\n meta: \"Mod\",\n control: \"Ctrl\",\n option: \"Alt\",\n};\n\n/** The canonical key name for a token someone typed or stored. */\nfunction canonicalKey(token: string): string {\n const alias = KEY_ALIASES[token.toLowerCase()];\n if (alias) return alias;\n if (token.length === 1) return token.toUpperCase();\n // ArrowUp, F5, Enter, Backspace, Delete, Home, End, PageUp…\n return token.charAt(0).toUpperCase() + token.slice(1);\n}\n\n/**\n * `Mod+Shift+K` to a shortcut. Accepts the spellings people use — `Cmd`,\n * `Command`, `Ctrl`, `Option` — and throws on an empty or modifier-only one.\n */\nexport function parseShortcut(text: string): Shortcut {\n const tokens = text\n .split(\"+\")\n .map((token) => token.trim())\n .filter(Boolean);\n if (tokens.length === 0) throw new Error(\"Enter a shortcut.\");\n\n const shortcut: Shortcut = { key: \"\", mod: false, ctrl: false, alt: false, shift: false };\n for (const token of tokens) {\n const canonical = canonicalKey(token);\n if (canonical === \"Mod\") shortcut.mod = true;\n else if (canonical === \"Ctrl\") shortcut.ctrl = true;\n else if (canonical === \"Alt\") shortcut.alt = true;\n else if (canonical === \"Shift\") shortcut.shift = true;\n else if (shortcut.key) throw new Error(`\"${text}\" has two keys; a shortcut has one.`);\n else shortcut.key = canonical;\n }\n if (!shortcut.key) throw new Error(`\"${text}\" is only modifiers; it needs a key.`);\n return shortcut;\n}\n\n/** The stored, platform-neutral form. Modifiers in a fixed order, so equal shortcuts are equal strings. */\nexport function serializeShortcut(shortcut: Shortcut): string {\n const parts: string[] = [];\n if (shortcut.mod) parts.push(\"Mod\");\n if (shortcut.ctrl) parts.push(\"Ctrl\");\n if (shortcut.alt) parts.push(\"Alt\");\n if (shortcut.shift) parts.push(\"Shift\");\n parts.push(shortcut.key);\n return parts.join(\"+\");\n}\n\nexport function shortcutsEqual(a: Shortcut, b: Shortcut): boolean {\n return serializeShortcut(a) === serializeShortcut(b);\n}\n\ninterface KeyLike {\n key: string;\n code?: string;\n metaKey: boolean;\n ctrlKey: boolean;\n altKey: boolean;\n shiftKey: boolean;\n}\n\n/**\n * The shortcut a keydown amounts to, or null while only modifiers are held.\n *\n * Letters and digits come from `code`, not `key`: with Shift or Option held,\n * `key` is `K`, `˚` or `∆` depending on the layout and the platform, and a\n * shortcut recorded as Option-∆ is one nobody can read back. `Mod` is Meta\n * on a Mac and Control elsewhere; the other one, when held, is kept as\n * itself.\n */\nexport function shortcutFromKey(event: KeyLike, platform: Platform): Shortcut | null {\n if (MODIFIER_KEYS.has(event.key)) return null;\n\n let key: string;\n const code = event.code ?? \"\";\n if (/^Key[A-Z]$/.test(code)) key = code.slice(3);\n else if (/^Digit[0-9]$/.test(code)) key = code.slice(5);\n else key = canonicalKey(event.key);\n\n const mac = platform === \"mac\";\n return {\n key,\n mod: mac ? event.metaKey : event.ctrlKey,\n ctrl: mac ? event.ctrlKey : false,\n alt: event.altKey,\n shift: event.shiftKey,\n };\n}\n\n/** Printable keys need a modifier: a bare K as a shortcut fires while someone types a sentence. */\nexport function needsModifier(shortcut: Shortcut): boolean {\n const printable = shortcut.key.length === 1 || shortcut.key === \"Space\";\n return printable && !shortcut.mod && !shortcut.ctrl && !shortcut.alt;\n}\n\nconst MAC_SYMBOLS: Record<string, string> = {\n Mod: \"⌘\",\n Ctrl: \"⌃\",\n Alt: \"⌥\",\n Shift: \"⇧\",\n Enter: \"↩\",\n Backspace: \"⌫\",\n Delete: \"⌦\",\n ArrowUp: \"↑\",\n ArrowDown: \"↓\",\n ArrowLeft: \"←\",\n ArrowRight: \"→\",\n Escape: \"⎋\",\n Space: \"Space\",\n};\n\nconst SPOKEN: Record<Platform, Record<string, string>> = {\n mac: { Mod: \"Command\", Ctrl: \"Control\", Alt: \"Option\", Shift: \"Shift\" },\n other: { Mod: \"Control\", Ctrl: \"Control\", Alt: \"Alt\", Shift: \"Shift\" },\n};\n\n/** The parts to draw, in order: symbols on a Mac, words elsewhere. */\nexport function shortcutParts(shortcut: Shortcut, platform: Platform): string[] {\n const parts: string[] = [];\n const name = (modifier: string) =>\n platform === \"mac\"\n ? (MAC_SYMBOLS[modifier] ?? modifier)\n : (SPOKEN.other[modifier] ?? modifier);\n if (shortcut.mod) parts.push(name(\"Mod\"));\n if (shortcut.ctrl && !(platform === \"other\" && shortcut.mod)) parts.push(name(\"Ctrl\"));\n if (shortcut.alt) parts.push(name(\"Alt\"));\n if (shortcut.shift) parts.push(name(\"Shift\"));\n parts.push(platform === \"mac\" ? (MAC_SYMBOLS[shortcut.key] ?? shortcut.key) : shortcut.key);\n return parts;\n}\n\n/** \"⌘⇧K\" on a Mac, \"Ctrl+Shift+K\" elsewhere. */\nexport function formatShortcut(shortcut: Shortcut, platform: Platform): string {\n return shortcutParts(shortcut, platform).join(platform === \"mac\" ? \"\" : \"+\");\n}\n\n/** \"Command Shift K\" — words, for the ear, whatever the screen shows. */\nexport function describeShortcut(shortcut: Shortcut, platform: Platform): string {\n const words: string[] = [];\n const spoken = SPOKEN[platform];\n if (shortcut.mod) words.push(spoken.Mod ?? \"Mod\");\n if (shortcut.ctrl && !(platform === \"other\" && shortcut.mod))\n words.push(spoken.Ctrl ?? \"Ctrl\");\n if (shortcut.alt) words.push(spoken.Alt ?? \"Alt\");\n if (shortcut.shift) words.push(spoken.Shift ?? \"Shift\");\n words.push(shortcut.key.replace(/^Arrow/, \"Arrow \"));\n return words.join(\" \");\n}\n\n/** Whether the browser is on a Mac, by the only signal it gives. */\nexport function detectPlatform(): Platform {\n if (typeof navigator === \"undefined\") return \"other\";\n const hint =\n (navigator as { userAgentData?: { platform?: string } }).userAgentData?.platform ??\n navigator.platform;\n return /mac|iphone|ipad|ipod/i.test(hint) ? \"mac\" : \"other\";\n}\n"],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ import { Platform } from "./shortcut-model.js";
2
+ import { ComponentPropsWithRef } from "react";
3
+ //#region src/components/shortcut-recorder/shortcut-recorder.d.ts
4
+ /**
5
+ * Press the keys you want.
6
+ *
7
+ * Linear, Slack, VS Code, Figma and Superhuman each wrote one, and no
8
+ * component library ships the recorder — only the `<kbd>` that displays the
9
+ * result. The recorder is the hard part: a button that, when pressed, stops
10
+ * being a button and starts being a keyboard listener, and has to come back.
11
+ *
12
+ * Three things it does that a listener bolted onto an input does not. It
13
+ * reads letters from `code`, not `key`, so Option-K records as Option K and
14
+ * not as the ˚ the Mac produced. It stores `Mod`, not Command or Control,
15
+ * so a binding saved on one machine is right on the other. And it refuses a
16
+ * bare printable key by saying so, because a shortcut that fires while
17
+ * someone types a sentence is the bug every app that allowed it later fixed.
18
+ *
19
+ * Tab and Escape are never recorded: Tab leaves, Escape cancels, and a
20
+ * recorder that captures both is a keyboard trap with a nice label. A
21
+ * chord that another command already uses is said, with that command's
22
+ * name, and applied only if the person says to use it anyway.
23
+ */
24
+ interface TakenShortcut {
25
+ /** Stored form: `Mod+K`. */
26
+ shortcut: string;
27
+ /** What it does: "Search". */
28
+ label: string;
29
+ }
30
+ interface ShortcutRecorderProps extends Omit<ComponentPropsWithRef<"div">, "onChange" | "defaultValue"> {
31
+ /** What the shortcut triggers: "Open search". Names the control. */
32
+ label: string;
33
+ /** Stored form, `Mod+Shift+K`, or null for none. Controlled. */
34
+ value?: string | null;
35
+ defaultValue?: string | null;
36
+ onChange?: (value: string | null) => void;
37
+ /** Shortcuts other commands already use. A clash is said, and applied only on request. */
38
+ taken?: TakenShortcut[];
39
+ /** Mac shows ⌘⇧K; everything else shows Ctrl+Shift+K. Detected if omitted. */
40
+ platform?: Platform;
41
+ /** Printable keys need a modifier. On by default, for the reason in the source. */
42
+ requireModifier?: boolean;
43
+ }
44
+ declare function ShortcutRecorder({ className, label, value: valueProp, defaultValue, onChange, taken, platform: platformProp, requireModifier, ...props }: ShortcutRecorderProps): import("react").JSX.Element;
45
+ //#endregion
46
+ export { ShortcutRecorder, ShortcutRecorderProps, TakenShortcut };
47
+ //# sourceMappingURL=shortcut-recorder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shortcut-recorder.d.ts","names":[],"sources":["../../../src/components/shortcut-recorder/shortcut-recorder.tsx"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;UA+CiB;;EAEf;;EAEA;;UAGe,8BAA8B,KAC7C;;EAIA;;EAEA;EACA;EACA,YAAY;;EAEZ,QAAQ;;EAER,WAAW;;EAEX;;iBAUc,mBACd,WACA,OACA,OAAO,WACP,cACA,UACA,OACA,UAAU,cACV,oBACG,SACF,wCAAqB,IAAA"}