@storyshelf/app 0.5.3

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 (268) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +18 -0
  3. package/dist/app-types.d.mts +54 -0
  4. package/dist/asset-manifest.mjs +41 -0
  5. package/dist/asset-manifest.mjs.map +1 -0
  6. package/dist/asset-paths.mjs +23 -0
  7. package/dist/asset-paths.mjs.map +1 -0
  8. package/dist/assets/htmx.min.js +3304 -0
  9. package/dist/assets/mark.svg +1 -0
  10. package/dist/capture-setup.mjs +139 -0
  11. package/dist/capture-setup.mjs.map +1 -0
  12. package/dist/credential-keys.mjs +20 -0
  13. package/dist/credential-keys.mjs.map +1 -0
  14. package/dist/credentials.mjs +56 -0
  15. package/dist/credentials.mjs.map +1 -0
  16. package/dist/index.d.mts +18 -0
  17. package/dist/index.mjs +310 -0
  18. package/dist/index.mjs.map +1 -0
  19. package/dist/lifecycle.mjs +132 -0
  20. package/dist/lifecycle.mjs.map +1 -0
  21. package/dist/middleware/auth-gate.mjs +36 -0
  22. package/dist/middleware/auth-gate.mjs.map +1 -0
  23. package/dist/middleware/csrf.mjs +75 -0
  24. package/dist/middleware/csrf.mjs.map +1 -0
  25. package/dist/middleware/rate-limit.mjs +68 -0
  26. package/dist/middleware/rate-limit.mjs.map +1 -0
  27. package/dist/middleware/request-log.mjs +30 -0
  28. package/dist/middleware/request-log.mjs.map +1 -0
  29. package/dist/middleware/server-timing.mjs +39 -0
  30. package/dist/middleware/server-timing.mjs.map +1 -0
  31. package/dist/middleware/setup-gate.mjs +37 -0
  32. package/dist/middleware/setup-gate.mjs.map +1 -0
  33. package/dist/middleware/store-scope.mjs +33 -0
  34. package/dist/middleware/store-scope.mjs.map +1 -0
  35. package/dist/notify.d.mts +17 -0
  36. package/dist/notify.mjs +141 -0
  37. package/dist/notify.mjs.map +1 -0
  38. package/dist/pages/admin-system.mjs +243 -0
  39. package/dist/pages/admin-system.mjs.map +1 -0
  40. package/dist/pages/build-detail.mjs +392 -0
  41. package/dist/pages/build-detail.mjs.map +1 -0
  42. package/dist/pages/build-diff-actions.mjs +162 -0
  43. package/dist/pages/build-diff-actions.mjs.map +1 -0
  44. package/dist/pages/build-diff-comments.mjs +104 -0
  45. package/dist/pages/build-diff-comments.mjs.map +1 -0
  46. package/dist/pages/build-diff-header.mjs +117 -0
  47. package/dist/pages/build-diff-header.mjs.map +1 -0
  48. package/dist/pages/build-diff-model.mjs +48 -0
  49. package/dist/pages/build-diff-model.mjs.map +1 -0
  50. package/dist/pages/build-diff-nav.mjs +122 -0
  51. package/dist/pages/build-diff-nav.mjs.map +1 -0
  52. package/dist/pages/build-diff-shortcuts.mjs +78 -0
  53. package/dist/pages/build-diff-shortcuts.mjs.map +1 -0
  54. package/dist/pages/build-diff-stale.mjs +70 -0
  55. package/dist/pages/build-diff-stale.mjs.map +1 -0
  56. package/dist/pages/build-diff-viewer.mjs +70 -0
  57. package/dist/pages/build-diff-viewer.mjs.map +1 -0
  58. package/dist/pages/build-diff.mjs +110 -0
  59. package/dist/pages/build-diff.mjs.map +1 -0
  60. package/dist/pages/compute-jobs.mjs +165 -0
  61. package/dist/pages/compute-jobs.mjs.map +1 -0
  62. package/dist/pages/invite.mjs +91 -0
  63. package/dist/pages/invite.mjs.map +1 -0
  64. package/dist/pages/label-detail.mjs +154 -0
  65. package/dist/pages/label-detail.mjs.map +1 -0
  66. package/dist/pages/library.mjs +343 -0
  67. package/dist/pages/library.mjs.map +1 -0
  68. package/dist/pages/login.mjs +117 -0
  69. package/dist/pages/login.mjs.map +1 -0
  70. package/dist/pages/passkey-ceremony.mjs +97 -0
  71. package/dist/pages/passkey-ceremony.mjs.map +1 -0
  72. package/dist/pages/profile.mjs +364 -0
  73. package/dist/pages/profile.mjs.map +1 -0
  74. package/dist/pages/project-builds-table.mjs +151 -0
  75. package/dist/pages/project-builds-table.mjs.map +1 -0
  76. package/dist/pages/project-builds.mjs +143 -0
  77. package/dist/pages/project-builds.mjs.map +1 -0
  78. package/dist/pages/project-create.mjs +90 -0
  79. package/dist/pages/project-create.mjs.map +1 -0
  80. package/dist/pages/project-settings.mjs +95 -0
  81. package/dist/pages/project-settings.mjs.map +1 -0
  82. package/dist/pages/projects-card.mjs +194 -0
  83. package/dist/pages/projects-card.mjs.map +1 -0
  84. package/dist/pages/projects.mjs +129 -0
  85. package/dist/pages/projects.mjs.map +1 -0
  86. package/dist/pages/root.mjs +98 -0
  87. package/dist/pages/root.mjs.map +1 -0
  88. package/dist/pages/settings-general.mjs +138 -0
  89. package/dist/pages/settings-general.mjs.map +1 -0
  90. package/dist/pages/settings-labels.mjs +107 -0
  91. package/dist/pages/settings-labels.mjs.map +1 -0
  92. package/dist/pages/settings-members.mjs +192 -0
  93. package/dist/pages/settings-members.mjs.map +1 -0
  94. package/dist/pages/settings-notifications.mjs +240 -0
  95. package/dist/pages/settings-notifications.mjs.map +1 -0
  96. package/dist/pages/settings-status.mjs +137 -0
  97. package/dist/pages/settings-status.mjs.map +1 -0
  98. package/dist/pages/settings-tests.mjs +111 -0
  99. package/dist/pages/settings-tests.mjs.map +1 -0
  100. package/dist/pages/settings-tokens.mjs +90 -0
  101. package/dist/pages/settings-tokens.mjs.map +1 -0
  102. package/dist/pages/settings-webhooks.mjs +104 -0
  103. package/dist/pages/settings-webhooks.mjs.map +1 -0
  104. package/dist/pages/storybook.mjs +116 -0
  105. package/dist/pages/storybook.mjs.map +1 -0
  106. package/dist/retention-timer.mjs +65 -0
  107. package/dist/retention-timer.mjs.map +1 -0
  108. package/dist/routers/admin-pages.mjs +48 -0
  109. package/dist/routers/admin-pages.mjs.map +1 -0
  110. package/dist/routers/admin.mjs +113 -0
  111. package/dist/routers/admin.mjs.map +1 -0
  112. package/dist/routers/assets.mjs +64 -0
  113. package/dist/routers/assets.mjs.map +1 -0
  114. package/dist/routers/attempts.mjs +96 -0
  115. package/dist/routers/attempts.mjs.map +1 -0
  116. package/dist/routers/auth-sync.mjs +64 -0
  117. package/dist/routers/auth-sync.mjs.map +1 -0
  118. package/dist/routers/auth.mjs +236 -0
  119. package/dist/routers/auth.mjs.map +1 -0
  120. package/dist/routers/baseline-guard.mjs +83 -0
  121. package/dist/routers/baseline-guard.mjs.map +1 -0
  122. package/dist/routers/builds.handlers.mjs +217 -0
  123. package/dist/routers/builds.handlers.mjs.map +1 -0
  124. package/dist/routers/builds.mjs +381 -0
  125. package/dist/routers/builds.mjs.map +1 -0
  126. package/dist/routers/comments.mjs +109 -0
  127. package/dist/routers/comments.mjs.map +1 -0
  128. package/dist/routers/flash.mjs +35 -0
  129. package/dist/routers/flash.mjs.map +1 -0
  130. package/dist/routers/health-report.mjs +159 -0
  131. package/dist/routers/health-report.mjs.map +1 -0
  132. package/dist/routers/health.mjs +46 -0
  133. package/dist/routers/health.mjs.map +1 -0
  134. package/dist/routers/helpers.mjs +110 -0
  135. package/dist/routers/helpers.mjs.map +1 -0
  136. package/dist/routers/htmx.mjs +24 -0
  137. package/dist/routers/htmx.mjs.map +1 -0
  138. package/dist/routers/labels.mjs +118 -0
  139. package/dist/routers/labels.mjs.map +1 -0
  140. package/dist/routers/media.mjs +53 -0
  141. package/dist/routers/media.mjs.map +1 -0
  142. package/dist/routers/members.mjs +177 -0
  143. package/dist/routers/members.mjs.map +1 -0
  144. package/dist/routers/notification-channels.mjs +190 -0
  145. package/dist/routers/notification-channels.mjs.map +1 -0
  146. package/dist/routers/notification-subscriptions.mjs +120 -0
  147. package/dist/routers/notification-subscriptions.mjs.map +1 -0
  148. package/dist/routers/profile.mjs +276 -0
  149. package/dist/routers/profile.mjs.map +1 -0
  150. package/dist/routers/projects.mjs +133 -0
  151. package/dist/routers/projects.mjs.map +1 -0
  152. package/dist/routers/review-next.mjs +47 -0
  153. package/dist/routers/review-next.mjs.map +1 -0
  154. package/dist/routers/schemas.mjs +337 -0
  155. package/dist/routers/schemas.mjs.map +1 -0
  156. package/dist/routers/settings-general.mjs +104 -0
  157. package/dist/routers/settings-general.mjs.map +1 -0
  158. package/dist/routers/settings-labels.mjs +52 -0
  159. package/dist/routers/settings-labels.mjs.map +1 -0
  160. package/dist/routers/settings-members.mjs +58 -0
  161. package/dist/routers/settings-members.mjs.map +1 -0
  162. package/dist/routers/settings-notifications.mjs +108 -0
  163. package/dist/routers/settings-notifications.mjs.map +1 -0
  164. package/dist/routers/settings-status.mjs +77 -0
  165. package/dist/routers/settings-status.mjs.map +1 -0
  166. package/dist/routers/settings-tokens.mjs +40 -0
  167. package/dist/routers/settings-tokens.mjs.map +1 -0
  168. package/dist/routers/settings-webhooks.mjs +67 -0
  169. package/dist/routers/settings-webhooks.mjs.map +1 -0
  170. package/dist/routers/settings.handlers.mjs +149 -0
  171. package/dist/routers/settings.handlers.mjs.map +1 -0
  172. package/dist/routers/settings.mjs +25 -0
  173. package/dist/routers/settings.mjs.map +1 -0
  174. package/dist/routers/snapshots.mjs +240 -0
  175. package/dist/routers/snapshots.mjs.map +1 -0
  176. package/dist/routers/status-configs.mjs +108 -0
  177. package/dist/routers/status-configs.mjs.map +1 -0
  178. package/dist/routers/storybook.mjs +171 -0
  179. package/dist/routers/storybook.mjs.map +1 -0
  180. package/dist/routers/tokens.mjs +91 -0
  181. package/dist/routers/tokens.mjs.map +1 -0
  182. package/dist/routers/ui.mjs +166 -0
  183. package/dist/routers/ui.mjs.map +1 -0
  184. package/dist/routers/webhooks.mjs +97 -0
  185. package/dist/routers/webhooks.mjs.map +1 -0
  186. package/dist/routers/well-known.mjs +43 -0
  187. package/dist/routers/well-known.mjs.map +1 -0
  188. package/dist/runtime.mjs +52 -0
  189. package/dist/runtime.mjs.map +1 -0
  190. package/dist/store.mjs +20 -0
  191. package/dist/store.mjs.map +1 -0
  192. package/dist/ui/avatar.mjs +76 -0
  193. package/dist/ui/avatar.mjs.map +1 -0
  194. package/dist/ui/buttons.mjs +205 -0
  195. package/dist/ui/buttons.mjs.map +1 -0
  196. package/dist/ui/code-block.mjs +47 -0
  197. package/dist/ui/code-block.mjs.map +1 -0
  198. package/dist/ui/compare-stage.mjs +344 -0
  199. package/dist/ui/compare-stage.mjs.map +1 -0
  200. package/dist/ui/csrf-field.mjs +20 -0
  201. package/dist/ui/csrf-field.mjs.map +1 -0
  202. package/dist/ui/css.mjs +36 -0
  203. package/dist/ui/css.mjs.map +1 -0
  204. package/dist/ui/document.mjs +130 -0
  205. package/dist/ui/document.mjs.map +1 -0
  206. package/dist/ui/dropdown.mjs +181 -0
  207. package/dist/ui/dropdown.mjs.map +1 -0
  208. package/dist/ui/feedback.mjs +341 -0
  209. package/dist/ui/feedback.mjs.map +1 -0
  210. package/dist/ui/filter-input.mjs +58 -0
  211. package/dist/ui/filter-input.mjs.map +1 -0
  212. package/dist/ui/forms.mjs +291 -0
  213. package/dist/ui/forms.mjs.map +1 -0
  214. package/dist/ui/icons/icon.mjs +53 -0
  215. package/dist/ui/icons/icon.mjs.map +1 -0
  216. package/dist/ui/icons/paths.mjs +61 -0
  217. package/dist/ui/icons/paths.mjs.map +1 -0
  218. package/dist/ui/icons/sprite.mjs +26 -0
  219. package/dist/ui/icons/sprite.mjs.map +1 -0
  220. package/dist/ui/kbd.mjs +33 -0
  221. package/dist/ui/kbd.mjs.map +1 -0
  222. package/dist/ui/layout.mjs +173 -0
  223. package/dist/ui/layout.mjs.map +1 -0
  224. package/dist/ui/progress.mjs +44 -0
  225. package/dist/ui/progress.mjs.map +1 -0
  226. package/dist/ui/segmented.mjs +77 -0
  227. package/dist/ui/segmented.mjs.map +1 -0
  228. package/dist/ui/shell/account.mjs +161 -0
  229. package/dist/ui/shell/account.mjs.map +1 -0
  230. package/dist/ui/shell/bare.mjs +69 -0
  231. package/dist/ui/shell/bare.mjs.map +1 -0
  232. package/dist/ui/shell/boot-script.mjs +46 -0
  233. package/dist/ui/shell/boot-script.mjs.map +1 -0
  234. package/dist/ui/shell/client-script.mjs +259 -0
  235. package/dist/ui/shell/client-script.mjs.map +1 -0
  236. package/dist/ui/shell/nav.mjs +111 -0
  237. package/dist/ui/shell/nav.mjs.map +1 -0
  238. package/dist/ui/shell/review-script.mjs +195 -0
  239. package/dist/ui/shell/review-script.mjs.map +1 -0
  240. package/dist/ui/shell/sidebar.mjs +323 -0
  241. package/dist/ui/shell/sidebar.mjs.map +1 -0
  242. package/dist/ui/shell/topbar.mjs +268 -0
  243. package/dist/ui/shell/topbar.mjs.map +1 -0
  244. package/dist/ui/stacks.mjs +82 -0
  245. package/dist/ui/stacks.mjs.map +1 -0
  246. package/dist/ui/styles/base.mjs +41 -0
  247. package/dist/ui/styles/base.mjs.map +1 -0
  248. package/dist/ui/styles/behavior.mjs +43 -0
  249. package/dist/ui/styles/behavior.mjs.map +1 -0
  250. package/dist/ui/styles/review-layout.mjs +347 -0
  251. package/dist/ui/styles/review-layout.mjs.map +1 -0
  252. package/dist/ui/styles/review.mjs +86 -0
  253. package/dist/ui/styles/review.mjs.map +1 -0
  254. package/dist/ui/styles/tokens.mjs +132 -0
  255. package/dist/ui/styles/tokens.mjs.map +1 -0
  256. package/dist/ui/styles.mjs +19 -0
  257. package/dist/ui/styles.mjs.map +1 -0
  258. package/dist/ui/subnav.mjs +91 -0
  259. package/dist/ui/subnav.mjs.map +1 -0
  260. package/dist/ui/table.mjs +73 -0
  261. package/dist/ui/table.mjs.map +1 -0
  262. package/dist/ui/theme.mjs +64 -0
  263. package/dist/ui/theme.mjs.map +1 -0
  264. package/dist/ui/thumbnail.mjs +51 -0
  265. package/dist/ui/thumbnail.mjs.map +1 -0
  266. package/dist/ui/time.mjs +50 -0
  267. package/dist/ui/time.mjs.map +1 -0
  268. package/package.json +92 -0
@@ -0,0 +1,132 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ //#region src/ui/styles/tokens.ts
5
+ /**
6
+ * CSS variable tokens: brand-driven colors (per theme), theme-independent
7
+ * scales (type, space, radius, motion), and tokens derived from the brand
8
+ * accent/status colors so customer themes need no extra fields.
9
+ */
10
+ function tokenCss(light, dark) {
11
+ return [
12
+ scaleTokens(),
13
+ `:root {\n${colorVars(light, LIGHT_FALLBACK)}\n}`,
14
+ `[data-theme="dark"] {\n${colorVars(dark, DARK_FALLBACK)}\n}`,
15
+ `@media (prefers-color-scheme: dark) {\n[data-theme="system"] {\n${colorVars(dark, DARK_FALLBACK)}\n}\n}`
16
+ ].join("\n");
17
+ }
18
+ const LIGHT_FALLBACK = {
19
+ accentContrast: "#fff",
20
+ muted: "#f4f4f5",
21
+ subtle: "#fafafa",
22
+ sidebar: "#ffffff",
23
+ shadow: "0 1px 2px rgba(0,0,0,.04)",
24
+ shadow2: "0 4px 12px rgba(0,0,0,.08)",
25
+ shadow3: "0 12px 32px rgba(0,0,0,.14)",
26
+ tint: "10%"
27
+ };
28
+ const DARK_FALLBACK = {
29
+ accentContrast: "#09090b",
30
+ muted: "#27272a",
31
+ subtle: "#18181b",
32
+ sidebar: "#111113",
33
+ shadow: "0 1px 2px rgba(0,0,0,.3)",
34
+ shadow2: "0 4px 12px rgba(0,0,0,.4)",
35
+ shadow3: "0 12px 32px rgba(0,0,0,.55)",
36
+ tint: "16%"
37
+ };
38
+ /** Theme-independent scales shared by every component. */
39
+ function scaleTokens() {
40
+ return `
41
+ :root {
42
+ --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
43
+ --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
44
+ --text-xs: 0.75rem;
45
+ --text-sm: 0.8125rem;
46
+ --text-base: 0.875rem;
47
+ --text-lg: 1rem;
48
+ --text-xl: 1.25rem;
49
+ --text-2xl: 1.5rem;
50
+ --leading-tight: 1.25;
51
+ --leading-normal: 1.5;
52
+ --space-1: 0.25rem;
53
+ --space-2: 0.5rem;
54
+ --space-3: 0.75rem;
55
+ --space-4: 1rem;
56
+ --space-5: 1.25rem;
57
+ --space-6: 1.5rem;
58
+ --space-8: 2rem;
59
+ --radius-pill: 999px;
60
+ --ease: cubic-bezier(0.2, 0, 0, 1);
61
+ --dur-fast: 120ms;
62
+ --dur-base: 200ms;
63
+ --sidebar-width: 220px;
64
+ --topbar-height: 52px;
65
+ }
66
+ @media (prefers-reduced-motion: reduce) {
67
+ :root { --dur-fast: 0ms; --dur-base: 0ms; }
68
+ }`;
69
+ }
70
+ /** Brand colors plus the tokens derived from them, for one theme. */
71
+ function colorVars(theme, fb) {
72
+ return [brandVars(theme, fb), derivedVars(fb)].join("\n");
73
+ }
74
+ function brandVars(theme, fb) {
75
+ const radius = theme.radius ?? "0.5rem";
76
+ const radiusSm = theme.radiusSm ?? "0.375rem";
77
+ return `
78
+ --accent: ${theme.accent};
79
+ --accent-contrast: ${theme.accentContrast ?? fb.accentContrast};
80
+ --ring: ${theme.ring ?? theme.accent};
81
+ --surface-base: ${theme.surface.base};
82
+ --surface-card: ${theme.surface.card};
83
+ --surface-muted: ${theme.surface.muted ?? fb.muted};
84
+ --surface-subtle: ${theme.surface.subtle ?? fb.subtle};
85
+ --text-primary: ${theme.text.primary};
86
+ --text-secondary: ${theme.text.secondary};
87
+ --text-muted: ${theme.text.muted ?? theme.text.secondary};
88
+ --border: ${theme.border};
89
+ --border-subtle: ${theme.borderSubtle ?? theme.border};
90
+ --radius: ${radius};
91
+ --radius-sm: ${radiusSm};
92
+ --radius-lg: calc(${radius} + 0.25rem);
93
+ --shadow: ${theme.shadow ?? fb.shadow};
94
+ --shadow-2: ${fb.shadow2};
95
+ --shadow-3: ${fb.shadow3};
96
+ --status-approved: ${theme.status.approved};
97
+ --status-new: ${theme.status.new};
98
+ --status-rejected: ${theme.status.rejected};
99
+ --sidebar-bg: ${theme.sidebarBg ?? fb.sidebar};`;
100
+ }
101
+ function mix(color, pct, into) {
102
+ return `color-mix(in srgb, var(${color}) ${pct}, var(${into}))`;
103
+ }
104
+ /**
105
+ * Tokens computed from the brand colors via `color-mix`. They resolve on
106
+ * the same element as the brand vars, so a customer accent flows through
107
+ * without extra config. The accent only ever tints; it never floods chrome.
108
+ */
109
+ function derivedVars(fb) {
110
+ return `
111
+ --accent-subtle: ${mix("--accent", "8%", "--surface-card")};
112
+ --accent-wash: ${mix("--accent", "4%", "--surface-base")};
113
+ --accent-border: ${mix("--accent", "30%", "--border")};
114
+ --accent-fg: ${mix("--accent", "88%", "--text-primary")};
115
+ --status-info: var(--accent);
116
+ --status-approved-fg: ${mix("--status-approved", "60%", "--text-primary")};
117
+ --status-new-fg: ${mix("--status-new", "60%", "--text-primary")};
118
+ --status-rejected-fg: ${mix("--status-rejected", "60%", "--text-primary")};
119
+ --status-info-fg: var(--accent-fg);
120
+ --status-approved-bg: ${mix("--status-approved", fb.tint, "--surface-card")};
121
+ --status-new-bg: ${mix("--status-new", fb.tint, "--surface-card")};
122
+ --status-rejected-bg: ${mix("--status-rejected", fb.tint, "--surface-card")};
123
+ --status-info-bg: ${mix("--status-info", fb.tint, "--surface-card")};
124
+ --status-approved-border: ${mix("--status-approved", "32%", "--border")};
125
+ --status-new-border: ${mix("--status-new", "32%", "--border")};
126
+ --status-rejected-border: ${mix("--status-rejected", "32%", "--border")};
127
+ --status-info-border: ${mix("--status-info", "32%", "--border")};`;
128
+ }
129
+ //#endregion
130
+ export { tokenCss };
131
+
132
+ //# sourceMappingURL=tokens.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.mjs","names":[],"sources":["../../../src/ui/styles/tokens.ts"],"sourcesContent":["import type { BrandTheme } from \"../theme.ts\";\n\n/**\n * CSS variable tokens: brand-driven colors (per theme), theme-independent\n * scales (type, space, radius, motion), and tokens derived from the brand\n * accent/status colors so customer themes need no extra fields.\n */\nexport function tokenCss(light: BrandTheme, dark: BrandTheme): string {\n return [\n scaleTokens(),\n `:root {\\n${colorVars(light, LIGHT_FALLBACK)}\\n}`,\n `[data-theme=\"dark\"] {\\n${colorVars(dark, DARK_FALLBACK)}\\n}`,\n `@media (prefers-color-scheme: dark) {\\n[data-theme=\"system\"] {\\n${colorVars(dark, DARK_FALLBACK)}\\n}\\n}`,\n ].join(\"\\n\");\n}\n\ninterface Fallbacks {\n accentContrast: string;\n muted: string;\n subtle: string;\n sidebar: string;\n shadow: string;\n shadow2: string;\n shadow3: string;\n /** Share of the status color mixed into tinted backgrounds. */\n tint: string;\n}\n\nconst LIGHT_FALLBACK: Fallbacks = {\n accentContrast: \"#fff\",\n muted: \"#f4f4f5\",\n subtle: \"#fafafa\",\n sidebar: \"#ffffff\",\n shadow: \"0 1px 2px rgba(0,0,0,.04)\",\n shadow2: \"0 4px 12px rgba(0,0,0,.08)\",\n shadow3: \"0 12px 32px rgba(0,0,0,.14)\",\n tint: \"10%\",\n};\n\nconst DARK_FALLBACK: Fallbacks = {\n accentContrast: \"#09090b\",\n muted: \"#27272a\",\n subtle: \"#18181b\",\n sidebar: \"#111113\",\n shadow: \"0 1px 2px rgba(0,0,0,.3)\",\n shadow2: \"0 4px 12px rgba(0,0,0,.4)\",\n shadow3: \"0 12px 32px rgba(0,0,0,.55)\",\n tint: \"16%\",\n};\n\n/** Theme-independent scales shared by every component. */\nfunction scaleTokens(): string {\n return `\n :root {\n --font-sans: ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;\n --text-xs: 0.75rem;\n --text-sm: 0.8125rem;\n --text-base: 0.875rem;\n --text-lg: 1rem;\n --text-xl: 1.25rem;\n --text-2xl: 1.5rem;\n --leading-tight: 1.25;\n --leading-normal: 1.5;\n --space-1: 0.25rem;\n --space-2: 0.5rem;\n --space-3: 0.75rem;\n --space-4: 1rem;\n --space-5: 1.25rem;\n --space-6: 1.5rem;\n --space-8: 2rem;\n --radius-pill: 999px;\n --ease: cubic-bezier(0.2, 0, 0, 1);\n --dur-fast: 120ms;\n --dur-base: 200ms;\n --sidebar-width: 220px;\n --topbar-height: 52px;\n }\n @media (prefers-reduced-motion: reduce) {\n :root { --dur-fast: 0ms; --dur-base: 0ms; }\n }`;\n}\n\n/** Brand colors plus the tokens derived from them, for one theme. */\nfunction colorVars(theme: BrandTheme, fb: Fallbacks): string {\n return [brandVars(theme, fb), derivedVars(fb)].join(\"\\n\");\n}\n\nfunction brandVars(theme: BrandTheme, fb: Fallbacks): string {\n const radius = theme.radius ?? \"0.5rem\";\n const radiusSm = theme.radiusSm ?? \"0.375rem\";\n return `\n --accent: ${theme.accent};\n --accent-contrast: ${theme.accentContrast ?? fb.accentContrast};\n --ring: ${theme.ring ?? theme.accent};\n --surface-base: ${theme.surface.base};\n --surface-card: ${theme.surface.card};\n --surface-muted: ${theme.surface.muted ?? fb.muted};\n --surface-subtle: ${theme.surface.subtle ?? fb.subtle};\n --text-primary: ${theme.text.primary};\n --text-secondary: ${theme.text.secondary};\n --text-muted: ${theme.text.muted ?? theme.text.secondary};\n --border: ${theme.border};\n --border-subtle: ${theme.borderSubtle ?? theme.border};\n --radius: ${radius};\n --radius-sm: ${radiusSm};\n --radius-lg: calc(${radius} + 0.25rem);\n --shadow: ${theme.shadow ?? fb.shadow};\n --shadow-2: ${fb.shadow2};\n --shadow-3: ${fb.shadow3};\n --status-approved: ${theme.status.approved};\n --status-new: ${theme.status.new};\n --status-rejected: ${theme.status.rejected};\n --sidebar-bg: ${theme.sidebarBg ?? fb.sidebar};`;\n}\n\nfunction mix(color: string, pct: string, into: string): string {\n return `color-mix(in srgb, var(${color}) ${pct}, var(${into}))`;\n}\n\n/**\n * Tokens computed from the brand colors via `color-mix`. They resolve on\n * the same element as the brand vars, so a customer accent flows through\n * without extra config. The accent only ever tints; it never floods chrome.\n */\nfunction derivedVars(fb: Fallbacks): string {\n return `\n --accent-subtle: ${mix(\"--accent\", \"8%\", \"--surface-card\")};\n --accent-wash: ${mix(\"--accent\", \"4%\", \"--surface-base\")};\n --accent-border: ${mix(\"--accent\", \"30%\", \"--border\")};\n --accent-fg: ${mix(\"--accent\", \"88%\", \"--text-primary\")};\n --status-info: var(--accent);\n --status-approved-fg: ${mix(\"--status-approved\", \"60%\", \"--text-primary\")};\n --status-new-fg: ${mix(\"--status-new\", \"60%\", \"--text-primary\")};\n --status-rejected-fg: ${mix(\"--status-rejected\", \"60%\", \"--text-primary\")};\n --status-info-fg: var(--accent-fg);\n --status-approved-bg: ${mix(\"--status-approved\", fb.tint, \"--surface-card\")};\n --status-new-bg: ${mix(\"--status-new\", fb.tint, \"--surface-card\")};\n --status-rejected-bg: ${mix(\"--status-rejected\", fb.tint, \"--surface-card\")};\n --status-info-bg: ${mix(\"--status-info\", fb.tint, \"--surface-card\")};\n --status-approved-border: ${mix(\"--status-approved\", \"32%\", \"--border\")};\n --status-new-border: ${mix(\"--status-new\", \"32%\", \"--border\")};\n --status-rejected-border: ${mix(\"--status-rejected\", \"32%\", \"--border\")};\n --status-info-border: ${mix(\"--status-info\", \"32%\", \"--border\")};`;\n}\n"],"mappings":";;;;;;;;;AAOA,SAAgB,SAAS,OAAmB,MAA0B;CACpE,OAAO;EACL,YAAY;EACZ,YAAY,UAAU,OAAO,cAAc,EAAE;EAC7C,0BAA0B,UAAU,MAAM,aAAa,EAAE;EACzD,mEAAmE,UAAU,MAAM,aAAa,EAAE;CACpG,CAAC,CAAC,KAAK,IAAI;AACb;AAcA,MAAM,iBAA4B;CAChC,gBAAgB;CAChB,OAAO;CACP,QAAQ;CACR,SAAS;CACT,QAAQ;CACR,SAAS;CACT,SAAS;CACT,MAAM;AACR;AAEA,MAAM,gBAA2B;CAC/B,gBAAgB;CAChB,OAAO;CACP,QAAQ;CACR,SAAS;CACT,QAAQ;CACR,SAAS;CACT,SAAS;CACT,MAAM;AACR;;AAGA,SAAS,cAAsB;CAC7B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BT;;AAGA,SAAS,UAAU,OAAmB,IAAuB;CAC3D,OAAO,CAAC,UAAU,OAAO,EAAE,GAAG,YAAY,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI;AAC1D;AAEA,SAAS,UAAU,OAAmB,IAAuB;CAC3D,MAAM,SAAS,MAAM,UAAU;CAC/B,MAAM,WAAW,MAAM,YAAY;CACnC,OAAO;kBACS,MAAM,OAAO;2BACJ,MAAM,kBAAkB,GAAG,eAAe;gBACrD,MAAM,QAAQ,MAAM,OAAO;wBACnB,MAAM,QAAQ,KAAK;wBACnB,MAAM,QAAQ,KAAK;yBAClB,MAAM,QAAQ,SAAS,GAAG,MAAM;0BAC/B,MAAM,QAAQ,UAAU,GAAG,OAAO;wBACpC,MAAM,KAAK,QAAQ;0BACjB,MAAM,KAAK,UAAU;sBACzB,MAAM,KAAK,SAAS,MAAM,KAAK,UAAU;kBAC7C,MAAM,OAAO;yBACN,MAAM,gBAAgB,MAAM,OAAO;kBAC1C,OAAO;qBACJ,SAAS;0BACJ,OAAO;kBACf,MAAM,UAAU,GAAG,OAAO;oBACxB,GAAG,QAAQ;oBACX,GAAG,QAAQ;2BACJ,MAAM,OAAO,SAAS;sBAC3B,MAAM,OAAO,IAAI;2BACZ,MAAM,OAAO,SAAS;sBAC3B,MAAM,aAAa,GAAG,QAAQ;AACpD;AAEA,SAAS,IAAI,OAAe,KAAa,MAAsB;CAC7D,OAAO,0BAA0B,MAAM,IAAI,IAAI,QAAQ,KAAK;AAC9D;;;;;;AAOA,SAAS,YAAY,IAAuB;CAC1C,OAAO;yBACgB,IAAI,YAAY,MAAM,gBAAgB,EAAE;uBAC1C,IAAI,YAAY,MAAM,gBAAgB,EAAE;yBACtC,IAAI,YAAY,OAAO,UAAU,EAAE;qBACvC,IAAI,YAAY,OAAO,gBAAgB,EAAE;;8BAEhC,IAAI,qBAAqB,OAAO,gBAAgB,EAAE;yBACvD,IAAI,gBAAgB,OAAO,gBAAgB,EAAE;8BACxC,IAAI,qBAAqB,OAAO,gBAAgB,EAAE;;8BAElD,IAAI,qBAAqB,GAAG,MAAM,gBAAgB,EAAE;yBACzD,IAAI,gBAAgB,GAAG,MAAM,gBAAgB,EAAE;8BAC1C,IAAI,qBAAqB,GAAG,MAAM,gBAAgB,EAAE;0BACxD,IAAI,iBAAiB,GAAG,MAAM,gBAAgB,EAAE;kCACxC,IAAI,qBAAqB,OAAO,UAAU,EAAE;6BACjD,IAAI,gBAAgB,OAAO,UAAU,EAAE;kCAClC,IAAI,qBAAqB,OAAO,UAAU,EAAE;8BAChD,IAAI,iBAAiB,OAAO,UAAU,EAAE;AACtE"}
@@ -0,0 +1,19 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { baseCss } from "./styles/base.mjs";
5
+ import { behaviorCss } from "./styles/behavior.mjs";
6
+ import { tokenCss } from "./styles/tokens.mjs";
7
+ //#region src/ui/styles.ts
8
+ /** Build the global CSS stylesheet from the light and dark brand themes. */
9
+ function baseStyle(light, dark) {
10
+ return [
11
+ tokenCss(light, dark),
12
+ baseCss(),
13
+ behaviorCss()
14
+ ].join("\n");
15
+ }
16
+ //#endregion
17
+ export { baseStyle };
18
+
19
+ //# sourceMappingURL=styles.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles.mjs","names":[],"sources":["../../src/ui/styles.ts"],"sourcesContent":["import { baseCss } from \"./styles/base.ts\";\nimport { behaviorCss } from \"./styles/behavior.ts\";\nimport { tokenCss } from \"./styles/tokens.ts\";\nimport type { BrandTheme } from \"./theme.ts\";\n\n/** Build the global CSS stylesheet from the light and dark brand themes. */\nexport function baseStyle(light: BrandTheme, dark: BrandTheme): string {\n return [tokenCss(light, dark), baseCss(), behaviorCss()].join(\"\\n\");\n}\n"],"mappings":";;;;;;;;AAMA,SAAgB,UAAU,OAAmB,MAA0B;CACrE,OAAO;EAAC,SAAS,OAAO,IAAI;EAAG,QAAQ;EAAG,YAAY;CAAC,CAAC,CAAC,KAAK,IAAI;AACpE"}
@@ -0,0 +1,91 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { css } from "./css.mjs";
5
+ import { Icon } from "./icons/icon.mjs";
6
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
7
+ //#region src/ui/subnav.tsx
8
+ const layout = css`
9
+ /* subnav-layout */
10
+ display: grid;
11
+ grid-template-columns: 210px minmax(0, 1fr);
12
+ gap: var(--space-6);
13
+ align-items: start;
14
+ @media (max-width: 880px) {
15
+ & {
16
+ grid-template-columns: minmax(0, 1fr);
17
+ gap: var(--space-4);
18
+ }
19
+ }
20
+ `;
21
+ const nav = css`
22
+ /* subnav */
23
+ position: sticky;
24
+ top: calc(var(--topbar-height) + var(--space-4));
25
+ display: flex;
26
+ flex-direction: column;
27
+ gap: 2px;
28
+ @media (max-width: 880px) {
29
+ & {
30
+ position: static;
31
+ flex-direction: row;
32
+ gap: var(--space-1);
33
+ padding-bottom: var(--space-2);
34
+ overflow-x: auto;
35
+ border-bottom: 1px solid var(--border);
36
+ }
37
+ }
38
+ `;
39
+ const link = css`
40
+ /* subnav-link */
41
+ display: flex;
42
+ align-items: center;
43
+ gap: 0.6rem;
44
+ min-height: 36px;
45
+ padding: 0.4rem var(--space-3);
46
+ border-radius: var(--radius);
47
+ color: var(--text-secondary);
48
+ font-size: var(--text-base);
49
+ font-weight: 500;
50
+ text-decoration: none;
51
+ white-space: nowrap;
52
+ transition:
53
+ background var(--dur-fast) var(--ease),
54
+ color var(--dur-fast) var(--ease);
55
+ &:hover {
56
+ background: var(--surface-muted);
57
+ color: var(--text-primary);
58
+ text-decoration: none;
59
+ }
60
+ `;
61
+ const linkActive = css`
62
+ /* subnav-link-active */
63
+ ${link}
64
+ background: var(--accent-subtle);
65
+ color: var(--accent-fg);
66
+ font-weight: 600;
67
+ &:hover {
68
+ background: var(--accent-subtle);
69
+ color: var(--accent-fg);
70
+ }
71
+ `;
72
+ /** Vertical section navigation beside content (a horizontal strip on phones). */
73
+ const SubNavLayout = ({ items, label, children }) => {
74
+ return /* @__PURE__ */ jsxs("div", {
75
+ class: layout,
76
+ children: [/* @__PURE__ */ jsx("nav", {
77
+ class: nav,
78
+ "aria-label": label,
79
+ children: items.map((item) => /* @__PURE__ */ jsxs("a", {
80
+ class: item.active ? linkActive : link,
81
+ href: item.href,
82
+ "aria-current": item.active ? "page" : void 0,
83
+ children: [item.icon ? /* @__PURE__ */ jsx(Icon, { name: item.icon }) : null, item.label]
84
+ }, item.href))
85
+ }), /* @__PURE__ */ jsx("div", { children })]
86
+ });
87
+ };
88
+ //#endregion
89
+ export { SubNavLayout };
90
+
91
+ //# sourceMappingURL=subnav.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subnav.mjs","names":[],"sources":["../../src/ui/subnav.tsx"],"sourcesContent":["import type { FC } from \"hono/jsx\";\nimport { css } from \"./css.ts\";\nimport { Icon } from \"./icons/icon.tsx\";\nimport type { IconName } from \"./icons/paths.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nconst layout = css`\n /* subnav-layout */\n display: grid;\n grid-template-columns: 210px minmax(0, 1fr);\n gap: var(--space-6);\n align-items: start;\n @media (max-width: 880px) {\n & {\n grid-template-columns: minmax(0, 1fr);\n gap: var(--space-4);\n }\n }\n`;\n\nconst nav = css`\n /* subnav */\n position: sticky;\n top: calc(var(--topbar-height) + var(--space-4));\n display: flex;\n flex-direction: column;\n gap: 2px;\n @media (max-width: 880px) {\n & {\n position: static;\n flex-direction: row;\n gap: var(--space-1);\n padding-bottom: var(--space-2);\n overflow-x: auto;\n border-bottom: 1px solid var(--border);\n }\n }\n`;\n\nconst link = css`\n /* subnav-link */\n display: flex;\n align-items: center;\n gap: 0.6rem;\n min-height: 36px;\n padding: 0.4rem var(--space-3);\n border-radius: var(--radius);\n color: var(--text-secondary);\n font-size: var(--text-base);\n font-weight: 500;\n text-decoration: none;\n white-space: nowrap;\n transition:\n background var(--dur-fast) var(--ease),\n color var(--dur-fast) var(--ease);\n &:hover {\n background: var(--surface-muted);\n color: var(--text-primary);\n text-decoration: none;\n }\n`;\n\nconst linkActive = css`\n /* subnav-link-active */\n ${link}\n background: var(--accent-subtle);\n color: var(--accent-fg);\n font-weight: 600;\n &:hover {\n background: var(--accent-subtle);\n color: var(--accent-fg);\n }\n`;\n\n/** One entry in a `SubNavLayout`. */\nexport interface SubNavItem {\n label: string;\n href: string;\n icon?: IconName;\n active?: boolean;\n}\n\n/** Vertical section navigation beside content (a horizontal strip on phones). */\nexport const SubNavLayout: FC<{ items: SubNavItem[]; label: string; children?: unknown }> = ({\n items,\n label,\n children,\n}) => {\n return (\n <div class={layout}>\n <nav class={nav} aria-label={label}>\n {items.map((item) => (\n <a\n key={item.href}\n class={item.active ? linkActive : link}\n href={item.href}\n aria-current={item.active ? \"page\" : undefined}\n >\n {item.icon ? <Icon name={item.icon} /> : null}\n {item.label}\n </a>\n ))}\n </nav>\n <div>{children}</div>\n </div>\n );\n};\n"],"mappings":";;;;;;;AAOA,MAAM,SAAS,GAAG;;;;;;;;;;;;;AAclB,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;AAmBf,MAAM,OAAO,GAAG;;;;;;;;;;;;;;;;;;;;;;AAuBhB,MAAM,aAAa,GAAG;;IAElB,KAAK;;;;;;;;;;AAmBT,MAAa,gBAAgF,EAC3F,OACA,OACA,eACI;CACJ,OACE,qBAAC,OAAD;EAAK,OAAO;EAAZ,UAAA,CACE,oBAAC,OAAD;GAAK,OAAO;GAAK,cAAY;GAC1B,UAAA,MAAM,KAAK,SACV,qBAAC,KAAD;IAEE,OAAO,KAAK,SAAS,aAAa;IAClC,MAAM,KAAK;IACX,gBAAc,KAAK,SAAS,SAAS,KAAA;IAJvC,UAAA,CAMG,KAAK,OAAO,oBAAC,MAAD,EAAM,MAAM,KAAK,KAAO,CAAA,IAAI,MACxC,KAAK,KACL;GAPI,GAAA,KAAK,IAOT,CACJ;EACE,CAAA,GACL,oBAAC,OAAD,EAAM,SAAc,CAAA,CACjB;;AAET"}
@@ -0,0 +1,73 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { css } from "./css.mjs";
5
+ import { jsx } from "hono/jsx/jsx-runtime";
6
+ //#region src/ui/table.tsx
7
+ const tableWrap = css`
8
+ /* table */
9
+ overflow: auto;
10
+ background: var(--surface-card);
11
+ border: 1px solid var(--border);
12
+ border-radius: var(--radius-lg);
13
+ box-shadow: var(--shadow);
14
+ & table {
15
+ width: 100%;
16
+ border-collapse: separate;
17
+ border-spacing: 0;
18
+ font-size: var(--text-base);
19
+ }
20
+ & th,
21
+ & td {
22
+ text-align: left;
23
+ padding: 0.7rem 0.9rem;
24
+ border-bottom: 1px solid var(--border-subtle);
25
+ vertical-align: middle;
26
+ }
27
+ & th {
28
+ position: sticky;
29
+ top: 0;
30
+ z-index: 1;
31
+ background: var(--surface-subtle);
32
+ color: var(--text-secondary);
33
+ font-size: var(--text-xs);
34
+ font-weight: 600;
35
+ letter-spacing: 0.04em;
36
+ text-transform: uppercase;
37
+ white-space: nowrap;
38
+ }
39
+ & tbody tr {
40
+ transition: background var(--dur-fast) var(--ease);
41
+ }
42
+ & tbody tr:hover {
43
+ background: var(--accent-wash);
44
+ }
45
+ & tbody tr:last-child td {
46
+ border-bottom: 0;
47
+ }
48
+ & td.nowrap {
49
+ white-space: nowrap;
50
+ }
51
+ `;
52
+ const tableDense = css`
53
+ /* table-dense */
54
+ ${tableWrap}
55
+ & th,
56
+ & td {
57
+ padding: 0.4rem 0.75rem;
58
+ }
59
+ `;
60
+ /**
61
+ * Scrollable bordered table surface. Pass native `<table>` markup as
62
+ * children; the header, row hover, and spacing are styled here.
63
+ */
64
+ const Table = ({ dense = false, children }) => {
65
+ return /* @__PURE__ */ jsx("div", {
66
+ class: dense ? tableDense : tableWrap,
67
+ children
68
+ });
69
+ };
70
+ //#endregion
71
+ export { Table };
72
+
73
+ //# sourceMappingURL=table.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"table.mjs","names":[],"sources":["../../src/ui/table.tsx"],"sourcesContent":["import type { FC } from \"hono/jsx\";\nimport { css } from \"./css.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nconst tableWrap = css`\n /* table */\n overflow: auto;\n background: var(--surface-card);\n border: 1px solid var(--border);\n border-radius: var(--radius-lg);\n box-shadow: var(--shadow);\n & table {\n width: 100%;\n border-collapse: separate;\n border-spacing: 0;\n font-size: var(--text-base);\n }\n & th,\n & td {\n text-align: left;\n padding: 0.7rem 0.9rem;\n border-bottom: 1px solid var(--border-subtle);\n vertical-align: middle;\n }\n & th {\n position: sticky;\n top: 0;\n z-index: 1;\n background: var(--surface-subtle);\n color: var(--text-secondary);\n font-size: var(--text-xs);\n font-weight: 600;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n white-space: nowrap;\n }\n & tbody tr {\n transition: background var(--dur-fast) var(--ease);\n }\n & tbody tr:hover {\n background: var(--accent-wash);\n }\n & tbody tr:last-child td {\n border-bottom: 0;\n }\n & td.nowrap {\n white-space: nowrap;\n }\n`;\n\nconst tableDense = css`\n /* table-dense */\n ${tableWrap}\n & th,\n & td {\n padding: 0.4rem 0.75rem;\n }\n`;\n\n/**\n * Scrollable bordered table surface. Pass native `<table>` markup as\n * children; the header, row hover, and spacing are styled here.\n */\nexport const Table: FC<{ dense?: boolean; children?: unknown }> = ({ dense = false, children }) => {\n return <div class={dense ? tableDense : tableWrap}>{children}</div>;\n};\n"],"mappings":";;;;;;AAKA,MAAM,YAAY,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CrB,MAAM,aAAa,GAAG;;IAElB,UAAU;;;;;;;;;;AAWd,MAAa,SAAsD,EAAE,QAAQ,OAAO,eAAe;CACjG,OAAO,oBAAC,OAAD;EAAK,OAAO,QAAQ,aAAa;EAAY;CAAc,CAAA;AACpE"}
@@ -0,0 +1,64 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ //#region src/ui/theme.ts
5
+ /** Default light brand theme (shadcn-like zinc, content-first). */
6
+ const LIGHT_THEME = {
7
+ accent: "#1d5fcf",
8
+ accentContrast: "#ffffff",
9
+ ring: "#1d5fcf",
10
+ surface: {
11
+ base: "#fafafa",
12
+ card: "#ffffff",
13
+ muted: "#f4f4f5",
14
+ subtle: "#fafafa"
15
+ },
16
+ text: {
17
+ primary: "#09090b",
18
+ secondary: "#5b5b64",
19
+ muted: "#6a6a73"
20
+ },
21
+ border: "#e4e4e7",
22
+ borderSubtle: "#f4f4f5",
23
+ status: {
24
+ approved: "#16a34a",
25
+ new: "#d97706",
26
+ rejected: "#dc2626"
27
+ },
28
+ sidebarBg: "#ffffff",
29
+ radius: "0.5rem",
30
+ radiusSm: "0.375rem",
31
+ shadow: "0 1px 2px rgba(0,0,0,.04)"
32
+ };
33
+ /** Default dark brand theme (shadcn-like zinc). */
34
+ const DARK_THEME = {
35
+ accent: "#4f8fff",
36
+ accentContrast: "#09090b",
37
+ ring: "#4f8fff",
38
+ surface: {
39
+ base: "#09090b",
40
+ card: "#111113",
41
+ muted: "#27272a",
42
+ subtle: "#18181b"
43
+ },
44
+ text: {
45
+ primary: "#fafafa",
46
+ secondary: "#a1a1aa",
47
+ muted: "#8a8a94"
48
+ },
49
+ border: "#27272a",
50
+ borderSubtle: "#1e1e22",
51
+ status: {
52
+ approved: "#22c55e",
53
+ new: "#f59e0b",
54
+ rejected: "#ef4444"
55
+ },
56
+ sidebarBg: "#111113",
57
+ radius: "0.5rem",
58
+ radiusSm: "0.375rem",
59
+ shadow: "0 1px 2px rgba(0,0,0,.3)"
60
+ };
61
+ //#endregion
62
+ export { DARK_THEME, LIGHT_THEME };
63
+
64
+ //# sourceMappingURL=theme.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.mjs","names":[],"sources":["../../src/ui/theme.ts"],"sourcesContent":["/** Brand theme tokens (accent, surfaces, text, borders, statuses). */\nexport interface BrandTheme {\n accent: string;\n accentContrast?: string;\n ring?: string;\n surface: { base: string; card: string; muted?: string; subtle?: string };\n text: { primary: string; secondary: string; muted?: string };\n border: string;\n borderSubtle?: string;\n status: { approved: string; new: string; rejected: string };\n sidebarBg?: string;\n /** @deprecated No longer used: the top bar is neutral. Accepted so existing configs still parse. */\n topbarBg?: string;\n radius?: string;\n radiusSm?: string;\n shadow?: string;\n}\n\n/** Default light brand theme (shadcn-like zinc, content-first). */\nexport const LIGHT_THEME: BrandTheme = {\n accent: \"#1d5fcf\",\n accentContrast: \"#ffffff\",\n ring: \"#1d5fcf\",\n surface: { base: \"#fafafa\", card: \"#ffffff\", muted: \"#f4f4f5\", subtle: \"#fafafa\" },\n text: { primary: \"#09090b\", secondary: \"#5b5b64\", muted: \"#6a6a73\" },\n border: \"#e4e4e7\",\n borderSubtle: \"#f4f4f5\",\n status: { approved: \"#16a34a\", new: \"#d97706\", rejected: \"#dc2626\" },\n sidebarBg: \"#ffffff\",\n radius: \"0.5rem\",\n radiusSm: \"0.375rem\",\n shadow: \"0 1px 2px rgba(0,0,0,.04)\",\n};\n\n/** Default dark brand theme (shadcn-like zinc). */\nexport const DARK_THEME: BrandTheme = {\n accent: \"#4f8fff\",\n accentContrast: \"#09090b\",\n ring: \"#4f8fff\",\n surface: { base: \"#09090b\", card: \"#111113\", muted: \"#27272a\", subtle: \"#18181b\" },\n text: { primary: \"#fafafa\", secondary: \"#a1a1aa\", muted: \"#8a8a94\" },\n border: \"#27272a\",\n borderSubtle: \"#1e1e22\",\n status: { approved: \"#22c55e\", new: \"#f59e0b\", rejected: \"#ef4444\" },\n sidebarBg: \"#111113\",\n radius: \"0.5rem\",\n radiusSm: \"0.375rem\",\n shadow: \"0 1px 2px rgba(0,0,0,.3)\",\n};\n"],"mappings":";;;;;AAmBA,MAAa,cAA0B;CACrC,QAAQ;CACR,gBAAgB;CAChB,MAAM;CACN,SAAS;EAAE,MAAM;EAAW,MAAM;EAAW,OAAO;EAAW,QAAQ;CAAU;CACjF,MAAM;EAAE,SAAS;EAAW,WAAW;EAAW,OAAO;CAAU;CACnE,QAAQ;CACR,cAAc;CACd,QAAQ;EAAE,UAAU;EAAW,KAAK;EAAW,UAAU;CAAU;CACnE,WAAW;CACX,QAAQ;CACR,UAAU;CACV,QAAQ;AACV;;AAGA,MAAa,aAAyB;CACpC,QAAQ;CACR,gBAAgB;CAChB,MAAM;CACN,SAAS;EAAE,MAAM;EAAW,MAAM;EAAW,OAAO;EAAW,QAAQ;CAAU;CACjF,MAAM;EAAE,SAAS;EAAW,WAAW;EAAW,OAAO;CAAU;CACnE,QAAQ;CACR,cAAc;CACd,QAAQ;EAAE,UAAU;EAAW,KAAK;EAAW,UAAU;CAAU;CACnE,WAAW;CACX,QAAQ;CACR,UAAU;CACV,QAAQ;AACV"}
@@ -0,0 +1,51 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { css } from "./css.mjs";
5
+ import { jsx } from "hono/jsx/jsx-runtime";
6
+ //#region src/ui/thumbnail.tsx
7
+ const thumbFrame = css`
8
+ /* thumbnail */
9
+ position: relative;
10
+ display: block;
11
+ overflow: hidden;
12
+ aspect-ratio: 16 / 10;
13
+ background: var(--surface-muted);
14
+ border: 1px solid var(--border-subtle);
15
+ border-radius: var(--radius-sm);
16
+ & img {
17
+ display: block;
18
+ width: 100%;
19
+ height: 100%;
20
+ object-fit: cover;
21
+ object-position: top center;
22
+ }
23
+ `;
24
+ const thumbEmpty = css`
25
+ /* thumbnail-empty */
26
+ ${thumbFrame}
27
+ display: grid;
28
+ place-items: center;
29
+ color: var(--text-secondary);
30
+ font-size: var(--text-xs);
31
+ `;
32
+ /** Fixed-ratio, lazy-loaded screenshot preview with a muted placeholder. */
33
+ const Thumbnail = ({ src, alt, placeholder = "No preview" }) => {
34
+ if (!src) return /* @__PURE__ */ jsx("span", {
35
+ class: thumbEmpty,
36
+ children: placeholder
37
+ });
38
+ return /* @__PURE__ */ jsx("span", {
39
+ class: thumbFrame,
40
+ children: /* @__PURE__ */ jsx("img", {
41
+ src,
42
+ alt,
43
+ loading: "lazy",
44
+ decoding: "async"
45
+ })
46
+ });
47
+ };
48
+ //#endregion
49
+ export { Thumbnail };
50
+
51
+ //# sourceMappingURL=thumbnail.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thumbnail.mjs","names":[],"sources":["../../src/ui/thumbnail.tsx"],"sourcesContent":["import type { FC } from \"hono/jsx\";\nimport { css } from \"./css.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nconst thumbFrame = css`\n /* thumbnail */\n position: relative;\n display: block;\n overflow: hidden;\n aspect-ratio: 16 / 10;\n background: var(--surface-muted);\n border: 1px solid var(--border-subtle);\n border-radius: var(--radius-sm);\n & img {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: cover;\n object-position: top center;\n }\n`;\n\nconst thumbEmpty = css`\n /* thumbnail-empty */\n ${thumbFrame}\n display: grid;\n place-items: center;\n color: var(--text-secondary);\n font-size: var(--text-xs);\n`;\n\n/** Fixed-ratio, lazy-loaded screenshot preview with a muted placeholder. */\nexport const Thumbnail: FC<{ src?: string | null; alt: string; placeholder?: string }> = ({\n src,\n alt,\n placeholder = \"No preview\",\n}) => {\n if (!src) {\n return <span class={thumbEmpty}>{placeholder}</span>;\n }\n return (\n <span class={thumbFrame}>\n <img src={src} alt={alt} loading=\"lazy\" decoding=\"async\" />\n </span>\n );\n};\n"],"mappings":";;;;;;AAKA,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;AAkBtB,MAAM,aAAa,GAAG;;IAElB,WAAW;;;;;;;AAQf,MAAa,aAA6E,EACxF,KACA,KACA,cAAc,mBACV;CACJ,IAAI,CAAC,KACH,OAAO,oBAAC,QAAD;EAAM,OAAO;EAAa,UAAA;CAAkB,CAAA;CAErD,OACE,oBAAC,QAAD;EAAM,OAAO;EACX,UAAA,oBAAC,OAAD;GAAU;GAAU;GAAK,SAAQ;GAAO,UAAS;EAAS,CAAA;CACtD,CAAA;AAEV"}
@@ -0,0 +1,50 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { jsx } from "hono/jsx/jsx-runtime";
5
+ //#region src/ui/time.tsx
6
+ const MINUTE = 60;
7
+ const HOUR = 3600;
8
+ const DAY = 86400;
9
+ /** [upper bound in seconds, unit size in seconds, unit label], tried in order. */
10
+ const STEPS = [
11
+ [
12
+ HOUR,
13
+ MINUTE,
14
+ "min"
15
+ ],
16
+ [
17
+ DAY,
18
+ HOUR,
19
+ "h"
20
+ ],
21
+ [
22
+ 7 * DAY,
23
+ DAY,
24
+ "d"
25
+ ]
26
+ ];
27
+ /** Compact relative time ("3 min ago"); falls back to an ISO date after a week. */
28
+ function formatRelative(value, now = /* @__PURE__ */ new Date()) {
29
+ const then = new Date(value);
30
+ if (Number.isNaN(then.getTime())) return "unknown";
31
+ const seconds = Math.floor((now.getTime() - then.getTime()) / 1e3);
32
+ if (seconds < 45) return "just now";
33
+ const step = STEPS.find(([limit]) => seconds < limit);
34
+ if (!step) return then.toISOString().slice(0, 10);
35
+ return `${Math.max(1, Math.round(seconds / step[1]))} ${step[2]} ago`;
36
+ }
37
+ /** `<time>` showing a relative label, with the exact timestamp on hover. */
38
+ const RelativeTime = ({ value }) => {
39
+ const date = new Date(value);
40
+ const valid = !Number.isNaN(date.getTime());
41
+ return /* @__PURE__ */ jsx("time", {
42
+ datetime: valid ? date.toISOString() : void 0,
43
+ title: valid ? date.toUTCString() : "",
44
+ children: formatRelative(value)
45
+ });
46
+ };
47
+ //#endregion
48
+ export { RelativeTime, formatRelative };
49
+
50
+ //# sourceMappingURL=time.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"time.mjs","names":[],"sources":["../../src/ui/time.tsx"],"sourcesContent":["import type { FC } from \"hono/jsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nconst MINUTE = 60;\nconst HOUR = 3600;\nconst DAY = 86_400;\n\n/** [upper bound in seconds, unit size in seconds, unit label], tried in order. */\nconst STEPS: [number, number, string][] = [\n [HOUR, MINUTE, \"min\"],\n [DAY, HOUR, \"h\"],\n [7 * DAY, DAY, \"d\"],\n];\n\n/** Compact relative time (\"3 min ago\"); falls back to an ISO date after a week. */\nexport function formatRelative(value: string | Date, now: Date = new Date()): string {\n const then = new Date(value);\n if (Number.isNaN(then.getTime())) {\n return \"unknown\";\n }\n const seconds = Math.floor((now.getTime() - then.getTime()) / 1000);\n if (seconds < 45) {\n return \"just now\";\n }\n const step = STEPS.find(([limit]) => seconds < limit);\n if (!step) {\n return then.toISOString().slice(0, 10);\n }\n return `${Math.max(1, Math.round(seconds / step[1]))} ${step[2]} ago`;\n}\n\n/** `<time>` showing a relative label, with the exact timestamp on hover. */\nexport const RelativeTime: FC<{ value: string | Date }> = ({ value }) => {\n const date = new Date(value);\n const valid = !Number.isNaN(date.getTime());\n return (\n <time datetime={valid ? date.toISOString() : undefined} title={valid ? date.toUTCString() : \"\"}>\n {formatRelative(value)}\n </time>\n );\n};\n"],"mappings":";;;;;AAIA,MAAM,SAAS;AACf,MAAM,OAAO;AACb,MAAM,MAAM;;AAGZ,MAAM,QAAoC;CACxC;EAAC;EAAM;EAAQ;CAAK;CACpB;EAAC;EAAK;EAAM;CAAG;CACf;EAAC,IAAI;EAAK;EAAK;CAAG;AACpB;;AAGA,SAAgB,eAAe,OAAsB,sBAAY,IAAI,KAAK,GAAW;CACnF,MAAM,OAAO,IAAI,KAAK,KAAK;CAC3B,IAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,GAC7B,OAAO;CAET,MAAM,UAAU,KAAK,OAAO,IAAI,QAAQ,IAAI,KAAK,QAAQ,KAAK,GAAI;CAClE,IAAI,UAAU,IACZ,OAAO;CAET,MAAM,OAAO,MAAM,MAAM,CAAC,WAAW,UAAU,KAAK;CACpD,IAAI,CAAC,MACH,OAAO,KAAK,YAAY,CAAC,CAAC,MAAM,GAAG,EAAE;CAEvC,OAAO,GAAG,KAAK,IAAI,GAAG,KAAK,MAAM,UAAU,KAAK,EAAE,CAAC,EAAE,GAAG,KAAK,GAAG;AAClE;;AAGA,MAAa,gBAA8C,EAAE,YAAY;CACvE,MAAM,OAAO,IAAI,KAAK,KAAK;CAC3B,MAAM,QAAQ,CAAC,OAAO,MAAM,KAAK,QAAQ,CAAC;CAC1C,OACE,oBAAC,QAAD;EAAM,UAAU,QAAQ,KAAK,YAAY,IAAI,KAAA;EAAW,OAAO,QAAQ,KAAK,YAAY,IAAI;EACzF,UAAA,eAAe,KAAK;CACjB,CAAA;AAEV"}
package/package.json ADDED
@@ -0,0 +1,92 @@
1
+ {
2
+ "name": "@storyshelf/app",
3
+ "version": "0.5.3",
4
+ "description": "StoryShelf app: Hono app, API routes, and server-rendered UI over @storyshelf/core.",
5
+ "keywords": [
6
+ "chromatic-alternative",
7
+ "hono",
8
+ "self-hosted",
9
+ "storybook",
10
+ "visual-testing"
11
+ ],
12
+ "homepage": "https://github.com/GuptaSiddhant/StoryShelf#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/GuptaSiddhant/StoryShelf/issues"
15
+ },
16
+ "license": "MIT",
17
+ "author": {
18
+ "name": "Siddhant Gupta",
19
+ "url": "https://guptasiddhant.com"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/GuptaSiddhant/StoryShelf.git",
24
+ "directory": "packages/app"
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "type": "module",
30
+ "sideEffects": false,
31
+ "types": "./dist/index.d.mts",
32
+ "exports": {
33
+ ".": {
34
+ "source": "./src/index.tsx",
35
+ "default": "./dist/index.mjs"
36
+ },
37
+ "./package.json": "./package.json"
38
+ },
39
+ "publishConfig": {
40
+ "exports": {
41
+ ".": "./dist/index.mjs",
42
+ "./package.json": "./package.json"
43
+ },
44
+ "access": "public"
45
+ },
46
+ "scripts": {
47
+ "build": "tsdown",
48
+ "dev": "tsdown -w",
49
+ "fmt": "oxfmt ./src",
50
+ "lint": "oxlint --type-aware --type-check ./src",
51
+ "lint:fix": "nub run lint -- --fix",
52
+ "prepublishOnly": "nub run build",
53
+ "test": "vitest run",
54
+ "test:coverage": "vitest run --coverage",
55
+ "test:watch": "vitest"
56
+ },
57
+ "dependencies": {
58
+ "@hono/swagger-ui": "^0.6.1",
59
+ "@hono/zod-openapi": "^1.1.5",
60
+ "@storyshelf/core": "0.5.3",
61
+ "@storyshelf/observability": "0.5.3",
62
+ "hono": "^4.11.0",
63
+ "zod": "^4.2.0"
64
+ },
65
+ "devDependencies": {
66
+ "@opentelemetry/api": "^1.9.0",
67
+ "@opentelemetry/context-async-hooks": "^2.11.0",
68
+ "@opentelemetry/core": "^2.11.0",
69
+ "@opentelemetry/sdk-trace-base": "^2.11.0",
70
+ "@storyshelf/auth": "0.5.3",
71
+ "@storyshelf/db-sqlite": "0.5.3",
72
+ "@types/node": "^26.0.0",
73
+ "@vitest/coverage-v8": "^4.0.0",
74
+ "adm-zip": "0.6.0",
75
+ "oxfmt": "^0.65.0",
76
+ "oxlint": "^1.80.0",
77
+ "oxlint-tsgolint": "^7.0.2001",
78
+ "pino": "^10.3.1",
79
+ "tsdown": "^0.22.14",
80
+ "typescript": "^7.0.2",
81
+ "vitest": "^5.0.0"
82
+ },
83
+ "jsr": {
84
+ "runtimeCompat": {
85
+ "node": true,
86
+ "deno": true,
87
+ "browser": false,
88
+ "workerd": false,
89
+ "bun": true
90
+ }
91
+ }
92
+ }