@gusnips/react 0.9.23 → 0.9.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -36
- package/dist/api-client.d.ts +51 -51
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +3 -3
- package/dist/api-error.d.ts +11 -11
- package/dist/api-error.js +11 -11
- package/dist/auth-email.d.ts +40 -0
- package/dist/auth-email.d.ts.map +1 -0
- package/dist/auth-email.js +106 -0
- package/dist/auth-email.js.map +1 -0
- package/dist/auth-store.d.ts +23 -23
- package/dist/auth-store.js +13 -13
- package/dist/copy.d.ts +3 -3
- package/dist/copy.js +3 -3
- package/dist/deploy-recovery.d.ts +40 -40
- package/dist/deploy-recovery.js +40 -40
- package/dist/describe-error.d.ts +68 -68
- package/dist/describe-error.d.ts.map +1 -1
- package/dist/describe-error.js +5 -5
- package/dist/error-boundary.d.ts +12 -12
- package/dist/guards.d.ts +15 -15
- package/dist/guards.js +3 -3
- package/dist/hydrate.d.ts +15 -16
- package/dist/hydrate.d.ts.map +1 -1
- package/dist/hydrate.js +9 -9
- package/dist/hydrate.js.map +1 -1
- package/dist/i18n.d.ts +15 -16
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +5 -6
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal-path.d.ts +8 -7
- package/dist/internal-path.d.ts.map +1 -1
- package/dist/internal-path.js +12 -9
- package/dist/internal-path.js.map +1 -1
- package/dist/prerender-contract.d.ts +7 -7
- package/dist/prerender-contract.js +7 -7
- package/dist/query.d.ts +16 -17
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +2 -2
- package/dist/query.js.map +1 -1
- package/dist/sse.d.ts +19 -20
- package/dist/sse.d.ts.map +1 -1
- package/dist/sse.js +18 -19
- package/dist/sse.js.map +1 -1
- package/dist/states.d.ts +12 -12
- package/dist/states.d.ts.map +1 -1
- package/dist/supabase-session.d.ts +47 -48
- package/dist/supabase-session.d.ts.map +1 -1
- package/dist/supabase-session.js +35 -35
- package/dist/supabase-session.js.map +1 -1
- package/dist/theme.d.ts +4 -4
- package/dist/theme.js +4 -4
- package/dist/ui/class-name.d.ts +3 -4
- package/dist/ui/class-name.d.ts.map +1 -1
- package/dist/ui/class-name.js +3 -4
- package/dist/ui/class-name.js.map +1 -1
- package/dist/ui/combobox.d.ts +11 -11
- package/dist/ui/combobox.d.ts.map +1 -1
- package/dist/ui/dialog.d.ts +5 -5
- package/dist/ui/dialog.js +2 -2
- package/dist/ui/drawer.d.ts +10 -10
- package/dist/ui/drawer.d.ts.map +1 -1
- package/dist/ui/drawer.js +2 -2
- package/dist/ui/input.d.ts +2 -2
- package/dist/ui/input.js +2 -2
- package/dist/ui/menu.d.ts +8 -8
- package/dist/ui/menu.d.ts.map +1 -1
- package/dist/ui/menu.js +4 -4
- package/dist/ui/menu.js.map +1 -1
- package/dist/ui/select.d.ts +12 -12
- package/dist/ui/select.d.ts.map +1 -1
- package/dist/ui/select.js +3 -3
- package/dist/ui/tabs.d.ts +14 -15
- package/dist/ui/tabs.d.ts.map +1 -1
- package/dist/ui/tabs.js +13 -14
- package/dist/ui/tabs.js.map +1 -1
- package/package.json +1 -1
- package/src/api-client.ts +51 -51
- package/src/api-error.ts +11 -11
- package/src/auth-email.test.ts +145 -0
- package/src/auth-email.ts +112 -0
- package/src/auth-store.ts +23 -23
- package/src/copy.ts +3 -3
- package/src/deploy-recovery.ts +40 -40
- package/src/describe-error.ts +68 -68
- package/src/error-boundary.tsx +12 -12
- package/src/guards.tsx +15 -15
- package/src/hydrate.ts +15 -16
- package/src/i18n.ts +15 -16
- package/src/index.ts +2 -0
- package/src/internal-path.test.ts +1 -0
- package/src/internal-path.ts +12 -9
- package/src/prerender-contract.ts +7 -7
- package/src/query.ts +16 -17
- package/src/sse.ts +19 -20
- package/src/states.ts +12 -12
- package/src/supabase-session.ts +46 -47
- package/src/theme.ts +4 -4
- package/src/ui/class-name.ts +3 -4
- package/src/ui/combobox.tsx +11 -11
- package/src/ui/dialog.tsx +5 -5
- package/src/ui/drawer.tsx +10 -10
- package/src/ui/input.tsx +2 -2
- package/src/ui/menu.tsx +8 -8
- package/src/ui/select.tsx +12 -12
- package/src/ui/tabs.tsx +14 -15
package/src/query.ts
CHANGED
|
@@ -3,29 +3,28 @@ import type { DefaultOptions } from "@tanstack/react-query";
|
|
|
3
3
|
import { ApiError } from "./api-error.ts";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* react-query defaults
|
|
6
|
+
* react-query defaults merged from three repos that each got part of this right.
|
|
7
7
|
*
|
|
8
8
|
* The default is three blind retries, which is wrong for an API that answers refusals
|
|
9
9
|
* precisely: a 402, a 403 or a 451 says exactly the same thing three times, and a 429 we hammer
|
|
10
10
|
* is a 429 we deserve. All three donors knew that much. What each knew alone:
|
|
11
11
|
*
|
|
12
12
|
* - one allowed **408** through, the only 4xx that a second attempt genuinely fixes;
|
|
13
|
-
* - one refused to retry a 429 whose CODE says the limit is durable
|
|
14
|
-
* spent balance clears by buying, not by waiting, so retrying burns another request against
|
|
13
|
+
* - one refused to retry a 429 whose CODE says the limit is durable. A monthly quota or a spent balance clears by buying, not by waiting, so retrying burns another request against
|
|
15
14
|
* the limiter and cannot succeed;
|
|
16
15
|
* - one set **`refetchOnReconnect`**, which none of the others had, and which is the single
|
|
17
16
|
* most useful refetch there is: coming back from a tunnel is exactly when the screen is stale.
|
|
18
17
|
*
|
|
19
18
|
* The second migration added the fourth rule, which no donor had: **when a 429 says how long,
|
|
20
19
|
* believe it.** Its limiter answers `Retry-After: 60` on a per-minute window, and the default
|
|
21
|
-
* backoff retries at 1s and 2s
|
|
22
|
-
* same limiter, and three seconds added before the screen says anything. So a stated wait is
|
|
20
|
+
* backoff retries at 1s and 2s, sending two more requests that cannot succeed, both charged against
|
|
21
|
+
* the same limiter, and three seconds added before the screen says anything. So a stated wait is
|
|
23
22
|
* waited out when it is short, and is an ANSWER when it is not.
|
|
24
23
|
*
|
|
25
24
|
* The fifth rule came from the server half of this fleet rather than from a donor: a refusal can
|
|
26
25
|
* state that waiting will never fix it at all, by sending `details.retryAfterSecs: null`. Read as
|
|
27
|
-
* "did not say how long"
|
|
28
|
-
*
|
|
26
|
+
* "did not say how long", as every version before this one did, that 429 is retried against a limit
|
|
27
|
+
* no amount of time moves.
|
|
29
28
|
*
|
|
30
29
|
* This is invariant 8. The rule itself lives in `@gusnips/http/retry`, so an SDK with no React
|
|
31
30
|
* in it follows the same one; what is here is the binding to react-query.
|
|
@@ -33,9 +32,9 @@ import { ApiError } from "./api-error.ts";
|
|
|
33
32
|
|
|
34
33
|
export interface QueryDefaultsOptions {
|
|
35
34
|
/**
|
|
36
|
-
* Error codes
|
|
37
|
-
*
|
|
38
|
-
*
|
|
35
|
+
* Error codes for limits that do not clear by waiting, such as a spent monthly quota or a balance
|
|
36
|
+
* that needs topping up. They usually arrive as 402 or 429; the status alone cannot tell them
|
|
37
|
+
* apart from a burst limit, which is why the caller names them.
|
|
39
38
|
*
|
|
40
39
|
* Only needed where the server does not say so itself. One that sends
|
|
41
40
|
* `details.retryAfterSecs: null` has already made the claim and `shouldRetry` reads it, which
|
|
@@ -47,10 +46,10 @@ export interface QueryDefaultsOptions {
|
|
|
47
46
|
/**
|
|
48
47
|
* Longest stated wait still worth retrying. Default 10 seconds.
|
|
49
48
|
*
|
|
50
|
-
* Only applies when the refusal named its own expiry (`Retry-After`, or `retryAfterSecs` in
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
49
|
+
* Only applies when the refusal named its own expiry (`Retry-After`, or `retryAfterSecs` in the
|
|
50
|
+
* body). A burst limit that clears in two seconds is worth sitting out; a monthly ceiling that
|
|
51
|
+
* clears in an hour is an answer, and holding a spinner for it helps nobody. Show the wait
|
|
52
|
+
* instead, which is what `humanizeWait` is for.
|
|
54
53
|
*/
|
|
55
54
|
maxRetryWaitSecs?: number;
|
|
56
55
|
staleTime?: number;
|
|
@@ -84,8 +83,8 @@ export function shouldRetry(
|
|
|
84
83
|
|
|
85
84
|
/**
|
|
86
85
|
* How long before the next attempt: the server's stated wait when there is one, an exponential
|
|
87
|
-
* backoff with jitter otherwise, and never less than the backoff
|
|
88
|
-
*
|
|
86
|
+
* backoff with jitter otherwise, and never less than the backoff. A `Retry-After: 0` is a server
|
|
87
|
+
* saying "immediately", which for a client that just got refused is still too soon.
|
|
89
88
|
*/
|
|
90
89
|
export function retryDelayMs(attemptIndex: number, error: unknown): number {
|
|
91
90
|
return delayFor(attemptIndex, answerOf(error));
|
|
@@ -130,7 +129,7 @@ export function queryDefaults({
|
|
|
130
129
|
* When it is ready, `refreshError` says whether the last refresh failed.
|
|
131
130
|
*
|
|
132
131
|
* **Never gate a screen on `isError`.** It is also true when a BACKGROUND refetch fails over data
|
|
133
|
-
* already on screen
|
|
132
|
+
* already on screen after a window regains focus, a poll or the invalidation after a save. So
|
|
134
133
|
* `isError ? <failure panel> : <data>` throws away what the reader was looking at over one blip.
|
|
135
134
|
* The adopters that had noticed each wrote `isLoadingError` beside a comment saying so; the ones
|
|
136
135
|
* that had not were a boundary and a great many inline ternaries. A refresh that failed is not a
|
package/src/sse.ts
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Server-sent events: the framing and the transport, kept apart.
|
|
3
3
|
*
|
|
4
|
-
* Three donors had three readers and each had half the answer. One was spec-correct about the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Three donors had three readers and each had half the answer. One was spec-correct about the wire
|
|
5
|
+
* but welded to `TextDecoderStream`, so the React Native app, whose fetch has no body reader, could
|
|
6
|
+
* not use a line of it. One was a platform-free `feed(text)` that three transports shared, and it
|
|
7
|
+
* silently dropped data. One was a whole second transport with its own fetch and its own 401
|
|
8
|
+
* handling, bypassing the app's only API client.
|
|
9
9
|
*
|
|
10
10
|
* So: {@link createSseParser} is the state machine and knows about no platform at all;
|
|
11
11
|
* {@link readSseStream} is fifteen lines of `ReadableStream` on top of it. A caller on a
|
|
12
12
|
* platform without body readers keeps its own fifteen lines and shares the machine underneath.
|
|
13
13
|
* (providerkit reached this exact split from the other direction and for the same reason.)
|
|
14
14
|
*
|
|
15
|
-
* And streams ride the ordinary API client
|
|
16
|
-
* "text/event-stream"}, timeoutMs: null})`. Not `EventSource`, which cannot carry an
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* And streams ride the ordinary API client: `client.request(path, {headers: {Accept:
|
|
16
|
+
* "text/event-stream"}, timeoutMs: null})`. Not `EventSource`, which cannot carry an Authorization
|
|
17
|
+
* header, and not a second fetch, which would need its own copy of the refresh logic and would
|
|
18
|
+
* drift from it.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
/** One frame off the wire. `data` is raw text; see {@link createSseParser} on why. */
|
|
22
22
|
export interface SseFrame {
|
|
23
|
-
/** The `event:` field, or `"message"`
|
|
23
|
+
/** The `event:` field, or `"message"` for an unnamed frame, as the spec requires. */
|
|
24
24
|
event: string;
|
|
25
25
|
/** Every `data:` line in the frame, joined with newlines. Never parsed here. */
|
|
26
26
|
data: string;
|
|
@@ -31,23 +31,22 @@ export interface SseFrame {
|
|
|
31
31
|
/**
|
|
32
32
|
* An incremental parser. Feed it text in whatever sizes the transport delivers.
|
|
33
33
|
*
|
|
34
|
-
* Frames split across reads are held until they complete, so a payload cut mid-line is never
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* Frames split across reads are held until they complete, so a payload cut mid-line is never lost
|
|
35
|
+
* or truncated. That is the failure this shape exists to prevent, and the one that only shows up
|
|
36
|
+
* under a slow network.
|
|
37
37
|
*
|
|
38
38
|
* Three rules that are easy to get wrong, and one donor got each of them wrong:
|
|
39
39
|
*
|
|
40
40
|
* - **`data:` repeats.** A frame may carry several `data:` lines and they are JOINED with
|
|
41
|
-
* newlines. One donor assigned instead of appending, so a multi-line payload arrived as its
|
|
42
|
-
* last line only — no error, no warning, just a truncated message.
|
|
41
|
+
* newlines. One donor assigned instead of appending, so a multi-line payload arrived as its last line only, with no error or warning, just a truncated message.
|
|
43
42
|
* - **Strip ONE leading space after the colon**, not all whitespace. `.trim()` is the obvious
|
|
44
43
|
* thing and it corrupts any payload whose own content begins or ends with a space.
|
|
45
44
|
* - **A frame with no `data:` is not dispatched.** That is what lets a server send bare
|
|
46
45
|
* `event: heartbeat` keepalives, or `:` comment lines, without waking a consumer.
|
|
47
46
|
*
|
|
48
|
-
* `data` is handed back as a raw string and deliberately NOT JSON-parsed. One donor parsed here
|
|
49
|
-
*
|
|
50
|
-
*
|
|
47
|
+
* `data` is handed back as a raw string and deliberately NOT JSON-parsed. One donor parsed here and
|
|
48
|
+
* its three consumers wanted three different things: one wraps non-JSON, one uses a tolerant
|
|
49
|
+
* parser, one parses directly. Parsing in the parser picks a policy for all of them.
|
|
51
50
|
*/
|
|
52
51
|
export function createSseParser(onFrame: (frame: SseFrame) => void): {
|
|
53
52
|
feed: (text: string) => void;
|
|
@@ -95,8 +94,8 @@ function parseFrame(raw: string): SseFrame | null {
|
|
|
95
94
|
* single-flight refresh, and a 402 or a 404 arriving as an `ApiError` the page can explain
|
|
96
95
|
* rather than an `onerror` event with nothing in it.
|
|
97
96
|
*
|
|
98
|
-
* Cancels the reader on the way out however it leaves
|
|
99
|
-
*
|
|
97
|
+
* Cancels the reader on the way out however it leaves. A stream abandoned without cancelling holds
|
|
98
|
+
* the connection open until the server gives up on it.
|
|
100
99
|
*/
|
|
101
100
|
export async function readSseStream(
|
|
102
101
|
response: Response,
|
package/src/states.ts
CHANGED
|
@@ -5,10 +5,10 @@ import type { ReactNode } from "react";
|
|
|
5
5
|
* somebody remembers.
|
|
6
6
|
*
|
|
7
7
|
* **No component ships here, on purpose.** Seven repos have an `EmptyState.tsx` and an
|
|
8
|
-
* `ErrorState.tsx`, and read side by side they overlap 9–37%: one draws a tinted icon badge,
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* `ErrorState.tsx`, and read side by side they overlap 9–37%: one draws a tinted icon badge, one a
|
|
9
|
+
* branded illustration set, one a mascot. What they genuinely share is the PROP SHAPE, which is the
|
|
10
|
+
* rule itself, and the only part a package can hold without deciding how eight brands look. Same
|
|
11
|
+
* reasoning as the styled Button: share behaviour, skin per product.
|
|
12
12
|
*
|
|
13
13
|
* Import the type, write your own component against it, and the compiler enforces the rest.
|
|
14
14
|
*/
|
|
@@ -22,16 +22,16 @@ import type { ReactNode } from "react";
|
|
|
22
22
|
* `action` is optional only because some panels fill themselves once a job upstream finishes.
|
|
23
23
|
*/
|
|
24
24
|
export interface EmptyStateProps {
|
|
25
|
-
/** What this panel is for. "No lookups yet"
|
|
25
|
+
/** What this panel is for. Use "No lookups yet", not "No results". */
|
|
26
26
|
title: string;
|
|
27
27
|
/** What belongs here, and what it will show once it does. */
|
|
28
28
|
description: ReactNode;
|
|
29
29
|
/** The way forward, as a control. Usually one button. */
|
|
30
30
|
action?: ReactNode;
|
|
31
31
|
/**
|
|
32
|
-
* `3`
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* Defaults to `3` for a panel that already sits under a page heading, the common case. `1` when
|
|
33
|
+
* this composer IS the page (a 404, a suspended account), which otherwise ships a document with
|
|
34
|
+
* no `h1` in it at all.
|
|
35
35
|
*
|
|
36
36
|
* Only one donor had this, and it is the difference between a screen reader announcing a
|
|
37
37
|
* page and announcing nothing.
|
|
@@ -49,9 +49,9 @@ export interface EmptyStateProps {
|
|
|
49
49
|
* Something did not work.
|
|
50
50
|
*
|
|
51
51
|
* `problem` names what failed, `cause` says why it most likely happened, `fix` says what to do
|
|
52
|
-
* about it, and `action` is that fix as a control. **`fix` and `action` are both required
|
|
53
|
-
* required prop is the only version of "never dead-end" that a caller in a hurry cannot skip,
|
|
54
|
-
*
|
|
52
|
+
* about it, and `action` is that fix as a control. **`fix` and `action` are both required**. A
|
|
53
|
+
* required prop is the only version of "never dead-end" that a caller in a hurry cannot skip, and
|
|
54
|
+
* the donor that made them required is the one whose error screens all have a way out.
|
|
55
55
|
*
|
|
56
56
|
* Not a place for a stack trace. `cause` is what a person can act on ("the site answered too
|
|
57
57
|
* slowly"), with the request id, if there is one, alongside it in `reference`.
|
|
@@ -66,7 +66,7 @@ export interface ErrorStateProps {
|
|
|
66
66
|
cause?: ReactNode;
|
|
67
67
|
/** The way forward, in words. */
|
|
68
68
|
fix: ReactNode;
|
|
69
|
-
/** The way forward
|
|
69
|
+
/** The way forward as a control, usually a retry button. */
|
|
70
70
|
action: ReactNode;
|
|
71
71
|
/** A request id or code, for a support thread. Set in mono, never shouted. */
|
|
72
72
|
reference?: ReactNode;
|
package/src/supabase-session.ts
CHANGED
|
@@ -43,33 +43,33 @@ type _StorageFitsCreateClient = Satisfied<
|
|
|
43
43
|
* Auth FAILED rather than answered, so nothing is known about the credential it was handed:
|
|
44
44
|
* either the request never landed, or GoTrue failed on its own.
|
|
45
45
|
*
|
|
46
|
-
* This is the question every site that ends a session has to ask
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
46
|
+
* This is the question every site that ends a session has to ask: a token refresh, an API door, and
|
|
47
|
+
* a callback choosing between "try again" and "that link is spent". Without it one bad minute at
|
|
48
|
+
* GoTrue signs out everybody whose token happened to need refreshing, and tells everybody clicking
|
|
49
|
+
* a recovery link that it has expired, which sends them to ask the service that is down for a
|
|
50
|
+
* replacement.
|
|
51
51
|
*
|
|
52
52
|
* Both clauses carry weight, and the second is why this is a function rather than one call.
|
|
53
53
|
* `isAuthRetryableFetchError` is a NAME check, and the statuses that earn that name are a list
|
|
54
|
-
* auth-js owns. The list has drifted
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
54
|
+
* auth-js owns. The list has drifted: 502, 503 and 504 at 2.91; those plus the 52x family and still
|
|
55
|
+
* no 500 at 2.106; 500 through 530 at 2.108. Code leaning on it alone is right at whichever version
|
|
56
|
+
* happens to be installed, which is not the same as being right, and a fleet installs several
|
|
57
|
+
* versions at once. It is also a LIST rather than a range, so it has holes at every version, drift
|
|
58
|
+
* or no drift: at 2.112 nothing covers 505 through 519 or 531 up, and a 507 or a 599 with a JSON
|
|
59
|
+
* body arrives as a plain `AuthApiError`. Pinning a recent SDK does not make the status clause
|
|
60
|
+
* redundant.
|
|
61
61
|
*
|
|
62
|
-
* The same statuses with a body that is NOT JSON
|
|
63
|
-
* `AuthUnknownError`, which carries no status at any status, so nothing here can tell that 507
|
|
64
|
-
*
|
|
65
|
-
*
|
|
62
|
+
* The same statuses with a body that is NOT JSON, such as a proxy's HTML page, arrive as an
|
|
63
|
+
* `AuthUnknownError`, which carries no status at any status, so nothing here can tell that 507 from
|
|
64
|
+
* a malformed 400. It answers false for both, on purpose: reading an unreadable 4xx as an outage
|
|
65
|
+
* would leave a dead session retrying forever, which is the other half of the same bug.
|
|
66
66
|
*
|
|
67
67
|
* Read the version a tree really serves with `bun why @supabase/auth-js`, which prints one
|
|
68
68
|
* heading per RESOLVED version; `node_modules` keeps copies the resolver does not serve.
|
|
69
69
|
*
|
|
70
|
-
* Anything that is not an auth error at all answers true: a failure from outside auth-js is not
|
|
71
|
-
*
|
|
72
|
-
*
|
|
70
|
+
* Anything that is not an auth error at all answers true: a failure from outside auth-js is not a
|
|
71
|
+
* verdict on the credential either. Nothing at all answers false. `null` is what every supabase-js
|
|
72
|
+
* auth call returns on success, and success is not an outage.
|
|
73
73
|
*/
|
|
74
74
|
export function isAuthOutage(error: unknown): boolean {
|
|
75
75
|
// Nothing thrown is not an outage, and this clause exists because the function is exported.
|
|
@@ -171,12 +171,12 @@ function localStorageOrMemory(): SupportedStorage {
|
|
|
171
171
|
* 2.116.0 and 41–51 s at 2.106.2. Nobody at a shared computer waits that long, and a page that
|
|
172
172
|
* navigates away in the meantime takes the removal with it.
|
|
173
173
|
*
|
|
174
|
-
* It resolves once the stored session is gone. `error` is `null` when auth ended the session on
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
174
|
+
* It resolves once the stored session is gone. `error` is `null` when auth ended the session on the
|
|
175
|
+
* server too. Otherwise it is the reason auth did not: its error, a thrown value or the deadline.
|
|
176
|
+
* The session may still be live on the server and on other devices. `SIGNED_OUT` arrives once
|
|
177
|
+
* auth-js has finished with the first call: about 100 ms later from 2.108.2 on, and up to 22 s
|
|
178
|
+
* later at 2.106.2. If your screen does not reload, clear your own store when this resolves instead
|
|
179
|
+
* of waiting for the event.
|
|
180
180
|
*/
|
|
181
181
|
export async function signOutEvenOffline(
|
|
182
182
|
auth: Pick<SupabaseSessionAuth, "signOut">,
|
|
@@ -254,7 +254,7 @@ export function createSupabaseSessionAdapter(
|
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
/**
|
|
257
|
-
* Every link type a GoTrue email can carry
|
|
257
|
+
* Every link type a GoTrue email can carry, including ones this app's templates do not send today.
|
|
258
258
|
* One donor rejected `invite` because "invites are never sent", which was true of the product and
|
|
259
259
|
* false of the deployment: an operator inviting somebody from Studio sent a branded email whose
|
|
260
260
|
* link then said it was invalid. A type the templates never emit costs nothing to accept: the hash
|
|
@@ -262,9 +262,9 @@ export function createSupabaseSessionAdapter(
|
|
|
262
262
|
*
|
|
263
263
|
* Closed on purpose: auth-js's own `EmailOtpType` ends in `(string & {})`, so it cannot make a
|
|
264
264
|
* destination map name every case. `Record<EmailLinkType, string>` can, and that is where an app
|
|
265
|
-
* decides that `recovery` and `invite` both continue to the new-password screen
|
|
266
|
-
*
|
|
267
|
-
*
|
|
265
|
+
* decides that `recovery` and `invite` both continue to the new-password screen. GoTrue creates an
|
|
266
|
+
* invited account with NO password, so landing one "inside" signs them in once and locks them out
|
|
267
|
+
* after.
|
|
268
268
|
*/
|
|
269
269
|
export type EmailLinkType =
|
|
270
270
|
"signup" | "invite" | "magiclink" | "recovery" | "email_change" | "email";
|
|
@@ -290,10 +290,10 @@ export type AuthCallback =
|
|
|
290
290
|
| { kind: "email-link"; tokenHash: string; type: EmailLinkType }
|
|
291
291
|
/**
|
|
292
292
|
* A session arriving in the address: a PKCE `code`, or the implicit flow's tokens. In a browser
|
|
293
|
-
* with `detectSessionInUrl` on, the client has already taken it
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
293
|
+
* with `detectSessionInUrl` on, the client has already taken it. Wait for the session, and do not
|
|
294
|
+
* trade it again, which would be a second exchange racing the first. Where nothing reads the
|
|
295
|
+
* address for you (React Native), `credential` is what `exchangeCodeForSession` or `setSession`
|
|
296
|
+
* takes.
|
|
297
297
|
*/
|
|
298
298
|
| {
|
|
299
299
|
kind: "session";
|
|
@@ -301,10 +301,9 @@ export type AuthCallback =
|
|
|
301
301
|
credential: { code: string } | { access_token: string; refresh_token: string };
|
|
302
302
|
}
|
|
303
303
|
/**
|
|
304
|
-
* GoTrue, or the provider behind it, reported a failure in the address. `code` is GoTrue's
|
|
305
|
-
*
|
|
306
|
-
* `error_description` is left out
|
|
307
|
-
* cause.
|
|
304
|
+
* GoTrue, or the provider behind it, reported a failure in the address. `code` is GoTrue's stable
|
|
305
|
+
* `error_code` (`otp_expired`, `bad_oauth_state`, …) and is what to choose words by;
|
|
306
|
+
* `error_description` is left out because it is English-only and often names the wrong cause.
|
|
308
307
|
*/
|
|
309
308
|
| { kind: "error"; error: string | null; code: string | null; cancelled: boolean }
|
|
310
309
|
/** Nothing usable: cut short between the mail client and here, or edited by hand. */
|
|
@@ -312,19 +311,19 @@ export type AuthCallback =
|
|
|
312
311
|
|
|
313
312
|
/**
|
|
314
313
|
* Reads an auth callback address. Pure, so every edge case is testable without a router or a
|
|
315
|
-
* client
|
|
314
|
+
* client. Pass `location.search` and `location.hash`, or the two halves of a deep link.
|
|
316
315
|
*
|
|
317
|
-
* GoTrue writes a failure into the FRAGMENT on every path and into the query only on some
|
|
318
|
-
*
|
|
319
|
-
*
|
|
316
|
+
* GoTrue writes a failure into the FRAGMENT on every path and into the query only on some. After a
|
|
317
|
+
* provider round trip, it writes both; after an implicit-flow email link, it writes the fragment
|
|
318
|
+
* alone. A callback reading one of the two misses a failure the other carries, and the person lands
|
|
320
319
|
* on a screen that says nothing. Both are read here.
|
|
321
320
|
*
|
|
322
|
-
* `cancelled` is the provider refusing on its own, which for Google is the person pressing Cancel
|
|
323
|
-
*
|
|
324
|
-
* that same value for an expired email link, a banned user and a disabled signup, each with
|
|
325
|
-
* `error_code` beside it, while a refusal relayed from the provider arrives with none. The
|
|
326
|
-
* test reads an expired link as a change of mind. And `error` itself is absent for a status
|
|
327
|
-
* outside GoTrue's OAuth mapping
|
|
321
|
+
* `cancelled` is the provider refusing on its own, which for Google is the person pressing Cancel.
|
|
322
|
+
* That is their choice, not a failure to report. It is NOT `error === "access_denied"`: GoTrue
|
|
323
|
+
* writes that same value for an expired email link, a banned user and a disabled signup, each with
|
|
324
|
+
* an `error_code` beside it, while a refusal relayed from the provider arrives with none. The
|
|
325
|
+
* obvious test reads an expired link as a change of mind. And `error` itself is absent for a status
|
|
326
|
+
* outside GoTrue's OAuth mapping. A rate-limited link arrives with `error_code` alone.
|
|
328
327
|
*/
|
|
329
328
|
export function parseAuthCallback(search: string | URLSearchParams, hash = ""): AuthCallback {
|
|
330
329
|
const query = new URLSearchParams(search);
|
package/src/theme.ts
CHANGED
|
@@ -109,10 +109,10 @@ declare global {
|
|
|
109
109
|
* "system" is stored as the word, not as an empty key. An empty key means the default, and the
|
|
110
110
|
* default is not always "system".
|
|
111
111
|
*
|
|
112
|
-
* **It writes to the page when the theme changes, and at no other time.** A page may override
|
|
113
|
-
*
|
|
114
|
-
* override until the theme really changes. Repainting on every resync would undo it a moment
|
|
115
|
-
*
|
|
112
|
+
* **It writes to the page when the theme changes, and at no other time.** A page may override the
|
|
113
|
+
* class for its own lifetime, as a public booking page that is always light does, and keeps that
|
|
114
|
+
* override until the theme really changes. Repainting on every resync would undo it a moment later,
|
|
115
|
+
* because `pageshow` fires on every load, not only on a back-forward restore.
|
|
116
116
|
*/
|
|
117
117
|
export function startTheme(options: ThemeOptions): ThemeController {
|
|
118
118
|
const running = window.__frontkitTheme;
|
package/src/ui/class-name.ts
CHANGED
|
@@ -10,10 +10,9 @@ export type StateClassName<State> = string | ((state: State) => string | undefin
|
|
|
10
10
|
/**
|
|
11
11
|
* Merge a wrapper's own classes with the caller's, keeping BOTH forms working.
|
|
12
12
|
*
|
|
13
|
-
* `cn()` on its own silently DROPS the function form
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* keeps one code path for both.
|
|
13
|
+
* `cn()` on its own silently DROPS the function form. clsx walks strings, arrays and plain objects,
|
|
14
|
+
* and a function is none of the three, so a caller styling by `open` or `transitionStatus` would
|
|
15
|
+
* get an empty class and no warning. Always returning a function keeps one code path for both.
|
|
17
16
|
*/
|
|
18
17
|
export function mergeClassName<State>(
|
|
19
18
|
base: ClassValue,
|
package/src/ui/combobox.tsx
CHANGED
|
@@ -37,7 +37,7 @@ export function comboboxInputValue<T>(
|
|
|
37
37
|
return value === null ? "" : getLabel(value);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/**
|
|
40
|
+
/** Pass text for the loading state. There is no default text or English fallback. */
|
|
41
41
|
type ComboboxLoadingState =
|
|
42
42
|
{ loading: boolean; loadingHint: ReactNode } | { loading?: undefined; loadingHint?: undefined };
|
|
43
43
|
|
|
@@ -60,15 +60,15 @@ export type ComboboxProps<T> = Omit<
|
|
|
60
60
|
> & {
|
|
61
61
|
value: T | null;
|
|
62
62
|
/**
|
|
63
|
-
* Items for the current query. Filtering is yours: this passes `filter={null}` to Base UI,
|
|
64
|
-
*
|
|
65
|
-
* page. Bound it here if it needs bounding; there is no cap in the wrapper, because a cap
|
|
66
|
-
*
|
|
63
|
+
* Items for the current query. Filtering is yours: this passes `filter={null}` to Base UI, so the
|
|
64
|
+
* list shows exactly what you return, whether it is a server search, a fuzzy match or a bounded
|
|
65
|
+
* page. Bound it here if it needs bounding; there is no cap in the wrapper, because a cap would
|
|
66
|
+
* quietly truncate a list a caller meant to show whole.
|
|
67
67
|
*/
|
|
68
68
|
search: (query: string) => readonly T[];
|
|
69
69
|
/** Stable identity of an item: its React key, and how two items are compared. */
|
|
70
70
|
getKey: (item: T) => string;
|
|
71
|
-
/** The item's text
|
|
71
|
+
/** The item's text, shown in the input once it is picked and used for typeahead matches. */
|
|
72
72
|
getLabel: (item: T) => string;
|
|
73
73
|
renderItem?: (item: T) => ReactNode;
|
|
74
74
|
/**
|
|
@@ -76,7 +76,7 @@ export type ComboboxProps<T> = Omit<
|
|
|
76
76
|
* end, and this is the line that offers one.
|
|
77
77
|
*/
|
|
78
78
|
emptyHint: ReactNode;
|
|
79
|
-
/** Label for the clear button. Required
|
|
79
|
+
/** Label for the clear button. Required because the button has an icon and no text. */
|
|
80
80
|
clearLabel: string;
|
|
81
81
|
/** Label for the button that opens the list. Required, same reason. */
|
|
82
82
|
openLabel: string;
|
|
@@ -88,10 +88,10 @@ export type ComboboxProps<T> = Omit<
|
|
|
88
88
|
popupClassName?: StateClassName<Primitive.Popup.State>;
|
|
89
89
|
inputProps?: Primitive.Input.Props;
|
|
90
90
|
/**
|
|
91
|
-
* Positioner props
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
91
|
+
* Positioner props: `align`, `side`, `sideOffset`, `collisionPadding` and the stacking order. The
|
|
92
|
+
* default `z-[60]` puts the list above the overlay layer (z-50: dialog, drawer); raise it with
|
|
93
|
+
* `positionerProps={{ className: "z-[80]" }}` if the app stacks something higher than a dialog
|
|
94
|
+
* above it.
|
|
95
95
|
*/
|
|
96
96
|
positionerProps?: Primitive.Positioner.Props;
|
|
97
97
|
} & ComboboxLoadingState;
|
package/src/ui/dialog.tsx
CHANGED
|
@@ -22,9 +22,9 @@ export type DialogContentProps = Primitive.Popup.Props & {
|
|
|
22
22
|
*/
|
|
23
23
|
label?: string;
|
|
24
24
|
/**
|
|
25
|
-
* Label for the built-in close button
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* Label for the built-in close button. REQUIRED to render the button: passing the label is the
|
|
26
|
+
* only way to render the button, so an unlabelled close button cannot be written. There is no
|
|
27
|
+
* default string, because a default would be English.
|
|
28
28
|
*/
|
|
29
29
|
closeLabel?: string;
|
|
30
30
|
backdropClassName?: StateClassName<Primitive.Backdrop.State>;
|
|
@@ -39,8 +39,8 @@ export type DialogContentProps = Primitive.Popup.Props & {
|
|
|
39
39
|
* page behind it) and is capped to the viewport, so a tall dialog is never taller than the
|
|
40
40
|
* screen it opens on.
|
|
41
41
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
42
|
+
* Padding, radius, shadow and type all belong to the product. What is here is what makes the
|
|
43
|
+
* primitive work.
|
|
44
44
|
*/
|
|
45
45
|
export function DialogContent({
|
|
46
46
|
className,
|
package/src/ui/drawer.tsx
CHANGED
|
@@ -38,9 +38,9 @@ const popupSide: Record<DrawerSide, string> = {
|
|
|
38
38
|
/**
|
|
39
39
|
* Where the portal mounts, and whether the drawer covers the page or a box inside it.
|
|
40
40
|
*
|
|
41
|
-
* A scoped drawer positions itself `absolute` against a container instead of `fixed` against
|
|
42
|
-
*
|
|
43
|
-
*
|
|
41
|
+
* A scoped drawer positions itself `absolute` against a container instead of `fixed` against the
|
|
42
|
+
* viewport, so it MUST be given that container, or it lands in `<body>` and positions against a box
|
|
43
|
+
* that is not there. The two travel together in the type.
|
|
44
44
|
*
|
|
45
45
|
* The container also needs a positioning context of its own (`relative`): `absolute` walks up
|
|
46
46
|
* to the nearest positioned ancestor, so a static container hands the drawer the whole page
|
|
@@ -56,20 +56,20 @@ export type DrawerProps<Payload = unknown> = Omit<
|
|
|
56
56
|
> &
|
|
57
57
|
DrawerPortalTarget & {
|
|
58
58
|
/**
|
|
59
|
-
* Which edge it comes from. This also sets `swipeDirection` on the root
|
|
60
|
-
*
|
|
61
|
-
*
|
|
59
|
+
* Which edge it comes from. This also sets `swipeDirection` on the root. The two must agree,
|
|
60
|
+
* and a mismatch is silent: the drawer looks right and dismisses on a swipe towards the screen
|
|
61
|
+
* it is attached to.
|
|
62
62
|
*/
|
|
63
63
|
side?: DrawerSide;
|
|
64
64
|
/** Accessible name, for a drawer with no visible `DrawerTitle`. */
|
|
65
65
|
label?: string;
|
|
66
66
|
children?: ReactNode;
|
|
67
|
-
/** Classes for the popup
|
|
67
|
+
/** Classes for the popup panel. */
|
|
68
68
|
className?: StateClassName<Primitive.Popup.State>;
|
|
69
69
|
backdropClassName?: StateClassName<Primitive.Backdrop.State>;
|
|
70
70
|
viewportClassName?: StateClassName<Primitive.Viewport.State>;
|
|
71
71
|
contentClassName?: StateClassName<Primitive.Content.State>;
|
|
72
|
-
/** Popup-level props the wrapper does not own
|
|
72
|
+
/** Popup-level props the wrapper does not own, such as `initialFocus`, `finalFocus` and `data-*`. */
|
|
73
73
|
popupProps?: Primitive.Popup.Props;
|
|
74
74
|
};
|
|
75
75
|
|
|
@@ -78,8 +78,8 @@ export type DrawerProps<Payload = unknown> = Omit<
|
|
|
78
78
|
* composed once here because the parts have to be arranged in exactly this order to work at
|
|
79
79
|
* all, and because two of the four rules below cost someone a bug that no test would catch.
|
|
80
80
|
*
|
|
81
|
-
*
|
|
82
|
-
* `
|
|
81
|
+
* Every root prop passes straight through, including `open`, `onOpenChange`, `modal`, `snapPoints`,
|
|
82
|
+
* `actionsRef` and `data-*`.
|
|
83
83
|
*/
|
|
84
84
|
export function Drawer<Payload = unknown>({
|
|
85
85
|
side = "right",
|
package/src/ui/input.tsx
CHANGED
|
@@ -12,8 +12,8 @@ const TEXTAREA_MAX_HEIGHT = 160;
|
|
|
12
12
|
/**
|
|
13
13
|
* The inline height an autosizing textarea should carry, or `""` for "leave it to CSS".
|
|
14
14
|
*
|
|
15
|
-
* ponytail: this whole observer is here only until `field-sizing: content` is baseline
|
|
16
|
-
*
|
|
15
|
+
* ponytail: this whole observer is here only until `field-sizing: content` is baseline. One CSS
|
|
16
|
+
* line does the same job. Ceiling: it does not exist in every browser we support yet.
|
|
17
17
|
*/
|
|
18
18
|
export function textareaHeight(clientWidth: number, scrollHeight: number): string {
|
|
19
19
|
if (clientWidth < TEXTAREA_MIN_AUTO_WIDTH) return "";
|
package/src/ui/menu.tsx
CHANGED
|
@@ -37,18 +37,18 @@ export type MenuContentProps = Primitive.Popup.Props & {
|
|
|
37
37
|
/** Where the portal mounts. Defaults to `<body>`. */
|
|
38
38
|
container?: Primitive.Portal.Props["container"];
|
|
39
39
|
/**
|
|
40
|
-
* Positioner props
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
40
|
+
* Positioner props: `side`, `align`, `sideOffset`, `collisionPadding` and the stacking order. The
|
|
41
|
+
* default `z-[60]` puts the menu above the overlay layer (z-50: dialog, drawer) so a menu opened
|
|
42
|
+
* inside a dialog is not painted behind it; raise it with `positionerProps={{ className: "z-[80]"
|
|
43
|
+
* }}` if the app stacks something higher.
|
|
44
44
|
*/
|
|
45
45
|
positionerProps?: Primitive.Positioner.Props;
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
* A dropdown
|
|
50
|
-
* and popup composed once, for the same reason the dialog's are: the arrangement is not
|
|
51
|
-
*
|
|
49
|
+
* A dropdown for choices, such as the account menu, a row's actions or a picker. Portal, positioner
|
|
50
|
+
* and popup composed once, for the same reason the dialog's are: the arrangement is not optional
|
|
51
|
+
* and getting it wrong throws a number.
|
|
52
52
|
*
|
|
53
53
|
* Roving focus, typeahead, escape, outside-dismiss and focus return all come from Base UI.
|
|
54
54
|
* Nothing here re-implements any of them.
|
|
@@ -146,7 +146,7 @@ export function MenuGroup({ label, children, ...rest }: Primitive.Group.Props &
|
|
|
146
146
|
);
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
/** A set of choices
|
|
149
|
+
/** A set of choices for a theme, locale or one axis of a filter. Same caption rule as `MenuGroup`. */
|
|
150
150
|
export function MenuRadioGroup({
|
|
151
151
|
label,
|
|
152
152
|
children,
|