aimeat 1.16.2 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/dist/.env.example +6 -1
  2. package/dist/locales/en.json +166 -4
  3. package/dist/locales/fi.json +166 -4
  4. package/dist/public/components/Card.js +17 -3
  5. package/dist/public/components/Collapsible.js +39 -0
  6. package/dist/public/components/CopyButton.js +55 -24
  7. package/dist/public/components/DataTable.js +57 -0
  8. package/dist/public/components/EmptyState.js +28 -0
  9. package/dist/public/components/KeyValueRow.js +37 -0
  10. package/dist/public/components/Modal.js +22 -5
  11. package/dist/public/components/Pagination.js +70 -0
  12. package/dist/public/components/SearchBar.js +44 -0
  13. package/dist/public/components/StatusDot.js +29 -0
  14. package/dist/public/components/TagList.js +53 -0
  15. package/dist/public/components/ToggleSwitch.js +28 -0
  16. package/dist/public/components/index.js +41 -1
  17. package/dist/public/css/components/tags.css +24 -0
  18. package/dist/public/css/style-testbed.css +135 -0
  19. package/dist/public/css/theme.css +556 -34
  20. package/dist/public/css/views/admin-agent-integration.css +2 -33
  21. package/dist/public/css/views/admin.css +730 -686
  22. package/dist/public/css/views/agents-detail.css +402 -14
  23. package/dist/public/css/views/help.css +2 -1
  24. package/dist/public/css/views/hobbies.css +53 -33
  25. package/dist/public/css/views/marketplace.css +73 -60
  26. package/dist/public/css/views/openclaw.css +12 -6
  27. package/dist/public/css/views/portal-classic.css +34 -26
  28. package/dist/public/css/views/portal-dev.css +41 -32
  29. package/dist/public/css/views/portal.css +14 -18
  30. package/dist/public/css/views/profile.css +252 -164
  31. package/dist/public/css/views/public-knowledge-viewer.css +18 -6
  32. package/dist/public/css/views/scheduler.css +100 -0
  33. package/dist/public/js/format.js +41 -0
  34. package/dist/public/js/services/agent-tasks.js +14 -1
  35. package/dist/public/js/services/generator-spec-validate.js +5 -1
  36. package/dist/public/js/services/schedules.js +53 -0
  37. package/dist/public/js/style-testbed.js +170 -0
  38. package/dist/public/js/theme.js +114 -0
  39. package/dist/public/spa.html +74 -16
  40. package/dist/public/style-testbed.html +420 -0
  41. package/dist/public/views/admin/agent-integration-tab.js +23 -38
  42. package/dist/public/views/admin/agent-tasks-tab.js +14 -21
  43. package/dist/public/views/admin/chat-instances-tab.js +1 -1
  44. package/dist/public/views/admin/config-tab.js +7 -2
  45. package/dist/public/views/admin/cortex-tab.js +20 -14
  46. package/dist/public/views/admin/csm-tab.js +22 -18
  47. package/dist/public/views/admin/directory-tab.js +3 -3
  48. package/dist/public/views/admin/email-tab.js +23 -28
  49. package/dist/public/views/admin/federation-tab.js +15 -5
  50. package/dist/public/views/admin/generator-debug-tab.js +10 -8
  51. package/dist/public/views/admin/genesis-tab.js +4 -4
  52. package/dist/public/views/admin/ghii-tab.js +3 -3
  53. package/dist/public/views/admin/knowledge-tab.js +15 -5
  54. package/dist/public/views/admin/matching-tab.js +3 -3
  55. package/dist/public/views/admin/memory-tab.js +16 -20
  56. package/dist/public/views/admin/msm-tab.js +30 -17
  57. package/dist/public/views/admin/overview-tab.js +7 -7
  58. package/dist/public/views/admin/packages-tab.js +10 -9
  59. package/dist/public/views/admin/portal-tab.js +14 -4
  60. package/dist/public/views/admin/prompts-tab.js +5 -5
  61. package/dist/public/views/admin/push-tab.js +15 -4
  62. package/dist/public/views/admin/realtime-tab.js +3 -3
  63. package/dist/public/views/admin/services-tab.js +18 -27
  64. package/dist/public/views/admin/shared.js +48 -58
  65. package/dist/public/views/admin/sharing-groups-tab.js +5 -10
  66. package/dist/public/views/admin/stats-tab.js +3 -3
  67. package/dist/public/views/guides.js +9 -13
  68. package/dist/public/views/help.js +3 -24
  69. package/dist/public/views/hobbies.js +47 -41
  70. package/dist/public/views/marketplace.js +42 -34
  71. package/dist/public/views/openclaw.js +0 -1
  72. package/dist/public/views/portal-classic.js +4 -15
  73. package/dist/public/views/portal-dev.js +28 -30
  74. package/dist/public/views/portal.js +16 -21
  75. package/dist/public/views/portfolio.js +11 -12
  76. package/dist/public/views/profile/access-tab.js +250 -1
  77. package/dist/public/views/profile/agents/agent-card.js +75 -4
  78. package/dist/public/views/profile/agents/tab-activity.js +15 -13
  79. package/dist/public/views/profile/agents/tab-agent-config.js +60 -6
  80. package/dist/public/views/profile/agents/tab-integration.js +37 -60
  81. package/dist/public/views/profile/agents/tab-messages.js +10 -1
  82. package/dist/public/views/profile/agents/tab-quality.js +25 -19
  83. package/dist/public/views/profile/agents/tab-schedules.js +110 -0
  84. package/dist/public/views/profile/agents-activity-subtab.js +4 -4
  85. package/dist/public/views/profile/agents-services-subtab.js +5 -4
  86. package/dist/public/views/profile/agents-tab.js +141 -37
  87. package/dist/public/views/profile/agents-tasks-subtab.js +221 -26
  88. package/dist/public/views/profile/calibrator-batch.js +5 -11
  89. package/dist/public/views/profile/calibrator-tab.js +4 -9
  90. package/dist/public/views/profile/chat-sessions-tab.js +9 -5
  91. package/dist/public/views/profile/email-tab.js +2 -2
  92. package/dist/public/views/profile/extensions-tab.js +27 -22
  93. package/dist/public/views/profile/federation-tab.js +4 -1
  94. package/dist/public/views/profile/foundry-detail.js +22 -23
  95. package/dist/public/views/profile/foundry-tab.js +12 -13
  96. package/dist/public/views/profile/generator-dashboard/DebugPanel.js +4 -7
  97. package/dist/public/views/profile/generator-dashboard/use-dashboard-core.js +4 -1
  98. package/dist/public/views/profile/generator-dashboard/use-edit-mode.js +3 -2
  99. package/dist/public/views/profile/generator-detail.js +26 -30
  100. package/dist/public/views/profile/generator-tab.js +5 -5
  101. package/dist/public/views/profile/knowledge-tab.js +12 -12
  102. package/dist/public/views/profile/landing-page.js +100 -149
  103. package/dist/public/views/profile/memory-tab.js +15 -11
  104. package/dist/public/views/profile/node-stats-tab.js +3 -3
  105. package/dist/public/views/profile/nodes-tab.js +10 -3
  106. package/dist/public/views/profile/notifications-tab.js +3 -3
  107. package/dist/public/views/profile/packages-tab.js +14 -14
  108. package/dist/public/views/profile/scheduler-tab.js +340 -0
  109. package/dist/public/views/profile/services-tab.js +1 -1
  110. package/dist/public/views/profile/shared.js +16 -12
  111. package/dist/public/views/profile/wallet-tab.js +27 -50
  112. package/dist/public/views/profile/work-tab.js +29 -31
  113. package/dist/public/views/profile.js +24 -0
  114. package/dist/public/views/public-knowledge-viewer.js +9 -18
  115. package/dist/src/auth/middleware.d.ts +3 -2
  116. package/dist/src/auth/middleware.d.ts.map +1 -1
  117. package/dist/src/auth/middleware.js +78 -3
  118. package/dist/src/auth/middleware.js.map +1 -1
  119. package/dist/src/cli/init-wizard.d.ts.map +1 -1
  120. package/dist/src/cli/init-wizard.js +13 -1
  121. package/dist/src/cli/init-wizard.js.map +1 -1
  122. package/dist/src/config.d.ts +6 -0
  123. package/dist/src/config.d.ts.map +1 -1
  124. package/dist/src/config.js +12 -1
  125. package/dist/src/config.js.map +1 -1
  126. package/dist/src/generated/api-types.d.ts +708 -8
  127. package/dist/src/generated/api-types.d.ts.map +1 -1
  128. package/dist/src/mcp/agent-schedules.d.ts +23 -0
  129. package/dist/src/mcp/agent-schedules.d.ts.map +1 -0
  130. package/dist/src/mcp/agent-schedules.js +175 -0
  131. package/dist/src/mcp/agent-schedules.js.map +1 -0
  132. package/dist/src/mcp/annotations.d.ts.map +1 -1
  133. package/dist/src/mcp/annotations.js +6 -0
  134. package/dist/src/mcp/annotations.js.map +1 -1
  135. package/dist/src/mcp/catalog/definitions.d.ts.map +1 -1
  136. package/dist/src/mcp/catalog/definitions.js +55 -0
  137. package/dist/src/mcp/catalog/definitions.js.map +1 -1
  138. package/dist/src/mcp/catalog/scopes.d.ts.map +1 -1
  139. package/dist/src/mcp/catalog/scopes.js +12 -0
  140. package/dist/src/mcp/catalog/scopes.js.map +1 -1
  141. package/dist/src/mcp/catalog/surfaces.d.ts.map +1 -1
  142. package/dist/src/mcp/catalog/surfaces.js +2 -0
  143. package/dist/src/mcp/catalog/surfaces.js.map +1 -1
  144. package/dist/src/mcp/index.d.ts.map +1 -1
  145. package/dist/src/mcp/index.js +2 -0
  146. package/dist/src/mcp/index.js.map +1 -1
  147. package/dist/src/models/agent-task-schemas.d.ts +6 -0
  148. package/dist/src/models/agent-task-schemas.d.ts.map +1 -1
  149. package/dist/src/models/agent-task-schemas.js +6 -0
  150. package/dist/src/models/agent-task-schemas.js.map +1 -1
  151. package/dist/src/models/schemas.d.ts +1 -0
  152. package/dist/src/models/schemas.d.ts.map +1 -1
  153. package/dist/src/models/schemas.js +4 -0
  154. package/dist/src/models/schemas.js.map +1 -1
  155. package/dist/src/routes/access-tokens.d.ts +17 -0
  156. package/dist/src/routes/access-tokens.d.ts.map +1 -0
  157. package/dist/src/routes/access-tokens.js +164 -0
  158. package/dist/src/routes/access-tokens.js.map +1 -0
  159. package/dist/src/routes/agent-tasks.d.ts +2 -0
  160. package/dist/src/routes/agent-tasks.d.ts.map +1 -1
  161. package/dist/src/routes/agent-tasks.js +118 -23
  162. package/dist/src/routes/agent-tasks.js.map +1 -1
  163. package/dist/src/routes/agents.d.ts.map +1 -1
  164. package/dist/src/routes/agents.js +46 -0
  165. package/dist/src/routes/agents.js.map +1 -1
  166. package/dist/src/routes/ai.d.ts +6 -9
  167. package/dist/src/routes/ai.d.ts.map +1 -1
  168. package/dist/src/routes/ai.js +26 -175
  169. package/dist/src/routes/ai.js.map +1 -1
  170. package/dist/src/routes/apps.d.ts.map +1 -1
  171. package/dist/src/routes/apps.js +104 -34
  172. package/dist/src/routes/apps.js.map +1 -1
  173. package/dist/src/routes/auth.d.ts.map +1 -1
  174. package/dist/src/routes/auth.js +77 -4
  175. package/dist/src/routes/auth.js.map +1 -1
  176. package/dist/src/routes/cortex.d.ts +2 -1
  177. package/dist/src/routes/cortex.d.ts.map +1 -1
  178. package/dist/src/routes/cortex.js +13 -8
  179. package/dist/src/routes/cortex.js.map +1 -1
  180. package/dist/src/routes/extensions.d.ts.map +1 -1
  181. package/dist/src/routes/extensions.js +85 -51
  182. package/dist/src/routes/extensions.js.map +1 -1
  183. package/dist/src/routes/generator.d.ts.map +1 -1
  184. package/dist/src/routes/generator.js +109 -43
  185. package/dist/src/routes/generator.js.map +1 -1
  186. package/dist/src/routes/ghii.d.ts.map +1 -1
  187. package/dist/src/routes/ghii.js +34 -13
  188. package/dist/src/routes/ghii.js.map +1 -1
  189. package/dist/src/routes/libs.d.ts +15 -0
  190. package/dist/src/routes/libs.d.ts.map +1 -1
  191. package/dist/src/routes/libs.js +178 -55
  192. package/dist/src/routes/libs.js.map +1 -1
  193. package/dist/src/routes/portal.d.ts.map +1 -1
  194. package/dist/src/routes/portal.js +1 -0
  195. package/dist/src/routes/portal.js.map +1 -1
  196. package/dist/src/routes/schedules.d.ts +33 -0
  197. package/dist/src/routes/schedules.d.ts.map +1 -0
  198. package/dist/src/routes/schedules.js +380 -0
  199. package/dist/src/routes/schedules.js.map +1 -0
  200. package/dist/src/server-bootstrap/config-init.js +2 -2
  201. package/dist/src/server-bootstrap/config-init.js.map +1 -1
  202. package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
  203. package/dist/src/server-bootstrap/routes-loader.js +7 -0
  204. package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
  205. package/dist/src/server-bootstrap/service-init.d.ts.map +1 -1
  206. package/dist/src/server-bootstrap/service-init.js +2 -1
  207. package/dist/src/server-bootstrap/service-init.js.map +1 -1
  208. package/dist/src/services/access-token.d.ts +28 -0
  209. package/dist/src/services/access-token.d.ts.map +1 -0
  210. package/dist/src/services/access-token.js +37 -0
  211. package/dist/src/services/access-token.js.map +1 -0
  212. package/dist/src/services/ai-completion.d.ts +83 -0
  213. package/dist/src/services/ai-completion.d.ts.map +1 -0
  214. package/dist/src/services/ai-completion.js +187 -0
  215. package/dist/src/services/ai-completion.js.map +1 -0
  216. package/dist/src/services/config-schema.d.ts.map +1 -1
  217. package/dist/src/services/config-schema.js +3 -0
  218. package/dist/src/services/config-schema.js.map +1 -1
  219. package/dist/src/services/generator-prompt-seeds.d.ts.map +1 -1
  220. package/dist/src/services/generator-prompt-seeds.js +6 -3
  221. package/dist/src/services/generator-prompt-seeds.js.map +1 -1
  222. package/dist/src/services/generator-prompts/spec-validate.d.ts.map +1 -1
  223. package/dist/src/services/generator-prompts/spec-validate.js +6 -2
  224. package/dist/src/services/generator-prompts/spec-validate.js.map +1 -1
  225. package/dist/src/services/generator-registration.d.ts +12 -4
  226. package/dist/src/services/generator-registration.d.ts.map +1 -1
  227. package/dist/src/services/generator-registration.js +71 -0
  228. package/dist/src/services/generator-registration.js.map +1 -1
  229. package/dist/src/services/generator-validate.d.ts +10 -0
  230. package/dist/src/services/generator-validate.d.ts.map +1 -1
  231. package/dist/src/services/generator-validate.js +180 -36
  232. package/dist/src/services/generator-validate.js.map +1 -1
  233. package/dist/src/services/owner-session.d.ts +43 -0
  234. package/dist/src/services/owner-session.d.ts.map +1 -0
  235. package/dist/src/services/owner-session.js +175 -0
  236. package/dist/src/services/owner-session.js.map +1 -0
  237. package/dist/src/services/schedule-constraints.d.ts +65 -0
  238. package/dist/src/services/schedule-constraints.d.ts.map +1 -0
  239. package/dist/src/services/schedule-constraints.js +106 -0
  240. package/dist/src/services/schedule-constraints.js.map +1 -0
  241. package/dist/src/services/scheduler.d.ts +34 -0
  242. package/dist/src/services/scheduler.d.ts.map +1 -1
  243. package/dist/src/services/scheduler.js +266 -22
  244. package/dist/src/services/scheduler.js.map +1 -1
  245. package/dist/src/services/skill-bundle/generator.js +19 -0
  246. package/dist/src/services/skill-bundle/generator.js.map +1 -1
  247. package/dist/src/storage/interface.d.ts +48 -4
  248. package/dist/src/storage/interface.d.ts.map +1 -1
  249. package/dist/src/storage/interface.js.map +1 -1
  250. package/dist/src/storage/providers/mongodb/index.d.ts +31 -0
  251. package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
  252. package/dist/src/storage/providers/mongodb/index.js +187 -9
  253. package/dist/src/storage/providers/mongodb/index.js.map +1 -1
  254. package/dist/src/storage/providers/sqlite/index.d.ts +31 -0
  255. package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
  256. package/dist/src/storage/providers/sqlite/index.js +131 -8
  257. package/dist/src/storage/providers/sqlite/index.js.map +1 -1
  258. package/dist/src/storage/providers/sqlite/repos/agent-task.d.ts.map +1 -1
  259. package/dist/src/storage/providers/sqlite/repos/agent-task.js +6 -4
  260. package/dist/src/storage/providers/sqlite/repos/agent-task.js.map +1 -1
  261. package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
  262. package/dist/src/storage/providers/sqlite/schema.js +79 -8
  263. package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
  264. package/dist/src/storage/repositories/pat.repository.d.ts +40 -0
  265. package/dist/src/storage/repositories/pat.repository.d.ts.map +1 -0
  266. package/dist/src/storage/repositories/pat.repository.js +14 -0
  267. package/dist/src/storage/repositories/pat.repository.js.map +1 -0
  268. package/dist/src/storage/repositories/scheduler.repository.d.ts +2 -0
  269. package/dist/src/storage/repositories/scheduler.repository.d.ts.map +1 -1
  270. package/dist/src/storage/repositories/session.repository.d.ts +51 -0
  271. package/dist/src/storage/repositories/session.repository.d.ts.map +1 -1
  272. package/dist/src/storage/repositories/session.repository.js +12 -0
  273. package/dist/src/storage/repositories/session.repository.js.map +1 -1
  274. package/dist/src/utils/env-config.d.ts.map +1 -1
  275. package/dist/src/utils/env-config.js +25 -1
  276. package/dist/src/utils/env-config.js.map +1 -1
  277. package/dist/src/utils/env-validator.d.ts.map +1 -1
  278. package/dist/src/utils/env-validator.js +4 -0
  279. package/dist/src/utils/env-validator.js.map +1 -1
  280. package/package.json +1 -1
  281. package/prisma/schema.prisma +48 -0
@@ -0,0 +1,57 @@
1
+ import { h } from 'preact';
2
+ import htm from 'htm';
3
+ const html = htm.bind(h);
4
+
5
+ /**
6
+ * @file DataTable.js
7
+ * @description Canonical generic table primitive — renders a plain
8
+ * `<table class="data-table ${className}">` (optionally inside a `.scrollable`
9
+ * wrapper) backed by the shared `.data-table` block in theme.css, so the table
10
+ * look (header row, cell padding, borders, hover) is one tokenized source of
11
+ * truth that flips correctly in dark mode. Unlike the admin `DataTable` (which
12
+ * wraps this in an `.adm-card`), this component renders NO card/container — the
13
+ * caller supplies its own wrapper (`.card`, `.adm-card`, etc.).
14
+ * @structure DataTable({ headers, rows, scroll, className })
15
+ * - headers: array of column header cells (string or VNode)
16
+ * - rows: array of rows; each row is an array of cells. A cell may be:
17
+ * • a primitive/VNode → rendered as plain `<td>`
18
+ * • `{ text, mono?, title?, _html? }` → `<td class="mono"? title=...>`;
19
+ * when `_html: true`, `text` is rendered as raw HTML (see SECURITY).
20
+ * - scroll: when truthy, wraps the table in `<div class="scrollable">`
21
+ * - className: extra class(es) appended to `.data-table` (e.g. for column-width
22
+ * or per-view tweaks)
23
+ * @usage
24
+ * import { DataTable } from '/components/index.js';
25
+ * html`<div class="card"><${DataTable} headers=${['Key','Value']} rows=${rows} /></div>`
26
+ *
27
+ * SECURITY: When a cell object has `_html: true`, `cell.text` is rendered as raw
28
+ * HTML via dangerouslySetInnerHTML. Callers MUST ensure `cell.text` is sanitized
29
+ * (use escHtml() for any user-generated content). Only use `_html` for trusted,
30
+ * server-generated markup like badges.
31
+ *
32
+ * @version-history
33
+ * v1.0.0 — 2026-06-02 — Component unification (#13): created canonical generic
34
+ * DataTable (same _html/mono cell protocol as admin's), backing the shared
35
+ * .data-table CSS. The admin shared.js DataTable now wraps this in .adm-card.
36
+ */
37
+ export function DataTable({ headers, rows, scroll, className }) {
38
+ const cls = `data-table${className ? ` ${className}` : ''}`;
39
+ const table = html`<table class=${cls}>
40
+ <thead><tr>${headers.map(hd => html`<th>${hd}</th>`)}</tr></thead>
41
+ <tbody>
42
+ ${rows.map(row => html`<tr>
43
+ ${row.map(cell => {
44
+ if (cell && typeof cell === 'object' && cell._html) {
45
+ return html`<td class=${cell.mono ? 'mono' : ''} title=${cell.title || ''}
46
+ dangerouslySetInnerHTML=${{ __html: cell.text }}></td>`;
47
+ }
48
+ if (cell && typeof cell === 'object' && cell.mono) {
49
+ return html`<td class="mono" title=${cell.title || ''}>${cell.text}</td>`;
50
+ }
51
+ return html`<td>${cell}</td>`;
52
+ })}
53
+ </tr>`)}
54
+ </tbody>
55
+ </table>`;
56
+ return scroll ? html`<div class="scrollable">${table}</div>` : table;
57
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @file EmptyState.js
3
+ * @description Canonical empty-state placeholder — a centered muted message with an
4
+ * optional icon. Replaces the per-view clones (pf-empty / mk-empty / hb-empty /
5
+ * agd-empty / pkg-empty …) and the bare `<div class="empty">` scattered across the
6
+ * frontend. Renders the themed `.empty` class so it flips with dark/light mode.
7
+ * @structure EmptyState({ icon?, text, children? })
8
+ * @usage import { EmptyState } from '/components/EmptyState.js';
9
+ * html`<${EmptyState} text=${t('x.empty')} />`
10
+ * html`<${EmptyState} icon="🎯" text=${t('x.noMatches')} />`
11
+ * @version-history
12
+ * v1.0.0 — 2026-06-02 — Component unification (#9): single empty-state primitive
13
+ * (themed .empty + optional .empty-icon); admin Empty() delegates to it.
14
+ */
15
+ import { h } from 'preact';
16
+ import htm from 'htm';
17
+ const html = htm.bind(h);
18
+
19
+ /**
20
+ * EmptyState — centered muted placeholder.
21
+ * @param {{ icon?: string, text?: any, children?: any }} props
22
+ */
23
+ export function EmptyState({ icon, text, children }) {
24
+ return html`<div class="empty">
25
+ ${icon ? html`<span class="empty-icon">${icon}</span>` : ''}
26
+ ${text != null ? text : children}
27
+ </div>`;
28
+ }
@@ -0,0 +1,37 @@
1
+ import { h } from 'preact';
2
+ import htm from 'htm';
3
+ const html = htm.bind(h);
4
+
5
+ /**
6
+ * @file KeyValueRow.js
7
+ * @description Canonical key/value detail row — a label/value pair laid out as
8
+ * flex space-between with a muted label and an emphasized value, separated by
9
+ * a subtle bottom border. Backed by `.kv-row` / `.kv-label` / `.kv-value` in
10
+ * theme.css; all colors come from theme tokens so it flips correctly in dark
11
+ * mode. Consolidates the common label+value-pair shape (the admin .adm-erow /
12
+ * .adm-hrow and the profile .pf-agd-info-row families). The existing per-view
13
+ * classes are intentionally left in place where their look differs (admin rows
14
+ * are mono/bordered, the profile agent-detail rows are borderless) — use this
15
+ * for NEW detail lists rather than reinventing the markup.
16
+ * @structure KeyValueRow({ label, value, badge?, mono? }) — renders
17
+ * `<div class="kv-row"><span class="kv-label">${label}</span>
18
+ * <span class="kv-value[ mono]">${value | badge}</span></div>`.
19
+ * - `mono` — render the value in the monospace font (for ids, hashes, amounts).
20
+ * - `badge` — a semantic badge tone (success|warn|danger|info); when set, the
21
+ * value is wrapped in a `.badge badge-${badge}` chip instead of plain text.
22
+ * @usage
23
+ * import { KeyValueRow } from '/components/index.js';
24
+ * html`<${KeyValueRow} label=${t('field.id')} value=${id} mono />`
25
+ * html`<${KeyValueRow} label=${t('field.status')} value=${status} badge="success" />`
26
+ * @version-history
27
+ * v1.0.0 — 2026-06-02 — Component unification (#21): created canonical
28
+ * key/value detail row backed by .kv-row in theme.css.
29
+ */
30
+ export function KeyValueRow({ label, value, badge, mono }) {
31
+ return html`<div class="kv-row">
32
+ <span class="kv-label">${label}</span>
33
+ <span class="kv-value${mono ? ' mono' : ''}">${
34
+ badge ? html`<span class="badge badge-${badge}">${value}</span>` : value
35
+ }</span>
36
+ </div>`;
37
+ }
@@ -11,6 +11,13 @@
11
11
  * v1.0.0 — 2026-03-10 — Initial Modal component
12
12
  * v1.1.0 — 2026-03-14 — Add ConfirmDialog with danger variant
13
13
  * v1.2.0 — 2026-03-16 — Add useConfirm hook for easy confirm() replacement
14
+ * v1.3.0 — 2026-06-02 — Fix: memoise useConfirm's ConfirmUI (useCallback) so it
15
+ * keeps a stable component identity. Previously it was redefined every render,
16
+ * so any re-render of the host (e.g. the agents list polling) unmounted +
17
+ * remounted the open dialog, causing it to strobe/flicker.
18
+ * v1.4.0 — 2026-06-02 — Add optional `className` prop on Modal (applied to the
19
+ * .modal box) so views can pass width modifiers (e.g. scope-modal,
20
+ * ext-modal-narrow) when migrating their inline overlays to <Modal> (#2).
14
21
  */
15
22
  import { h } from 'preact';
16
23
  import { useState, useEffect, useCallback } from 'preact/hooks';
@@ -20,9 +27,10 @@ import { t } from '/js/i18n.js';
20
27
 
21
28
  /**
22
29
  * Modal — overlay dialog with close on Escape and backdrop click.
23
- * @param {{ open: boolean, onClose: () => void, title?: string, children: any }} props
30
+ * @param {{ open: boolean, onClose: () => void, title?: string, className?: string, children: any }} props
31
+ * - className: optional extra class on the .modal box (e.g. a width modifier)
24
32
  */
25
- export function Modal({ open, onClose, title, children }) {
33
+ export function Modal({ open, onClose, title, className = '', children }) {
26
34
  useEffect(() => {
27
35
  if (!open) return;
28
36
  const handler = (e) => { if (e.key === 'Escape') onClose(); };
@@ -37,7 +45,7 @@ export function Modal({ open, onClose, title, children }) {
37
45
  if (!open) return null;
38
46
  return html`
39
47
  <div class="modal-overlay" onClick=${onBackdrop}>
40
- <div class="modal">
48
+ <div class="modal ${className}">
41
49
  <div class="modal-header">
42
50
  ${title && html`<h3>${title}</h3>`}
43
51
  <button class="modal-close" onClick=${onClose}>✕</button>
@@ -103,7 +111,16 @@ export function useConfirm() {
103
111
 
104
112
  const close = useCallback(() => setState(null), []);
105
113
 
106
- function ConfirmUI() {
114
+ // ConfirmUI MUST keep a stable identity across unrelated re-renders of the
115
+ // host component (e.g. the agents list polls every ~5-10s and refreshes on
116
+ // SSE live-updates while a delete confirm is open). When ConfirmUI was a plain
117
+ // function redefined on every render, Preact saw a new component *type* each
118
+ // time and unmounted+remounted the whole dialog — re-initialising its
119
+ // backdrop-filter overlay and making it strobe (appear/disappear rapidly
120
+ // during a burst of re-renders). Memoising on [state, close] means the
121
+ // reference only changes when the dialog actually opens/closes, so polling no
122
+ // longer remounts it.
123
+ const ConfirmUI = useCallback(() => {
107
124
  if (!state) return null;
108
125
  return html`<${ConfirmDialog}
109
126
  open=${true}
@@ -115,7 +132,7 @@ export function useConfirm() {
115
132
  cancelLabel=${state.cancelLabel}
116
133
  danger=${state.danger}
117
134
  />`;
118
- }
135
+ }, [state, close]);
119
136
 
120
137
  return { confirm, ConfirmUI };
121
138
  }
@@ -0,0 +1,70 @@
1
+ import { h } from 'preact';
2
+ import htm from 'htm';
3
+ const html = htm.bind(h);
4
+
5
+ /**
6
+ * @file Pagination.js
7
+ * @description Canonical pagination primitives backed by `.pagination` /
8
+ * `.load-more` in theme.css. Consolidates the near-identical per-view
9
+ * prev/page/next clones (.adm-mem-pagination, and the prev/"Page X"/next
10
+ * shape of .mk-pagination) and the "Load more" button pattern. All colors
11
+ * come from theme tokens so both flip correctly in dark mode.
12
+ *
13
+ * Two exports:
14
+ * - `Pagination` — a prev / "Page X / Y" / next control (arrow buttons with a
15
+ * disabled state at the ends). Matches the existing admin pagination shape.
16
+ * Does NOT render numbered page buttons (the .hb-pagination numbered variant
17
+ * is intentionally left in place — different UI).
18
+ * - `LoadMore` — a centered "load more" button (themed `.load-more` wrapper +
19
+ * canonical `.btn-outline`), with an optional loading state.
20
+ * @structure
21
+ * Pagination({ page, totalPages, onPage, prevLabel?, nextLabel?, pageLabel? })
22
+ * — `page` is 1-based; `onPage(n)` fires with the requested page. Buttons
23
+ * auto-disable at the first/last page. `pageLabel` overrides the default
24
+ * "{page} / {totalPages}" middle text (pass a string or node).
25
+ * LoadMore({ onMore, loading?, label? })
26
+ * — renders a centered `.btn-outline`; disabled + shows `label` unchanged
27
+ * while `loading` is truthy (caller supplies any "loading…" text itself).
28
+ * @usage
29
+ * import { Pagination, LoadMore } from '/components/index.js';
30
+ * html`<${Pagination} page=${page} totalPages=${pages} onPage=${go}
31
+ * prevLabel=${t('common.prev')} nextLabel=${t('common.next')} />`
32
+ * html`<${LoadMore} onMore=${loadMore} loading=${loading}
33
+ * label=${t('pkv.loadMore')} />`
34
+ * @version-history
35
+ * v1.0.0 — 2026-06-02 — Component unification (#17): canonical Pagination +
36
+ * LoadMore, backed by .pagination / .load-more in theme.css.
37
+ */
38
+
39
+ /**
40
+ * Pagination — prev / "Page X / Y" / next control with arrow buttons.
41
+ * @param {{ page: number, totalPages: number, onPage: (n:number)=>void,
42
+ * prevLabel?: string, nextLabel?: string, pageLabel?: any }} props
43
+ */
44
+ export function Pagination({ page, totalPages, onPage, prevLabel, nextLabel, pageLabel }) {
45
+ return html`<div class="pagination">
46
+ <button
47
+ class="btn-outline btn-sm"
48
+ disabled=${page <= 1}
49
+ onClick=${() => onPage(page - 1)}
50
+ >← ${prevLabel ?? 'Prev'}</button>
51
+ <span class="pagination-info">${pageLabel ?? html`${page} / ${totalPages}`}</span>
52
+ <button
53
+ class="btn-outline btn-sm"
54
+ disabled=${page >= totalPages}
55
+ onClick=${() => onPage(page + 1)}
56
+ >${nextLabel ?? 'Next'} →</button>
57
+ </div>`;
58
+ }
59
+
60
+ /**
61
+ * LoadMore — centered "load more" button.
62
+ * @param {{ onMore: ()=>void, loading?: boolean, label?: any }} props
63
+ */
64
+ export function LoadMore({ onMore, loading, label }) {
65
+ return html`<div class="load-more">
66
+ <button class="btn-outline" disabled=${!!loading} onClick=${onMore}>
67
+ ${label ?? 'Load more'}
68
+ </button>
69
+ </div>`;
70
+ }
@@ -0,0 +1,44 @@
1
+ import { h } from 'preact';
2
+ import htm from 'htm';
3
+ const html = htm.bind(h);
4
+
5
+ /**
6
+ * @file SearchBar.js
7
+ * @description Canonical search-bar shell — a flex row wrapping a single
8
+ * `type="search"` input that reuses the canonical `.input-field` (theme.css)
9
+ * for its styling. Provides one place to define the search-toolbar layout so
10
+ * per-view hand-rolled search inputs (.pkv-search, .cap-search-input,
11
+ * .pf-agd-search-input, the profile .search-bar + .input-field combo) can
12
+ * converge. The shell layout lives in `.search-bar` / `.search-bar-input`
13
+ * (theme.css); all input visuals (border, focus ring, placeholder, dark-mode
14
+ * flip) come from `.input-field`, so colors are fully token-driven.
15
+ * @structure SearchBar({ value, onInput, placeholder?, onSubmit?, ariaLabel? }) —
16
+ * renders `<div class="search-bar"><input class="input-field search-bar-input"
17
+ * type="search" .../></div>`. When `onSubmit` is supplied, pressing Enter calls
18
+ * `onSubmit(currentValue)`.
19
+ * @usage
20
+ * import { SearchBar } from '/components/index.js';
21
+ * html`<${SearchBar} value=${q} onInput=${e => setQ(e.target.value)}
22
+ * placeholder=${t('common.search')} />`
23
+ * @version-history
24
+ * v1.0.0 — 2026-06-02 — Component unification (#24): created canonical
25
+ * search-bar shell backed by .search-bar / .search-bar-input in theme.css.
26
+ */
27
+ export function SearchBar({ value, onInput, placeholder, onSubmit, ariaLabel }) {
28
+ const onKeyDown = onSubmit
29
+ ? (e) => { if (e.key === 'Enter') onSubmit(e.target.value); }
30
+ : undefined;
31
+ return html`
32
+ <div class="search-bar">
33
+ <input
34
+ class="input-field search-bar-input"
35
+ type="search"
36
+ placeholder=${placeholder}
37
+ aria-label=${ariaLabel || placeholder}
38
+ value=${value}
39
+ onInput=${onInput}
40
+ onKeyDown=${onKeyDown}
41
+ />
42
+ </div>
43
+ `;
44
+ }
@@ -0,0 +1,29 @@
1
+ import { h } from 'preact';
2
+ import htm from 'htm';
3
+ const html = htm.bind(h);
4
+
5
+ /**
6
+ * @file StatusDot.js
7
+ * @description Canonical status-indicator dot — a small colored circle whose
8
+ * color is driven by a semantic status token (online/healthy → success,
9
+ * syncing/pending → warn, error/offline → danger, idle/muted → text-muted).
10
+ * Consolidates the per-view bespoke dots (.live-dot, .peer-dot, .pn-status-dot,
11
+ * .ext-status-dot, .dv-dot) into one component backed by `.status-dot` in
12
+ * theme.css. All colors come from theme tokens so the dot flips correctly in
13
+ * dark mode.
14
+ * @structure StatusDot({ status, title? }) — renders
15
+ * `<span class="status-dot status-dot--${status}" title=${title}></span>`.
16
+ * Recognized statuses: online, active, healthy, live (→ success);
17
+ * warn, syncing, pending (→ warn); error, offline, down (→ danger);
18
+ * idle, muted (→ text-muted). `live` additionally pulses.
19
+ * @usage
20
+ * import { StatusDot } from '/components/index.js';
21
+ * html`<${StatusDot} status="online" title=${t('...')} />`
22
+ * @version-history
23
+ * v1.0.0 — 2026-06-02 — Component unification (#11): created canonical
24
+ * status-dot, replacing .live-dot / .peer-dot / .pn-status-dot /
25
+ * .ext-status-dot / .dv-dot bespoke classes.
26
+ */
27
+ export function StatusDot({ status, title }) {
28
+ return html`<span class="status-dot status-dot--${status}" title=${title}></span>`;
29
+ }
@@ -0,0 +1,53 @@
1
+ import { h } from 'preact';
2
+ import htm from 'htm';
3
+ const html = htm.bind(h);
4
+
5
+ /**
6
+ * @file TagList.js
7
+ * @description Canonical tag/chip row — renders a flex row of tag chips using the
8
+ * shared `.tag-cloud` + `.tag-pill` classes from css/components/tags.css, with an
9
+ * optional `+N` overflow pill when the list exceeds `max`. Consolidates the many
10
+ * per-view bespoke tag clones (mk-tag, hb-tag, pkv-tag, pkg-tag, kpkg-tag,
11
+ * cap-tag, file-tag, etc.) into one component whose colors come entirely from
12
+ * theme tokens, so chips flip correctly in dark mode.
13
+ * @structure TagList({ tags, max?, prefix?, onTag? }) — renders
14
+ * `<div class="tag-cloud">` containing one `<span class="tag-pill">` per tag.
15
+ * - `tags` — array of strings (falsy/empty → renders nothing).
16
+ * - `max` — when set and `tags.length > max`, only the first `max` chips are
17
+ * shown followed by a `+N` overflow pill.
18
+ * - `prefix`— optional decorator wrapping each label (e.g. `'#'` → `#foo`).
19
+ * - `onTag` — optional click handler `(tag) => void`; when provided chips get a
20
+ * `tag-pill--clickable` flag and an onClick wiring.
21
+ * @usage
22
+ * import { TagList } from '/components/index.js';
23
+ * html`<${TagList} tags=${manifest.tags} max=${5} />`
24
+ * html`<${TagList} tags=${interests} onTag=${name => goTo('search', { interest: name })} />`
25
+ * @version-history
26
+ * v1.0.0 — 2026-06-02 — Component unification (#16): created canonical tag/chip
27
+ * list backed by .tag-pill / .tag-cloud, replacing scattered bespoke tag CSS
28
+ * clones and a +N overflow pattern.
29
+ */
30
+ export function TagList({ tags, max, prefix = '', onTag }) {
31
+ const list = Array.isArray(tags) ? tags : [];
32
+ if (list.length === 0) return null;
33
+
34
+ const limited = typeof max === 'number' && list.length > max;
35
+ const shown = limited ? list.slice(0, max) : list;
36
+ const overflow = limited ? list.length - max : 0;
37
+ const clickable = typeof onTag === 'function';
38
+
39
+ const chipClass = clickable ? 'tag-pill tag-pill--clickable' : 'tag-pill';
40
+
41
+ return html`
42
+ <div class="tag-cloud">
43
+ ${shown.map((tag) => html`
44
+ <span
45
+ key=${tag}
46
+ class=${chipClass}
47
+ onClick=${clickable ? () => onTag(tag) : undefined}
48
+ >${prefix}${tag}</span>
49
+ `)}
50
+ ${overflow > 0 && html`<span class="tag-pill tag-pill--overflow">+${overflow}</span>`}
51
+ </div>
52
+ `;
53
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @file ToggleSwitch.js
3
+ * @description Canonical on/off toggle switch (checkbox styled as a sliding pill).
4
+ * Relocated from profile/shared.js (where it was a mis-placed generic primitive)
5
+ * and de-`pf-`-prefixed. Renders the themed .toggle* classes so it flips with
6
+ * dark/light mode. profile/shared.js re-exports it for back-compat.
7
+ * @structure ToggleSwitch({ checked, onChange, label? })
8
+ * @usage import { ToggleSwitch } from '/components/ToggleSwitch.js';
9
+ * html`<${ToggleSwitch} checked=${on} onChange=${e => setOn(e.target.checked)} label=${t('x')} />`
10
+ * @version-history
11
+ * v1.0.0 — 2026-06-02 — Component unification (#14): promote the toggle to /components,
12
+ * tokenize the off-state track (#D1D5DB → var(--border)) for dark mode.
13
+ */
14
+ import { h } from 'preact';
15
+ import htm from 'htm';
16
+ const html = htm.bind(h);
17
+
18
+ /**
19
+ * ToggleSwitch — accessible checkbox rendered as a sliding pill.
20
+ * @param {{ checked: boolean, onChange: (e: Event) => void, label?: any }} props
21
+ */
22
+ export function ToggleSwitch({ checked, onChange, label }) {
23
+ return html`<label class="toggle">
24
+ <input type="checkbox" checked=${checked} onChange=${onChange} class="toggle-input" />
25
+ <span class="toggle-slider"></span>
26
+ ${label && html`<span class="toggle-label">${label}</span>`}
27
+ </label>`;
28
+ }
@@ -1,8 +1,48 @@
1
+ /**
2
+ * @file index.js
3
+ * @description Barrel re-export for the canonical design-system primitives in
4
+ * /components. Prefer importing shared primitives from here (e.g.
5
+ * `import { Modal, ConfirmDialog, Markdown } from '/components/index.js'`)
6
+ * over deep-importing the individual component files.
7
+ * @version-history
8
+ * v1.8.0 — 2026-06-02 — Component unification (#23): export Collapsible — the
9
+ * canonical expand/collapse (chevron) section consolidating the per-view
10
+ * .expand-btn + .pf-chevron / .scope-advanced-toggle / .pkv-entry-arrow toggles.
11
+ * v1.7.0 — 2026-06-02 — Component unification (#13): export DataTable — the
12
+ * canonical generic table primitive (plain `<table class="data-table">`,
13
+ * same _html/mono cell protocol as admin's, no card wrapper) backing the
14
+ * shared .data-table CSS. Admin's shared.js DataTable now wraps this.
15
+ * v1.6.0 — 2026-06-02 — Component unification (#24): export SearchBar — the
16
+ * canonical search-bar shell (`type="search"` input on `.input-field`),
17
+ * consolidating the per-view hand-rolled search inputs.
18
+ * v1.5.0 — 2026-06-02 — Component unification (#21): export KeyValueRow — the
19
+ * canonical label/value detail row backed by .kv-row in theme.css.
20
+ * v1.4.0 — 2026-06-02 — Component unification (#17): export Pagination + LoadMore
21
+ * (canonical prev/page/next control and centered load-more button).
22
+ * v1.3.0 — 2026-06-02 — Component unification (#16): export TagList — the
23
+ * canonical tag/chip row (with `+N` overflow) consolidating the per-view
24
+ * bespoke tag CSS clones.
25
+ * v1.2.0 — 2026-06-02 — Component unification (#11): export StatusDot — the
26
+ * canonical status-indicator dot consolidating the per-view bespoke dots.
27
+ * v1.1.0 — 2026-06-02 — Component unification (#28): export ConfirmDialog
28
+ * (from Modal.js) and Markdown + sanitizeHref (from Markdown.js), which
29
+ * were previously reachable only via deep import.
30
+ */
1
31
  export { Alert } from './Alert.js';
2
32
  export { useToast } from './Toast.js';
3
33
  export { Spinner } from './Spinner.js';
4
- export { Modal, useConfirm } from './Modal.js';
34
+ export { EmptyState } from './EmptyState.js';
35
+ export { ToggleSwitch } from './ToggleSwitch.js';
36
+ export { Modal, ConfirmDialog, useConfirm } from './Modal.js';
5
37
  export { CopyButton } from './CopyButton.js';
6
38
  export { FormField } from './FormField.js';
7
39
  export { Card } from './Card.js';
40
+ export { StatusDot } from './StatusDot.js';
41
+ export { TagList } from './TagList.js';
42
+ export { Pagination, LoadMore } from './Pagination.js';
43
+ export { KeyValueRow } from './KeyValueRow.js';
44
+ export { SearchBar } from './SearchBar.js';
45
+ export { DataTable } from './DataTable.js';
46
+ export { Collapsible } from './Collapsible.js';
47
+ export { Markdown, sanitizeHref } from './Markdown.js';
8
48
  export { useViewCSS } from './useViewCSS.js';
@@ -1,3 +1,18 @@
1
+ /**
2
+ * @file tags.css
3
+ * @description Shared tag/chip styles — the canonical `.tag-pill` / `.tag-cloud` /
4
+ * `.tag-editor` classes backing the TagList component (/components/TagList.js).
5
+ * All colors come from theme tokens so chips flip correctly in dark mode.
6
+ * @structure .tag-cloud (flex row) · .tag-pill (chip, +active/clear/clickable/
7
+ * overflow modifiers) · .tag-editor (+pills/input) — the editable variant.
8
+ * @usage Loaded via useViewCSS / view <link>; rendered by /components/TagList.js.
9
+ * @version-history
10
+ * v1.1.0 — 2026-06-02 — Component unification (#16): add layout-only
11
+ * .tag-pill--clickable / .tag-pill--overflow flags for TagList; add file
12
+ * header. No color changes.
13
+ * v1.0.0 — initial shared tag component styles.
14
+ */
15
+
1
16
  /* ── Shared Tag Components ── */
2
17
 
3
18
  .tag-cloud {
@@ -29,6 +44,15 @@
29
44
  border-color: var(--accent, #E8564A);
30
45
  }
31
46
 
47
+ /* Layout-only modifiers — no color change vs. base .tag-pill. */
48
+ .tag-pill--clickable {
49
+ cursor: pointer;
50
+ }
51
+
52
+ .tag-pill--overflow {
53
+ cursor: default;
54
+ }
55
+
32
56
  .tag-clear {
33
57
  color: var(--text-dim, #6B7280);
34
58
  font-style: italic;
@@ -0,0 +1,135 @@
1
+ /*
2
+ * @file style-testbed.css
3
+ * @description Layout/chrome for the dev style-decision testbed
4
+ * (style-testbed.html). All page chrome uses theme.css tokens so it flips
5
+ * with the light/dark toggle. The .stb-* prefix avoids colliding with the
6
+ * real component classes being showcased. Critically, .stb-specimens.pf /
7
+ * .stb-specimens.adm RESET the profile/admin container layout (max-width,
8
+ * margin, padding) so those wrappers act purely as selector scopes for the
9
+ * clone overrides without imposing their page-container geometry.
10
+ * @version-history
11
+ * v1.0.0 — 2026-06-02 — Initial testbed layout
12
+ */
13
+
14
+ .stb {
15
+ margin: 0;
16
+ padding: 0 0 280px; /* leave room for the sticky output bar */
17
+ background: var(--bg);
18
+ color: var(--text);
19
+ font-family: var(--font, system-ui, sans-serif);
20
+ font-size: 0.9rem;
21
+ line-height: 1.45;
22
+ }
23
+
24
+ /* ── Header ── */
25
+ .stb-header {
26
+ padding: 1.25rem 1.5rem;
27
+ border-bottom: 1px solid var(--border);
28
+ background: var(--bg-card);
29
+ }
30
+ .stb-header-row {
31
+ display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap;
32
+ }
33
+ .stb-title { font-size: 1.25rem; font-weight: 800; margin: 0; }
34
+ .stb-controls { display: flex; align-items: center; gap: 0.5rem; }
35
+ .stb-theme-label { color: var(--text-dim); font-size: 0.8rem; }
36
+ .stb-theme-btn {
37
+ padding: 6px 12px; border: 1px solid var(--border); border-radius: var(--radius-sm);
38
+ background: var(--bg); color: var(--text); cursor: pointer; font-size: 0.82rem;
39
+ }
40
+ .stb-theme-btn.stb-active { border-color: var(--accent); color: var(--accent); font-weight: 600; }
41
+ .stb-intro { max-width: 1100px; margin: 0.75rem 0 0; color: var(--text); }
42
+ .stb-intro.stb-muted { color: var(--text-dim); font-size: 0.82rem; }
43
+ .stb code { font-family: var(--font-mono, monospace); font-size: 0.85em; background: var(--bg-dim); padding: 1px 5px; border-radius: 4px; }
44
+
45
+ /* ── Families ── */
46
+ .stb-main { padding: 0 1.5rem; }
47
+ .stb-family { padding: 1.5rem 0; border-bottom: 1px dashed var(--border); }
48
+ .stb-fam-title { font-size: 1.05rem; font-weight: 700; margin: 0 0 0.25rem; }
49
+ .stb-fam-desc { color: var(--text-dim); margin: 0 0 1rem; max-width: 1100px; font-size: 0.84rem; }
50
+
51
+ /* ── Variant cards (canonical + each clone) ── */
52
+ .stb-variant-row { display: flex; flex-wrap: wrap; gap: 14px; align-items: stretch; }
53
+ .stb-variant {
54
+ flex: 1 1 280px; min-width: 260px; max-width: 360px;
55
+ border: 1px solid var(--border); border-radius: var(--radius-sm);
56
+ background: var(--bg-card); padding: 12px; display: flex; flex-direction: column; gap: 8px;
57
+ }
58
+ .stb-variant-row-stats .stb-variant { max-width: 300px; }
59
+ .stb-canonical { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; }
60
+ .stb-dead { opacity: 0.92; border-style: dashed; }
61
+
62
+ .stb-variant-head { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; min-height: 22px; }
63
+ .stb-vname { font-size: 0.78rem; font-weight: 600; }
64
+ .stb-tag {
65
+ font-size: 0.62rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.4px;
66
+ padding: 1px 6px; border-radius: 10px; background: var(--bg-dim); color: var(--text-dim);
67
+ border: 1px solid var(--border); white-space: nowrap;
68
+ }
69
+ .stb-ref { background: var(--accent-subtle, var(--bg-dim)); color: var(--accent); border-color: var(--accent); }
70
+ .stb-warn { background: var(--warn-bg, var(--bg-dim)); color: var(--warn-fg, var(--text)); border-color: var(--warn-border, var(--border)); }
71
+ .stb-dead-tag { background: var(--danger-bg, var(--bg-dim)); color: var(--danger-fg, var(--text)); border-color: var(--danger-border, var(--border)); }
72
+ .stb-admin-tag { background: var(--bg-dim); color: var(--text-dim); }
73
+
74
+ /* Specimen containers */
75
+ .stb-specimens {
76
+ display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
77
+ padding: 10px; border-radius: var(--radius-sm); background: var(--bg);
78
+ border: 1px dashed var(--border); min-height: 48px;
79
+ }
80
+ .stb-specimens-col { flex-direction: column; align-items: stretch; }
81
+ .stb-specimens-relative { position: relative; }
82
+ .stb-mini-note { font-size: 0.7rem; color: var(--text-dim); }
83
+
84
+ /* SCOPE RESET — keep .pf/.adm as selector scopes, drop their page-container layout */
85
+ .stb-specimens.pf,
86
+ .stb-specimens.adm {
87
+ max-width: none !important;
88
+ margin: 0 !important;
89
+ padding: 10px !important;
90
+ position: static !important;
91
+ z-index: auto !important;
92
+ }
93
+
94
+ .stb-diff { font-size: 0.72rem; color: var(--text-dim); margin: 0; }
95
+ .stb-map { font-size: 0.72rem; color: var(--text); margin: 0; opacity: 0.9; }
96
+ .stb-map code { font-size: 0.92em; }
97
+ .stb-map.stb-map-warn { color: var(--warn-fg, var(--accent)); }
98
+
99
+ /* Decision select (injected by JS into .stb-decide-wrap) */
100
+ .stb-decide-wrap { margin-top: auto; }
101
+ .stb-decide {
102
+ width: 100%; padding: 6px 8px; font-size: 0.78rem;
103
+ border: 1px solid var(--border); border-radius: var(--radius-sm);
104
+ background: var(--bg); color: var(--text); cursor: pointer;
105
+ }
106
+ .stb-decide.stb-chosen { border-color: var(--accent); font-weight: 600; }
107
+
108
+ /* Family note */
109
+ .stb-note-wrap { display: block; margin-top: 12px; font-size: 0.8rem; color: var(--text-dim); }
110
+ .stb-note {
111
+ width: 100%; max-width: 700px; margin-top: 4px; padding: 7px 10px; font-size: 0.82rem;
112
+ border: 1px solid var(--border); border-radius: var(--radius-sm);
113
+ background: var(--bg); color: var(--text); display: block;
114
+ }
115
+
116
+ /* ── Sticky output bar ── */
117
+ .stb-output-bar {
118
+ position: fixed; left: 0; right: 0; bottom: 0; z-index: 50;
119
+ background: var(--bg-card); border-top: 2px solid var(--accent);
120
+ padding: 10px 1.5rem 12px; box-shadow: 0 -6px 20px rgba(0,0,0,0.18);
121
+ }
122
+ .stb-output-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
123
+ .stb-output-title { font-size: 0.85rem; font-weight: 700; }
124
+ .stb-output-count { color: var(--text-dim); font-weight: 500; font-size: 0.78rem; }
125
+ .stb-copy-btn {
126
+ padding: 6px 14px; border: none; border-radius: var(--radius-sm);
127
+ background: var(--accent); color: var(--on-inverted, #fff); cursor: pointer; font-size: 0.8rem; font-weight: 600;
128
+ }
129
+ .stb-copy-btn.stb-copied { background: var(--success-fg, #16a34a); }
130
+ .stb-output {
131
+ width: 100%; box-sizing: border-box; resize: vertical;
132
+ font-family: var(--font-mono, monospace); font-size: 0.78rem; line-height: 1.4;
133
+ border: 1px solid var(--border); border-radius: var(--radius-sm);
134
+ background: var(--bg); color: var(--text); padding: 8px 10px;
135
+ }