@gusnips/react 0.9.24 → 0.9.26

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 (116) hide show
  1. package/README.md +173 -36
  2. package/dist/api-client.d.ts +51 -51
  3. package/dist/api-client.d.ts.map +1 -1
  4. package/dist/api-client.js +3 -3
  5. package/dist/api-error.d.ts +11 -11
  6. package/dist/api-error.js +11 -11
  7. package/dist/auth-email.d.ts +40 -0
  8. package/dist/auth-email.d.ts.map +1 -0
  9. package/dist/auth-email.js +106 -0
  10. package/dist/auth-email.js.map +1 -0
  11. package/dist/auth-store.d.ts +23 -23
  12. package/dist/auth-store.js +13 -13
  13. package/dist/commands.d.ts +80 -0
  14. package/dist/commands.d.ts.map +1 -0
  15. package/dist/commands.js +192 -0
  16. package/dist/commands.js.map +1 -0
  17. package/dist/copy.d.ts +3 -3
  18. package/dist/copy.js +3 -3
  19. package/dist/deploy-recovery.d.ts +40 -40
  20. package/dist/deploy-recovery.js +40 -40
  21. package/dist/describe-error.d.ts +68 -68
  22. package/dist/describe-error.d.ts.map +1 -1
  23. package/dist/describe-error.js +5 -5
  24. package/dist/error-boundary.d.ts +12 -12
  25. package/dist/guards.d.ts +15 -15
  26. package/dist/guards.js +3 -3
  27. package/dist/hydrate.d.ts +15 -16
  28. package/dist/hydrate.d.ts.map +1 -1
  29. package/dist/hydrate.js +9 -9
  30. package/dist/hydrate.js.map +1 -1
  31. package/dist/i18n.d.ts +15 -16
  32. package/dist/i18n.d.ts.map +1 -1
  33. package/dist/i18n.js +5 -6
  34. package/dist/i18n.js.map +1 -1
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +1 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/internal-path.d.ts +8 -7
  40. package/dist/internal-path.d.ts.map +1 -1
  41. package/dist/internal-path.js +8 -7
  42. package/dist/internal-path.js.map +1 -1
  43. package/dist/prerender-contract.d.ts +7 -7
  44. package/dist/prerender-contract.js +7 -7
  45. package/dist/query.d.ts +16 -17
  46. package/dist/query.d.ts.map +1 -1
  47. package/dist/query.js +2 -2
  48. package/dist/query.js.map +1 -1
  49. package/dist/sse.d.ts +19 -20
  50. package/dist/sse.d.ts.map +1 -1
  51. package/dist/sse.js +18 -19
  52. package/dist/sse.js.map +1 -1
  53. package/dist/states.d.ts +12 -12
  54. package/dist/states.d.ts.map +1 -1
  55. package/dist/supabase-session.d.ts +47 -48
  56. package/dist/supabase-session.d.ts.map +1 -1
  57. package/dist/supabase-session.js +35 -35
  58. package/dist/supabase-session.js.map +1 -1
  59. package/dist/theme.d.ts +4 -4
  60. package/dist/theme.js +4 -4
  61. package/dist/ui/class-name.d.ts +3 -4
  62. package/dist/ui/class-name.d.ts.map +1 -1
  63. package/dist/ui/class-name.js +3 -4
  64. package/dist/ui/class-name.js.map +1 -1
  65. package/dist/ui/combobox.d.ts +11 -11
  66. package/dist/ui/combobox.d.ts.map +1 -1
  67. package/dist/ui/dialog.d.ts +5 -5
  68. package/dist/ui/dialog.js +2 -2
  69. package/dist/ui/drawer.d.ts +10 -10
  70. package/dist/ui/drawer.d.ts.map +1 -1
  71. package/dist/ui/drawer.js +2 -2
  72. package/dist/ui/input.d.ts +2 -2
  73. package/dist/ui/input.js +2 -2
  74. package/dist/ui/menu.d.ts +9 -9
  75. package/dist/ui/menu.d.ts.map +1 -1
  76. package/dist/ui/menu.js +4 -4
  77. package/dist/ui/menu.js.map +1 -1
  78. package/dist/ui/select.d.ts +12 -12
  79. package/dist/ui/select.d.ts.map +1 -1
  80. package/dist/ui/select.js +3 -3
  81. package/dist/ui/tabs.d.ts +14 -15
  82. package/dist/ui/tabs.d.ts.map +1 -1
  83. package/dist/ui/tabs.js +13 -14
  84. package/dist/ui/tabs.js.map +1 -1
  85. package/package.json +5 -1
  86. package/src/api-client.ts +51 -51
  87. package/src/api-error.ts +11 -11
  88. package/src/auth-email.test.ts +145 -0
  89. package/src/auth-email.ts +112 -0
  90. package/src/auth-store.ts +23 -23
  91. package/src/commands-hook.test.ts +444 -0
  92. package/src/commands.test.ts +245 -0
  93. package/src/commands.ts +268 -0
  94. package/src/copy.ts +3 -3
  95. package/src/deploy-recovery.ts +40 -40
  96. package/src/describe-error.ts +68 -68
  97. package/src/error-boundary.tsx +12 -12
  98. package/src/guards.tsx +15 -15
  99. package/src/hydrate.ts +15 -16
  100. package/src/i18n.ts +15 -16
  101. package/src/index.ts +2 -0
  102. package/src/internal-path.ts +8 -7
  103. package/src/prerender-contract.ts +7 -7
  104. package/src/query.ts +16 -17
  105. package/src/sse.ts +19 -20
  106. package/src/states.ts +12 -12
  107. package/src/supabase-session.ts +46 -47
  108. package/src/theme.ts +4 -4
  109. package/src/ui/class-name.ts +3 -4
  110. package/src/ui/combobox.tsx +11 -11
  111. package/src/ui/dialog.tsx +5 -5
  112. package/src/ui/drawer.tsx +10 -10
  113. package/src/ui/input.tsx +2 -2
  114. package/src/ui/menu.tsx +8 -8
  115. package/src/ui/select.tsx +12 -12
  116. package/src/ui/tabs.tsx +14 -15
@@ -2,9 +2,9 @@ import type { ApiErrorBody } from "@gusnips/http";
2
2
  /**
3
3
  * A refusal, with the whole envelope intact.
4
4
  *
5
- * `code` is what the UI switches on, `messageKey` is what gets localized, and `details` carries
6
- * the parts that make a refusal actionable — the `resetAt` on a 429, the plan that lifts a 402.
7
- * Keeping all of it means a caller never has to re-parse a response body that has already been
5
+ * `code` is what the UI switches on, `messageKey` is what gets localized, and `details` carries the
6
+ * parts that make a refusal actionable, such as the `resetAt` on a 429 or the plan that lifts a
7
+ * 402. Keeping all of it means a caller never has to re-parse a response body that has already been
8
8
  * read once.
9
9
  *
10
10
  * A real `class` and not an interface with a factory: one donor used the interface shape, which
@@ -22,16 +22,16 @@ export declare class ApiError extends Error {
22
22
  *
23
23
  * Read from the `Retry-After` HEADER, which is where HTTP puts it and where a rate limiter
24
24
  * written by anyone puts it. The second migration is why this exists: that API sends
25
- * `Retry-After: 60` on every 429 and nothing in its body, so the package's only reader —
26
- * which looked in `details.retryAfterSecs`, the first donor's convention — found nothing, and
27
- * both the copy and the retry rule were flying blind on the one refusal that states its own
28
- * expiry. Two conventions, same fact; `retryAfterSecs` from `@gusnips/http/retry` reads either.
25
+ * `Retry-After: 60` on every 429 and nothing in its body, so the package's only reader looked in
26
+ * `details.retryAfterSecs`, the first donor's convention, and found nothing, and both the copy
27
+ * and the retry rule were flying blind on the one refusal that states its own expiry. Two
28
+ * conventions, same fact; `retryAfterSecs` from `@gusnips/http/retry` reads either.
29
29
  */
30
30
  readonly retryAfterSecs: number | undefined;
31
31
  /**
32
- * The client raised this itself to stop a caller while it was already handling the
33
- * situation — today, the 401 thrown after a dead session, where the person is already on
34
- * their way to the sign-in page.
32
+ * The client raised this itself to stop a caller while it was already handling the situation.
33
+ * Today this is the 401 thrown after a dead session, where the person is already on their way to
34
+ * the sign-in page.
35
35
  *
36
36
  * It is control flow, not a fault. Exception reporting should skip it, and an error surface
37
37
  * should not ask somebody to retry something that is not broken. One donor had this and it is
@@ -46,7 +46,7 @@ export declare class ApiError extends Error {
46
46
  });
47
47
  }
48
48
  /**
49
- * An aborted request — the caller's own `AbortController`, or a navigation that unmounted the
49
+ * A request aborted by the caller's own `AbortController` or a navigation that unmounted the
50
50
  * component waiting on it.
51
51
  *
52
52
  * Worth its own predicate because it is the one failure that must NOT be reported, retried or
package/dist/api-error.js CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * A refusal, with the whole envelope intact.
3
3
  *
4
- * `code` is what the UI switches on, `messageKey` is what gets localized, and `details` carries
5
- * the parts that make a refusal actionable — the `resetAt` on a 429, the plan that lifts a 402.
6
- * Keeping all of it means a caller never has to re-parse a response body that has already been
4
+ * `code` is what the UI switches on, `messageKey` is what gets localized, and `details` carries the
5
+ * parts that make a refusal actionable, such as the `resetAt` on a 429 or the plan that lifts a
6
+ * 402. Keeping all of it means a caller never has to re-parse a response body that has already been
7
7
  * read once.
8
8
  *
9
9
  * A real `class` and not an interface with a factory: one donor used the interface shape, which
@@ -21,16 +21,16 @@ export class ApiError extends Error {
21
21
  *
22
22
  * Read from the `Retry-After` HEADER, which is where HTTP puts it and where a rate limiter
23
23
  * written by anyone puts it. The second migration is why this exists: that API sends
24
- * `Retry-After: 60` on every 429 and nothing in its body, so the package's only reader —
25
- * which looked in `details.retryAfterSecs`, the first donor's convention — found nothing, and
26
- * both the copy and the retry rule were flying blind on the one refusal that states its own
27
- * expiry. Two conventions, same fact; `retryAfterSecs` from `@gusnips/http/retry` reads either.
24
+ * `Retry-After: 60` on every 429 and nothing in its body, so the package's only reader looked in
25
+ * `details.retryAfterSecs`, the first donor's convention, and found nothing, and both the copy
26
+ * and the retry rule were flying blind on the one refusal that states its own expiry. Two
27
+ * conventions, same fact; `retryAfterSecs` from `@gusnips/http/retry` reads either.
28
28
  */
29
29
  retryAfterSecs;
30
30
  /**
31
- * The client raised this itself to stop a caller while it was already handling the
32
- * situation — today, the 401 thrown after a dead session, where the person is already on
33
- * their way to the sign-in page.
31
+ * The client raised this itself to stop a caller while it was already handling the situation.
32
+ * Today this is the 401 thrown after a dead session, where the person is already on their way to
33
+ * the sign-in page.
34
34
  *
35
35
  * It is control flow, not a fault. Exception reporting should skip it, and an error surface
36
36
  * should not ask somebody to retry something that is not broken. One donor had this and it is
@@ -51,7 +51,7 @@ export class ApiError extends Error {
51
51
  }
52
52
  }
53
53
  /**
54
- * An aborted request — the caller's own `AbortController`, or a navigation that unmounted the
54
+ * A request aborted by the caller's own `AbortController` or a navigation that unmounted the
55
55
  * component waiting on it.
56
56
  *
57
57
  * Worth its own predicate because it is the one failure that must NOT be reported, retried or
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The email a person has typed during sign-in, kept so the next auth screen opens with it filled.
3
+ *
4
+ * Sign-in, "use a code instead", "forgot password" and "create account" are separate screens, and
5
+ * each one used to start with an empty email field. Someone who typed their address, then chose
6
+ * another way in, typed it again. The fix is the same everywhere, so it lives here once.
7
+ *
8
+ * ```tsx
9
+ * <input name="email" type="email" defaultValue={recallAuthEmail()}
10
+ * onChange={(event) => rememberAuthEmail(event.currentTarget.value)} />
11
+ * ```
12
+ *
13
+ * - **Not in the URL.** A query string rides in `Referer`, in access logs, in history and in every
14
+ * analytics tool that records page addresses. An email is personal data and has no business there.
15
+ * - **`localStorage`, not `sessionStorage`.** An emailed link opens in a new tab, and a new tab
16
+ * does not inherit `sessionStorage`. The person who confirms their account from the mail lands
17
+ * on sign-in in that new tab, which is the screen that most needs the address.
18
+ * - **Thirty minutes, sliding.** Long enough to read a code in a mail client and come back; short
19
+ * enough that a shared computer forgets. Every keystroke renews it, and a value found expired is
20
+ * deleted when it is read, so it does not sit on disk afterwards.
21
+ * - **Memory behind it.** A browser that blocks site data throws on `localStorage` itself, and
22
+ * React Native has none. The value then lasts until the page (or the app) is left, which still
23
+ * covers moving between screens.
24
+ *
25
+ * Read it once when a screen opens, as a default value. It is not a live store: nothing re-renders
26
+ * when it changes, and a prerendered page must read it after hydration, never while rendering.
27
+ * Never put a password, a code or a token here. Call `forgetAuthEmail()` once a session exists, so
28
+ * the next person at this browser starts empty.
29
+ */
30
+ /** The address typed earlier in this sign-in, or an empty string when there is none. */
31
+ export declare function recallAuthEmail(): string;
32
+ /**
33
+ * Keep what the person has typed. Call it from the email field's change handler, so every screen
34
+ * they reach next can open with it. The value is stored as typed: trim it where you submit it.
35
+ * An empty value forgets.
36
+ */
37
+ export declare function rememberAuthEmail(value: string): void;
38
+ /** Drop it. Call this once a session exists, so the next person at this browser starts empty. */
39
+ export declare function forgetAuthEmail(): void;
40
+ //# sourceMappingURL=auth-email.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-email.d.ts","sourceRoot":"","sources":["../src/auth-email.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAgDH,wFAAwF;AACxF,wBAAgB,eAAe,IAAI,MAAM,CAQxC;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CASrD;AAED,iGAAiG;AACjG,wBAAgB,eAAe,IAAI,IAAI,CAOtC"}
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The email a person has typed during sign-in, kept so the next auth screen opens with it filled.
3
+ *
4
+ * Sign-in, "use a code instead", "forgot password" and "create account" are separate screens, and
5
+ * each one used to start with an empty email field. Someone who typed their address, then chose
6
+ * another way in, typed it again. The fix is the same everywhere, so it lives here once.
7
+ *
8
+ * ```tsx
9
+ * <input name="email" type="email" defaultValue={recallAuthEmail()}
10
+ * onChange={(event) => rememberAuthEmail(event.currentTarget.value)} />
11
+ * ```
12
+ *
13
+ * - **Not in the URL.** A query string rides in `Referer`, in access logs, in history and in every
14
+ * analytics tool that records page addresses. An email is personal data and has no business there.
15
+ * - **`localStorage`, not `sessionStorage`.** An emailed link opens in a new tab, and a new tab
16
+ * does not inherit `sessionStorage`. The person who confirms their account from the mail lands
17
+ * on sign-in in that new tab, which is the screen that most needs the address.
18
+ * - **Thirty minutes, sliding.** Long enough to read a code in a mail client and come back; short
19
+ * enough that a shared computer forgets. Every keystroke renews it, and a value found expired is
20
+ * deleted when it is read, so it does not sit on disk afterwards.
21
+ * - **Memory behind it.** A browser that blocks site data throws on `localStorage` itself, and
22
+ * React Native has none. The value then lasts until the page (or the app) is left, which still
23
+ * covers moving between screens.
24
+ *
25
+ * Read it once when a screen opens, as a default value. It is not a live store: nothing re-renders
26
+ * when it changes, and a prerendered page must read it after hydration, never while rendering.
27
+ * Never put a password, a code or a token here. Call `forgetAuthEmail()` once a session exists, so
28
+ * the next person at this browser starts empty.
29
+ */
30
+ const KEY = "frontkit.auth-email";
31
+ const TTL_MS = 30 * 60 * 1000;
32
+ /** The longest an address can be (RFC 5321). A longer value is not one, and is not kept. */
33
+ const MAX_LENGTH = 254;
34
+ let memory = null;
35
+ function isSaved(value) {
36
+ return (typeof value === "object" &&
37
+ value !== null &&
38
+ "email" in value &&
39
+ typeof value.email === "string" &&
40
+ "at" in value &&
41
+ typeof value.at === "number");
42
+ }
43
+ function readStored() {
44
+ try {
45
+ const raw = globalThis.localStorage?.getItem(KEY);
46
+ if (!raw)
47
+ return null;
48
+ const parsed = JSON.parse(raw);
49
+ return isSaved(parsed) ? parsed : null;
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ /**
56
+ * Storage and memory are written together, so they only differ when one of them missed a write:
57
+ * storage that is full keeps an older address while memory holds the new one, and a second tab
58
+ * writes storage while this tab's memory goes stale. The newer of the two is the one the person
59
+ * typed last.
60
+ */
61
+ function newest() {
62
+ const stored = readStored();
63
+ if (!stored || !memory)
64
+ return stored ?? memory;
65
+ return stored.at >= memory.at ? stored : memory;
66
+ }
67
+ /** The address typed earlier in this sign-in, or an empty string when there is none. */
68
+ export function recallAuthEmail() {
69
+ const saved = newest();
70
+ if (!saved)
71
+ return "";
72
+ if (Date.now() - saved.at > TTL_MS) {
73
+ forgetAuthEmail();
74
+ return "";
75
+ }
76
+ return saved.email;
77
+ }
78
+ /**
79
+ * Keep what the person has typed. Call it from the email field's change handler, so every screen
80
+ * they reach next can open with it. The value is stored as typed: trim it where you submit it.
81
+ * An empty value forgets.
82
+ */
83
+ export function rememberAuthEmail(value) {
84
+ if (value === "")
85
+ return forgetAuthEmail();
86
+ if (value.length > MAX_LENGTH)
87
+ return;
88
+ memory = { email: value, at: Date.now() };
89
+ try {
90
+ globalThis.localStorage?.setItem(KEY, JSON.stringify(memory));
91
+ }
92
+ catch {
93
+ // Blocked or full: the memory copy above carries it until the page is left.
94
+ }
95
+ }
96
+ /** Drop it. Call this once a session exists, so the next person at this browser starts empty. */
97
+ export function forgetAuthEmail() {
98
+ memory = null;
99
+ try {
100
+ globalThis.localStorage?.removeItem(KEY);
101
+ }
102
+ catch {
103
+ // Blocked: nothing was stored, so there is nothing to drop.
104
+ }
105
+ }
106
+ //# sourceMappingURL=auth-email.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-email.js","sourceRoot":"","sources":["../src/auth-email.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,MAAM,GAAG,GAAG,qBAAqB,CAAC;AAClC,MAAM,MAAM,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAC9B,4FAA4F;AAC5F,MAAM,UAAU,GAAG,GAAG,CAAC;AAOvB,IAAI,MAAM,GAAiB,IAAI,CAAC;AAEhC,SAAS,OAAO,CAAC,KAAc;IAC7B,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,OAAO,IAAI,KAAK;QAChB,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ;QAC/B,IAAI,IAAI,KAAK;QACb,OAAO,KAAK,CAAC,EAAE,KAAK,QAAQ,CAC7B,CAAC;AACJ,CAAC;AAED,SAAS,UAAU;IACjB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,UAAU,CAAC,YAAY,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;QAClD,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAC;QACtB,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACxC,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,SAAS,MAAM;IACb,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM;QAAE,OAAO,MAAM,IAAI,MAAM,CAAC;IAChD,OAAO,MAAM,CAAC,EAAE,IAAI,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;AAClD,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,eAAe;IAC7B,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC;IACvB,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,CAAC;IACtB,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,EAAE,GAAG,MAAM,EAAE,CAAC;QACnC,eAAe,EAAE,CAAC;QAClB,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,OAAO,KAAK,CAAC,KAAK,CAAC;AACrB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAa;IAC7C,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,eAAe,EAAE,CAAC;IAC3C,IAAI,KAAK,CAAC,MAAM,GAAG,UAAU;QAAE,OAAO;IACtC,MAAM,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;IAC1C,IAAI,CAAC;QACH,UAAU,CAAC,YAAY,EAAE,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;IAChE,CAAC;IAAC,MAAM,CAAC;QACP,4EAA4E;IAC9E,CAAC;AACH,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,eAAe;IAC7B,MAAM,GAAG,IAAI,CAAC;IACd,IAAI,CAAC;QACH,UAAU,CAAC,YAAY,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC;IAC3C,CAAC;IAAC,MAAM,CAAC;QACP,4DAA4D;IAC9D,CAAC;AACH,CAAC"}
@@ -2,14 +2,14 @@ import { type StoreApi, type UseBoundStore } from "zustand";
2
2
  /**
3
3
  * Session FLAGS only.
4
4
  *
5
- * Who the person is — their plan, their staff bit, whether they are suspended — comes from
6
- * `GET /auth/me` through react-query, never from here. Mirroring server state in a client store
7
- * is how two sources of truth start disagreeing, and the one that is wrong is always the one on
8
- * screen. Three repos wrote this same store; two of them wrote that same warning in a comment.
5
+ * Who the person is, including their plan, staff bit and suspension status, comes from `GET
6
+ * /auth/me` through react-query, never from here. Mirroring server state in a client store is how
7
+ * two sources of truth start disagreeing, and the one that is wrong is always the one on screen.
8
+ * Three repos wrote this same store; two of them wrote that same warning in a comment.
9
9
  *
10
10
  * `TUser` stays generic because it is the one part that differs: one product carries an
11
- * anonymous-browsing flag, another does not. Keep it to what a GUARD needs — an id and an
12
- * email is what all three donors had.
11
+ * anonymous-browsing flag, another does not. Keep it to what a GUARD needs. All three donors had an
12
+ * id and an email.
13
13
  */
14
14
  export interface AuthState<TUser> {
15
15
  user: TUser | null;
@@ -20,10 +20,10 @@ export interface AuthState<TUser> {
20
20
  */
21
21
  isLoading: boolean;
22
22
  /**
23
- * Set (or clear) the signed-in user. This ENDS the loading state — knowing who they are is
24
- * what the bootstrap was waiting for. Two of the three donors left `isLoading` alone here and
25
- * relied on a separate `setLoading(false)`; forgetting that call leaves every guard spinning,
26
- * so the safe default is to do it in one write.
23
+ * Set (or clear) the signed-in user. This ENDS the loading state, because the bootstrap was
24
+ * waiting to learn who they are. Two of the three donors left `isLoading` alone here and relied
25
+ * on a separate `setLoading(false)`; forgetting that call leaves every guard spinning, so the
26
+ * safe default is to do it in one write.
27
27
  */
28
28
  setUser: (user: TUser | null) => void;
29
29
  setLoading: (isLoading: boolean) => void;
@@ -36,12 +36,12 @@ export interface AuthState<TUser> {
36
36
  * The whole reason this is a factory and not a store: `isLoading` must start FALSE where there
37
37
  * is no window, and only a factory can decide that at the call site rather than at import.
38
38
  *
39
- * A session bootstrap can only be in flight in a browser. The BUILD renders this app to files
40
- * with no window at all, so `true` there is a wait that never ends — one donor shipped a route
41
- * guard holding its loading screen forever, and prerendered a public page as 1,174 bytes of
42
- * `role="status"`: a spinner as the indexable body of a page whose entire purpose was to be
43
- * found. With no browser there is no session and never will be, which is exactly the state a
44
- * first-time visitor arrives in, so that is what the page should render.
39
+ * A session bootstrap can only be in flight in a browser. The BUILD renders this app to files with
40
+ * no window at all, so `true` there is a wait that never ends. One donor shipped a route guard
41
+ * holding its loading screen forever, and prerendered a public page as 1,174 bytes of
42
+ * `role="status"`: a spinner as the indexable body of a page whose entire purpose was to be found.
43
+ * With no browser there is no session and never will be, which is exactly the state a first-time
44
+ * visitor arrives in, so that is what the page should render.
45
45
  *
46
46
  * Only one of the three donors knew this. It is invariant 7.
47
47
  */
@@ -59,13 +59,13 @@ type AuthSet<TUser, TExtra> = (partial: (TExtra & Partial<AuthState<TUser>>) | P
59
59
  /**
60
60
  * The session flags as a plain object, for a product whose store needs more than them.
61
61
  *
62
- * `createAuthStore` owns its `create()` call, which means it owns the whole store: a product
63
- * cannot wrap it in `persist`, cannot add an action, and cannot add a field. Both adopters that
64
- * met it had a superset and neither could use it. One holds a remember-me choice, a profile row
65
- * and the sign-in methods themselves, under `persist`. The other adds `isAnonymous` — and that
66
- * one is the reason this takes a `derive` function rather than just letting the caller spread
67
- * extra keys in: `isAnonymous` is read off the user, so it has to be rewritten by `setUser` and
68
- * `clear`, which are exactly the two writes the product does not own.
62
+ * `createAuthStore` owns its `create()` call, which means it owns the whole store: a product cannot
63
+ * wrap it in `persist`, cannot add an action, and cannot add a field. Both adopters that met it had
64
+ * a superset and neither could use it. One holds a remember-me choice, a profile row and the
65
+ * sign-in methods themselves, under `persist`. The other adds `isAnonymous`, which is the reason
66
+ * this takes a `derive` function rather than just letting the caller spread extra keys in:
67
+ * `isAnonymous` is read off the user, so it has to be rewritten by `setUser` and `clear`, which are
68
+ * exactly the two writes the product does not own.
69
69
  *
70
70
  * The caller keeps `create`, so middleware, extra actions and the store's own name stay theirs:
71
71
  *
@@ -5,12 +5,12 @@ import { create } from "zustand";
5
5
  * The whole reason this is a factory and not a store: `isLoading` must start FALSE where there
6
6
  * is no window, and only a factory can decide that at the call site rather than at import.
7
7
  *
8
- * A session bootstrap can only be in flight in a browser. The BUILD renders this app to files
9
- * with no window at all, so `true` there is a wait that never ends — one donor shipped a route
10
- * guard holding its loading screen forever, and prerendered a public page as 1,174 bytes of
11
- * `role="status"`: a spinner as the indexable body of a page whose entire purpose was to be
12
- * found. With no browser there is no session and never will be, which is exactly the state a
13
- * first-time visitor arrives in, so that is what the page should render.
8
+ * A session bootstrap can only be in flight in a browser. The BUILD renders this app to files with
9
+ * no window at all, so `true` there is a wait that never ends. One donor shipped a route guard
10
+ * holding its loading screen forever, and prerendered a public page as 1,174 bytes of
11
+ * `role="status"`: a spinner as the indexable body of a page whose entire purpose was to be found.
12
+ * With no browser there is no session and never will be, which is exactly the state a first-time
13
+ * visitor arrives in, so that is what the page should render.
14
14
  *
15
15
  * Only one of the three donors knew this. It is invariant 7.
16
16
  */
@@ -20,13 +20,13 @@ export function createAuthStore() {
20
20
  /**
21
21
  * The session flags as a plain object, for a product whose store needs more than them.
22
22
  *
23
- * `createAuthStore` owns its `create()` call, which means it owns the whole store: a product
24
- * cannot wrap it in `persist`, cannot add an action, and cannot add a field. Both adopters that
25
- * met it had a superset and neither could use it. One holds a remember-me choice, a profile row
26
- * and the sign-in methods themselves, under `persist`. The other adds `isAnonymous` — and that
27
- * one is the reason this takes a `derive` function rather than just letting the caller spread
28
- * extra keys in: `isAnonymous` is read off the user, so it has to be rewritten by `setUser` and
29
- * `clear`, which are exactly the two writes the product does not own.
23
+ * `createAuthStore` owns its `create()` call, which means it owns the whole store: a product cannot
24
+ * wrap it in `persist`, cannot add an action, and cannot add a field. Both adopters that met it had
25
+ * a superset and neither could use it. One holds a remember-me choice, a profile row and the
26
+ * sign-in methods themselves, under `persist`. The other adds `isAnonymous`, which is the reason
27
+ * this takes a `derive` function rather than just letting the caller spread extra keys in:
28
+ * `isAnonymous` is read off the user, so it has to be rewritten by `setUser` and `clear`, which are
29
+ * exactly the two writes the product does not own.
30
30
  *
31
31
  * The caller keeps `create`, so middleware, extra actions and the store's own name stay theirs:
32
32
  *
@@ -0,0 +1,80 @@
1
+ export interface SlashCommand {
2
+ /** One token, without the leading slash. */
3
+ name: string;
4
+ description: string;
5
+ /** Other executable names. Tags only help search. */
6
+ aliases: readonly string[];
7
+ tags: readonly string[];
8
+ takesArg?: boolean;
9
+ }
10
+ export interface ParsedCommand {
11
+ name: string;
12
+ /** Undefined before a space; empty after a space with no argument yet. */
13
+ arg?: string;
14
+ }
15
+ /** Multiline text and a slash inside a message are not commands. */
16
+ export declare function parseCommand(text: string): ParsedCommand | null;
17
+ export type CommandResolution<T extends SlashCommand = SlashCommand> = {
18
+ kind: "matched";
19
+ command: T;
20
+ } | {
21
+ kind: "unknown";
22
+ name: string;
23
+ } | {
24
+ kind: "ambiguous";
25
+ name: string;
26
+ commands: readonly T[];
27
+ };
28
+ /** Exact names win over aliases. Search tags never execute an action. */
29
+ export declare function resolveCommand<T extends SlashCommand>(commands: readonly T[], name: string): CommandResolution<T>;
30
+ /** One row per name, in registry order when two results have the same rank. */
31
+ export declare function matchCommands<T extends SlashCommand>(commands: readonly T[], query: string): T[];
32
+ /** Replace only the name, keeping the argument already typed. */
33
+ export declare function completeCommand(command: SlashCommand, text: string): string;
34
+ export type CommandProblem<T extends SlashCommand = SlashCommand> = Exclude<CommandResolution<T>, {
35
+ kind: "matched";
36
+ }>;
37
+ export interface CommandOptions<T extends SlashCommand> {
38
+ text: string;
39
+ /** Supply only commands allowed here, with current translated descriptions and tags. */
40
+ commands: readonly T[];
41
+ onChange: (text: string) => void;
42
+ /** The app owns authorization, errors, draft cleanup and attachments. */
43
+ onExecute: (command: T, arg: string | undefined, altKey: boolean) => void;
44
+ /** Required: show a way back to the available commands, never send this text to the model. */
45
+ onError: (problem: CommandProblem<T>) => void;
46
+ }
47
+ /** Structural so React events and small test events both fit. */
48
+ export interface CommandKeyEvent {
49
+ key: string;
50
+ shiftKey?: boolean;
51
+ altKey?: boolean;
52
+ ctrlKey?: boolean;
53
+ metaKey?: boolean;
54
+ isComposing?: boolean;
55
+ keyCode?: number;
56
+ nativeEvent?: {
57
+ isComposing?: boolean;
58
+ keyCode?: number;
59
+ };
60
+ preventDefault: () => void;
61
+ }
62
+ export interface SlashCommandMenu<T extends SlashCommand = SlashCommand> {
63
+ parsed: ParsedCommand | null;
64
+ open: boolean;
65
+ items: readonly T[];
66
+ index: number;
67
+ active: T | undefined;
68
+ listId: string;
69
+ itemId: (command: T) => string;
70
+ activeId: string | undefined;
71
+ highlight: (index: number) => void;
72
+ close: () => void;
73
+ showAll: () => void;
74
+ pick: (command: T, altKey?: boolean) => void;
75
+ submit: (altKey?: boolean) => boolean;
76
+ handleKeyDown: (event: CommandKeyEvent) => boolean;
77
+ }
78
+ /** A controlled textarea menu. No UI, browser globals, timers or attachment state. */
79
+ export declare function useSlashCommands<T extends SlashCommand>({ text, commands, onChange, onExecute, onError, }: CommandOptions<T>): SlashCommandMenu<T>;
80
+ //# sourceMappingURL=commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,192 @@
1
+ import { useId, useState } from "react";
2
+ /** Multiline text and a slash inside a message are not commands. */
3
+ export function parseCommand(text) {
4
+ if (/[\r\n\u{2028}\u{2029}]/u.test(text))
5
+ return null;
6
+ const match = /^\s*\/(\S*)(?:[^\S\r\n]+(.*))?$/u.exec(text);
7
+ if (!match)
8
+ return null;
9
+ const name = (match[1] ?? "").toLowerCase();
10
+ return match[2] === undefined ? { name } : { name, arg: match[2] };
11
+ }
12
+ /** Exact names win over aliases. Search tags never execute an action. */
13
+ export function resolveCommand(commands, name) {
14
+ const token = name.toLowerCase();
15
+ if (!token)
16
+ return { kind: "unknown", name };
17
+ let matches = commands.filter((command) => command.name.toLowerCase() === token);
18
+ if (matches.length === 0) {
19
+ matches = commands.filter((command) => command.aliases.some((alias) => alias.toLowerCase() === token));
20
+ }
21
+ if (matches.length > 1)
22
+ return { kind: "ambiguous", name, commands: matches };
23
+ const command = matches[0];
24
+ return command ? { kind: "matched", command } : { kind: "unknown", name };
25
+ }
26
+ function searchText(text) {
27
+ return text.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
28
+ }
29
+ function rank(command, query) {
30
+ if (!query)
31
+ return 0;
32
+ const name = searchText(command.name);
33
+ const aliases = command.aliases.map(searchText);
34
+ if (name === query)
35
+ return 0;
36
+ if (aliases.includes(query))
37
+ return 1;
38
+ if (name.startsWith(query))
39
+ return 2;
40
+ if (aliases.some((alias) => alias.startsWith(query)))
41
+ return 3;
42
+ if (command.tags.some((tag) => searchText(tag).includes(query)))
43
+ return 4;
44
+ if (searchText(command.description).includes(query))
45
+ return 5;
46
+ return Infinity;
47
+ }
48
+ /** One row per name, in registry order when two results have the same rank. */
49
+ export function matchCommands(commands, query) {
50
+ const token = searchText(query.trim());
51
+ const names = new Set();
52
+ return commands
53
+ .filter((command) => {
54
+ const name = command.name.toLowerCase();
55
+ if (names.has(name))
56
+ return false;
57
+ names.add(name);
58
+ return true;
59
+ })
60
+ .map((command) => ({ command, rank: rank(command, token) }))
61
+ .filter((match) => Number.isFinite(match.rank))
62
+ .sort((a, b) => a.rank - b.rank)
63
+ .map((match) => match.command);
64
+ }
65
+ /** Replace only the name, keeping the argument already typed. */
66
+ export function completeCommand(command, text) {
67
+ const arg = parseCommand(text)?.arg;
68
+ if (arg !== undefined)
69
+ return `/${command.name} ${arg}`;
70
+ return `/${command.name}${command.takesArg ? " " : ""}`;
71
+ }
72
+ /** A controlled textarea menu. No UI, browser globals, timers or attachment state. */
73
+ export function useSlashCommands({ text, commands, onChange, onExecute, onError, }) {
74
+ const parsed = parseCommand(text);
75
+ const resolution = parsed ? resolveCommand(commands, parsed.name) : null;
76
+ const editingName = parsed !== null && (parsed.arg === undefined || resolution?.kind !== "matched");
77
+ const items = editingName ? matchCommands(commands, parsed.name) : [];
78
+ const key = JSON.stringify([text, items.map((command) => command.name)]);
79
+ const [state, setState] = useState({ key, text, index: 0, deliberate: false, dismissed: false });
80
+ // Reset selection on a changed result set, but keep Escape's dismissal until the draft changes.
81
+ if (state.key !== key) {
82
+ setState({
83
+ key,
84
+ text,
85
+ index: 0,
86
+ deliberate: false,
87
+ dismissed: state.text === text && state.dismissed,
88
+ });
89
+ }
90
+ const listId = `${useId()}-commands`;
91
+ const open = editingName && !state.dismissed;
92
+ const active = open ? items[state.index] : undefined;
93
+ const itemId = (command) => `${listId}-${encodeURIComponent(command.name)}`;
94
+ function close() {
95
+ setState((current) => ({ ...current, dismissed: true }));
96
+ }
97
+ function showAll() {
98
+ setState((current) => ({ ...current, index: 0, deliberate: false, dismissed: false }));
99
+ onChange("/");
100
+ }
101
+ function highlight(index) {
102
+ if (!items[index])
103
+ return;
104
+ setState((current) => ({ ...current, index, deliberate: true }));
105
+ }
106
+ function pick(command, altKey = false) {
107
+ // Re-resolve against current capabilities, not a descriptor retained by a caller.
108
+ const selected = resolveCommand(commands, command.name);
109
+ if (selected.kind !== "matched") {
110
+ onError(selected);
111
+ return;
112
+ }
113
+ if (selected.command.takesArg || parsed?.arg) {
114
+ onChange(completeCommand(selected.command, text));
115
+ return;
116
+ }
117
+ onExecute(selected.command, parsed?.arg, altKey);
118
+ }
119
+ /** Call before the normal send/queue/quota path, including button and imperative submits. */
120
+ function submit(altKey = false) {
121
+ if (!parsed || !resolution)
122
+ return false;
123
+ if (resolution.kind !== "matched") {
124
+ onError(resolution);
125
+ return true;
126
+ }
127
+ if (resolution.command.takesArg &&
128
+ parsed.arg === undefined &&
129
+ parsed.name !== resolution.command.name.toLowerCase()) {
130
+ onChange(completeCommand(resolution.command, text));
131
+ return true;
132
+ }
133
+ onExecute(resolution.command, parsed.arg, altKey);
134
+ return true;
135
+ }
136
+ /** Call first. True means skip your own key handler; IME still gets the browser's default. */
137
+ function handleKeyDown(event) {
138
+ const native = event.nativeEvent ?? event;
139
+ if (native.isComposing || native.keyCode === 229)
140
+ return true;
141
+ if (event.shiftKey)
142
+ return false;
143
+ if (open && event.key === "Escape") {
144
+ event.preventDefault();
145
+ close();
146
+ return true;
147
+ }
148
+ if (open && items.length > 0 && !event.ctrlKey && !event.metaKey) {
149
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
150
+ event.preventDefault();
151
+ const step = event.key === "ArrowDown" ? 1 : -1;
152
+ highlight((state.index + step + items.length) % items.length);
153
+ return true;
154
+ }
155
+ if (active && (event.key === "Enter" || event.key === "Tab")) {
156
+ event.preventDefault();
157
+ if (event.key === "Tab") {
158
+ onChange(completeCommand(active, text));
159
+ }
160
+ else if (resolution?.kind === "ambiguous" && !state.deliberate) {
161
+ onError(resolution);
162
+ }
163
+ else {
164
+ pick(active, event.altKey);
165
+ }
166
+ return true;
167
+ }
168
+ }
169
+ if (parsed && event.key === "Enter") {
170
+ event.preventDefault();
171
+ return submit(event.altKey);
172
+ }
173
+ return false;
174
+ }
175
+ return {
176
+ parsed,
177
+ open,
178
+ items,
179
+ index: state.index,
180
+ active,
181
+ listId,
182
+ itemId,
183
+ activeId: active ? itemId(active) : undefined,
184
+ highlight,
185
+ close,
186
+ showAll,
187
+ pick,
188
+ submit,
189
+ handleKeyDown,
190
+ };
191
+ }
192
+ //# sourceMappingURL=commands.js.map