@kingsleyweb/bun-ui 0.0.0-stage → 3.0.0-rc.2
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/LICENSE +21 -0
- package/README.md +80 -2
- package/app/docs/App.tsx +72 -0
- package/app/docs/api/client.ts +303 -0
- package/app/docs/api/contract.ts +24 -0
- package/app/docs/api/docs.ts +38 -0
- package/app/docs/api/errors.ts +144 -0
- package/app/docs/boot.tsx +58 -0
- package/app/docs/components/Badge.tsx +65 -0
- package/app/docs/components/Button.tsx +37 -0
- package/app/docs/components/Card.tsx +41 -0
- package/app/docs/components/ConfirmDialog.tsx +173 -0
- package/app/docs/components/CopyButton.tsx +68 -0
- package/app/docs/components/Dialog.tsx +230 -0
- package/app/docs/components/EmptyState.tsx +34 -0
- package/app/docs/components/ErrorView.tsx +70 -0
- package/app/docs/components/Field.tsx +93 -0
- package/app/docs/components/JsonEditor.tsx +151 -0
- package/app/docs/components/JsonView.tsx +338 -0
- package/app/docs/components/KeyValue.tsx +63 -0
- package/app/docs/components/ProblemBanner.tsx +116 -0
- package/app/docs/components/ScreenErrorBoundary.tsx +141 -0
- package/app/docs/components/Spinner.tsx +33 -0
- package/app/docs/components/Table.tsx +33 -0
- package/app/docs/components/Tabs.tsx +226 -0
- package/app/docs/components/classNames.ts +4 -0
- package/app/docs/components/components.css +691 -0
- package/app/docs/components/copy.ts +89 -0
- package/app/docs/components/fieldContext.ts +29 -0
- package/app/docs/components/inputValues.ts +142 -0
- package/app/docs/components/inputs.tsx +387 -0
- package/app/docs/components/jsonParse.ts +338 -0
- package/app/docs/components/listKeys.ts +30 -0
- package/app/docs/config.ts +86 -0
- package/app/docs/context.ts +33 -0
- package/app/docs/embed.ts +74 -0
- package/app/docs/env.d.ts +2 -0
- package/app/docs/format.ts +9 -0
- package/app/docs/hooks/useUrlTab.ts +16 -0
- package/app/docs/main.tsx +11 -0
- package/app/docs/queryClient.ts +54 -0
- package/app/docs/router.tsx +194 -0
- package/app/docs/routing.ts +194 -0
- package/app/docs/screens/docs/document.ts +27 -0
- package/app/docs/screens/docs/http/OperationView.tsx +407 -0
- package/app/docs/screens/docs/http/Sidebar.tsx +190 -0
- package/app/docs/screens/docs/http/TryItPanel.tsx +603 -0
- package/app/docs/screens/docs/http/http.css +405 -0
- package/app/docs/screens/docs/http/index.tsx +395 -0
- package/app/docs/screens/docs/http/model.ts +604 -0
- package/app/docs/screens/docs/http/tryIt.ts +455 -0
- package/app/docs/screens/docs/permissions.ts +6 -0
- package/app/docs/screens/docs/schema/Prose.tsx +43 -0
- package/app/docs/screens/docs/schema/SchemaTree.tsx +319 -0
- package/app/docs/screens/docs/schema/index.ts +30 -0
- package/app/docs/screens/docs/schema/resolve.ts +466 -0
- package/app/docs/screens/docs/schema/schema.css +137 -0
- package/app/docs/screens/docs/schema/skeleton.ts +92 -0
- package/app/docs/screens/docs/viewer.tsx +154 -0
- package/app/docs/screens/docs/ws/InlineText.tsx +25 -0
- package/app/docs/screens/docs/ws/PayloadSchema.tsx +39 -0
- package/app/docs/screens/docs/ws/format.ts +82 -0
- package/app/docs/screens/docs/ws/index.tsx +286 -0
- package/app/docs/screens/docs/ws/items.tsx +786 -0
- package/app/docs/screens/docs/ws/model.ts +1178 -0
- package/app/docs/screens/docs/ws/panels.tsx +440 -0
- package/app/docs/screens/docs/ws/ws.css +231 -0
- package/app/docs/shortcuts.ts +94 -0
- package/app/docs/styles/base.css +847 -0
- package/app/docs/styles/tokens.css +143 -0
- package/app/docs/theme.ts +56 -0
- package/app/kit/unauthorized.ts +222 -0
- package/dist/docs/assets/main-0rchxtv5.js +18 -0
- package/dist/docs/assets/main-0rchxtv5.js.map +99 -0
- package/dist/docs/assets/main-cqgyvs86.css +1 -0
- package/dist/docs/manifest.json +26 -0
- package/dist/jobs/assets/AddFlowDialog-h6xkgx9b.js +6 -0
- package/dist/jobs/assets/AddFlowDialog-h6xkgx9b.js.map +11 -0
- package/dist/jobs/assets/AddFlowDialog-n7gtc84v.css +1 -0
- package/dist/jobs/assets/added-cbwsxv3e.js +4 -0
- package/dist/jobs/assets/added-cbwsxv3e.js.map +11 -0
- package/dist/jobs/assets/bodies-yyw5hmrp.js +6 -0
- package/dist/jobs/assets/bodies-yyw5hmrp.js.map +10 -0
- package/dist/jobs/assets/channel-tgkvqm8y.js +4 -0
- package/dist/jobs/assets/channel-tgkvqm8y.js.map +10 -0
- package/dist/jobs/assets/channelChecks-s4yz4yhw.js +4 -0
- package/dist/jobs/assets/channelChecks-s4yz4yhw.js.map +10 -0
- package/dist/jobs/assets/index-02427smp.css +1 -0
- package/dist/jobs/assets/index-09z2cjft.js +4 -0
- package/dist/jobs/assets/index-09z2cjft.js.map +16 -0
- package/dist/jobs/assets/index-18bn0gvy.css +1 -0
- package/dist/jobs/assets/index-47pxmyxn.js +4 -0
- package/dist/jobs/assets/index-47pxmyxn.js.map +10 -0
- package/dist/jobs/assets/index-4e55z986.js +4 -0
- package/dist/jobs/assets/index-4e55z986.js.map +11 -0
- package/dist/jobs/assets/index-5c933424.js +4 -0
- package/dist/jobs/assets/index-5c933424.js.map +18 -0
- package/dist/jobs/assets/index-5kbyqspm.css +1 -0
- package/dist/jobs/assets/index-5ng37hk6.css +1 -0
- package/dist/jobs/assets/index-5w8w7fvc.css +1 -0
- package/dist/jobs/assets/index-71qs5qyx.css +1 -0
- package/dist/jobs/assets/index-btpg4r8b.css +1 -0
- package/dist/jobs/assets/index-d5r5n0mm.js +4 -0
- package/dist/jobs/assets/index-d5r5n0mm.js.map +23 -0
- package/dist/jobs/assets/index-e0yy2skq.css +1 -0
- package/dist/jobs/assets/index-hs60b25v.js +10 -0
- package/dist/jobs/assets/index-hs60b25v.js.map +62 -0
- package/dist/jobs/assets/index-k2smrr70.js +4 -0
- package/dist/jobs/assets/index-k2smrr70.js.map +9 -0
- package/dist/jobs/assets/index-q24jw3w9.js +4 -0
- package/dist/jobs/assets/index-q24jw3w9.js.map +32 -0
- package/dist/jobs/assets/index-qfb80xen.css +1 -0
- package/dist/jobs/assets/index-rtynx6wj.js +4 -0
- package/dist/jobs/assets/index-rtynx6wj.js.map +10 -0
- package/dist/jobs/assets/live-6k7s6358.js +4 -0
- package/dist/jobs/assets/live-6k7s6358.js.map +10 -0
- package/dist/jobs/assets/main-0jmeh6mq.js +6 -0
- package/dist/jobs/assets/main-0jmeh6mq.js.map +12 -0
- package/dist/jobs/assets/main-3yj6bv90.js +5 -0
- package/dist/jobs/assets/main-3yj6bv90.js.map +10 -0
- package/dist/jobs/assets/main-4g69fn3x.js +5 -0
- package/dist/jobs/assets/main-4g69fn3x.js.map +10 -0
- package/dist/jobs/assets/main-4gte3eqq.js +5 -0
- package/dist/jobs/assets/main-4gte3eqq.js.map +10 -0
- package/dist/jobs/assets/main-4j1fcevh.js +5 -0
- package/dist/jobs/assets/main-4j1fcevh.js.map +10 -0
- package/dist/jobs/assets/main-54qwkz28.css +1 -0
- package/dist/jobs/assets/main-5grjvsy7.js +5 -0
- package/dist/jobs/assets/main-5grjvsy7.js.map +11 -0
- package/dist/jobs/assets/main-5zbx60gq.js +5 -0
- package/dist/jobs/assets/main-5zbx60gq.js.map +11 -0
- package/dist/jobs/assets/main-9prgjhg4.js +5 -0
- package/dist/jobs/assets/main-9prgjhg4.js.map +10 -0
- package/dist/jobs/assets/main-a250ffk7.js +17 -0
- package/dist/jobs/assets/main-a250ffk7.js.map +115 -0
- package/dist/jobs/assets/main-d6ge7w08.js +5 -0
- package/dist/jobs/assets/main-d6ge7w08.js.map +10 -0
- package/dist/jobs/assets/main-gngzht43.js +5 -0
- package/dist/jobs/assets/main-gngzht43.js.map +10 -0
- package/dist/jobs/assets/main-hg0hrg8j.js +5 -0
- package/dist/jobs/assets/main-hg0hrg8j.js.map +10 -0
- package/dist/jobs/assets/main-hm1ng3yg.js +5 -0
- package/dist/jobs/assets/main-hm1ng3yg.js.map +12 -0
- package/dist/jobs/assets/main-jkvd92z9.js +5 -0
- package/dist/jobs/assets/main-jkvd92z9.js.map +10 -0
- package/dist/jobs/assets/main-jv5xqd13.js +5 -0
- package/dist/jobs/assets/main-jv5xqd13.js.map +21 -0
- package/dist/jobs/assets/main-jxre7w93.js +5 -0
- package/dist/jobs/assets/main-jxre7w93.js.map +23 -0
- package/dist/jobs/assets/main-kwmqkjfx.js +5 -0
- package/dist/jobs/assets/main-kwmqkjfx.js.map +11 -0
- package/dist/jobs/assets/main-n9cf6bxm.js +5 -0
- package/dist/jobs/assets/main-n9cf6bxm.js.map +11 -0
- package/dist/jobs/assets/main-pegp0ygb.js +5 -0
- package/dist/jobs/assets/main-pegp0ygb.js.map +10 -0
- package/dist/jobs/assets/main-qr573853.js +5 -0
- package/dist/jobs/assets/main-qr573853.js.map +12 -0
- package/dist/jobs/assets/main-rq97rdr4.js +4 -0
- package/dist/jobs/assets/main-rq97rdr4.js.map +22 -0
- package/dist/jobs/assets/main-szjdmydc.js +5 -0
- package/dist/jobs/assets/main-szjdmydc.js.map +13 -0
- package/dist/jobs/assets/main-tnbhrm9n.js +7 -0
- package/dist/jobs/assets/main-tnbhrm9n.js.map +11 -0
- package/dist/jobs/assets/main-ts19j4n0.js +5 -0
- package/dist/jobs/assets/main-ts19j4n0.js.map +25 -0
- package/dist/jobs/assets/main-w93x1dzs.js +5 -0
- package/dist/jobs/assets/main-w93x1dzs.js.map +10 -0
- package/dist/jobs/assets/sections-0ksdxs8m.css +1 -0
- package/dist/jobs/assets/sections-0y7w4fb4.js +4 -0
- package/dist/jobs/assets/sections-0y7w4fb4.js.map +10 -0
- package/dist/jobs/assets/sse-mn0t0fh7.js +5 -0
- package/dist/jobs/assets/sse-mn0t0fh7.js.map +11 -0
- package/dist/jobs/assets/sseChecks-t7tcm7ra.js +4 -0
- package/dist/jobs/assets/sseChecks-t7tcm7ra.js.map +10 -0
- package/dist/jobs/assets/validation-ysjfy072.js +4 -0
- package/dist/jobs/assets/validation-ysjfy072.js.map +9 -0
- package/dist/jobs/manifest.json +511 -0
- package/dts/docs/assets.d.ts +144 -0
- package/dts/docs/assets.d.ts.map +1 -0
- package/dts/docs/browser/config.d.ts +61 -0
- package/dts/docs/browser/config.d.ts.map +1 -0
- package/dts/docs/browser/index.d.ts +12 -0
- package/dts/docs/browser/index.d.ts.map +1 -0
- package/dts/docs/docsUi.d.ts +42 -0
- package/dts/docs/docsUi.d.ts.map +1 -0
- package/dts/docs/hosted.d.ts +22 -0
- package/dts/docs/hosted.d.ts.map +1 -0
- package/dts/docs/index.d.ts +18 -0
- package/dts/docs/index.d.ts.map +1 -0
- package/dts/docs/shell.d.ts +57 -0
- package/dts/docs/shell.d.ts.map +1 -0
- package/dts/docs/types.d.ts +137 -0
- package/dts/docs/types.d.ts.map +1 -0
- package/dts/index.d.ts +21 -0
- package/dts/index.d.ts.map +1 -0
- package/dts/jobs/assets.d.ts +157 -0
- package/dts/jobs/assets.d.ts.map +1 -0
- package/dts/jobs/browser/config.d.ts +67 -0
- package/dts/jobs/browser/config.d.ts.map +1 -0
- package/dts/jobs/browser/index.d.ts +10 -0
- package/dts/jobs/browser/index.d.ts.map +1 -0
- package/dts/jobs/browser/unauthorized.d.ts +9 -0
- package/dts/jobs/browser/unauthorized.d.ts.map +1 -0
- package/dts/jobs/index.d.ts +24 -0
- package/dts/jobs/index.d.ts.map +1 -0
- package/dts/jobs/jobsUi.d.ts +63 -0
- package/dts/jobs/jobsUi.d.ts.map +1 -0
- package/dts/jobs/shell.d.ts +77 -0
- package/dts/jobs/shell.d.ts.map +1 -0
- package/dts/jobs/types.d.ts +146 -0
- package/dts/jobs/types.d.ts.map +1 -0
- package/dts/kit/browser/shared.d.ts +10 -0
- package/dts/kit/browser/shared.d.ts.map +1 -0
- package/dts/kit/browser/unauthorized.d.ts +74 -0
- package/dts/kit/browser/unauthorized.d.ts.map +1 -0
- package/dts/kit/unauthorized.d.ts +47 -0
- package/dts/kit/unauthorized.d.ts.map +1 -0
- package/guides/docs.md +170 -0
- package/guides/jobs.md +1120 -0
- package/lib/docs/assets.ts +427 -0
- package/lib/docs/browser/config.ts +66 -0
- package/lib/docs/browser/index.ts +24 -0
- package/lib/docs/docsUi.ts +623 -0
- package/lib/docs/hosted.ts +31 -0
- package/lib/docs/index.ts +37 -0
- package/lib/docs/shell.ts +156 -0
- package/lib/docs/types.ts +149 -0
- package/lib/index.ts +27 -0
- package/lib/jobs/assets.ts +448 -0
- package/lib/jobs/browser/config.ts +65 -0
- package/lib/jobs/browser/index.ts +9 -0
- package/lib/jobs/browser/unauthorized.ts +11 -0
- package/lib/jobs/index.ts +41 -0
- package/lib/jobs/jobsUi.ts +709 -0
- package/lib/jobs/shell.ts +205 -0
- package/lib/jobs/types.ts +162 -0
- package/lib/kit/browser/shared.ts +11 -0
- package/lib/kit/browser/unauthorized.ts +114 -0
- package/lib/kit/unauthorized.ts +222 -0
- package/package.json +198 -4
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Design tokens. Light is the default; dark applies when the OS prefers it
|
|
3
|
+
* and no theme is forced (`data-theme` absent = "system"), or when
|
|
4
|
+
* `data-theme="dark"` forces it. `data-theme="light"` forces light.
|
|
5
|
+
*
|
|
6
|
+
* Every text/background and control/background pair the app uses is checked
|
|
7
|
+
* for WCAG AA contrast, in both themes, by `__tests__/app/a11y/contrast.test.ts`
|
|
8
|
+
* (parsed from this file): change a colour and that test says whether it
|
|
9
|
+
* still reads.
|
|
10
|
+
*/
|
|
11
|
+
:root {
|
|
12
|
+
color-scheme: light;
|
|
13
|
+
|
|
14
|
+
--font-sans:
|
|
15
|
+
system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial,
|
|
16
|
+
sans-serif;
|
|
17
|
+
--font-mono:
|
|
18
|
+
ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
|
19
|
+
|
|
20
|
+
--space-1: 4px;
|
|
21
|
+
--space-2: 8px;
|
|
22
|
+
--space-3: 12px;
|
|
23
|
+
--space-4: 16px;
|
|
24
|
+
--space-5: 24px;
|
|
25
|
+
--space-6: 32px;
|
|
26
|
+
|
|
27
|
+
--radius-sm: 4px;
|
|
28
|
+
--radius: 8px;
|
|
29
|
+
--radius-pill: 999px;
|
|
30
|
+
|
|
31
|
+
--text-xs: 0.75rem;
|
|
32
|
+
--text-sm: 0.8125rem;
|
|
33
|
+
--text-md: 0.9375rem;
|
|
34
|
+
--text-lg: 1.125rem;
|
|
35
|
+
--text-xl: 1.5rem;
|
|
36
|
+
|
|
37
|
+
--bg: #f6f7f9;
|
|
38
|
+
--surface: #ffffff;
|
|
39
|
+
--surface-2: #f0f2f5;
|
|
40
|
+
--border: #d9dde3;
|
|
41
|
+
/* A control's outline (inputs): 3:1 against the surfaces, WCAG 1.4.11. */
|
|
42
|
+
--border-strong: #838c99;
|
|
43
|
+
--text: #1b1f24;
|
|
44
|
+
--text-muted: #5b6573;
|
|
45
|
+
--accent: #2957c7;
|
|
46
|
+
--accent-contrast: #ffffff;
|
|
47
|
+
--focus: #2957c7;
|
|
48
|
+
--danger: #b42318;
|
|
49
|
+
--danger-contrast: #ffffff;
|
|
50
|
+
--warning-bg: #fff4d6;
|
|
51
|
+
--warning-text: #7a4b00;
|
|
52
|
+
--info-bg: #e3ecff;
|
|
53
|
+
--info-text: #1f3f8f;
|
|
54
|
+
--success-bg: #ddf5e6;
|
|
55
|
+
--success-text: #13603a;
|
|
56
|
+
--danger-bg: #fde5e3;
|
|
57
|
+
--danger-text: #92211a;
|
|
58
|
+
--neutral-bg: #eceff3;
|
|
59
|
+
--neutral-text: #3b4450;
|
|
60
|
+
|
|
61
|
+
/* Job states: a line/dot colour each. */
|
|
62
|
+
--state-waiting: #2f6fdb;
|
|
63
|
+
--state-delayed: #7a4fcf;
|
|
64
|
+
--state-active: #0f8a8a;
|
|
65
|
+
--state-completed: #238a4a;
|
|
66
|
+
--state-failed: #d4541a;
|
|
67
|
+
--state-dead: #b42318;
|
|
68
|
+
--state-waiting-children: #7a6a1f;
|
|
69
|
+
|
|
70
|
+
--shadow: 0 1px 2px rgb(16 24 40 / 6%), 0 1px 3px rgb(16 24 40 / 8%);
|
|
71
|
+
--header-height: 56px;
|
|
72
|
+
--nav-width: 200px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@media (prefers-color-scheme: dark) {
|
|
76
|
+
:root:not([data-theme="light"]) {
|
|
77
|
+
color-scheme: dark;
|
|
78
|
+
--bg: #0f1216;
|
|
79
|
+
--surface: #171b21;
|
|
80
|
+
--surface-2: #1e232b;
|
|
81
|
+
--border: #2d343e;
|
|
82
|
+
--border-strong: #65717f;
|
|
83
|
+
--text: #e6e9ee;
|
|
84
|
+
--text-muted: #9aa4b2;
|
|
85
|
+
--accent: #7ea2ff;
|
|
86
|
+
--accent-contrast: #0b1020;
|
|
87
|
+
--focus: #7ea2ff;
|
|
88
|
+
--danger: #ff7b70;
|
|
89
|
+
--danger-contrast: #1a0806;
|
|
90
|
+
--warning-bg: #3a2c0a;
|
|
91
|
+
--warning-text: #ffd27a;
|
|
92
|
+
--info-bg: #182646;
|
|
93
|
+
--info-text: #b7caff;
|
|
94
|
+
--success-bg: #11301f;
|
|
95
|
+
--success-text: #8fe0ad;
|
|
96
|
+
--danger-bg: #3d1512;
|
|
97
|
+
--danger-text: #ffb4ad;
|
|
98
|
+
--neutral-bg: #242a33;
|
|
99
|
+
--neutral-text: #c9d0da;
|
|
100
|
+
--state-waiting: #6f9ef0;
|
|
101
|
+
--state-delayed: #b08cf0;
|
|
102
|
+
--state-active: #3cc4c4;
|
|
103
|
+
--state-completed: #52c47f;
|
|
104
|
+
--state-failed: #f08a52;
|
|
105
|
+
--state-dead: #ff7b70;
|
|
106
|
+
--state-waiting-children: #cdb64a;
|
|
107
|
+
--shadow: 0 1px 2px rgb(0 0 0 / 40%);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
:root[data-theme="dark"] {
|
|
112
|
+
color-scheme: dark;
|
|
113
|
+
--bg: #0f1216;
|
|
114
|
+
--surface: #171b21;
|
|
115
|
+
--surface-2: #1e232b;
|
|
116
|
+
--border: #2d343e;
|
|
117
|
+
--border-strong: #65717f;
|
|
118
|
+
--text: #e6e9ee;
|
|
119
|
+
--text-muted: #9aa4b2;
|
|
120
|
+
--accent: #7ea2ff;
|
|
121
|
+
--accent-contrast: #0b1020;
|
|
122
|
+
--focus: #7ea2ff;
|
|
123
|
+
--danger: #ff7b70;
|
|
124
|
+
--danger-contrast: #1a0806;
|
|
125
|
+
--warning-bg: #3a2c0a;
|
|
126
|
+
--warning-text: #ffd27a;
|
|
127
|
+
--info-bg: #182646;
|
|
128
|
+
--info-text: #b7caff;
|
|
129
|
+
--success-bg: #11301f;
|
|
130
|
+
--success-text: #8fe0ad;
|
|
131
|
+
--danger-bg: #3d1512;
|
|
132
|
+
--danger-text: #ffb4ad;
|
|
133
|
+
--neutral-bg: #242a33;
|
|
134
|
+
--neutral-text: #c9d0da;
|
|
135
|
+
--state-waiting: #6f9ef0;
|
|
136
|
+
--state-delayed: #b08cf0;
|
|
137
|
+
--state-active: #3cc4c4;
|
|
138
|
+
--state-completed: #52c47f;
|
|
139
|
+
--state-failed: #f08a52;
|
|
140
|
+
--state-dead: #ff7b70;
|
|
141
|
+
--state-waiting-children: #cdb64a;
|
|
142
|
+
--shadow: 0 1px 2px rgb(0 0 0 / 40%);
|
|
143
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { DocsUiTheme } from "../../lib/docs/browser/config.ts";
|
|
2
|
+
|
|
3
|
+
/** `localStorage` key the chosen theme is remembered under. */
|
|
4
|
+
export const THEME_STORAGE_KEY = "bun-api-docs-ui:theme";
|
|
5
|
+
|
|
6
|
+
/** Every theme, in the order the toggle cycles through them. */
|
|
7
|
+
export const THEMES: readonly DocsUiTheme[] = ["system", "light", "dark"];
|
|
8
|
+
|
|
9
|
+
/** Whether a value is a theme. */
|
|
10
|
+
export function isTheme(value: unknown): value is DocsUiTheme {
|
|
11
|
+
return THEMES.includes(value as DocsUiTheme);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The theme after `theme` in the toggle's cycle. */
|
|
15
|
+
export function nextTheme(theme: DocsUiTheme): DocsUiTheme {
|
|
16
|
+
return THEMES[(THEMES.indexOf(theme) + 1) % THEMES.length];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The remembered theme, or `null`. Storage can be absent or throw (private mode, blocked site data). */
|
|
20
|
+
export function readStoredTheme(): DocsUiTheme | null {
|
|
21
|
+
try {
|
|
22
|
+
const value = globalThis.localStorage?.getItem(THEME_STORAGE_KEY);
|
|
23
|
+
return isTheme(value) ? value : null;
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Remembers a theme; a storage failure only means it is not remembered. */
|
|
30
|
+
export function storeTheme(theme: DocsUiTheme): void {
|
|
31
|
+
try {
|
|
32
|
+
globalThis.localStorage?.setItem(THEME_STORAGE_KEY, theme);
|
|
33
|
+
} catch {
|
|
34
|
+
// Not remembered; the page still works.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The theme to start in: the remembered choice, else the mount's default. */
|
|
39
|
+
export function initialTheme(configured: DocsUiTheme): DocsUiTheme {
|
|
40
|
+
return readStoredTheme() ?? configured;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Applies a theme to the document: `data-theme="light"|"dark"` forces one,
|
|
45
|
+
* no attribute follows `prefers-color-scheme` (see `styles/tokens.css`).
|
|
46
|
+
*/
|
|
47
|
+
export function applyTheme(
|
|
48
|
+
theme: DocsUiTheme,
|
|
49
|
+
root: HTMLElement = document.documentElement,
|
|
50
|
+
): void {
|
|
51
|
+
if (theme === "system") {
|
|
52
|
+
delete root.dataset.theme;
|
|
53
|
+
} else {
|
|
54
|
+
root.dataset.theme = theme;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import type { QueryClient } from "@tanstack/react-query";
|
|
2
|
+
import type { UiUnauthorized } from "../../lib/kit/browser/unauthorized.ts";
|
|
3
|
+
import { createContext, use } from "react";
|
|
4
|
+
import { unauthorizedRedirectUrl } from "../../lib/kit/browser/unauthorized.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `unauthorized` in the browser: the one place an authorization failure
|
|
8
|
+
* becomes a navigation, once per page load. Shared by the docs viewer and
|
|
9
|
+
* the jobs UI (as `@kingsleyweb/bun-ui/docs/embed/unauthorized`).
|
|
10
|
+
*
|
|
11
|
+
* Sources report here: the query and mutation caches through
|
|
12
|
+
* {@link watchAuthFailures} (any failed call, whichever client made it: the
|
|
13
|
+
* viewer's document read; every jobs UI API call), the jobs UI's live
|
|
14
|
+
* socket (`LiveClient`'s `onAuthFailure`), and the jobs UI's crashed screens
|
|
15
|
+
* (through {@link UnauthorizedRedirect.probe}: a lazy screen's chunk the
|
|
16
|
+
* shell's `authorize` refused fails with no status to read). No screen knows
|
|
17
|
+
* about it; a failure that is not redirected shows its message as before.
|
|
18
|
+
*
|
|
19
|
+
* One per page: the app that owns the page creates it. The embedded docs
|
|
20
|
+
* screens never do, so the jobs UI's docs section redirects through
|
|
21
|
+
* the jobs UI's alone.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** The parts of `window.location` the redirect reads and calls. */
|
|
25
|
+
export interface RedirectLocation {
|
|
26
|
+
/** The page's URL, to resolve the target and compare origins. */
|
|
27
|
+
readonly href: string;
|
|
28
|
+
/** Sends the browser to a URL. */
|
|
29
|
+
assign: (url: string) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A mount's redirect, made by {@link createUnauthorizedRedirect}. */
|
|
33
|
+
export interface UnauthorizedRedirect {
|
|
34
|
+
/** Whether `unauthorized` is configured at all. */
|
|
35
|
+
readonly enabled: boolean;
|
|
36
|
+
/** Whether a report has sent the browser away (later reports do nothing). */
|
|
37
|
+
readonly redirected: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Reports a failure's status. One the mount lists sends the browser to the
|
|
40
|
+
* target, once: the first such report navigates, every later one returns
|
|
41
|
+
* `true` without navigating again. Returns whether the browser was (or
|
|
42
|
+
* already had been) sent away.
|
|
43
|
+
*/
|
|
44
|
+
report: (status: number) => boolean;
|
|
45
|
+
/** {@link UnauthorizedRedirect.report} for an error from any client: reads its numeric `status`, if it has one. */
|
|
46
|
+
reportError: (error: unknown) => boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Asks the server whether this page may still be loaded, for a failure
|
|
49
|
+
* that carries no status: a `HEAD` of the shell (`basePath`), whose status
|
|
50
|
+
* is reported. A screen whose chunk failed to load is the case: the
|
|
51
|
+
* shell's `authorize` guards the bundle files, and a failed dynamic import
|
|
52
|
+
* never says why. Resolves what {@link UnauthorizedRedirect.report} returned, or
|
|
53
|
+
* `false` when the probe itself failed.
|
|
54
|
+
*/
|
|
55
|
+
probe: () => Promise<boolean>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Options of {@link createUnauthorizedRedirect}; tests replace both. */
|
|
59
|
+
export interface UnauthorizedRedirectOptions {
|
|
60
|
+
/** Where to read the page's URL and navigate. Defaults to `globalThis.location`, looked up per report. */
|
|
61
|
+
location?: () => RedirectLocation | undefined;
|
|
62
|
+
/** The `fetch` {@link UnauthorizedRedirect.probe} calls. Defaults to `globalThis.fetch`, looked up per probe. */
|
|
63
|
+
fetch?: () => typeof fetch;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The status an error carries, if any: `ApiError`'s, bun-client's `HttpError`'s, anything with a numeric `status`. */
|
|
67
|
+
export function statusOf(error: unknown): number | undefined {
|
|
68
|
+
if (typeof error !== "object" || error === null || !("status" in error)) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
const { status } = error;
|
|
72
|
+
return typeof status === "number" ? status : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Whether `path` is `base` or lies under it, segment-wise. */
|
|
76
|
+
function isWithin(path: string, base: string): boolean {
|
|
77
|
+
return path === base || path.startsWith(`${base}/`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The pathname as the router compares it: decoded where it can be, lower-cased, no trailing `/`. */
|
|
81
|
+
function comparablePath(pathname: string): string {
|
|
82
|
+
let decoded = pathname;
|
|
83
|
+
try {
|
|
84
|
+
decoded = decodeURIComponent(pathname);
|
|
85
|
+
} catch {
|
|
86
|
+
// A malformed escape: compare it as written.
|
|
87
|
+
}
|
|
88
|
+
return decoded.replace(/\/+$/, "").toLowerCase();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** A redirect that never navigates: `unauthorized` is off. */
|
|
92
|
+
export const NO_UNAUTHORIZED_REDIRECT: UnauthorizedRedirect = Object.freeze({
|
|
93
|
+
enabled: false,
|
|
94
|
+
redirected: false,
|
|
95
|
+
report: () => false,
|
|
96
|
+
reportError: () => false,
|
|
97
|
+
probe: () => Promise.resolve(false),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Creates the redirect for one page load. `null` gives {@link NO_UNAUTHORIZED_REDIRECT}.
|
|
102
|
+
*
|
|
103
|
+
* The target carries `returnParam` set to the page the user is on (path,
|
|
104
|
+
* query and fragment). A target that resolves into this UI on this origin is
|
|
105
|
+
* never followed (an absolute URL `jobsUi()` could not check, since it does
|
|
106
|
+
* not know its own origin): it would load the UI, fail again and loop, so
|
|
107
|
+
* the screens show their messages instead.
|
|
108
|
+
*
|
|
109
|
+
* @param redirect The UI config's `unauthorized` (`JobsUiConfig`, `DocsUiConfig`).
|
|
110
|
+
* @param basePath The UI config's `basePath`.
|
|
111
|
+
* @param options Where to navigate and what to fetch with; see {@link UnauthorizedRedirectOptions}.
|
|
112
|
+
*/
|
|
113
|
+
export function createUnauthorizedRedirect(
|
|
114
|
+
redirect: UiUnauthorized | null,
|
|
115
|
+
basePath: string,
|
|
116
|
+
options: UnauthorizedRedirectOptions = {},
|
|
117
|
+
): UnauthorizedRedirect {
|
|
118
|
+
const getLocation = options.location ?? (() => globalThis.location);
|
|
119
|
+
const getFetch = options.fetch ?? (() => globalThis.fetch);
|
|
120
|
+
if (redirect === null) {
|
|
121
|
+
return NO_UNAUTHORIZED_REDIRECT;
|
|
122
|
+
}
|
|
123
|
+
const statuses = new Set<number>(redirect.statuses);
|
|
124
|
+
const base = basePath.toLowerCase();
|
|
125
|
+
let redirected = false;
|
|
126
|
+
|
|
127
|
+
const report = (status: number): boolean => {
|
|
128
|
+
if (redirected) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
if (!statuses.has(status)) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
const location = getLocation();
|
|
135
|
+
if (location === undefined) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
const here = new URL(location.href);
|
|
139
|
+
const target = unauthorizedRedirectUrl(
|
|
140
|
+
redirect,
|
|
141
|
+
`${here.pathname}${here.search}${here.hash}`,
|
|
142
|
+
);
|
|
143
|
+
const resolved = new URL(target, here);
|
|
144
|
+
if (
|
|
145
|
+
resolved.origin === here.origin &&
|
|
146
|
+
isWithin(comparablePath(resolved.pathname), base)
|
|
147
|
+
) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
redirected = true;
|
|
151
|
+
location.assign(target);
|
|
152
|
+
return true;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
enabled: true,
|
|
157
|
+
get redirected() {
|
|
158
|
+
return redirected;
|
|
159
|
+
},
|
|
160
|
+
report,
|
|
161
|
+
reportError: (error) => {
|
|
162
|
+
const status = statusOf(error);
|
|
163
|
+
return status === undefined ? false : report(status);
|
|
164
|
+
},
|
|
165
|
+
probe: async () => {
|
|
166
|
+
if (redirected) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
let status: number;
|
|
170
|
+
try {
|
|
171
|
+
const response = await getFetch()(basePath, {
|
|
172
|
+
method: "HEAD",
|
|
173
|
+
credentials: "same-origin",
|
|
174
|
+
cache: "no-store",
|
|
175
|
+
});
|
|
176
|
+
status = response.status;
|
|
177
|
+
} catch {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
return report(status);
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Reports every failed query and mutation of `queryClient` to `redirect`:
|
|
187
|
+
* the cache-level hook, so every API call is covered whichever client made it
|
|
188
|
+
* and no screen needs code of its own. Returns the unsubscribe. Subscribes to
|
|
189
|
+
* nothing when the redirect is off.
|
|
190
|
+
*/
|
|
191
|
+
export function watchAuthFailures(
|
|
192
|
+
queryClient: QueryClient,
|
|
193
|
+
redirect: UnauthorizedRedirect,
|
|
194
|
+
): () => void {
|
|
195
|
+
if (!redirect.enabled) {
|
|
196
|
+
return () => {};
|
|
197
|
+
}
|
|
198
|
+
const offQueries = queryClient.getQueryCache().subscribe((event) => {
|
|
199
|
+
if (event.type === "updated" && event.action.type === "error") {
|
|
200
|
+
redirect.reportError(event.action.error);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
const offMutations = queryClient.getMutationCache().subscribe((event) => {
|
|
204
|
+
if (event.type === "updated" && event.action.type === "error") {
|
|
205
|
+
redirect.reportError(event.action.error);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
return () => {
|
|
209
|
+
offQueries();
|
|
210
|
+
offMutations();
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Carries the page's {@link UnauthorizedRedirect}; {@link NO_UNAUTHORIZED_REDIRECT} outside `AppProviders`. */
|
|
215
|
+
export const UnauthorizedRedirectContext = createContext<UnauthorizedRedirect>(
|
|
216
|
+
NO_UNAUTHORIZED_REDIRECT,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
/** The page's {@link UnauthorizedRedirect}. */
|
|
220
|
+
export function useUnauthorizedRedirect(): UnauthorizedRedirect {
|
|
221
|
+
return use(UnauthorizedRedirectContext);
|
|
222
|
+
}
|