@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.
Files changed (184) hide show
  1. package/README.md +428 -44
  2. package/bin/atelic-check-base-path.ts +5 -0
  3. package/bin/atelic-clean-sitemap.ts +5 -0
  4. package/bin/atelic-contact-canary.ts +5 -0
  5. package/bin/atelic-print-sheets.sh +1 -1
  6. package/bin/atelic-seal-document.mjs +1 -1
  7. package/bin/atelic-serve-static.ts +1 -1
  8. package/dist/gate/index.d.ts +1 -1
  9. package/dist/gate/index.js +1 -1
  10. package/dist/gate/middleware.d.ts +4 -1
  11. package/dist/gate/middleware.js +14 -5
  12. package/dist/gate/wall.d.ts +25 -0
  13. package/dist/gate/wall.js +41 -2
  14. package/dist/routing/basePath.d.ts +24 -0
  15. package/dist/routing/basePath.js +54 -0
  16. package/dist/vite/index.d.ts +3 -2
  17. package/dist/vite/index.js +3 -2
  18. package/dist/vite/marketing-config.d.ts +50 -0
  19. package/dist/vite/marketing-config.js +83 -0
  20. package/package.json +164 -5
  21. package/src/app/AppProvider.tsx +41 -0
  22. package/src/app/AuthGuard.tsx +27 -0
  23. package/src/app/CardErrorBoundary.tsx +39 -0
  24. package/src/app/ErrorFallback.tsx +39 -0
  25. package/src/app/GoogleGlyph.tsx +22 -0
  26. package/src/app/config.ts +54 -0
  27. package/src/app/configure.ts +18 -0
  28. package/src/app/context/AppContext.tsx +52 -0
  29. package/src/app/context/SidebarContext.tsx +124 -0
  30. package/src/app/context/UserContext.tsx +139 -0
  31. package/src/app/hooks/useDebounce.ts +12 -0
  32. package/src/app/hooks/useIsDesktop.ts +31 -0
  33. package/src/app/hooks/useWakeLock.ts +49 -0
  34. package/src/app/index.ts +64 -0
  35. package/src/app/layout/ActionButton.tsx +67 -0
  36. package/src/app/layout/AppLayout.tsx +110 -0
  37. package/src/app/layout/AppPage.tsx +268 -0
  38. package/src/app/layout/Breadcrumbs.tsx +57 -0
  39. package/src/app/layout/DesktopRail.tsx +168 -0
  40. package/src/app/layout/FormActionButtons.tsx +69 -0
  41. package/src/app/layout/MobileMenu.tsx +156 -0
  42. package/src/app/layout/MobileTabBar.tsx +43 -0
  43. package/src/app/layout/PageError.tsx +30 -0
  44. package/src/app/layout/PageSkeleton.tsx +65 -0
  45. package/src/app/layout/SectionCards.tsx +33 -0
  46. package/src/app/layout/UserMenu.tsx +104 -0
  47. package/src/app/layout/identity.ts +22 -0
  48. package/src/app/layout/index.ts +20 -0
  49. package/src/app/layout/nav.ts +52 -0
  50. package/src/app/lib/dates.ts +57 -0
  51. package/src/app/lib/env.ts +37 -0
  52. package/src/app/lib/format.ts +61 -0
  53. package/src/app/lib/http.ts +205 -0
  54. package/src/app/lib/lastAuth.ts +45 -0
  55. package/src/app/lib/oauth.ts +32 -0
  56. package/src/app/lib/publicRoutes.ts +33 -0
  57. package/src/app/lib/queryClient.ts +38 -0
  58. package/src/app/lib/search.ts +17 -0
  59. package/src/app/lib/session.ts +36 -0
  60. package/src/app/lib/tokenRefresh.ts +137 -0
  61. package/src/app/lib/transformKeys.ts +60 -0
  62. package/src/app/lib/units.ts +228 -0
  63. package/src/app/lib/unwrap.ts +48 -0
  64. package/src/app/lib/utils.ts +10 -0
  65. package/src/app/stores/authStore.ts +80 -0
  66. package/src/app/stores/storage.ts +40 -0
  67. package/src/app/stores/unitsStore.ts +54 -0
  68. package/src/app/types/index.ts +72 -0
  69. package/src/app/types/query.ts +11 -0
  70. package/src/app/ui/accordion.tsx +54 -0
  71. package/src/app/ui/alert-dialog.tsx +114 -0
  72. package/src/app/ui/alert.tsx +49 -0
  73. package/src/app/ui/avatar.tsx +45 -0
  74. package/src/app/ui/badge-list.tsx +51 -0
  75. package/src/app/ui/badge.tsx +33 -0
  76. package/src/app/ui/button.tsx +49 -0
  77. package/src/app/ui/callout.tsx +22 -0
  78. package/src/app/ui/card.tsx +55 -0
  79. package/src/app/ui/checkbox.tsx +26 -0
  80. package/src/app/ui/clear-button.tsx +42 -0
  81. package/src/app/ui/command.tsx +140 -0
  82. package/src/app/ui/country-dropdown.tsx +184 -0
  83. package/src/app/ui/dialog.tsx +104 -0
  84. package/src/app/ui/dropdown-menu.tsx +185 -0
  85. package/src/app/ui/form.tsx +172 -0
  86. package/src/app/ui/index.ts +36 -0
  87. package/src/app/ui/input.tsx +22 -0
  88. package/src/app/ui/label.tsx +19 -0
  89. package/src/app/ui/option-select.tsx +56 -0
  90. package/src/app/ui/pagination.tsx +90 -0
  91. package/src/app/ui/popover.tsx +29 -0
  92. package/src/app/ui/progress.tsx +25 -0
  93. package/src/app/ui/search-input.tsx +63 -0
  94. package/src/app/ui/select.tsx +151 -0
  95. package/src/app/ui/separator.tsx +24 -0
  96. package/src/app/ui/sheet.tsx +119 -0
  97. package/src/app/ui/slider.tsx +23 -0
  98. package/src/app/ui/switch.tsx +27 -0
  99. package/src/app/ui/textarea.tsx +21 -0
  100. package/src/app/ui/tooltip.tsx +32 -0
  101. package/src/artifact/ArtifactShell.tsx +2 -2
  102. package/src/artifact/Gate.tsx +2 -2
  103. package/src/artifact/Writeup.tsx +7 -7
  104. package/src/chrome/SiteShell.tsx +141 -0
  105. package/src/chrome/index.ts +2 -0
  106. package/src/components/Disclosure.tsx +80 -14
  107. package/src/components/GateLogin.tsx +1 -1
  108. package/src/components/Watermark.tsx +1 -1
  109. package/src/components/index.ts +6 -2
  110. package/src/gate/index.ts +1 -1
  111. package/src/gate/middleware.ts +17 -6
  112. package/src/gate/wall.ts +60 -2
  113. package/src/lib/basePathAudit.ts +96 -0
  114. package/src/lib/displayDate.ts +25 -0
  115. package/src/lib/honeypot.ts +11 -0
  116. package/src/lib/hours.ts +28 -0
  117. package/src/lib/sitemap.ts +34 -0
  118. package/src/lib/trackEvent.ts +26 -0
  119. package/src/lib/useOpenHeight.ts +28 -0
  120. package/src/primitives/Button.tsx +3 -1
  121. package/src/primitives/Chip.tsx +3 -1
  122. package/src/primitives/ImageFrame.tsx +48 -0
  123. package/src/primitives/ProseBlocks.tsx +67 -0
  124. package/src/primitives/QuoteCard.tsx +20 -0
  125. package/src/primitives/StarRow.tsx +37 -0
  126. package/src/primitives/TickList.tsx +24 -0
  127. package/src/primitives/index.ts +5 -0
  128. package/src/routing/BasePathProvider.tsx +47 -0
  129. package/src/routing/basePath.ts +57 -0
  130. package/src/routing/index.ts +8 -0
  131. package/src/scripts/check-base-path.ts +130 -0
  132. package/src/scripts/clean-sitemap.ts +27 -0
  133. package/src/scripts/contact-canary.ts +98 -0
  134. package/src/scripts/serve-static.ts +29 -3
  135. package/src/sections/BlogIndex.tsx +57 -0
  136. package/src/sections/BlogPost.tsx +65 -0
  137. package/src/sections/CTABanner.tsx +93 -0
  138. package/src/sections/CTASection.tsx +60 -0
  139. package/src/sections/ComparisonTable.tsx +114 -0
  140. package/src/sections/ContactForm.tsx +163 -0
  141. package/src/sections/ContactInfoCard.tsx +92 -0
  142. package/src/sections/CredentialList.tsx +28 -0
  143. package/src/sections/DeviceShowcase.tsx +39 -0
  144. package/src/sections/ExpandList.tsx +58 -0
  145. package/src/sections/FAQBrowser.tsx +283 -0
  146. package/src/sections/FeatureDetail.tsx +60 -0
  147. package/src/sections/FeatureGrid.tsx +83 -0
  148. package/src/sections/Gallery.tsx +84 -0
  149. package/src/sections/Hero.tsx +110 -0
  150. package/src/sections/HoursTable.tsx +23 -0
  151. package/src/sections/JumpRail.tsx +35 -0
  152. package/src/sections/LegalPage.tsx +43 -0
  153. package/src/sections/LocationSection.tsx +66 -0
  154. package/src/sections/Locations.tsx +71 -0
  155. package/src/sections/LogoMarquee.tsx +27 -0
  156. package/src/sections/MapEmbed.tsx +75 -0
  157. package/src/sections/MediaBand.tsx +20 -0
  158. package/src/sections/MenuList.tsx +44 -0
  159. package/src/sections/PageHero.tsx +25 -0
  160. package/src/sections/PressQuotes.tsx +51 -0
  161. package/src/sections/PricingBand.tsx +43 -0
  162. package/src/sections/PricingCard.tsx +36 -0
  163. package/src/sections/ProcessSteps.tsx +45 -0
  164. package/src/sections/ProfileCards.tsx +73 -0
  165. package/src/sections/RevealSlider.tsx +156 -0
  166. package/src/sections/ReviewCarousel.tsx +223 -0
  167. package/src/sections/ReviewWall.tsx +220 -0
  168. package/src/sections/SocialStrip.tsx +83 -0
  169. package/src/sections/SplitSection.tsx +56 -0
  170. package/src/sections/index.ts +76 -0
  171. package/src/sections/siteConfig.ts +201 -0
  172. package/src/styles/app.css +673 -0
  173. package/src/styles/artifact.css +472 -399
  174. package/src/styles/base.css +49 -47
  175. package/src/styles/chrome.css +71 -71
  176. package/src/styles/components.css +150 -140
  177. package/src/styles/gate.css +29 -29
  178. package/src/styles/primitives.css +190 -25
  179. package/src/styles/sections.css +2652 -0
  180. package/src/styles/star-rating.css +2 -2
  181. package/src/vite/index.ts +9 -2
  182. package/src/vite/marketing-config.ts +128 -0
  183. package/src/components/Collapsible.tsx +0 -43
  184. /package/src/scripts/{build-gate-payload.mjs → seal-document.mjs} +0 -0
@@ -0,0 +1,673 @@
1
+ /*
2
+ * The app shell's stylesheet: the shadcn token mapping, the default light and
3
+ * dark token values, the base layer, and the AppPage scaffold (the ap-*
4
+ * classes, the house buttons, the breadcrumbs, the section cards, the right
5
+ * rail). It is Tailwind source, imported inside an app's Tailwind entry
6
+ * straight after `@import "tailwindcss"` (an @import below any other rule is
7
+ * dropped), with the app's own theme after it to override any value here:
8
+ *
9
+ * @import "tailwindcss";
10
+ * @import "@atelic-action/ui/styles/app.css";
11
+ * @plugin "tailwindcss-animate";
12
+ *
13
+ * Tailwind never scans node_modules on its own, so the @source below points
14
+ * it at the shell's components, relative to this file, and the app adds no
15
+ * line of its own. An app that keeps its own tokens and does not import this
16
+ * sheet adds the same thing from its stylesheet instead, relative to that
17
+ * file: `@source "../../node_modules/@atelic-action/ui/src/app";` from
18
+ * src/styles/globals.css.
19
+ *
20
+ * Unlike the package's other sheets this one is not in the atelic-ui layer:
21
+ * the tokens and resets sit in Tailwind's base layer and the scaffold stays
22
+ * unlayered, exactly as the template carried them, so the shell renders as
23
+ * it did there. Fonts stay with the app, which serves the files.
24
+ */
25
+
26
+ @custom-variant dark (&:is(.dark *));
27
+
28
+ @source "../app";
29
+
30
+ @theme {
31
+ --color-border: hsl(var(--border));
32
+ --color-input: hsl(var(--input));
33
+ --color-ring: hsl(var(--ring));
34
+ --color-background: hsl(var(--background));
35
+ --color-foreground: hsl(var(--foreground));
36
+
37
+ --color-primary: hsl(var(--primary));
38
+ --color-primary-foreground: hsl(var(--primary-foreground));
39
+
40
+ --color-secondary: hsl(var(--secondary));
41
+ --color-secondary-foreground: hsl(var(--secondary-foreground));
42
+
43
+ --color-destructive: hsl(var(--destructive));
44
+ --color-destructive-foreground: hsl(var(--destructive-foreground));
45
+
46
+ --color-muted: hsl(var(--muted));
47
+ --color-muted-foreground: hsl(var(--muted-foreground));
48
+
49
+ --color-accent: hsl(var(--accent));
50
+ --color-accent-foreground: hsl(var(--accent-foreground));
51
+
52
+ --color-card: hsl(var(--card));
53
+ --color-card-foreground: hsl(var(--card-foreground));
54
+
55
+ --color-popover: hsl(var(--popover));
56
+ --color-popover-foreground: hsl(var(--popover-foreground));
57
+
58
+ /* The brand accent. Used sparingly: the dot beside the wordmark, active
59
+ nav indicators, live signals. Never as a fill. */
60
+ --color-brand: hsl(var(--brand));
61
+ --color-brand-soft: hsl(var(--brand) / 0.1);
62
+
63
+ --radius-lg: var(--radius);
64
+ --radius-md: calc(var(--radius) - 2px);
65
+ --radius-sm: calc(var(--radius) - 4px);
66
+
67
+ --font-sans:
68
+ "Geist", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
69
+ sans-serif;
70
+ --font-mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
71
+ --font-serif: "Newsreader", Georgia, serif;
72
+ }
73
+
74
+ @layer base {
75
+ :root {
76
+ /* Paper warmth for surfaces. Cards stay pure white so they sit a
77
+ touch above the page. */
78
+ --background: 60 17% 98%; /* #FAFAF8 */
79
+ --foreground: 240 10% 3.9%;
80
+ --card: 0 0% 100%;
81
+ --card-foreground: 240 10% 3.9%;
82
+ --popover: 0 0% 100%;
83
+ --popover-foreground: 240 10% 3.9%;
84
+ --primary: 240 5.9% 10%;
85
+ --primary-foreground: 0 0% 98%;
86
+ --secondary: 240 4.8% 95.9%;
87
+ --secondary-foreground: 240 5.9% 10%;
88
+ --muted: 240 4.8% 95.9%;
89
+ --muted-foreground: 240 3.8% 46.1%;
90
+ --accent: 240 4.8% 95.9%;
91
+ --accent-foreground: 240 5.9% 10%;
92
+ --destructive: 0 84.2% 60.2%;
93
+ --destructive-foreground: 0 0% 98%;
94
+ --border: 240 5.9% 90%;
95
+ --input: 240 5.9% 90%;
96
+ --ring: 240 5.9% 10%;
97
+ --radius: 0.625rem;
98
+ /* The brand accent, an orange. HSL components only, consumed via
99
+ hsl(var(--brand)). */
100
+ --brand: 19 98% 50%;
101
+ /* Density rhythm for the AppPage scaffold. Default values;
102
+ a `data-density` attribute on the root swaps compact/roomy. We ship
103
+ `default` only — no toggle UI yet. */
104
+ --d-pad-card: 18px;
105
+ --d-pad-screen: 24px;
106
+ --d-gap: 14px;
107
+ --d-row-pad: 14px;
108
+ }
109
+
110
+ [data-density="compact"] {
111
+ --d-pad-card: 14px;
112
+ --d-pad-screen: 18px;
113
+ --d-gap: 10px;
114
+ --d-row-pad: 11px;
115
+ }
116
+ [data-density="roomy"] {
117
+ --d-pad-card: 26px;
118
+ --d-pad-screen: 32px;
119
+ --d-gap: 20px;
120
+ --d-row-pad: 18px;
121
+ }
122
+
123
+ .dark {
124
+ --background: 30 5% 7%;
125
+ --foreground: 30 8% 95%;
126
+ --card: 30 5% 9%;
127
+ --card-foreground: 30 8% 95%;
128
+ --popover: 30 5% 9%;
129
+ --popover-foreground: 30 8% 95%;
130
+ --primary: 30 8% 95%;
131
+ --primary-foreground: 30 8% 8%;
132
+ --secondary: 30 5% 14%;
133
+ --secondary-foreground: 30 8% 95%;
134
+ --muted: 30 5% 14%;
135
+ --muted-foreground: 30 5% 60%;
136
+ --accent: 30 5% 14%;
137
+ --accent-foreground: 30 8% 95%;
138
+ --destructive: 0 62.8% 30.6%;
139
+ --destructive-foreground: 30 8% 95%;
140
+ --border: 30 5% 18%;
141
+ --input: 30 5% 18%;
142
+ --ring: 30 4.9% 83.9%;
143
+ --brand: 19 98% 55%;
144
+ }
145
+ }
146
+
147
+ @layer base {
148
+ * {
149
+ @apply border-border;
150
+ }
151
+ body {
152
+ @apply bg-background text-foreground font-sans antialiased;
153
+ }
154
+ a {
155
+ @apply cursor-pointer;
156
+ }
157
+ }
158
+
159
+ /* ============================================================
160
+ AppPage scaffold: ap-*
161
+ ------------------------------------------------------------
162
+ The common page chrome every authenticated surface
163
+ reuses: a page header (crumb · title · live dot · subtitle ·
164
+ aside), a tools bar (search + filter dropdowns + actions),
165
+ and a canonical full-height right rail. Ported from the
166
+ claude.ai/design prototype (lib/app-page.jsx + styles.css),
167
+ token-remapped to the app's hsl(var(--*)) scale. Page-specific
168
+ content is passed as children; nothing here is page-specific.
169
+ See src/app/layout/AppPage.tsx for the React.
170
+ ============================================================ */
171
+
172
+ /* ---- house buttons (toolbar actions) ---- */
173
+ .btn-ghost {
174
+ display: inline-flex;
175
+ align-items: center;
176
+ gap: 6px;
177
+ background: transparent;
178
+ border: 1px solid hsl(var(--border));
179
+ padding: 7px 12px;
180
+ border-radius: 6px;
181
+ color: hsl(var(--foreground));
182
+ font: 500 13px / 1 var(--font-sans);
183
+ cursor: pointer;
184
+ transition: border-color 150ms ease;
185
+ }
186
+ .btn-ghost:hover {
187
+ border-color: hsl(var(--foreground));
188
+ }
189
+ .btn-ghost:disabled {
190
+ opacity: 0.5;
191
+ cursor: not-allowed;
192
+ }
193
+ .btn-ghost:disabled:hover {
194
+ border-color: hsl(var(--border));
195
+ }
196
+ .btn-solid {
197
+ display: inline-flex;
198
+ align-items: center;
199
+ gap: 6px;
200
+ background: hsl(var(--primary));
201
+ color: hsl(var(--primary-foreground));
202
+ border: none;
203
+ padding: 9px 14px;
204
+ border-radius: 6px;
205
+ font: 500 13px / 1 var(--font-sans);
206
+ cursor: pointer;
207
+ }
208
+ .btn-solid:disabled {
209
+ opacity: 0.5;
210
+ cursor: not-allowed;
211
+ }
212
+
213
+ /* ---- roots ----
214
+ One responsive DOM keyed on the app's lg breakpoint (1024px), matching where
215
+ AppLayout swaps its sidebar in. Mobile flows in the document (the shell's
216
+ body scroll, the shell's bottom nav). Desktop becomes a full-height flex row:
217
+ a main column that scrolls on its own beside an optional full-height rail.
218
+ AppPage does NOT render nav — AppLayout owns the rail / tab bar. */
219
+ .ap-page {
220
+ min-height: 100%;
221
+ background: hsl(var(--background));
222
+ color: hsl(var(--foreground));
223
+ }
224
+ .ap-col {
225
+ display: flex;
226
+ flex-direction: column;
227
+ min-width: 0;
228
+ }
229
+ .ap-scroll::-webkit-scrollbar {
230
+ width: 0;
231
+ height: 0;
232
+ }
233
+
234
+ @media (min-width: 1024px) {
235
+ .ap-page {
236
+ display: flex;
237
+ flex-direction: row;
238
+ height: 100dvh;
239
+ overflow: hidden;
240
+ }
241
+ .ap-col {
242
+ flex: 1;
243
+ min-height: 0;
244
+ }
245
+ .ap-scroll {
246
+ flex: 1;
247
+ overflow-y: auto;
248
+ overflow-x: hidden;
249
+ padding-bottom: 48px;
250
+ scrollbar-width: none;
251
+ }
252
+ }
253
+
254
+ /* ---- header ---- */
255
+ .ap-head {
256
+ padding: 30px var(--d-pad-screen, 24px) 0;
257
+ }
258
+ @media (min-width: 1024px) {
259
+ .ap-head {
260
+ padding: 28px 36px 0;
261
+ }
262
+ }
263
+ .ap-head .ap-crumb {
264
+ display: flex;
265
+ align-items: center;
266
+ justify-content: space-between;
267
+ gap: 16px;
268
+ margin-bottom: 14px;
269
+ }
270
+ /* the crumb content keeps the app's Breadcrumbs styling; only the aside
271
+ (an attribution) sits opposite it. */
272
+ .ap-head .ap-crumb > :first-child {
273
+ min-width: 0;
274
+ }
275
+ .ap-title {
276
+ display: flex;
277
+ align-items: center;
278
+ gap: 13px;
279
+ }
280
+ .ap-title .glyph {
281
+ color: hsl(var(--foreground));
282
+ display: inline-flex;
283
+ }
284
+ .ap-title h1 {
285
+ font: 300 38px / 1 var(--font-sans);
286
+ letter-spacing: -0.028em;
287
+ margin: 0;
288
+ }
289
+ @media (min-width: 1024px) {
290
+ .ap-title h1 {
291
+ font-size: 40px;
292
+ }
293
+ }
294
+ .ap-title .live {
295
+ width: 7px;
296
+ height: 7px;
297
+ border-radius: 9999px;
298
+ background: hsl(var(--brand));
299
+ align-self: flex-start;
300
+ margin-top: 5px;
301
+ }
302
+ .ap-sub {
303
+ margin: 14px 0 0;
304
+ font: 300 21px / 1.36 var(--font-sans);
305
+ letter-spacing: -0.012em;
306
+ color: hsl(var(--foreground));
307
+ max-width: 30ch;
308
+ }
309
+ .ap-sub em {
310
+ font-family: var(--font-serif);
311
+ font-style: italic;
312
+ font-weight: 300;
313
+ }
314
+ @media (min-width: 1024px) {
315
+ .ap-sub {
316
+ max-width: 42ch;
317
+ }
318
+ }
319
+
320
+ /* ---- tools: search + filter dropdowns + actions ---- */
321
+ .ap-tools {
322
+ padding: 24px var(--d-pad-screen, 24px) 4px;
323
+ display: flex;
324
+ flex-direction: column;
325
+ gap: 12px;
326
+ }
327
+ @media (min-width: 1024px) {
328
+ .ap-tools {
329
+ padding: 22px 36px 6px;
330
+ flex-direction: row;
331
+ align-items: center;
332
+ flex-wrap: wrap;
333
+ gap: 12px;
334
+ }
335
+ }
336
+ /* Below lg the row wraps: the search claims the width it needs to be typed
337
+ into and the actions drop beneath it when they would otherwise squeeze it
338
+ to a sliver (a phone with a search and a sync button). Desktop keeps one
339
+ line. */
340
+ .ap-toolrow {
341
+ display: flex;
342
+ flex-wrap: wrap;
343
+ gap: 10px;
344
+ align-items: stretch;
345
+ }
346
+ @media (min-width: 1024px) {
347
+ .ap-toolrow {
348
+ flex: 1;
349
+ flex-wrap: nowrap;
350
+ min-width: 320px;
351
+ }
352
+ }
353
+ .ap-search {
354
+ flex: 1 1 240px;
355
+ min-width: 0;
356
+ display: flex;
357
+ align-items: center;
358
+ gap: 10px;
359
+ padding: 11px 14px;
360
+ border: 1px solid hsl(var(--border));
361
+ border-radius: 12px;
362
+ background: hsl(var(--card));
363
+ color: hsl(var(--muted-foreground));
364
+ transition: border-color 150ms ease;
365
+ }
366
+ .ap-search:focus-within {
367
+ border-color: hsl(var(--foreground));
368
+ }
369
+ .ap-search svg {
370
+ flex-shrink: 0;
371
+ }
372
+ .ap-search input {
373
+ flex: 1;
374
+ min-width: 0;
375
+ border: none;
376
+ background: transparent;
377
+ outline: none;
378
+ font: 400 15px / 1 var(--font-sans);
379
+ color: hsl(var(--foreground));
380
+ }
381
+ .ap-search input::placeholder {
382
+ color: hsl(var(--muted-foreground));
383
+ }
384
+ .ap-search .ap-search-clear {
385
+ border: none;
386
+ background: transparent;
387
+ color: hsl(var(--muted-foreground));
388
+ cursor: pointer;
389
+ display: inline-flex;
390
+ padding: 0;
391
+ }
392
+ .ap-search .ap-search-clear:hover {
393
+ color: hsl(var(--foreground));
394
+ }
395
+ .ap-selects {
396
+ display: flex;
397
+ gap: 10px;
398
+ flex: 0 0 auto;
399
+ }
400
+ .ap-select {
401
+ flex: 1;
402
+ min-width: 0;
403
+ appearance: none;
404
+ -webkit-appearance: none;
405
+ border: 1px solid hsl(var(--border));
406
+ border-radius: 12px;
407
+ background-color: hsl(var(--card));
408
+ color: hsl(var(--foreground));
409
+ font: 500 14px / 1 var(--font-sans);
410
+ padding: 13px 38px 13px 15px;
411
+ cursor: pointer;
412
+ background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none' stroke='%238a8a93' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'><path d='M1 1.5 6 6.5 11 1.5'/></svg>");
413
+ background-repeat: no-repeat;
414
+ background-position: right 14px center;
415
+ transition: border-color 150ms ease;
416
+ }
417
+ .ap-select:hover {
418
+ border-color: hsl(var(--foreground));
419
+ }
420
+ .ap-select:focus {
421
+ outline: none;
422
+ border-color: hsl(var(--foreground));
423
+ }
424
+ @media (min-width: 1024px) {
425
+ .ap-select {
426
+ flex: 0 0 auto;
427
+ min-width: 178px;
428
+ }
429
+ }
430
+ .ap-actions {
431
+ display: flex;
432
+ flex-wrap: wrap;
433
+ align-items: center;
434
+ gap: 10px;
435
+ flex: 0 0 auto;
436
+ }
437
+ .ap-actions .btn-ghost,
438
+ .ap-actions .btn-solid {
439
+ padding: 11px 14px;
440
+ white-space: nowrap;
441
+ }
442
+ .ap-actions .ago {
443
+ font: 500 11px / 1 var(--font-mono);
444
+ color: hsl(var(--muted-foreground));
445
+ letter-spacing: 0.02em;
446
+ }
447
+ .ap-toolnotice {
448
+ display: flex;
449
+ align-items: center;
450
+ gap: 8px;
451
+ font: 500 12px / 1.3 var(--font-mono);
452
+ font-variant-numeric: tabular-nums;
453
+ color: hsl(var(--muted-foreground));
454
+ letter-spacing: 0.02em;
455
+ }
456
+ .ap-toolnotice svg {
457
+ flex-shrink: 0;
458
+ }
459
+
460
+ /* ---- breadcrumbs ----
461
+ Two renderings in one nav, one per breakpoint (see Breadcrumbs.tsx). Below
462
+ lg a page with a parent shows a thumb sized back link and a root page shows
463
+ nothing, its title being the section. At lg and up the full trail runs, the
464
+ current page truncating instead of wrapping. The breakpoint lives here rather than in
465
+ Tailwind classes, since these rules set display and would otherwise beat
466
+ the utility in the cascade. */
467
+ .crumbs {
468
+ min-width: 0;
469
+ }
470
+ .crumbs-back {
471
+ display: inline-flex;
472
+ align-items: center;
473
+ gap: 2px;
474
+ min-height: 40px;
475
+ margin: -10px 0 -8px -8px;
476
+ padding: 0 12px 0 4px;
477
+ border-radius: 10px;
478
+ font: 500 15px / 1 var(--font-sans);
479
+ letter-spacing: -0.01em;
480
+ color: hsl(var(--foreground));
481
+ transition: background-color 150ms ease;
482
+ }
483
+ .crumbs-back:active {
484
+ background: hsl(var(--accent));
485
+ }
486
+ .crumbs-back span {
487
+ overflow: hidden;
488
+ text-overflow: ellipsis;
489
+ white-space: nowrap;
490
+ }
491
+ .crumbs-trail {
492
+ display: none;
493
+ align-items: center;
494
+ gap: 8px;
495
+ min-width: 0;
496
+ font: 500 13px / 1 var(--font-sans);
497
+ color: hsl(var(--muted-foreground));
498
+ }
499
+ @media (min-width: 1024px) {
500
+ .crumbs-back {
501
+ display: none;
502
+ }
503
+ .crumbs-trail {
504
+ display: flex;
505
+ }
506
+ }
507
+ .crumbs-trail li {
508
+ display: inline-flex;
509
+ align-items: center;
510
+ gap: 8px;
511
+ min-width: 0;
512
+ }
513
+ .crumbs-trail li:last-child {
514
+ flex: 0 1 auto;
515
+ }
516
+ .crumbs-trail a {
517
+ color: hsl(var(--foreground));
518
+ white-space: nowrap;
519
+ }
520
+ .crumbs-trail [aria-current="page"] {
521
+ overflow: hidden;
522
+ text-overflow: ellipsis;
523
+ white-space: nowrap;
524
+ max-width: 40ch;
525
+ }
526
+
527
+ /* ---- section landing cards ----
528
+ A section's sub pages as tap targets on its landing page, read from the app's nav.
529
+ Two across on a phone, up to the full row on desktop. */
530
+ .ap-sections {
531
+ display: grid;
532
+ grid-template-columns: repeat(2, minmax(0, 1fr));
533
+ gap: 10px;
534
+ padding: 6px var(--d-pad-screen, 24px) 8px;
535
+ }
536
+ @media (min-width: 640px) {
537
+ .ap-sections {
538
+ grid-template-columns: repeat(3, minmax(0, 1fr));
539
+ gap: 12px;
540
+ }
541
+ }
542
+ @media (min-width: 1024px) {
543
+ .ap-sections {
544
+ grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
545
+ padding: 6px 36px 8px;
546
+ }
547
+ }
548
+ .ap-section-card {
549
+ display: flex;
550
+ flex-direction: column;
551
+ gap: 10px;
552
+ min-height: 108px;
553
+ padding: 14px 14px 12px;
554
+ border: 1px solid hsl(var(--border));
555
+ border-radius: 14px;
556
+ background: hsl(var(--card));
557
+ color: inherit;
558
+ transition: border-color 150ms ease;
559
+ }
560
+ .ap-section-card:hover {
561
+ border-color: hsl(var(--foreground));
562
+ }
563
+ .ap-section-card:active {
564
+ background: hsl(var(--accent));
565
+ }
566
+ .ap-section-card .ic {
567
+ display: inline-flex;
568
+ color: hsl(var(--foreground));
569
+ }
570
+ .ap-section-card .lb {
571
+ font: 500 15px / 1.2 var(--font-sans);
572
+ letter-spacing: -0.01em;
573
+ }
574
+ .ap-section-card .bl {
575
+ font: 400 12px / 1.35 var(--font-sans);
576
+ color: hsl(var(--muted-foreground));
577
+ }
578
+
579
+ /* Generic content padding for AppPage children that don't bring their own
580
+ (e.g. a grid of cards). Matches the scaffold's screen padding. */
581
+ .ap-body {
582
+ padding: 18px var(--d-pad-screen, 24px) 8px;
583
+ }
584
+ @media (min-width: 1024px) {
585
+ .ap-body {
586
+ padding: 18px 36px 8px;
587
+ }
588
+ }
589
+
590
+ /* ---- canonical right rail ----
591
+ A page may pass a `rail` to AppPage. On mobile it stacks inline beneath the
592
+ header (.ap-rail-inline, inside the scroll flow). On desktop it becomes a
593
+ true full-height right column (.ap-rail) that mirrors the left nav rail,
594
+ divided by a hairline running the entire height, scrolling independently
595
+ while the main column scrolls to its left. The rail node is rendered in both
596
+ slots; CSS shows exactly one per breakpoint. */
597
+ .ap-rail-inline {
598
+ padding: 22px var(--d-pad-screen, 24px) 0;
599
+ }
600
+ .ap-rail {
601
+ display: none;
602
+ }
603
+ @media (min-width: 1024px) {
604
+ .ap-rail-inline {
605
+ display: none;
606
+ }
607
+ .ap-rail {
608
+ display: block;
609
+ width: 344px;
610
+ flex-shrink: 0;
611
+ border-left: 1px solid hsl(var(--border));
612
+ overflow-y: auto;
613
+ padding: 30px 32px 40px;
614
+ scrollbar-width: none;
615
+ transition:
616
+ width 200ms ease-out,
617
+ padding 200ms ease-out,
618
+ border-color 200ms ease-out,
619
+ visibility 200ms ease-out;
620
+ }
621
+ .ap-rail::-webkit-scrollbar {
622
+ width: 0;
623
+ height: 0;
624
+ }
625
+ /* Collapsed desktop rail: width animates to 0 and the main column reclaims
626
+ the space (it is flex:1). Mirrors the left nav-rail collapse. `visibility:
627
+ hidden` (which snaps at the end of the collapse and the start of the expand)
628
+ pulls the rail's interactive elements out of the tab order while hidden. The
629
+ mobile inline rail (.ap-rail-inline) is a separate node and is never collapsed. */
630
+ .ap-rail.is-collapsed {
631
+ width: 0;
632
+ padding-left: 0;
633
+ padding-right: 0;
634
+ border-left-color: transparent;
635
+ overflow: hidden;
636
+ visibility: hidden;
637
+ }
638
+ }
639
+
640
+ /* Desktop-only rail collapse toggle, rendered in the AppPage header opposite
641
+ the breadcrumb. Hidden on mobile, where the rail is always inline. */
642
+ .ap-rail-toggle {
643
+ display: none;
644
+ }
645
+ .ap-head .ap-crumb-actions {
646
+ display: flex;
647
+ align-items: center;
648
+ gap: 12px;
649
+ flex-shrink: 0;
650
+ }
651
+ @media (min-width: 1024px) {
652
+ .ap-rail-toggle {
653
+ display: inline-flex;
654
+ align-items: center;
655
+ justify-content: center;
656
+ width: 34px;
657
+ height: 34px;
658
+ border-radius: 8px;
659
+ color: hsl(var(--muted-foreground));
660
+ flex-shrink: 0;
661
+ transition:
662
+ background-color 150ms ease,
663
+ color 150ms ease;
664
+ }
665
+ .ap-rail-toggle:hover {
666
+ background: hsl(var(--accent));
667
+ color: hsl(var(--foreground));
668
+ }
669
+ .ap-rail-toggle:focus-visible {
670
+ outline: 2px solid hsl(var(--ring));
671
+ outline-offset: 2px;
672
+ }
673
+ }