@atelic-action/ui 0.17.2 → 1.0.0
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 +428 -44
- package/bin/atelic-check-base-path.ts +5 -0
- package/bin/atelic-clean-sitemap.ts +5 -0
- package/bin/atelic-contact-canary.ts +5 -0
- package/bin/atelic-print-sheets.sh +1 -1
- package/bin/atelic-seal-document.mjs +1 -1
- package/bin/atelic-serve-static.ts +1 -1
- package/dist/gate/index.d.ts +1 -1
- package/dist/gate/index.js +1 -1
- package/dist/gate/middleware.d.ts +4 -1
- package/dist/gate/middleware.js +14 -5
- package/dist/gate/wall.d.ts +25 -0
- package/dist/gate/wall.js +41 -2
- package/dist/routing/basePath.d.ts +24 -0
- package/dist/routing/basePath.js +54 -0
- package/dist/vite/index.d.ts +3 -2
- package/dist/vite/index.js +3 -2
- package/dist/vite/marketing-config.d.ts +50 -0
- package/dist/vite/marketing-config.js +83 -0
- package/package.json +164 -5
- package/src/app/AppProvider.tsx +41 -0
- package/src/app/AuthGuard.tsx +27 -0
- package/src/app/CardErrorBoundary.tsx +39 -0
- package/src/app/ErrorFallback.tsx +39 -0
- package/src/app/GoogleGlyph.tsx +22 -0
- package/src/app/config.ts +54 -0
- package/src/app/configure.ts +18 -0
- package/src/app/context/AppContext.tsx +52 -0
- package/src/app/context/SidebarContext.tsx +124 -0
- package/src/app/context/UserContext.tsx +139 -0
- package/src/app/hooks/useDebounce.ts +12 -0
- package/src/app/hooks/useIsDesktop.ts +31 -0
- package/src/app/hooks/useWakeLock.ts +49 -0
- package/src/app/index.ts +64 -0
- package/src/app/layout/ActionButton.tsx +67 -0
- package/src/app/layout/AppLayout.tsx +110 -0
- package/src/app/layout/AppPage.tsx +268 -0
- package/src/app/layout/Breadcrumbs.tsx +57 -0
- package/src/app/layout/DesktopRail.tsx +168 -0
- package/src/app/layout/FormActionButtons.tsx +69 -0
- package/src/app/layout/MobileMenu.tsx +156 -0
- package/src/app/layout/MobileTabBar.tsx +43 -0
- package/src/app/layout/PageError.tsx +30 -0
- package/src/app/layout/PageSkeleton.tsx +65 -0
- package/src/app/layout/SectionCards.tsx +33 -0
- package/src/app/layout/UserMenu.tsx +104 -0
- package/src/app/layout/identity.ts +22 -0
- package/src/app/layout/index.ts +20 -0
- package/src/app/layout/nav.ts +52 -0
- package/src/app/lib/dates.ts +57 -0
- package/src/app/lib/env.ts +37 -0
- package/src/app/lib/format.ts +61 -0
- package/src/app/lib/http.ts +205 -0
- package/src/app/lib/lastAuth.ts +45 -0
- package/src/app/lib/oauth.ts +32 -0
- package/src/app/lib/publicRoutes.ts +33 -0
- package/src/app/lib/queryClient.ts +38 -0
- package/src/app/lib/search.ts +17 -0
- package/src/app/lib/session.ts +36 -0
- package/src/app/lib/tokenRefresh.ts +137 -0
- package/src/app/lib/transformKeys.ts +60 -0
- package/src/app/lib/units.ts +228 -0
- package/src/app/lib/unwrap.ts +48 -0
- package/src/app/lib/utils.ts +10 -0
- package/src/app/stores/authStore.ts +80 -0
- package/src/app/stores/storage.ts +40 -0
- package/src/app/stores/unitsStore.ts +54 -0
- package/src/app/types/index.ts +72 -0
- package/src/app/types/query.ts +11 -0
- package/src/app/ui/accordion.tsx +54 -0
- package/src/app/ui/alert-dialog.tsx +114 -0
- package/src/app/ui/alert.tsx +49 -0
- package/src/app/ui/avatar.tsx +45 -0
- package/src/app/ui/badge-list.tsx +51 -0
- package/src/app/ui/badge.tsx +33 -0
- package/src/app/ui/button.tsx +49 -0
- package/src/app/ui/callout.tsx +22 -0
- package/src/app/ui/card.tsx +55 -0
- package/src/app/ui/checkbox.tsx +26 -0
- package/src/app/ui/clear-button.tsx +42 -0
- package/src/app/ui/command.tsx +140 -0
- package/src/app/ui/country-dropdown.tsx +184 -0
- package/src/app/ui/dialog.tsx +104 -0
- package/src/app/ui/dropdown-menu.tsx +185 -0
- package/src/app/ui/form.tsx +172 -0
- package/src/app/ui/index.ts +36 -0
- package/src/app/ui/input.tsx +22 -0
- package/src/app/ui/label.tsx +19 -0
- package/src/app/ui/option-select.tsx +56 -0
- package/src/app/ui/pagination.tsx +90 -0
- package/src/app/ui/popover.tsx +29 -0
- package/src/app/ui/progress.tsx +25 -0
- package/src/app/ui/search-input.tsx +63 -0
- package/src/app/ui/select.tsx +151 -0
- package/src/app/ui/separator.tsx +24 -0
- package/src/app/ui/sheet.tsx +119 -0
- package/src/app/ui/slider.tsx +23 -0
- package/src/app/ui/switch.tsx +27 -0
- package/src/app/ui/textarea.tsx +21 -0
- package/src/app/ui/tooltip.tsx +32 -0
- package/src/artifact/ArtifactShell.tsx +2 -2
- package/src/artifact/Gate.tsx +2 -2
- package/src/artifact/Writeup.tsx +7 -7
- package/src/chrome/SiteShell.tsx +141 -0
- package/src/chrome/index.ts +2 -0
- package/src/components/Disclosure.tsx +80 -14
- package/src/components/GateLogin.tsx +1 -1
- package/src/components/Watermark.tsx +1 -1
- package/src/components/index.ts +6 -2
- package/src/gate/index.ts +1 -1
- package/src/gate/middleware.ts +17 -6
- package/src/gate/wall.ts +60 -2
- package/src/lib/basePathAudit.ts +96 -0
- package/src/lib/displayDate.ts +25 -0
- package/src/lib/honeypot.ts +11 -0
- package/src/lib/hours.ts +28 -0
- package/src/lib/sitemap.ts +34 -0
- package/src/lib/trackEvent.ts +26 -0
- package/src/lib/useOpenHeight.ts +28 -0
- package/src/primitives/Button.tsx +3 -1
- package/src/primitives/Chip.tsx +3 -1
- package/src/primitives/ImageFrame.tsx +48 -0
- package/src/primitives/ProseBlocks.tsx +67 -0
- package/src/primitives/QuoteCard.tsx +20 -0
- package/src/primitives/StarRow.tsx +37 -0
- package/src/primitives/TickList.tsx +24 -0
- package/src/primitives/index.ts +5 -0
- package/src/routing/BasePathProvider.tsx +47 -0
- package/src/routing/basePath.ts +57 -0
- package/src/routing/index.ts +8 -0
- package/src/scripts/check-base-path.ts +130 -0
- package/src/scripts/clean-sitemap.ts +27 -0
- package/src/scripts/contact-canary.ts +98 -0
- package/src/scripts/serve-static.ts +29 -3
- package/src/sections/BlogIndex.tsx +57 -0
- package/src/sections/BlogPost.tsx +65 -0
- package/src/sections/CTABanner.tsx +93 -0
- package/src/sections/CTASection.tsx +60 -0
- package/src/sections/ComparisonTable.tsx +114 -0
- package/src/sections/ContactForm.tsx +163 -0
- package/src/sections/ContactInfoCard.tsx +92 -0
- package/src/sections/CredentialList.tsx +28 -0
- package/src/sections/DeviceShowcase.tsx +39 -0
- package/src/sections/ExpandList.tsx +58 -0
- package/src/sections/FAQBrowser.tsx +283 -0
- package/src/sections/FeatureDetail.tsx +60 -0
- package/src/sections/FeatureGrid.tsx +83 -0
- package/src/sections/Gallery.tsx +84 -0
- package/src/sections/Hero.tsx +110 -0
- package/src/sections/HoursTable.tsx +23 -0
- package/src/sections/JumpRail.tsx +35 -0
- package/src/sections/LegalPage.tsx +43 -0
- package/src/sections/LocationSection.tsx +66 -0
- package/src/sections/Locations.tsx +71 -0
- package/src/sections/LogoMarquee.tsx +27 -0
- package/src/sections/MapEmbed.tsx +75 -0
- package/src/sections/MediaBand.tsx +20 -0
- package/src/sections/MenuList.tsx +44 -0
- package/src/sections/PageHero.tsx +25 -0
- package/src/sections/PressQuotes.tsx +51 -0
- package/src/sections/PricingBand.tsx +43 -0
- package/src/sections/PricingCard.tsx +36 -0
- package/src/sections/ProcessSteps.tsx +45 -0
- package/src/sections/ProfileCards.tsx +73 -0
- package/src/sections/RevealSlider.tsx +156 -0
- package/src/sections/ReviewCarousel.tsx +223 -0
- package/src/sections/ReviewWall.tsx +220 -0
- package/src/sections/SocialStrip.tsx +83 -0
- package/src/sections/SplitSection.tsx +56 -0
- package/src/sections/index.ts +76 -0
- package/src/sections/siteConfig.ts +201 -0
- package/src/styles/app.css +673 -0
- package/src/styles/artifact.css +472 -399
- package/src/styles/base.css +49 -47
- package/src/styles/chrome.css +71 -71
- package/src/styles/components.css +150 -140
- package/src/styles/gate.css +29 -29
- package/src/styles/primitives.css +190 -25
- package/src/styles/sections.css +2652 -0
- package/src/styles/star-rating.css +2 -2
- package/src/vite/index.ts +9 -2
- package/src/vite/marketing-config.ts +128 -0
- package/src/components/Collapsible.tsx +0 -43
- /package/src/scripts/{build-gate-payload.mjs → seal-document.mjs} +0 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atelic-action/ui",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Shared UI for the Atelic templates: site chrome, base styles, the component library,
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Shared UI for the Atelic templates: site chrome, base styles, the component library, the access gate, and the app shell they install",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"repository": {
|
|
@@ -18,8 +18,11 @@
|
|
|
18
18
|
],
|
|
19
19
|
"bin": {
|
|
20
20
|
"atelic-check-access": "./bin/atelic-check-access.ts",
|
|
21
|
+
"atelic-check-base-path": "./bin/atelic-check-base-path.ts",
|
|
21
22
|
"atelic-check-links": "./bin/atelic-check-links.ts",
|
|
22
23
|
"atelic-check-wall": "./bin/atelic-check-wall.ts",
|
|
24
|
+
"atelic-clean-sitemap": "./bin/atelic-clean-sitemap.ts",
|
|
25
|
+
"atelic-contact-canary": "./bin/atelic-contact-canary.ts",
|
|
23
26
|
"atelic-print-sheets": "./bin/atelic-print-sheets.sh",
|
|
24
27
|
"atelic-reader-link": "./bin/atelic-reader-link.ts",
|
|
25
28
|
"atelic-seal-document": "./bin/atelic-seal-document.mjs",
|
|
@@ -30,6 +33,8 @@
|
|
|
30
33
|
"**/*.css"
|
|
31
34
|
],
|
|
32
35
|
"exports": {
|
|
36
|
+
"./app": "./src/app/index.ts",
|
|
37
|
+
"./app/ui": "./src/app/ui/index.ts",
|
|
33
38
|
"./artifact": "./src/artifact/index.ts",
|
|
34
39
|
"./artifact/readers": {
|
|
35
40
|
"types": "./dist/artifact/readers.d.ts",
|
|
@@ -42,13 +47,20 @@
|
|
|
42
47
|
"./hooks": "./src/hooks/index.ts",
|
|
43
48
|
"./primitives": "./src/primitives/index.ts",
|
|
44
49
|
"./routing": "./src/routing/index.ts",
|
|
50
|
+
"./routing/base-path": {
|
|
51
|
+
"types": "./dist/routing/basePath.d.ts",
|
|
52
|
+
"default": "./dist/routing/basePath.js"
|
|
53
|
+
},
|
|
54
|
+
"./sections": "./src/sections/index.ts",
|
|
45
55
|
"./tokens": "./src/tokens/index.ts",
|
|
56
|
+
"./styles/app.css": "./src/styles/app.css",
|
|
46
57
|
"./styles/artifact.css": "./src/styles/artifact.css",
|
|
47
58
|
"./styles/base.css": "./src/styles/base.css",
|
|
48
59
|
"./styles/chrome.css": "./src/styles/chrome.css",
|
|
49
60
|
"./styles/components.css": "./src/styles/components.css",
|
|
50
61
|
"./styles/gate.css": "./src/styles/gate.css",
|
|
51
62
|
"./styles/primitives.css": "./src/styles/primitives.css",
|
|
63
|
+
"./styles/sections.css": "./src/styles/sections.css",
|
|
52
64
|
"./styles/star-rating.css": "./src/styles/star-rating.css",
|
|
53
65
|
"./gate": {
|
|
54
66
|
"types": "./dist/gate/index.d.ts",
|
|
@@ -70,11 +82,15 @@
|
|
|
70
82
|
"types": "./dist/vite/index.d.ts",
|
|
71
83
|
"default": "./dist/vite/index.js"
|
|
72
84
|
},
|
|
85
|
+
"./vite/private-chunks": {
|
|
86
|
+
"types": "./dist/vite/private-chunks.d.ts",
|
|
87
|
+
"default": "./dist/vite/private-chunks.js"
|
|
88
|
+
},
|
|
73
89
|
"./package.json": "./package.json"
|
|
74
90
|
},
|
|
75
91
|
"scripts": {
|
|
76
92
|
"build": "rm -rf dist && tsc -p tsconfig.server.json",
|
|
77
|
-
"build:smoke": "node --input-type=module -e \"for (const entry of ['gate', 'gate/middleware', 'gate/request-link', 'gate/verify', 'vite', 'artifact/readers']) await import('@atelic-action/ui/' + entry)\"",
|
|
93
|
+
"build:smoke": "node --input-type=module -e \"for (const entry of ['gate', 'gate/middleware', 'gate/request-link', 'gate/verify', 'vite', 'vite/private-chunks', 'artifact/readers', 'routing/base-path']) await import('@atelic-action/ui/' + entry)\"",
|
|
78
94
|
"prepack": "bun run build",
|
|
79
95
|
"lint": "biome check .",
|
|
80
96
|
"lint:fix": "biome check . --write",
|
|
@@ -88,42 +104,185 @@
|
|
|
88
104
|
"qrcode": "^1.5.4"
|
|
89
105
|
},
|
|
90
106
|
"peerDependencies": {
|
|
107
|
+
"@radix-ui/react-accordion": "^1.2.12",
|
|
108
|
+
"@radix-ui/react-alert-dialog": "^1.1.15",
|
|
109
|
+
"@radix-ui/react-avatar": "^1.1.11",
|
|
110
|
+
"@radix-ui/react-checkbox": "^1.1.2",
|
|
111
|
+
"@radix-ui/react-dialog": "^1.1.15",
|
|
112
|
+
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
113
|
+
"@radix-ui/react-label": "^2.1.8",
|
|
114
|
+
"@radix-ui/react-popover": "^1.1.15",
|
|
115
|
+
"@radix-ui/react-progress": "^1.1.8",
|
|
116
|
+
"@radix-ui/react-select": "^2.2.6",
|
|
117
|
+
"@radix-ui/react-separator": "^1.1.0",
|
|
118
|
+
"@radix-ui/react-slider": "^1.3.6",
|
|
119
|
+
"@radix-ui/react-slot": "^1.2.4",
|
|
120
|
+
"@radix-ui/react-switch": "^1.2.6",
|
|
121
|
+
"@radix-ui/react-tooltip": "^1.2.8",
|
|
122
|
+
"@tanstack/react-query": "^5.59.0",
|
|
123
|
+
"@tanstack/react-router": "^1.81.4",
|
|
91
124
|
"@tanstack/react-start": "^1.168.0",
|
|
92
125
|
"@vitejs/plugin-react": "^5.0.0 || ^6.0.0",
|
|
126
|
+
"class-variance-authority": "^0.7.0",
|
|
127
|
+
"clsx": "^2.1.1",
|
|
128
|
+
"cmdk": "^1.1.1",
|
|
129
|
+
"country-data-list": "^1.6.0",
|
|
93
130
|
"lucide-react": "^1.23.0",
|
|
94
131
|
"react": "^19.0.0",
|
|
132
|
+
"react-circle-flags": "^0.0.25",
|
|
95
133
|
"react-dom": "^19.0.0",
|
|
96
|
-
"
|
|
134
|
+
"react-hook-form": "^7.71.1",
|
|
135
|
+
"sonner": "^2.0.7",
|
|
136
|
+
"tailwind-merge": "^3.0.0",
|
|
137
|
+
"tailwindcss": "^4.1.17",
|
|
138
|
+
"vite": "^7.0.0 || ^8.0.0",
|
|
139
|
+
"zod": "^4.3.5",
|
|
140
|
+
"zustand": "^5.0.1"
|
|
97
141
|
},
|
|
98
142
|
"peerDependenciesMeta": {
|
|
143
|
+
"@radix-ui/react-accordion": {
|
|
144
|
+
"optional": true
|
|
145
|
+
},
|
|
146
|
+
"@radix-ui/react-alert-dialog": {
|
|
147
|
+
"optional": true
|
|
148
|
+
},
|
|
149
|
+
"@radix-ui/react-avatar": {
|
|
150
|
+
"optional": true
|
|
151
|
+
},
|
|
152
|
+
"@radix-ui/react-checkbox": {
|
|
153
|
+
"optional": true
|
|
154
|
+
},
|
|
155
|
+
"@radix-ui/react-dialog": {
|
|
156
|
+
"optional": true
|
|
157
|
+
},
|
|
158
|
+
"@radix-ui/react-dropdown-menu": {
|
|
159
|
+
"optional": true
|
|
160
|
+
},
|
|
161
|
+
"@radix-ui/react-label": {
|
|
162
|
+
"optional": true
|
|
163
|
+
},
|
|
164
|
+
"@radix-ui/react-popover": {
|
|
165
|
+
"optional": true
|
|
166
|
+
},
|
|
167
|
+
"@radix-ui/react-progress": {
|
|
168
|
+
"optional": true
|
|
169
|
+
},
|
|
170
|
+
"@radix-ui/react-select": {
|
|
171
|
+
"optional": true
|
|
172
|
+
},
|
|
173
|
+
"@radix-ui/react-separator": {
|
|
174
|
+
"optional": true
|
|
175
|
+
},
|
|
176
|
+
"@radix-ui/react-slider": {
|
|
177
|
+
"optional": true
|
|
178
|
+
},
|
|
179
|
+
"@radix-ui/react-slot": {
|
|
180
|
+
"optional": true
|
|
181
|
+
},
|
|
182
|
+
"@radix-ui/react-switch": {
|
|
183
|
+
"optional": true
|
|
184
|
+
},
|
|
185
|
+
"@radix-ui/react-tooltip": {
|
|
186
|
+
"optional": true
|
|
187
|
+
},
|
|
188
|
+
"@tanstack/react-query": {
|
|
189
|
+
"optional": true
|
|
190
|
+
},
|
|
191
|
+
"@tanstack/react-router": {
|
|
192
|
+
"optional": true
|
|
193
|
+
},
|
|
99
194
|
"@tanstack/react-start": {
|
|
100
195
|
"optional": true
|
|
101
196
|
},
|
|
102
197
|
"@vitejs/plugin-react": {
|
|
103
198
|
"optional": true
|
|
104
199
|
},
|
|
200
|
+
"class-variance-authority": {
|
|
201
|
+
"optional": true
|
|
202
|
+
},
|
|
203
|
+
"clsx": {
|
|
204
|
+
"optional": true
|
|
205
|
+
},
|
|
206
|
+
"cmdk": {
|
|
207
|
+
"optional": true
|
|
208
|
+
},
|
|
209
|
+
"country-data-list": {
|
|
210
|
+
"optional": true
|
|
211
|
+
},
|
|
212
|
+
"react-circle-flags": {
|
|
213
|
+
"optional": true
|
|
214
|
+
},
|
|
215
|
+
"react-hook-form": {
|
|
216
|
+
"optional": true
|
|
217
|
+
},
|
|
218
|
+
"sonner": {
|
|
219
|
+
"optional": true
|
|
220
|
+
},
|
|
221
|
+
"tailwind-merge": {
|
|
222
|
+
"optional": true
|
|
223
|
+
},
|
|
224
|
+
"tailwindcss": {
|
|
225
|
+
"optional": true
|
|
226
|
+
},
|
|
105
227
|
"vite": {
|
|
106
228
|
"optional": true
|
|
229
|
+
},
|
|
230
|
+
"zod": {
|
|
231
|
+
"optional": true
|
|
232
|
+
},
|
|
233
|
+
"zustand": {
|
|
234
|
+
"optional": true
|
|
107
235
|
}
|
|
108
236
|
},
|
|
109
237
|
"devDependencies": {
|
|
110
238
|
"@biomejs/biome": "^2.5.13",
|
|
239
|
+
"@radix-ui/react-accordion": "^1.2.12",
|
|
240
|
+
"@radix-ui/react-alert-dialog": "^1.1.15",
|
|
241
|
+
"@radix-ui/react-avatar": "^1.1.11",
|
|
242
|
+
"@radix-ui/react-checkbox": "^1.1.2",
|
|
243
|
+
"@radix-ui/react-dialog": "^1.1.15",
|
|
244
|
+
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
245
|
+
"@radix-ui/react-label": "^2.1.8",
|
|
246
|
+
"@radix-ui/react-popover": "^1.1.15",
|
|
247
|
+
"@radix-ui/react-progress": "^1.1.8",
|
|
248
|
+
"@radix-ui/react-select": "^2.2.6",
|
|
249
|
+
"@radix-ui/react-separator": "^1.1.0",
|
|
250
|
+
"@radix-ui/react-slider": "^1.3.6",
|
|
251
|
+
"@radix-ui/react-slot": "^1.2.4",
|
|
252
|
+
"@radix-ui/react-switch": "^1.2.6",
|
|
253
|
+
"@radix-ui/react-tooltip": "^1.2.8",
|
|
254
|
+
"@tailwindcss/vite": "^4.1.17",
|
|
255
|
+
"@tanstack/react-query": "^5.59.0",
|
|
256
|
+
"@tanstack/react-router": "^1.81.4",
|
|
111
257
|
"@tanstack/react-start": "^1.168.60",
|
|
112
258
|
"@testing-library/jest-dom": "^6.6.2",
|
|
113
259
|
"@testing-library/react": "^16.0.1",
|
|
260
|
+
"@testing-library/user-event": "^14.6.1",
|
|
114
261
|
"@types/bun": "^1.4.2",
|
|
115
262
|
"@types/node": "^24.0.0",
|
|
116
263
|
"@types/qrcode": "^1.5.6",
|
|
117
264
|
"@types/react": "^19.0.0",
|
|
118
265
|
"@types/react-dom": "^19.0.0",
|
|
119
266
|
"@vitejs/plugin-react": "^6.1.1",
|
|
267
|
+
"class-variance-authority": "^0.7.0",
|
|
268
|
+
"clsx": "^2.1.1",
|
|
269
|
+
"cmdk": "^1.1.1",
|
|
270
|
+
"country-data-list": "^1.6.0",
|
|
120
271
|
"jsdom": "^25.0.1",
|
|
121
272
|
"lucide-react": "^1.23.0",
|
|
122
273
|
"react": "^19.0.0",
|
|
274
|
+
"react-circle-flags": "^0.0.25",
|
|
123
275
|
"react-dom": "^19.0.0",
|
|
276
|
+
"react-hook-form": "^7.71.1",
|
|
277
|
+
"sonner": "^2.0.7",
|
|
278
|
+
"tailwind-merge": "^3.0.0",
|
|
279
|
+
"tailwindcss": "^4.1.17",
|
|
280
|
+
"tailwindcss-animate": "^1.0.7",
|
|
124
281
|
"typescript": "^5.6.3",
|
|
125
282
|
"vite": "^8.3.2",
|
|
126
|
-
"vitest": "^4.0.0"
|
|
283
|
+
"vitest": "^4.0.0",
|
|
284
|
+
"zod": "^4.3.5",
|
|
285
|
+
"zustand": "^5.0.1"
|
|
127
286
|
},
|
|
128
287
|
"trustedDependencies": [
|
|
129
288
|
"@biomejs/biome"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { Toaster } from "sonner";
|
|
4
|
+
import { AppConfigProvider, type AppConfigProviderProps } from "./context/AppContext";
|
|
5
|
+
import { SidebarProvider } from "./context/SidebarContext";
|
|
6
|
+
import { UserProvider } from "./context/UserContext";
|
|
7
|
+
import { queryClient as defaultQueryClient } from "./lib/queryClient";
|
|
8
|
+
|
|
9
|
+
export interface AppProviderProps extends AppConfigProviderProps {
|
|
10
|
+
/** The query client. Defaults to the shell's, whose mutation cache raises `meta.toast`. */
|
|
11
|
+
queryClient?: QueryClient;
|
|
12
|
+
/** The toaster the shell's toasts land in. Defaults to sonner's, top right; `false` renders none. */
|
|
13
|
+
toaster?: ReactNode | false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Everything the shell needs above the router: the app's config and nav, the
|
|
18
|
+
* query client, the rail state, the signed in user, and the toaster. Render
|
|
19
|
+
* it once, around the `RouterProvider`, so the stores are hydrated before the
|
|
20
|
+
* first route loads; the root route then renders `AuthGuard` and `AppLayout`.
|
|
21
|
+
*/
|
|
22
|
+
export function AppProvider({
|
|
23
|
+
config,
|
|
24
|
+
nav,
|
|
25
|
+
queryClient = defaultQueryClient,
|
|
26
|
+
toaster,
|
|
27
|
+
children,
|
|
28
|
+
}: AppProviderProps) {
|
|
29
|
+
return (
|
|
30
|
+
<AppConfigProvider config={config} nav={nav}>
|
|
31
|
+
<QueryClientProvider client={queryClient}>
|
|
32
|
+
<SidebarProvider>
|
|
33
|
+
<UserProvider>
|
|
34
|
+
{children}
|
|
35
|
+
{toaster === undefined ? <Toaster position="top-right" richColors /> : toaster || null}
|
|
36
|
+
</UserProvider>
|
|
37
|
+
</SidebarProvider>
|
|
38
|
+
</QueryClientProvider>
|
|
39
|
+
</AppConfigProvider>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Navigate, useLocation } from "@tanstack/react-router";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { isPublicRoute } from "./lib/publicRoutes";
|
|
5
|
+
import { useAuthStore } from "./stores/authStore";
|
|
6
|
+
|
|
7
|
+
interface AuthGuardProps {
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Protects routes that require authentication. What a signed out visitor may
|
|
13
|
+
* load is the app's call, read through publicRoutes.ts: the exact public
|
|
14
|
+
* routes ("/" doubles as the sign in surface, "/oauth/callback" lands the
|
|
15
|
+
* redirect) and the public page patterns. Everything else redirects to "/",
|
|
16
|
+
* which renders the adaptive auth page.
|
|
17
|
+
*/
|
|
18
|
+
export function AuthGuard({ children }: AuthGuardProps) {
|
|
19
|
+
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
|
20
|
+
const pathname = useLocation({ select: (location) => location.pathname });
|
|
21
|
+
|
|
22
|
+
if (!isAuthenticated && !isPublicRoute(pathname)) {
|
|
23
|
+
return <Navigate to="/" />;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return <>{children}</>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Component, type ErrorInfo, type ReactNode } from "react";
|
|
2
|
+
import { Card, CardContent } from "./ui/card";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface State {
|
|
9
|
+
hasError: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class CardErrorBoundary extends Component<Props, State> {
|
|
13
|
+
constructor(props: Props) {
|
|
14
|
+
super(props);
|
|
15
|
+
this.state = { hasError: false };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static getDerivedStateFromError(): State {
|
|
19
|
+
return { hasError: true };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
componentDidCatch(error: Error, info: ErrorInfo) {
|
|
23
|
+
console.error("CardErrorBoundary caught:", error, info.componentStack);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
render() {
|
|
27
|
+
if (this.state.hasError) {
|
|
28
|
+
return (
|
|
29
|
+
<Card className="border-destructive/30">
|
|
30
|
+
<CardContent className="py-6 text-center text-sm text-muted-foreground">
|
|
31
|
+
Unable to display this item
|
|
32
|
+
</CardContent>
|
|
33
|
+
</Card>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return this.props.children;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Button } from "./ui/button";
|
|
2
|
+
|
|
3
|
+
export interface ErrorFallbackProps {
|
|
4
|
+
error: unknown;
|
|
5
|
+
resetError: () => void;
|
|
6
|
+
/**
|
|
7
|
+
* Show the error's message under the apology. The package reads no
|
|
8
|
+
* environment, so an app passes `import.meta.env.DEV` here to see it while
|
|
9
|
+
* developing and never in production.
|
|
10
|
+
*/
|
|
11
|
+
showDetails?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Fallback UI displayed when an error boundary catches an error.
|
|
16
|
+
* Provides user feedback and a way to recover.
|
|
17
|
+
*/
|
|
18
|
+
export function ErrorFallback({ error, resetError, showDetails = false }: ErrorFallbackProps) {
|
|
19
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="flex min-h-screen items-center justify-center bg-background p-4">
|
|
23
|
+
<div className="max-w-md text-center">
|
|
24
|
+
<h1 className="text-2xl font-semibold text-foreground">Something went wrong</h1>
|
|
25
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
26
|
+
An unexpected error occurred. Please try again.
|
|
27
|
+
</p>
|
|
28
|
+
{showDetails && (
|
|
29
|
+
<pre className="mt-4 max-h-40 overflow-auto rounded bg-muted p-3 text-left text-xs text-muted-foreground">
|
|
30
|
+
{errorMessage}
|
|
31
|
+
</pre>
|
|
32
|
+
)}
|
|
33
|
+
<Button onClick={resetError} className="mt-6">
|
|
34
|
+
Try Again
|
|
35
|
+
</Button>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function GoogleGlyph({ className }: { className?: string }) {
|
|
2
|
+
return (
|
|
3
|
+
<svg className={className} viewBox="0 0 48 48" aria-hidden="true">
|
|
4
|
+
<path
|
|
5
|
+
fill="#FFC107"
|
|
6
|
+
d="M43.611 20.083H42V20H24v8h11.303c-1.649 4.657-6.08 8-11.303 8-6.627 0-12-5.373-12-12s5.373-12 12-12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4 12.955 4 4 12.955 4 24s8.955 20 20 20 20-8.955 20-20c0-1.341-.138-2.65-.389-3.917z"
|
|
7
|
+
/>
|
|
8
|
+
<path
|
|
9
|
+
fill="#FF3D00"
|
|
10
|
+
d="M6.306 14.691l6.571 4.819C14.655 15.108 18.961 12 24 12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4 16.318 4 9.656 8.337 6.306 14.691z"
|
|
11
|
+
/>
|
|
12
|
+
<path
|
|
13
|
+
fill="#4CAF50"
|
|
14
|
+
d="M24 44c5.166 0 9.86-1.977 13.409-5.192l-6.19-5.238C29.211 35.091 26.715 36 24 36c-5.202 0-9.619-3.317-11.283-7.946l-6.522 5.025C9.505 39.556 16.227 44 24 44z"
|
|
15
|
+
/>
|
|
16
|
+
<path
|
|
17
|
+
fill="#1976D2"
|
|
18
|
+
d="M43.611 20.083H42V20H24v8h11.303c-.792 2.237-2.231 4.166-4.094 5.571.001-.001.002-.001.003-.002l6.19 5.238C36.971 39.205 44 34 44 24c0-1.341-.138-2.65-.389-3.917z"
|
|
19
|
+
/>
|
|
20
|
+
</svg>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What makes an app that app, as the shell reads it: the name on the mark and
|
|
3
|
+
* in aria labels, the prefix on every browser storage key, the routes a signed
|
|
4
|
+
* out visitor may see, and the api the transport talks to. The app owns the
|
|
5
|
+
* values and hands them to `AppProvider`, which records them here, since the
|
|
6
|
+
* transport and the stores run outside React and read them by function.
|
|
7
|
+
*/
|
|
8
|
+
export interface AppConfig {
|
|
9
|
+
/** Display name, used in aria labels. */
|
|
10
|
+
name: string;
|
|
11
|
+
/** The lowercase wordmark the shell renders beside the brand dot. */
|
|
12
|
+
mark: string;
|
|
13
|
+
/** Prefix for every localStorage key and cookie the shell writes. */
|
|
14
|
+
storagePrefix: string;
|
|
15
|
+
/** Exact routes a signed out visitor can load bare; everything else redirects home. */
|
|
16
|
+
publicRoutes: readonly string[];
|
|
17
|
+
/**
|
|
18
|
+
* Pages that stand on their own signed out, as prefix patterns ending in
|
|
19
|
+
* "/*". They render inside the public chrome (brand bar, sign in link) for
|
|
20
|
+
* a visitor and inside the app shell for the signed in owner. The
|
|
21
|
+
* convention is a person's public records under /u/<username>/...
|
|
22
|
+
*/
|
|
23
|
+
publicPages?: readonly string[];
|
|
24
|
+
/**
|
|
25
|
+
* The api's base URL, resolved by the app (typically
|
|
26
|
+
* `import.meta.env.VITE_API_BASE_URL ?? "https://api.example.com"`), so the
|
|
27
|
+
* package never reads the app's environment itself.
|
|
28
|
+
*/
|
|
29
|
+
apiBaseUrl: string;
|
|
30
|
+
/** Where the Profile link in the rail menu and the mobile menu goes. Defaults to "/profile". */
|
|
31
|
+
profileHref?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let current: AppConfig | null = null;
|
|
35
|
+
|
|
36
|
+
/** Record the app's config. `configureApp` (or `AppProvider`) calls this; the stores rehydrate there. */
|
|
37
|
+
export function setAppConfig(config: AppConfig): void {
|
|
38
|
+
current = config;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The app's config, or null before `configureApp` has run. */
|
|
42
|
+
export function peekAppConfig(): AppConfig | null {
|
|
43
|
+
return current;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The app's config. Throws when the app has not configured the shell yet. */
|
|
47
|
+
export function getAppConfig(): AppConfig {
|
|
48
|
+
if (!current) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
"The app shell is not configured: render AppProvider (or call configureApp) before using the transport.",
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return current;
|
|
54
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type AppConfig, peekAppConfig, setAppConfig } from "./config";
|
|
2
|
+
import { useAuthStore } from "./stores/authStore";
|
|
3
|
+
import { useUnitsStore } from "./stores/unitsStore";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hand the shell the app's config, and hydrate the persisted stores under its
|
|
7
|
+
* storage prefix. `AppProvider` calls this on its first render, which covers
|
|
8
|
+
* an app that renders it above its router. Call it yourself only where shell
|
|
9
|
+
* code runs before that render: a module that calls the api at import, or a
|
|
10
|
+
* test. Safe to call again; the stores rehydrate only when the prefix changes.
|
|
11
|
+
*/
|
|
12
|
+
export function configureApp(config: AppConfig): void {
|
|
13
|
+
const previous = peekAppConfig();
|
|
14
|
+
setAppConfig(config);
|
|
15
|
+
if (previous?.storagePrefix === config.storagePrefix) return;
|
|
16
|
+
void useAuthStore.persist.rehydrate();
|
|
17
|
+
void useUnitsStore.persist.rehydrate();
|
|
18
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createContext, type ReactNode, useContext, useMemo, useRef } from "react";
|
|
2
|
+
import { type AppConfig, getAppConfig } from "../config";
|
|
3
|
+
import { configureApp } from "../configure";
|
|
4
|
+
import type { NavItem } from "../layout/nav";
|
|
5
|
+
|
|
6
|
+
interface AppContextValue {
|
|
7
|
+
config: AppConfig;
|
|
8
|
+
nav: NavItem[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const AppContext = createContext<AppContextValue | null>(null);
|
|
12
|
+
|
|
13
|
+
export interface AppConfigProviderProps {
|
|
14
|
+
/** The app's facts: name, mark, storage prefix, public routes, api base URL. */
|
|
15
|
+
config: AppConfig;
|
|
16
|
+
/** The sections and sub pages, in nav order: the one place a destination is declared. */
|
|
17
|
+
nav: NavItem[];
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The app's config and nav, and nothing else: what the frame (the rail, the
|
|
23
|
+
* tab bar, the menu sheet, a section's cards) reads. `AppProvider` wraps it
|
|
24
|
+
* with the query client, the rail state, the signed in user, and the toaster;
|
|
25
|
+
* a test that mounts one surface can use it alone.
|
|
26
|
+
*/
|
|
27
|
+
export function AppConfigProvider({ config, nav, children }: AppConfigProviderProps) {
|
|
28
|
+
// Configure synchronously on the first render, and again if the config
|
|
29
|
+
// object changes, so the stores hydrate under the app's prefix before any
|
|
30
|
+
// child reads them.
|
|
31
|
+
const configured = useRef<AppConfig | null>(null);
|
|
32
|
+
if (configured.current !== config) {
|
|
33
|
+
configureApp(config);
|
|
34
|
+
configured.current = config;
|
|
35
|
+
}
|
|
36
|
+
const value = useMemo(() => ({ config, nav }), [config, nav]);
|
|
37
|
+
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The app's config, from the nearest provider, or as configured with `configureApp`. */
|
|
41
|
+
export function useAppConfig(): AppConfig {
|
|
42
|
+
return useContext(AppContext)?.config ?? getAppConfig();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The app's nav items. Throws outside an `AppProvider` or `AppConfigProvider`. */
|
|
46
|
+
export function useNav(): NavItem[] {
|
|
47
|
+
const context = useContext(AppContext);
|
|
48
|
+
if (!context) {
|
|
49
|
+
throw new Error("useNav must be used within an AppProvider");
|
|
50
|
+
}
|
|
51
|
+
return context.nav;
|
|
52
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
type ReactNode,
|
|
4
|
+
useCallback,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useState,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { peekAppConfig } from "../config";
|
|
11
|
+
|
|
12
|
+
interface SidebarContextType {
|
|
13
|
+
isCollapsed: boolean;
|
|
14
|
+
toggleSidebar: () => void;
|
|
15
|
+
setCollapsed: (collapsed: boolean) => void;
|
|
16
|
+
/** Right (AppPage) rail collapse — mirrors the left nav rail. */
|
|
17
|
+
isRailCollapsed: boolean;
|
|
18
|
+
toggleRail: () => void;
|
|
19
|
+
setRailCollapsed: (collapsed: boolean) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const SidebarContext = createContext<SidebarContextType | undefined>(undefined);
|
|
23
|
+
|
|
24
|
+
interface SidebarProviderProps {
|
|
25
|
+
children: ReactNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const SIDEBAR_KEY = "sidebarCollapsed";
|
|
29
|
+
const RAIL_KEY = "railCollapsed";
|
|
30
|
+
|
|
31
|
+
/** localStorage under the app's prefix; a no op before the app is configured or where storage is refused. */
|
|
32
|
+
function readSaved(name: string): boolean {
|
|
33
|
+
const config = peekAppConfig();
|
|
34
|
+
if (!config) return false;
|
|
35
|
+
try {
|
|
36
|
+
return localStorage.getItem(config.storagePrefix + "-" + name) === "true";
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function writeSaved(name: string, value: boolean) {
|
|
43
|
+
const config = peekAppConfig();
|
|
44
|
+
if (!config) return;
|
|
45
|
+
try {
|
|
46
|
+
localStorage.setItem(config.storagePrefix + "-" + name, String(value));
|
|
47
|
+
} catch {
|
|
48
|
+
// Storage refused; the state lives in memory for this visit.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function SidebarProvider({ children }: SidebarProviderProps) {
|
|
53
|
+
// Both start open so a prerender and the first client render agree; the
|
|
54
|
+
// saved choice is read in an effect, then written back on every change.
|
|
55
|
+
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
56
|
+
const [isRailCollapsed, setIsRailCollapsed] = useState(false);
|
|
57
|
+
const [ready, setReady] = useState(false);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
setIsCollapsed(readSaved(SIDEBAR_KEY));
|
|
61
|
+
setIsRailCollapsed(readSaved(RAIL_KEY));
|
|
62
|
+
setReady(true);
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (ready) writeSaved(SIDEBAR_KEY, isCollapsed);
|
|
67
|
+
}, [ready, isCollapsed]);
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (ready) writeSaved(RAIL_KEY, isRailCollapsed);
|
|
71
|
+
}, [ready, isRailCollapsed]);
|
|
72
|
+
|
|
73
|
+
const toggleSidebar = useCallback(() => {
|
|
74
|
+
setIsCollapsed((prev) => !prev);
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
const setCollapsed = useCallback((collapsed: boolean) => {
|
|
78
|
+
setIsCollapsed(collapsed);
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
const toggleRail = useCallback(() => {
|
|
82
|
+
setIsRailCollapsed((prev) => !prev);
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
const setRailCollapsed = useCallback((collapsed: boolean) => {
|
|
86
|
+
setIsRailCollapsed(collapsed);
|
|
87
|
+
}, []);
|
|
88
|
+
|
|
89
|
+
const value = useMemo(
|
|
90
|
+
() => ({
|
|
91
|
+
isCollapsed,
|
|
92
|
+
toggleSidebar,
|
|
93
|
+
setCollapsed,
|
|
94
|
+
isRailCollapsed,
|
|
95
|
+
toggleRail,
|
|
96
|
+
setRailCollapsed,
|
|
97
|
+
}),
|
|
98
|
+
[isCollapsed, toggleSidebar, setCollapsed, isRailCollapsed, toggleRail, setRailCollapsed],
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return <SidebarContext.Provider value={value}>{children}</SidebarContext.Provider>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function useSidebar() {
|
|
105
|
+
const context = useContext(SidebarContext);
|
|
106
|
+
if (context === undefined) {
|
|
107
|
+
throw new Error("useSidebar must be used within a SidebarProvider");
|
|
108
|
+
}
|
|
109
|
+
return context;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Tolerant accessor for the right-rail collapse state. Unlike `useSidebar`, it
|
|
114
|
+
* does NOT throw when used outside a `SidebarProvider`: AppPage surfaces are
|
|
115
|
+
* mounted bare in unit tests, so the rail toggle simply degrades to a no-op
|
|
116
|
+
* there rather than blowing up the render.
|
|
117
|
+
*/
|
|
118
|
+
export function useRail() {
|
|
119
|
+
const context = useContext(SidebarContext);
|
|
120
|
+
return {
|
|
121
|
+
isRailCollapsed: context?.isRailCollapsed ?? false,
|
|
122
|
+
toggleRail: context?.toggleRail ?? (() => {}),
|
|
123
|
+
};
|
|
124
|
+
}
|