@gusnips/react 0.9.5 → 0.9.7
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 +118 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/supabase-session.d.ts +72 -0
- package/dist/supabase-session.d.ts.map +1 -1
- package/dist/supabase-session.js +55 -0
- package/dist/supabase-session.js.map +1 -1
- package/dist/theme.d.ts +90 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +160 -0
- package/dist/theme.js.map +1 -0
- package/package.json +5 -1
- package/src/index.ts +2 -0
- package/src/supabase-session.test.ts +68 -0
- package/src/supabase-session.ts +104 -0
- package/src/theme.test.ts +268 -0
- package/src/theme.ts +220 -0
package/README.md
CHANGED
|
@@ -86,11 +86,70 @@ Supabase email links have two complete patterns. Keep either one, never half of
|
|
|
86
86
|
- A link carrying `ConfirmationURL` needs `detectSessionInUrl` enabled. That reader consumes an
|
|
87
87
|
implicit `#access_token` or a PKCE `?code`; it does not consume `token_hash`.
|
|
88
88
|
|
|
89
|
+
Either pattern can carry a destination — the `token_hash` one has to be asked. GoTrue hands every
|
|
90
|
+
template a `{{ .RedirectTo }}`: the `redirectTo` the app passed, already checked against the
|
|
91
|
+
allowlist, falling back to the Site URL when it was absent or not on it. A `{{ .ConfirmationURL }}`
|
|
92
|
+
link carries it for you. A template that writes its own landing drops it unless it asks:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
{{ .SiteURL }}/auth/callback?token_hash={{ .TokenHash }}&type=recovery&next={{ .RedirectTo }}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Leave that parameter off and `redirectTo` and `emailRedirectTo` do nothing on every e-mail flow,
|
|
99
|
+
however carefully the app sets them — four of the codebases this came from pass one that the
|
|
100
|
+
template throws away, so a person bounced off a protected page signs in and lands on the front
|
|
101
|
+
door. Nothing reports it: the app's call is correct, the link works, and the destination was
|
|
102
|
+
dropped in an HTML file hosted somewhere else.
|
|
103
|
+
|
|
104
|
+
`{{ .RedirectTo }}` arrives as an absolute URL rather than a path, so the landing reads it as one:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
const raw = new URLSearchParams(location.search).get("next");
|
|
108
|
+
const url = raw ? new URL(raw, location.origin) : null;
|
|
109
|
+
const next = url?.origin === location.origin ? safeInternalPath(url.pathname + url.search) : null;
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Validate it on arrival even though GoTrue already did: that check is against a list of origins,
|
|
113
|
+
so it says the link may come back here, not which page it may open.
|
|
114
|
+
|
|
89
115
|
Recovery and invite links continue to the new-password screen after the session opens. A callback
|
|
90
116
|
must also tell a used or expired link apart from a request that never landed — or one that landed
|
|
91
117
|
and came back 5xx: the first needs a new link, and the other two can retry the same one, because
|
|
92
118
|
the hash was never spent.
|
|
93
119
|
|
|
120
|
+
`parseAuthCallback` reads a callback address and says what arrived:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { parseAuthCallback } from "@gusnips/react/supabase";
|
|
124
|
+
|
|
125
|
+
parseAuthCallback("?token_hash=abc&type=recovery");
|
|
126
|
+
// → { kind: "email-link", tokenHash: "abc", type: "recovery" }
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
There are four kinds. `email-link` needs your one `verifyOtp` call. `session` is a PKCE `code` or
|
|
130
|
+
implicit-flow tokens; in a browser the client has already taken it, so wait for the session, and on
|
|
131
|
+
React Native hand `credential` to `exchangeCodeForSession` or `setSession`. `error` is a failure
|
|
132
|
+
GoTrue wrote into the address. `invalid` is a link that was cut short or edited.
|
|
133
|
+
|
|
134
|
+
Pass the fragment too — `parseAuthCallback(location.search, location.hash)`. GoTrue writes a
|
|
135
|
+
failure into the fragment every time and into the query only sometimes: after an implicit-flow
|
|
136
|
+
email link it is in the fragment alone, so a query-only reader shows nothing to somebody whose
|
|
137
|
+
link expired.
|
|
138
|
+
|
|
139
|
+
Choose the words by `code` (GoTrue's `error_code`, such as `otp_expired`), not by `error`.
|
|
140
|
+
`access_denied` covers an expired link, a banned user and a disabled signup as well as a person
|
|
141
|
+
pressing Cancel at Google; only the last one arrives with no code, and that is what `cancelled`
|
|
142
|
+
means. A cancel was their choice, so say nothing or offer the button again.
|
|
143
|
+
|
|
144
|
+
A handler you mount for Google's refusals also receives every email link's. An expired magic link
|
|
145
|
+
or reset link comes back with the same parameters, to whatever page `redirectTo` named. If that
|
|
146
|
+
handler only knows Google copy, the person whose link ran out is told Google failed. Check `code`
|
|
147
|
+
before you reach for provider copy: `otp_expired` is a link, never a provider.
|
|
148
|
+
|
|
149
|
+
Every email link type is accepted, `invite` included, even if your templates never send it. An
|
|
150
|
+
operator can still send one from Studio. Name a destination for each with
|
|
151
|
+
`Record<EmailLinkType, string>`, so a new type is a compile error rather than a wrong page.
|
|
152
|
+
|
|
94
153
|
There is also a deadline on every request, which none of the codebases this came from had. A
|
|
95
154
|
request with no timeout is a spinner with no end.
|
|
96
155
|
|
|
@@ -279,6 +338,55 @@ Some APIs write `message` for whoever reads the screen. Some write it for whoeve
|
|
|
279
338
|
in English, in an app that ships three languages. Your arm also gets the stated wait, which the
|
|
280
339
|
default has nowhere to put.
|
|
281
340
|
|
|
341
|
+
## Light, dark, or the system's
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
const { mode, resolved, setMode } = useTheme();
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
`mode` is what the person picked: `"light"`, `"dark"` or `"system"`. `resolved` is what is on
|
|
348
|
+
screen. `setMode("dark")` switches, saves the choice, and every other open tab follows.
|
|
349
|
+
|
|
350
|
+
You make `useTheme` once, with the storage key your app already uses:
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
// src/theme.ts
|
|
354
|
+
import { createTheme, type ThemeOptions } from "@gusnips/react/theme";
|
|
355
|
+
|
|
356
|
+
export const THEME = { key: "app.theme" } satisfies ThemeOptions;
|
|
357
|
+
export const { useTheme } = createTheme(THEME);
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Then give the same options to the Vite plugin, which paints the page in the right theme before
|
|
361
|
+
its first frame. Without it, a reader who chose dark sees one light frame on every load.
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
// vite.config.ts
|
|
365
|
+
import { themeScript } from "@gusnips/vite/theme";
|
|
366
|
+
import { THEME } from "./src/theme";
|
|
367
|
+
|
|
368
|
+
export default defineConfig({ plugins: [themeScript(THEME)] });
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
What it does that the copies it replaced each missed at least once:
|
|
372
|
+
|
|
373
|
+
- A browser that blocks site data throws when you touch `localStorage`. Here that is not a white
|
|
374
|
+
page: the default applies, and a choice made after that lasts until the page is left.
|
|
375
|
+
- The script that runs before paint is not a second copy. The plugin writes this function's own
|
|
376
|
+
source into a file, so the two cannot disagree about a stale value or blocked storage.
|
|
377
|
+
- That file is a real file, not an inline script, so a `script-src 'self'` policy lets it run.
|
|
378
|
+
An inline one under that policy never runs, and nothing tells you.
|
|
379
|
+
- `"system"` follows the OS live, even when the person switched to it mid-page. A laptop that
|
|
380
|
+
turns dark at sunset takes the page with it.
|
|
381
|
+
- `themeColor: { light: "#fff", dark: "#111" }` moves `<meta name="theme-color">` too, so the
|
|
382
|
+
browser toolbar matches the page instead of the OS.
|
|
383
|
+
- `attribute: "data-theme"` writes `data-theme="dark"` instead of the `dark` class, and
|
|
384
|
+
`defaultMode` picks what an empty key means (`"system"` unless you say otherwise).
|
|
385
|
+
|
|
386
|
+
In jsdom tests, mock `window.matchMedia`, which jsdom does not have, and run
|
|
387
|
+
`delete window.__frontkitTheme` between tests. There is one controller per page, and a jsdom
|
|
388
|
+
window lasts the whole file.
|
|
389
|
+
|
|
282
390
|
## Also here
|
|
283
391
|
|
|
284
392
|
`ErrorBoundary`, an SSE reader split into a platform-free parser and a stream wrapper,
|
|
@@ -343,15 +451,16 @@ something.
|
|
|
343
451
|
`@gusnips/react` itself needs `react` and nothing else. Anything that needs another runtime peer
|
|
344
452
|
lives behind a subpath, so you install a dependency only if you import the thing that uses it:
|
|
345
453
|
|
|
346
|
-
| Import from | What is in it
|
|
347
|
-
| ------------------------- |
|
|
348
|
-
| `@gusnips/react` | the client and rest
|
|
349
|
-
| `@gusnips/react/store` | `createAuthStore`, `authSlice`
|
|
350
|
-
| `@gusnips/react/guards` | the route guards
|
|
351
|
-
| `@gusnips/react/hydrate` | `hydrateOrMount`
|
|
352
|
-
| `@gusnips/react/supabase` | the session adapter
|
|
353
|
-
| `@gusnips/react/ui` | the seven wrappers
|
|
354
|
-
| `@gusnips/react/contract` | two prerender names
|
|
454
|
+
| Import from | What is in it | What you must have |
|
|
455
|
+
| ------------------------- | ---------------------------------------- | --------------------- |
|
|
456
|
+
| `@gusnips/react` | the client and rest | react |
|
|
457
|
+
| `@gusnips/react/store` | `createAuthStore`, `authSlice` | zustand |
|
|
458
|
+
| `@gusnips/react/guards` | the route guards | react-router-dom |
|
|
459
|
+
| `@gusnips/react/hydrate` | `hydrateOrMount` | react-dom |
|
|
460
|
+
| `@gusnips/react/supabase` | the session adapter, `parseAuthCallback` | @supabase/supabase-js |
|
|
461
|
+
| `@gusnips/react/ui` | the seven wrappers | @base-ui/react |
|
|
462
|
+
| `@gusnips/react/contract` | two prerender names | nothing |
|
|
463
|
+
| `@gusnips/react/theme` | `createTheme`, `startTheme` | react |
|
|
355
464
|
|
|
356
465
|
The rule behind that table: **a peer marked optional must not be reachable from the main entry
|
|
357
466
|
point.** An optional peer the barrel imports anyway is not optional — it is a required one whose
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAgCA,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE9F,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACxE,OAAO,EACL,eAAe,EACf,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAAE,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEjG,OAAO,EACL,aAAa,EACb,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,GACxB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,EACZ,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,qBAAqB,EAC1B,KAAK,YAAY,EACjB,KAAK,SAAS,GACf,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,oBAAoB,EACpB,YAAY,EACZ,gBAAgB,EAChB,UAAU,GACX,MAAM,sBAAsB,CAAC;AAK9B,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,WAAW,CAAC;AAElF,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
// @gusnips/react/guards the route guards → react-router-dom
|
|
14
14
|
// @gusnips/react/hydrate hydrateOrMount → react-dom
|
|
15
15
|
// @gusnips/react/supabase the session adapter → @supabase/supabase-js
|
|
16
|
+
// @gusnips/react/theme the theme controller → react only, but `@gusnips/vite/theme`
|
|
17
|
+
// serializes it and must not load the rest
|
|
16
18
|
//
|
|
17
19
|
// `react-dom` is the one the second migration added, and it is the sharpest case yet: that
|
|
18
20
|
// adopter ships a REACT NATIVE app beside its two web apps, and React Native has no react-dom to
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,2CAA2C;AAC3C,EAAE;AACF,+FAA+F;AAC/F,gGAAgG;AAChG,+FAA+F;AAC/F,iGAAiG;AACjG,0BAA0B;AAC1B,EAAE;AACF,qEAAqE;AACrE,8DAA8D;AAC9D,uEAAuE;AACvE,gEAAgE;AAChE,4EAA4E;AAC5E,EAAE;AACF,2FAA2F;AAC3F,iGAAiG;AACjG,+FAA+F;AAC/F,gGAAgG;AAChG,4EAA4E;AAC5E,EAAE;AACF,2FAA2F;AAC3F,iGAAiG;AACjG,qFAAqF;AACrF,6FAA6F;AAC7F,iGAAiG;AACjG,yFAAyF;AACzF,uBAAuB;AAEvB,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAkB,MAAM,oBAAoB,CAAC;AAE9F,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACxE,OAAO,EACL,eAAe,GAMhB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,aAAa,EAAiB,MAAM,UAAU,CAAC;AAEzE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAA6B,MAAM,YAAY,CAAC;AAEjG,OAAO,EACL,aAAa,GAGd,MAAM,qBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,GAQb,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,oBAAoB,EACpB,YAAY,EACZ,gBAAgB,EAChB,UAAU,GACX,MAAM,sBAAsB,CAAC;AAE9B,iGAAiG;AACjG,kGAAkG;AAClG,6FAA6F;AAC7F,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E,OAAO,EAAE,cAAc,EAAE,eAAe,EAAwB,MAAM,WAAW,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,2CAA2C;AAC3C,EAAE;AACF,+FAA+F;AAC/F,gGAAgG;AAChG,+FAA+F;AAC/F,iGAAiG;AACjG,0BAA0B;AAC1B,EAAE;AACF,qEAAqE;AACrE,8DAA8D;AAC9D,uEAAuE;AACvE,gEAAgE;AAChE,4EAA4E;AAC5E,4FAA4F;AAC5F,8FAA8F;AAC9F,EAAE;AACF,2FAA2F;AAC3F,iGAAiG;AACjG,+FAA+F;AAC/F,gGAAgG;AAChG,4EAA4E;AAC5E,EAAE;AACF,2FAA2F;AAC3F,iGAAiG;AACjG,qFAAqF;AACrF,6FAA6F;AAC7F,iGAAiG;AACjG,yFAAyF;AACzF,uBAAuB;AAEvB,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAkB,MAAM,oBAAoB,CAAC;AAE9F,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACxE,OAAO,EACL,eAAe,GAMhB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,aAAa,EAAiB,MAAM,UAAU,CAAC;AAEzE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAA6B,MAAM,YAAY,CAAC;AAEjG,OAAO,EACL,aAAa,GAGd,MAAM,qBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,GAQb,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,oBAAoB,EACpB,YAAY,EACZ,gBAAgB,EAChB,UAAU,GACX,MAAM,sBAAsB,CAAC;AAE9B,iGAAiG;AACjG,kGAAkG;AAClG,6FAA6F;AAC7F,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E,OAAO,EAAE,cAAc,EAAE,eAAe,EAAwB,MAAM,WAAW,CAAC"}
|
|
@@ -53,5 +53,77 @@ export declare function isAuthOutage(error: unknown): boolean;
|
|
|
53
53
|
* stays on that side of the line too.
|
|
54
54
|
*/
|
|
55
55
|
export declare function createSupabaseSessionAdapter(auth: SupabaseSessionAuth): SessionAdapter;
|
|
56
|
+
/**
|
|
57
|
+
* Every link type a GoTrue email can carry — not only the ones this app's templates send today.
|
|
58
|
+
* One donor rejected `invite` because "invites are never sent", which was true of the product and
|
|
59
|
+
* false of the deployment: an operator inviting somebody from Studio sent a branded email whose
|
|
60
|
+
* link then said it was invalid. A type the templates never emit costs nothing to accept: the hash
|
|
61
|
+
* is the proof, and GoTrue opens a session only for the person whose one-time token it is.
|
|
62
|
+
*
|
|
63
|
+
* Closed on purpose: auth-js's own `EmailOtpType` ends in `(string & {})`, so it cannot make a
|
|
64
|
+
* destination map name every case. `Record<EmailLinkType, string>` can, and that is where an app
|
|
65
|
+
* decides that `recovery` and `invite` both continue to the new-password screen — GoTrue creates
|
|
66
|
+
* an invited account with NO password, so landing one "inside" signs them in once and locks them
|
|
67
|
+
* out after.
|
|
68
|
+
*/
|
|
69
|
+
export type EmailLinkType = "signup" | "invite" | "magiclink" | "recovery" | "email_change" | "email";
|
|
70
|
+
/** Everything an auth callback address can carry, decided from the address alone. */
|
|
71
|
+
export type AuthCallback =
|
|
72
|
+
/** An email link with a one-use `token_hash`: trade it with `verifyOtp`, exactly once. */
|
|
73
|
+
{
|
|
74
|
+
kind: "email-link";
|
|
75
|
+
tokenHash: string;
|
|
76
|
+
type: EmailLinkType;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* A session arriving in the address: a PKCE `code`, or the implicit flow's tokens. In a browser
|
|
80
|
+
* with `detectSessionInUrl` on, the client has already taken it — wait for the session, and do
|
|
81
|
+
* not trade it again, which would be a second exchange racing the first. Where nothing reads
|
|
82
|
+
* the address for you (React Native), `credential` is what `exchangeCodeForSession` or
|
|
83
|
+
* `setSession` takes.
|
|
84
|
+
*/
|
|
85
|
+
| {
|
|
86
|
+
kind: "session";
|
|
87
|
+
type: EmailLinkType | null;
|
|
88
|
+
credential: {
|
|
89
|
+
code: string;
|
|
90
|
+
} | {
|
|
91
|
+
access_token: string;
|
|
92
|
+
refresh_token: string;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* GoTrue, or the provider behind it, reported a failure in the address. `code` is GoTrue's
|
|
97
|
+
* stable `error_code` (`otp_expired`, `bad_oauth_state`, …) and is what to choose words by;
|
|
98
|
+
* `error_description` is left out on purpose — it is English-only and often names the wrong
|
|
99
|
+
* cause.
|
|
100
|
+
*/
|
|
101
|
+
| {
|
|
102
|
+
kind: "error";
|
|
103
|
+
error: string | null;
|
|
104
|
+
code: string | null;
|
|
105
|
+
cancelled: boolean;
|
|
106
|
+
}
|
|
107
|
+
/** Nothing usable: cut short between the mail client and here, or edited by hand. */
|
|
108
|
+
| {
|
|
109
|
+
kind: "invalid";
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Reads an auth callback address. Pure, so every edge case is testable without a router or a
|
|
113
|
+
* client — pass `location.search` and `location.hash`, or the two halves of a deep link.
|
|
114
|
+
*
|
|
115
|
+
* GoTrue writes a failure into the FRAGMENT on every path and into the query only on some — the
|
|
116
|
+
* query too after a provider round trip, the fragment alone after an implicit-flow email link —
|
|
117
|
+
* so a callback reading one of the two misses a failure the other carries, and the person lands
|
|
118
|
+
* on a screen that says nothing. Both are read here.
|
|
119
|
+
*
|
|
120
|
+
* `cancelled` is the provider refusing on its own, which for Google is the person pressing Cancel
|
|
121
|
+
* — their choice, not a failure to report. It is NOT `error === "access_denied"`: GoTrue writes
|
|
122
|
+
* that same value for an expired email link, a banned user and a disabled signup, each with an
|
|
123
|
+
* `error_code` beside it, while a refusal relayed from the provider arrives with none. The obvious
|
|
124
|
+
* test reads an expired link as a change of mind. And `error` itself is absent for a status
|
|
125
|
+
* outside GoTrue's OAuth mapping — a rate-limited link arrives with `error_code` alone.
|
|
126
|
+
*/
|
|
127
|
+
export declare function parseAuthCallback(search: string | URLSearchParams, hash?: string): AuthCallback;
|
|
56
128
|
export {};
|
|
57
129
|
//# sourceMappingURL=supabase-session.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"supabase-session.d.ts","sourceRoot":"","sources":["../src/supabase-session.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD,UAAU,qBAAqB;IAC7B,IAAI,EAAE;QAAE,OAAO,EAAE;YAAE,YAAY,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAA;KAAE,CAAC;CACpD;AAED,8FAA8F;AAC9F,MAAM,WAAW,mBAAmB;IAClC,UAAU,IAAI,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAC7C,cAAc,IAAI,OAAO,CAAC,qBAAqB,GAAG;QAAE,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACtE,OAAO,IAAI,OAAO,CAAC;QAAE,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACxC;AAOD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CASpD;AAaD;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,mBAAmB,GAAG,cAAc,CAsBtF"}
|
|
1
|
+
{"version":3,"file":"supabase-session.d.ts","sourceRoot":"","sources":["../src/supabase-session.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD,UAAU,qBAAqB;IAC7B,IAAI,EAAE;QAAE,OAAO,EAAE;YAAE,YAAY,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAA;KAAE,CAAC;CACpD;AAED,8FAA8F;AAC9F,MAAM,WAAW,mBAAmB;IAClC,UAAU,IAAI,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAC7C,cAAc,IAAI,OAAO,CAAC,qBAAqB,GAAG;QAAE,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACtE,OAAO,IAAI,OAAO,CAAC;QAAE,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACxC;AAOD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CASpD;AAaD;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,mBAAmB,GAAG,cAAc,CAsBtF;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,aAAa,GACvB,QAAQ,GAAG,QAAQ,GAAG,WAAW,GAAG,UAAU,GAAG,cAAc,GAAG,OAAO,CAAC;AAiB5E,qFAAqF;AACrF,MAAM,MAAM,YAAY;AACtB,0FAA0F;AACxF;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,aAAa,CAAA;CAAE;AAChE;;;;;;GAMG;GACD;IACE,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE,aAAa,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC;CAChF;AACH;;;;;GAKG;GACD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE;AAClF,qFAAqF;GACnF;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAExB;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,EAAE,IAAI,SAAK,GAAG,YAAY,CA6B3F"}
|
package/dist/supabase-session.js
CHANGED
|
@@ -82,4 +82,59 @@ export function createSupabaseSessionAdapter(auth) {
|
|
|
82
82
|
},
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
|
+
const EMAIL_LINK_TYPES = {
|
|
86
|
+
signup: true,
|
|
87
|
+
invite: true,
|
|
88
|
+
magiclink: true,
|
|
89
|
+
recovery: true,
|
|
90
|
+
email_change: true,
|
|
91
|
+
email: true,
|
|
92
|
+
};
|
|
93
|
+
function isEmailLinkType(value) {
|
|
94
|
+
// Not `in`: `"toString" in EMAIL_LINK_TYPES` is true, and the type is typed by whoever sends
|
|
95
|
+
// the link.
|
|
96
|
+
return Object.hasOwn(EMAIL_LINK_TYPES, value);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Reads an auth callback address. Pure, so every edge case is testable without a router or a
|
|
100
|
+
* client — pass `location.search` and `location.hash`, or the two halves of a deep link.
|
|
101
|
+
*
|
|
102
|
+
* GoTrue writes a failure into the FRAGMENT on every path and into the query only on some — the
|
|
103
|
+
* query too after a provider round trip, the fragment alone after an implicit-flow email link —
|
|
104
|
+
* so a callback reading one of the two misses a failure the other carries, and the person lands
|
|
105
|
+
* on a screen that says nothing. Both are read here.
|
|
106
|
+
*
|
|
107
|
+
* `cancelled` is the provider refusing on its own, which for Google is the person pressing Cancel
|
|
108
|
+
* — their choice, not a failure to report. It is NOT `error === "access_denied"`: GoTrue writes
|
|
109
|
+
* that same value for an expired email link, a banned user and a disabled signup, each with an
|
|
110
|
+
* `error_code` beside it, while a refusal relayed from the provider arrives with none. The obvious
|
|
111
|
+
* test reads an expired link as a change of mind. And `error` itself is absent for a status
|
|
112
|
+
* outside GoTrue's OAuth mapping — a rate-limited link arrives with `error_code` alone.
|
|
113
|
+
*/
|
|
114
|
+
export function parseAuthCallback(search, hash = "") {
|
|
115
|
+
const query = new URLSearchParams(search);
|
|
116
|
+
const fragment = new URLSearchParams(hash.replace(/^#/, ""));
|
|
117
|
+
const read = (name) => query.get(name) || fragment.get(name) || null;
|
|
118
|
+
const error = read("error");
|
|
119
|
+
const code = read("error_code");
|
|
120
|
+
if (error || code || read("error_description"))
|
|
121
|
+
return { kind: "error", error, code, cancelled: error === "access_denied" && code === null };
|
|
122
|
+
const rawType = read("type") ?? "";
|
|
123
|
+
const type = isEmailLinkType(rawType) ? rawType : null;
|
|
124
|
+
const tokenHash = query.get("token_hash")?.trim();
|
|
125
|
+
if (tokenHash)
|
|
126
|
+
return type ? { kind: "email-link", tokenHash, type } : { kind: "invalid" };
|
|
127
|
+
const pkceCode = query.get("code");
|
|
128
|
+
if (pkceCode)
|
|
129
|
+
return { kind: "session", type, credential: { code: pkceCode } };
|
|
130
|
+
const accessToken = fragment.get("access_token");
|
|
131
|
+
const refreshToken = fragment.get("refresh_token");
|
|
132
|
+
if (accessToken && refreshToken)
|
|
133
|
+
return {
|
|
134
|
+
kind: "session",
|
|
135
|
+
type,
|
|
136
|
+
credential: { access_token: accessToken, refresh_token: refreshToken },
|
|
137
|
+
};
|
|
138
|
+
return { kind: "invalid" };
|
|
139
|
+
}
|
|
85
140
|
//# sourceMappingURL=supabase-session.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"supabase-session.js","sourceRoot":"","sources":["../src/supabase-session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,yBAAyB,EAAuB,MAAM,uBAAuB,CAAC;AAmBpG;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,4FAA4F;IAC5F,6EAA6E;IAC7E,8FAA8F;IAC9F,8FAA8F;IAC9F,gFAAgF;IAChF,IAAI,KAAK,IAAI,IAAI;QAAE,OAAO,KAAK,CAAC;IAChC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACrC,OAAO,yBAAyB,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC,IAAI,GAAG,CAAC;AACxE,CAAC;AAED,SAAS,WAAW,CAAC,KAAc;IACjC,0FAA0F;IAC1F,uFAAuF;IACvF,kFAAkF;IAClF,8EAA8E;IAC9E,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,IAAI,YAAY,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC7D,8FAA8F;IAC9F,8FAA8F;IAC9F,OAAO,KAAK,CAAC,IAAI,KAAK,2BAA2B,CAAC;AACpD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,4BAA4B,CAAC,IAAyB;IACpE,OAAO;QACL,QAAQ,EAAE,KAAK,IAAI,EAAE;YACnB,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC;YACzC,OAAO,IAAI,CAAC,OAAO,EAAE,YAAY,IAAI,IAAI,CAAC;QAC5C,CAAC;QACD,OAAO,EAAE,KAAK,IAAI,EAAE;YAClB,IAAI,CAAC;gBACH,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,cAAc,EAAE,CAAC;gBACpD,OAAO;oBACL,KAAK,EAAE,IAAI,CAAC,OAAO,EAAE,YAAY,IAAI,IAAI;oBACzC,WAAW,EAAE,KAAK,KAAK,IAAI,IAAI,WAAW,CAAC,KAAK,CAAC;iBAClD,CAAC;YACJ,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1D,CAAC;QACH,CAAC;QACD,OAAO,EAAE,KAAK,IAAI,EAAE;YAClB,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;YACvC,IAAI,KAAK;gBAAE,MAAM,KAAK,CAAC;QACzB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
1
|
+
{"version":3,"file":"supabase-session.js","sourceRoot":"","sources":["../src/supabase-session.ts"],"names":[],"mappings":"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"}
|
package/dist/theme.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/** What a person picked. `"system"` means "follow the operating system", live. */
|
|
2
|
+
export type ThemeMode = "light" | "dark" | "system";
|
|
3
|
+
/** What is on screen right now. */
|
|
4
|
+
export type ResolvedTheme = "light" | "dark";
|
|
5
|
+
export interface ThemeOptions {
|
|
6
|
+
/**
|
|
7
|
+
* The `localStorage` key holding the choice. Keep the key your app already uses: a new key
|
|
8
|
+
* resets everybody's choice once.
|
|
9
|
+
*/
|
|
10
|
+
key: string;
|
|
11
|
+
/**
|
|
12
|
+
* What an empty, unreadable or unrecognised key means. Defaults to `"system"`.
|
|
13
|
+
*/
|
|
14
|
+
defaultMode?: ThemeMode;
|
|
15
|
+
/**
|
|
16
|
+
* Where the resolved theme goes on `<html>`. `"class"` toggles `dark`, which is what
|
|
17
|
+
* Tailwind's `dark:` variant and `@gusnips/tokens` read. `"data-theme"` writes `"light"` or
|
|
18
|
+
* `"dark"` into that attribute, for a stylesheet whose `@custom-variant` reads it. Defaults
|
|
19
|
+
* to `"class"`.
|
|
20
|
+
*/
|
|
21
|
+
attribute?: "class" | "data-theme";
|
|
22
|
+
/**
|
|
23
|
+
* The browser toolbar colour for each theme, written to every `<meta name="theme-color">`.
|
|
24
|
+
* Leave it out and the tags are not touched. It is needed whenever the choice can differ from
|
|
25
|
+
* the OS setting: a `media` query on the tag follows the OS, not your app.
|
|
26
|
+
*/
|
|
27
|
+
themeColor?: Readonly<Record<ResolvedTheme, string>>;
|
|
28
|
+
}
|
|
29
|
+
export interface ThemeSnapshot {
|
|
30
|
+
readonly mode: ThemeMode;
|
|
31
|
+
readonly resolved: ResolvedTheme;
|
|
32
|
+
}
|
|
33
|
+
export interface ThemeController {
|
|
34
|
+
getSnapshot(): ThemeSnapshot;
|
|
35
|
+
/** Applies at once, and is remembered when storage allows it. */
|
|
36
|
+
setMode(mode: ThemeMode): void;
|
|
37
|
+
subscribe(listener: () => void): () => void;
|
|
38
|
+
}
|
|
39
|
+
declare global {
|
|
40
|
+
interface Window {
|
|
41
|
+
/** The page's one theme controller. See {@link startTheme}. */
|
|
42
|
+
__frontkitTheme?: ThemeController;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Reads the choice, paints it, and keeps it painted: when the OS switches (a laptop that goes
|
|
47
|
+
* dark at sunset), when another tab picks a theme, and when the page comes back from the
|
|
48
|
+
* back-forward cache.
|
|
49
|
+
*
|
|
50
|
+
* **This function is serialized.** `@gusnips/vite/theme` writes `startTheme.toString()` into the
|
|
51
|
+
* pre-paint file, so it must not reference anything outside its own body: no imports, no helpers
|
|
52
|
+
* or constants from this module. It would still pass here and throw a `ReferenceError` in that
|
|
53
|
+
* file, before first paint. The vite package's test runs the serialized copy in a bare context
|
|
54
|
+
* to catch that. Comments go up here rather than in the body, because the body ships.
|
|
55
|
+
*
|
|
56
|
+
* **One per page.** The first call stores the controller on `window` and later calls return it,
|
|
57
|
+
* whatever options they pass. Two would both follow the OS, and the one that missed a `setMode`
|
|
58
|
+
* would repaint its stale choice on the next change. So the pre-paint script and the app must
|
|
59
|
+
* pass the same options, which is easiest when both import them from one module.
|
|
60
|
+
*
|
|
61
|
+
* **Blocked storage is not an empty key.** When storage throws, the default applies at boot, and
|
|
62
|
+
* a choice made afterwards lasts until the page is left. A resync never replaces that choice
|
|
63
|
+
* with the default, because a read that failed says nothing about what was chosen.
|
|
64
|
+
*
|
|
65
|
+
* "system" is stored as the word, not as an empty key. An empty key means the default, and the
|
|
66
|
+
* default is not always "system".
|
|
67
|
+
*/
|
|
68
|
+
export declare function startTheme(options: ThemeOptions): ThemeController;
|
|
69
|
+
export interface Theme {
|
|
70
|
+
/** The choice and what it resolves to, re-rendering when either changes. */
|
|
71
|
+
useTheme(): ThemeSnapshot & {
|
|
72
|
+
setMode(mode: ThemeMode): void;
|
|
73
|
+
};
|
|
74
|
+
/** For code outside React: a keyboard shortcut, a command palette. Browser only. */
|
|
75
|
+
setMode(mode: ThemeMode): void;
|
|
76
|
+
/** For code outside React. Answers the prerender snapshot where there is no window. */
|
|
77
|
+
getSnapshot(): ThemeSnapshot;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The React side of {@link startTheme}. Nothing starts at import, so a module calling this at
|
|
81
|
+
* its top level is safe to prerender. The controller starts on first use, or is found if the
|
|
82
|
+
* pre-paint script already started it.
|
|
83
|
+
*
|
|
84
|
+
* During a prerender, and while hydrating, `useTheme` answers the default. `"system"` cannot be
|
|
85
|
+
* resolved without a window, so it reads as light, as `color-scheme` does before the pre-paint
|
|
86
|
+
* script runs. React then re-renders with the real snapshot, so a toggle hydrates cleanly and
|
|
87
|
+
* shows the right choice a frame later instead of failing to hydrate.
|
|
88
|
+
*/
|
|
89
|
+
export declare function createTheme(options: ThemeOptions): Theme;
|
|
90
|
+
//# sourceMappingURL=theme.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAsCA,kFAAkF;AAClF,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEpD,mCAAmC;AACnC,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,MAAM,CAAC;AAE7C,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;OAEG;IACH,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,YAAY,CAAC;IACnC;;;;OAIG;IACH,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC;CACtD;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,IAAI,aAAa,CAAC;IAC7B,iEAAiE;IACjE,OAAO,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IAC/B,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;CAC7C;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,+DAA+D;QAC/D,eAAe,CAAC,EAAE,eAAe,CAAC;KACnC;CACF;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,eAAe,CAsEjE;AAED,MAAM,WAAW,KAAK;IACpB,4EAA4E;IAC5E,QAAQ,IAAI,aAAa,GAAG;QAAE,OAAO,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAAA;KAAE,CAAC;IAC/D,oFAAoF;IACpF,OAAO,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IAC/B,uFAAuF;IACvF,WAAW,IAAI,aAAa,CAAC;CAC9B;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,KAAK,CAiBxD"}
|
package/dist/theme.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Light, dark, or whatever the operating system says — chosen once, remembered, and painted
|
|
3
|
+
* before the first frame.
|
|
4
|
+
*
|
|
5
|
+
* Nearly every app in the fleet had written this on its own, and each copy knew something the
|
|
6
|
+
* others did not. What they got wrong is the design brief for this file:
|
|
7
|
+
*
|
|
8
|
+
* - **Storage is read unguarded.** A browser that blocks site data throws on `localStorage`
|
|
9
|
+
* itself, and the read ran during a render, so a privacy setting was a white page. Apps fixed
|
|
10
|
+
* this one at a time, and the last copy still had it.
|
|
11
|
+
* - **The pre-paint script and the runtime disagreed.** Most apps kept two copies of "read the
|
|
12
|
+
* key, resolve system, set the class": an inline script in `index.html`, because it has to run
|
|
13
|
+
* before any module loads, and the store the app renders from. They drifted on exactly the
|
|
14
|
+
* inputs nobody tests. With storage blocked one forced light while the other followed the OS,
|
|
15
|
+
* and with a stale value in the key one read it as light while the other read it as system.
|
|
16
|
+
* Either way the page painted one theme, and React swapped it for the other one frame later.
|
|
17
|
+
* One app had written a build check that the two copies at least named the same key.
|
|
18
|
+
* - **The OS preference started as a guess.** A provider that holds the OS preference in state,
|
|
19
|
+
* starting at `"light"` and correcting it in an effect, commits a light frame first. That
|
|
20
|
+
* frame REMOVES the `.dark` class the pre-paint script had just set, so a reader whose OS is
|
|
21
|
+
* dark sees a flash on every load.
|
|
22
|
+
* - **The pre-paint script was inline, and a strict CSP blocks inline scripts.** Under
|
|
23
|
+
* `script-src 'self'` it never ran in production: the app booted light and turned dark a frame
|
|
24
|
+
* later. A CSP hash works until the first whitespace change silently breaks it again.
|
|
25
|
+
* - **Another tab's choice was ignored.** A tab left open kept the old theme until it reloaded.
|
|
26
|
+
* One app did listen for `storage`, then re-read its own page, which the other tab had not
|
|
27
|
+
* changed.
|
|
28
|
+
*
|
|
29
|
+
* So there is ONE function, {@link startTheme}. The page runs it before first paint and the app
|
|
30
|
+
* uses it after React mounts. `@gusnips/vite/theme` serves it as a same-origin FILE, built from
|
|
31
|
+
* this function's own source, so the two cannot drift and a strict CSP allows it. The runtime
|
|
32
|
+
* the pre-paint script creates is also the one the app talks to: {@link createTheme} finds it
|
|
33
|
+
* rather than starting a second one.
|
|
34
|
+
*
|
|
35
|
+
* What stays in the app: the control (three buttons, a menu, a switch) and its words.
|
|
36
|
+
*/
|
|
37
|
+
import { useSyncExternalStore } from "react";
|
|
38
|
+
/**
|
|
39
|
+
* Reads the choice, paints it, and keeps it painted: when the OS switches (a laptop that goes
|
|
40
|
+
* dark at sunset), when another tab picks a theme, and when the page comes back from the
|
|
41
|
+
* back-forward cache.
|
|
42
|
+
*
|
|
43
|
+
* **This function is serialized.** `@gusnips/vite/theme` writes `startTheme.toString()` into the
|
|
44
|
+
* pre-paint file, so it must not reference anything outside its own body: no imports, no helpers
|
|
45
|
+
* or constants from this module. It would still pass here and throw a `ReferenceError` in that
|
|
46
|
+
* file, before first paint. The vite package's test runs the serialized copy in a bare context
|
|
47
|
+
* to catch that. Comments go up here rather than in the body, because the body ships.
|
|
48
|
+
*
|
|
49
|
+
* **One per page.** The first call stores the controller on `window` and later calls return it,
|
|
50
|
+
* whatever options they pass. Two would both follow the OS, and the one that missed a `setMode`
|
|
51
|
+
* would repaint its stale choice on the next change. So the pre-paint script and the app must
|
|
52
|
+
* pass the same options, which is easiest when both import them from one module.
|
|
53
|
+
*
|
|
54
|
+
* **Blocked storage is not an empty key.** When storage throws, the default applies at boot, and
|
|
55
|
+
* a choice made afterwards lasts until the page is left. A resync never replaces that choice
|
|
56
|
+
* with the default, because a read that failed says nothing about what was chosen.
|
|
57
|
+
*
|
|
58
|
+
* "system" is stored as the word, not as an empty key. An empty key means the default, and the
|
|
59
|
+
* default is not always "system".
|
|
60
|
+
*/
|
|
61
|
+
export function startTheme(options) {
|
|
62
|
+
const running = window.__frontkitTheme;
|
|
63
|
+
if (running)
|
|
64
|
+
return running;
|
|
65
|
+
const { key, defaultMode = "system", attribute = "class", themeColor } = options;
|
|
66
|
+
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
|
67
|
+
const listeners = new Set();
|
|
68
|
+
const isMode = (value) => value === "light" || value === "dark" || value === "system";
|
|
69
|
+
const read = () => {
|
|
70
|
+
try {
|
|
71
|
+
const stored = window.localStorage.getItem(key);
|
|
72
|
+
return isMode(stored) ? stored : defaultMode;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const resolve = (mode) => mode === "system" ? (media.matches ? "dark" : "light") : mode;
|
|
79
|
+
const paint = (resolved) => {
|
|
80
|
+
const root = document.documentElement;
|
|
81
|
+
if (attribute === "class")
|
|
82
|
+
root.classList.toggle("dark", resolved === "dark");
|
|
83
|
+
else
|
|
84
|
+
root.setAttribute(attribute, resolved);
|
|
85
|
+
if (themeColor)
|
|
86
|
+
document
|
|
87
|
+
.querySelectorAll('meta[name="theme-color"]')
|
|
88
|
+
.forEach((meta) => meta.setAttribute("content", themeColor[resolved]));
|
|
89
|
+
};
|
|
90
|
+
const initial = read() ?? defaultMode;
|
|
91
|
+
let snapshot = { mode: initial, resolved: resolve(initial) };
|
|
92
|
+
const apply = (mode) => {
|
|
93
|
+
const resolved = resolve(mode);
|
|
94
|
+
paint(resolved);
|
|
95
|
+
if (mode === snapshot.mode && resolved === snapshot.resolved)
|
|
96
|
+
return;
|
|
97
|
+
snapshot = { mode, resolved };
|
|
98
|
+
listeners.forEach((listener) => listener());
|
|
99
|
+
};
|
|
100
|
+
const sync = () => {
|
|
101
|
+
const stored = read();
|
|
102
|
+
if (stored)
|
|
103
|
+
apply(stored);
|
|
104
|
+
};
|
|
105
|
+
paint(snapshot.resolved);
|
|
106
|
+
media.addEventListener("change", () => apply(snapshot.mode));
|
|
107
|
+
window.addEventListener("storage", (event) => {
|
|
108
|
+
if (event.key === key || event.key === null)
|
|
109
|
+
sync();
|
|
110
|
+
});
|
|
111
|
+
window.addEventListener("pageshow", sync);
|
|
112
|
+
const controller = {
|
|
113
|
+
getSnapshot: () => snapshot,
|
|
114
|
+
setMode(mode) {
|
|
115
|
+
if (!isMode(mode))
|
|
116
|
+
return;
|
|
117
|
+
try {
|
|
118
|
+
window.localStorage.setItem(key, mode);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Blocked: the choice lasts until the page is left.
|
|
122
|
+
}
|
|
123
|
+
apply(mode);
|
|
124
|
+
},
|
|
125
|
+
subscribe(listener) {
|
|
126
|
+
listeners.add(listener);
|
|
127
|
+
return () => {
|
|
128
|
+
listeners.delete(listener);
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
window.__frontkitTheme = controller;
|
|
133
|
+
return controller;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The React side of {@link startTheme}. Nothing starts at import, so a module calling this at
|
|
137
|
+
* its top level is safe to prerender. The controller starts on first use, or is found if the
|
|
138
|
+
* pre-paint script already started it.
|
|
139
|
+
*
|
|
140
|
+
* During a prerender, and while hydrating, `useTheme` answers the default. `"system"` cannot be
|
|
141
|
+
* resolved without a window, so it reads as light, as `color-scheme` does before the pre-paint
|
|
142
|
+
* script runs. React then re-renders with the real snapshot, so a toggle hydrates cleanly and
|
|
143
|
+
* shows the right choice a frame later instead of failing to hydrate.
|
|
144
|
+
*/
|
|
145
|
+
export function createTheme(options) {
|
|
146
|
+
const defaultMode = options.defaultMode ?? "system";
|
|
147
|
+
const serverSnapshot = {
|
|
148
|
+
mode: defaultMode,
|
|
149
|
+
resolved: defaultMode === "dark" ? "dark" : "light",
|
|
150
|
+
};
|
|
151
|
+
const subscribe = (listener) => startTheme(options).subscribe(listener);
|
|
152
|
+
const getSnapshot = () => typeof window === "undefined" ? serverSnapshot : startTheme(options).getSnapshot();
|
|
153
|
+
const getServerSnapshot = () => serverSnapshot;
|
|
154
|
+
const setMode = (mode) => startTheme(options).setMode(mode);
|
|
155
|
+
function useTheme() {
|
|
156
|
+
return { ...useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot), setMode };
|
|
157
|
+
}
|
|
158
|
+
return { useTheme, setMode, getSnapshot };
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=theme.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme.js","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gusnips/react",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.7",
|
|
4
4
|
"description": "The headless runtime under a Vite + React SPA: typed fetch client with single-flight refresh, auth store, query defaults, route guards, error boundary, stale-tab recovery, i18n bootstrap.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,6 +37,10 @@
|
|
|
37
37
|
"./contract": {
|
|
38
38
|
"types": "./dist/prerender-contract.d.ts",
|
|
39
39
|
"default": "./dist/prerender-contract.js"
|
|
40
|
+
},
|
|
41
|
+
"./theme": {
|
|
42
|
+
"types": "./dist/theme.d.ts",
|
|
43
|
+
"default": "./dist/theme.js"
|
|
40
44
|
}
|
|
41
45
|
},
|
|
42
46
|
"types": "./dist/index.d.ts",
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
// @gusnips/react/guards the route guards → react-router-dom
|
|
14
14
|
// @gusnips/react/hydrate hydrateOrMount → react-dom
|
|
15
15
|
// @gusnips/react/supabase the session adapter → @supabase/supabase-js
|
|
16
|
+
// @gusnips/react/theme the theme controller → react only, but `@gusnips/vite/theme`
|
|
17
|
+
// serializes it and must not load the rest
|
|
16
18
|
//
|
|
17
19
|
// `react-dom` is the one the second migration added, and it is the sharpest case yet: that
|
|
18
20
|
// adopter ships a REACT NATIVE app beside its two web apps, and React Native has no react-dom to
|