@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,173 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { css } from "./css.mjs";
5
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
6
+ //#region src/ui/layout.tsx
7
+ const cardBase = css`
8
+ /* card */
9
+ background: var(--surface-card);
10
+ border: 1px solid var(--border);
11
+ border-radius: var(--radius-lg);
12
+ box-shadow: var(--shadow);
13
+ `;
14
+ const cardColumn = css`
15
+ /* card-column */
16
+ ${cardBase}
17
+ display: flex;
18
+ flex-direction: column;
19
+ height: 100%;
20
+ `;
21
+ const cardPadded = css`
22
+ /* card-padded */
23
+ ${cardBase}
24
+ padding: 1rem;
25
+ `;
26
+ const cardDanger = css`
27
+ /* card-danger */
28
+ ${cardPadded}
29
+ border-color: var(--status-rejected);
30
+ `;
31
+ const cardSection = css`
32
+ /* card-section */
33
+ padding: 1rem;
34
+ `;
35
+ const cardSectionDivider = css`
36
+ /* card-section-divider */
37
+ ${cardSection}
38
+ border-bottom: 1px solid var(--border);
39
+ `;
40
+ const pageHeader = css`
41
+ /* page-header */
42
+ margin-bottom: 1.25rem;
43
+ `;
44
+ const pageHeaderRow = css`
45
+ /* page-header-row */
46
+ display: flex;
47
+ gap: 1rem;
48
+ align-items: flex-start;
49
+ justify-content: space-between;
50
+ flex-wrap: wrap;
51
+ `;
52
+ const pageHeaderTitle = css`
53
+ /* page-header-title */
54
+ margin: 0;
55
+ font-size: 1.45rem;
56
+ line-height: 1.2;
57
+ letter-spacing: -0.02em;
58
+ font-weight: 700;
59
+ `;
60
+ const pageHeaderDesc = css`
61
+ /* page-header-desc */
62
+ margin: 0.35rem 0 0;
63
+ color: var(--text-secondary);
64
+ font-size: 0.9rem;
65
+ max-width: 65ch;
66
+ /* Links inside running text must not rely on colour alone. */
67
+ & a {
68
+ text-decoration: underline;
69
+ text-underline-offset: 2px;
70
+ }
71
+ `;
72
+ const pageHeaderMeta = css`
73
+ /* page-header-meta */
74
+ margin: 0.4rem 0 0;
75
+ color: var(--text-secondary);
76
+ font-size: 0.82rem;
77
+ display: flex;
78
+ gap: 0.4rem;
79
+ align-items: center;
80
+ flex-wrap: wrap;
81
+ font-variant-numeric: tabular-nums;
82
+ `;
83
+ const pageHeaderActions = css`
84
+ /* page-header-actions */
85
+ display: flex;
86
+ gap: 0.5rem;
87
+ flex-wrap: wrap;
88
+ align-items: center;
89
+ `;
90
+ const sectionTitleH2 = css`
91
+ /* section-title */
92
+ margin: 0 0 0.5rem;
93
+ font-size: 1.05rem;
94
+ font-weight: 650;
95
+ letter-spacing: -0.01em;
96
+ `;
97
+ const sectionTitleH3 = css`
98
+ /* section-title-sm */
99
+ ${sectionTitleH2}
100
+ font-size: 0.95rem;
101
+ `;
102
+ const sectionTitleDanger = css`
103
+ /* section-title-danger */
104
+ ${sectionTitleH3}
105
+ color: var(--status-rejected);
106
+ `;
107
+ /** Card or section heading (h2 by default, h3 for subsections). */
108
+ /** Card container, padded by default, with an optional danger tone. */
109
+ const Card = ({ children, padded = true, tone = "neutral", fill = false, ...rest }) => {
110
+ if (fill) return /* @__PURE__ */ jsx("div", {
111
+ class: cardColumn,
112
+ ...rest,
113
+ children
114
+ });
115
+ if (!padded) return /* @__PURE__ */ jsx("div", {
116
+ class: cardBase,
117
+ ...rest,
118
+ children
119
+ });
120
+ return /* @__PURE__ */ jsx("div", {
121
+ class: tone === "danger" ? cardDanger : cardPadded,
122
+ ...rest,
123
+ children
124
+ });
125
+ };
126
+ /** Padded card section, with an optional bottom divider. */
127
+ const CardSection = ({ divider = false, children }) => {
128
+ return /* @__PURE__ */ jsx("div", {
129
+ class: divider ? cardSectionDivider : cardSection,
130
+ children
131
+ });
132
+ };
133
+ /** Page header with title, description, meta line, and actions (breadcrumbs live in the top bar). */
134
+ const PageHeader = ({ title, description, meta, actions }) => {
135
+ return /* @__PURE__ */ jsx("div", {
136
+ class: pageHeader,
137
+ children: /* @__PURE__ */ jsxs("div", {
138
+ class: pageHeaderRow,
139
+ children: [/* @__PURE__ */ jsxs("div", { children: [
140
+ /* @__PURE__ */ jsx("h1", {
141
+ class: pageHeaderTitle,
142
+ children: title
143
+ }),
144
+ description ? /* @__PURE__ */ jsx("p", {
145
+ class: pageHeaderDesc,
146
+ children: description
147
+ }) : null,
148
+ meta ? /* @__PURE__ */ jsx("p", {
149
+ class: pageHeaderMeta,
150
+ children: meta
151
+ }) : null
152
+ ] }), actions ? /* @__PURE__ */ jsx("div", {
153
+ class: pageHeaderActions,
154
+ children: actions
155
+ }) : null]
156
+ })
157
+ });
158
+ };
159
+ /** Card or section heading (h2 by default, h3 for subsections). */
160
+ const SectionTitle = ({ level = 2, tone = "neutral", children }) => {
161
+ if (level === 3) return /* @__PURE__ */ jsx("h3", {
162
+ class: tone === "danger" ? sectionTitleDanger : sectionTitleH3,
163
+ children
164
+ });
165
+ return /* @__PURE__ */ jsx("h2", {
166
+ class: sectionTitleH2,
167
+ children
168
+ });
169
+ };
170
+ //#endregion
171
+ export { Card, CardSection, PageHeader, SectionTitle };
172
+
173
+ //# sourceMappingURL=layout.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout.mjs","names":[],"sources":["../../src/ui/layout.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 cardBase = css`\n /* card */\n background: var(--surface-card);\n border: 1px solid var(--border);\n border-radius: var(--radius-lg);\n box-shadow: var(--shadow);\n`;\n\nconst cardColumn = css`\n /* card-column */\n ${cardBase}\n display: flex;\n flex-direction: column;\n height: 100%;\n`;\n\nconst cardPadded = css`\n /* card-padded */\n ${cardBase}\n padding: 1rem;\n`;\n\nconst cardDanger = css`\n /* card-danger */\n ${cardPadded}\n border-color: var(--status-rejected);\n`;\n\nconst cardSection = css`\n /* card-section */\n padding: 1rem;\n`;\n\nconst cardSectionDivider = css`\n /* card-section-divider */\n ${cardSection}\n border-bottom: 1px solid var(--border);\n`;\n\nconst pageHeader = css`\n /* page-header */\n margin-bottom: 1.25rem;\n`;\n\nconst pageHeaderRow = css`\n /* page-header-row */\n display: flex;\n gap: 1rem;\n align-items: flex-start;\n justify-content: space-between;\n flex-wrap: wrap;\n`;\n\nconst pageHeaderTitle = css`\n /* page-header-title */\n margin: 0;\n font-size: 1.45rem;\n line-height: 1.2;\n letter-spacing: -0.02em;\n font-weight: 700;\n`;\n\nconst pageHeaderDesc = css`\n /* page-header-desc */\n margin: 0.35rem 0 0;\n color: var(--text-secondary);\n font-size: 0.9rem;\n max-width: 65ch;\n /* Links inside running text must not rely on colour alone. */\n & a {\n text-decoration: underline;\n text-underline-offset: 2px;\n }\n`;\n\nconst pageHeaderMeta = css`\n /* page-header-meta */\n margin: 0.4rem 0 0;\n color: var(--text-secondary);\n font-size: 0.82rem;\n display: flex;\n gap: 0.4rem;\n align-items: center;\n flex-wrap: wrap;\n font-variant-numeric: tabular-nums;\n`;\n\nconst pageHeaderActions = css`\n /* page-header-actions */\n display: flex;\n gap: 0.5rem;\n flex-wrap: wrap;\n align-items: center;\n`;\n\nconst sectionTitleH2 = css`\n /* section-title */\n margin: 0 0 0.5rem;\n font-size: 1.05rem;\n font-weight: 650;\n letter-spacing: -0.01em;\n`;\n\nconst sectionTitleH3 = css`\n /* section-title-sm */\n ${sectionTitleH2}\n font-size: 0.95rem;\n`;\n\nconst sectionTitleDanger = css`\n /* section-title-danger */\n ${sectionTitleH3}\n color: var(--status-rejected);\n`;\n\n/** Card or section heading (h2 by default, h3 for subsections). */\n\n/** Card container, padded by default, with an optional danger tone. */\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const Card: FC<{\n children?: unknown;\n padded?: boolean;\n tone?: \"neutral\" | \"danger\";\n /** Flex column filling its grid cell, so a footer can sit at the bottom of equal-height cards. */\n fill?: boolean;\n [key: string]: unknown;\n}> = ({ children, padded = true, tone = \"neutral\", fill = false, ...rest }) => {\n if (fill) {\n return (\n <div class={cardColumn} {...rest}>\n {children}\n </div>\n );\n }\n if (!padded) {\n return (\n <div class={cardBase} {...rest}>\n {children}\n </div>\n );\n }\n return (\n <div class={tone === \"danger\" ? cardDanger : cardPadded} {...rest}>\n {children}\n </div>\n );\n};\n\n/** Padded card section, with an optional bottom divider. */\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const CardSection: FC<{ divider?: boolean; children?: unknown }> = ({\n divider = false,\n children,\n}) => {\n return <div class={divider ? cardSectionDivider : cardSection}>{children}</div>;\n};\n\n/** Page header with title, description, meta line, and actions (breadcrumbs live in the top bar). */\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const PageHeader: FC<{\n title: unknown;\n description?: unknown;\n meta?: unknown;\n actions?: unknown;\n}> = ({ title, description, meta, actions }) => {\n return (\n <div class={pageHeader}>\n <div class={pageHeaderRow}>\n <div>\n <h1 class={pageHeaderTitle}>{title}</h1>\n {description ? <p class={pageHeaderDesc}>{description}</p> : null}\n {meta ? <p class={pageHeaderMeta}>{meta}</p> : null}\n </div>\n {actions ? <div class={pageHeaderActions}>{actions}</div> : null}\n </div>\n </div>\n );\n};\n\n/** Card or section heading (h2 by default, h3 for subsections). */\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const SectionTitle: FC<{\n level?: 2 | 3;\n tone?: \"neutral\" | \"danger\";\n children?: unknown;\n}> = ({ level = 2, tone = \"neutral\", children }) => {\n if (level === 3) {\n return <h3 class={tone === \"danger\" ? sectionTitleDanger : sectionTitleH3}>{children}</h3>;\n }\n return <h2 class={sectionTitleH2}>{children}</h2>;\n};\n"],"mappings":";;;;;;AAKA,MAAM,WAAW,GAAG;;;;;;;AAQpB,MAAM,aAAa,GAAG;;IAElB,SAAS;;;;;AAMb,MAAM,aAAa,GAAG;;IAElB,SAAS;;;AAIb,MAAM,aAAa,GAAG;;IAElB,WAAW;;;AAIf,MAAM,cAAc,GAAG;;;;AAKvB,MAAM,qBAAqB,GAAG;;IAE1B,YAAY;;;AAIhB,MAAM,aAAa,GAAG;;;;AAKtB,MAAM,gBAAgB,GAAG;;;;;;;;AASzB,MAAM,kBAAkB,GAAG;;;;;;;;AAS3B,MAAM,iBAAiB,GAAG;;;;;;;;;;;;AAa1B,MAAM,iBAAiB,GAAG;;;;;;;;;;;AAY1B,MAAM,oBAAoB,GAAG;;;;;;;AAQ7B,MAAM,iBAAiB,GAAG;;;;;;;AAQ1B,MAAM,iBAAiB,GAAG;;IAEtB,eAAe;;;AAInB,MAAM,qBAAqB,GAAG;;IAE1B,eAAe;;;;;AAQnB,MAAa,QAOP,EAAE,UAAU,SAAS,MAAM,OAAO,WAAW,OAAO,OAAO,GAAG,WAAW;CAC7E,IAAI,MACF,OACE,oBAAC,OAAD;EAAK,OAAO;EAAY,GAAI;EACzB;CACE,CAAA;CAGT,IAAI,CAAC,QACH,OACE,oBAAC,OAAD;EAAK,OAAO;EAAU,GAAI;EACvB;CACE,CAAA;CAGT,OACE,oBAAC,OAAD;EAAK,OAAO,SAAS,WAAW,aAAa;EAAY,GAAI;EAC1D;CACE,CAAA;AAET;;AAIA,MAAa,eAA8D,EACzE,UAAU,OACV,eACI;CACJ,OAAO,oBAAC,OAAD;EAAK,OAAO,UAAU,qBAAqB;EAAc;CAAc,CAAA;AAChF;;AAIA,MAAa,cAKP,EAAE,OAAO,aAAa,MAAM,cAAc;CAC9C,OACE,oBAAC,OAAD;EAAK,OAAO;EACV,UAAA,qBAAC,OAAD;GAAK,OAAO;GAAZ,UAAA,CACE,qBAAC,OAAD,EAAA,UAAA;IACE,oBAAC,MAAD;KAAI,OAAO;KAAkB,UAAA;IAAU,CAAA;IACtC,cAAc,oBAAC,KAAD;KAAG,OAAO;KAAiB,UAAA;IAAe,CAAA,IAAI;IAC5D,OAAO,oBAAC,KAAD;KAAG,OAAO;KAAiB,UAAA;IAAQ,CAAA,IAAI;GAC5C,EAAA,CAAA,GACJ,UAAU,oBAAC,OAAD;IAAK,OAAO;IAAoB,UAAA;GAAa,CAAA,IAAI,IACzD;;CACF,CAAA;AAET;;AAIA,MAAa,gBAIP,EAAE,QAAQ,GAAG,OAAO,WAAW,eAAe;CAClD,IAAI,UAAU,GACZ,OAAO,oBAAC,MAAD;EAAI,OAAO,SAAS,WAAW,qBAAqB;EAAiB;CAAa,CAAA;CAE3F,OAAO,oBAAC,MAAD;EAAI,OAAO;EAAiB;CAAa,CAAA;AAClD"}
@@ -0,0 +1,44 @@
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/progress.tsx
7
+ const track = css`
8
+ /* progress */
9
+ height: 6px;
10
+ overflow: hidden;
11
+ background: var(--surface-muted);
12
+ border-radius: var(--radius-pill);
13
+ & > span {
14
+ display: block;
15
+ height: 100%;
16
+ background: var(--accent);
17
+ border-radius: inherit;
18
+ transition: width var(--dur-base) var(--ease);
19
+ }
20
+ `;
21
+ const trackDone = css`
22
+ /* progress-done */
23
+ ${track}
24
+ & > span {
25
+ background: var(--status-approved);
26
+ }
27
+ `;
28
+ /** Determinate progress bar; turns green when `value` reaches `max`. */
29
+ const Progress = ({ value, max, label }) => {
30
+ const percent = max > 0 ? Math.min(100, Math.round(value / max * 100)) : 0;
31
+ return /* @__PURE__ */ jsx("div", {
32
+ class: max > 0 && value >= max ? trackDone : track,
33
+ role: "progressbar",
34
+ "aria-label": label,
35
+ "aria-valuemin": 0,
36
+ "aria-valuemax": max,
37
+ "aria-valuenow": Math.min(value, max),
38
+ children: /* @__PURE__ */ jsx("span", { style: `width:${percent}%` })
39
+ });
40
+ };
41
+ //#endregion
42
+ export { Progress };
43
+
44
+ //# sourceMappingURL=progress.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"progress.mjs","names":[],"sources":["../../src/ui/progress.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 track = css`\n /* progress */\n height: 6px;\n overflow: hidden;\n background: var(--surface-muted);\n border-radius: var(--radius-pill);\n & > span {\n display: block;\n height: 100%;\n background: var(--accent);\n border-radius: inherit;\n transition: width var(--dur-base) var(--ease);\n }\n`;\n\nconst trackDone = css`\n /* progress-done */\n ${track}\n & > span {\n background: var(--status-approved);\n }\n`;\n\n/** Determinate progress bar; turns green when `value` reaches `max`. */\nexport const Progress: FC<{ value: number; max: number; label: string }> = ({\n value,\n max,\n label,\n}) => {\n const percent = max > 0 ? Math.min(100, Math.round((value / max) * 100)) : 0;\n return (\n <div\n class={max > 0 && value >= max ? trackDone : track}\n role=\"progressbar\"\n aria-label={label}\n aria-valuemin={0}\n aria-valuemax={max}\n aria-valuenow={Math.min(value, max)}\n >\n <span style={`width:${percent}%`} />\n </div>\n );\n};\n"],"mappings":";;;;;;AAKA,MAAM,QAAQ,GAAG;;;;;;;;;;;;;;AAejB,MAAM,YAAY,GAAG;;IAEjB,MAAM;;;;;;AAOV,MAAa,YAA+D,EAC1E,OACA,KACA,YACI;CACJ,MAAM,UAAU,MAAM,IAAI,KAAK,IAAI,KAAK,KAAK,MAAO,QAAQ,MAAO,GAAG,CAAC,IAAI;CAC3E,OACE,oBAAC,OAAD;EACE,OAAO,MAAM,KAAK,SAAS,MAAM,YAAY;EAC7C,MAAK;EACL,cAAY;EACZ,iBAAe;EACf,iBAAe;EACf,iBAAe,KAAK,IAAI,OAAO,GAAG;EAElC,UAAA,oBAAC,QAAD,EAAM,OAAO,SAAS,QAAQ,GAAK,CAAA;CAChC,CAAA;AAET"}
@@ -0,0 +1,77 @@
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/segmented.tsx
7
+ const segmentedRoot = css`
8
+ /* segmented */
9
+ display: inline-flex;
10
+ padding: 2px;
11
+ gap: 2px;
12
+ background: var(--surface-muted);
13
+ border: 1px solid var(--border);
14
+ border-radius: var(--radius);
15
+ max-width: 100%;
16
+ overflow-x: auto;
17
+ `;
18
+ const segmentedItem = css`
19
+ /* segmented-item */
20
+ appearance: none;
21
+ display: inline-flex;
22
+ align-items: center;
23
+ gap: 0.35rem;
24
+ min-height: 34px;
25
+ padding: 0.2rem 0.7rem;
26
+ border: 0;
27
+ border-radius: var(--radius-sm);
28
+ background: transparent;
29
+ color: var(--text-secondary);
30
+ font: inherit;
31
+ font-size: var(--text-sm);
32
+ font-weight: 600;
33
+ white-space: nowrap;
34
+ cursor: pointer;
35
+ text-decoration: none;
36
+ transition:
37
+ background var(--dur-fast) var(--ease),
38
+ color var(--dur-fast) var(--ease);
39
+ &:hover {
40
+ color: var(--text-primary);
41
+ text-decoration: none;
42
+ }
43
+ &[aria-pressed="true"],
44
+ &[aria-current="page"] {
45
+ background: var(--surface-card);
46
+ color: var(--text-primary);
47
+ box-shadow: var(--shadow);
48
+ }
49
+ `;
50
+ /**
51
+ * Compact single-choice control. Items with `href` navigate (filters, tabs);
52
+ * items without toggle in place and carry `data-view-value` for client code.
53
+ */
54
+ const Segmented = ({ items, label, ...rest }) => {
55
+ return /* @__PURE__ */ jsx("div", {
56
+ class: segmentedRoot,
57
+ role: "group",
58
+ "aria-label": label,
59
+ ...rest,
60
+ children: items.map((item) => item.href ? /* @__PURE__ */ jsx("a", {
61
+ class: segmentedItem,
62
+ href: item.href,
63
+ "aria-current": item.active ? "page" : void 0,
64
+ children: item.label
65
+ }, item.value) : /* @__PURE__ */ jsx("button", {
66
+ class: segmentedItem,
67
+ type: "button",
68
+ "data-view-value": item.value,
69
+ "aria-pressed": item.active ? "true" : "false",
70
+ children: item.label
71
+ }, item.value))
72
+ });
73
+ };
74
+ //#endregion
75
+ export { Segmented };
76
+
77
+ //# sourceMappingURL=segmented.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"segmented.mjs","names":[],"sources":["../../src/ui/segmented.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 segmentedRoot = css`\n /* segmented */\n display: inline-flex;\n padding: 2px;\n gap: 2px;\n background: var(--surface-muted);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n max-width: 100%;\n overflow-x: auto;\n`;\n\nconst segmentedItem = css`\n /* segmented-item */\n appearance: none;\n display: inline-flex;\n align-items: center;\n gap: 0.35rem;\n min-height: 34px;\n padding: 0.2rem 0.7rem;\n border: 0;\n border-radius: var(--radius-sm);\n background: transparent;\n color: var(--text-secondary);\n font: inherit;\n font-size: var(--text-sm);\n font-weight: 600;\n white-space: nowrap;\n cursor: pointer;\n text-decoration: none;\n transition:\n background var(--dur-fast) var(--ease),\n color var(--dur-fast) var(--ease);\n &:hover {\n color: var(--text-primary);\n text-decoration: none;\n }\n &[aria-pressed=\"true\"],\n &[aria-current=\"page\"] {\n background: var(--surface-card);\n color: var(--text-primary);\n box-shadow: var(--shadow);\n }\n`;\n\n/** One option in a `Segmented` control. */\nexport interface SegmentedItem {\n label: string;\n value: string;\n /** Renders a link (navigation) instead of a toggle button. */\n href?: string;\n active?: boolean;\n}\n\n/**\n * Compact single-choice control. Items with `href` navigate (filters, tabs);\n * items without toggle in place and carry `data-view-value` for client code.\n */\nexport const Segmented: FC<{\n items: SegmentedItem[];\n label: string;\n [key: string]: unknown;\n}> = ({ items, label, ...rest }) => {\n return (\n <div class={segmentedRoot} role=\"group\" aria-label={label} {...rest}>\n {items.map((item) =>\n item.href ? (\n <a\n key={item.value}\n class={segmentedItem}\n href={item.href}\n aria-current={item.active ? \"page\" : undefined}\n >\n {item.label}\n </a>\n ) : (\n <button\n key={item.value}\n class={segmentedItem}\n type=\"button\"\n data-view-value={item.value}\n aria-pressed={item.active ? \"true\" : \"false\"}\n >\n {item.label}\n </button>\n ),\n )}\n </div>\n );\n};\n"],"mappings":";;;;;;AAKA,MAAM,gBAAgB,GAAG;;;;;;;;;;;AAYzB,MAAM,gBAAgB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CzB,MAAa,aAIP,EAAE,OAAO,OAAO,GAAG,WAAW;CAClC,OACE,oBAAC,OAAD;EAAK,OAAO;EAAe,MAAK;EAAQ,cAAY;EAAO,GAAI;EAC5D,UAAA,MAAM,KAAK,SACV,KAAK,OACH,oBAAC,KAAD;GAEE,OAAO;GACP,MAAM,KAAK;GACX,gBAAc,KAAK,SAAS,SAAS,KAAA;GAEpC,UAAA,KAAK;EACL,GANI,KAAK,KAMT,IAEH,oBAAC,UAAD;GAEE,OAAO;GACP,MAAK;GACL,mBAAiB,KAAK;GACtB,gBAAc,KAAK,SAAS,SAAS;GAEpC,UAAA,KAAK;EACA,GAPD,KAAK,KAOJ,CAEZ;CACG,CAAA;AAET"}
@@ -0,0 +1,161 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { getStore } from "../../store.mjs";
5
+ import { css } from "../css.mjs";
6
+ import { Icon } from "../icons/icon.mjs";
7
+ import { Button } from "../buttons.mjs";
8
+ import { Avatar } from "../avatar.mjs";
9
+ import { Dropdown, DropdownDivider, DropdownItem } from "../dropdown.mjs";
10
+ import { csrfField } from "../csrf-field.mjs";
11
+ import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
12
+ import { safeImageUrl } from "@storyshelf/core/urls";
13
+ //#region src/ui/shell/account.tsx
14
+ /** Styles for the account widgets that live in the sidebar footer. */
15
+ const accountRoot = css`
16
+ /* account */
17
+ display: grid;
18
+ gap: var(--space-1);
19
+ .account__who {
20
+ display: flex;
21
+ align-items: center;
22
+ gap: var(--space-2);
23
+ min-width: 0;
24
+ }
25
+ .account__text {
26
+ display: grid;
27
+ min-width: 0;
28
+ text-align: left;
29
+ line-height: 1.2;
30
+ }
31
+ .account__name {
32
+ overflow: hidden;
33
+ font-size: var(--text-base);
34
+ font-weight: 600;
35
+ text-overflow: ellipsis;
36
+ white-space: nowrap;
37
+ }
38
+ .account__role {
39
+ color: var(--text-secondary);
40
+ font-size: var(--text-xs);
41
+ text-transform: capitalize;
42
+ }
43
+ .account__meta {
44
+ padding: 0.4rem 0.6rem 0.5rem;
45
+ color: var(--text-secondary);
46
+ font-size: var(--text-sm);
47
+ line-height: 1.3;
48
+ }
49
+ .account__meta strong {
50
+ display: block;
51
+ color: var(--text-primary);
52
+ font-size: var(--text-base);
53
+ }
54
+ .account__logout {
55
+ margin: 0;
56
+ }
57
+ `;
58
+ /** Light / Dark / System menu; the trigger shows the active mode's icon. */
59
+ const ThemeMenu = () => {
60
+ return /* @__PURE__ */ jsxs(Dropdown, {
61
+ iconOnly: true,
62
+ placement: "above",
63
+ ariaLabel: "Theme",
64
+ label: /* @__PURE__ */ jsxs(Fragment, { children: [
65
+ /* @__PURE__ */ jsx("span", {
66
+ "data-theme-icon": "light",
67
+ children: /* @__PURE__ */ jsx(Icon, { name: "sun" })
68
+ }),
69
+ /* @__PURE__ */ jsx("span", {
70
+ "data-theme-icon": "dark",
71
+ children: /* @__PURE__ */ jsx(Icon, { name: "moon" })
72
+ }),
73
+ /* @__PURE__ */ jsx("span", {
74
+ "data-theme-icon": "system",
75
+ children: /* @__PURE__ */ jsx(Icon, { name: "monitor" })
76
+ })
77
+ ] }),
78
+ children: [
79
+ /* @__PURE__ */ jsx(DropdownItem, {
80
+ icon: "sun",
81
+ "data-theme-set": "light",
82
+ children: "Light"
83
+ }),
84
+ /* @__PURE__ */ jsx(DropdownItem, {
85
+ icon: "moon",
86
+ "data-theme-set": "dark",
87
+ children: "Dark"
88
+ }),
89
+ /* @__PURE__ */ jsx(DropdownItem, {
90
+ icon: "monitor",
91
+ "data-theme-set": "system",
92
+ children: "System"
93
+ })
94
+ ]
95
+ });
96
+ };
97
+ /** Signed-in user row (name, role, profile, sign out); a sign-in button when signed out. */
98
+ const UserMenu = () => {
99
+ const { user, authEnabled } = getStore();
100
+ if (!authEnabled) return null;
101
+ if (!user) return /* @__PURE__ */ jsx(Button, {
102
+ href: "/auth/login",
103
+ size: "sm",
104
+ icon: "user",
105
+ children: /* @__PURE__ */ jsx("span", {
106
+ class: "sidebar__label",
107
+ children: "Sign in"
108
+ })
109
+ });
110
+ return /* @__PURE__ */ jsx("div", {
111
+ class: accountRoot,
112
+ children: /* @__PURE__ */ jsxs(Dropdown, {
113
+ block: true,
114
+ placement: "above",
115
+ ariaLabel: `Account: ${user.name}`,
116
+ label: /* @__PURE__ */ jsxs("span", {
117
+ class: "account__who",
118
+ children: [/* @__PURE__ */ jsx(Avatar, {
119
+ name: user.name,
120
+ src: safeImageUrl(user.avatarUrl),
121
+ size: "sm"
122
+ }), /* @__PURE__ */ jsxs("span", {
123
+ class: "account__text sidebar__label",
124
+ children: [/* @__PURE__ */ jsx("span", {
125
+ class: "account__name",
126
+ children: user.name
127
+ }), /* @__PURE__ */ jsx("span", {
128
+ class: "account__role",
129
+ children: user.role
130
+ })]
131
+ })]
132
+ }),
133
+ children: [
134
+ /* @__PURE__ */ jsxs("div", {
135
+ class: "account__meta",
136
+ children: [/* @__PURE__ */ jsx("strong", { children: user.name }), user.role]
137
+ }),
138
+ /* @__PURE__ */ jsx(DropdownDivider, {}),
139
+ /* @__PURE__ */ jsx(DropdownItem, {
140
+ href: "/profile",
141
+ icon: "user",
142
+ children: "Profile"
143
+ }),
144
+ /* @__PURE__ */ jsxs("form", {
145
+ class: "account__logout",
146
+ method: "post",
147
+ action: "/auth/logout",
148
+ children: [csrfField(), /* @__PURE__ */ jsx(DropdownItem, {
149
+ type: "submit",
150
+ icon: "log-out",
151
+ children: "Sign out"
152
+ })]
153
+ })
154
+ ]
155
+ })
156
+ });
157
+ };
158
+ //#endregion
159
+ export { ThemeMenu, UserMenu };
160
+
161
+ //# sourceMappingURL=account.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"account.mjs","names":[],"sources":["../../../src/ui/shell/account.tsx"],"sourcesContent":["import { safeImageUrl } from \"@storyshelf/core/urls\";\nimport type { FC } from \"hono/jsx\";\nimport { getStore } from \"../../store.ts\";\nimport { Avatar } from \"../avatar.tsx\";\nimport { Button } from \"../buttons.tsx\";\nimport { csrfField } from \"../csrf-field.tsx\";\nimport { css } from \"../css.ts\";\nimport { Dropdown, DropdownDivider, DropdownItem } from \"../dropdown.tsx\";\nimport { Icon } from \"../icons/icon.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/** Styles for the account widgets that live in the sidebar footer. */\nconst accountRoot = css`\n /* account */\n display: grid;\n gap: var(--space-1);\n .account__who {\n display: flex;\n align-items: center;\n gap: var(--space-2);\n min-width: 0;\n }\n .account__text {\n display: grid;\n min-width: 0;\n text-align: left;\n line-height: 1.2;\n }\n .account__name {\n overflow: hidden;\n font-size: var(--text-base);\n font-weight: 600;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .account__role {\n color: var(--text-secondary);\n font-size: var(--text-xs);\n text-transform: capitalize;\n }\n .account__meta {\n padding: 0.4rem 0.6rem 0.5rem;\n color: var(--text-secondary);\n font-size: var(--text-sm);\n line-height: 1.3;\n }\n .account__meta strong {\n display: block;\n color: var(--text-primary);\n font-size: var(--text-base);\n }\n .account__logout {\n margin: 0;\n }\n`;\n\n/** Light / Dark / System menu; the trigger shows the active mode's icon. */\nexport const ThemeMenu: FC = () => {\n return (\n <Dropdown\n iconOnly\n placement=\"above\"\n ariaLabel=\"Theme\"\n label={\n <>\n <span data-theme-icon=\"light\">\n <Icon name=\"sun\" />\n </span>\n <span data-theme-icon=\"dark\">\n <Icon name=\"moon\" />\n </span>\n <span data-theme-icon=\"system\">\n <Icon name=\"monitor\" />\n </span>\n </>\n }\n >\n <DropdownItem icon=\"sun\" data-theme-set=\"light\">\n Light\n </DropdownItem>\n <DropdownItem icon=\"moon\" data-theme-set=\"dark\">\n Dark\n </DropdownItem>\n <DropdownItem icon=\"monitor\" data-theme-set=\"system\">\n System\n </DropdownItem>\n </Dropdown>\n );\n};\n\n/** Signed-in user row (name, role, profile, sign out); a sign-in button when signed out. */\nexport const UserMenu: FC = () => {\n const { user, authEnabled } = getStore();\n if (!authEnabled) {\n return null;\n }\n if (!user) {\n return (\n <Button href=\"/auth/login\" size=\"sm\" icon=\"user\">\n <span class=\"sidebar__label\">Sign in</span>\n </Button>\n );\n }\n return (\n <div class={accountRoot}>\n <Dropdown\n block\n placement=\"above\"\n ariaLabel={`Account: ${user.name}`}\n label={\n <span class=\"account__who\">\n <Avatar name={user.name} src={safeImageUrl(user.avatarUrl)} size=\"sm\" />\n <span class=\"account__text sidebar__label\">\n <span class=\"account__name\">{user.name}</span>\n <span class=\"account__role\">{user.role}</span>\n </span>\n </span>\n }\n >\n <div class=\"account__meta\">\n <strong>{user.name}</strong>\n {user.role}\n </div>\n <DropdownDivider />\n <DropdownItem href=\"/profile\" icon=\"user\">\n Profile\n </DropdownItem>\n <form class=\"account__logout\" method=\"post\" action=\"/auth/logout\">\n {csrfField()}\n <DropdownItem type=\"submit\" icon=\"log-out\">\n Sign out\n </DropdownItem>\n </form>\n </Dropdown>\n </div>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;AAaA,MAAM,cAAc,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6CvB,MAAa,kBAAsB;CACjC,OACE,qBAAC,UAAD;EACE,UAAA;EACA,WAAU;EACV,WAAU;EACV,OACE,qBAAA,UAAA,EAAA,UAAA;GACE,oBAAC,QAAD;IAAM,mBAAgB;IACpB,UAAA,oBAAC,MAAD,EAAM,MAAK,MAAO,CAAA;GACd,CAAA;GACN,oBAAC,QAAD;IAAM,mBAAgB;IACpB,UAAA,oBAAC,MAAD,EAAM,MAAK,OAAQ,CAAA;GACf,CAAA;GACN,oBAAC,QAAD;IAAM,mBAAgB;IACpB,UAAA,oBAAC,MAAD,EAAM,MAAK,UAAW,CAAA;GAClB,CAAA;EACN,EAAA,CAAA;EAfN,UAAA;GAkBE,oBAAC,cAAD;IAAc,MAAK;IAAM,kBAAe;IAAQ,UAAA;GAElC,CAAA;GACd,oBAAC,cAAD;IAAc,MAAK;IAAO,kBAAe;IAAO,UAAA;GAElC,CAAA;GACd,oBAAC,cAAD;IAAc,MAAK;IAAU,kBAAe;IAAS,UAAA;GAEvC,CAAA;EACN;;AAEd;;AAGA,MAAa,iBAAqB;CAChC,MAAM,EAAE,MAAM,gBAAgB,SAAS;CACvC,IAAI,CAAC,aACH,OAAO;CAET,IAAI,CAAC,MACH,OACE,oBAAC,QAAD;EAAQ,MAAK;EAAc,MAAK;EAAK,MAAK;EACxC,UAAA,oBAAC,QAAD;GAAM,OAAM;GAAiB,UAAA;EAAa,CAAA;CACpC,CAAA;CAGZ,OACE,oBAAC,OAAD;EAAK,OAAO;EACV,UAAA,qBAAC,UAAD;GACE,OAAA;GACA,WAAU;GACV,WAAW,YAAY,KAAK;GAC5B,OACE,qBAAC,QAAD;IAAM,OAAM;IAAZ,UAAA,CACE,oBAAC,QAAD;KAAQ,MAAM,KAAK;KAAM,KAAK,aAAa,KAAK,SAAS;KAAG,MAAK;IAAM,CAAA,GACvE,qBAAC,QAAD;KAAM,OAAM;KAAZ,UAAA,CACE,oBAAC,QAAD;MAAM,OAAM;MAAiB,UAAA,KAAK;KAAW,CAAA,GAC7C,oBAAC,QAAD;MAAM,OAAM;MAAiB,UAAA,KAAK;KAAW,CAAA,CACzC;IACF,CAAA,CAAA;;GAXV,UAAA;IAcE,qBAAC,OAAD;KAAK,OAAM;KAAX,UAAA,CACE,oBAAC,UAAD,EAAA,UAAS,KAAK,KAAa,CAAA,GAC1B,KAAK,IACH;;IACL,oBAAC,iBAAD,CAAkB,CAAA;IAClB,oBAAC,cAAD;KAAc,MAAK;KAAW,MAAK;KAAO,UAAA;IAE5B,CAAA;IACd,qBAAC,QAAD;KAAM,OAAM;KAAkB,QAAO;KAAO,QAAO;KAAnD,UAAA,CACG,UAAU,GACX,oBAAC,cAAD;MAAc,MAAK;MAAS,MAAK;MAAU,UAAA;KAE7B,CAAA,CACV;;GACE;;CACP,CAAA;AAET"}
@@ -0,0 +1,69 @@
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
+ import { safeImageUrl } from "@storyshelf/core/urls";
8
+ //#region src/ui/shell/bare.tsx
9
+ /** Chrome-free page for signed-out flows: brand over a centered column. */
10
+ const bareRoot = css`
11
+ /* bare */
12
+ display: grid;
13
+ place-content: center;
14
+ justify-items: center;
15
+ gap: var(--space-5);
16
+ min-height: 100vh;
17
+ padding: var(--space-6) var(--space-4);
18
+ background: radial-gradient(60rem 30rem at 50% -10%, var(--accent-subtle), transparent 70%);
19
+ .bare__brand {
20
+ display: inline-flex;
21
+ align-items: center;
22
+ gap: 0.6rem;
23
+ color: var(--text-primary);
24
+ font-size: var(--text-xl);
25
+ font-weight: 700;
26
+ letter-spacing: -0.02em;
27
+ }
28
+ .bare__mark {
29
+ display: inline-grid;
30
+ place-items: center;
31
+ width: 34px;
32
+ height: 34px;
33
+ border-radius: var(--radius);
34
+ background: var(--accent);
35
+ color: var(--accent-contrast);
36
+ }
37
+ .bare__body {
38
+ width: min(400px, 100%);
39
+ }
40
+ `;
41
+ /** Brand header plus a narrow column, used instead of the app shell. */
42
+ const BareShell = ({ name, logo, children }) => {
43
+ const safeLogo = safeImageUrl(logo);
44
+ return /* @__PURE__ */ jsxs("main", {
45
+ id: "main-content",
46
+ class: bareRoot,
47
+ tabindex: -1,
48
+ children: [/* @__PURE__ */ jsxs("div", {
49
+ class: "bare__brand",
50
+ children: [safeLogo ? /* @__PURE__ */ jsx("img", {
51
+ src: safeLogo,
52
+ alt: "",
53
+ width: "34",
54
+ height: "34"
55
+ }) : /* @__PURE__ */ jsx("span", {
56
+ class: "bare__mark",
57
+ "aria-hidden": "true",
58
+ children: /* @__PURE__ */ jsx(Icon, { name: "layers" })
59
+ }), /* @__PURE__ */ jsx("span", { children: name })]
60
+ }), /* @__PURE__ */ jsx("div", {
61
+ class: "bare__body",
62
+ children
63
+ })]
64
+ });
65
+ };
66
+ //#endregion
67
+ export { BareShell };
68
+
69
+ //# sourceMappingURL=bare.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bare.mjs","names":[],"sources":["../../../src/ui/shell/bare.tsx"],"sourcesContent":["import { safeImageUrl } from \"@storyshelf/core/urls\";\nimport type { FC } from \"hono/jsx\";\nimport { css } from \"../css.ts\";\nimport { Icon } from \"../icons/icon.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/** Chrome-free page for signed-out flows: brand over a centered column. */\nconst bareRoot = css`\n /* bare */\n display: grid;\n place-content: center;\n justify-items: center;\n gap: var(--space-5);\n min-height: 100vh;\n padding: var(--space-6) var(--space-4);\n background: radial-gradient(60rem 30rem at 50% -10%, var(--accent-subtle), transparent 70%);\n .bare__brand {\n display: inline-flex;\n align-items: center;\n gap: 0.6rem;\n color: var(--text-primary);\n font-size: var(--text-xl);\n font-weight: 700;\n letter-spacing: -0.02em;\n }\n .bare__mark {\n display: inline-grid;\n place-items: center;\n width: 34px;\n height: 34px;\n border-radius: var(--radius);\n background: var(--accent);\n color: var(--accent-contrast);\n }\n .bare__body {\n width: min(400px, 100%);\n }\n`;\n\n/** Brand header plus a narrow column, used instead of the app shell. */\nexport const BareShell: FC<{ name: string; logo?: string; children?: unknown }> = ({\n name,\n logo,\n children,\n}) => {\n const safeLogo = safeImageUrl(logo);\n return (\n <main id=\"main-content\" class={bareRoot} tabindex={-1}>\n <div class=\"bare__brand\">\n {safeLogo ? (\n <img src={safeLogo} alt=\"\" width=\"34\" height=\"34\" />\n ) : (\n <span class=\"bare__mark\" aria-hidden=\"true\">\n <Icon name=\"layers\" />\n </span>\n )}\n <span>{name}</span>\n </div>\n <div class=\"bare__body\">{children}</div>\n </main>\n );\n};\n"],"mappings":";;;;;;;;;AAQA,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCpB,MAAa,aAAsE,EACjF,MACA,MACA,eACI;CACJ,MAAM,WAAW,aAAa,IAAI;CAClC,OACE,qBAAC,QAAD;EAAM,IAAG;EAAe,OAAO;EAAU,UAAU;EAAnD,UAAA,CACE,qBAAC,OAAD;GAAK,OAAM;GAAX,UAAA,CACG,WACC,oBAAC,OAAD;IAAK,KAAK;IAAU,KAAI;IAAG,OAAM;IAAK,QAAO;GAAM,CAAA,IAEnD,oBAAC,QAAD;IAAM,OAAM;IAAa,eAAY;IACnC,UAAA,oBAAC,MAAD,EAAM,MAAK,SAAU,CAAA;GACjB,CAAA,GAER,oBAAC,QAAD,EAAA,UAAO,KAAW,CAAA,CACf;EACL,CAAA,GAAA,oBAAC,OAAD;GAAK,OAAM;GAAc;EAAc,CAAA,CACnC;;AAEV"}
@@ -0,0 +1,46 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ //#region src/ui/shell/boot-script.ts
5
+ /**
6
+ * Runs in `<head>` before first paint so the saved theme and sidebar state
7
+ * apply without a flash. Exposes cookie helpers to the client script.
8
+ */
9
+ function bootScript() {
10
+ return `
11
+ (function(){
12
+ var themeCookie='storyshelf_theme';
13
+ var sidebarCookie='storyshelf_sidebar';
14
+ function readCookie(name){
15
+ var parts=document.cookie.split('; ');
16
+ for(var i=0;i<parts.length;i++){
17
+ var at=parts[i].indexOf('=');
18
+ if(at>0&&parts[i].slice(0,at)===name){ return decodeURIComponent(parts[i].slice(at+1)); }
19
+ }
20
+ return null;
21
+ }
22
+ function writeCookie(name,value){
23
+ document.cookie=name+'='+encodeURIComponent(value)+'; Path=/; Max-Age=31536000; SameSite=Lax';
24
+ }
25
+ function applyTheme(value){
26
+ var ok=value==='light'||value==='dark'||value==='system';
27
+ document.documentElement.setAttribute('data-theme',ok?value:'system');
28
+ }
29
+ function applySidebar(value){
30
+ if(value==='rail'){ document.documentElement.setAttribute('data-sidebar','rail'); }
31
+ else { document.documentElement.removeAttribute('data-sidebar'); }
32
+ }
33
+ var theme=readCookie(themeCookie);
34
+ if(theme){ applyTheme(theme); }
35
+ applySidebar(readCookie(sidebarCookie));
36
+ window.__storyshelfShell={
37
+ readCookie:readCookie,writeCookie:writeCookie,applyTheme:applyTheme,applySidebar:applySidebar,
38
+ themeCookie:themeCookie,sidebarCookie:sidebarCookie
39
+ };
40
+ })();
41
+ `.trim();
42
+ }
43
+ //#endregion
44
+ export { bootScript };
45
+
46
+ //# sourceMappingURL=boot-script.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boot-script.mjs","names":[],"sources":["../../../src/ui/shell/boot-script.ts"],"sourcesContent":["/**\n * Runs in `<head>` before first paint so the saved theme and sidebar state\n * apply without a flash. Exposes cookie helpers to the client script.\n */\nexport function bootScript(): string {\n return `\n (function(){\n var themeCookie='storyshelf_theme';\n var sidebarCookie='storyshelf_sidebar';\n function readCookie(name){\n var parts=document.cookie.split('; ');\n for(var i=0;i<parts.length;i++){\n var at=parts[i].indexOf('=');\n if(at>0&&parts[i].slice(0,at)===name){ return decodeURIComponent(parts[i].slice(at+1)); }\n }\n return null;\n }\n function writeCookie(name,value){\n document.cookie=name+'='+encodeURIComponent(value)+'; Path=/; Max-Age=31536000; SameSite=Lax';\n }\n function applyTheme(value){\n var ok=value==='light'||value==='dark'||value==='system';\n document.documentElement.setAttribute('data-theme',ok?value:'system');\n }\n function applySidebar(value){\n if(value==='rail'){ document.documentElement.setAttribute('data-sidebar','rail'); }\n else { document.documentElement.removeAttribute('data-sidebar'); }\n }\n var theme=readCookie(themeCookie);\n if(theme){ applyTheme(theme); }\n applySidebar(readCookie(sidebarCookie));\n window.__storyshelfShell={\n readCookie:readCookie,writeCookie:writeCookie,applyTheme:applyTheme,applySidebar:applySidebar,\n themeCookie:themeCookie,sidebarCookie:sidebarCookie\n };\n })();\n `.trim();\n}\n"],"mappings":";;;;;;;;AAIA,SAAgB,aAAqB;CACnC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA+BL,KAAK;AACT"}