@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
package/README.md CHANGED
@@ -11,7 +11,7 @@ bun add @gusnips/react
11
11
  const user = await api.get<User>("/me");
12
12
  ```
13
13
 
14
- That is the whole call. `user` is a `User` — not a `Response`, not `res.json()`, not a null
14
+ That is the whole call. `user` is a `User`: not a `Response`, not `res.json()`, not a null
15
15
  check. The client attached the token, unwrapped the envelope, and threw a typed `ApiError`
16
16
  carrying the code and the request id if the server refused.
17
17
 
@@ -30,18 +30,18 @@ const api = createApiClient({
30
30
  ## The client knows three things people learn the hard way
31
31
 
32
32
  **Six queries firing at once send one refresh, not six.** The auth server rotates the refresh
33
- token when you use it, so the losers of that race each invalidate the winner — and the person is
33
+ token when you use it, so the losers of that race each invalidate the winner, and the person is
34
34
  signed out in the middle of a load that was working. Three separate codebases arrived at
35
35
  single-flight refresh independently. This is that.
36
36
 
37
37
  **Only auth ANSWERING is a "no".** Dropping a packet tells you nothing about whether a session is
38
- good, and neither does a 500 — that is auth failing, not auth answering. So `refresh()` answers
38
+ good, and neither does a 500. That is auth failing, not auth answering. So `refresh()` answers
39
39
  `{ token, reachedAuth }`, and only a real refusal signs anyone out. Before that split, a Wi-Fi
40
40
  blip logged people out mid-load; without the second half, one bad minute at the auth server signs
41
41
  out everybody whose token happened to need refreshing.
42
42
 
43
43
  **A sign-out has a fail-safe timer.** Awaiting `signOut()` before redirecting covers a rejection,
44
- not a hang — and a hang leaves someone signed out in name only: every request 401ing, nothing
44
+ not a hang, and a hang leaves someone signed out in name only: every request 401ing, nothing
45
45
  left that could redirect it.
46
46
 
47
47
  ### Supabase sessions
@@ -65,14 +65,14 @@ import { isAuthOutage } from "@gusnips/react/supabase";
65
65
 
66
66
  // A callback that just failed to trade its one-use link.
67
67
  if (isAuthOutage(error))
68
- showRetry(); // the link was never spent — the same one still works
68
+ showRetry(); // the link was never spent: the same one still works
69
69
  else showLinkSpent(); // auth said no: only a new link helps
70
70
  ```
71
71
 
72
- `isAuthOutage` is true when auth FAILED rather than answered — a request that never landed, or a
72
+ `isAuthOutage` is true when auth FAILED rather than answered: a request that never landed, or a
73
73
  5xx. Only a real refusal is an answer, and only an answer may end a session or a link. Use it
74
- anywhere that decision is made; the sentence you show is a separate question, and a narrower one
75
- — "check your connection" is true of a request that never landed and false of a 502, which is
74
+ anywhere that decision is made; the sentence you show is a separate question, and a narrower one:
75
+ "check your connection" is true of a request that never landed and false of a 502, which is
76
76
  ours. auth-js marks "nothing came back", and only that, with status 0.
77
77
 
78
78
  It is safe to hand it the `error` from a `{ data, error }` result without checking it first:
@@ -120,7 +120,7 @@ Supabase email links have two complete patterns. Keep either one, never half of
120
120
  - A link carrying `ConfirmationURL` needs `detectSessionInUrl` enabled. That reader consumes an
121
121
  implicit `#access_token` or a PKCE `?code`; it does not consume `token_hash`.
122
122
 
123
- Either pattern can carry a destination — the `token_hash` one has to be asked. GoTrue hands every
123
+ Either pattern can carry a destination: the `token_hash` one has to be asked. GoTrue hands every
124
124
  template a `{{ .RedirectTo }}`: the `redirectTo` the app passed, already checked against the
125
125
  allowlist, falling back to the Site URL when it was absent or not on it. A `{{ .ConfirmationURL }}`
126
126
  link carries it for you. A template that writes its own landing drops it unless it asks:
@@ -130,7 +130,7 @@ link carries it for you. A template that writes its own landing drops it unless
130
130
  ```
131
131
 
132
132
  Leave that parameter off and `redirectTo` and `emailRedirectTo` do nothing on every e-mail flow,
133
- however carefully the app sets them — four of the codebases this came from pass one that the
133
+ however carefully the app sets them: four of the codebases this came from pass one that the
134
134
  template throws away, so a person bounced off a protected page signs in and lands on the front
135
135
  door. Nothing reports it: the app's call is correct, the link works, and the destination was
136
136
  dropped in an HTML file hosted somewhere else.
@@ -147,7 +147,7 @@ Validate it on arrival even though GoTrue already did: that check is against a l
147
147
  so it says the link may come back here, not which page it may open.
148
148
 
149
149
  Recovery and invite links continue to the new-password screen after the session opens. A callback
150
- must also tell a used or expired link apart from a request that never landed — or one that landed
150
+ must also tell a used or expired link apart from a request that never landed, or one that landed
151
151
  and came back 5xx: the first needs a new link, and the other two can retry the same one, because
152
152
  the hash was never spent.
153
153
 
@@ -165,7 +165,7 @@ implicit-flow tokens; in a browser the client has already taken it, so wait for
165
165
  React Native hand `credential` to `exchangeCodeForSession` or `setSession`. `error` is a failure
166
166
  GoTrue wrote into the address. `invalid` is a link that was cut short or edited.
167
167
 
168
- Pass the fragment too — `parseAuthCallback(location.search, location.hash)`. GoTrue writes a
168
+ Pass the fragment too: `parseAuthCallback(location.search, location.hash)`. GoTrue writes a
169
169
  failure into the fragment every time and into the query only sometimes: after an implicit-flow
170
170
  email link it is in the fragment alone, so a query-only reader shows nothing to somebody whose
171
171
  link expired.
@@ -197,7 +197,7 @@ new QueryClient({
197
197
  });
198
198
  ```
199
199
 
200
- Retries 408, 425, a transient 429, 5xx, and no-response-at-all. Nothing else — every other 4xx is
200
+ Retries 408, 425, a transient 429, 5xx, and no-response-at-all. Nothing else: every other 4xx is
201
201
  an answer, and a second attempt just says it again. The rule itself is
202
202
  [`@gusnips/http/retry`](../http#should-this-request-be-tried-again), so an SDK with no React in it
203
203
  can retry the same way.
@@ -210,7 +210,7 @@ those codes and they are never retried.
210
210
  You only need that list where your API stays quiet. If it answers `Retry-After: 2`, the wait is
211
211
  sat out; past `maxRetryWaitSecs` (10 seconds by default) the wait is the answer and the screen
212
212
  says so instead of holding a spinner. And if it sends `details.retryAfterSecs: null`, it is
213
- saying waiting will never fix this — a cap that frees only when somebody deletes something, a
213
+ saying waiting will never fix this: a cap that frees only when somebody deletes something, a
214
214
  slot that frees when another job ends. That `null` is read as the claim it is, not as a field
215
215
  somebody forgot, so the code never reaches your list.
216
216
 
@@ -244,14 +244,14 @@ offer comes from `describeError(view.error).recover`, below.
244
244
  ## Survives your deploys
245
245
 
246
246
  An app with `lazy()` routes serves chunks by hashed filename. Deploy while someone has a tab
247
- open, and their next click asks for a file that no longer exists — a white screen on a button
247
+ open, and their next click asks for a file that no longer exists: a white screen on a button
248
248
  that worked a minute ago. The same deploy breaks a second way that looks nothing like the first:
249
249
  the old bundle reads a field your API just renamed, and throws a plain `TypeError` with nothing
250
250
  in it about a deploy.
251
251
 
252
252
  **Fix the cause in your host config first.** This package cannot do it for you and it is worth
253
253
  more than everything below. A static host answers a missing `/assets/x.js` with your SPA
254
- fallback — `index.html`, at a 200, as `text/html` — and that is what the browser's "MIME type
254
+ fallback (`index.html`, at a 200, as `text/html`), and that is what the browser's "MIME type
255
255
  text/html" module-script refusal actually is. Hosts then match cache rules against the _request
256
256
  path_ rather than the outcome, so the `immutable, max-age=31536000` rule you wrote for hashed
257
257
  assets lands on that HTML body: one poisoned asset URL, at the edge and in every browser that
@@ -264,12 +264,12 @@ Then two detectors, for the two failures:
264
264
  if (await isStaleBuild()) reloadOnce();
265
265
  ```
266
266
 
267
- `isStaleBuild` asks the server instead of reading the error — it compares the entry script this
267
+ `isStaleBuild` asks the server instead of reading the error: it compares the entry script this
268
268
  document loaded against the one the live page names. That is the only thing that catches the
269
269
  renamed-field crash, where the message carries no signal at all. It asks for the page, not for
270
270
  the asset: a CDN serves assets with a long `s-maxage`, so a bundle retired an hour ago still
271
271
  answers 200 from the edge and would report "current" during exactly the window when skew is most
272
- likely. Anything ambiguous — offline, a non-200, a timeout — answers `false`, because dressing a
272
+ likely. Anything ambiguous (offline, a non-200, a timeout) answers `false`, because dressing a
273
273
  real bug up as an update hides it from you too.
274
274
 
275
275
  ```ts
@@ -277,12 +277,12 @@ if (isChunkLoadError(error) && reloadOnce()) return null;
277
277
  ```
278
278
 
279
279
  `isChunkLoadError` matches the message, which is all you have for a failure that never reaches a
280
- boundary — an `unhandledrejection`, or a listener. Every browser's phrasing is in there.
280
+ boundary: an `unhandledrejection`, or a listener. Every browser's phrasing is in there.
281
281
 
282
282
  `reloadOnce` reloads at most once a minute, and there is **one** stamp for every reason to
283
283
  reload. A tab can reload because a chunk 404'd, because the server says it is behind, or from a
284
- guard inlined in `index.html` that runs before any module does. Three detectors, one predicament
285
- — give each its own key and a broken deploy gets three reloads a minute to take turns with. That
284
+ guard inlined in `index.html` that runs before any module does. Three detectors, one predicament:
285
+ give each its own key and a broken deploy gets three reloads a minute to take turns with. That
286
286
  inline guard cannot import anything, so the key it has to copy is exported as `RELOAD_GUARD_KEY`.
287
287
 
288
288
  And in your browser entry:
@@ -294,7 +294,7 @@ installPreloadErrorHandler();
294
294
  ```
295
295
 
296
296
  It is narrower than it looks: it swallows Vite's CSS _preload hint_ failure and nothing else. A
297
- hint that fails is harmless — the import runs straight after and usually works. Taking Vite's
297
+ hint that fails is harmless: the import runs straight after and usually works. Taking Vite's
298
298
  `preventDefault()` for every preload error instead resolves the dynamic import with `undefined`,
299
299
  `React.lazy` reads `.default` off nothing, and you get a crash screen plus a TypeError with only
300
300
  React frames in it, naming no chunk.
@@ -315,7 +315,7 @@ const RequireStaff = requireProfile(
315
315
 
316
316
  `onDenied` and `onError` are separate because `!me?.isStaff` reads a 500 as "not staff". An
317
317
  operator arriving while `/auth/me` is down gets told the page does not exist: the wrong cause, no
318
- retry, and no request id to quote. A guard has three answers — wait, fail, refuse — and only a
318
+ retry, and no request id to quote. A guard has three answers (wait, fail, refuse) and only a
319
319
  real `false` reaches the refusal.
320
320
 
321
321
  `createRequireAuth` sends an anonymous visitor to `?next=`, carrying path, query and hash. A hash
@@ -333,7 +333,7 @@ const next = safeInternalPath(new URLSearchParams(location.search).get("next"))
333
333
  `createRequireAnonymous` already performs that check and falls back to the home path you gave it.
334
334
 
335
335
  Writing a return target down is the other direction, and it has its own hazard. A Supabase client
336
- with no `flowType` is on the **implicit** flow — the default — so a callback comes back as
336
+ with no `flowType` is on the **implicit** flow (the default), so a callback comes back as
337
337
  `#access_token=…&refresh_token=…`. A guard renders exactly when there is no session yet, which is
338
338
  that callback's own state while the client parses the fragment: point an OAuth `redirectTo` at a
339
339
  guarded route and a concatenated `pathname + search + hash` puts a refresh token into a query
@@ -347,8 +347,40 @@ const returnTo = returnPathFromLocation(window.location);
347
347
  ```
348
348
 
349
349
  It keeps the page and drops only a fragment carrying a credential, because the page is the part
350
- worth returning to. `createRequireAuth` uses it; use it anywhere else you record where someone was
351
- — a dead-session redirect is the common one.
350
+ worth returning to. `createRequireAuth` uses it; use it anywhere else you record where someone was:
351
+ a dead-session redirect is the common one.
352
+
353
+ ## Keep the email between sign-in screens
354
+
355
+ ```tsx
356
+ import { recallAuthEmail, rememberAuthEmail } from "@gusnips/react";
357
+
358
+ <input
359
+ name="email"
360
+ type="email"
361
+ defaultValue={recallAuthEmail()}
362
+ onChange={(event) => rememberAuthEmail(event.currentTarget.value)}
363
+ />;
364
+ ```
365
+
366
+ Sign in, "use a code instead", forgot password and create account are separate screens, and each
367
+ one used to open with an empty email field. Put those two props on every one of them and the
368
+ address follows the person. Call `forgetAuthEmail()` where your app learns a session exists, so the
369
+ next person at this browser starts empty.
370
+
371
+ It is kept in `localStorage` for 30 minutes after the last keystroke, and never in a URL, where it
372
+ would reach `Referer`, access logs, history and analytics. It is `localStorage` and not
373
+ `sessionStorage` because an emailed link opens in a new tab, and a new tab does not inherit
374
+ `sessionStorage`. That tab is where a person lands on sign-in after confirming an account. Where
375
+ the browser blocks site data, or on React Native, the address lasts in memory until the page or the
376
+ app is left.
377
+
378
+ - Read it once, when the screen opens. It is not a store: nothing re-renders when it changes. On a
379
+ prerendered page, read it after hydration, never while rendering.
380
+ - Leave the field uncontrolled. A `defaultValue` and a change handler are enough. Also call
381
+ `rememberAuthEmail(value)` where you read the field at submit, because some password managers fill
382
+ a form without firing events.
383
+ - Never put a password, a code or a token here.
352
384
 
353
385
  ## Never dead-end anyone
354
386
 
@@ -356,13 +388,13 @@ worth returning to. `createRequireAuth` uses it; use it anywhere else you record
356
388
  import type { ErrorStateProps } from "@gusnips/react";
357
389
  ```
358
390
 
359
- No component ships — seven codebases have an `ErrorState.tsx` and they overlap 9–37%, because one
391
+ No component ships: seven codebases have an `ErrorState.tsx` and they overlap 9–37%, because one
360
392
  draws a tinted icon, one an illustration, one a mascot. What they share is the prop shape, and
361
393
  the prop shape _is_ the rule: `problem`, `cause`, `fix`, and `action`. **`fix` and `action` are
362
394
  required.** A required prop is the only version of "always offer a way out" that a caller in a
363
395
  hurry cannot skip.
364
396
 
365
- `createErrorDescriber` turns a thrown `ApiError` into the `cause` and `fix` to put in it — plus
397
+ `createErrorDescriber` turns a thrown `ApiError` into the `cause` and `fix` to put in it, plus
366
398
  which control to offer, and the request id to print under it:
367
399
 
368
400
  ```ts
@@ -393,7 +425,7 @@ the catalog has, the reader gets it in their own language.
393
425
  The field is `fix`, the same word as the prop above. It was `hint` until 0.6.0, which left every
394
426
  adopter writing `fix={hint}` at every error surface.
395
427
 
396
- `recover` is derived from `shouldRetry` — the same rule react-query retries on — so the button a
428
+ `recover` is derived from `shouldRetry` (the same rule react-query retries on), so the button a
397
429
  reader sees and the retry that actually happens cannot disagree. Give it the same
398
430
  `durableLimitCodes` list you give `queryDefaults`, and a spent quota offers no button instead of
399
431
  one that cannot work. The words follow it too: "try again in a moment" appears only on a 5xx that
@@ -420,7 +452,7 @@ fallback: ({ says, wait }) => ({
420
452
  });
421
453
  ```
422
454
 
423
- Some APIs write `message` for whoever reads the screen. Some write it for whoever reads the log —
455
+ Some APIs write `message` for whoever reads the screen. Some write it for whoever reads the log,
424
456
  in English, in an app that ships three languages. Your arm also gets the stated wait, which the
425
457
  default has nowhere to put.
426
458
 
@@ -497,6 +529,110 @@ toast needs no `catch`. Outside a component, `copyText(text)` does the write on
497
529
  The words are yours, and so is the live region: a label that changes needs `aria-live` to be
498
530
  heard.
499
531
 
532
+ ## Slash commands in a text field
533
+
534
+ A slash command is text like `/help` that picks an action instead of sending a message.
535
+
536
+ ```ts
537
+ import { parseCommand } from "@gusnips/react/commands";
538
+
539
+ parseCommand("/help");
540
+ // { name: "help" }
541
+ ```
542
+
543
+ Names have no leading slash. Give each command other names (`aliases`) and search words (`tags`):
544
+
545
+ ```ts
546
+ const commands = [
547
+ {
548
+ name: "help",
549
+ description: "See commands",
550
+ aliases: ["commands", "shortcuts"],
551
+ tags: ["keyboard", "guide"],
552
+ },
553
+ ];
554
+ ```
555
+
556
+ `/commands` runs `/help`. Searching for `keyboard` finds its menu row, but `/keyboard` is not an
557
+ executable name. `matchCommands(commands, query)` searches names, aliases, tags and descriptions,
558
+ ignoring case and accents. Exact names rank first, then aliases, name prefixes, alias prefixes,
559
+ tags and descriptions. Equal ranks keep registry order; each name gets one row.
560
+
561
+ `resolveCommand(commands, name)` answers `matched`, `unknown` or `ambiguous`. An exact name wins
562
+ over an alias. Two commands sharing an alias are ambiguous, not a reason to run the first one.
563
+
564
+ For a controlled text field, use the hook:
565
+
566
+ ```tsx
567
+ import { useSlashCommands } from "@gusnips/react/commands";
568
+
569
+ const slash = useSlashCommands({
570
+ text,
571
+ commands,
572
+ onChange: setText,
573
+ onExecute: runCommand,
574
+ onError: showCommandError,
575
+ });
576
+
577
+ function submit() {
578
+ if (slash.submit()) return;
579
+ sendMessage(text);
580
+ }
581
+ ```
582
+
583
+ The functions in that example belong to your app. `runCommand(command, arg, altKey)` gets the
584
+ original command object, the argument and whether Alt was held. It owns permissions and action
585
+ errors. Supply only commands allowed in this field, with descriptions and tags in the current
586
+ language. Changing those commands or the language refreshes the menu even if `text` stays the same.
587
+
588
+ Call `slash.handleKeyDown(event)` first in the field's key handler. If it returns `true`, stop
589
+ your own handler. Arrows move the selection; Enter picks it. Tab completes the name without
590
+ running it. Escape hides the menu until the query changes. Shift+Enter stays yours. During IME
591
+ composition (when a keyboard combines keystrokes into a character), the hook lets the browser
592
+ finish that character without running or sending anything.
593
+
594
+ Render the rows in your own menu. Keep focus in the field with the browser's `preventDefault`
595
+ on mouse down; use click for the action so keyboard and screen-reader clicks work too:
596
+
597
+ ```tsx
598
+ return slash.open ? (
599
+ <div id={slash.listId} role="listbox" aria-label="Commands">
600
+ {slash.items.map((command, index) => (
601
+ <button
602
+ key={command.name}
603
+ id={slash.itemId(command)}
604
+ type="button"
605
+ role="option"
606
+ tabIndex={-1}
607
+ aria-selected={slash.index === index}
608
+ onMouseEnter={() => slash.highlight(index)}
609
+ onMouseDown={(event) => event.preventDefault()}
610
+ onClick={(event) => slash.pick(command, event.altKey)}
611
+ >
612
+ /{command.name}: {command.description}
613
+ </button>
614
+ ))}
615
+ </div>
616
+ ) : null;
617
+ ```
618
+
619
+ Give the field `role="combobox"`, `aria-autocomplete="list"`, `aria-expanded={slash.open}`,
620
+ `aria-controls={slash.open ? slash.listId : undefined}` and `aria-activedescendant={slash.activeId}`.
621
+ Translate the menu label too. If there are no rows, explain how to find commands and offer a
622
+ "See commands" button that calls `slash.showAll()`. It opens the full list, even after Escape.
623
+ `onError` is required: show what was not recognized, or which names share an alias, with the
624
+ same way back to the list.
625
+
626
+ Set `takesArg: true` when a command accepts an argument. Selecting it, or submitting its bare
627
+ alias, completes `/name ` without running it. Completion keeps arguments already typed.
628
+ `parseCommand` leaves multiline text as a normal message.
629
+
630
+ Call `submit()` before every normal send, queue or quota check, including button and imperative
631
+ paths. It returns `true` for commands it ran, completed or rejected, so unknown commands never
632
+ silently become model messages. It returns `false` for ordinary text. The hook never sends a
633
+ message, clears a draft or changes attached files; those stay with the app. It also has no timer
634
+ or scheduler: offer a repeat command only when your app has real scheduling behind it.
635
+
500
636
  ## Also here
501
637
 
502
638
  `ErrorBoundary`, an SSE reader split into a platform-free parser and a stream wrapper,
@@ -511,13 +647,13 @@ Detection walks the browser's language list in the reader's order, exact tag fir
511
647
  subtag: `[pt-PT, en]` gets `pt-BR` when that is what you ship, and `[en-GB, pt-BR]` gets `en`.
512
648
  `localeGateScript` uses the same rule, so the gate and the app agree about a reader.
513
649
 
514
- `createAuthStore` is at `@gusnips/react/store` — a zustand store whose `isLoading` starts `false`
650
+ `createAuthStore` is at `@gusnips/react/store`: a zustand store whose `isLoading` starts `false`
515
651
  where there is no window. A session bootstrap can only be in flight in a browser, and `true`
516
652
  during a build is a wait that never ends: it once shipped a spinner as the indexable body of a
517
653
  page whose whole job was to be found.
518
654
 
519
655
  When your store needs more than those flags, `authSlice` in the same place gives you them as a
520
- plain object to spread, so `create` stays yours — and with it `persist`, your own actions, and
656
+ plain object to spread, so `create` stays yours, and with it `persist`, your own actions, and
521
657
  fields of your own:
522
658
 
523
659
  ```ts
@@ -537,7 +673,7 @@ writes you no longer own.
537
673
  ## On a phone
538
674
 
539
675
  The main entry works in React Native. It imports `react` and nothing else you would have to go
540
- find — no `react-dom`, no router, no store — so the fetch client, the retry rule and the SSE
676
+ find (no `react-dom`, no router, no store), so the fetch client, the retry rule and the SSE
541
677
  parser all come along, and the four things that need a browser stay behind the subpaths above.
542
678
  `useCopy` imports fine there but has no `navigator.clipboard` to write to, so every copy answers
543
679
  `"failed"`: use the platform's clipboard on a phone.
@@ -555,8 +691,8 @@ no wrapper added scroll lock, focus trap, ESC, outside-dismiss, focus return, ro
555
691
  typeahead. Base UI already does all of that, and a wrapper "adding" them is a second
556
692
  implementation of something that works. Twenty were dropped.
557
693
 
558
- What the remaining seven buy is composition you cannot skip — no caller can ship a scrimless
559
- dialog — required a11y props expressed as types, and a handful of facts that each cost somebody
694
+ What the remaining seven buy is composition you cannot skip (no caller can ship a scrimless
695
+ dialog), required a11y props expressed as types, and a handful of facts that each cost somebody
560
696
  an afternoon. z-index goes on the Viewport, not the Popup, because `position: fixed` makes a
561
697
  stacking context. A drawer _ties_ with dialog rather than beating it, or a modal opened from
562
698
  inside a drawer never paints. A wrapper earns its place by knowing something, not by styling
@@ -577,9 +713,10 @@ lives behind a subpath, so you install a dependency only if you import the thing
577
713
  | `@gusnips/react/ui` | the seven wrappers | @base-ui/react |
578
714
  | `@gusnips/react/contract` | two prerender names | nothing |
579
715
  | `@gusnips/react/theme` | `createTheme`, `startTheme` | react |
716
+ | `@gusnips/react/commands` | slash matching and `useSlashCommands` | react |
580
717
 
581
718
  The rule behind that table: **a peer marked optional must not be reachable from the main entry
582
- point.** An optional peer the barrel imports anyway is not optional — it is a required one whose
719
+ point.** An optional peer the barrel imports anyway is not optional: it is a required one whose
583
720
  error moved from install time to your first build, which is the worse of the two places to learn
584
721
  about it.
585
722
 
@@ -3,10 +3,10 @@ import { ApiError } from "./api-error.ts";
3
3
  /**
4
4
  * The app's one door to the API. Nothing else should call `fetch`.
5
5
  *
6
- * Every route answers the same envelope, so unwrapping and error-shaping belong here once
7
- * rather than in every hook. Three donors built this independently and all three arrived at
8
- * single-flight token refresh — see {@link SessionAdapter.refresh} for the reason, which is
9
- * the sharpest thing in this file.
6
+ * Every route answers the same envelope, so unwrapping and error-shaping belong here once rather
7
+ * than in every hook. Three donors built this independently and all three arrived at single-flight
8
+ * token refresh. See {@link SessionAdapter.refresh} for the reason, which is the sharpest thing in
9
+ * this file.
10
10
  *
11
11
  * Four things deliberately did NOT come across from the donors, because they belong to a
12
12
  * product and not to a client:
@@ -14,7 +14,7 @@ import { ApiError } from "./api-error.ts";
14
14
  * - **Toasts.** Whether a failure is spoken aloud is a design decision that differs per surface.
15
15
  * Throw, and let the caller decide.
16
16
  * - **In-flight GET deduplication.** react-query already dedups by key, so the client's copy is
17
- * redundant — and one donor's version returned a `clone()` of a body already being consumed,
17
+ * redundant. One donor's version also returned a `clone()` of a body already being consumed,
18
18
  * which is a race with no symptom until it has one.
19
19
  * - **Service-status tracking and impersonation.** One donor's, and firmly its own.
20
20
  * - **Analytics correlation headers.** Injected through {@link ApiClientOptions.headers}.
@@ -23,8 +23,8 @@ import { ApiError } from "./api-error.ts";
23
23
  export interface RefreshResult {
24
24
  token: string | null;
25
25
  /**
26
- * False when the refresh never got an ANSWER out of the auth server — a dropped connection,
27
- * a 5xx, a captive portal.
26
+ * False when the refresh never got an ANSWER out of the auth server because of a dropped
27
+ * connection, a 5xx or a captive portal.
28
28
  *
29
29
  * This flag is invariant 3, and only one of three donors had it. Losing a packet says nothing
30
30
  * about whether a session is still good, so a client that collapses "auth said no" into
@@ -35,8 +35,8 @@ export interface RefreshResult {
35
35
  reachedAuth: boolean;
36
36
  }
37
37
  /**
38
- * The seam onto whatever holds the session. Supabase in every donor so far, but nothing here
39
- * knows that — an adapter is four lines and keeps the auth vendor out of this package.
38
+ * The seam onto whatever holds the session. Supabase in every donor so far, but nothing here knows
39
+ * that. An adapter is four lines and keeps the auth vendor out of this package.
40
40
  */
41
41
  export interface SessionAdapter {
42
42
  getToken(): Promise<string | null>;
@@ -44,9 +44,9 @@ export interface SessionAdapter {
44
44
  signOut(): Promise<void>;
45
45
  }
46
46
  /**
47
- * How long anything here waits on a sign-out: the default for
48
- * {@link ApiClientOptions.signOutTimeoutMs}, and the deadline of `signOutEvenOffline` in
49
- * `@gusnips/react/supabase`. Shared so the two give up together — a redirect that gave up sooner
47
+ * How long anything here waits on a sign-out: the default for {@link
48
+ * ApiClientOptions.signOutTimeoutMs}, and the deadline of `signOutEvenOffline` in
49
+ * `@gusnips/react/supabase`. Shared so the two give up together. A redirect that gave up sooner
50
50
  * would navigate away while the session was still in storage.
51
51
  */
52
52
  export declare const SIGN_OUT_TIMEOUT_MS = 3000;
@@ -64,54 +64,54 @@ export interface ApiClientOptions {
64
64
  */
65
65
  headers?: () => Record<string, string>;
66
66
  /**
67
- * The session is gone and cannot be renewed — send them to sign in.
67
+ * The session is gone and cannot be renewed. Send them to sign in.
68
68
  *
69
69
  * The client does not know the route, and it deliberately still THROWS after calling this:
70
70
  * `location.replace()` does not stop the current frame, so a caller's own `onError` must
71
71
  * still run or a half-finished screen keeps rendering against data that will never arrive.
72
72
  * The error it throws carries `expected: true`.
73
73
  *
74
- * Ending the session IN PLACE is equally supported — routing to the sign-in screen without a
75
- * reload — and it is the case that keeps this client alive across the next sign-in. The
76
- * sign-out latch is keyed on the refused token for exactly that reason; see `refusedToken`.
74
+ * Routing to the sign-in screen without a reload is equally supported. It is the case that keeps
75
+ * this client alive across the next sign-in. The sign-out latch is keyed on the refused token for
76
+ * exactly that reason; see `refusedToken`.
77
77
  */
78
78
  onSessionDead: () => void;
79
79
  /**
80
80
  * Every failed response, seen once, just before it is thrown.
81
81
  *
82
- * For a reaction that belongs to the whole app rather than to one call site. The donor's case
83
- * is the sharp one: a mid-session account suspension 403s every authed route except
84
- * `GET /auth/me`, so the moment one arrives the app has to refresh `me` and route to the
85
- * screen that explains it — otherwise every query on the page fails at once and the shell
86
- * half-renders behind an error storm until `me` goes stale on its own.
82
+ * For a reaction that belongs to the whole app rather than to one call site. The donor's case is
83
+ * the sharp one: a mid-session account suspension 403s every authed route except `GET /auth/me`,
84
+ * so the moment one arrives the app has to refresh `me` and route to the screen that explains it.
85
+ * Otherwise every query on the page fails at once and the shell half-renders behind an error
86
+ * storm until `me` goes stale on its own.
87
87
  *
88
- * It cannot live at a call site, because the point is that it fires from whichever call
89
- * happened to be first. It went in the donor's client directly, which made the client import
90
- * its query cache and its query keys — a cycle that this hook removes.
88
+ * It cannot live at a call site, because the point is that it fires from whichever call happened
89
+ * to be first. It went in the donor's client directly, which made the client import its query
90
+ * cache and its query keys, creating a cycle that this hook removes.
91
91
  *
92
92
  * Observation only: the error is thrown either way, and throwing from here would replace a
93
93
  * real API failure with whatever the listener hit.
94
94
  *
95
95
  * **The response comes with it, because a refusal's headers are otherwise unreachable.**
96
- * `request` throws before the `Response` escapes, so an adopter whose API states something on
97
- * the error path had no way to read it at all — one reports what every call cost, refusals
98
- * included, which is how "a blocked page costs nothing" becomes a thing a customer can check
99
- * rather than a thing we assert. That is the distinction worth keeping: field-error flattening
100
- * and a per-call meter callback both stayed in their products, because a product can write
101
- * those itself. This one it cannot, at any price, and a capability the kit makes unreachable
102
- * is a regression the kit has to undo.
96
+ * `request` throws before the `Response` escapes, so an adopter whose API states something on the
97
+ * error path had no way to read it at all. One reports what every call cost, refusals included,
98
+ * which is how "a blocked page costs nothing" becomes a thing a customer can check rather than a
99
+ * thing we assert. That is the distinction worth keeping: field-error flattening and a per-call
100
+ * meter callback both stayed in their products, because a product can write those itself. This
101
+ * one it cannot, at any price, and a capability the kit makes unreachable is a regression the kit
102
+ * has to undo.
103
103
  *
104
- * The BODY has already been read by the time this runs — the envelope was needed to build the
105
- * error — so `response.json()` here throws. The headers are what is left, and what this is for.
104
+ * The BODY has already been read by the time this runs to build the error from the envelope, so
105
+ * `response.json()` here throws. The headers are what is left, and what this is for.
106
106
  */
107
107
  onError?: (error: ApiError, response: Response) => void;
108
108
  /**
109
109
  * Abort a request that has not answered. Default 30s.
110
110
  *
111
- * Neither donor bounded its authenticated requests at all — both bounded only their keyless
112
- * clients — so a stalled connection was a spinner with no end. providerkit learned the same
113
- * lesson about streams: a request with no deadline is a bug that only shows up on a bad
114
- * network, which is exactly when nobody can reproduce it.
111
+ * Neither donor bounded its authenticated requests. Both bounded only their keyless clients, so a
112
+ * stalled connection was a spinner with no end. providerkit learned the same lesson about
113
+ * streams: a request with no deadline is a bug that only shows up on a bad network, which is
114
+ * exactly when nobody can reproduce it.
115
115
  */
116
116
  timeoutMs?: number;
117
117
  /** How many times to refresh-and-retry a 401. Default 2. */
@@ -121,9 +121,9 @@ export interface ApiClientOptions {
121
121
  /**
122
122
  * How long to wait for `signOut()` before redirecting anyway. Default 3s.
123
123
  *
124
- * A fail-safe, and one donor added it after the failure it prevents: awaiting `signOut()`
125
- * covers a rejection but not a HANG, and a hang leaves the tab signed out in name only —
126
- * still on the page, every request 401ing, nothing left that could redirect it.
124
+ * A fail-safe, and one donor added it after the failure it prevents: awaiting `signOut()` covers
125
+ * a rejection but not a HANG, and a hang leaves the tab signed out in name only: still on the
126
+ * page, every request 401ing, nothing left that could redirect it.
127
127
  */
128
128
  signOutTimeoutMs?: number;
129
129
  /** Header carrying the server's request id, echoed onto {@link ApiError.requestId}. */
@@ -143,9 +143,9 @@ export interface ApiClient {
143
143
  put<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>;
144
144
  patch<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>;
145
145
  /**
146
- * DELETE, discarding whatever comes back. Returns `void` because 204 is the usual answer and
147
- * a 204 has NO BODY — `res.json()` on one throws `Unexpected end of JSON input`, which reads
148
- * like a malformed response and is really just a success nobody was allowed to parse.
146
+ * DELETE, discarding whatever comes back. Returns `void` because 204 is the usual answer and a
147
+ * 204 has NO BODY. `res.json()` on one throws `Unexpected end of JSON input`, which reads like a
148
+ * malformed response and is really just a success nobody was allowed to parse.
149
149
  *
150
150
  * Split from {@link ApiClient.delJson} rather than guessing per response, because the guess is
151
151
  * the part that hides a bug: a route that was supposed to answer with data and returned
@@ -153,21 +153,21 @@ export interface ApiClient {
153
153
  * layers away. The donor that hit this had written exactly these two functions.
154
154
  */
155
155
  del(path: string, options?: RequestOptions): Promise<void>;
156
- /** The DELETE that answers with something worth reading — a queue entry it handed back. */
156
+ /** The DELETE that answers with something worth reading, such as a queue entry it handed back. */
157
157
  delJson<T>(path: string, options?: RequestOptions): Promise<T>;
158
158
  /**
159
- * The whole envelope rather than just `data`, for a route whose `meta` is half the answer —
160
- * a list's `total`, or a metered call's receipt.
159
+ * The whole envelope rather than just `data`, for a route whose `meta` is half the answer, such
160
+ * as a list's `total` or a metered call's receipt.
161
161
  */
162
162
  page<T, M>(path: string, options?: RequestOptions): Promise<ApiSuccess<T, M>>;
163
163
  /**
164
164
  * The same, off a POST.
165
165
  *
166
- * Two adopters needed this and both wrote the same workaround: `client.request(…)` followed by
167
- * a hand-rolled `res.json()`, stepping around the client's own envelope reader and its one
168
- * assertion about the success shape. One of them left the reason sitting in a comment — "the
169
- * whole envelope is reachable off a GET and not off a POST, so this one reads the body itself"
170
- * — which is this package describing its own gap in somebody else's file.
166
+ * Two adopters needed this and both wrote the same workaround: `client.request(…)` followed by a
167
+ * hand-rolled `res.json()`, stepping around the client's own envelope reader and its one
168
+ * assertion about the success shape. One of them left the reason sitting in a comment: "the whole
169
+ * envelope is reachable off a GET and not off a POST, so this one reads the body itself". This
170
+ * package's gap was described in somebody else's file.
171
171
  *
172
172
  * A metered route takes a body whenever its input is too long or too punctuated to be a path
173
173
  * segment (a URL, a search query), and then the receipt has to survive the POST too. When the
@@ -1 +1 @@
1
- {"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"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"}
@@ -2,9 +2,9 @@ import { isApiError } from "@gusnips/http";
2
2
  import { parseRetryAfter } from "@gusnips/http/retry";
3
3
  import { ApiError } from "./api-error.js";
4
4
  /**
5
- * How long anything here waits on a sign-out: the default for
6
- * {@link ApiClientOptions.signOutTimeoutMs}, and the deadline of `signOutEvenOffline` in
7
- * `@gusnips/react/supabase`. Shared so the two give up together — a redirect that gave up sooner
5
+ * How long anything here waits on a sign-out: the default for {@link
6
+ * ApiClientOptions.signOutTimeoutMs}, and the deadline of `signOutEvenOffline` in
7
+ * `@gusnips/react/supabase`. Shared so the two give up together. A redirect that gave up sooner
8
8
  * would navigate away while the session was still in storage.
9
9
  */
10
10
  export const SIGN_OUT_TIMEOUT_MS = 3_000;