@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
@@ -6,21 +6,21 @@ import { shouldRetry } from "./query.ts";
6
6
  * Turning anything thrown into copy a person can act on.
7
7
  *
8
8
  * The API already answers with a message written for a developer, a stable `messageKey` for the
9
- * domain refusals, and `details` carrying the parts that make a refusal actionable. This is the
10
- * one place that decides what the SCREEN says — which is not always the same sentence.
11
- * "Monthly quota spent, upgrade or buy a pack" is right in a JSON body and wrong under a button
12
- * that could just say what to do next.
9
+ * domain refusals, and `details` carrying the parts that make a refusal actionable. This is the one
10
+ * place that decides what the SCREEN says. It is not always the same sentence. "Monthly quota
11
+ * spent, upgrade or buy a pack" is right in a JSON body and wrong under a button that could just
12
+ * say what to do next.
13
13
  *
14
14
  * `cause` is what happened. `fix` is what to do about it. **A surface that shows only the first
15
- * half is the dead end this exists to prevent** — which is why the type has two fields and not
16
- * one, and why `fix` being absent should be a deliberate choice at each call site rather than
17
- * the default nobody noticed.
15
+ * half is the dead end this exists to prevent**. That is why the type has two fields and not one,
16
+ * and why `fix` being absent should be a deliberate choice at each call site rather than the
17
+ * default nobody noticed.
18
18
  *
19
- * `fix`, and not the `hint` this returned until 0.6.0. `ErrorStateProps` — the contract this same
20
- * package ships for the component that renders this — has always called it `fix`, so every
21
- * adopter wrote `fix={hint}` at every error surface: a rename adapter, which is the shape of an
22
- * API we got wrong. It also clears a collision, because in at least one adopter a form field's
23
- * `hint` is the requirement text under the input, which is a different thing entirely.
19
+ * `fix`, and not the `hint` this returned until 0.6.0. `ErrorStateProps`, the contract this same
20
+ * package ships for the component that renders this, has always called it `fix`, so every adopter
21
+ * wrote `fix={hint}` at every error surface: a rename adapter, which is the shape of an API we got
22
+ * wrong. It also clears a collision, because in at least one adopter a form field's `hint` is the
23
+ * requirement text under the input, which is a different thing entirely.
24
24
  *
25
25
  * The per-code switch stays in the product: two donors' switches shared their SHAPE and almost
26
26
  * no arms, because the arms are that API's vocabulary. What ships is the scaffolding around it,
@@ -30,11 +30,11 @@ import { shouldRetry } from "./query.ts";
30
30
  /**
31
31
  * Which control to offer beside the words.
32
32
  *
33
- * The fourth adopter had this and the package did not, which left every adopter re-deriving
34
- * "can a second attempt fix this" from its own switch — beside a `shouldRetry` already deciding
35
- * exactly that for react-query. Two answers to one question drift, and the drift is visible: a
36
- * screen offering "try again" for a refusal the query layer refuses to retry, so the button does
37
- * nothing and the reader presses it twice.
33
+ * The fourth adopter had this and the package did not, which left every adopter re-deriving "can a
34
+ * second attempt fix this" from its own switch, beside a `shouldRetry` already deciding exactly
35
+ * that for react-query. Two answers to one question drift, and the drift is visible: a screen
36
+ * offering "try again" for a refusal the query layer refuses to retry, so the button does nothing
37
+ * and the reader presses it twice.
38
38
  *
39
39
  * So this is DERIVED from `shouldRetry`, not switched on separately. One rule, two consumers.
40
40
  */
@@ -54,7 +54,7 @@ export interface ErrorCopy {
54
54
  cause: string;
55
55
  /** What to do about it. Its absence should be a decision, not an oversight. */
56
56
  fix?: string;
57
- /** Override the derived kind — a 429 whose code means a spent quota is `"none"`, not `"wait"`. */
57
+ /** Override the derived kind. A 429 whose code means a spent quota is `"none"`, not `"wait"`. */
58
58
  recover?: RecoveryKind;
59
59
  /** Override the request id. Rarely wanted; the envelope's is used by default. */
60
60
  reference?: string;
@@ -66,14 +66,14 @@ export interface DescribedError extends ErrorCopy {
66
66
  }
67
67
 
68
68
  /**
69
- * The subset of i18next's `t` this module needs — typed here so `i18next` stays optional.
69
+ * The subset of i18next's `t` this module needs, typed here so `i18next` stays optional.
70
70
  *
71
- * `Key` is a parameter and not plain `string` for a reason the first migration found. Every app
72
- * on this stack declares `CustomTypeOptions.resources`, which types `t` to accept ONLY the keys
73
- * its catalog has — so a `t` is not assignable to anything asking for `(key: string) => string`,
74
- * and the adopter is left with a cast. Naming the keys instead turns that around: each function
75
- * below asks for exactly the keys it looks up, a typed `t` accepts a superset of them and goes
76
- * in directly, and a catalog missing one is a compile error rather than a raw key on screen.
71
+ * `Key` is a parameter and not plain `string` for a reason the first migration found. Every app on
72
+ * this stack declares `CustomTypeOptions.resources`, which types `t` to accept ONLY the keys its
73
+ * catalog has, so a `t` is not assignable to anything asking for `(key: string) => string`, and the
74
+ * adopter is left with a cast. Naming the keys instead turns that around: each function below asks
75
+ * for exactly the keys it looks up, a typed `t` accepts a superset of them and goes in directly,
76
+ * and a catalog missing one is a compile error rather than a raw key on screen.
77
77
  */
78
78
  export type Translate<Key extends string = string> = (
79
79
  key: Key,
@@ -87,8 +87,8 @@ type WaitName = "waitSeconds" | "waitMinutes" | "waitHours";
87
87
  * Every name {@link createErrorDescriber} looks up itself. Your catalog carries all of them.
88
88
  *
89
89
  * The three wait names are deliberately NOT here, which is the other half of 0.6.0. This module
90
- * used to humanize a stated wait itself, so `waitSeconds`/`waitMinutes`/`waitHours` — each with
91
- * an ICU plural — landed in the key union of every adopter's `t` whether or not a wait was ever
90
+ * used to humanize a stated wait itself, so `waitSeconds`/`waitMinutes`/`waitHours`, each with an
91
+ * ICU plural, landed in the key union of every adopter's `t` whether or not a wait was ever
92
92
  * rendered. An app that formats waits with `Intl.RelativeTimeFormat` needs none of them and was
93
93
  * left adding six entries per language to satisfy a compiler. `formatWait` owns that now.
94
94
  */
@@ -99,11 +99,11 @@ type CopyName = "network" | "networkHint" | "unexpected" | "retrySoon";
99
99
  *
100
100
  * i18next keeps `seatLimitReached_one` and `seatLimitReached_other`; the server sends
101
101
  * `serverErrors.seatLimitReached`, because that is the key a person resolves with a `count`. So
102
- * `Object.keys` on a catalog does NOT contain the name the server actually sends, and a gate
103
- * built from it alone rejects a sentence the app is carrying — falling back to the server's
104
- * English for the one refusal that had a written answer. Measured in the seventh migration: two
105
- * of that API's 112 keys, both of them plan limits, both of them the exact screen where a
106
- * specific sentence earns its keep.
102
+ * `Object.keys` on a catalog does NOT contain the name the server actually sends, and a gate built
103
+ * from it alone rejects a sentence the app is carrying, falling back to the server's English for
104
+ * the one refusal that had a written answer. Measured in the seventh migration: two of that API's
105
+ * 112 keys, both of them plan limits, both of them the exact screen where a specific sentence earns
106
+ * its keep.
107
107
  */
108
108
  type PluralSuffix = "zero" | "one" | "two" | "few" | "many" | "other";
109
109
  type PluralBase<N extends string> = N extends `${infer Base}_${PluralSuffix}` ? Base : N;
@@ -135,17 +135,17 @@ type _TypedCatalogNeedsNoCast = Satisfied<
135
135
  >;
136
136
 
137
137
  /**
138
- * "in 4 minutes" / "in 2 hours" — a wait nobody has to convert from seconds.
138
+ * Format a wait as "in 4 minutes" or "in 2 hours", without making the reader convert seconds.
139
139
  *
140
140
  * The thresholds are deliberately not round: 90 seconds rather than 60, so "in 75 seconds" does
141
141
  * not become the less precise "in 1 minute", and 90 minutes rather than 60 for the same reason
142
142
  * one rung up. Needs `waitSeconds` / `waitMinutes` / `waitHours` under `prefix`, each with a
143
143
  * `count` plural.
144
144
  *
145
- * This is what you pass as `formatWait` when your catalog carries those three:
146
- * `formatWait: (secs) => humanizeWait(t, secs, "errors.")`. An app that formats a wait with
147
- * `Intl.RelativeTimeFormat` passes its own one-liner instead and needs no wait keys at all —
148
- * which also gets it correct plurals in languages with more than two forms, for free.
145
+ * This is what you pass as `formatWait` when your catalog carries those three: `formatWait: (secs)
146
+ * => humanizeWait(t, secs, "errors.")`. An app that formats a wait with `Intl.RelativeTimeFormat`
147
+ * passes its own one-liner instead and needs no wait keys at all. It also gets correct plurals in
148
+ * languages with more than two forms, for free.
149
149
  */
150
150
  export function humanizeWait<Prefix extends string>(
151
151
  t: Translate<`${Prefix}${WaitName}`>,
@@ -181,17 +181,17 @@ interface DescriberRules {
181
181
  * How a stated wait becomes words, for the `wait` an arm reads off its context.
182
182
  *
183
183
  * Required rather than defaulted, because there is no answer that is right for everyone and a
184
- * default hid the cost: {@link humanizeWait} reads three ICU plural keys out of your catalog,
185
- * and an app that already formats relative time with `Intl` would have had to add them to
186
- * compile. Say which you want — `(secs) => humanizeWait(t, secs, "errors.")`, or your own
187
- * `(secs) => formatIn(secs, locale)` — and the catalog you need is the one you can see.
184
+ * default hid the cost: {@link humanizeWait} reads three ICU plural keys out of your catalog, and
185
+ * an app that already formats relative time with `Intl` would have had to add them to compile.
186
+ * Pass `(secs) => humanizeWait(t, secs, "errors.")` or your own `(secs) => formatIn(secs,
187
+ * locale)`. The choice makes the catalog you need visible.
188
188
  */
189
189
  formatWait: (secs: number) => string;
190
190
  /**
191
191
  * Per-code copy. Everything not listed falls through to the default arm.
192
192
  *
193
- * The arms look up the app's own keys, so they close over the app's `t` rather than being
194
- * handed one — this module has no names for that copy and no business typing it.
193
+ * The arms look up the app's own keys, so they close over the app's `t` rather than being handed
194
+ * one. This module has no names for that copy and no business typing it.
195
195
  *
196
196
  * Annotate your object `Partial<Record<YourErrorCode, ErrorArm>>` where you write it, and a
197
197
  * code that does not exist is a compile error at the arm rather than a branch that never runs.
@@ -201,21 +201,21 @@ interface DescriberRules {
201
201
  * What to say when nothing in `codes` matches: a code this build has no arm for, or an answer
202
202
  * with no envelope at all.
203
203
  *
204
- * The built-in arm hands the reader the server's own `message`, and for one API in the fleet
205
- * that is right — its message IS the sentence a person should read. For another it is exactly
204
+ * The built-in arm hands the reader the server's own `message`, and for one API in the fleet that
205
+ * is right, because its message IS the sentence a person should read. For another it is exactly
206
206
  * wrong: that repo's own i18n rules say `message` is "the English fallback for logs", and it
207
207
  * ships three languages, so the built-in would put developer English in front of a reader who
208
- * does not speak it. One default cannot be right for both, and the one baked in here was
209
- * simply the first adopter's convention, mistaken for a fact.
208
+ * does not speak it. One default cannot be right for both, and the one baked in here was simply
209
+ * the first adopter's convention, mistaken for a fact.
210
210
  *
211
211
  * An arm here is also handed the stated wait, which the built-in spends on nothing: a refusal
212
212
  * with no arm of its own could state its expiry and have that go unsaid.
213
213
  */
214
214
  fallback?: ErrorArm;
215
215
  /**
216
- * The codes that mean "this limit does not clear by waiting" — **the same list you hand
217
- * `queryDefaults`**, because it answers the same question and a second copy is a second
218
- * chance to disagree with the retry rule. Declare it once in the app and pass it twice.
216
+ * The codes that mean "this limit does not clear by waiting". Pass **the same list to
217
+ * `queryDefaults`**, because it answers the same question and a second copy is a second chance to
218
+ * disagree with the retry rule. Declare it once in the app and pass it twice.
219
219
  */
220
220
  durableLimitCodes?: readonly string[];
221
221
  /** Longest stated wait still worth a retry rather than a countdown. Matches `queryDefaults`. */
@@ -230,10 +230,10 @@ export interface ErrorDescriberOptions<
230
230
  /**
231
231
  * Checked against the keys, never used to find them: the three type parameters come from
232
232
  * `copyPrefix`, `messageKeyPrefix` and `knownMessageKeys`. With `t` as a source too, the call
233
- * every adopter writes — an inline `(key, params) => i18n.t(key, params)` beside an inline arm —
233
+ * every adopter writes, an inline `(key, params) => i18n.t(key, params)` beside an inline arm,
234
234
  * failed overload resolution (TS2769, measured on TypeScript 5.9.3) once the one-language
235
- * overload existed, although this signature alone accepted it. A typed `t` missing a key is
236
- * still refused.
235
+ * overload existed, although this signature alone accepted it. A typed `t` missing a key is still
236
+ * refused.
237
237
  */
238
238
  t: Translate<
239
239
  NoInfer<`${Prefix}${CopyName}` | `${ServerPrefix}${ServerName | PluralBase<ServerName>}`>
@@ -242,13 +242,13 @@ export interface ErrorDescriberOptions<
242
242
  copyPrefix: Prefix;
243
243
  /**
244
244
  * The server's own catalog namespace, e.g. `"serverErrors."`. A `messageKey` outside it is
245
- * ignored — the server names a sentence, it does not get to name any key in the app.
245
+ * ignored. The server names a sentence, it does not get to name any key in the app.
246
246
  *
247
- * The `code` is read under this prefix too, for an API that has no `messageKey` field and
248
- * lets the code name the sentence — one repo in the fleet has 98 of those, `errors.NOT_FOUND`
249
- * and its siblings, against another repo's `messageKey`. Both are the same claim ("the server
250
- * named a sentence this app carries"), both pass the same gate below, and an app that uses
251
- * neither is unaffected because nothing matches.
247
+ * The `code` is read under this prefix too, for an API that has no `messageKey` field and lets
248
+ * the code name the sentence. One repo in the fleet has 98 of those, `errors.NOT_FOUND` and its
249
+ * siblings, against another repo's `messageKey`. Both are the same claim ("the server named a
250
+ * sentence this app carries"), both pass the same gate below, and an app that uses neither is
251
+ * unaffected because nothing matches.
252
252
  */
253
253
  messageKeyPrefix: ServerPrefix;
254
254
  /**
@@ -256,13 +256,13 @@ export interface ErrorDescriberOptions<
256
256
  * catalog. Passing the catalog itself is both the membership test and the proof the compiler
257
257
  * wants: a name that is in it IS a key, so the lookup below needs no cast.
258
258
  *
259
- * It is required because leaving it out is not a smaller version of this — it is the bug the
260
- * option exists to prevent. i18next answers a key it does not have with the key itself, so a
259
+ * It is required because leaving it out reintroduces the bug the option exists to prevent rather
260
+ * than making this smaller. i18next answers a key it does not have with the key itself, so a
261
261
  * deploy landing ahead of the bundle a tab is still running would render `serverErrors.foo` at
262
262
  * somebody. A name that is absent falls back to the server's English `message` instead.
263
263
  *
264
- * Hand it over as it is. A plural sits in the catalog under suffixed names while the server
265
- * names the base, so the base is admitted too — see {@link PluralBase}.
264
+ * Hand it over as it is. A plural sits in the catalog under suffixed names while the server names
265
+ * the base, so the base is admitted too. See {@link PluralBase}.
266
266
  */
267
267
  knownMessageKeys: Record<ServerName, unknown>;
268
268
  /**
@@ -270,13 +270,13 @@ export interface ErrorDescriberOptions<
270
270
  *
271
271
  * The envelope carries what the SERVER knows, which is not always a word anyone should read: a
272
272
  * role id (`admin`), an action verb (`approve_content`). The sentence needs whatever the app's
273
- * own screens call those things, and the substitution happens here — inside the one lookup that
274
- * reads `params` — so there is nowhere else an adopter could intervene. Without this seam the
275
- * choice is to ship the raw id into `{{role}}`, or to stop using the server's sentence at all
276
- * and re-resolve every key by hand, which is how i18n ended up inside a transport once already.
273
+ * own screens call those things, and the substitution happens here, inside the one lookup that
274
+ * reads `params`. There is nowhere else an adopter could intervene. Without this seam the choice
275
+ * is to ship the raw id into `{{role}}`, or to stop using the server's sentence at all and
276
+ * re-resolve every key by hand, which is how i18n ended up inside a transport once already.
277
277
  *
278
278
  * Optional, and identity by default: an API whose params are already words passes nothing. It
279
- * deliberately takes only the params — an arm that needs the error has `ctx` for that.
279
+ * deliberately takes only the params. An arm that needs the error has `ctx` for that.
280
280
  */
281
281
  localizeParams?: (
282
282
  params: Record<string, string | number> | undefined,
@@ -3,18 +3,18 @@ import { Component, type ErrorInfo, type ReactNode } from "react";
3
3
  /**
4
4
  * The last stop before a white screen.
5
5
  *
6
- * React unmounts the whole tree when a render throws, and an app with no boundary anywhere
7
- * answers that with a blank page: no words, no navigation, no reload — the most complete dead
8
- * end a product can produce, and the one the never-dead-end rule cannot reach, because by then
9
- * there is no component left to render a state from.
6
+ * React unmounts the whole tree when a render throws, and an app with no boundary anywhere answers
7
+ * that with a blank page: no words, no navigation, no reload. That is the most complete dead end a
8
+ * product can produce, and the one the never-dead-end rule cannot reach, because by then there is
9
+ * no component left to render a state from.
10
10
  *
11
11
  * It is deliberately dumb. No retry loop, no error reporting, no reset on a timer: it catches,
12
12
  * it hands the error to a `fallback` the app draws in its own words, and it offers `reset`.
13
13
  * Every donor that baked its own UI in here had to keep a second copy for its second surface;
14
14
  * a render prop has no such problem, and the brand stays in the product where it belongs.
15
15
  *
16
- * A class, because `getDerivedStateFromError` has no hook equivalent — this is the one thing in
17
- * the kit React still has no other way to express.
16
+ * A class, because `getDerivedStateFromError` has no hook equivalent. This is the one thing in the
17
+ * kit React still has no other way to express.
18
18
  */
19
19
 
20
20
  export interface ErrorBoundaryFallbackProps {
@@ -22,9 +22,9 @@ export interface ErrorBoundaryFallbackProps {
22
22
  /** Put the children back. */
23
23
  reset: () => void;
24
24
  /**
25
- * True when the throw was a failed lazy-chunk import — a deploy landed while this tab was
26
- * open, so the fix is a reload and NOT a retry (a rejected dynamic import rethrows on
27
- * re-render forever). Draw the "updating" screen, not the crash screen.
25
+ * True when the throw was a failed lazy-chunk import because a deploy landed while this tab was
26
+ * open, so the fix is a reload and NOT a retry (a rejected dynamic import rethrows on re-render
27
+ * forever). Draw the "updating" screen, not the crash screen.
28
28
  */
29
29
  isChunkError: boolean;
30
30
  }
@@ -33,9 +33,9 @@ export interface ErrorBoundaryProps {
33
33
  /** Drawn instead of the children once something has thrown. */
34
34
  fallback: (props: ErrorBoundaryFallbackProps) => ReactNode;
35
35
  /**
36
- * Changing this value clears the error and remounts the children — pass the route, so
37
- * navigating away from a screen that broke actually leaves it rather than carrying its
38
- * wreckage to every page after it.
36
+ * Changing this value clears the error and remounts the children. Pass the route, so navigating
37
+ * away from a screen that broke actually leaves it rather than carrying its wreckage to every
38
+ * page after it.
39
39
  */
40
40
  resetKey?: string;
41
41
  /** Called once per catch. Report to whatever the app reports to. */
package/src/guards.tsx CHANGED
@@ -6,9 +6,9 @@ import { returnPathFromLocation, safeInternalPath } from "./internal-path.ts";
6
6
  /**
7
7
  * Route guards.
8
8
  *
9
- * Two donors wrote these four, gave two of them the same names, and explained them in nearly
10
- * the same words. What differed was one line — and that line is invariant 4, so it is the
11
- * reason this file exists rather than being copied a ninth time.
9
+ * Two donors wrote these four, gave two of them the same names, and explained them in nearly the
10
+ * same words. What differed was one line. That line is invariant 4, so it is the reason this file
11
+ * exists rather than being copied a ninth time.
12
12
  *
13
13
  * These are factories because the destinations are the app's: `paths.signIn` is not something a
14
14
  * package can know, and one donor hardcoded `/login` in four places for exactly that reason.
@@ -27,18 +27,18 @@ export interface SessionState {
27
27
  /**
28
28
  * The answer to a "who is this?" query, in the three states it actually has.
29
29
  *
30
- * This shape is invariant 4. A guard reading `!me?.isStaff` collapses "pending", "failed" and
31
- * "no" into one branch — so an operator arriving while `/auth/me` is 500ing is told the page
32
- * does not exist. Wrong cause, no retry, and no request id to quote to support. One donor hit
33
- * that and fixed it; the other still has the collapsed version.
30
+ * This shape is invariant 4. A guard reading `!me?.isStaff` collapses "pending", "failed" and "no"
31
+ * into one branch, so an operator arriving while `/auth/me` is 500ing is told the page does not
32
+ * exist. Wrong cause, no retry, and no request id to quote to support. One donor hit that and fixed
33
+ * it; the other still has the collapsed version.
34
34
  *
35
35
  * `"pending"` and not `"loading"` for one measured reason: it is react-query's own word for this
36
36
  * state since v5, and with the words matching a `UseQueryResult<TMe>` satisfies this type
37
- * STRUCTURALLY — `createRequireProfile(useMe, …)` takes the hook directly, no adapter. With
38
- * `"loading"` it does not typecheck and every adopter hand-writes the same six-line mapping;
39
- * eight of the twelve repos this serves are on react-query, and the first migration wrote that
40
- * mapping before anyone noticed. Nothing here imports react-query at runtime — the type stays
41
- * structural, so an app on SWR or a plain `useState` still answers it in three lines.
37
+ * STRUCTURALLY: `createRequireProfile(useMe, …)` takes the hook directly, no adapter. With
38
+ * `"loading"` it does not typecheck and every adopter hand-writes the same six-line mapping; eight
39
+ * of the twelve repos this serves are on react-query, and the first migration wrote that mapping
40
+ * before anyone noticed. Nothing here imports react-query at runtime. The type stays structural, so
41
+ * an app on SWR or a plain `useState` still answers it in three lines.
42
42
  */
43
43
  export type MeQuery<TMe> =
44
44
  { status: "pending" } | { status: "error"; error: unknown } | { status: "success"; data: TMe };
@@ -68,8 +68,8 @@ function signInUrl(signInPath: string, returnTo: string): string {
68
68
  }
69
69
 
70
70
  /**
71
- * Signed in, or off to sign in — remembering where they were headed, so the redirect afterwards
72
- * lands on the page they actually wanted rather than the home screen.
71
+ * Require sign-in and remember where they were headed, so the redirect afterwards lands on the page
72
+ * they actually wanted rather than the home screen.
73
73
  */
74
74
  export function createRequireAuth(
75
75
  useSession: () => SessionState,
@@ -114,7 +114,7 @@ export function createRequireAnonymous(
114
114
  }
115
115
 
116
116
  /**
117
- * A gate on something the profile says — suspended, staff, on a given plan.
117
+ * A gate on a profile field, such as whether the person is suspended, staff or on a given plan.
118
118
  *
119
119
  * `allow` gets the profile and answers yes or no. `onDenied` draws the refusal; `onError` draws
120
120
  * the failure, and they are SEPARATE arguments on purpose. That separation is the whole point
package/src/hydrate.ts CHANGED
@@ -1,18 +1,17 @@
1
1
  /**
2
2
  * The browser half of prerendering: deciding whether to hydrate or to mount fresh.
3
3
  *
4
- * Three separate repos invented all of this independently — same constant name, same string
5
- * value, same decision, and near-identical explaining comments. That is not a coincidence worth
6
- * deduping; it is one lesson learned three times, and it belongs somewhere it can be learned
7
- * once.
4
+ * Three separate repos invented all of this independently, with the same constant name, string
5
+ * value and decision, and near-identical explaining comments. That is not a coincidence worth
6
+ * deduping; it is one lesson learned three times, and it belongs somewhere it can be learned once.
8
7
  *
9
8
  * The constants themselves live in `prerender-contract.ts`, which imports nothing, because the
10
9
  * BUILD needs them too and must not pull `react-dom/client` into a Node process to get a string.
11
10
  *
12
11
  * This module sits behind `@gusnips/react/hydrate` rather than in the barrel for the same reason
13
- * one rung further out: it is the package's ONLY `react-dom` import, and a React Native app has
14
- * no react-dom to give. Leaving it in the barrel made `react-dom` a required peer and shut the
15
- * whole package out of every phone — for one function a phone would never call.
12
+ * one rung further out: it is the package's ONLY `react-dom` import, and a React Native app has no
13
+ * react-dom to give. Leaving it in the barrel made `react-dom` a required peer and shut the whole
14
+ * package out of every phone for one function a phone would never call.
16
15
  */
17
16
  import type { ReactNode } from "react";
18
17
  import { createRoot, hydrateRoot } from "react-dom/client";
@@ -24,16 +23,16 @@ export { PRERENDERED_ROUTE_ATTR, SHELL_ROUTE } from "./prerender-contract.ts";
24
23
  * Hydrate the render this file IS, and mount fresh over anything else.
25
24
  *
26
25
  * "Does the root have children" is the wrong question, and getting it wrong is silent. A static
27
- * host answers every address it does not publish with the nearest `404.html` — and that file has
28
- * the not-found page rendered INTO it — so an address served from a shell arrives at a full root
29
- * holding somebody else's markup. Hydrating that is React reconciling two different pages: it
30
- * recovers by throwing the whole tree away and says so in the console, which is a page that
31
- * works and a bug nobody sees. The file names the route it is a render of, so we can simply ask.
26
+ * host answers every address it does not publish with the nearest `404.html`, which has the
27
+ * not-found page rendered INTO it. An address served from a shell arrives at a full root holding
28
+ * somebody else's markup. Hydrating that is React reconciling two different pages: it recovers by
29
+ * throwing the whole tree away and says so in the console, which is a page that works and a bug
30
+ * nobody sees. The file names the route it is a render of, so we can simply ask.
32
31
  *
33
- * `route` is passed in rather than read from `window.location` on purpose. A localized app
34
- * serves `/pt/pricing` from a file rendered for `/pricing`, with the locale carried in the
35
- * router's `basename` — so the address and the route are different strings, and only the caller
36
- * knows which one the build wrote.
32
+ * `route` is passed in rather than read from `window.location` on purpose. A localized app serves
33
+ * `/pt/pricing` from a file rendered for `/pricing`, with the locale carried in the router's
34
+ * `basename`, so the address and the route are different strings, and only the caller knows which
35
+ * one the build wrote.
37
36
  *
38
37
  * **A trailing slash is not a different page.** The router renders `/precos/` and `/precos` alike,
39
38
  * and which one is in the address bar is the host's call: a host serving `precos/index.html`
package/src/i18n.ts CHANGED
@@ -8,8 +8,7 @@
8
8
  * ```
9
9
  *
10
10
  * Keeping the options here is what stops a console, a storefront and a docs site in the same
11
- * product from drifting apart on the details below — each of which is a bug somebody already
12
- * shipped.
11
+ * product from drifting apart on the details below. Each is a bug somebody already shipped.
13
12
  */
14
13
 
15
14
  export interface I18nInitOptions {
@@ -28,10 +27,10 @@ export interface I18nInitOptions {
28
27
  /**
29
28
  * The query parameter that hands a language ACROSS an origin, e.g. `"lang"`.
30
29
  *
31
- * When set it is detected FIRST, ahead of storage and the browser — it is the only one of the
30
+ * When set it is detected FIRST, ahead of storage and the browser. It is the only one of the
32
31
  * three that somebody chose on purpose and just now. A reader who picked Português on the
33
- * storefront and followed a link into the app means it more than their laptop's locale does.
34
- * It is cached on arrival, so it decides once and no URL inside the app has to carry it.
32
+ * storefront and followed a link into the app means it more than their laptop's locale does. It
33
+ * is cached on arrival, so it decides once and no URL inside the app has to carry it.
35
34
  *
36
35
  * Only one donor had this. The other has the same split-origin layout and the same problem.
37
36
  */
@@ -39,15 +38,15 @@ export interface I18nInitOptions {
39
38
  /**
40
39
  * Who WRITES {@link storageKey}. The detector, by default.
41
40
  *
42
- * Pass `"app"` when your own language control writes it. That is not a preference — it is what
43
- * a "follow the browser" option costs. Going back to following the browser means clearing the
44
- * key and calling `changeLanguage(undefined)`, which re-runs detection; a detector that caches
45
- * then writes the language it just detected straight back into the key it was told to clear.
46
- * The choice re-pins itself and nothing can reach that state again.
41
+ * Pass `"app"` when your own language control writes it. A "follow the browser" option needs
42
+ * this. Going back to following the browser means clearing the key and calling
43
+ * `changeLanguage(undefined)`, which re-runs detection; a detector that caches then writes the
44
+ * language it just detected straight back into the key it was told to clear. The choice re-pins
45
+ * itself and nothing can reach that state again.
47
46
  *
48
47
  * Three repos on this stack, three answers: one lets the detector write, one writes it from its
49
- * own control, and one does both — harmless there only because it offers no "follow the
50
- * browser" state. So the rule, rather than the default: one writer per key.
48
+ * own control, and one does both. Doing both is harmless there only because it offers no "follow
49
+ * the browser" state. So the rule, rather than the default: one writer per key.
51
50
  */
52
51
  storageWriter?: "detector" | "app";
53
52
  }
@@ -106,13 +105,13 @@ export function i18nInitOptions({
106
105
  * i18next ever sees it.
107
106
  *
108
107
  * Done here rather than through interpolation because i18next v26 has no global interpolation
109
- * defaults, and these strings are often resolved by dynamic key — so a wrapping `t()` would not
108
+ * defaults, and these strings are often resolved by dynamic key, so a wrapping `t()` would not
110
109
  * reach them. The point is that a rename or a domain move is ONE edit in the brand constants
111
110
  * instead of a sweep across every locale file.
112
111
  *
113
- * Returns a fresh tree; the input is untouched. The placeholder names come from `vars`, so
114
- * adding one needs no change here — two donors hardcoded their own list in the regex and both
115
- * had to remember to update it.
112
+ * Returns a fresh tree; the input is untouched. The placeholder names come from `vars`, so adding
113
+ * one needs no change here. Two donors hardcoded their own list in the regex and both had to
114
+ * remember to update it.
116
115
  */
117
116
  export function applyBrandVars<T>(resources: T, vars: Record<string, string>): T {
118
117
  const names = Object.keys(vars);
package/src/index.ts CHANGED
@@ -34,6 +34,8 @@ export { cn } from "./cn.ts";
34
34
  export { copyText, useCopy, type CopyOptions, type CopyState } from "./copy.ts";
35
35
  export { returnPathFromLocation, safeInternalPath, type PathParts } from "./internal-path.ts";
36
36
 
37
+ export { forgetAuthEmail, recallAuthEmail, rememberAuthEmail } from "./auth-email.ts";
38
+
37
39
  export { ApiError, isAbortError } from "./api-error.ts";
38
40
  export { retryAfterSecs } from "@gusnips/http/retry";
39
41
  export {
@@ -12,9 +12,9 @@ function containsControl(value: string): boolean {
12
12
  * A route the browser can follow without leaving the current origin.
13
13
  *
14
14
  * Return targets cross a trust boundary: query strings, history state and storage are all writable
15
- * by somebody outside the app. `startsWith("/")` is not enough — URL parsers read both `//host`
16
- * and `/\\host` as another origin, and strip tabs or newlines before making the same decision.
17
- * Parsing against a fixed origin applies the browser's own rules rather than trying to copy them.
15
+ * by somebody outside the app. `startsWith("/")` is not enough: URL parsers read both `//host` and
16
+ * `/\\host` as another origin, and strip tabs or newlines before making the same decision. Parsing
17
+ * against a fixed origin applies the browser's own rules rather than trying to copy them.
18
18
  */
19
19
  export function safeInternalPath(value: unknown): string | null {
20
20
  if (typeof value !== "string" || !value.startsWith("/") || containsControl(value)) return null;
@@ -55,14 +55,15 @@ export interface PathParts {
55
55
  *
56
56
  * The fragment is the half that is easy to get wrong, and getting it wrong leaks a credential. A
57
57
  * Supabase client with no `flowType` uses the IMPLICIT flow, which returns the session in the
58
- * fragment — `#access_token=…&refresh_token=…`. A return target is recorded in a query string, and
59
- * a query string is not a fragment: it rides in `Referer`, it is written to every access log on the
58
+ * fragment, `#access_token=…&refresh_token=…`. A return target is recorded in a query string, and a
59
+ * query string is not a fragment: it rides in `Referer`, it is written to every access log on the
60
60
  * way, and it stays in history. So a fragment carrying a credential is dropped and the page is
61
61
  * kept, because the page is the part worth returning to and the anchor is the part that is
62
62
  * dangerous.
63
63
  *
64
- * Total on purpose — the caller is handing over its OWN address, which is on this origin by
65
- * construction. Use `safeInternalPath` for the other direction, where someone else wrote the value.
64
+ * Accepts every caller address on purpose. The caller is handing over its OWN address, which is on
65
+ * this origin by construction. Use `safeInternalPath` for the other direction, where someone else
66
+ * wrote the value.
66
67
  */
67
68
  export function returnPathFromLocation({ pathname, search, hash }: PathParts): string {
68
69
  const fragment = hash.startsWith("#") ? hash.slice(1) : hash;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The two constants the build and the browser both need — and **nothing else in this file**.
2
+ * The two constants the build and the browser both need. Keep **nothing else in this file**.
3
3
  *
4
4
  * It is separate from `hydrate.ts` for one reason: that module imports `react-dom/client`, and
5
5
  * `@gusnips/vite` needs these constants to WRITE the attribute at build time. If the constants
@@ -12,8 +12,8 @@
12
12
  */
13
13
 
14
14
  /**
15
- * The attribute a prerendered file names its own route in — written by the build, read by the
16
- * browser entry.
15
+ * The attribute where a prerendered file names its own route. The build writes it and the browser
16
+ * entry reads it.
17
17
  *
18
18
  * One constant because the writer and the reader are in different builds, and a typo between
19
19
  * them would show up only as a silent full re-render: a page that works, and a bug nobody sees.
@@ -23,9 +23,9 @@ export const PRERENDERED_ROUTE_ATTR = "data-prerendered-route";
23
23
  /**
24
24
  * What a 404 shell writes instead of a route.
25
25
  *
26
- * It is the pattern that actually matched — the catch-all — and it can never equal an address,
27
- * which is the property that matters: every route a static host answers from the shell mounts
28
- * fresh rather than hydrating the not-found page over itself. A real 404 pays one redundant
29
- * client render for that, and keeps the markup a crawler reads.
26
+ * It is the catch-all pattern that actually matched, and it can never equal an address, which is
27
+ * the property that matters: every route a static host answers from the shell mounts fresh rather
28
+ * than hydrating the not-found page over itself. A real 404 pays one redundant client render for
29
+ * that, and keeps the markup a crawler reads.
30
30
  */
31
31
  export const SHELL_ROUTE = "*";