@storyshelf/core 0.1.2 → 0.1.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 (170) hide show
  1. package/dist/adapter/auth.mjs.map +1 -1
  2. package/dist/adapter/git-host/helpers.d.mts +1 -0
  3. package/dist/adapter/git-host/helpers.mjs +1 -0
  4. package/dist/adapter/git-host/helpers.mjs.map +1 -1
  5. package/dist/adapters/capture-queue.d.mts +2 -0
  6. package/dist/adapters/webhook-events.mjs +8 -0
  7. package/dist/adapters/webhook-events.mjs.map +1 -1
  8. package/dist/capture/adapter.d.mts +2 -0
  9. package/dist/capture/adapter.mjs +3 -0
  10. package/dist/capture/adapter.mjs.map +1 -1
  11. package/dist/capture/orchestrator.d.mts +7 -0
  12. package/dist/capture/orchestrator.mjs +6 -0
  13. package/dist/capture/orchestrator.mjs.map +1 -1
  14. package/dist/capture/queue.d.mts +2 -0
  15. package/dist/capture/queue.mjs +1 -0
  16. package/dist/capture/queue.mjs.map +1 -1
  17. package/dist/capture/status-fanout.mjs +1 -0
  18. package/dist/capture/status-fanout.mjs.map +1 -1
  19. package/dist/capture/storybook.d.mts +1 -0
  20. package/dist/capture/storybook.mjs +1 -0
  21. package/dist/capture/storybook.mjs.map +1 -1
  22. package/dist/config.d.mts +27 -20
  23. package/dist/config.mjs +14 -0
  24. package/dist/config.mjs.map +1 -1
  25. package/dist/ddl.d.mts +1 -0
  26. package/dist/ddl.mjs +1 -0
  27. package/dist/ddl.mjs.map +1 -1
  28. package/dist/diff/engine.d.mts +8 -0
  29. package/dist/diff/engine.mjs +8 -0
  30. package/dist/diff/engine.mjs.map +1 -1
  31. package/dist/index.d.mts +3 -0
  32. package/dist/index.mjs +6 -1
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/logger.mjs.map +1 -1
  35. package/dist/middleware/csrf.mjs +1 -0
  36. package/dist/middleware/csrf.mjs.map +1 -1
  37. package/dist/middleware/rate-limit.mjs +1 -0
  38. package/dist/middleware/rate-limit.mjs.map +1 -1
  39. package/dist/models/baseline.d.mts +1 -0
  40. package/dist/models/baseline.mjs +3 -1
  41. package/dist/models/baseline.mjs.map +1 -1
  42. package/dist/models/build.d.mts +3 -0
  43. package/dist/models/build.mjs +3 -1
  44. package/dist/models/build.mjs.map +1 -1
  45. package/dist/models/comment.d.mts +2 -0
  46. package/dist/models/comment.mjs +3 -1
  47. package/dist/models/comment.mjs.map +1 -1
  48. package/dist/models/label.mjs +2 -1
  49. package/dist/models/label.mjs.map +1 -1
  50. package/dist/models/member.mjs +2 -1
  51. package/dist/models/member.mjs.map +1 -1
  52. package/dist/models/project.d.mts +1 -0
  53. package/dist/models/project.mjs +2 -1
  54. package/dist/models/project.mjs.map +1 -1
  55. package/dist/models/snapshot.d.mts +1 -0
  56. package/dist/models/snapshot.mjs +2 -1
  57. package/dist/models/snapshot.mjs.map +1 -1
  58. package/dist/models/status-config.mjs +2 -1
  59. package/dist/models/status-config.mjs.map +1 -1
  60. package/dist/models/token.mjs +2 -1
  61. package/dist/models/token.mjs.map +1 -1
  62. package/dist/models/webhook.d.mts +1 -0
  63. package/dist/models/webhook.mjs +2 -1
  64. package/dist/models/webhook.mjs.map +1 -1
  65. package/dist/pages/build-detail.mjs +1 -0
  66. package/dist/pages/build-detail.mjs.map +1 -1
  67. package/dist/pages/build-diff.mjs +1 -0
  68. package/dist/pages/build-diff.mjs.map +1 -1
  69. package/dist/pages/compute-jobs.mjs +2 -0
  70. package/dist/pages/compute-jobs.mjs.map +1 -1
  71. package/dist/pages/login.mjs +1 -0
  72. package/dist/pages/login.mjs.map +1 -1
  73. package/dist/pages/project-builds.mjs +1 -0
  74. package/dist/pages/project-builds.mjs.map +1 -1
  75. package/dist/pages/project-create.mjs +1 -0
  76. package/dist/pages/project-create.mjs.map +1 -1
  77. package/dist/pages/project-settings.mjs +1 -0
  78. package/dist/pages/project-settings.mjs.map +1 -1
  79. package/dist/pages/projects.mjs +1 -0
  80. package/dist/pages/projects.mjs.map +1 -1
  81. package/dist/pages/root.mjs +1 -0
  82. package/dist/pages/root.mjs.map +1 -1
  83. package/dist/pages/settings-general.mjs +1 -0
  84. package/dist/pages/settings-general.mjs.map +1 -1
  85. package/dist/pages/settings-labels.mjs +1 -0
  86. package/dist/pages/settings-labels.mjs.map +1 -1
  87. package/dist/pages/settings-members.mjs +1 -0
  88. package/dist/pages/settings-members.mjs.map +1 -1
  89. package/dist/pages/settings-status.mjs +1 -0
  90. package/dist/pages/settings-status.mjs.map +1 -1
  91. package/dist/pages/settings-tests.mjs +1 -0
  92. package/dist/pages/settings-tests.mjs.map +1 -1
  93. package/dist/pages/settings-tokens.mjs +1 -0
  94. package/dist/pages/settings-tokens.mjs.map +1 -1
  95. package/dist/pages/settings-webhooks.mjs +1 -0
  96. package/dist/pages/settings-webhooks.mjs.map +1 -1
  97. package/dist/retention/purge.d.mts +3 -0
  98. package/dist/retention/purge.mjs +2 -1
  99. package/dist/retention/purge.mjs.map +1 -1
  100. package/dist/routers/admin.mjs +1 -0
  101. package/dist/routers/admin.mjs.map +1 -1
  102. package/dist/routers/assets.mjs +1 -0
  103. package/dist/routers/assets.mjs.map +1 -1
  104. package/dist/routers/auth.mjs +1 -0
  105. package/dist/routers/auth.mjs.map +1 -1
  106. package/dist/routers/builds.handlers.mjs +10 -0
  107. package/dist/routers/builds.handlers.mjs.map +1 -1
  108. package/dist/routers/builds.mjs +1 -0
  109. package/dist/routers/builds.mjs.map +1 -1
  110. package/dist/routers/comments.mjs +1 -0
  111. package/dist/routers/comments.mjs.map +1 -1
  112. package/dist/routers/helpers.mjs +7 -1
  113. package/dist/routers/helpers.mjs.map +1 -1
  114. package/dist/routers/htmx.mjs +2 -0
  115. package/dist/routers/htmx.mjs.map +1 -1
  116. package/dist/routers/labels.mjs +1 -0
  117. package/dist/routers/labels.mjs.map +1 -1
  118. package/dist/routers/media.mjs +1 -0
  119. package/dist/routers/media.mjs.map +1 -1
  120. package/dist/routers/members.mjs +1 -0
  121. package/dist/routers/members.mjs.map +1 -1
  122. package/dist/routers/projects.mjs +1 -0
  123. package/dist/routers/projects.mjs.map +1 -1
  124. package/dist/routers/schemas.mjs +25 -0
  125. package/dist/routers/schemas.mjs.map +1 -1
  126. package/dist/routers/settings.mjs +2 -0
  127. package/dist/routers/settings.mjs.map +1 -1
  128. package/dist/routers/snapshots.mjs +1 -0
  129. package/dist/routers/snapshots.mjs.map +1 -1
  130. package/dist/routers/status-configs.mjs +1 -0
  131. package/dist/routers/status-configs.mjs.map +1 -1
  132. package/dist/routers/storybook.mjs +1 -0
  133. package/dist/routers/storybook.mjs.map +1 -1
  134. package/dist/routers/tokens.mjs +1 -0
  135. package/dist/routers/tokens.mjs.map +1 -1
  136. package/dist/routers/ui.mjs +1 -0
  137. package/dist/routers/ui.mjs.map +1 -1
  138. package/dist/routers/webhooks.mjs +1 -0
  139. package/dist/routers/webhooks.mjs.map +1 -1
  140. package/dist/schema-tables.mjs +166 -0
  141. package/dist/schema-tables.mjs.map +1 -0
  142. package/dist/schema.d.mts +194 -4420
  143. package/dist/schema.mjs +26 -138
  144. package/dist/schema.mjs.map +1 -1
  145. package/dist/store.mjs +2 -0
  146. package/dist/store.mjs.map +1 -1
  147. package/dist/types.d.mts +12 -0
  148. package/dist/types.mjs +8 -0
  149. package/dist/types.mjs.map +1 -1
  150. package/dist/ui/components.mjs +5 -0
  151. package/dist/ui/components.mjs.map +1 -1
  152. package/dist/ui/document.d.mts +1 -0
  153. package/dist/ui/document.mjs +1 -0
  154. package/dist/ui/document.mjs.map +1 -1
  155. package/dist/ui/styles.mjs +1 -0
  156. package/dist/ui/styles.mjs.map +1 -1
  157. package/dist/ui/theme.mjs +2 -0
  158. package/dist/ui/theme.mjs.map +1 -1
  159. package/dist/urls.d.mts +8 -0
  160. package/dist/urls.mjs +7 -0
  161. package/dist/urls.mjs.map +1 -1
  162. package/dist/utils/hash.mjs +3 -0
  163. package/dist/utils/hash.mjs.map +1 -1
  164. package/dist/utils/paths.d.mts +5 -0
  165. package/dist/utils/paths.mjs +5 -0
  166. package/dist/utils/paths.mjs.map +1 -1
  167. package/dist/utils/ulid.d.mts +2 -0
  168. package/dist/utils/ulid.mjs +2 -0
  169. package/dist/utils/ulid.mjs.map +1 -1
  170. package/package.json +1 -1
package/dist/schema.mjs CHANGED
@@ -1,145 +1,33 @@
1
1
  import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
- import { index, integer, real, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
4
+ import { baselines as baselines$1, buildLabels as buildLabels$1, builds as builds$1, comments as comments$1, labelTypes as labelTypes$1, projectMembers as projectMembers$1, projectStatusConfigs as projectStatusConfigs$1, projects as projects$1, snapshots as snapshots$1, tokens as tokens$1, users as users$1, webhooks as webhooks$1 } from "./schema-tables.mjs";
5
5
  //#region src/schema.ts
6
- const projects = sqliteTable("projects", {
7
- id: text("id").primaryKey(),
8
- name: text("name").notNull(),
9
- slug: text("slug").notNull(),
10
- gitRepository: text("git_repository"),
11
- gitDefaultBranch: text("git_default_branch").notNull().default("main"),
12
- pixelThreshold: real("pixel_threshold").notNull().default(.1),
13
- maxDiffRatio: real("max_diff_ratio").notNull().default(.01),
14
- publicBranchRegex: text("public_branch_regex"),
15
- storybookMeta: text("storybook_meta").$type().default(null),
16
- executePlay: integer("execute_play", { mode: "boolean" }).notNull().default(false),
17
- playTimeoutMs: integer("play_timeout_ms").notNull().default(1e4),
18
- createdAt: text("created_at").notNull(),
19
- updatedAt: text("updated_at").notNull()
20
- });
21
- const projectStatusConfigs = sqliteTable("project_status_configs", {
22
- id: text("id").primaryKey(),
23
- projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
24
- provider: text("provider").notNull(),
25
- config: text("config").notNull(),
26
- tokenEncrypted: text("token_encrypted").notNull(),
27
- createdAt: text("created_at").notNull(),
28
- updatedAt: text("updated_at").notNull()
29
- });
30
- const builds = sqliteTable("builds", {
31
- id: text("id").primaryKey(),
32
- projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
33
- gitSha: text("git_sha").notNull(),
34
- gitBranch: text("git_branch").notNull(),
35
- isDefault: integer("is_default", { mode: "boolean" }).notNull().default(false),
36
- authorEmail: text("author_email"),
37
- authorName: text("author_name"),
38
- message: text("message"),
39
- public: integer("public", { mode: "boolean" }).notNull().default(false),
40
- status: text("status").$type().notNull().default("pending"),
41
- snapshotCount: integer("snapshot_count").notNull().default(0),
42
- changedCount: integer("changed_count").notNull().default(0),
43
- approvedCount: integer("approved_count").notNull().default(0),
44
- rejectedCount: integer("rejected_count").notNull().default(0),
45
- createdAt: text("created_at").notNull(),
46
- updatedAt: text("updated_at").notNull()
47
- }, (t) => [uniqueIndex("builds_project_gitsha_idx").on(t.projectId, t.gitSha), index("builds_git_branch_idx").on(t.gitBranch)]);
48
- const snapshots = sqliteTable("snapshots", {
49
- id: text("id").primaryKey(),
50
- projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
51
- buildId: text("build_id").notNull().references(() => builds.id, { onDelete: "cascade" }),
52
- storyId: text("story_id").notNull(),
53
- storyName: text("story_name").notNull(),
54
- storyTitle: text("story_title").notNull(),
55
- storyImportPath: text("story_import_path"),
56
- viewportName: text("viewport_name").notNull().default("desktop"),
57
- viewportWidth: integer("viewport_width").notNull().default(1280),
58
- viewportHeight: integer("viewport_height").notNull().default(720),
59
- screenshotPath: text("screenshot_path").notNull(),
60
- diffPath: text("diff_path"),
61
- diffPixels: integer("diff_pixels"),
62
- diffRatio: real("diff_ratio"),
63
- diffPassed: integer("diff_passed", { mode: "boolean" }),
64
- status: text("status").$type().notNull().default("pending"),
65
- reviewedBy: text("reviewed_by"),
66
- reviewedAt: text("reviewed_at"),
67
- createdAt: text("created_at").notNull(),
68
- updatedAt: text("updated_at").notNull()
69
- }, (t) => [uniqueIndex("snapshots_build_story_viewport_idx").on(t.buildId, t.storyId, t.viewportName), index("snapshots_build_id_idx").on(t.buildId)]);
70
- const baselines = sqliteTable("baselines", {
71
- id: text("id").primaryKey(),
72
- projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
73
- storyId: text("story_id").notNull(),
74
- viewportName: text("viewport_name").notNull().default("desktop"),
75
- branch: text("branch").notNull(),
76
- snapshotId: text("snapshot_id"),
77
- screenshotPath: text("screenshot_path").notNull(),
78
- createdAt: text("created_at").notNull(),
79
- updatedAt: text("updated_at").notNull()
80
- }, (t) => [uniqueIndex("baselines_project_story_viewport_branch_idx").on(t.projectId, t.storyId, t.viewportName, t.branch), index("baselines_project_story_idx").on(t.projectId, t.storyId)]);
81
- const comments = sqliteTable("comments", {
82
- id: text("id").primaryKey(),
83
- projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
84
- buildId: text("build_id").notNull().references(() => builds.id, { onDelete: "cascade" }),
85
- snapshotId: text("snapshot_id").references(() => snapshots.id, { onDelete: "cascade" }),
86
- userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
87
- body: text("body").notNull(),
88
- parentId: text("parent_id"),
89
- resolved: integer("resolved", { mode: "boolean" }).notNull().default(false),
90
- createdAt: text("created_at").notNull(),
91
- updatedAt: text("updated_at").notNull()
92
- }, (t) => [index("comments_build_id_idx").on(t.buildId)]);
93
- const labelTypes = sqliteTable("label_types", {
94
- id: text("id").primaryKey(),
95
- projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
96
- key: text("key").notNull(),
97
- name: text("name").notNull(),
98
- linkTemplate: text("link_template"),
99
- color: text("color"),
100
- createdAt: text("created_at").notNull()
101
- }, (t) => [uniqueIndex("label_types_project_key_idx").on(t.projectId, t.key)]);
102
- const buildLabels = sqliteTable("build_labels", {
103
- id: text("id").primaryKey(),
104
- projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
105
- buildId: text("build_id").notNull().references(() => builds.id, { onDelete: "cascade" }),
106
- typeKey: text("type_key").notNull(),
107
- value: text("value").notNull(),
108
- createdAt: text("created_at").notNull()
109
- }, (t) => [uniqueIndex("build_labels_build_type_value_idx").on(t.buildId, t.typeKey, t.value)]);
110
- const tokens = sqliteTable("tokens", {
111
- id: text("id").primaryKey(),
112
- projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
113
- name: text("name").notNull(),
114
- hash: text("hash").notNull(),
115
- lastUsedAt: text("last_used_at"),
116
- createdAt: text("created_at").notNull()
117
- });
118
- const webhooks = sqliteTable("webhooks", {
119
- id: text("id").primaryKey(),
120
- projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
121
- url: text("url").notNull(),
122
- secret: text("secret").notNull(),
123
- events: text("events"),
124
- createdAt: text("created_at").notNull(),
125
- updatedAt: text("updated_at").notNull()
126
- });
127
- const users = sqliteTable("users", {
128
- id: text("id").primaryKey(),
129
- email: text("email").notNull(),
130
- name: text("name").notNull(),
131
- avatarUrl: text("avatar_url"),
132
- role: text("role").$type().notNull().default("member"),
133
- lastLoginAt: text("last_login_at"),
134
- createdAt: text("created_at").notNull()
135
- });
136
- const projectMembers = sqliteTable("project_members", {
137
- id: text("id").primaryKey(),
138
- projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
139
- userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
140
- role: text("role").$type().notNull().default("viewer"),
141
- createdAt: text("created_at").notNull()
142
- }, (t) => [uniqueIndex("project_members_project_user_idx").on(t.projectId, t.userId)]);
6
+ /** Projects table (one row per Storybook project). */
7
+ const projects = projects$1;
8
+ /** Project status-configs table (merge-gate configurations). */
9
+ const projectStatusConfigs = projectStatusConfigs$1;
10
+ /** Builds table (one row per uploaded Storybook build). */
11
+ const builds = builds$1;
12
+ /** Snapshots table (one row per captured story screenshot). */
13
+ const snapshots = snapshots$1;
14
+ /** Baselines table (accepted reference snapshots per branch). */
15
+ const baselines = baselines$1;
16
+ /** Comments table (review comments on snapshots). */
17
+ const comments = comments$1;
18
+ /** Label-types table (project-defined build label vocabularies). */
19
+ const labelTypes = labelTypes$1;
20
+ /** Build-labels table (labels attached to builds). */
21
+ const buildLabels = buildLabels$1;
22
+ /** Tokens table (CI token hashes per project). */
23
+ const tokens = tokens$1;
24
+ /** Webhooks table (outgoing build-event subscriptions). */
25
+ const webhooks = webhooks$1;
26
+ /** Users table (dashboard members). */
27
+ const users = users$1;
28
+ /** Project-members table (per-project memberships and roles). */
29
+ const projectMembers = projectMembers$1;
30
+ /** Full Drizzle schema object passed to the database client. */
143
31
  const schema = {
144
32
  projects,
145
33
  projectStatusConfigs,
@@ -1 +1 @@
1
- {"version":3,"file":"schema.mjs","names":[],"sources":["../src/schema.ts"],"sourcesContent":["import { index, integer, real, sqliteTable, text, uniqueIndex } from \"drizzle-orm/sqlite-core\";\n\nimport type { BuildStatus, ProjectRole, SiteRole, SnapshotStatus } from \"./types.ts\";\n\nexport interface StorybookMeta {\n framework?: { name?: string; options?: unknown };\n addons?: string[];\n storiesGlobs?: string[];\n staticDirs?: string[];\n packagePath?: string;\n previewParameters?: Record<string, unknown>;\n}\n\nexport const projects = sqliteTable(\"projects\", {\n id: text(\"id\").primaryKey(),\n name: text(\"name\").notNull(),\n slug: text(\"slug\").notNull(),\n gitRepository: text(\"git_repository\"),\n gitDefaultBranch: text(\"git_default_branch\").notNull().default(\"main\"),\n pixelThreshold: real(\"pixel_threshold\").notNull().default(0.1),\n maxDiffRatio: real(\"max_diff_ratio\").notNull().default(0.01),\n publicBranchRegex: text(\"public_branch_regex\"),\n storybookMeta: text(\"storybook_meta\").$type<string | null | undefined>().default(null),\n executePlay: integer(\"execute_play\", { mode: \"boolean\" }).notNull().default(false),\n playTimeoutMs: integer(\"play_timeout_ms\").notNull().default(10_000),\n createdAt: text(\"created_at\").notNull(),\n updatedAt: text(\"updated_at\").notNull(),\n});\n\nexport const projectStatusConfigs = sqliteTable(\"project_status_configs\", {\n id: text(\"id\").primaryKey(),\n projectId: text(\"project_id\")\n .notNull()\n .references(() => projects.id, { onDelete: \"cascade\" }),\n provider: text(\"provider\").notNull(),\n config: text(\"config\").notNull(),\n tokenEncrypted: text(\"token_encrypted\").notNull(),\n createdAt: text(\"created_at\").notNull(),\n updatedAt: text(\"updated_at\").notNull(),\n});\n\nexport const builds = sqliteTable(\n \"builds\",\n {\n id: text(\"id\").primaryKey(),\n projectId: text(\"project_id\")\n .notNull()\n .references(() => projects.id, { onDelete: \"cascade\" }),\n gitSha: text(\"git_sha\").notNull(),\n gitBranch: text(\"git_branch\").notNull(),\n isDefault: integer(\"is_default\", { mode: \"boolean\" }).notNull().default(false),\n authorEmail: text(\"author_email\"),\n authorName: text(\"author_name\"),\n message: text(\"message\"),\n public: integer(\"public\", { mode: \"boolean\" }).notNull().default(false),\n status: text(\"status\").$type<BuildStatus>().notNull().default(\"pending\"),\n snapshotCount: integer(\"snapshot_count\").notNull().default(0),\n changedCount: integer(\"changed_count\").notNull().default(0),\n approvedCount: integer(\"approved_count\").notNull().default(0),\n rejectedCount: integer(\"rejected_count\").notNull().default(0),\n createdAt: text(\"created_at\").notNull(),\n updatedAt: text(\"updated_at\").notNull(),\n },\n (t) => [\n uniqueIndex(\"builds_project_gitsha_idx\").on(t.projectId, t.gitSha),\n index(\"builds_git_branch_idx\").on(t.gitBranch),\n ],\n);\n\nexport const snapshots = sqliteTable(\n \"snapshots\",\n {\n id: text(\"id\").primaryKey(),\n projectId: text(\"project_id\")\n .notNull()\n .references(() => projects.id, { onDelete: \"cascade\" }),\n buildId: text(\"build_id\")\n .notNull()\n .references(() => builds.id, { onDelete: \"cascade\" }),\n storyId: text(\"story_id\").notNull(),\n storyName: text(\"story_name\").notNull(),\n storyTitle: text(\"story_title\").notNull(),\n storyImportPath: text(\"story_import_path\"),\n viewportName: text(\"viewport_name\").notNull().default(\"desktop\"),\n viewportWidth: integer(\"viewport_width\").notNull().default(1280),\n viewportHeight: integer(\"viewport_height\").notNull().default(720),\n screenshotPath: text(\"screenshot_path\").notNull(),\n diffPath: text(\"diff_path\"),\n diffPixels: integer(\"diff_pixels\"),\n diffRatio: real(\"diff_ratio\"),\n diffPassed: integer(\"diff_passed\", { mode: \"boolean\" }),\n status: text(\"status\").$type<SnapshotStatus>().notNull().default(\"pending\"),\n reviewedBy: text(\"reviewed_by\"),\n reviewedAt: text(\"reviewed_at\"),\n createdAt: text(\"created_at\").notNull(),\n updatedAt: text(\"updated_at\").notNull(),\n },\n (t) => [\n uniqueIndex(\"snapshots_build_story_viewport_idx\").on(t.buildId, t.storyId, t.viewportName),\n index(\"snapshots_build_id_idx\").on(t.buildId),\n ],\n);\n\nexport const baselines = sqliteTable(\n \"baselines\",\n {\n id: text(\"id\").primaryKey(),\n projectId: text(\"project_id\")\n .notNull()\n .references(() => projects.id, { onDelete: \"cascade\" }),\n storyId: text(\"story_id\").notNull(),\n viewportName: text(\"viewport_name\").notNull().default(\"desktop\"),\n branch: text(\"branch\").notNull(),\n snapshotId: text(\"snapshot_id\"),\n screenshotPath: text(\"screenshot_path\").notNull(),\n createdAt: text(\"created_at\").notNull(),\n updatedAt: text(\"updated_at\").notNull(),\n },\n (t) => [\n uniqueIndex(\"baselines_project_story_viewport_branch_idx\").on(t.projectId, t.storyId, t.viewportName, t.branch),\n index(\"baselines_project_story_idx\").on(t.projectId, t.storyId),\n ],\n);\n\nexport const comments = sqliteTable(\n \"comments\",\n {\n id: text(\"id\").primaryKey(),\n projectId: text(\"project_id\")\n .notNull()\n .references(() => projects.id, { onDelete: \"cascade\" }),\n buildId: text(\"build_id\")\n .notNull()\n .references(() => builds.id, { onDelete: \"cascade\" }),\n snapshotId: text(\"snapshot_id\").references(() => snapshots.id, { onDelete: \"cascade\" }),\n userId: text(\"user_id\")\n .notNull()\n .references(() => users.id, { onDelete: \"cascade\" }),\n body: text(\"body\").notNull(),\n parentId: text(\"parent_id\"),\n resolved: integer(\"resolved\", { mode: \"boolean\" }).notNull().default(false),\n createdAt: text(\"created_at\").notNull(),\n updatedAt: text(\"updated_at\").notNull(),\n },\n (t) => [index(\"comments_build_id_idx\").on(t.buildId)],\n);\n\nexport const labelTypes = sqliteTable(\n \"label_types\",\n {\n id: text(\"id\").primaryKey(),\n projectId: text(\"project_id\")\n .notNull()\n .references(() => projects.id, { onDelete: \"cascade\" }),\n key: text(\"key\").notNull(),\n name: text(\"name\").notNull(),\n linkTemplate: text(\"link_template\"),\n color: text(\"color\"),\n createdAt: text(\"created_at\").notNull(),\n },\n (t) => [uniqueIndex(\"label_types_project_key_idx\").on(t.projectId, t.key)],\n);\n\nexport const buildLabels = sqliteTable(\n \"build_labels\",\n {\n id: text(\"id\").primaryKey(),\n projectId: text(\"project_id\")\n .notNull()\n .references(() => projects.id, { onDelete: \"cascade\" }),\n buildId: text(\"build_id\")\n .notNull()\n .references(() => builds.id, { onDelete: \"cascade\" }),\n typeKey: text(\"type_key\").notNull(),\n value: text(\"value\").notNull(),\n createdAt: text(\"created_at\").notNull(),\n },\n (t) => [uniqueIndex(\"build_labels_build_type_value_idx\").on(t.buildId, t.typeKey, t.value)],\n);\n\nexport const tokens = sqliteTable(\"tokens\", {\n id: text(\"id\").primaryKey(),\n projectId: text(\"project_id\")\n .notNull()\n .references(() => projects.id, { onDelete: \"cascade\" }),\n name: text(\"name\").notNull(),\n hash: text(\"hash\").notNull(),\n lastUsedAt: text(\"last_used_at\"),\n createdAt: text(\"created_at\").notNull(),\n});\n\nexport const webhooks = sqliteTable(\"webhooks\", {\n id: text(\"id\").primaryKey(),\n projectId: text(\"project_id\")\n .notNull()\n .references(() => projects.id, { onDelete: \"cascade\" }),\n url: text(\"url\").notNull(),\n secret: text(\"secret\").notNull(),\n events: text(\"events\"),\n createdAt: text(\"created_at\").notNull(),\n updatedAt: text(\"updated_at\").notNull(),\n});\n\nexport const users = sqliteTable(\"users\", {\n id: text(\"id\").primaryKey(),\n email: text(\"email\").notNull(),\n name: text(\"name\").notNull(),\n avatarUrl: text(\"avatar_url\"),\n role: text(\"role\").$type<SiteRole>().notNull().default(\"member\"),\n lastLoginAt: text(\"last_login_at\"),\n createdAt: text(\"created_at\").notNull(),\n});\n\nexport const projectMembers = sqliteTable(\n \"project_members\",\n {\n id: text(\"id\").primaryKey(),\n projectId: text(\"project_id\")\n .notNull()\n .references(() => projects.id, { onDelete: \"cascade\" }),\n userId: text(\"user_id\")\n .notNull()\n .references(() => users.id, { onDelete: \"cascade\" }),\n role: text(\"role\").$type<ProjectRole>().notNull().default(\"viewer\"),\n createdAt: text(\"created_at\").notNull(),\n },\n (t) => [uniqueIndex(\"project_members_project_user_idx\").on(t.projectId, t.userId)],\n);\n\nexport const schema = {\n projects,\n projectStatusConfigs,\n builds,\n snapshots,\n baselines,\n comments,\n labelTypes,\n buildLabels,\n tokens,\n webhooks,\n users,\n projectMembers,\n};\n\nexport type Schema = typeof schema;\n\nexport type Project = typeof projects.$inferSelect;\nexport type ProjectStatusConfig = typeof projectStatusConfigs.$inferSelect;\nexport type Build = typeof builds.$inferSelect;\nexport type Snapshot = typeof snapshots.$inferSelect;\nexport type Baseline = typeof baselines.$inferSelect;\nexport type Comment = typeof comments.$inferSelect;\nexport type LabelType = typeof labelTypes.$inferSelect;\nexport type BuildLabel = typeof buildLabels.$inferSelect;\nexport type Token = typeof tokens.$inferSelect;\nexport type Webhook = typeof webhooks.$inferSelect;\nexport type User = typeof users.$inferSelect;\nexport type ProjectMember = typeof projectMembers.$inferSelect;\n"],"mappings":"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"}
1
+ {"version":3,"file":"schema.mjs","names":["projectsTable","projectStatusConfigsTable","buildsTable","snapshotsTable","baselinesTable","commentsTable","labelTypesTable","buildLabelsTable","tokensTable","webhooksTable","usersTable","projectMembersTable"],"sources":["../src/schema.ts"],"sourcesContent":["import type { AnySQLiteTable } from \"drizzle-orm/sqlite-core\";\n\nimport {\n baselines as baselinesTable,\n buildLabels as buildLabelsTable,\n builds as buildsTable,\n comments as commentsTable,\n labelTypes as labelTypesTable,\n projectMembers as projectMembersTable,\n projects as projectsTable,\n projectStatusConfigs as projectStatusConfigsTable,\n snapshots as snapshotsTable,\n tokens as tokensTable,\n users as usersTable,\n webhooks as webhooksTable,\n} from \"./schema-tables.ts\";\nimport type { BuildStatus, ProjectRole, SiteRole, SnapshotStatus } from \"./types.ts\";\n\n/**\n * Database schema: table handles, row types, and the schema object passed to\n * the Drizzle client.\n */\n\n/** Storybook metadata synced from a project's published Storybook. */\nexport interface StorybookMeta {\n framework?: { name?: string; options?: unknown };\n addons?: string[];\n storiesGlobs?: string[];\n staticDirs?: string[];\n packagePath?: string;\n previewParameters?: Record<string, unknown>;\n}\n\n/** Projects table (one row per Storybook project). */\nexport const projects: AnySQLiteTable = projectsTable;\n/** Project status-configs table (merge-gate configurations). */\nexport const projectStatusConfigs: AnySQLiteTable = projectStatusConfigsTable;\n/** Builds table (one row per uploaded Storybook build). */\nexport const builds: AnySQLiteTable = buildsTable;\n/** Snapshots table (one row per captured story screenshot). */\nexport const snapshots: AnySQLiteTable = snapshotsTable;\n/** Baselines table (accepted reference snapshots per branch). */\nexport const baselines: AnySQLiteTable = baselinesTable;\n/** Comments table (review comments on snapshots). */\nexport const comments: AnySQLiteTable = commentsTable;\n/** Label-types table (project-defined build label vocabularies). */\nexport const labelTypes: AnySQLiteTable = labelTypesTable;\n/** Build-labels table (labels attached to builds). */\nexport const buildLabels: AnySQLiteTable = buildLabelsTable;\n/** Tokens table (CI token hashes per project). */\nexport const tokens: AnySQLiteTable = tokensTable;\n/** Webhooks table (outgoing build-event subscriptions). */\nexport const webhooks: AnySQLiteTable = webhooksTable;\n/** Users table (dashboard members). */\nexport const users: AnySQLiteTable = usersTable;\n/** Project-members table (per-project memberships and roles). */\nexport const projectMembers: AnySQLiteTable = projectMembersTable;\n\n/** Full Drizzle schema object passed to the database client. */\nexport const schema = {\n projects,\n projectStatusConfigs,\n builds,\n snapshots,\n baselines,\n comments,\n labelTypes,\n buildLabels,\n tokens,\n webhooks,\n users,\n projectMembers,\n};\n\n/** The full database schema type. */\nexport type Schema = typeof schema;\n\n/** A project row. */\nexport interface Project {\n id: string;\n name: string;\n slug: string;\n gitRepository: string | null;\n gitDefaultBranch: string;\n pixelThreshold: number;\n maxDiffRatio: number;\n publicBranchRegex: string | null;\n storybookMeta: string | null | undefined;\n executePlay: boolean;\n playTimeoutMs: number;\n createdAt: string;\n updatedAt: string;\n}\n\n/** A project status-config row. */\nexport interface ProjectStatusConfig {\n id: string;\n projectId: string;\n provider: string;\n config: string;\n tokenEncrypted: string;\n createdAt: string;\n updatedAt: string;\n}\n\n/** A build row. */\nexport interface Build {\n id: string;\n projectId: string;\n gitSha: string;\n gitBranch: string;\n isDefault: boolean;\n authorEmail: string | null;\n authorName: string | null;\n message: string | null;\n public: boolean;\n status: BuildStatus;\n snapshotCount: number;\n changedCount: number;\n approvedCount: number;\n rejectedCount: number;\n createdAt: string;\n updatedAt: string;\n}\n\n/** A snapshot row. */\nexport interface Snapshot {\n id: string;\n projectId: string;\n buildId: string;\n storyId: string;\n storyName: string;\n storyTitle: string;\n storyImportPath: string | null;\n viewportName: string;\n viewportWidth: number;\n viewportHeight: number;\n screenshotPath: string;\n diffPath: string | null;\n diffPixels: number | null;\n diffRatio: number | null;\n diffPassed: boolean | null;\n status: SnapshotStatus;\n reviewedBy: string | null;\n reviewedAt: string | null;\n createdAt: string;\n updatedAt: string;\n}\n\n/** A baseline row. */\nexport interface Baseline {\n id: string;\n projectId: string;\n storyId: string;\n viewportName: string;\n branch: string;\n snapshotId: string | null;\n screenshotPath: string;\n createdAt: string;\n updatedAt: string;\n}\n\n/** A comment row. */\nexport interface Comment {\n id: string;\n projectId: string;\n buildId: string;\n snapshotId: string | null;\n userId: string;\n body: string;\n parentId: string | null;\n resolved: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\n/** A label-type row. */\nexport interface LabelType {\n id: string;\n projectId: string;\n key: string;\n name: string;\n linkTemplate: string | null;\n color: string | null;\n createdAt: string;\n}\n\n/** A build-label row. */\nexport interface BuildLabel {\n id: string;\n projectId: string;\n buildId: string;\n typeKey: string;\n value: string;\n createdAt: string;\n}\n\n/** A CI token row (hash only; the secret itself is never stored). */\nexport interface Token {\n id: string;\n projectId: string;\n name: string;\n hash: string;\n lastUsedAt: string | null;\n createdAt: string;\n}\n\n/** A webhook subscription row. */\nexport interface Webhook {\n id: string;\n projectId: string;\n url: string;\n secret: string;\n events: string | null;\n createdAt: string;\n updatedAt: string;\n}\n\n/** A user row. */\nexport interface User {\n id: string;\n email: string;\n name: string;\n avatarUrl: string | null;\n role: SiteRole;\n lastLoginAt: string | null;\n createdAt: string;\n}\n\n/** A project-membership row. */\nexport interface ProjectMember {\n id: string;\n projectId: string;\n userId: string;\n role: ProjectRole;\n createdAt: string;\n}\n"],"mappings":";;;;;;AAkCA,MAAa,WAA2BA;;AAExC,MAAa,uBAAuCC;;AAEpD,MAAa,SAAyBC;;AAEtC,MAAa,YAA4BC;;AAEzC,MAAa,YAA4BC;;AAEzC,MAAa,WAA2BC;;AAExC,MAAa,aAA6BC;;AAE1C,MAAa,cAA8BC;;AAE3C,MAAa,SAAyBC;;AAEtC,MAAa,WAA2BC;;AAExC,MAAa,QAAwBC;;AAErC,MAAa,iBAAiCC;;AAG9C,MAAa,SAAS;CACpB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF"}
package/dist/store.mjs CHANGED
@@ -4,9 +4,11 @@ import.meta.url;
4
4
  import { AsyncLocalStorage } from "node:async_hooks";
5
5
  //#region src/store.ts
6
6
  const storage = new AsyncLocalStorage();
7
+ /** Run a function with the given request store in scope. */
7
8
  function runWithStore(store, fn) {
8
9
  return storage.run(store, fn);
9
10
  }
11
+ /** Return the current request store, throwing outside a request scope. */
10
12
  function getStore() {
11
13
  const store = storage.getStore();
12
14
  if (!store) throw new Error("Store not available. Use runWithStore().");
@@ -1 +1 @@
1
- {"version":3,"file":"store.mjs","names":[],"sources":["../src/store.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"node:async_hooks\";\n\nimport type { Logger } from \"pino\";\n\nimport type { AuthUser } from \"./adapters/auth.ts\";\nimport type { CaptureQueue } from \"./adapters/capture-queue.ts\";\nimport type { DatabaseAdapter } from \"./adapters/database.ts\";\nimport type { GitHostProvider } from \"./adapters/git-host/index.ts\";\nimport type { StorageAdapter } from \"./adapters/storage.ts\";\nimport type { ShelfConfig, UIConfig } from \"./config.ts\";\n\nexport interface Store {\n db: DatabaseAdapter;\n storage: StorageAdapter;\n config: ShelfConfig;\n ui: UIConfig;\n logger: Logger;\n user: AuthUser | null;\n authEnabled: boolean;\n enqueueCapture?: (buildId: string, reqId?: string) => Promise<void>;\n captureQueue?: CaptureQueue | null;\n gitHosts: GitHostProvider[];\n}\n\nconst storage = new AsyncLocalStorage<Store>();\n\nexport function runWithStore<T>(store: Store, fn: () => T): T {\n return storage.run(store, fn);\n}\n\nexport function getStore(): Store {\n const store = storage.getStore();\n if (!store) {\n throw new Error(\"Store not available. Use runWithStore().\");\n }\n return store;\n}\n"],"mappings":";;;;;AAwBA,MAAM,UAAU,IAAI,kBAAyB;AAE7C,SAAgB,aAAgB,OAAc,IAAgB;CAC5D,OAAO,QAAQ,IAAI,OAAO,EAAE;AAC9B;AAEA,SAAgB,WAAkB;CAChC,MAAM,QAAQ,QAAQ,SAAS;CAC/B,IAAI,CAAC,OACH,MAAM,IAAI,MAAM,0CAA0C;CAE5D,OAAO;AACT"}
1
+ {"version":3,"file":"store.mjs","names":[],"sources":["../src/store.ts"],"sourcesContent":["import { AsyncLocalStorage } from \"node:async_hooks\";\n\nimport type { Logger } from \"pino\";\n\nimport type { AuthUser } from \"./adapters/auth.ts\";\nimport type { CaptureQueue } from \"./adapters/capture-queue.ts\";\nimport type { DatabaseAdapter } from \"./adapters/database.ts\";\nimport type { GitHostProvider } from \"./adapters/git-host/index.ts\";\nimport type { StorageAdapter } from \"./adapters/storage.ts\";\nimport type { ShelfConfig, UIConfig } from \"./config.ts\";\n\n/** Request-scoped dependencies and session state. */\nexport interface Store {\n db: DatabaseAdapter;\n storage: StorageAdapter;\n config: ShelfConfig;\n ui: UIConfig;\n logger: Logger;\n user: AuthUser | null;\n authEnabled: boolean;\n enqueueCapture?: (buildId: string, reqId?: string) => Promise<void>;\n captureQueue?: CaptureQueue | null;\n gitHosts: GitHostProvider[];\n}\n\nconst storage = new AsyncLocalStorage<Store>();\n\n/** Run a function with the given request store in scope. */\nexport function runWithStore<T>(store: Store, fn: () => T): T {\n return storage.run(store, fn);\n}\n\n/** Return the current request store, throwing outside a request scope. */\nexport function getStore(): Store {\n const store = storage.getStore();\n if (!store) {\n throw new Error(\"Store not available. Use runWithStore().\");\n }\n return store;\n}\n"],"mappings":";;;;;AAyBA,MAAM,UAAU,IAAI,kBAAyB;;AAG7C,SAAgB,aAAgB,OAAc,IAAgB;CAC5D,OAAO,QAAQ,IAAI,OAAO,EAAE;AAC9B;;AAGA,SAAgB,WAAkB;CAChC,MAAM,QAAQ,QAAQ,SAAS;CAC/B,IAAI,CAAC,OACH,MAAM,IAAI,MAAM,0CAA0C;CAE5D,OAAO;AACT"}
package/dist/types.d.mts CHANGED
@@ -2,17 +2,29 @@ import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
3
  import { Logger } from "./logger.mjs";
4
4
  //#region src/types.d.ts
5
+ /** All lifecycle statuses a build can carry. */
5
6
  declare const BUILD_STATUSES: readonly ["pending", "capturing", "comparing", "reviewing", "approved", "rejected", "failed"];
7
+ /** Lifecycle status of a build. */
6
8
  type BuildStatus = (typeof BUILD_STATUSES)[number];
9
+ /** All review statuses a snapshot can carry. */
7
10
  declare const SNAPSHOT_STATUSES: readonly ["pending", "new", "unchanged", "changed", "approved", "rejected"];
11
+ /** Review status of a snapshot. */
8
12
  type SnapshotStatus = (typeof SNAPSHOT_STATUSES)[number];
13
+ /** Build statuses eligible for retention purging. */
9
14
  declare const TERMINAL_BUILD_STATUSES: readonly BuildStatus[];
15
+ /** All site-wide user roles. */
10
16
  declare const SITE_ROLES: readonly ["admin", "member"];
17
+ /** Site-wide role of a user. */
11
18
  type SiteRole = (typeof SITE_ROLES)[number];
19
+ /** All per-project membership roles. */
12
20
  declare const PROJECT_ROLES: readonly ["admin", "approver", "developer", "viewer"];
21
+ /** Per-project role of a member. */
13
22
  type ProjectRole = (typeof PROJECT_ROLES)[number];
23
+ /** Label type keys seeded for every new project. */
14
24
  declare const SEEDED_LABEL_KEYS: readonly ["branch", "persistent", "pr", "mr", "jira", "linear", "figma", "custom"];
25
+ /** Label type keys reserved for internal use. */
15
26
  declare const RESERVED_LABEL_KEYS: readonly ["build"];
27
+ /** Label key marking a build as exempt from retention purging. */
16
28
  declare const PERSISTENT_LABEL_KEY = "persistent";
17
29
  //#endregion
18
30
  export { BUILD_STATUSES, BuildStatus, type Logger, PERSISTENT_LABEL_KEY, PROJECT_ROLES, ProjectRole, RESERVED_LABEL_KEYS, SEEDED_LABEL_KEYS, SITE_ROLES, SNAPSHOT_STATUSES, SiteRole, SnapshotStatus, TERMINAL_BUILD_STATUSES };
package/dist/types.mjs CHANGED
@@ -2,6 +2,7 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  //#region src/types.ts
5
+ /** All lifecycle statuses a build can carry. */
5
6
  const BUILD_STATUSES = [
6
7
  "pending",
7
8
  "capturing",
@@ -11,6 +12,7 @@ const BUILD_STATUSES = [
11
12
  "rejected",
12
13
  "failed"
13
14
  ];
15
+ /** All review statuses a snapshot can carry. */
14
16
  const SNAPSHOT_STATUSES = [
15
17
  "pending",
16
18
  "new",
@@ -19,18 +21,22 @@ const SNAPSHOT_STATUSES = [
19
21
  "approved",
20
22
  "rejected"
21
23
  ];
24
+ /** Build statuses eligible for retention purging. */
22
25
  const TERMINAL_BUILD_STATUSES = [
23
26
  "approved",
24
27
  "rejected",
25
28
  "failed"
26
29
  ];
30
+ /** All site-wide user roles. */
27
31
  const SITE_ROLES = ["admin", "member"];
32
+ /** All per-project membership roles. */
28
33
  const PROJECT_ROLES = [
29
34
  "admin",
30
35
  "approver",
31
36
  "developer",
32
37
  "viewer"
33
38
  ];
39
+ /** Label type keys seeded for every new project. */
34
40
  const SEEDED_LABEL_KEYS = [
35
41
  "branch",
36
42
  "persistent",
@@ -41,7 +47,9 @@ const SEEDED_LABEL_KEYS = [
41
47
  "figma",
42
48
  "custom"
43
49
  ];
50
+ /** Label type keys reserved for internal use. */
44
51
  const RESERVED_LABEL_KEYS = ["build"];
52
+ /** Label key marking a build as exempt from retention purging. */
45
53
  const PERSISTENT_LABEL_KEY = "persistent";
46
54
  //#endregion
47
55
  export { BUILD_STATUSES, PERSISTENT_LABEL_KEY, PROJECT_ROLES, RESERVED_LABEL_KEYS, SEEDED_LABEL_KEYS, SITE_ROLES, SNAPSHOT_STATUSES, TERMINAL_BUILD_STATUSES };
@@ -1 +1 @@
1
- {"version":3,"file":"types.mjs","names":[],"sources":["../src/types.ts"],"sourcesContent":["export const BUILD_STATUSES = [\n \"pending\",\n \"capturing\",\n \"comparing\",\n \"reviewing\",\n \"approved\",\n \"rejected\",\n \"failed\",\n] as const;\n\nexport type BuildStatus = (typeof BUILD_STATUSES)[number];\n\nexport const SNAPSHOT_STATUSES = [\"pending\", \"new\", \"unchanged\", \"changed\", \"approved\", \"rejected\"] as const;\n\nexport type SnapshotStatus = (typeof SNAPSHOT_STATUSES)[number];\n\nexport const TERMINAL_BUILD_STATUSES: readonly BuildStatus[] = [\"approved\", \"rejected\", \"failed\"];\n\nexport const SITE_ROLES = [\"admin\", \"member\"] as const;\n\nexport type SiteRole = (typeof SITE_ROLES)[number];\n\nexport const PROJECT_ROLES = [\"admin\", \"approver\", \"developer\", \"viewer\"] as const;\n\nexport type ProjectRole = (typeof PROJECT_ROLES)[number];\n\nexport const SEEDED_LABEL_KEYS = [\"branch\", \"persistent\", \"pr\", \"mr\", \"jira\", \"linear\", \"figma\", \"custom\"] as const;\n\nexport const RESERVED_LABEL_KEYS = [\"build\"] as const;\n\nexport const PERSISTENT_LABEL_KEY = \"persistent\";\n\nexport type { Logger } from \"./logger.ts\";\n"],"mappings":";;;;AAAA,MAAa,iBAAiB;CAC5B;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAIA,MAAa,oBAAoB;CAAC;CAAW;CAAO;CAAa;CAAW;CAAY;AAAU;AAIlG,MAAa,0BAAkD;CAAC;CAAY;CAAY;AAAQ;AAEhG,MAAa,aAAa,CAAC,SAAS,QAAQ;AAI5C,MAAa,gBAAgB;CAAC;CAAS;CAAY;CAAa;AAAQ;AAIxE,MAAa,oBAAoB;CAAC;CAAU;CAAc;CAAM;CAAM;CAAQ;CAAU;CAAS;AAAQ;AAEzG,MAAa,sBAAsB,CAAC,OAAO;AAE3C,MAAa,uBAAuB"}
1
+ {"version":3,"file":"types.mjs","names":[],"sources":["../src/types.ts"],"sourcesContent":["/** All lifecycle statuses a build can carry. */\nexport const BUILD_STATUSES = [\n \"pending\",\n \"capturing\",\n \"comparing\",\n \"reviewing\",\n \"approved\",\n \"rejected\",\n \"failed\",\n] as const;\n\n/** Lifecycle status of a build. */\nexport type BuildStatus = (typeof BUILD_STATUSES)[number];\n\n/** All review statuses a snapshot can carry. */\nexport const SNAPSHOT_STATUSES = [\"pending\", \"new\", \"unchanged\", \"changed\", \"approved\", \"rejected\"] as const;\n\n/** Review status of a snapshot. */\nexport type SnapshotStatus = (typeof SNAPSHOT_STATUSES)[number];\n\n/** Build statuses eligible for retention purging. */\nexport const TERMINAL_BUILD_STATUSES: readonly BuildStatus[] = [\"approved\", \"rejected\", \"failed\"];\n\n/** All site-wide user roles. */\nexport const SITE_ROLES = [\"admin\", \"member\"] as const;\n\n/** Site-wide role of a user. */\nexport type SiteRole = (typeof SITE_ROLES)[number];\n\n/** All per-project membership roles. */\nexport const PROJECT_ROLES = [\"admin\", \"approver\", \"developer\", \"viewer\"] as const;\n\n/** Per-project role of a member. */\nexport type ProjectRole = (typeof PROJECT_ROLES)[number];\n\n/** Label type keys seeded for every new project. */\nexport const SEEDED_LABEL_KEYS = [\"branch\", \"persistent\", \"pr\", \"mr\", \"jira\", \"linear\", \"figma\", \"custom\"] as const;\n\n/** Label type keys reserved for internal use. */\nexport const RESERVED_LABEL_KEYS = [\"build\"] as const;\n\n/** Label key marking a build as exempt from retention purging. */\nexport const PERSISTENT_LABEL_KEY = \"persistent\";\n\nexport type { Logger } from \"./logger.ts\";\n"],"mappings":";;;;;AACA,MAAa,iBAAiB;CAC5B;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;AAMA,MAAa,oBAAoB;CAAC;CAAW;CAAO;CAAa;CAAW;CAAY;AAAU;;AAMlG,MAAa,0BAAkD;CAAC;CAAY;CAAY;AAAQ;;AAGhG,MAAa,aAAa,CAAC,SAAS,QAAQ;;AAM5C,MAAa,gBAAgB;CAAC;CAAS;CAAY;CAAa;AAAQ;;AAMxE,MAAa,oBAAoB;CAAC;CAAU;CAAc;CAAM;CAAM;CAAQ;CAAU;CAAS;AAAQ;;AAGzG,MAAa,sBAAsB,CAAC,OAAO;;AAG3C,MAAa,uBAAuB"}
@@ -3,12 +3,14 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
5
5
  //#region src/ui/components.tsx
6
+ /** Small status pill with a color tone. */
6
7
  const Badge = ({ tone = "neutral", children }) => {
7
8
  return /* @__PURE__ */ jsx("span", {
8
9
  class: `badge badge--${tone}`,
9
10
  children
10
11
  });
11
12
  };
13
+ /** Map a build or snapshot status string to its badge tone. */
12
14
  function statusTone(status) {
13
15
  if (status === "approved" || status === "unchanged") return "success";
14
16
  if (status === "rejected" || status === "failed") return "danger";
@@ -34,6 +36,7 @@ function fieldDescribedBy(name, error, hint) {
34
36
  if (error) return `${name}-error`;
35
37
  if (hint) return `${name}-hint`;
36
38
  }
39
+ /** Labeled text input with error and hint states. */
37
40
  const Field = ({ label, name, type = "text", value, placeholder, required, error, hint, autocomplete }) => {
38
41
  return /* @__PURE__ */ jsxs("div", {
39
42
  class: "field",
@@ -66,6 +69,7 @@ const Field = ({ label, name, type = "text", value, placeholder, required, error
66
69
  ]
67
70
  });
68
71
  };
72
+ /** Labeled textarea with error and hint states. */
69
73
  const TextareaField = ({ label, name, value, placeholder, rows = 3, hint, error }) => {
70
74
  return /* @__PURE__ */ jsxs("div", {
71
75
  class: "field",
@@ -93,6 +97,7 @@ const TextareaField = ({ label, name, value, placeholder, rows = 3, hint, error
93
97
  ]
94
98
  });
95
99
  };
100
+ /** Labeled select dropdown with an optional hint. */
96
101
  const SelectField = ({ label, name, value, options, hint }) => {
97
102
  return /* @__PURE__ */ jsxs("div", {
98
103
  class: "field",
@@ -1 +1 @@
1
- {"version":3,"file":"components.mjs","names":[],"sources":["../../src/ui/components.tsx"],"sourcesContent":["import type { FC } from \"hono/jsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\ntype ButtonVariant = \"primary\" | \"secondary\" | \"ghost\" | \"danger\";\ntype BadgeTone = \"neutral\" | \"success\" | \"warning\" | \"danger\" | \"info\";\n\nexport const Button: FC<{\n variant?: ButtonVariant;\n type?: \"button\" | \"submit\" | \"reset\";\n href?: string;\n disabled?: boolean;\n children?: unknown;\n [key: string]: unknown;\n}> = ({ variant = \"primary\", type = \"button\", href, disabled, children, ...rest }) => {\n const className = `btn btn--${variant}`;\n if (href) {\n return (\n <a class={className} href={href} aria-disabled={disabled ? \"true\" : undefined} {...rest}>\n {children}\n </a>\n );\n }\n return (\n <button class={className} type={type} disabled={disabled} {...rest}>\n {children}\n </button>\n );\n};\n\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const Badge: FC<{ tone?: BadgeTone; children?: unknown }> = ({ tone = \"neutral\", children }) => {\n return <span class={`badge badge--${tone}`}>{children}</span>;\n};\n\nexport function statusTone(status: string): BadgeTone {\n if (status === \"approved\" || status === \"unchanged\") {\n return \"success\";\n }\n if (status === \"rejected\" || status === \"failed\") {\n return \"danger\";\n }\n if (status === \"changed\" || status === \"new\" || status === \"reviewing\") {\n return \"warning\";\n }\n if (status === \"capturing\" || status === \"comparing\" || status === \"pending\") {\n return \"info\";\n }\n return \"neutral\";\n}\n\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const Card: FC<{ children?: unknown; padded?: boolean }> = ({ children, padded = true }) => {\n return <div class={padded ? \"card card--padded\" : \"card\"}>{children}</div>;\n};\n\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const Alert: FC<{ tone?: BadgeTone; title?: string; children?: unknown }> = ({ tone = \"info\", title, children }) => {\n return (\n <div class={`alert alert--${tone}`} role=\"alert\">\n {title ? <strong class=\"alert__title\">{title}</strong> : null}\n <div class=\"alert__body\">{children}</div>\n </div>\n );\n};\n\n// eslint-disable-next-line promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString>\nconst FieldAssistant: FC<{ name: string; error: string | undefined; hint: string | undefined }> = ({ name, error, hint }) => {\n if (error) {\n return (\n <p class=\"field__error\" id={`${name}-error`} role=\"alert\">\n {error}\n </p>\n );\n }\n if (hint) {\n return (\n <p class=\"field__hint\" id={`${name}-hint`}>\n {hint}\n </p>\n );\n }\n return null;\n};\n\nfunction fieldDescribedBy(name: string, error: string | undefined, hint: string | undefined): string | undefined {\n if (error) {\n return `${name}-error`;\n }\n if (hint) {\n return `${name}-hint`;\n }\n return undefined;\n}\n\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const Field: FC<{\n label: string;\n name: string;\n type?: string;\n value?: string;\n placeholder?: string;\n required?: boolean;\n error?: string;\n hint?: string;\n autocomplete?: string;\n}> = ({ label, name, type = \"text\", value, placeholder, required, error, hint, autocomplete }) => {\n return (\n <div class=\"field\">\n <label class=\"field__label\" for={name}>\n {label}\n {required ? <span aria-hidden=\"true\"> *</span> : null}\n </label>\n <input\n class={`field__input ${error ? \"field__input--error\" : \"\"}`}\n id={name}\n name={name}\n type={type}\n value={value}\n placeholder={placeholder}\n required={required}\n aria-invalid={error ? \"true\" : undefined}\n aria-describedby={fieldDescribedBy(name, error, hint)}\n autocomplete={autocomplete}\n />\n <FieldAssistant name={name} error={error} hint={hint} />\n </div>\n );\n};\n\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const TextareaField: FC<{\n label: string;\n name: string;\n value?: string;\n placeholder?: string;\n rows?: number;\n hint?: string;\n error?: string;\n}> = ({ label, name, value, placeholder, rows = 3, hint, error }) => {\n return (\n <div class=\"field\">\n <label class=\"field__label\" for={name}>\n {label}\n </label>\n <textarea\n class={`field__input field__input--textarea ${error ? \"field__input--error\" : \"\"}`}\n id={name}\n name={name}\n placeholder={placeholder}\n rows={rows}\n aria-invalid={error ? \"true\" : undefined}\n aria-describedby={fieldDescribedBy(name, error, hint)}\n >\n {value}\n </textarea>\n <FieldAssistant name={name} error={error} hint={hint} />\n </div>\n );\n};\n\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const SelectField: FC<{\n label: string;\n name: string;\n value?: string;\n options: { value: string; label: string }[];\n hint?: string;\n}> = ({ label, name, value, options, hint }) => {\n return (\n <div class=\"field\">\n <label class=\"field__label\" for={name}>\n {label}\n </label>\n <select class=\"field__input\" id={name} name={name} aria-describedby={hint ? `${name}-hint` : undefined}>\n {options.map((opt) => (\n <option value={opt.value} selected={opt.value === value}>\n {opt.label}\n </option>\n ))}\n </select>\n {hint ? (\n <p class=\"field__hint\" id={`${name}-hint`}>\n {hint}\n </p>\n ) : null}\n </div>\n );\n};\n\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const PageHeader: FC<{\n title: string;\n description?: string;\n actions?: unknown;\n breadcrumbs?: { label: string; href?: string }[];\n}> = ({ title, description, actions, breadcrumbs }) => {\n return (\n <div class=\"page-header\">\n {breadcrumbs && breadcrumbs.length > 0 ? (\n <nav class=\"breadcrumbs\" aria-label=\"Breadcrumb\">\n <ol>\n {breadcrumbs.map((crumb, index) => (\n <li key={String(index)}>\n {crumb.href ? <a href={crumb.href}>{crumb.label}</a> : <span aria-current=\"page\">{crumb.label}</span>}\n </li>\n ))}\n </ol>\n </nav>\n ) : null}\n <div class=\"page-header__row\">\n <div>\n <h1 class=\"page-header__title\">{title}</h1>\n {description ? <p class=\"page-header__desc\">{description}</p> : null}\n </div>\n {actions ? <div class=\"page-header__actions\">{actions}</div> : null}\n </div>\n </div>\n );\n};\n\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const EmptyState: FC<{ title: string; description?: string; action?: unknown }> = ({ title, description, action }) => {\n return (\n <div class=\"empty\">\n <h2 class=\"empty__title\">{title}</h2>\n {description ? <p class=\"empty__desc\">{description}</p> : null}\n {action ? <div class=\"empty__action\">{action}</div> : null}\n </div>\n );\n};\n\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}> = ({ tabs }) => {\n return (\n <nav class=\"tabs\" aria-label=\"Sections\">\n {tabs.map((tab) => (\n <a class={`tabs__link ${tab.active ? \"tabs__link--active\" : \"\"}`} href={tab.href} aria-current={tab.active ? \"page\" : undefined}>\n {tab.label}\n </a>\n ))}\n </nav>\n );\n};\n\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const Stat: FC<{ label: string; value: string | number }> = ({ label, value }) => {\n return (\n <div class=\"stat\">\n <div class=\"stat__value\">{String(value)}</div>\n <div class=\"stat__label\">{label}</div>\n </div>\n );\n};\n"],"mappings":";;;;;AA+BA,MAAa,SAAuD,EAAE,OAAO,WAAW,eAAe;CACrG,OAAO,oBAAC,QAAD;EAAM,OAAO,gBAAgB;EAAS;CAAe,CAAA;AAC9D;AAEA,SAAgB,WAAW,QAA2B;CACpD,IAAI,WAAW,cAAc,WAAW,aACtC,OAAO;CAET,IAAI,WAAW,cAAc,WAAW,UACtC,OAAO;CAET,IAAI,WAAW,aAAa,WAAW,SAAS,WAAW,aACzD,OAAO;CAET,IAAI,WAAW,eAAe,WAAW,eAAe,WAAW,WACjE,OAAO;CAET,OAAO;AACT;AAkBA,MAAM,kBAA6F,EAAE,MAAM,OAAO,WAAW;CAC3H,IAAI,OACF,OACE,oBAAC,KAAD;EAAG,OAAM;EAAe,IAAI,GAAG,KAAK;EAAS,MAAK;EAC/C,UAAA;CACA,CAAA;CAGP,IAAI,MACF,OACE,oBAAC,KAAD;EAAG,OAAM;EAAc,IAAI,GAAG,KAAK;EAChC,UAAA;CACA,CAAA;CAGP,OAAO;AACT;AAEA,SAAS,iBAAiB,MAAc,OAA2B,MAA8C;CAC/G,IAAI,OACF,OAAO,GAAG,KAAK;CAEjB,IAAI,MACF,OAAO,GAAG,KAAK;AAGnB;AAGA,MAAa,SAUP,EAAE,OAAO,MAAM,OAAO,QAAQ,OAAO,aAAa,UAAU,OAAO,MAAM,mBAAmB;CAChG,OACE,qBAAC,OAAD;EAAK,OAAM;EAAX,UAAA;GACE,qBAAC,SAAD;IAAO,OAAM;IAAe,KAAK;IAAjC,UAAA,CACG,OACA,WAAW,oBAAC,QAAD;KAAM,eAAY;KAAO,UAAA;IAAQ,CAAA,IAAI,IAC5C;;GACP,oBAAC,SAAD;IACE,OAAO,gBAAgB,QAAQ,wBAAwB;IACvD,IAAI;IACE;IACA;IACC;IACM;IACH;IACV,gBAAc,QAAQ,SAAS,KAAA;IAC/B,oBAAkB,iBAAiB,MAAM,OAAO,IAAI;IACtC;GACf,CAAA;GACD,oBAAC,gBAAD;IAAsB;IAAa;IAAa;GAAO,CAAA;EACpD;;AAET;AAGA,MAAa,iBAQP,EAAE,OAAO,MAAM,OAAO,aAAa,OAAO,GAAG,MAAM,YAAY;CACnE,OACE,qBAAC,OAAD;EAAK,OAAM;EAAX,UAAA;GACE,oBAAC,SAAD;IAAO,OAAM;IAAe,KAAK;IAC9B,UAAA;GACI,CAAA;GACP,oBAAC,YAAD;IACE,OAAO,uCAAuC,QAAQ,wBAAwB;IAC9E,IAAI;IACE;IACO;IACP;IACN,gBAAc,QAAQ,SAAS,KAAA;IAC/B,oBAAkB,iBAAiB,MAAM,OAAO,IAAI;IAEnD,UAAA;GACO,CAAA;GACV,oBAAC,gBAAD;IAAsB;IAAa;IAAa;GAAO,CAAA;EACpD;;AAET;AAGA,MAAa,eAMP,EAAE,OAAO,MAAM,OAAO,SAAS,WAAW;CAC9C,OACE,qBAAC,OAAD;EAAK,OAAM;EAAX,UAAA;GACE,oBAAC,SAAD;IAAO,OAAM;IAAe,KAAK;IAC9B,UAAA;GACI,CAAA;GACP,oBAAC,UAAD;IAAQ,OAAM;IAAe,IAAI;IAAY;IAAM,oBAAkB,OAAO,GAAG,KAAK,SAAS,KAAA;IAC1F,UAAA,QAAQ,KAAK,QACZ,oBAAC,UAAD;KAAQ,OAAO,IAAI;KAAO,UAAU,IAAI,UAAU;KAC/C,UAAA,IAAI;IACC,CAAA,CACT;GACK,CAAA;GACP,OACC,oBAAC,KAAD;IAAG,OAAM;IAAc,IAAI,GAAG,KAAK;IAChC,UAAA;GACA,CAAA,IACD;EACD;;AAET"}
1
+ {"version":3,"file":"components.mjs","names":[],"sources":["../../src/ui/components.tsx"],"sourcesContent":["import type { FC } from \"hono/jsx\";\n\n/* eslint-disable promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString> */\n\ntype ButtonVariant = \"primary\" | \"secondary\" | \"ghost\" | \"danger\";\ntype BadgeTone = \"neutral\" | \"success\" | \"warning\" | \"danger\" | \"info\";\n\n/** Button or link styled as a button (renders an anchor when `href` is set). */\nexport const Button: FC<{\n variant?: ButtonVariant;\n type?: \"button\" | \"submit\" | \"reset\";\n href?: string;\n disabled?: boolean;\n children?: unknown;\n [key: string]: unknown;\n}> = ({ variant = \"primary\", type = \"button\", href, disabled, children, ...rest }) => {\n const className = `btn btn--${variant}`;\n if (href) {\n return (\n <a class={className} href={href} aria-disabled={disabled ? \"true\" : undefined} {...rest}>\n {children}\n </a>\n );\n }\n return (\n <button class={className} type={type} disabled={disabled} {...rest}>\n {children}\n </button>\n );\n};\n\n/** Small status pill with a color tone. */\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const Badge: FC<{ tone?: BadgeTone; children?: unknown }> = ({ tone = \"neutral\", children }) => {\n return <span class={`badge badge--${tone}`}>{children}</span>;\n};\n\n/** Map a build or snapshot status string to its badge tone. */\nexport function statusTone(status: string): BadgeTone {\n if (status === \"approved\" || status === \"unchanged\") {\n return \"success\";\n }\n if (status === \"rejected\" || status === \"failed\") {\n return \"danger\";\n }\n if (status === \"changed\" || status === \"new\" || status === \"reviewing\") {\n return \"warning\";\n }\n if (status === \"capturing\" || status === \"comparing\" || status === \"pending\") {\n return \"info\";\n }\n return \"neutral\";\n}\n\n/** Card container, padded by default. */\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const Card: FC<{ children?: unknown; padded?: boolean }> = ({ children, padded = true }) => {\n return <div class={padded ? \"card card--padded\" : \"card\"}>{children}</div>;\n};\n\n/** Alert banner with an optional title. */\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const Alert: FC<{ tone?: BadgeTone; title?: string; children?: unknown }> = ({ tone = \"info\", title, children }) => {\n return (\n <div class={`alert alert--${tone}`} role=\"alert\">\n {title ? <strong class=\"alert__title\">{title}</strong> : null}\n <div class=\"alert__body\">{children}</div>\n </div>\n );\n};\n\n// eslint-disable-next-line promise-function-async -- Hono JSX components return HtmlEscapedString | Promise<HtmlEscapedString>\nconst FieldAssistant: FC<{ name: string; error: string | undefined; hint: string | undefined }> = ({ name, error, hint }) => {\n if (error) {\n return (\n <p class=\"field__error\" id={`${name}-error`} role=\"alert\">\n {error}\n </p>\n );\n }\n if (hint) {\n return (\n <p class=\"field__hint\" id={`${name}-hint`}>\n {hint}\n </p>\n );\n }\n return null;\n};\n\nfunction fieldDescribedBy(name: string, error: string | undefined, hint: string | undefined): string | undefined {\n if (error) {\n return `${name}-error`;\n }\n if (hint) {\n return `${name}-hint`;\n }\n return undefined;\n}\n\n/** Labeled text input with error and hint states. */\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const Field: FC<{\n label: string;\n name: string;\n type?: string;\n value?: string;\n placeholder?: string;\n required?: boolean;\n error?: string;\n hint?: string;\n autocomplete?: string;\n}> = ({ label, name, type = \"text\", value, placeholder, required, error, hint, autocomplete }) => {\n return (\n <div class=\"field\">\n <label class=\"field__label\" for={name}>\n {label}\n {required ? <span aria-hidden=\"true\"> *</span> : null}\n </label>\n <input\n class={`field__input ${error ? \"field__input--error\" : \"\"}`}\n id={name}\n name={name}\n type={type}\n value={value}\n placeholder={placeholder}\n required={required}\n aria-invalid={error ? \"true\" : undefined}\n aria-describedby={fieldDescribedBy(name, error, hint)}\n autocomplete={autocomplete}\n />\n <FieldAssistant name={name} error={error} hint={hint} />\n </div>\n );\n};\n\n/** Labeled textarea with error and hint states. */\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const TextareaField: FC<{\n label: string;\n name: string;\n value?: string;\n placeholder?: string;\n rows?: number;\n hint?: string;\n error?: string;\n}> = ({ label, name, value, placeholder, rows = 3, hint, error }) => {\n return (\n <div class=\"field\">\n <label class=\"field__label\" for={name}>\n {label}\n </label>\n <textarea\n class={`field__input field__input--textarea ${error ? \"field__input--error\" : \"\"}`}\n id={name}\n name={name}\n placeholder={placeholder}\n rows={rows}\n aria-invalid={error ? \"true\" : undefined}\n aria-describedby={fieldDescribedBy(name, error, hint)}\n >\n {value}\n </textarea>\n <FieldAssistant name={name} error={error} hint={hint} />\n </div>\n );\n};\n\n/** Labeled select dropdown with an optional hint. */\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const SelectField: FC<{\n label: string;\n name: string;\n value?: string;\n options: { value: string; label: string }[];\n hint?: string;\n}> = ({ label, name, value, options, hint }) => {\n return (\n <div class=\"field\">\n <label class=\"field__label\" for={name}>\n {label}\n </label>\n <select class=\"field__input\" id={name} name={name} aria-describedby={hint ? `${name}-hint` : undefined}>\n {options.map((opt) => (\n <option value={opt.value} selected={opt.value === value}>\n {opt.label}\n </option>\n ))}\n </select>\n {hint ? (\n <p class=\"field__hint\" id={`${name}-hint`}>\n {hint}\n </p>\n ) : null}\n </div>\n );\n};\n\n/** Page header with breadcrumbs, title, description, and actions. */\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const PageHeader: FC<{\n title: string;\n description?: string;\n actions?: unknown;\n breadcrumbs?: { label: string; href?: string }[];\n}> = ({ title, description, actions, breadcrumbs }) => {\n return (\n <div class=\"page-header\">\n {breadcrumbs && breadcrumbs.length > 0 ? (\n <nav class=\"breadcrumbs\" aria-label=\"Breadcrumb\">\n <ol>\n {breadcrumbs.map((crumb, index) => (\n <li key={String(index)}>\n {crumb.href ? <a href={crumb.href}>{crumb.label}</a> : <span aria-current=\"page\">{crumb.label}</span>}\n </li>\n ))}\n </ol>\n </nav>\n ) : null}\n <div class=\"page-header__row\">\n <div>\n <h1 class=\"page-header__title\">{title}</h1>\n {description ? <p class=\"page-header__desc\">{description}</p> : null}\n </div>\n {actions ? <div class=\"page-header__actions\">{actions}</div> : null}\n </div>\n </div>\n );\n};\n\n/** Centered empty state with an optional call-to-action. */\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const EmptyState: FC<{ title: string; description?: string; action?: unknown }> = ({ title, description, action }) => {\n return (\n <div class=\"empty\">\n <h2 class=\"empty__title\">{title}</h2>\n {description ? <p class=\"empty__desc\">{description}</p> : null}\n {action ? <div class=\"empty__action\">{action}</div> : null}\n </div>\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}> = ({ tabs }) => {\n return (\n <nav class=\"tabs\" aria-label=\"Sections\">\n {tabs.map((tab) => (\n <a class={`tabs__link ${tab.active ? \"tabs__link--active\" : \"\"}`} href={tab.href} aria-current={tab.active ? \"page\" : undefined}>\n {tab.label}\n </a>\n ))}\n </nav>\n );\n};\n\n/** Centered statistic value with a label. */\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const Stat: FC<{ label: string; value: string | number }> = ({ label, value }) => {\n return (\n <div class=\"stat\">\n <div class=\"stat__value\">{String(value)}</div>\n <div class=\"stat__label\">{label}</div>\n </div>\n );\n};\n"],"mappings":";;;;;;AAiCA,MAAa,SAAuD,EAAE,OAAO,WAAW,eAAe;CACrG,OAAO,oBAAC,QAAD;EAAM,OAAO,gBAAgB;EAAS;CAAe,CAAA;AAC9D;;AAGA,SAAgB,WAAW,QAA2B;CACpD,IAAI,WAAW,cAAc,WAAW,aACtC,OAAO;CAET,IAAI,WAAW,cAAc,WAAW,UACtC,OAAO;CAET,IAAI,WAAW,aAAa,WAAW,SAAS,WAAW,aACzD,OAAO;CAET,IAAI,WAAW,eAAe,WAAW,eAAe,WAAW,WACjE,OAAO;CAET,OAAO;AACT;AAoBA,MAAM,kBAA6F,EAAE,MAAM,OAAO,WAAW;CAC3H,IAAI,OACF,OACE,oBAAC,KAAD;EAAG,OAAM;EAAe,IAAI,GAAG,KAAK;EAAS,MAAK;EAC/C,UAAA;CACA,CAAA;CAGP,IAAI,MACF,OACE,oBAAC,KAAD;EAAG,OAAM;EAAc,IAAI,GAAG,KAAK;EAChC,UAAA;CACA,CAAA;CAGP,OAAO;AACT;AAEA,SAAS,iBAAiB,MAAc,OAA2B,MAA8C;CAC/G,IAAI,OACF,OAAO,GAAG,KAAK;CAEjB,IAAI,MACF,OAAO,GAAG,KAAK;AAGnB;;AAIA,MAAa,SAUP,EAAE,OAAO,MAAM,OAAO,QAAQ,OAAO,aAAa,UAAU,OAAO,MAAM,mBAAmB;CAChG,OACE,qBAAC,OAAD;EAAK,OAAM;EAAX,UAAA;GACE,qBAAC,SAAD;IAAO,OAAM;IAAe,KAAK;IAAjC,UAAA,CACG,OACA,WAAW,oBAAC,QAAD;KAAM,eAAY;KAAO,UAAA;IAAQ,CAAA,IAAI,IAC5C;;GACP,oBAAC,SAAD;IACE,OAAO,gBAAgB,QAAQ,wBAAwB;IACvD,IAAI;IACE;IACA;IACC;IACM;IACH;IACV,gBAAc,QAAQ,SAAS,KAAA;IAC/B,oBAAkB,iBAAiB,MAAM,OAAO,IAAI;IACtC;GACf,CAAA;GACD,oBAAC,gBAAD;IAAsB;IAAa;IAAa;GAAO,CAAA;EACpD;;AAET;;AAIA,MAAa,iBAQP,EAAE,OAAO,MAAM,OAAO,aAAa,OAAO,GAAG,MAAM,YAAY;CACnE,OACE,qBAAC,OAAD;EAAK,OAAM;EAAX,UAAA;GACE,oBAAC,SAAD;IAAO,OAAM;IAAe,KAAK;IAC9B,UAAA;GACI,CAAA;GACP,oBAAC,YAAD;IACE,OAAO,uCAAuC,QAAQ,wBAAwB;IAC9E,IAAI;IACE;IACO;IACP;IACN,gBAAc,QAAQ,SAAS,KAAA;IAC/B,oBAAkB,iBAAiB,MAAM,OAAO,IAAI;IAEnD,UAAA;GACO,CAAA;GACV,oBAAC,gBAAD;IAAsB;IAAa;IAAa;GAAO,CAAA;EACpD;;AAET;;AAIA,MAAa,eAMP,EAAE,OAAO,MAAM,OAAO,SAAS,WAAW;CAC9C,OACE,qBAAC,OAAD;EAAK,OAAM;EAAX,UAAA;GACE,oBAAC,SAAD;IAAO,OAAM;IAAe,KAAK;IAC9B,UAAA;GACI,CAAA;GACP,oBAAC,UAAD;IAAQ,OAAM;IAAe,IAAI;IAAY;IAAM,oBAAkB,OAAO,GAAG,KAAK,SAAS,KAAA;IAC1F,UAAA,QAAQ,KAAK,QACZ,oBAAC,UAAD;KAAQ,OAAO,IAAI;KAAO,UAAU,IAAI,UAAU;KAC/C,UAAA,IAAI;IACC,CAAA,CACT;GACK,CAAA;GACP,OACC,oBAAC,KAAD;IAAG,OAAM;IAAc,IAAI,GAAG,KAAK;IAChC,UAAA;GACA,CAAA,IACD;EACD;;AAET"}
@@ -2,6 +2,7 @@ import __tsdown_shims_path from 'node:path';
2
2
  import __tsdown_shims_url from 'node:url';
3
3
  import "hono/jsx";
4
4
  //#region src/ui/document.d.ts
5
+ /** Rendered HTML content returned by page components. */
5
6
  type RenderedContent = string | Promise<string>;
6
7
  //#endregion
7
8
  export { RenderedContent };
@@ -6,6 +6,7 @@ import { baseStyle } from "./styles.mjs";
6
6
  import { DARK_THEME, LIGHT_THEME } from "./theme.mjs";
7
7
  import { Fragment, jsx, jsxs } from "hono/jsx/jsx-runtime";
8
8
  //#region src/ui/document.tsx
9
+ /** Full HTML document shell with top bar, sidebar, theme, and HTMX. */
9
10
  const DocumentLayout = ({ title, nav, children }) => {
10
11
  const { ui } = getStore();
11
12
  const name = ui.name ?? "StoryShelf";
@@ -1 +1 @@
1
- {"version":3,"file":"document.mjs","names":[],"sources":["../../src/ui/document.tsx"],"sourcesContent":["import type { FC } from \"hono/jsx\";\n\nimport { getStore } from \"../store.ts\";\nimport { baseStyle } from \"./styles.ts\";\nimport { DARK_THEME, LIGHT_THEME } from \"./theme.ts\";\n\nexport type RenderedContent = string | Promise<string>;\n\ninterface NavConfig {\n active?: string;\n projectSlug?: string;\n projectName?: string;\n}\n\n// Hono's JSX.Element is typed as `HtmlEscapedString | Promise<...>`, so JSX-returning\n// Functions can legitimately return a promise; the rule is a false positive here.\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const DocumentLayout: FC<{ title: string; nav?: NavConfig; children?: unknown }> = ({ title, nav, children }) => {\n const { ui } = getStore();\n const name = ui.name ?? \"StoryShelf\";\n const light = ui.lightTheme ?? LIGHT_THEME;\n const dark = ui.darkTheme ?? DARK_THEME;\n\n return (\n <html lang=\"en\" data-theme=\"system\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <meta name=\"color-scheme\" content=\"light dark\" />\n {ui.favicon ? <link rel=\"icon\" href={ui.favicon} /> : null}\n <title>\n {title} · {name}\n </title>\n <style>{baseStyle(light, dark)}</style>\n <script dangerouslySetInnerHTML={{ __html: themeScript() }} />\n </head>\n <body>\n <a class=\"skip-link\" href=\"#main-content\">\n Skip to content\n </a>\n <TopBar name={name} logo={ui.logo} nav={nav} />\n <div class=\"shell\">\n <Sidebar nav={nav} />\n <main id=\"main-content\" class=\"content\" tabindex={-1}>\n {children}\n </main>\n </div>\n <script src=\"/assets/htmx.js\" />\n <script dangerouslySetInnerHTML={{ __html: clientScript() }} />\n </body>\n </html>\n );\n};\n\n// eslint-disable-next-line promise-function-async -- JSX component return type\nconst TopBar: FC<{ name: string; logo?: string; nav?: NavConfig }> = ({ name, logo, nav }) => {\n return (\n <header class=\"topbar\" role=\"banner\">\n <div class=\"topbar__inner\">\n <div class=\"topbar__left\">\n <button class=\"topbar__menu\" type=\"button\" aria-label=\"Toggle navigation\" aria-expanded=\"false\" aria-controls=\"sidebar\" data-sidebar-toggle>\n <span aria-hidden=\"true\">☰</span>\n </button>\n <a class=\"brand\" href=\"/\">\n {logo ? <img class=\"logo\" src={logo} alt={name} width=\"28\" height=\"28\" /> : <span class=\"brand__mark\" aria-hidden=\"true\">◆</span>}\n <span class=\"brand__name\">{name}</span>\n </a>\n {nav?.projectSlug ? (\n <nav class=\"project-crumb\" aria-label=\"Project\">\n <span class=\"project-crumb__sep\" aria-hidden=\"true\">\n /\n </span>\n <a href={`/projects/${nav.projectSlug}/builds`}>{nav.projectName ?? nav.projectSlug}</a>\n </nav>\n ) : null}\n </div>\n <div class=\"topbar__right\">\n <ThemeToggle />\n <AuthMenu />\n </div>\n </div>\n </header>\n );\n};\n\n// eslint-disable-next-line promise-function-async -- JSX component return type\nconst ThemeToggle: FC = () => {\n return (\n <button class=\"topbar__theme\" type=\"button\" aria-label=\"Toggle theme\" data-theme-toggle>\n <span data-theme-icon=\"light\" aria-hidden=\"true\">\n ☀\n </span>\n <span data-theme-icon=\"dark\" aria-hidden=\"true\">\n ☾\n </span>\n <span data-theme-icon=\"system\" aria-hidden=\"true\">\n ◐\n </span>\n </button>\n );\n};\n\n// eslint-disable-next-line promise-function-async -- JSX component return type\nconst AuthMenu: FC = () => {\n const { user, authEnabled } = getStore();\n if (!authEnabled) {\n return null;\n }\n if (!user) {\n return (\n <a class=\"topbar__login\" href=\"/auth/login\">\n Sign in\n </a>\n );\n }\n return (\n <div class=\"user-menu\">\n {user.avatarUrl ? (\n <img class=\"user-menu__avatar\" src={user.avatarUrl} alt=\"\" width=\"28\" height=\"28\" />\n ) : (\n <span class=\"user-menu__avatar user-menu__avatar--fallback\" aria-hidden=\"true\">\n {user.name.slice(0, 1).toUpperCase()}\n </span>\n )}\n <span class=\"user-menu__name\">{user.name}</span>\n <span class={`user-menu__role user-menu__role--${user.role}`}>{user.role}</span>\n <form method=\"post\" action=\"/auth/logout\">\n <button class=\"user-menu__logout\" type=\"submit\">\n Sign out\n </button>\n </form>\n </div>\n );\n};\n\n// eslint-disable-next-line promise-function-async -- JSX component return type\nconst Sidebar: FC<{ nav?: NavConfig }> = ({ nav }) => {\n return (\n <aside id=\"sidebar\" class=\"sidebar\" aria-label=\"Primary\">\n <nav class=\"sidebar__nav\">\n <a class={`sidebar__link ${nav?.active === \"projects\" ? \"sidebar__link--active\" : \"\"}`} href=\"/projects\" aria-current={nav?.active === \"projects\" ? \"page\" : undefined}>\n <span aria-hidden=\"true\">▦</span> Projects\n </a>\n {nav?.projectSlug ? (\n <>\n <div class=\"sidebar__section\">Project</div>\n <a class={`sidebar__link ${nav.active === \"builds\" ? \"sidebar__link--active\" : \"\"}`} href={`/projects/${nav.projectSlug}/builds`} aria-current={nav.active === \"builds\" ? \"page\" : undefined}>\n <span aria-hidden=\"true\">◧</span> Builds\n </a>\n <a class={`sidebar__link ${nav.active === \"jobs\" ? \"sidebar__link--active\" : \"\"}`} href={`/projects/${nav.projectSlug}/jobs`} aria-current={nav.active === \"jobs\" ? \"page\" : undefined}>\n <span aria-hidden=\"true\">◫</span> Jobs\n </a>\n <a class={`sidebar__link ${nav.active === \"labels\" ? \"sidebar__link--active\" : \"\"}`} href={`/projects/${nav.projectSlug}/labels`} aria-current={nav.active === \"labels\" ? \"page\" : undefined}>\n <span aria-hidden=\"true\">⌗</span> Labels\n </a>\n <a class={`sidebar__link ${nav.active === \"settings\" ? \"sidebar__link--active\" : \"\"}`} href={`/projects/${nav.projectSlug}/settings`} aria-current={nav.active === \"settings\" ? \"page\" : undefined}>\n <span aria-hidden=\"true\">⚙</span> Settings\n </a>\n </>\n ) : null}\n <div class=\"sidebar__section\">Resources</div>\n <a class=\"sidebar__link\" href=\"/api/v1/projects\" aria-label=\"API (JSON)\">\n <span aria-hidden=\"true\">{\"{} \"}</span> API\n </a>\n </nav>\n </aside>\n );\n};\n\nfunction themeScript(): string {\n return `\n (function(){\n var cookieName='storyshelf_theme';\n function readCookie(n){\n var m=document.cookie.match(new RegExp('(?:^|; )'+n.replace(/([.$?*|{}\\\\[\\\\]\\\\\\\\/\\\\+^])/gu,'\\\\\\\\$1')+'=([^;]*)'));\n return m?decodeURIComponent(m[1]):null;\n }\n function writeCookie(n,v){\n document.cookie=n+'='+encodeURIComponent(v)+'; Path=/; Max-Age=31536000; SameSite=Lax';\n }\n function applyTheme(v){\n var html=document.documentElement;\n if(v==='light'||v==='dark'||v==='system'){ html.setAttribute('data-theme',v); }\n else { html.setAttribute('data-theme','system'); }\n }\n var initial=readCookie(cookieName);\n if(initial){ applyTheme(initial); }\n window.__storyshelfTheme={readCookie:readCookie,writeCookie:writeCookie,applyTheme:applyTheme,cookieName:cookieName};\n })();\n `.trim();\n}\n\nfunction clientScript(): string {\n return `\n (function(){\n var theme=window.__storyshelfTheme;\n var toggle=document.querySelector('[data-theme-toggle]');\n if(toggle&&theme){\n toggle.addEventListener('click',function(){\n var cur=document.documentElement.getAttribute('data-theme')||'system';\n var next=cur==='system'?'light':cur==='light'?'dark':'system';\n theme.applyTheme(next);\n theme.writeCookie(theme.cookieName,next);\n });\n }\n var menuBtn=document.querySelector('[data-sidebar-toggle]');\n var sidebar=document.getElementById('sidebar');\n if(menuBtn&&sidebar){\n menuBtn.addEventListener('click',function(){\n var open=sidebar.classList.toggle('sidebar--open');\n menuBtn.setAttribute('aria-expanded', open ? 'true' : 'false');\n });\n }\n document.addEventListener('keydown',function(e){\n if(e.key==='Escape'&&sidebar&&sidebar.classList.contains('sidebar--open')){\n sidebar.classList.remove('sidebar--open');\n if(menuBtn) menuBtn.setAttribute('aria-expanded','false');\n menuBtn && menuBtn.focus();\n }\n });\n // HTMX after swap focus management\n document.body.addEventListener('htmx:afterSwap',function(e){\n var target=e.detail && e.detail.target;\n if(target&&target.querySelector){\n var focusable=target.querySelector('[autofocus], input, select, textarea, button');\n if(focusable) try{ focusable.focus(); }catch(_){}\n }\n });\n // Diff keyboard shortcuts\n document.addEventListener('keydown',function(e){\n if(e.target instanceof HTMLElement && (e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA'||e.target.isContentEditable)) return;\n var diffRoot=document.querySelector('[data-diff-nav]');\n if(!diffRoot) return;\n if(e.key==='ArrowLeft' || e.key==='ArrowRight'){\n var links=Array.from(diffRoot.querySelectorAll('[data-snapshot-link]'));\n if(links.length===0) return;\n var active=document.activeElement;\n var idx=links.indexOf(active);\n if(idx===-1){\n var current=diffRoot.getAttribute('data-current');\n idx=links.findIndex(function(a){ return a.getAttribute('data-snapshot-id')===current; });\n }\n var nextIdx=e.key==='ArrowRight' ? Math.min(links.length-1, idx+1) : Math.max(0, idx-1);\n if(links[nextIdx]){ e.preventDefault(); links[nextIdx].focus(); links[nextIdx].click(); }\n }\n });\n })();\n `.trim();\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"document.mjs","names":[],"sources":["../../src/ui/document.tsx"],"sourcesContent":["import type { FC } from \"hono/jsx\";\n\nimport { getStore } from \"../store.ts\";\nimport { baseStyle } from \"./styles.ts\";\nimport { DARK_THEME, LIGHT_THEME } from \"./theme.ts\";\n\n/** Rendered HTML content returned by page components. */\nexport type RenderedContent = string | Promise<string>;\n\ninterface NavConfig {\n active?: string;\n projectSlug?: string;\n projectName?: string;\n}\n\n// Hono's JSX.Element is typed as `HtmlEscapedString | Promise<...>`, so JSX-returning\n// Functions can legitimately return a promise; the rule is a false positive here.\n/** Full HTML document shell with top bar, sidebar, theme, and HTMX. */\n// eslint-disable-next-line promise-function-async -- JSX component return type\nexport const DocumentLayout: FC<{ title: string; nav?: NavConfig; children?: unknown }> = ({ title, nav, children }) => {\n const { ui } = getStore();\n const name = ui.name ?? \"StoryShelf\";\n const light = ui.lightTheme ?? LIGHT_THEME;\n const dark = ui.darkTheme ?? DARK_THEME;\n\n return (\n <html lang=\"en\" data-theme=\"system\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <meta name=\"color-scheme\" content=\"light dark\" />\n {ui.favicon ? <link rel=\"icon\" href={ui.favicon} /> : null}\n <title>\n {title} · {name}\n </title>\n <style>{baseStyle(light, dark)}</style>\n <script dangerouslySetInnerHTML={{ __html: themeScript() }} />\n </head>\n <body>\n <a class=\"skip-link\" href=\"#main-content\">\n Skip to content\n </a>\n <TopBar name={name} logo={ui.logo} nav={nav} />\n <div class=\"shell\">\n <Sidebar nav={nav} />\n <main id=\"main-content\" class=\"content\" tabindex={-1}>\n {children}\n </main>\n </div>\n <script src=\"/assets/htmx.js\" />\n <script dangerouslySetInnerHTML={{ __html: clientScript() }} />\n </body>\n </html>\n );\n};\n\n// eslint-disable-next-line promise-function-async -- JSX component return type\nconst TopBar: FC<{ name: string; logo?: string; nav?: NavConfig }> = ({ name, logo, nav }) => {\n return (\n <header class=\"topbar\" role=\"banner\">\n <div class=\"topbar__inner\">\n <div class=\"topbar__left\">\n <button class=\"topbar__menu\" type=\"button\" aria-label=\"Toggle navigation\" aria-expanded=\"false\" aria-controls=\"sidebar\" data-sidebar-toggle>\n <span aria-hidden=\"true\">☰</span>\n </button>\n <a class=\"brand\" href=\"/\">\n {logo ? <img class=\"logo\" src={logo} alt={name} width=\"28\" height=\"28\" /> : <span class=\"brand__mark\" aria-hidden=\"true\">◆</span>}\n <span class=\"brand__name\">{name}</span>\n </a>\n {nav?.projectSlug ? (\n <nav class=\"project-crumb\" aria-label=\"Project\">\n <span class=\"project-crumb__sep\" aria-hidden=\"true\">\n /\n </span>\n <a href={`/projects/${nav.projectSlug}/builds`}>{nav.projectName ?? nav.projectSlug}</a>\n </nav>\n ) : null}\n </div>\n <div class=\"topbar__right\">\n <ThemeToggle />\n <AuthMenu />\n </div>\n </div>\n </header>\n );\n};\n\n// eslint-disable-next-line promise-function-async -- JSX component return type\nconst ThemeToggle: FC = () => {\n return (\n <button class=\"topbar__theme\" type=\"button\" aria-label=\"Toggle theme\" data-theme-toggle>\n <span data-theme-icon=\"light\" aria-hidden=\"true\">\n ☀\n </span>\n <span data-theme-icon=\"dark\" aria-hidden=\"true\">\n ☾\n </span>\n <span data-theme-icon=\"system\" aria-hidden=\"true\">\n ◐\n </span>\n </button>\n );\n};\n\n// eslint-disable-next-line promise-function-async -- JSX component return type\nconst AuthMenu: FC = () => {\n const { user, authEnabled } = getStore();\n if (!authEnabled) {\n return null;\n }\n if (!user) {\n return (\n <a class=\"topbar__login\" href=\"/auth/login\">\n Sign in\n </a>\n );\n }\n return (\n <div class=\"user-menu\">\n {user.avatarUrl ? (\n <img class=\"user-menu__avatar\" src={user.avatarUrl} alt=\"\" width=\"28\" height=\"28\" />\n ) : (\n <span class=\"user-menu__avatar user-menu__avatar--fallback\" aria-hidden=\"true\">\n {user.name.slice(0, 1).toUpperCase()}\n </span>\n )}\n <span class=\"user-menu__name\">{user.name}</span>\n <span class={`user-menu__role user-menu__role--${user.role}`}>{user.role}</span>\n <form method=\"post\" action=\"/auth/logout\">\n <button class=\"user-menu__logout\" type=\"submit\">\n Sign out\n </button>\n </form>\n </div>\n );\n};\n\n// eslint-disable-next-line promise-function-async -- JSX component return type\nconst Sidebar: FC<{ nav?: NavConfig }> = ({ nav }) => {\n return (\n <aside id=\"sidebar\" class=\"sidebar\" aria-label=\"Primary\">\n <nav class=\"sidebar__nav\">\n <a class={`sidebar__link ${nav?.active === \"projects\" ? \"sidebar__link--active\" : \"\"}`} href=\"/projects\" aria-current={nav?.active === \"projects\" ? \"page\" : undefined}>\n <span aria-hidden=\"true\">▦</span> Projects\n </a>\n {nav?.projectSlug ? (\n <>\n <div class=\"sidebar__section\">Project</div>\n <a class={`sidebar__link ${nav.active === \"builds\" ? \"sidebar__link--active\" : \"\"}`} href={`/projects/${nav.projectSlug}/builds`} aria-current={nav.active === \"builds\" ? \"page\" : undefined}>\n <span aria-hidden=\"true\">◧</span> Builds\n </a>\n <a class={`sidebar__link ${nav.active === \"jobs\" ? \"sidebar__link--active\" : \"\"}`} href={`/projects/${nav.projectSlug}/jobs`} aria-current={nav.active === \"jobs\" ? \"page\" : undefined}>\n <span aria-hidden=\"true\">◫</span> Jobs\n </a>\n <a class={`sidebar__link ${nav.active === \"labels\" ? \"sidebar__link--active\" : \"\"}`} href={`/projects/${nav.projectSlug}/labels`} aria-current={nav.active === \"labels\" ? \"page\" : undefined}>\n <span aria-hidden=\"true\">⌗</span> Labels\n </a>\n <a class={`sidebar__link ${nav.active === \"settings\" ? \"sidebar__link--active\" : \"\"}`} href={`/projects/${nav.projectSlug}/settings`} aria-current={nav.active === \"settings\" ? \"page\" : undefined}>\n <span aria-hidden=\"true\">⚙</span> Settings\n </a>\n </>\n ) : null}\n <div class=\"sidebar__section\">Resources</div>\n <a class=\"sidebar__link\" href=\"/api/v1/projects\" aria-label=\"API (JSON)\">\n <span aria-hidden=\"true\">{\"{} \"}</span> API\n </a>\n </nav>\n </aside>\n );\n};\n\nfunction themeScript(): string {\n return `\n (function(){\n var cookieName='storyshelf_theme';\n function readCookie(n){\n var m=document.cookie.match(new RegExp('(?:^|; )'+n.replace(/([.$?*|{}\\\\[\\\\]\\\\\\\\/\\\\+^])/gu,'\\\\\\\\$1')+'=([^;]*)'));\n return m?decodeURIComponent(m[1]):null;\n }\n function writeCookie(n,v){\n document.cookie=n+'='+encodeURIComponent(v)+'; Path=/; Max-Age=31536000; SameSite=Lax';\n }\n function applyTheme(v){\n var html=document.documentElement;\n if(v==='light'||v==='dark'||v==='system'){ html.setAttribute('data-theme',v); }\n else { html.setAttribute('data-theme','system'); }\n }\n var initial=readCookie(cookieName);\n if(initial){ applyTheme(initial); }\n window.__storyshelfTheme={readCookie:readCookie,writeCookie:writeCookie,applyTheme:applyTheme,cookieName:cookieName};\n })();\n `.trim();\n}\n\nfunction clientScript(): string {\n return `\n (function(){\n var theme=window.__storyshelfTheme;\n var toggle=document.querySelector('[data-theme-toggle]');\n if(toggle&&theme){\n toggle.addEventListener('click',function(){\n var cur=document.documentElement.getAttribute('data-theme')||'system';\n var next=cur==='system'?'light':cur==='light'?'dark':'system';\n theme.applyTheme(next);\n theme.writeCookie(theme.cookieName,next);\n });\n }\n var menuBtn=document.querySelector('[data-sidebar-toggle]');\n var sidebar=document.getElementById('sidebar');\n if(menuBtn&&sidebar){\n menuBtn.addEventListener('click',function(){\n var open=sidebar.classList.toggle('sidebar--open');\n menuBtn.setAttribute('aria-expanded', open ? 'true' : 'false');\n });\n }\n document.addEventListener('keydown',function(e){\n if(e.key==='Escape'&&sidebar&&sidebar.classList.contains('sidebar--open')){\n sidebar.classList.remove('sidebar--open');\n if(menuBtn) menuBtn.setAttribute('aria-expanded','false');\n menuBtn && menuBtn.focus();\n }\n });\n // HTMX after swap focus management\n document.body.addEventListener('htmx:afterSwap',function(e){\n var target=e.detail && e.detail.target;\n if(target&&target.querySelector){\n var focusable=target.querySelector('[autofocus], input, select, textarea, button');\n if(focusable) try{ focusable.focus(); }catch(_){}\n }\n });\n // Diff keyboard shortcuts\n document.addEventListener('keydown',function(e){\n if(e.target instanceof HTMLElement && (e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA'||e.target.isContentEditable)) return;\n var diffRoot=document.querySelector('[data-diff-nav]');\n if(!diffRoot) return;\n if(e.key==='ArrowLeft' || e.key==='ArrowRight'){\n var links=Array.from(diffRoot.querySelectorAll('[data-snapshot-link]'));\n if(links.length===0) return;\n var active=document.activeElement;\n var idx=links.indexOf(active);\n if(idx===-1){\n var current=diffRoot.getAttribute('data-current');\n idx=links.findIndex(function(a){ return a.getAttribute('data-snapshot-id')===current; });\n }\n var nextIdx=e.key==='ArrowRight' ? Math.min(links.length-1, idx+1) : Math.max(0, idx-1);\n if(links[nextIdx]){ e.preventDefault(); links[nextIdx].focus(); links[nextIdx].click(); }\n }\n });\n })();\n `.trim();\n}\n"],"mappings":"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"}
@@ -2,6 +2,7 @@ import "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  //#region src/ui/styles.ts
5
+ /** Build the global CSS stylesheet from the light and dark brand themes. */
5
6
  function baseStyle(light, dark) {
6
7
  return `
7
8
  :root {