@gusnips/react 0.9.7 → 0.9.9
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 +30 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/query.d.ts +38 -0
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +8 -0
- package/dist/query.js.map +1 -1
- package/dist/theme.d.ts +5 -0
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +7 -1
- package/dist/theme.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +8 -1
- package/src/query.test.ts +69 -1
- package/src/query.ts +39 -0
- package/src/theme.test.ts +20 -0
- package/src/theme.ts +6 -1
package/README.md
CHANGED
|
@@ -178,6 +178,33 @@ saying waiting will never fix this — a cap that frees only when somebody delet
|
|
|
178
178
|
slot that frees when another job ends. That `null` is read as the claim it is, not as a field
|
|
179
179
|
somebody forgot, so the code never reaches your list.
|
|
180
180
|
|
|
181
|
+
## A failed refresh is not a failed page
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
import { queryView } from "@gusnips/react";
|
|
185
|
+
|
|
186
|
+
const view = queryView(numbers); // what useQuery returned
|
|
187
|
+
|
|
188
|
+
if (view.state === "waiting") return <Spinner />;
|
|
189
|
+
if (view.state === "failed") return <ErrorScreen error={view.error} />;
|
|
190
|
+
return <Numbers data={view.data} />;
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Don't write `isError ? <ErrorScreen /> : <Numbers />`. `isError` is also true when a refresh fails
|
|
194
|
+
in the background while the numbers are already on screen: the window gets focus back, a poll
|
|
195
|
+
runs, a save reloads the list. One dropped request then swaps a working screen for an error.
|
|
196
|
+
`queryView` shows the error only when there is nothing else to show.
|
|
197
|
+
|
|
198
|
+
When a refresh fails, the view stays `ready` and `refreshError` holds the error. Use it to say the
|
|
199
|
+
numbers may be out of date, with a button to try again.
|
|
200
|
+
|
|
201
|
+
`waiting` also covers a query that is switched off until something else loads. `isLoading` is
|
|
202
|
+
false there, so `isLoading ? <Spinner /> : <List />` shows an empty list, as if the answer were
|
|
203
|
+
"nothing".
|
|
204
|
+
|
|
205
|
+
It reads only `data` and `error`, so a test can pass `{ data: 2, error: null }`. Which button to
|
|
206
|
+
offer comes from `describeError(view.error).recover`, below.
|
|
207
|
+
|
|
181
208
|
## Survives your deploys
|
|
182
209
|
|
|
183
210
|
An app with `lazy()` routes serves chunks by hashed filename. Deploy while someone has a tab
|
|
@@ -378,6 +405,9 @@ What it does that the copies it replaced each missed at least once:
|
|
|
378
405
|
An inline one under that policy never runs, and nothing tells you.
|
|
379
406
|
- `"system"` follows the OS live, even when the person switched to it mid-page. A laptop that
|
|
380
407
|
turns dark at sunset takes the page with it.
|
|
408
|
+
- It writes to the page only when the theme changes. A page that removes `dark` for as long as
|
|
409
|
+
it is open, like a booking page that is always light, stays light until the theme really
|
|
410
|
+
changes.
|
|
381
411
|
- `themeColor: { light: "#fff", dark: "#111" }` moves `<meta name="theme-color">` too, so the
|
|
382
412
|
browser toolbar matches the page instead of the OS.
|
|
383
413
|
- `attribute: "data-theme"` writes `data-theme="dark"` instead of the `dark` class, and
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { returnPathFromLocation, safeInternalPath, type PathParts } from "./inte
|
|
|
3
3
|
export { ApiError, isAbortError, retryAfterSecs } from "./api-error.ts";
|
|
4
4
|
export { createApiClient, type ApiClient, type ApiClientOptions, type RefreshResult, type RequestOptions, type SessionAdapter, } from "./api-client.ts";
|
|
5
5
|
export { createSseParser, readSseStream, type SseFrame } from "./sse.ts";
|
|
6
|
-
export { queryDefaults, retryDelayMs, shouldRetry, type QueryDefaultsOptions } from "./query.ts";
|
|
6
|
+
export { queryDefaults, queryView, retryDelayMs, shouldRetry, type QueryDefaultsOptions, type QueryView, } from "./query.ts";
|
|
7
7
|
export { ErrorBoundary, type ErrorBoundaryFallbackProps, type ErrorBoundaryProps, } from "./error-boundary.tsx";
|
|
8
8
|
export { createErrorDescriber, humanizeWait, type DescribedError, type ErrorArm, type ErrorContext, type ErrorCopy, type ErrorDescriberOptions, type RecoveryKind, type Translate, } from "./describe-error.ts";
|
|
9
9
|
export { installPreloadErrorHandler, isChunkLoadError, isPreloadHintFailure, isStaleBuild, RELOAD_GUARD_KEY, reloadOnce, } from "./deploy-recovery.ts";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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,
|
|
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,EACL,aAAa,EACb,SAAS,EACT,YAAY,EACZ,WAAW,EACX,KAAK,oBAAoB,EACzB,KAAK,SAAS,GACf,MAAM,YAAY,CAAC;AAEpB,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
|
@@ -34,7 +34,7 @@ export { returnPathFromLocation, safeInternalPath } from "./internal-path.js";
|
|
|
34
34
|
export { ApiError, isAbortError, retryAfterSecs } from "./api-error.js";
|
|
35
35
|
export { createApiClient, } from "./api-client.js";
|
|
36
36
|
export { createSseParser, readSseStream } from "./sse.js";
|
|
37
|
-
export { queryDefaults, retryDelayMs, shouldRetry } from "./query.js";
|
|
37
|
+
export { queryDefaults, queryView, retryDelayMs, shouldRetry, } from "./query.js";
|
|
38
38
|
export { ErrorBoundary, } from "./error-boundary.js";
|
|
39
39
|
export { createErrorDescriber, humanizeWait, } from "./describe-error.js";
|
|
40
40
|
export { installPreloadErrorHandler, isChunkLoadError, isPreloadHintFailure, isStaleBuild, RELOAD_GUARD_KEY, reloadOnce, } from "./deploy-recovery.js";
|
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,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,
|
|
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,EACL,aAAa,EACb,SAAS,EACT,YAAY,EACZ,WAAW,GAGZ,MAAM,YAAY,CAAC;AAEpB,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"}
|
package/dist/query.d.ts
CHANGED
|
@@ -44,4 +44,42 @@ export declare function shouldRetry(error: unknown, durableLimitCodes?: readonly
|
|
|
44
44
|
*/
|
|
45
45
|
export declare function retryDelayMs(attemptIndex: number, error: unknown): number;
|
|
46
46
|
export declare function queryDefaults({ durableLimitCodes, maxRetries, maxRetryWaitSecs, staleTime, gcTime, refetchOnWindowFocus, }?: QueryDefaultsOptions): DefaultOptions;
|
|
47
|
+
/**
|
|
48
|
+
* What a screen should show for one query: still waiting, failed with nothing to show, or ready.
|
|
49
|
+
* When it is ready, `refreshError` says whether the last refresh failed.
|
|
50
|
+
*
|
|
51
|
+
* **Never gate a screen on `isError`.** It is also true when a BACKGROUND refetch fails over data
|
|
52
|
+
* already on screen — a window regaining focus, a poll, the invalidation after a save — so
|
|
53
|
+
* `isError ? <failure panel> : <data>` throws away what the reader was looking at over one blip.
|
|
54
|
+
* The adopters that had noticed each wrote `isLoadingError` beside a comment saying so; the ones
|
|
55
|
+
* that had not were a boundary and a great many inline ternaries. A refresh that failed is not a
|
|
56
|
+
* page that failed: the data is still real, and `refreshError` is there to say it may be stale.
|
|
57
|
+
*
|
|
58
|
+
* `waiting` is anything with no data and no error, and that includes a query switched off until
|
|
59
|
+
* something upstream answers. `isLoading` is false there, because it means `isPending &&
|
|
60
|
+
* isFetching`, which is how `isLoading ? <spinner> : <list>` shows an empty list as though the
|
|
61
|
+
* answer were "nothing".
|
|
62
|
+
*
|
|
63
|
+
* Only `data` and `error` are read, and that is react-query's own definition, not a shortcut:
|
|
64
|
+
* every success clears `error`, and a refetch after a failed first load goes back to pending. So
|
|
65
|
+
* an error beside data IS `isRefetchError`, and an error without data IS `isLoadingError`. It
|
|
66
|
+
* takes any query result, infinite ones included, and a test passes a plain object.
|
|
67
|
+
*
|
|
68
|
+
* Which control to offer is not decided here. Hand the error to the describer: its `recover` comes
|
|
69
|
+
* from the same rule the query client retries by, so the button and the retry cannot disagree.
|
|
70
|
+
*/
|
|
71
|
+
export type QueryView<T, E = unknown> = {
|
|
72
|
+
readonly state: "waiting";
|
|
73
|
+
} | {
|
|
74
|
+
readonly state: "failed";
|
|
75
|
+
readonly error: E;
|
|
76
|
+
} | {
|
|
77
|
+
readonly state: "ready";
|
|
78
|
+
readonly data: T;
|
|
79
|
+
readonly refreshError: E | null;
|
|
80
|
+
};
|
|
81
|
+
export declare function queryView<T, E>(query: {
|
|
82
|
+
readonly data: T | undefined;
|
|
83
|
+
readonly error: E | null;
|
|
84
|
+
}): QueryView<T, E>;
|
|
47
85
|
//# sourceMappingURL=query.d.ts.map
|
package/dist/query.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAoC5D,MAAM,WAAW,oBAAoB;IACnC;;;;;;;;OAQG;IACH,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,kFAAkF;IAClF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,OAAO,EACd,iBAAiB,GAAE,SAAS,MAAM,EAAO,EACzC,gBAAgB,SAA8B,GAC7C,OAAO,CAqBT;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAKzE;AAED,wBAAgB,aAAa,CAAC,EAC5B,iBAAsB,EACtB,UAAc,EACd,gBAA8C,EAC9C,SAAyB,EACzB,MAAuB,EACvB,oBAA4B,GAC7B,GAAE,oBAAyB,GAAG,cAAc,CAe5C"}
|
|
1
|
+
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAoC5D,MAAM,WAAW,oBAAoB;IACnC;;;;;;;;OAQG;IACH,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,kFAAkF;IAClF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,OAAO,EACd,iBAAiB,GAAE,SAAS,MAAM,EAAO,EACzC,gBAAgB,SAA8B,GAC7C,OAAO,CAqBT;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAKzE;AAED,wBAAgB,aAAa,CAAC,EAC5B,iBAAsB,EACtB,UAAc,EACd,gBAA8C,EAC9C,SAAyB,EACzB,MAAuB,EACvB,oBAA4B,GAC7B,GAAE,oBAAyB,GAAG,cAAc,CAe5C;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,EAAE,CAAC,GAAG,OAAO,IAChC;IAAE,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;CAAE,GAC7B;IAAE,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAC/C;IAAE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IAAC,QAAQ,CAAC,YAAY,EAAE,CAAC,GAAG,IAAI,CAAA;CAAE,CAAC;AAEnF,wBAAgB,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE;IACrC,QAAQ,CAAC,IAAI,EAAE,CAAC,GAAG,SAAS,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;CAC1B,GAAG,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,CAKlB"}
|
package/dist/query.js
CHANGED
|
@@ -85,4 +85,12 @@ export function queryDefaults({ durableLimitCodes = [], maxRetries = 2, maxRetry
|
|
|
85
85
|
mutations: { retry: false },
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
|
+
export function queryView(query) {
|
|
89
|
+
const { data, error } = query;
|
|
90
|
+
if (data !== undefined)
|
|
91
|
+
return { state: "ready", data, refreshError: error };
|
|
92
|
+
if (error !== null)
|
|
93
|
+
return { state: "failed", error };
|
|
94
|
+
return { state: "waiting" };
|
|
95
|
+
}
|
|
88
96
|
//# sourceMappingURL=query.js.map
|
package/dist/query.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query.js","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAE7E;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,6FAA6F;AAC7F,MAAM,uBAAuB,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;AAEpD,MAAM,2BAA2B,GAAG,EAAE,CAAC;AAkCvC;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CACzB,KAAc,EACd,oBAAuC,EAAE,EACzC,gBAAgB,GAAG,2BAA2B;IAE9C,wFAAwF;IACxF,wEAAwE;IACxE,IAAI,CAAC,CAAC,KAAK,YAAY,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IAE9C,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,IAAI,iBAAiB,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAErF,6FAA6F;IAC7F,8FAA8F;IAC9F,8FAA8F;IAC9F,4FAA4F;IAC5F,gBAAgB;IAChB,IAAI,iBAAiB,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAE3C,4FAA4F;IAC5F,4FAA4F;IAC5F,MAAM,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IACnC,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,GAAG,gBAAgB;QAAE,OAAO,KAAK,CAAC;IAE3D,IAAI,KAAK,CAAC,MAAM,IAAI,GAAG;QAAE,OAAO,IAAI,CAAC;IACrC,OAAO,uBAAuB,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;AACnD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,YAAoB,EAAE,KAAc;IAC/D,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,IAAI,YAAY,EAAE,MAAM,CAAC,CAAC;IAC3D,IAAI,CAAC,CAAC,KAAK,YAAY,QAAQ,CAAC;QAAE,OAAO,OAAO,CAAC;IACjD,MAAM,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IACnC,OAAO,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,GAAG,IAAI,CAAC,CAAC;AAClE,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,EAC5B,iBAAiB,GAAG,EAAE,EACtB,UAAU,GAAG,CAAC,EACd,gBAAgB,GAAG,2BAA2B,EAC9C,SAAS,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,EACzB,MAAM,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,EACvB,oBAAoB,GAAG,KAAK,MACJ,EAAE;IAC1B,OAAO;QACL,OAAO,EAAE;YACP,SAAS;YACT,MAAM;YACN,KAAK,EAAE,CAAC,YAAY,EAAE,KAAK,EAAE,EAAE,CAC7B,YAAY,GAAG,UAAU,IAAI,WAAW,CAAC,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,CAAC;YACtF,UAAU,EAAE,YAAY;YACxB,oBAAoB;YACpB,kBAAkB,EAAE,IAAI;SACzB;QACD,2FAA2F;QAC3F,yFAAyF;QACzF,SAAS,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE;KAC5B,CAAC;AACJ,CAAC"}
|
|
1
|
+
{"version":3,"file":"query.js","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAE7E;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,6FAA6F;AAC7F,MAAM,uBAAuB,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;AAEpD,MAAM,2BAA2B,GAAG,EAAE,CAAC;AAkCvC;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CACzB,KAAc,EACd,oBAAuC,EAAE,EACzC,gBAAgB,GAAG,2BAA2B;IAE9C,wFAAwF;IACxF,wEAAwE;IACxE,IAAI,CAAC,CAAC,KAAK,YAAY,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IAE9C,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,IAAI,iBAAiB,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAErF,6FAA6F;IAC7F,8FAA8F;IAC9F,8FAA8F;IAC9F,4FAA4F;IAC5F,gBAAgB;IAChB,IAAI,iBAAiB,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAE3C,4FAA4F;IAC5F,4FAA4F;IAC5F,MAAM,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IACnC,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,GAAG,gBAAgB;QAAE,OAAO,KAAK,CAAC;IAE3D,IAAI,KAAK,CAAC,MAAM,IAAI,GAAG;QAAE,OAAO,IAAI,CAAC;IACrC,OAAO,uBAAuB,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;AACnD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,YAAoB,EAAE,KAAc;IAC/D,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,IAAI,YAAY,EAAE,MAAM,CAAC,CAAC;IAC3D,IAAI,CAAC,CAAC,KAAK,YAAY,QAAQ,CAAC;QAAE,OAAO,OAAO,CAAC;IACjD,MAAM,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IACnC,OAAO,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,GAAG,IAAI,CAAC,CAAC;AAClE,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,EAC5B,iBAAiB,GAAG,EAAE,EACtB,UAAU,GAAG,CAAC,EACd,gBAAgB,GAAG,2BAA2B,EAC9C,SAAS,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,EACzB,MAAM,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,EACvB,oBAAoB,GAAG,KAAK,MACJ,EAAE;IAC1B,OAAO;QACL,OAAO,EAAE;YACP,SAAS;YACT,MAAM;YACN,KAAK,EAAE,CAAC,YAAY,EAAE,KAAK,EAAE,EAAE,CAC7B,YAAY,GAAG,UAAU,IAAI,WAAW,CAAC,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,CAAC;YACtF,UAAU,EAAE,YAAY;YACxB,oBAAoB;YACpB,kBAAkB,EAAE,IAAI;SACzB;QACD,2FAA2F;QAC3F,yFAAyF;QACzF,SAAS,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE;KAC5B,CAAC;AACJ,CAAC;AA+BD,MAAM,UAAU,SAAS,CAAO,KAG/B;IACC,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC;IAC9B,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC;IAC7E,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IACtD,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;AAC9B,CAAC"}
|
package/dist/theme.d.ts
CHANGED
|
@@ -64,6 +64,11 @@ declare global {
|
|
|
64
64
|
*
|
|
65
65
|
* "system" is stored as the word, not as an empty key. An empty key means the default, and the
|
|
66
66
|
* default is not always "system".
|
|
67
|
+
*
|
|
68
|
+
* **It writes to the page when the theme changes, and at no other time.** A page may override
|
|
69
|
+
* the class for its own lifetime — a public booking page that is always light — and keeps that
|
|
70
|
+
* override until the theme really changes. Repainting on every resync would undo it a moment
|
|
71
|
+
* later, because `pageshow` fires on every load, not only on a back-forward restore.
|
|
67
72
|
*/
|
|
68
73
|
export declare function startTheme(options: ThemeOptions): ThemeController;
|
|
69
74
|
export interface Theme {
|
package/dist/theme.d.ts.map
CHANGED
|
@@ -1 +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
|
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;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
CHANGED
|
@@ -57,6 +57,11 @@ import { useSyncExternalStore } from "react";
|
|
|
57
57
|
*
|
|
58
58
|
* "system" is stored as the word, not as an empty key. An empty key means the default, and the
|
|
59
59
|
* default is not always "system".
|
|
60
|
+
*
|
|
61
|
+
* **It writes to the page when the theme changes, and at no other time.** A page may override
|
|
62
|
+
* the class for its own lifetime — a public booking page that is always light — and keeps that
|
|
63
|
+
* override until the theme really changes. Repainting on every resync would undo it a moment
|
|
64
|
+
* later, because `pageshow` fires on every load, not only on a back-forward restore.
|
|
60
65
|
*/
|
|
61
66
|
export function startTheme(options) {
|
|
62
67
|
const running = window.__frontkitTheme;
|
|
@@ -91,7 +96,8 @@ export function startTheme(options) {
|
|
|
91
96
|
let snapshot = { mode: initial, resolved: resolve(initial) };
|
|
92
97
|
const apply = (mode) => {
|
|
93
98
|
const resolved = resolve(mode);
|
|
94
|
-
|
|
99
|
+
if (resolved !== snapshot.resolved)
|
|
100
|
+
paint(resolved);
|
|
95
101
|
if (mode === snapshot.mode && resolved === snapshot.resolved)
|
|
96
102
|
return;
|
|
97
103
|
snapshot = { mode, resolved };
|
package/dist/theme.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme.js","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAoD7C
|
|
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.9",
|
|
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",
|
package/src/index.ts
CHANGED
|
@@ -44,7 +44,14 @@ export {
|
|
|
44
44
|
} from "./api-client.ts";
|
|
45
45
|
export { createSseParser, readSseStream, type SseFrame } from "./sse.ts";
|
|
46
46
|
|
|
47
|
-
export {
|
|
47
|
+
export {
|
|
48
|
+
queryDefaults,
|
|
49
|
+
queryView,
|
|
50
|
+
retryDelayMs,
|
|
51
|
+
shouldRetry,
|
|
52
|
+
type QueryDefaultsOptions,
|
|
53
|
+
type QueryView,
|
|
54
|
+
} from "./query.ts";
|
|
48
55
|
|
|
49
56
|
export {
|
|
50
57
|
ErrorBoundary,
|
package/src/query.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { QueryClient, QueryObserver, type QueryObserverResult } from "@tanstack/react-query";
|
|
1
2
|
import { describe, expect, it } from "vitest";
|
|
2
3
|
import { ApiError } from "./api-error.ts";
|
|
3
|
-
import { retryDelayMs, shouldRetry } from "./query.ts";
|
|
4
|
+
import { queryView, retryDelayMs, shouldRetry } from "./query.ts";
|
|
4
5
|
|
|
5
6
|
const refusal = (status: number, code?: string) =>
|
|
6
7
|
new ApiError(status, code ? { code, message: code } : null);
|
|
@@ -144,3 +145,70 @@ describe("retryDelayMs", () => {
|
|
|
144
145
|
expect(retryDelayMs(0, new TypeError("Failed to fetch"))).toBe(1000);
|
|
145
146
|
});
|
|
146
147
|
});
|
|
148
|
+
|
|
149
|
+
describe("queryView", () => {
|
|
150
|
+
/**
|
|
151
|
+
* One real query, taken through every state a screen meets, with `queryView` checked against
|
|
152
|
+
* react-query's own flags at each step. A plain object would only check this file against
|
|
153
|
+
* itself; the observer is what says the reading of `data` and `error` is react-query's.
|
|
154
|
+
*/
|
|
155
|
+
it("agrees with react-query's own flags through a query's whole life", async () => {
|
|
156
|
+
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
157
|
+
let answer: () => number = () => {
|
|
158
|
+
throw new Error("down");
|
|
159
|
+
};
|
|
160
|
+
const observer = new QueryObserver(client, {
|
|
161
|
+
queryKey: ["numbers"],
|
|
162
|
+
queryFn: async () => answer(),
|
|
163
|
+
enabled: false,
|
|
164
|
+
});
|
|
165
|
+
// Subscribed, as a mounted `useQuery` is. An observer nobody listens to never hears the query
|
|
166
|
+
// change, and reads the last result it computed.
|
|
167
|
+
const unsubscribe = observer.subscribe(() => {});
|
|
168
|
+
const now = () => {
|
|
169
|
+
const result: QueryObserverResult<number, Error> = observer.getCurrentResult();
|
|
170
|
+
const view = queryView(result);
|
|
171
|
+
expect(view.state === "failed").toBe(result.isLoadingError);
|
|
172
|
+
expect(view.state === "ready" && view.refreshError !== null).toBe(result.isRefetchError);
|
|
173
|
+
return { result, view };
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Switched off until something upstream answers: pending, but not loading.
|
|
177
|
+
const off = now();
|
|
178
|
+
expect(off.result.isLoading).toBe(false);
|
|
179
|
+
expect(off.view).toEqual({ state: "waiting" });
|
|
180
|
+
|
|
181
|
+
await observer.refetch();
|
|
182
|
+
expect(now().view.state).toBe("failed");
|
|
183
|
+
|
|
184
|
+
// Retrying a failed first load goes back to pending, so the reader sees it working.
|
|
185
|
+
answer = () => 2;
|
|
186
|
+
const retry = observer.refetch();
|
|
187
|
+
expect(now().view.state).toBe("waiting");
|
|
188
|
+
await retry;
|
|
189
|
+
expect(now().view).toEqual({ state: "ready", data: 2, refreshError: null });
|
|
190
|
+
|
|
191
|
+
// The case this exists for: a refresh fails over data already on screen.
|
|
192
|
+
answer = () => {
|
|
193
|
+
throw new Error("blip");
|
|
194
|
+
};
|
|
195
|
+
await observer.refetch();
|
|
196
|
+
const stale = now();
|
|
197
|
+
expect(stale.result.isError).toBe(true);
|
|
198
|
+
expect(stale.view).toMatchObject({ state: "ready", data: 2 });
|
|
199
|
+
expect(stale.view.state === "ready" && stale.view.refreshError?.message).toBe("blip");
|
|
200
|
+
|
|
201
|
+
answer = () => 3;
|
|
202
|
+
await observer.refetch();
|
|
203
|
+
expect(now().view).toEqual({ state: "ready", data: 3, refreshError: null });
|
|
204
|
+
unsubscribe();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("treats null as an answer, not as waiting", () => {
|
|
208
|
+
expect(queryView({ data: null, error: null })).toEqual({
|
|
209
|
+
state: "ready",
|
|
210
|
+
data: null,
|
|
211
|
+
refreshError: null,
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|
package/src/query.ts
CHANGED
|
@@ -134,3 +134,42 @@ export function queryDefaults({
|
|
|
134
134
|
mutations: { retry: false },
|
|
135
135
|
};
|
|
136
136
|
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* What a screen should show for one query: still waiting, failed with nothing to show, or ready.
|
|
140
|
+
* When it is ready, `refreshError` says whether the last refresh failed.
|
|
141
|
+
*
|
|
142
|
+
* **Never gate a screen on `isError`.** It is also true when a BACKGROUND refetch fails over data
|
|
143
|
+
* already on screen — a window regaining focus, a poll, the invalidation after a save — so
|
|
144
|
+
* `isError ? <failure panel> : <data>` throws away what the reader was looking at over one blip.
|
|
145
|
+
* The adopters that had noticed each wrote `isLoadingError` beside a comment saying so; the ones
|
|
146
|
+
* that had not were a boundary and a great many inline ternaries. A refresh that failed is not a
|
|
147
|
+
* page that failed: the data is still real, and `refreshError` is there to say it may be stale.
|
|
148
|
+
*
|
|
149
|
+
* `waiting` is anything with no data and no error, and that includes a query switched off until
|
|
150
|
+
* something upstream answers. `isLoading` is false there, because it means `isPending &&
|
|
151
|
+
* isFetching`, which is how `isLoading ? <spinner> : <list>` shows an empty list as though the
|
|
152
|
+
* answer were "nothing".
|
|
153
|
+
*
|
|
154
|
+
* Only `data` and `error` are read, and that is react-query's own definition, not a shortcut:
|
|
155
|
+
* every success clears `error`, and a refetch after a failed first load goes back to pending. So
|
|
156
|
+
* an error beside data IS `isRefetchError`, and an error without data IS `isLoadingError`. It
|
|
157
|
+
* takes any query result, infinite ones included, and a test passes a plain object.
|
|
158
|
+
*
|
|
159
|
+
* Which control to offer is not decided here. Hand the error to the describer: its `recover` comes
|
|
160
|
+
* from the same rule the query client retries by, so the button and the retry cannot disagree.
|
|
161
|
+
*/
|
|
162
|
+
export type QueryView<T, E = unknown> =
|
|
163
|
+
| { readonly state: "waiting" }
|
|
164
|
+
| { readonly state: "failed"; readonly error: E }
|
|
165
|
+
| { readonly state: "ready"; readonly data: T; readonly refreshError: E | null };
|
|
166
|
+
|
|
167
|
+
export function queryView<T, E>(query: {
|
|
168
|
+
readonly data: T | undefined;
|
|
169
|
+
readonly error: E | null;
|
|
170
|
+
}): QueryView<T, E> {
|
|
171
|
+
const { data, error } = query;
|
|
172
|
+
if (data !== undefined) return { state: "ready", data, refreshError: error };
|
|
173
|
+
if (error !== null) return { state: "failed", error };
|
|
174
|
+
return { state: "waiting" };
|
|
175
|
+
}
|
package/src/theme.test.ts
CHANGED
|
@@ -78,6 +78,8 @@ function browser({
|
|
|
78
78
|
storageEvent: (key: string | null) => fire("storage", { key }),
|
|
79
79
|
backForwardRestore: () => fire("pageshow"),
|
|
80
80
|
block: () => (state.blocked = true),
|
|
81
|
+
/** A page that forces light for its own lifetime, the way a public booking page does. */
|
|
82
|
+
forceLight: () => classes.delete("dark"),
|
|
81
83
|
};
|
|
82
84
|
}
|
|
83
85
|
|
|
@@ -213,6 +215,24 @@ describe("startTheme", () => {
|
|
|
213
215
|
expect(listener).not.toHaveBeenCalled();
|
|
214
216
|
});
|
|
215
217
|
|
|
218
|
+
// `pageshow` fires on every load, not only on a back-forward restore, so a controller that
|
|
219
|
+
// repainted on every resync would undo a page's override a moment after the page set it.
|
|
220
|
+
it("writes to the page only when the theme changes, so a page can override it", () => {
|
|
221
|
+
const page = browser({ stored: "dark" });
|
|
222
|
+
const theme = startTheme(options);
|
|
223
|
+
page.forceLight();
|
|
224
|
+
|
|
225
|
+
page.backForwardRestore();
|
|
226
|
+
page.storageEvent(KEY);
|
|
227
|
+
page.osSwitches(true);
|
|
228
|
+
theme.setMode("dark");
|
|
229
|
+
expect(page.dark()).toBe(false);
|
|
230
|
+
|
|
231
|
+
page.otherTabPicks("light");
|
|
232
|
+
page.otherTabPicks("dark");
|
|
233
|
+
expect(page.dark()).toBe(true);
|
|
234
|
+
});
|
|
235
|
+
|
|
216
236
|
it("writes data-theme both ways when asked to", () => {
|
|
217
237
|
const page = browser({ stored: "dark" });
|
|
218
238
|
const theme = startTheme({ key: KEY, attribute: "data-theme" });
|
package/src/theme.ts
CHANGED
|
@@ -108,6 +108,11 @@ declare global {
|
|
|
108
108
|
*
|
|
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
|
+
*
|
|
112
|
+
* **It writes to the page when the theme changes, and at no other time.** A page may override
|
|
113
|
+
* the class for its own lifetime — a public booking page that is always light — and keeps that
|
|
114
|
+
* override until the theme really changes. Repainting on every resync would undo it a moment
|
|
115
|
+
* later, because `pageshow` fires on every load, not only on a back-forward restore.
|
|
111
116
|
*/
|
|
112
117
|
export function startTheme(options: ThemeOptions): ThemeController {
|
|
113
118
|
const running = window.__frontkitTheme;
|
|
@@ -142,7 +147,7 @@ export function startTheme(options: ThemeOptions): ThemeController {
|
|
|
142
147
|
let snapshot: ThemeSnapshot = { mode: initial, resolved: resolve(initial) };
|
|
143
148
|
const apply = (mode: ThemeMode) => {
|
|
144
149
|
const resolved = resolve(mode);
|
|
145
|
-
paint(resolved);
|
|
150
|
+
if (resolved !== snapshot.resolved) paint(resolved);
|
|
146
151
|
if (mode === snapshot.mode && resolved === snapshot.resolved) return;
|
|
147
152
|
snapshot = { mode, resolved };
|
|
148
153
|
listeners.forEach((listener) => listener());
|