@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,205 @@
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 { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
7
+ //#region src/ui/buttons.tsx
8
+ const btnBase = css`
9
+ /* btn */
10
+ appearance: none;
11
+ border: 1px solid transparent;
12
+ border-radius: var(--radius);
13
+ padding: 0.55rem 0.9rem;
14
+ font-weight: 600;
15
+ font-size: var(--text-base);
16
+ cursor: pointer;
17
+ text-decoration: none;
18
+ display: inline-flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ gap: 0.4rem;
22
+ line-height: 1.2;
23
+ /* Top-aligned so a button in a <form> (a block with a line box) is exactly
24
+ as tall as the button instead of picking up a baseline strut. */
25
+ vertical-align: top;
26
+ min-height: 40px;
27
+ transition:
28
+ background var(--dur-fast) var(--ease),
29
+ border-color var(--dur-fast) var(--ease),
30
+ filter var(--dur-fast) var(--ease),
31
+ transform var(--dur-fast) var(--ease);
32
+ &:active:not(:disabled) {
33
+ transform: translateY(1px);
34
+ }
35
+ &:disabled {
36
+ opacity: 0.55;
37
+ cursor: not-allowed;
38
+ }
39
+ `;
40
+ const btnPrimary = css`
41
+ /* btn-primary */
42
+ ${btnBase}
43
+ background: var(--accent);
44
+ color: var(--accent-contrast);
45
+ border-color: var(--accent);
46
+ box-shadow: var(--shadow);
47
+ &:hover {
48
+ filter: brightness(1.06);
49
+ text-decoration: none;
50
+ }
51
+ `;
52
+ const btnPrimarySm = css`
53
+ /* btn-primary-sm */
54
+ ${btnPrimary}
55
+ padding: 0.35rem 0.6rem;
56
+ font-size: 0.8rem;
57
+ min-height: 32px;
58
+ `;
59
+ const btnSecondary = css`
60
+ /* btn-secondary */
61
+ ${btnBase}
62
+ background: var(--surface-card);
63
+ color: var(--text-primary);
64
+ border-color: var(--border);
65
+ box-shadow: var(--shadow);
66
+ &:hover {
67
+ background: var(--surface-subtle);
68
+ text-decoration: none;
69
+ }
70
+ `;
71
+ const btnSecondarySm = css`
72
+ /* btn-secondary-sm */
73
+ ${btnSecondary}
74
+ padding: 0.35rem 0.6rem;
75
+ font-size: 0.8rem;
76
+ min-height: 32px;
77
+ `;
78
+ const btnOutline = css`
79
+ /* btn-outline */
80
+ ${btnSecondary}
81
+ &:hover {
82
+ background: var(--surface-subtle);
83
+ text-decoration: none;
84
+ }
85
+ `;
86
+ const btnOutlineSm = css`
87
+ /* btn-outline-sm */
88
+ ${btnOutline}
89
+ padding: 0.35rem 0.6rem;
90
+ font-size: 0.8rem;
91
+ min-height: 32px;
92
+ `;
93
+ const btnGhost = css`
94
+ /* btn-ghost */
95
+ ${btnBase}
96
+ background: transparent;
97
+ color: var(--text-secondary);
98
+ border-color: transparent;
99
+ &:hover {
100
+ background: var(--surface-muted);
101
+ color: var(--text-primary);
102
+ text-decoration: none;
103
+ }
104
+ `;
105
+ const btnGhostSm = css`
106
+ /* btn-ghost-sm */
107
+ ${btnGhost}
108
+ padding: 0.35rem 0.6rem;
109
+ font-size: 0.8rem;
110
+ min-height: 32px;
111
+ `;
112
+ const btnDanger = css`
113
+ /* btn-danger */
114
+ ${btnBase}
115
+ background: color-mix(in srgb, var(--status-rejected) 82%, #000);
116
+ color: #fff;
117
+ border-color: color-mix(in srgb, var(--status-rejected) 82%, #000);
118
+ &:hover {
119
+ filter: brightness(1.06);
120
+ text-decoration: none;
121
+ }
122
+ `;
123
+ const btnDangerSm = css`
124
+ /* btn-danger-sm */
125
+ ${btnDanger}
126
+ padding: 0.35rem 0.6rem;
127
+ font-size: 0.8rem;
128
+ min-height: 32px;
129
+ `;
130
+ const btnClasses = {
131
+ primary: {
132
+ md: btnPrimary,
133
+ sm: btnPrimarySm
134
+ },
135
+ secondary: {
136
+ md: btnSecondary,
137
+ sm: btnSecondarySm
138
+ },
139
+ outline: {
140
+ md: btnOutline,
141
+ sm: btnOutlineSm
142
+ },
143
+ ghost: {
144
+ md: btnGhost,
145
+ sm: btnGhostSm
146
+ },
147
+ danger: {
148
+ md: btnDanger,
149
+ sm: btnDangerSm
150
+ }
151
+ };
152
+ css`
153
+ /* tabs */
154
+ display: flex;
155
+ gap: 0.25rem;
156
+ border-bottom: 1px solid var(--border);
157
+ margin-bottom: 1rem;
158
+ overflow-x: auto;
159
+ `;
160
+ const tabsLink = css`
161
+ /* tabs-link */
162
+ padding: 0.55rem 0.75rem;
163
+ border-bottom: 2px solid transparent;
164
+ color: var(--text-secondary);
165
+ font-size: 0.875rem;
166
+ text-decoration: none;
167
+ white-space: nowrap;
168
+ font-weight: 600;
169
+ &:hover {
170
+ color: var(--text-primary);
171
+ text-decoration: none;
172
+ }
173
+ `;
174
+ css`
175
+ /* tabs-link-active */
176
+ ${tabsLink}
177
+ color: var(--text-primary);
178
+ border-bottom-color: var(--accent);
179
+ `;
180
+ /** Button or link styled as a button (renders an anchor when `href` is set). */
181
+ const Button = ({ variant = "primary", size = "md", type = "button", href, disabled, icon, children, ...rest }) => {
182
+ const cls = btnClasses[variant][size];
183
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [icon ? /* @__PURE__ */ jsx(Icon, {
184
+ name: icon,
185
+ size: size === "sm" ? "sm" : "md"
186
+ }) : null, children] });
187
+ if (href) return /* @__PURE__ */ jsx("a", {
188
+ class: cls,
189
+ href,
190
+ "aria-disabled": disabled ? "true" : void 0,
191
+ ...rest,
192
+ children: content
193
+ });
194
+ return /* @__PURE__ */ jsx("button", {
195
+ class: cls,
196
+ type,
197
+ disabled,
198
+ ...rest,
199
+ children: content
200
+ });
201
+ };
202
+ //#endregion
203
+ export { Button };
204
+
205
+ //# sourceMappingURL=buttons.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buttons.mjs","names":[],"sources":["../../src/ui/buttons.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\ntype ButtonVariant = \"primary\" | \"secondary\" | \"ghost\" | \"danger\" | \"outline\";\ntype ButtonSize = \"md\" | \"sm\";\n\nconst btnBase = css`\n /* btn */\n appearance: none;\n border: 1px solid transparent;\n border-radius: var(--radius);\n padding: 0.55rem 0.9rem;\n font-weight: 600;\n font-size: var(--text-base);\n cursor: pointer;\n text-decoration: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 0.4rem;\n line-height: 1.2;\n /* Top-aligned so a button in a <form> (a block with a line box) is exactly\n as tall as the button instead of picking up a baseline strut. */\n vertical-align: top;\n min-height: 40px;\n transition:\n background var(--dur-fast) var(--ease),\n border-color var(--dur-fast) var(--ease),\n filter var(--dur-fast) var(--ease),\n transform var(--dur-fast) var(--ease);\n &:active:not(:disabled) {\n transform: translateY(1px);\n }\n &:disabled {\n opacity: 0.55;\n cursor: not-allowed;\n }\n`;\n\nconst btnPrimary = css`\n /* btn-primary */\n ${btnBase}\n background: var(--accent);\n color: var(--accent-contrast);\n border-color: var(--accent);\n box-shadow: var(--shadow);\n &:hover {\n filter: brightness(1.06);\n text-decoration: none;\n }\n`;\n\nconst btnPrimarySm = css`\n /* btn-primary-sm */\n ${btnPrimary}\n padding: 0.35rem 0.6rem;\n font-size: 0.8rem;\n min-height: 32px;\n`;\n\nconst btnSecondary = css`\n /* btn-secondary */\n ${btnBase}\n background: var(--surface-card);\n color: var(--text-primary);\n border-color: var(--border);\n box-shadow: var(--shadow);\n &:hover {\n background: var(--surface-subtle);\n text-decoration: none;\n }\n`;\n\nconst btnSecondarySm = css`\n /* btn-secondary-sm */\n ${btnSecondary}\n padding: 0.35rem 0.6rem;\n font-size: 0.8rem;\n min-height: 32px;\n`;\n\nconst btnOutline = css`\n /* btn-outline */\n ${btnSecondary}\n &:hover {\n background: var(--surface-subtle);\n text-decoration: none;\n }\n`;\n\nconst btnOutlineSm = css`\n /* btn-outline-sm */\n ${btnOutline}\n padding: 0.35rem 0.6rem;\n font-size: 0.8rem;\n min-height: 32px;\n`;\n\nconst btnGhost = css`\n /* btn-ghost */\n ${btnBase}\n background: transparent;\n color: var(--text-secondary);\n border-color: transparent;\n &:hover {\n background: var(--surface-muted);\n color: var(--text-primary);\n text-decoration: none;\n }\n`;\n\nconst btnGhostSm = css`\n /* btn-ghost-sm */\n ${btnGhost}\n padding: 0.35rem 0.6rem;\n font-size: 0.8rem;\n min-height: 32px;\n`;\n\nconst btnDanger = css`\n /* btn-danger */\n ${btnBase}\n background: color-mix(in srgb, var(--status-rejected) 82%, #000);\n color: #fff;\n border-color: color-mix(in srgb, var(--status-rejected) 82%, #000);\n &:hover {\n filter: brightness(1.06);\n text-decoration: none;\n }\n`;\n\nconst btnDangerSm = css`\n /* btn-danger-sm */\n ${btnDanger}\n padding: 0.35rem 0.6rem;\n font-size: 0.8rem;\n min-height: 32px;\n`;\n\nconst btnClasses: Record<ButtonVariant, Record<ButtonSize, Promise<string>>> = {\n primary: { md: btnPrimary, sm: btnPrimarySm },\n secondary: { md: btnSecondary, sm: btnSecondarySm },\n outline: { md: btnOutline, sm: btnOutlineSm },\n ghost: { md: btnGhost, sm: btnGhostSm },\n danger: { md: btnDanger, sm: btnDangerSm },\n};\n\nconst tabsNav = css`\n /* tabs */\n display: flex;\n gap: 0.25rem;\n border-bottom: 1px solid var(--border);\n margin-bottom: 1rem;\n overflow-x: auto;\n`;\n\nconst tabsLink = css`\n /* tabs-link */\n padding: 0.55rem 0.75rem;\n border-bottom: 2px solid transparent;\n color: var(--text-secondary);\n font-size: 0.875rem;\n text-decoration: none;\n white-space: nowrap;\n font-weight: 600;\n &:hover {\n color: var(--text-primary);\n text-decoration: none;\n }\n`;\n\nconst tabsLinkActive = css`\n /* tabs-link-active */\n ${tabsLink}\n color: var(--text-primary);\n border-bottom-color: var(--accent);\n`;\n\n/** Button or link styled as a button (renders an anchor when `href` is set). */\nexport const Button: FC<{\n variant?: ButtonVariant;\n size?: ButtonSize;\n type?: \"button\" | \"submit\" | \"reset\";\n href?: string;\n disabled?: boolean;\n /** Leading icon from the sprite. */\n icon?: IconName;\n children?: unknown;\n [key: string]: unknown;\n}> = ({\n variant = \"primary\",\n size = \"md\",\n type = \"button\",\n href,\n disabled,\n icon,\n children,\n ...rest\n}) => {\n const cls = btnClasses[variant][size];\n const content = (\n <>\n {icon ? <Icon name={icon} size={size === \"sm\" ? \"sm\" : \"md\"} /> : null}\n {children}\n </>\n );\n if (href) {\n return (\n <a class={cls} href={href} aria-disabled={disabled ? \"true\" : undefined} {...rest}>\n {content}\n </a>\n );\n }\n return (\n <button class={cls} type={type} disabled={disabled} {...rest}>\n {content}\n </button>\n );\n};\n\n/** Tab navigation bar for section switching. */\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const Tabs: FC<{\n tabs: { label: string; href: string; active?: boolean }[];\n label?: string;\n}> = ({ tabs, label = \"Sections\" }) => {\n return (\n <nav class={tabsNav} aria-label={label}>\n {tabs.map((tab) => (\n <a\n class={tab.active ? tabsLinkActive : tabsLink}\n href={tab.href}\n aria-current={tab.active ? \"page\" : undefined}\n >\n {tab.label}\n </a>\n ))}\n </nav>\n );\n};\n"],"mappings":";;;;;;;AAUA,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCnB,MAAM,aAAa,GAAG;;IAElB,QAAQ;;;;;;;;;;AAWZ,MAAM,eAAe,GAAG;;IAEpB,WAAW;;;;;AAMf,MAAM,eAAe,GAAG;;IAEpB,QAAQ;;;;;;;;;;AAWZ,MAAM,iBAAiB,GAAG;;IAEtB,aAAa;;;;;AAMjB,MAAM,aAAa,GAAG;;IAElB,aAAa;;;;;;AAOjB,MAAM,eAAe,GAAG;;IAEpB,WAAW;;;;;AAMf,MAAM,WAAW,GAAG;;IAEhB,QAAQ;;;;;;;;;;AAWZ,MAAM,aAAa,GAAG;;IAElB,SAAS;;;;;AAMb,MAAM,YAAY,GAAG;;IAEjB,QAAQ;;;;;;;;;AAUZ,MAAM,cAAc,GAAG;;IAEnB,UAAU;;;;;AAMd,MAAM,aAAyE;CAC7E,SAAS;EAAE,IAAI;EAAY,IAAI;CAAa;CAC5C,WAAW;EAAE,IAAI;EAAc,IAAI;CAAe;CAClD,SAAS;EAAE,IAAI;EAAY,IAAI;CAAa;CAC5C,OAAO;EAAE,IAAI;EAAU,IAAI;CAAW;CACtC,QAAQ;EAAE,IAAI;EAAW,IAAI;CAAY;AAC3C;AAEgB,GAAG;;;;;;;;AASnB,MAAM,WAAW,GAAG;;;;;;;;;;;;;;AAeG,GAAG;;IAEtB,SAAS;;;;;AAMb,MAAa,UAUP,EACJ,UAAU,WACV,OAAO,MACP,OAAO,UACP,MACA,UACA,MACA,UACA,GAAG,WACC;CACJ,MAAM,MAAM,WAAW,QAAQ,CAAC;CAChC,MAAM,UACJ,qBAAA,UAAA,EAAA,UAAA,CACG,OAAO,oBAAC,MAAD;EAAM,MAAM;EAAM,MAAM,SAAS,OAAO,OAAO;CAAO,CAAA,IAAI,MACjE,QACD,EAAA,CAAA;CAEJ,IAAI,MACF,OACE,oBAAC,KAAD;EAAG,OAAO;EAAW;EAAM,iBAAe,WAAW,SAAS,KAAA;EAAW,GAAI;EAC1E,UAAA;CACA,CAAA;CAGP,OACE,oBAAC,UAAD;EAAQ,OAAO;EAAW;EAAgB;EAAU,GAAI;EACrD,UAAA;CACK,CAAA;AAEZ"}
@@ -0,0 +1,47 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { css } from "./css.mjs";
5
+ import { Button } from "./buttons.mjs";
6
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
7
+ //#region src/ui/code-block.tsx
8
+ const codeWrap = css`
9
+ /* code-block */
10
+ display: flex;
11
+ gap: var(--space-2);
12
+ align-items: flex-start;
13
+ justify-content: space-between;
14
+ padding: var(--space-2) var(--space-2) var(--space-2) var(--space-3);
15
+ background: var(--surface-subtle);
16
+ border: 1px solid var(--border);
17
+ border-radius: var(--radius);
18
+ & pre {
19
+ flex: 1;
20
+ min-width: 0;
21
+ margin: 0;
22
+ padding: 0.3rem 0;
23
+ overflow-x: auto;
24
+ font-family: var(--font-mono);
25
+ font-size: var(--text-sm);
26
+ line-height: var(--leading-normal);
27
+ white-space: pre;
28
+ }
29
+ `;
30
+ /** Monospace command/snippet with a copy button (wired by the shell script). */
31
+ const CodeBlock = ({ code, label = "Copy" }) => {
32
+ return /* @__PURE__ */ jsxs("div", {
33
+ class: codeWrap,
34
+ children: [/* @__PURE__ */ jsx("pre", { children: /* @__PURE__ */ jsx("code", { children: code }) }), /* @__PURE__ */ jsx(Button, {
35
+ variant: "ghost",
36
+ size: "sm",
37
+ icon: "copy",
38
+ "data-copy": code,
39
+ "aria-label": label,
40
+ children: label
41
+ })]
42
+ });
43
+ };
44
+ //#endregion
45
+ export { CodeBlock };
46
+
47
+ //# sourceMappingURL=code-block.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"code-block.mjs","names":[],"sources":["../../src/ui/code-block.tsx"],"sourcesContent":["import type { FC } from \"hono/jsx\";\nimport { Button } from \"./buttons.tsx\";\nimport { css } from \"./css.ts\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\nconst codeWrap = css`\n /* code-block */\n display: flex;\n gap: var(--space-2);\n align-items: flex-start;\n justify-content: space-between;\n padding: var(--space-2) var(--space-2) var(--space-2) var(--space-3);\n background: var(--surface-subtle);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n & pre {\n flex: 1;\n min-width: 0;\n margin: 0;\n padding: 0.3rem 0;\n overflow-x: auto;\n font-family: var(--font-mono);\n font-size: var(--text-sm);\n line-height: var(--leading-normal);\n white-space: pre;\n }\n`;\n\n/** Monospace command/snippet with a copy button (wired by the shell script). */\nexport const CodeBlock: FC<{ code: string; label?: string }> = ({ code, label = \"Copy\" }) => {\n return (\n <div class={codeWrap}>\n <pre>\n <code>{code}</code>\n </pre>\n <Button variant=\"ghost\" size=\"sm\" icon=\"copy\" data-copy={code} aria-label={label}>\n {label}\n </Button>\n </div>\n );\n};\n"],"mappings":";;;;;;;AAMA,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;AAwBpB,MAAa,aAAmD,EAAE,MAAM,QAAQ,aAAa;CAC3F,OACE,qBAAC,OAAD;EAAK,OAAO;EAAZ,UAAA,CACE,oBAAC,OAAD,EAAA,UACE,oBAAC,QAAD,EAAA,UAAO,KAAW,CAAA,EACf,CAAA,GACL,oBAAC,QAAD;GAAQ,SAAQ;GAAQ,MAAK;GAAK,MAAK;GAAO,aAAW;GAAM,cAAY;GACxE,UAAA;EACK,CAAA,CACL;;AAET"}
@@ -0,0 +1,344 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { css } from "./css.mjs";
5
+ import { Segmented } from "./segmented.mjs";
6
+ import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
7
+ //#region src/ui/compare-stage.tsx
8
+ /**
9
+ * Screenshot comparison surface. All modes share one set of images; the
10
+ * `data-view` / `data-zoom` / `data-flip` attributes on the root (driven by
11
+ * the review script) decide what is shown. Without JS it stays side-by-side.
12
+ */
13
+ const stageRoot = css`
14
+ /* compare-stage */
15
+ display: grid;
16
+ gap: var(--space-3);
17
+ .stage__bar {
18
+ display: flex;
19
+ flex-wrap: wrap;
20
+ gap: var(--space-3);
21
+ align-items: center;
22
+ justify-content: space-between;
23
+ }
24
+ .stage__grid {
25
+ display: grid;
26
+ grid-template-columns: repeat(3, minmax(0, 1fr));
27
+ gap: var(--space-3);
28
+ align-items: start;
29
+ }
30
+ .stage__pane {
31
+ position: relative;
32
+ min-width: 0;
33
+ max-height: 78vh;
34
+ overflow: auto;
35
+ background: var(--surface-card);
36
+ border: 1px solid var(--border);
37
+ border-radius: var(--radius);
38
+ }
39
+ .stage__label {
40
+ position: sticky;
41
+ top: 0;
42
+ left: 0;
43
+ z-index: 2;
44
+ display: flex;
45
+ align-items: center;
46
+ justify-content: space-between;
47
+ padding: 0.35rem 0.6rem;
48
+ background: var(--surface-subtle);
49
+ border-bottom: 1px solid var(--border);
50
+ color: var(--text-secondary);
51
+ font-size: var(--text-xs);
52
+ font-weight: 600;
53
+ letter-spacing: 0.06em;
54
+ text-transform: uppercase;
55
+ }
56
+ .stage__img {
57
+ display: block;
58
+ width: 100%;
59
+ height: auto;
60
+ background: repeating-conic-gradient(var(--surface-muted) 0% 25%, var(--surface-subtle) 0% 50%)
61
+ 0 0 / 16px 16px;
62
+ }
63
+ .stage__empty {
64
+ display: grid;
65
+ place-items: center;
66
+ aspect-ratio: 16 / 9;
67
+ padding: var(--space-4);
68
+ background: var(--surface-subtle);
69
+ color: var(--text-secondary);
70
+ font-size: var(--text-sm);
71
+ text-align: center;
72
+ }
73
+ .stage__divider,
74
+ .stage__controls {
75
+ display: none;
76
+ }
77
+ .stage__controls {
78
+ align-items: center;
79
+ gap: var(--space-3);
80
+ color: var(--text-secondary);
81
+ font-size: var(--text-sm);
82
+ }
83
+ .stage__controls input {
84
+ flex: 1;
85
+ accent-color: var(--accent);
86
+ }
87
+ &[data-zoom="100"] .stage__img {
88
+ width: auto;
89
+ max-width: none;
90
+ }
91
+ &[data-zoom="200"] .stage__img {
92
+ width: auto;
93
+ max-width: none;
94
+ zoom: 2;
95
+ }
96
+ &[data-zoom="400"] .stage__img {
97
+ width: auto;
98
+ max-width: none;
99
+ zoom: 4;
100
+ }
101
+ /* Zoomed in, show real pixels instead of a smoothed blur. */
102
+ &[data-zoom="200"] .stage__img,
103
+ &[data-zoom="400"] .stage__img {
104
+ image-rendering: pixelated;
105
+ }
106
+ &[data-view="diff"] .stage__grid,
107
+ &[data-view="flip"] .stage__grid,
108
+ &[data-view="swipe"] .stage__grid,
109
+ &[data-view="onion"] .stage__grid {
110
+ grid-template-columns: minmax(0, 1fr);
111
+ }
112
+ &[data-view="diff"] .stage__pane:not([data-pane="diff"]) {
113
+ display: none;
114
+ }
115
+ &[data-view="flip"][data-flip="baseline"] .stage__pane:not([data-pane="baseline"]),
116
+ &[data-view="flip"]:not([data-flip="baseline"]) .stage__pane:not([data-pane="current"]) {
117
+ display: none;
118
+ }
119
+ &[data-view="swipe"] .stage__pane[data-pane="diff"],
120
+ &[data-view="onion"] .stage__pane[data-pane="diff"] {
121
+ display: none;
122
+ }
123
+ &[data-view="swipe"] .stage__pane,
124
+ &[data-view="onion"] .stage__pane,
125
+ &[data-view="swipe"] .stage__divider {
126
+ grid-area: 1 / 1;
127
+ }
128
+ &[data-view="swipe"] .stage__label,
129
+ &[data-view="onion"] .stage__label {
130
+ display: none;
131
+ }
132
+ &[data-view="swipe"] .stage__pane[data-pane="current"] {
133
+ clip-path: inset(0 0 0 var(--swipe, 50%));
134
+ background: transparent;
135
+ }
136
+ &[data-view="onion"] .stage__pane[data-pane="current"] {
137
+ opacity: var(--onion, 0.5);
138
+ background: transparent;
139
+ }
140
+ &[data-view="swipe"] .stage__divider {
141
+ position: relative;
142
+ display: block;
143
+ z-index: 3;
144
+ justify-self: start;
145
+ width: 2px;
146
+ height: 100%;
147
+ margin-left: var(--swipe, 50%);
148
+ background: var(--accent);
149
+ pointer-events: none;
150
+ }
151
+ /* The only part of the divider that takes pointer input: drag it to wipe. */
152
+ .stage__knob {
153
+ position: absolute;
154
+ top: 50%;
155
+ left: 50%;
156
+ display: grid;
157
+ place-items: center;
158
+ width: 32px;
159
+ height: 32px;
160
+ transform: translate(-50%, -50%);
161
+ background: var(--accent);
162
+ color: var(--accent-contrast);
163
+ border: 2px solid var(--surface-card);
164
+ border-radius: var(--radius-pill);
165
+ box-shadow: var(--shadow-2);
166
+ font-size: var(--text-lg);
167
+ line-height: 1;
168
+ cursor: ew-resize;
169
+ pointer-events: auto;
170
+ touch-action: none;
171
+ user-select: none;
172
+ }
173
+ &[data-view="flip"] .stage__grid {
174
+ cursor: pointer;
175
+ }
176
+ &[data-view="swipe"] .stage__controls[data-for="swipe"],
177
+ &[data-view="onion"] .stage__controls[data-for="onion"] {
178
+ display: flex;
179
+ }
180
+ `;
181
+ const MODES = [
182
+ {
183
+ label: "Side by side",
184
+ value: "split"
185
+ },
186
+ {
187
+ label: "Swipe",
188
+ value: "swipe"
189
+ },
190
+ {
191
+ label: "Onion skin",
192
+ value: "onion"
193
+ },
194
+ {
195
+ label: "Diff",
196
+ value: "diff"
197
+ },
198
+ {
199
+ label: "Flip",
200
+ value: "flip"
201
+ }
202
+ ];
203
+ const ZOOMS = [
204
+ {
205
+ label: "Fit",
206
+ value: "fit"
207
+ },
208
+ {
209
+ label: "100%",
210
+ value: "100"
211
+ },
212
+ {
213
+ label: "200%",
214
+ value: "200"
215
+ },
216
+ {
217
+ label: "400%",
218
+ value: "400"
219
+ }
220
+ ];
221
+ function withActive(items, active) {
222
+ return items.map((item) => ({
223
+ ...item,
224
+ active: item.value === active
225
+ }));
226
+ }
227
+ const Pane = ({ kind, label, src, alt, empty, meta }) => {
228
+ return /* @__PURE__ */ jsxs("div", {
229
+ class: "stage__pane",
230
+ "data-pane": kind,
231
+ children: [/* @__PURE__ */ jsxs("div", {
232
+ class: "stage__label",
233
+ children: [/* @__PURE__ */ jsx("span", { children: label }), meta ? /* @__PURE__ */ jsx("span", { children: meta }) : null]
234
+ }), src ? /* @__PURE__ */ jsx("img", {
235
+ class: "stage__img",
236
+ src,
237
+ alt,
238
+ loading: "lazy",
239
+ decoding: "async"
240
+ }) : /* @__PURE__ */ jsx("div", {
241
+ class: "stage__empty",
242
+ children: empty
243
+ })]
244
+ });
245
+ };
246
+ /** Swipe and onion-skin sliders; each is shown only in its own mode (CSS). */
247
+ const RangeControls = () => {
248
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("label", {
249
+ class: "stage__controls",
250
+ "data-for": "swipe",
251
+ children: [
252
+ /* @__PURE__ */ jsx("span", { children: "Baseline" }),
253
+ /* @__PURE__ */ jsx("input", {
254
+ type: "range",
255
+ min: "0",
256
+ max: "100",
257
+ value: "50",
258
+ "data-compare-swipe": true,
259
+ "aria-label": "Swipe"
260
+ }),
261
+ /* @__PURE__ */ jsx("span", { children: "Current" })
262
+ ]
263
+ }), /* @__PURE__ */ jsxs("label", {
264
+ class: "stage__controls",
265
+ "data-for": "onion",
266
+ children: [
267
+ /* @__PURE__ */ jsx("span", { children: "Baseline" }),
268
+ /* @__PURE__ */ jsx("input", {
269
+ type: "range",
270
+ min: "0",
271
+ max: "100",
272
+ value: "50",
273
+ "data-compare-onion": true,
274
+ "aria-label": "Opacity"
275
+ }),
276
+ /* @__PURE__ */ jsx("span", { children: "Current" })
277
+ ]
278
+ })] });
279
+ };
280
+ /** Baseline / current / diff comparison with swipe, onion-skin, flip, and zoom. */
281
+ const CompareStage = ({ baselineSrc, currentSrc, diffSrc, subject, baselineEmpty = "New story — no baseline yet. Approve to set the baseline.", diffEmpty = "No diff", diffMeta }) => {
282
+ return /* @__PURE__ */ jsxs("div", {
283
+ class: stageRoot,
284
+ "data-compare": true,
285
+ "data-view": "split",
286
+ "data-zoom": "fit",
287
+ "data-flip": "current",
288
+ children: [
289
+ /* @__PURE__ */ jsxs("div", {
290
+ class: "stage__bar",
291
+ children: [/* @__PURE__ */ jsx(Segmented, {
292
+ label: "Comparison mode",
293
+ "data-compare-mode": true,
294
+ items: withActive(MODES, "split")
295
+ }), /* @__PURE__ */ jsx(Segmented, {
296
+ label: "Zoom",
297
+ "data-compare-zoom": true,
298
+ items: withActive(ZOOMS, "fit")
299
+ })]
300
+ }),
301
+ /* @__PURE__ */ jsxs("div", {
302
+ class: "stage__grid",
303
+ children: [
304
+ /* @__PURE__ */ jsx(Pane, {
305
+ kind: "baseline",
306
+ label: "Baseline",
307
+ src: baselineSrc,
308
+ alt: `Baseline for ${subject}`,
309
+ empty: baselineEmpty
310
+ }),
311
+ /* @__PURE__ */ jsx(Pane, {
312
+ kind: "current",
313
+ label: "Current",
314
+ src: currentSrc,
315
+ alt: `Current for ${subject}`,
316
+ empty: "No screenshot"
317
+ }),
318
+ /* @__PURE__ */ jsx(Pane, {
319
+ kind: "diff",
320
+ label: "Diff",
321
+ src: diffSrc,
322
+ alt: `Diff for ${subject}`,
323
+ empty: diffEmpty,
324
+ meta: diffMeta
325
+ }),
326
+ /* @__PURE__ */ jsx("span", {
327
+ class: "stage__divider",
328
+ "aria-hidden": "true",
329
+ children: /* @__PURE__ */ jsx("span", {
330
+ class: "stage__knob",
331
+ "data-compare-knob": true,
332
+ children: "↔"
333
+ })
334
+ })
335
+ ]
336
+ }),
337
+ /* @__PURE__ */ jsx(RangeControls, {})
338
+ ]
339
+ });
340
+ };
341
+ //#endregion
342
+ export { CompareStage };
343
+
344
+ //# sourceMappingURL=compare-stage.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compare-stage.mjs","names":[],"sources":["../../src/ui/compare-stage.tsx"],"sourcesContent":["import type { FC } from \"hono/jsx\";\nimport { css } from \"./css.ts\";\nimport { Segmented } from \"./segmented.tsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\n/**\n * Screenshot comparison surface. All modes share one set of images; the\n * `data-view` / `data-zoom` / `data-flip` attributes on the root (driven by\n * the review script) decide what is shown. Without JS it stays side-by-side.\n */\nconst stageRoot = css`\n /* compare-stage */\n display: grid;\n gap: var(--space-3);\n .stage__bar {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-3);\n align-items: center;\n justify-content: space-between;\n }\n .stage__grid {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: var(--space-3);\n align-items: start;\n }\n .stage__pane {\n position: relative;\n min-width: 0;\n max-height: 78vh;\n overflow: auto;\n background: var(--surface-card);\n border: 1px solid var(--border);\n border-radius: var(--radius);\n }\n .stage__label {\n position: sticky;\n top: 0;\n left: 0;\n z-index: 2;\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0.35rem 0.6rem;\n background: var(--surface-subtle);\n border-bottom: 1px solid var(--border);\n color: var(--text-secondary);\n font-size: var(--text-xs);\n font-weight: 600;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n }\n .stage__img {\n display: block;\n width: 100%;\n height: auto;\n background: repeating-conic-gradient(var(--surface-muted) 0% 25%, var(--surface-subtle) 0% 50%)\n 0 0 / 16px 16px;\n }\n .stage__empty {\n display: grid;\n place-items: center;\n aspect-ratio: 16 / 9;\n padding: var(--space-4);\n background: var(--surface-subtle);\n color: var(--text-secondary);\n font-size: var(--text-sm);\n text-align: center;\n }\n .stage__divider,\n .stage__controls {\n display: none;\n }\n .stage__controls {\n align-items: center;\n gap: var(--space-3);\n color: var(--text-secondary);\n font-size: var(--text-sm);\n }\n .stage__controls input {\n flex: 1;\n accent-color: var(--accent);\n }\n &[data-zoom=\"100\"] .stage__img {\n width: auto;\n max-width: none;\n }\n &[data-zoom=\"200\"] .stage__img {\n width: auto;\n max-width: none;\n zoom: 2;\n }\n &[data-zoom=\"400\"] .stage__img {\n width: auto;\n max-width: none;\n zoom: 4;\n }\n /* Zoomed in, show real pixels instead of a smoothed blur. */\n &[data-zoom=\"200\"] .stage__img,\n &[data-zoom=\"400\"] .stage__img {\n image-rendering: pixelated;\n }\n &[data-view=\"diff\"] .stage__grid,\n &[data-view=\"flip\"] .stage__grid,\n &[data-view=\"swipe\"] .stage__grid,\n &[data-view=\"onion\"] .stage__grid {\n grid-template-columns: minmax(0, 1fr);\n }\n &[data-view=\"diff\"] .stage__pane:not([data-pane=\"diff\"]) {\n display: none;\n }\n &[data-view=\"flip\"][data-flip=\"baseline\"] .stage__pane:not([data-pane=\"baseline\"]),\n &[data-view=\"flip\"]:not([data-flip=\"baseline\"]) .stage__pane:not([data-pane=\"current\"]) {\n display: none;\n }\n &[data-view=\"swipe\"] .stage__pane[data-pane=\"diff\"],\n &[data-view=\"onion\"] .stage__pane[data-pane=\"diff\"] {\n display: none;\n }\n &[data-view=\"swipe\"] .stage__pane,\n &[data-view=\"onion\"] .stage__pane,\n &[data-view=\"swipe\"] .stage__divider {\n grid-area: 1 / 1;\n }\n &[data-view=\"swipe\"] .stage__label,\n &[data-view=\"onion\"] .stage__label {\n display: none;\n }\n &[data-view=\"swipe\"] .stage__pane[data-pane=\"current\"] {\n clip-path: inset(0 0 0 var(--swipe, 50%));\n background: transparent;\n }\n &[data-view=\"onion\"] .stage__pane[data-pane=\"current\"] {\n opacity: var(--onion, 0.5);\n background: transparent;\n }\n &[data-view=\"swipe\"] .stage__divider {\n position: relative;\n display: block;\n z-index: 3;\n justify-self: start;\n width: 2px;\n height: 100%;\n margin-left: var(--swipe, 50%);\n background: var(--accent);\n pointer-events: none;\n }\n /* The only part of the divider that takes pointer input: drag it to wipe. */\n .stage__knob {\n position: absolute;\n top: 50%;\n left: 50%;\n display: grid;\n place-items: center;\n width: 32px;\n height: 32px;\n transform: translate(-50%, -50%);\n background: var(--accent);\n color: var(--accent-contrast);\n border: 2px solid var(--surface-card);\n border-radius: var(--radius-pill);\n box-shadow: var(--shadow-2);\n font-size: var(--text-lg);\n line-height: 1;\n cursor: ew-resize;\n pointer-events: auto;\n touch-action: none;\n user-select: none;\n }\n &[data-view=\"flip\"] .stage__grid {\n cursor: pointer;\n }\n &[data-view=\"swipe\"] .stage__controls[data-for=\"swipe\"],\n &[data-view=\"onion\"] .stage__controls[data-for=\"onion\"] {\n display: flex;\n }\n`;\n\nconst MODES = [\n { label: \"Side by side\", value: \"split\" },\n { label: \"Swipe\", value: \"swipe\" },\n { label: \"Onion skin\", value: \"onion\" },\n { label: \"Diff\", value: \"diff\" },\n { label: \"Flip\", value: \"flip\" },\n];\n\nconst ZOOMS = [\n { label: \"Fit\", value: \"fit\" },\n { label: \"100%\", value: \"100\" },\n { label: \"200%\", value: \"200\" },\n { label: \"400%\", value: \"400\" },\n];\n\nfunction withActive(\n items: { label: string; value: string }[],\n active: string,\n): { label: string; value: string; active: boolean }[] {\n return items.map((item) => ({ ...item, active: item.value === active }));\n}\n\ninterface PaneProps {\n kind: \"baseline\" | \"current\" | \"diff\";\n label: string;\n src: string | null;\n alt: string;\n empty: unknown;\n meta?: string;\n}\n\nconst Pane: FC<PaneProps> = ({ kind, label, src, alt, empty, meta }) => {\n return (\n <div class=\"stage__pane\" data-pane={kind}>\n <div class=\"stage__label\">\n <span>{label}</span>\n {meta ? <span>{meta}</span> : null}\n </div>\n {src ? (\n <img class=\"stage__img\" src={src} alt={alt} loading=\"lazy\" decoding=\"async\" />\n ) : (\n <div class=\"stage__empty\">{empty}</div>\n )}\n </div>\n );\n};\n\n/** Swipe and onion-skin sliders; each is shown only in its own mode (CSS). */\nconst RangeControls: FC = () => {\n return (\n <>\n <label class=\"stage__controls\" data-for=\"swipe\">\n <span>Baseline</span>\n <input type=\"range\" min=\"0\" max=\"100\" value=\"50\" data-compare-swipe aria-label=\"Swipe\" />\n <span>Current</span>\n </label>\n <label class=\"stage__controls\" data-for=\"onion\">\n <span>Baseline</span>\n <input type=\"range\" min=\"0\" max=\"100\" value=\"50\" data-compare-onion aria-label=\"Opacity\" />\n <span>Current</span>\n </label>\n </>\n );\n};\n\n/** Props for the screenshot comparison surface. */\nexport interface CompareStageProps {\n /** Baseline image URL, or null when the story has no baseline yet. */\n baselineSrc: string | null;\n currentSrc: string;\n /** Diff overlay URL, or null when none was produced. */\n diffSrc: string | null;\n /** Accessible description shared by the images, e.g. \"Button / Primary\". */\n subject: string;\n baselineEmpty?: unknown;\n diffEmpty?: unknown;\n diffMeta?: string;\n}\n\n/** Baseline / current / diff comparison with swipe, onion-skin, flip, and zoom. */\nexport const CompareStage: FC<CompareStageProps> = ({\n baselineSrc,\n currentSrc,\n diffSrc,\n subject,\n baselineEmpty = \"New story — no baseline yet. Approve to set the baseline.\",\n diffEmpty = \"No diff\",\n diffMeta,\n}) => {\n return (\n <div class={stageRoot} data-compare data-view=\"split\" data-zoom=\"fit\" data-flip=\"current\">\n <div class=\"stage__bar\">\n <Segmented label=\"Comparison mode\" data-compare-mode items={withActive(MODES, \"split\")} />\n <Segmented label=\"Zoom\" data-compare-zoom items={withActive(ZOOMS, \"fit\")} />\n </div>\n <div class=\"stage__grid\">\n <Pane\n kind=\"baseline\"\n label=\"Baseline\"\n src={baselineSrc}\n alt={`Baseline for ${subject}`}\n empty={baselineEmpty}\n />\n <Pane\n kind=\"current\"\n label=\"Current\"\n src={currentSrc}\n alt={`Current for ${subject}`}\n empty=\"No screenshot\"\n />\n <Pane\n kind=\"diff\"\n label=\"Diff\"\n src={diffSrc}\n alt={`Diff for ${subject}`}\n empty={diffEmpty}\n meta={diffMeta}\n />\n <span class=\"stage__divider\" aria-hidden=\"true\">\n <span class=\"stage__knob\" data-compare-knob>\n ↔\n </span>\n </span>\n </div>\n <RangeControls />\n </div>\n );\n};\n"],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { getStore } from "../store.mjs";
5
+ import { getCsrfToken } from "../middleware/csrf.mjs";
6
+ import { jsx } from "hono/jsx/jsx-runtime";
7
+ //#region src/ui/csrf-field.tsx
8
+ /** Hidden CSRF token so native (non-HTMX) form posts pass the csrf middleware. */
9
+ function csrfField() {
10
+ const { config, sessionId } = getStore();
11
+ return /* @__PURE__ */ jsx("input", {
12
+ type: "hidden",
13
+ name: "csrf_token",
14
+ value: getCsrfToken(config.secret, sessionId)
15
+ });
16
+ }
17
+ //#endregion
18
+ export { csrfField };
19
+
20
+ //# sourceMappingURL=csrf-field.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csrf-field.mjs","names":[],"sources":["../../src/ui/csrf-field.tsx"],"sourcesContent":["import { getCsrfToken } from \"../middleware/csrf.ts\";\nimport { getStore } from \"../store.ts\";\n\n/** Hidden CSRF token so native (non-HTMX) form posts pass the csrf middleware. */\nexport function csrfField(): unknown {\n const { config, sessionId } = getStore();\n return <input type=\"hidden\" name=\"csrf_token\" value={getCsrfToken(config.secret, sessionId)} />;\n}\n"],"mappings":";;;;;;;;AAIA,SAAgB,YAAqB;CACnC,MAAM,EAAE,QAAQ,cAAc,SAAS;CACvC,OAAO,oBAAC,SAAD;EAAO,MAAK;EAAS,MAAK;EAAa,OAAO,aAAa,OAAO,QAAQ,SAAS;CAAI,CAAA;AAChG"}