@tollerud/ui 1.1.5 → 3.1.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 (368) hide show
  1. package/AGENTS.md +36 -13
  2. package/CHANGELOG.md +411 -0
  3. package/COMPONENTS.md +951 -0
  4. package/GETTING_STARTED.md +159 -0
  5. package/README.md +51 -43
  6. package/SKILL.md +59 -24
  7. package/components.json +18 -0
  8. package/dist/accordion.d.ts +20 -0
  9. package/dist/accordion.js +5 -0
  10. package/dist/accordion.js.map +1 -0
  11. package/dist/action-diff.d.ts +26 -0
  12. package/dist/action-diff.js +5 -0
  13. package/dist/action-diff.js.map +1 -0
  14. package/dist/action-row.d.ts +36 -0
  15. package/dist/action-row.js +6 -0
  16. package/dist/action-row.js.map +1 -0
  17. package/dist/alert-inbox.d.ts +23 -0
  18. package/dist/alert-inbox.js +6 -0
  19. package/dist/alert-inbox.js.map +1 -0
  20. package/dist/alert.d.ts +33 -0
  21. package/dist/alert.js +5 -0
  22. package/dist/alert.js.map +1 -0
  23. package/dist/approval-card.d.ts +27 -0
  24. package/dist/approval-card.js +5 -0
  25. package/dist/approval-card.js.map +1 -0
  26. package/dist/area-chart.d.ts +10 -0
  27. package/dist/area-chart.js +5 -0
  28. package/dist/area-chart.js.map +1 -0
  29. package/dist/avatar.d.ts +28 -0
  30. package/dist/avatar.js +5 -0
  31. package/dist/avatar.js.map +1 -0
  32. package/dist/backup-status-panel.d.ts +25 -0
  33. package/dist/backup-status-panel.js +6 -0
  34. package/dist/backup-status-panel.js.map +1 -0
  35. package/dist/badge.d.ts +17 -0
  36. package/dist/badge.js +5 -0
  37. package/dist/badge.js.map +1 -0
  38. package/dist/bar-chart.d.ts +15 -0
  39. package/dist/bar-chart.js +5 -0
  40. package/dist/bar-chart.js.map +1 -0
  41. package/dist/bento-dashboard.d.ts +30 -0
  42. package/dist/bento-dashboard.js +10 -0
  43. package/dist/bento-dashboard.js.map +1 -0
  44. package/dist/breadcrumb.d.ts +16 -0
  45. package/dist/breadcrumb.js +5 -0
  46. package/dist/breadcrumb.js.map +1 -0
  47. package/dist/button.d.ts +29 -0
  48. package/dist/button.js +5 -0
  49. package/dist/button.js.map +1 -0
  50. package/dist/card.d.ts +10 -0
  51. package/dist/card.js +5 -0
  52. package/dist/card.js.map +1 -0
  53. package/dist/checkbox.d.ts +9 -0
  54. package/dist/checkbox.js +5 -0
  55. package/dist/checkbox.js.map +1 -0
  56. package/dist/chunk-2QWKOCWF.js +79 -0
  57. package/dist/chunk-2QWKOCWF.js.map +1 -0
  58. package/dist/chunk-3LTW224O.js +53 -0
  59. package/dist/chunk-3LTW224O.js.map +1 -0
  60. package/dist/chunk-3XTZPDNV.js +94 -0
  61. package/dist/chunk-3XTZPDNV.js.map +1 -0
  62. package/dist/chunk-435JHF7G.js +65 -0
  63. package/dist/chunk-435JHF7G.js.map +1 -0
  64. package/dist/chunk-4PA2ACNF.js +52 -0
  65. package/dist/chunk-4PA2ACNF.js.map +1 -0
  66. package/dist/chunk-5GWHUJ5D.js +29 -0
  67. package/dist/chunk-5GWHUJ5D.js.map +1 -0
  68. package/dist/chunk-6FUKJD3W.js +123 -0
  69. package/dist/chunk-6FUKJD3W.js.map +1 -0
  70. package/dist/chunk-6IS2AYYG.js +106 -0
  71. package/dist/chunk-6IS2AYYG.js.map +1 -0
  72. package/dist/chunk-6PZKU6ZL.js +78 -0
  73. package/dist/chunk-6PZKU6ZL.js.map +1 -0
  74. package/dist/chunk-6SKTH45H.js +75 -0
  75. package/dist/chunk-6SKTH45H.js.map +1 -0
  76. package/dist/chunk-6UXW5YUC.js +77 -0
  77. package/dist/chunk-6UXW5YUC.js.map +1 -0
  78. package/dist/chunk-7EP2T3OW.js +52 -0
  79. package/dist/chunk-7EP2T3OW.js.map +1 -0
  80. package/dist/chunk-7J5QXUQN.js +38 -0
  81. package/dist/chunk-7J5QXUQN.js.map +1 -0
  82. package/dist/chunk-A6L5C3IJ.js +47 -0
  83. package/dist/chunk-A6L5C3IJ.js.map +1 -0
  84. package/dist/chunk-ADE22JSR.js +54 -0
  85. package/dist/chunk-ADE22JSR.js.map +1 -0
  86. package/dist/chunk-ANW6J6PV.js +42 -0
  87. package/dist/chunk-ANW6J6PV.js.map +1 -0
  88. package/dist/chunk-APFFKNPS.js +80 -0
  89. package/dist/chunk-APFFKNPS.js.map +1 -0
  90. package/dist/chunk-AQT3FZRQ.js +23 -0
  91. package/dist/chunk-AQT3FZRQ.js.map +1 -0
  92. package/dist/chunk-AZADSX4Z.js +85 -0
  93. package/dist/chunk-AZADSX4Z.js.map +1 -0
  94. package/dist/chunk-BPCH5LJ3.js +36 -0
  95. package/dist/chunk-BPCH5LJ3.js.map +1 -0
  96. package/dist/chunk-CDI7353B.js +40 -0
  97. package/dist/chunk-CDI7353B.js.map +1 -0
  98. package/dist/chunk-CKNWXYMA.js +53 -0
  99. package/dist/chunk-CKNWXYMA.js.map +1 -0
  100. package/dist/chunk-DNJI65VQ.js +22 -0
  101. package/dist/chunk-DNJI65VQ.js.map +1 -0
  102. package/dist/chunk-DOUDJU4P.js +63 -0
  103. package/dist/chunk-DOUDJU4P.js.map +1 -0
  104. package/dist/chunk-DRCMGIQ6.js +64 -0
  105. package/dist/chunk-DRCMGIQ6.js.map +1 -0
  106. package/dist/chunk-DZOBXK2S.js +28 -0
  107. package/dist/chunk-DZOBXK2S.js.map +1 -0
  108. package/dist/chunk-EN4OJCEF.js +54 -0
  109. package/dist/chunk-EN4OJCEF.js.map +1 -0
  110. package/dist/chunk-EVHZFYWX.js +33 -0
  111. package/dist/chunk-EVHZFYWX.js.map +1 -0
  112. package/dist/chunk-G2VKWNZA.js +53 -0
  113. package/dist/chunk-G2VKWNZA.js.map +1 -0
  114. package/dist/chunk-GTM2DE4C.js +156 -0
  115. package/dist/chunk-GTM2DE4C.js.map +1 -0
  116. package/dist/chunk-H3ZVGTJM.js +165 -0
  117. package/dist/chunk-H3ZVGTJM.js.map +1 -0
  118. package/dist/chunk-HWAWUEHC.js +28 -0
  119. package/dist/chunk-HWAWUEHC.js.map +1 -0
  120. package/dist/chunk-HWJVRTWO.js +36 -0
  121. package/dist/chunk-HWJVRTWO.js.map +1 -0
  122. package/dist/chunk-ILADNTUB.js +77 -0
  123. package/dist/chunk-ILADNTUB.js.map +1 -0
  124. package/dist/chunk-IUPVQWO5.js +31 -0
  125. package/dist/chunk-IUPVQWO5.js.map +1 -0
  126. package/dist/chunk-JFOW2DI5.js +43 -0
  127. package/dist/chunk-JFOW2DI5.js.map +1 -0
  128. package/dist/chunk-JRFSUVSO.js +66 -0
  129. package/dist/chunk-JRFSUVSO.js.map +1 -0
  130. package/dist/chunk-KI6OTVID.js +91 -0
  131. package/dist/chunk-KI6OTVID.js.map +1 -0
  132. package/dist/chunk-LUM2YJBH.js +73 -0
  133. package/dist/chunk-LUM2YJBH.js.map +1 -0
  134. package/dist/chunk-NHPISZWS.js +71 -0
  135. package/dist/chunk-NHPISZWS.js.map +1 -0
  136. package/dist/chunk-NOLWJJHT.js +52 -0
  137. package/dist/chunk-NOLWJJHT.js.map +1 -0
  138. package/dist/chunk-NPVINX3Q.js +20 -0
  139. package/dist/chunk-NPVINX3Q.js.map +1 -0
  140. package/dist/chunk-NSMU66ZX.js +47 -0
  141. package/dist/chunk-NSMU66ZX.js.map +1 -0
  142. package/dist/chunk-O5SWPHUQ.js +79 -0
  143. package/dist/chunk-O5SWPHUQ.js.map +1 -0
  144. package/dist/chunk-OGVSZ7NV.js +53 -0
  145. package/dist/chunk-OGVSZ7NV.js.map +1 -0
  146. package/dist/chunk-OLHMMFQ7.js +43 -0
  147. package/dist/chunk-OLHMMFQ7.js.map +1 -0
  148. package/dist/chunk-ONMTHBZ4.js +54 -0
  149. package/dist/chunk-ONMTHBZ4.js.map +1 -0
  150. package/dist/chunk-OVSIOZHJ.js +56 -0
  151. package/dist/chunk-OVSIOZHJ.js.map +1 -0
  152. package/dist/chunk-Q54CVE3W.js +154 -0
  153. package/dist/chunk-Q54CVE3W.js.map +1 -0
  154. package/dist/chunk-QEHTPQHL.js +35 -0
  155. package/dist/chunk-QEHTPQHL.js.map +1 -0
  156. package/dist/chunk-QEIEWGHA.js +62 -0
  157. package/dist/chunk-QEIEWGHA.js.map +1 -0
  158. package/dist/chunk-QQHBEACI.js +88 -0
  159. package/dist/chunk-QQHBEACI.js.map +1 -0
  160. package/dist/chunk-RJTDQOT2.js +73 -0
  161. package/dist/chunk-RJTDQOT2.js.map +1 -0
  162. package/dist/chunk-RQ3RXKAZ.js +203 -0
  163. package/dist/chunk-RQ3RXKAZ.js.map +1 -0
  164. package/dist/chunk-RZK2S2OO.js +126 -0
  165. package/dist/chunk-RZK2S2OO.js.map +1 -0
  166. package/dist/chunk-SAP7JSSO.js +106 -0
  167. package/dist/chunk-SAP7JSSO.js.map +1 -0
  168. package/dist/chunk-T3TQPOVM.js +79 -0
  169. package/dist/chunk-T3TQPOVM.js.map +1 -0
  170. package/dist/chunk-T3UQ7G4T.js +58 -0
  171. package/dist/chunk-T3UQ7G4T.js.map +1 -0
  172. package/dist/chunk-T56TTOI6.js +53 -0
  173. package/dist/chunk-T56TTOI6.js.map +1 -0
  174. package/dist/chunk-T7EFDE2L.js +36 -0
  175. package/dist/chunk-T7EFDE2L.js.map +1 -0
  176. package/dist/chunk-VFS3V3VY.js +91 -0
  177. package/dist/chunk-VFS3V3VY.js.map +1 -0
  178. package/dist/chunk-VOARBYVQ.js +44 -0
  179. package/dist/chunk-VOARBYVQ.js.map +1 -0
  180. package/dist/chunk-WDANALHD.js +95 -0
  181. package/dist/chunk-WDANALHD.js.map +1 -0
  182. package/dist/chunk-WSQNPRGN.js +12 -0
  183. package/dist/chunk-WSQNPRGN.js.map +1 -0
  184. package/dist/chunk-YPP7QHYT.js +393 -0
  185. package/dist/chunk-YPP7QHYT.js.map +1 -0
  186. package/dist/chunk-YTU7BRDW.js +72 -0
  187. package/dist/chunk-YTU7BRDW.js.map +1 -0
  188. package/dist/chunk-YYWODLER.js +111 -0
  189. package/dist/chunk-YYWODLER.js.map +1 -0
  190. package/dist/chunk-ZOXO3G3I.js +50 -0
  191. package/dist/chunk-ZOXO3G3I.js.map +1 -0
  192. package/dist/chunk-ZTFOR3AN.js +79 -0
  193. package/dist/chunk-ZTFOR3AN.js.map +1 -0
  194. package/dist/code-block.d.ts +14 -0
  195. package/dist/code-block.js +5 -0
  196. package/dist/code-block.js.map +1 -0
  197. package/dist/combobox.d.ts +26 -0
  198. package/dist/combobox.js +5 -0
  199. package/dist/combobox.js.map +1 -0
  200. package/dist/command-menu.d.ts +52 -0
  201. package/dist/command-menu.js +7 -0
  202. package/dist/command-menu.js.map +1 -0
  203. package/dist/container.d.ts +9 -0
  204. package/dist/container.js +5 -0
  205. package/dist/container.js.map +1 -0
  206. package/dist/cta-band.d.ts +12 -0
  207. package/dist/cta-band.js +5 -0
  208. package/dist/cta-band.js.map +1 -0
  209. package/dist/data-table.d.ts +58 -0
  210. package/dist/data-table.js +12 -0
  211. package/dist/data-table.js.map +1 -0
  212. package/dist/date-picker.d.ts +20 -0
  213. package/dist/date-picker.js +5 -0
  214. package/dist/date-picker.js.map +1 -0
  215. package/dist/dialog.d.ts +21 -0
  216. package/dist/dialog.js +5 -0
  217. package/dist/dialog.js.map +1 -0
  218. package/dist/divider.d.ts +12 -0
  219. package/dist/divider.js +5 -0
  220. package/dist/divider.js.map +1 -0
  221. package/dist/docker-stack-card.d.ts +21 -0
  222. package/dist/docker-stack-card.js +6 -0
  223. package/dist/docker-stack-card.js.map +1 -0
  224. package/dist/donut.d.ts +15 -0
  225. package/dist/donut.js +5 -0
  226. package/dist/donut.js.map +1 -0
  227. package/dist/dropdown-menu.d.ts +15 -0
  228. package/dist/dropdown-menu.js +5 -0
  229. package/dist/dropdown-menu.js.map +1 -0
  230. package/dist/empty.d.ts +12 -0
  231. package/dist/empty.js +5 -0
  232. package/dist/empty.js.map +1 -0
  233. package/dist/feature-card.d.ts +11 -0
  234. package/dist/feature-card.js +6 -0
  235. package/dist/feature-card.js.map +1 -0
  236. package/dist/file-upload.d.ts +20 -0
  237. package/dist/file-upload.js +5 -0
  238. package/dist/file-upload.js.map +1 -0
  239. package/dist/footer.d.ts +35 -0
  240. package/dist/footer.js +6 -0
  241. package/dist/footer.js.map +1 -0
  242. package/dist/form-row.d.ts +19 -0
  243. package/dist/form-row.js +5 -0
  244. package/dist/form-row.js.map +1 -0
  245. package/dist/glow-card.d.ts +14 -0
  246. package/dist/glow-card.js +5 -0
  247. package/dist/glow-card.js.map +1 -0
  248. package/dist/hero-block.d.ts +16 -0
  249. package/dist/hero-block.js +7 -0
  250. package/dist/hero-block.js.map +1 -0
  251. package/dist/host-card.d.ts +27 -0
  252. package/dist/host-card.js +6 -0
  253. package/dist/host-card.js.map +1 -0
  254. package/dist/incident-card.d.ts +23 -0
  255. package/dist/incident-card.js +5 -0
  256. package/dist/incident-card.js.map +1 -0
  257. package/dist/index.d.ts +77 -960
  258. package/dist/index.js +69 -3812
  259. package/dist/index.js.map +1 -1
  260. package/dist/input.d.ts +10 -0
  261. package/dist/input.js +5 -0
  262. package/dist/input.js.map +1 -0
  263. package/dist/kbd.d.ts +24 -0
  264. package/dist/kbd.js +5 -0
  265. package/dist/kbd.js.map +1 -0
  266. package/dist/log-viewer.d.ts +35 -0
  267. package/dist/log-viewer.js +5 -0
  268. package/dist/log-viewer.js.map +1 -0
  269. package/dist/meter.d.ts +23 -0
  270. package/dist/meter.js +5 -0
  271. package/dist/meter.js.map +1 -0
  272. package/dist/monogram.d.ts +20 -0
  273. package/dist/monogram.js +5 -0
  274. package/dist/monogram.js.map +1 -0
  275. package/dist/noir-glow-background.d.ts +56 -0
  276. package/dist/noir-glow-background.js +4 -0
  277. package/dist/noir-glow-background.js.map +1 -0
  278. package/dist/pagination.d.ts +16 -0
  279. package/dist/pagination.js +5 -0
  280. package/dist/pagination.js.map +1 -0
  281. package/dist/panel.d.ts +12 -0
  282. package/dist/panel.js +5 -0
  283. package/dist/panel.js.map +1 -0
  284. package/dist/password-input.d.ts +10 -0
  285. package/dist/password-input.js +5 -0
  286. package/dist/password-input.js.map +1 -0
  287. package/dist/pill.d.ts +17 -0
  288. package/dist/pill.js +5 -0
  289. package/dist/pill.js.map +1 -0
  290. package/dist/pricing-card.d.ts +20 -0
  291. package/dist/pricing-card.js +6 -0
  292. package/dist/pricing-card.js.map +1 -0
  293. package/dist/progress.d.ts +6 -0
  294. package/dist/progress.js +5 -0
  295. package/dist/progress.js.map +1 -0
  296. package/dist/radio-group.d.ts +18 -0
  297. package/dist/radio-group.js +5 -0
  298. package/dist/radio-group.js.map +1 -0
  299. package/dist/rollback-plan.d.ts +23 -0
  300. package/dist/rollback-plan.js +5 -0
  301. package/dist/rollback-plan.js.map +1 -0
  302. package/dist/segmented.d.ts +17 -0
  303. package/dist/segmented.js +5 -0
  304. package/dist/segmented.js.map +1 -0
  305. package/dist/select.d.ts +18 -0
  306. package/dist/select.js +5 -0
  307. package/dist/select.js.map +1 -0
  308. package/dist/service-health-card.d.ts +21 -0
  309. package/dist/service-health-card.js +6 -0
  310. package/dist/service-health-card.js.map +1 -0
  311. package/dist/sheet.d.ts +25 -0
  312. package/dist/sheet.js +5 -0
  313. package/dist/sheet.js.map +1 -0
  314. package/dist/skeleton.d.ts +13 -0
  315. package/dist/skeleton.js +5 -0
  316. package/dist/skeleton.js.map +1 -0
  317. package/dist/slider.d.ts +12 -0
  318. package/dist/slider.js +5 -0
  319. package/dist/slider.js.map +1 -0
  320. package/dist/sparkline.d.ts +16 -0
  321. package/dist/sparkline.js +5 -0
  322. package/dist/sparkline.js.map +1 -0
  323. package/dist/stat-card.d.ts +15 -0
  324. package/dist/stat-card.js +5 -0
  325. package/dist/stat-card.js.map +1 -0
  326. package/dist/status-dot.d.ts +13 -0
  327. package/dist/status-dot.js +5 -0
  328. package/dist/status-dot.js.map +1 -0
  329. package/dist/stepper.d.ts +16 -0
  330. package/dist/stepper.js +5 -0
  331. package/dist/stepper.js.map +1 -0
  332. package/dist/switch.d.ts +9 -0
  333. package/dist/switch.js +5 -0
  334. package/dist/switch.js.map +1 -0
  335. package/dist/tabs.d.ts +9 -0
  336. package/dist/tabs.js +5 -0
  337. package/dist/tabs.js.map +1 -0
  338. package/dist/tag-input.d.ts +20 -0
  339. package/dist/tag-input.js +5 -0
  340. package/dist/tag-input.js.map +1 -0
  341. package/dist/textarea.d.ts +10 -0
  342. package/dist/textarea.js +5 -0
  343. package/dist/textarea.js.map +1 -0
  344. package/dist/timeline.d.ts +30 -0
  345. package/dist/timeline.js +5 -0
  346. package/dist/timeline.js.map +1 -0
  347. package/dist/toaster.d.ts +10 -0
  348. package/dist/toaster.js +4 -0
  349. package/dist/toaster.js.map +1 -0
  350. package/dist/tooltip.d.ts +12 -0
  351. package/dist/tooltip.js +5 -0
  352. package/dist/tooltip.js.map +1 -0
  353. package/dist/utils.d.ts +5 -0
  354. package/dist/utils.js +4 -0
  355. package/dist/utils.js.map +1 -0
  356. package/globals-layers.css +1019 -0
  357. package/globals-v3.css +17 -0
  358. package/globals-v4.css +2 -0
  359. package/globals.css +12 -939
  360. package/package.json +85 -17
  361. package/registry.json +936 -0
  362. package/tailwind.css +9 -0
  363. package/tokens.css +20 -0
  364. package/tollerud-avatar-full.png +0 -0
  365. package/dist/index.cjs +0 -3938
  366. package/dist/index.cjs.map +0 -1
  367. package/dist/index.d.cts +0 -960
  368. /package/{tia-full-figure.svg → tollerud-avatar-full.svg} +0 -0
package/COMPONENTS.md ADDED
@@ -0,0 +1,951 @@
1
+ # Tollerud Design System — Component Library
2
+
3
+ All components come as CSS classes (in `globals.css` / `tokens.css`) with React `.tsx` examples in `components/`.
4
+
5
+ > **Note:** As of **v1.0.9** the vast majority of components documented here ship in `@tollerud/ui`. Sections for components that are still docs-site-only or roadmap-only are marked with a ⚠️ warning; sections already in the package are marked ✅. **For the authoritative, props-complete reference use [SKILL.md](SKILL.md)** (verified against `components/index.ts`) or [COMPLETENESS_ROADMAP.md](COMPLETENESS_ROADMAP.md).
6
+
7
+ ## NoirGlowBackground
8
+
9
+ Tollerud.no-inspired animated WebGL background using `@paper-design/shaders-react`, with CSS fallback classes.
10
+
11
+ ```tsx
12
+ <section className="relative overflow-hidden bg-black">
13
+ <NoirGlowBackground
14
+ intensity="medium"
15
+ speed="slow"
16
+ grain="soft"
17
+ shape="corners"
18
+ preserveCenter
19
+ />
20
+ <div className="relative z-10">Content</div>
21
+ </section>
22
+ ```
23
+
24
+ See `BACKGROUNDS.md` for install, props, usage rules, and fallback HTML.
25
+
26
+ ## Button
27
+
28
+ | Variant | Class | React Component | Usage |
29
+ |---------|-------|----------------|-------|
30
+ | Primary | `.tollerud-btn--primary` | `<Button variant="primary">` | Main CTA |
31
+ | Secondary | `.tollerud-btn--secondary` | `<Button variant="secondary">` | Secondary action |
32
+ | Ghost | `.tollerud-btn--ghost` | `<Button variant="ghost">` | Low emphasis |
33
+ | Destructive | `.tollerud-btn--destructive` | `<Button variant="destructive">` | Delete/remove |
34
+ | Terminal | `.tollerud-btn--terminal` | `<Button variant="terminal">` | Dev tools, CLIs |
35
+
36
+ Sizes: `--sm`, `--md`, `--lg`
37
+
38
+ ```jsx
39
+ <Button variant="primary" size="md" onClick={handleClick}>
40
+ Deploy
41
+ </Button>
42
+ <Button variant="terminal" size="sm">
43
+ start_building
44
+ </Button>
45
+ ```
46
+
47
+ ## Card
48
+
49
+ | Class | React | Props |
50
+ |-------|-------|-------|
51
+ | `.tollerud-card` | `<Card>` | — |
52
+ | `.tollerud-card border-tollerud-yellow/25` | `<Card accent>` | accent: boolean |
53
+
54
+ ```jsx
55
+ <Card>
56
+ <h3 className="font-semibold mb-1">Title</h3>
57
+ <p className="text-tollerud-text-secondary text-sm">Content</p>
58
+ </Card>
59
+ <Card accent>
60
+ <p>Highlighted card with yellow border</p>
61
+ </Card>
62
+ ```
63
+
64
+ ## Badge
65
+
66
+ | Variant | Class | React |
67
+ |---------|-------|-------|
68
+ | Default | `.tollerud-badge--default` | `<Badge>` |
69
+ | Accent | `.tollerud-badge--accent` | `<Badge variant="accent">` |
70
+ | Success | `.tollerud-badge--success` | `<Badge variant="success">` |
71
+ | Error | `.tollerud-badge--error` | `<Badge variant="error">` |
72
+ | Info | `.tollerud-badge--info` | `<Badge variant="info">` |
73
+ | Warning | `.tollerud-badge--warning` | `<Badge variant="warning">` |
74
+
75
+ ```jsx
76
+ <Badge variant="success">Online</Badge>
77
+ <Badge variant="accent">New</Badge>
78
+ ```
79
+
80
+ ## StatusDot
81
+
82
+ | Status | Color | Glow |
83
+ |--------|-------|------|
84
+ | online | `#22C55E` | 6px green glow |
85
+ | offline | `#EF4444` | — |
86
+ | warning | `#E8D500` | 6px yellow glow |
87
+ | idle | `#666666` | — |
88
+
89
+ ```jsx
90
+ <StatusDot status="online" label="SSH Connected" />
91
+ <StatusDot status="warning" label="CPU 87%" />
92
+ ```
93
+
94
+ ## Input
95
+
96
+ ```jsx
97
+ <Input
98
+ label="Server Name"
99
+ placeholder="e.g. emma.tollerud.no"
100
+ error={errors.name}
101
+ />
102
+ ```
103
+
104
+ ## Pill Tag
105
+
106
+ > ✅ `Pill` ships in `@tollerud/ui >= 1.0.9`. Import: `import { Pill } from '@tollerud/ui'`
107
+
108
+
109
+ ```html
110
+ <span class="tollerud-pill tollerud-pill--outline">new</span>
111
+ <span class="tollerud-pill tollerud-pill--muted">deprecated</span>
112
+ <span class="tollerud-pill tollerud-pill--success">stable</span>
113
+ <span class="tollerud-pill tollerud-pill--error">critical</span>
114
+ ```
115
+
116
+ ## StatCard
117
+
118
+ ```jsx
119
+ <StatCard
120
+ label="Active Sessions"
121
+ value={42}
122
+ change={{ value: "+12%", direction: "up" }}
123
+ />
124
+ ```
125
+
126
+ ## CodeBlock
127
+
128
+ ```jsx
129
+ <CodeBlock>
130
+ {`$ systemctl status tollerud-agent
131
+ ● active (running)`}
132
+ </CodeBlock>
133
+ ```
134
+
135
+ ## Divider
136
+
137
+ > ✅ `Divider` ships in `@tollerud/ui >= 1.0.9`. Import: `import { Divider } from '@tollerud/ui'`
138
+
139
+
140
+ ```html
141
+ <hr class="tollerud-divider" />
142
+ <hr class="tollerud-divider--accent" />
143
+ <hr class="tollerud-accent-bar" /> <!-- gradient -->
144
+ ```
145
+
146
+ ## Kbd — Keyboard Shortcut Chip
147
+
148
+ Raycast-inspired shortcut badge for displaying key combinations.
149
+
150
+ ```tsx
151
+ <Kbd keys="⌘K" />
152
+ <Kbd keys={["⌘", "⇧", "S"]} size="sm" />
153
+ <Kbd keys="Esc" />
154
+ ```
155
+
156
+ Props:
157
+ - `keys: string | string[]` — The keys to display. Separate with `+` for chords.
158
+ - `size?: "sm" | "md"` — Small variant for inline use.
159
+
160
+ CSS class: `.tollerud-kbd`, children `.tollerud-kbd__key`.
161
+
162
+ ## ActionRow — Command / Action Item
163
+
164
+ A single command/action row used by `CommandMenu` and standalone.
165
+
166
+ ```tsx
167
+ <ActionRow
168
+ action={{
169
+ id: 'deploy',
170
+ label: 'Deploy Stack',
171
+ description: 'emma: docker-compose up -d',
172
+ icon: <Rocket />,
173
+ shortcut: '↵',
174
+ onSelect: () => deploy()
175
+ }}
176
+ highlighted={index === 0}
177
+ />
178
+ ```
179
+
180
+ Used as a controlled row with keyboard navigation via the `highlighted` prop.
181
+
182
+ ## CommandMenu — Raycast-style Command Palette
183
+
184
+ The signature keyboard-first component. Full command palette with search, groups, keyboard navigation, and overlay.
185
+
186
+ ```tsx
187
+ const [open, setOpen] = useState(false)
188
+ const servers = ['Emma', 'Miriam', 'Pia', 'Iris', 'Victoria', 'Embla']
189
+
190
+ <Button onClick={() => setOpen(true)}>Open Menu</Button>
191
+
192
+ <CommandMenu
193
+ open={open}
194
+ onOpenChange={setOpen}
195
+ groups={[
196
+ {
197
+ label: 'Servers',
198
+ items: servers.map((name) => ({
199
+ id: name.toLowerCase(),
200
+ label: `${name}.tollerud.no`,
201
+ description: 'SSH · 4 containers · uptime 14d',
202
+ onSelect: () => ssh(name),
203
+ })),
204
+ },
205
+ {
206
+ label: 'Actions',
207
+ items: [
208
+ { id: 'backup', label: 'Run Backup', description: 'rclone to JottaCloud', onSelect: () => runBackup() },
209
+ { id: 'update', label: 'System Update', description: 'apt update && upgrade', onSelect: () => update() },
210
+ ],
211
+ },
212
+ ]}
213
+ />
214
+ ```
215
+
216
+ Features:
217
+ - Built-in `⌘K` / `Ctrl+K` global listener
218
+ - Arrow key navigation, `Enter` selects, `Esc` closes
219
+ - Search filters across groups, labels, and descriptions
220
+ - Auto-focus on open, body scroll lock
221
+ - Footer keyboard hints
222
+ - Optional `filter` prop for custom search logic
223
+ - `onAction` callback for analytics/tracking
224
+
225
+ Usage: Import as a client component in your page/layout.
226
+
227
+ See `KEYBOARD.md` for full keyboard contract.
228
+
229
+ ## Homelab / Infrastructure Components
230
+
231
+ The Tier 2 component set — these make Tia a real infrastructure assistant. All ship from `components/*.tsx` (`@tollerud/ui`) and are showcased on the **Infrastructure** page; `HostCard`, `AlertInbox` and `ApprovalCard` also power the Mission Control dashboard. They share a five-level `SEVERITY` scale (`critical · high · medium · low · info`).
232
+
233
+ ### ServiceHealthCard
234
+
235
+ ```tsx
236
+ <ServiceHealthCard
237
+ service="emma.tollerud.no"
238
+ status="online"
239
+ uptime="14d 3h"
240
+ responseTime="23ms"
241
+ version="22.04 LTS"
242
+ />
243
+ ```
244
+
245
+ Props: `service`, `status`, `uptime`, `responseTime`, `version`, `loading`.
246
+
247
+ ### HostCard
248
+
249
+ ```tsx
250
+ <HostCard
251
+ hostname="emma"
252
+ ip="10.0.10.10"
253
+ status="online"
254
+ cpu="23%"
255
+ memory="6.2/16 GB"
256
+ disk="45%"
257
+ uptime="14d"
258
+ containers={4}
259
+ />
260
+ ```
261
+
262
+ Props: `hostname`, `ip`, `status`, `cpu`, `memory`, `disk`, `uptime`, `containers`, `loading`.
263
+
264
+ ### DockerStackCard
265
+
266
+ ```tsx
267
+ <DockerStackCard
268
+ name="monitoring"
269
+ composePath="/hdd/config/prometheus-stack/compose.yml"
270
+ services={[
271
+ { name: 'prometheus', status: 'online' },
272
+ { name: 'grafana', status: 'online' },
273
+ { name: 'alertmanager', status: 'online' },
274
+ ]}
275
+ />
276
+ ```
277
+
278
+ Props: `name`, `services: StackService[]`, `composePath`, `loading`.
279
+
280
+ ### IncidentCard
281
+
282
+ ```tsx
283
+ <IncidentCard
284
+ title="High CPU on emma"
285
+ severity="high"
286
+ timestamp="2026-05-26 14:32"
287
+ description="CPU sustained at 92% for 5 minutes"
288
+ service="emma"
289
+ acknowledged={false}
290
+ />
291
+ ```
292
+
293
+ Prop `severity`: `'critical' | 'high' | 'medium' | 'low' | 'info'`. Dot color and border match severity.
294
+
295
+ ### ApprovalCard
296
+
297
+ ```tsx
298
+ <ApprovalCard
299
+ action="restart_container"
300
+ description="Restart emma:tollerud-hermes container"
301
+ source="emma → /hdd/config/tia/compose.yml"
302
+ state="pending"
303
+ timestamp="2026-05-26 14:35"
304
+ onApprove={() => {}}
305
+ onReject={() => {}}
306
+ />
307
+ ```
308
+
309
+ States: `pending` (shows Approve/Reject buttons), `approved`, `rejected`.
310
+
311
+ ### ActionDiff
312
+
313
+ ```tsx
314
+ <ActionDiff
315
+ label="docker-compose.yml"
316
+ lines={[
317
+ { text: ' image: hermes:latest', type: 'remove', oldLine: 5 },
318
+ { text: ' image: hermes:v2.0', type: 'add', newLine: 5 },
319
+ { text: ' restart: unless-stopped', type: 'context', oldLine: 6, newLine: 6 },
320
+ ]}
321
+ />
322
+ ```
323
+
324
+ Renders a unified diff. `showContext` (default true) toggles context lines off to focus on edits. Line shape: `{ type: 'add' | 'remove' | 'context', text, oldLine?, newLine? }`.
325
+
326
+ ### LogViewer
327
+
328
+ ```tsx
329
+ <LogViewer
330
+ lines={[
331
+ { text: 'Starting deployment...', level: 'info', timestamp: '14:32:01', source: 'deploy' },
332
+ { text: 'Connection refused on port 443', level: 'error', timestamp: '14:32:05', source: 'nginx' },
333
+ { text: 'Health check passed', level: 'info', timestamp: '14:32:10', source: 'hermes' },
334
+ ]}
335
+ follow
336
+ searchable
337
+ showLineNumbers
338
+ height={300}
339
+ />
340
+ ```
341
+
342
+ | Prop | Type | Default | Description |
343
+ |------|------|---------|-------------|
344
+ | `lines` | `{ text, level, timestamp?, source? }[]` | `[]` | Log entries. `level` is one of `trace` `debug` `info` `warn` `error`. |
345
+ | `follow` | `boolean` | `false` | Auto-scroll to the newest line whenever `lines` changes. |
346
+ | `searchable` | `boolean` | `false` | Show a filter input (matches `text` + `source`) with a live match count. |
347
+ | `showLineNumbers` | `boolean` | `false` | Prefix each row with a zero-padded line number. |
348
+ | `height` | `number` | `300` | Scroll-area height in px. |
349
+
350
+ Level color coding: `trace` (muted) → `debug` (info blue) → `info` (secondary) → `warn` (yellow) → `error` (red, brightened message). Errors carry a colored left border. For a streaming/level-filter/export toolbar on top of it, see the **Logs & Console** build page.
351
+
352
+ ### AlertInbox
353
+
354
+ ```tsx
355
+ <AlertInbox
356
+ alerts={[
357
+ { id: '1', title: 'emma high CPU', severity: 'high', timestamp: '14:32', acknowledged: false },
358
+ { id: '2', title: 'pia disk 91%', severity: 'critical', timestamp: '13:15', acknowledged: true },
359
+ ]}
360
+ onAcknowledge={(id) => {}}
361
+ />
362
+ ```
363
+
364
+ Features: severity count badges, hover-to-acknowledge, severity filter, scrollable list. Pass `loading` for a skeleton state; shows “No alerts — everything looks good” when empty.
365
+
366
+ ### Timeline
367
+
368
+ ```tsx
369
+ <Timeline
370
+ active={true}
371
+ items={[
372
+ { id: '1', time: '14:32', title: 'Deployed hermes v2.0', description: 'Rolled out to emma', status: 'online',
373
+ meta: ['success', '3s'] },
374
+ { id: '2', time: '14:31', title: 'Restarted nginx', description: 'Config reloaded', status: 'warning',
375
+ meta: ['warning'] },
376
+ { id: '3', time: '14:30', title: 'SSH connection failed', description: 'emma refused connection', status: 'offline' },
377
+ ]}
378
+ />
379
+ ```
380
+
381
+ Features: status-colored dots, connecting vertical lines, animated pulse for active items, metadata badges, icon support.
382
+
383
+ ### RollbackPlan
384
+
385
+ ```tsx
386
+ <RollbackPlan
387
+ name="Rollback hermes v2.0"
388
+ executing={true}
389
+ steps={[
390
+ { id: '1', label: 'Stop hermes container', status: 'success' },
391
+ { id: '2', label: 'Restore previous image tag', status: 'running' },
392
+ { id: '3', label: 'Start container', status: 'pending' },
393
+ { id: '4', label: 'Run health check', status: 'pending' },
394
+ ]}
395
+ />
396
+ ```
397
+
398
+ Step statuses: `pending` `running` `success` `failed` `skipped` — each with distinct icon and color.
399
+
400
+ ### BackupStatusPanel
401
+
402
+ ```tsx
403
+ <BackupStatusPanel
404
+ jobs={[
405
+ { name: 'emma-config', status: 'online', lastRun: '03:00', size: '4.2 GB', target: 'JottaCloud' },
406
+ { name: 'iris-config', status: 'online', lastRun: '03:05', size: '1.8 GB', target: 'JottaCloud' },
407
+ { name: 'miriam-media', status: 'offline', lastRun: '02:45' },
408
+ ]}
409
+ totalSize="6.0 GB"
410
+ lastFullBackup="2026-05-25"
411
+ />
412
+ ```
413
+
414
+ Features: per-job status dots, size/target display, failed job warning footer. Pass `loading` for a skeleton state and `emptyState` (or rely on the default) when no jobs are configured.
415
+
416
+ ## Layout & form components
417
+
418
+ ### Panel
419
+
420
+ > ✅ `Panel` ships in `@tollerud/ui >= 1.0.9`. Import: `import { Panel } from '@tollerud/ui'`
421
+
422
+
423
+ A card with a header bar (title + optional actions) and optional footer — the structural workhorse behind the log viewer, data table, alert inbox and most dashboard surfaces.
424
+
425
+ ```tsx
426
+ <Panel title="Compose stack" icon="grid"
427
+ actions={<Button variant="ghost" size="sm">Edit</Button>}
428
+ footer={<span className="ds-mono">compose.yml · 4 services</span>}>
429
+ …content…
430
+ </Panel>
431
+ ```
432
+
433
+ Props: `title`, `icon` (icon-set name), `actions`, `footer`, `noPadding`, `className`, `style`. CSS: `.ds-panel__head` / `.ds-panel__title` / `.ds-panel__foot`.
434
+
435
+ ### Meter
436
+
437
+ > ✅ `Meter` ships in `@tollerud/ui >= 1.0.9`. Import: `import { Meter } from '@tollerud/ui'`
438
+
439
+
440
+ A labeled progress row that turns red past a hot threshold.
441
+
442
+ ```tsx
443
+ <Meter label="CPU" value={23} valueLabel="23%" />
444
+ <Meter label="Containers" value={28} unlimited valueLabel="28 running" />
445
+ ```
446
+
447
+ | Prop | Type | Default | Description |
448
+ |------|------|---------|-------------|
449
+ | `label` | `string` | — | Left-hand label. |
450
+ | `value` | `number` | — | Current value. |
451
+ | `max` | `number` | `100` | Denominator for the percentage. |
452
+ | `valueLabel` | `string` | — | Overrides the right-hand readout (else `value / max`). |
453
+ | `hot` | `number` | `85` | Percentage past which the bar turns red. |
454
+ | `unlimited` | `boolean` | `false` | Full dimmed bar; shows `value` alone. |
455
+
456
+ ### Stepper
457
+
458
+ > ✅ `Stepper` ships in `@tollerud/ui >= 1.0.9`. Import: `import { Stepper } from '@tollerud/ui'`
459
+
460
+
461
+ Horizontal step indicator for wizards. Completed steps fill yellow; the current one carries a ring.
462
+
463
+ ```tsx
464
+ <Stepper steps={['Connect host', 'Choose stacks', 'Invite team', 'Finish']} current={1} />
465
+ ```
466
+
467
+ Props: `steps: string[]`, `current` (0-indexed). CSS: `.ds-wizard__*`.
468
+
469
+ ### PasswordInput
470
+
471
+ > ✅ `PasswordInput` ships in `@tollerud/ui >= 1.0.9`. Import: `import { PasswordInput } from '@tollerud/ui'`
472
+
473
+
474
+ A password field with a show/hide toggle and an optional label action (e.g. a "Forgot?" link).
475
+
476
+ ```tsx
477
+ <PasswordInput label="Password" placeholder="••••••••"
478
+ error={pwError}
479
+ labelAction={<a href="#">Forgot?</a>} />
480
+ ```
481
+
482
+ Props: `label`, `labelAction`, `error`, `id`, plus all native `<input>` props.
483
+
484
+ ### Spinner
485
+
486
+ > ⚠️ **Not yet in the `@tollerud/ui` npm package** — this is a docs-site / roadmap component (see [COMPLETENESS_ROADMAP.md](COMPLETENESS_ROADMAP.md)). Do not import `Spinner` from `@tollerud/ui` — it will not resolve. Check [SKILL.md](SKILL.md) for what's actually shipped.
487
+
488
+
489
+ Inline loading spinner; respects reduced-motion.
490
+
491
+ ```tsx
492
+ <Button variant="primary"><Spinner size={14} /> Signing in…</Button>
493
+ ```
494
+
495
+ Props: `size` (px, default 16), `style`. CSS: `.ds-spin`.
496
+
497
+ ### FormRow
498
+
499
+ > ✅ `FormRow` ships in `@tollerud/ui >= 1.0.9`. Import: `import { FormRow } from '@tollerud/ui'`
500
+
501
+
502
+ Label + hint on the left, control on the right. The canonical settings-form layout; stacks vertically under 560px.
503
+
504
+ ```tsx
505
+ <FormRow label="Two-factor auth" hint="Require a TOTP code at sign-in.">
506
+ <Switch defaultChecked />
507
+ </FormRow>
508
+ ```
509
+
510
+ Props: `label`, `hint`, `children`. CSS: `.ds-formrow`.
511
+
512
+ ### PricingCard
513
+
514
+ > ✅ `PricingCard` ships in `@tollerud/ui >= 1.0.9`. Import: `import { PricingCard } from '@tollerud/ui'`
515
+
516
+
517
+ A single plan tier with optional ribbon, feature list and CTA. Powers the Billing page and the marketing pricing block.
518
+
519
+ ```tsx
520
+ <PricingCard name="Pro" tagline="For a growing fleet"
521
+ price={12} priceNote="billed monthly" recommended
522
+ features={['10 hosts', 'Approvals & rollback', 'Priority support']}
523
+ cta="Upgrade to Pro" />
524
+ ```
525
+
526
+ | Prop | Type | Description |
527
+ |------|------|-------------|
528
+ | `name` / `tagline` | `string` | Title + sub-line. |
529
+ | `price` | `number \| string` | `0`, `'Free'`, `'$12'` render as "Free"; numbers get a `$` prefix. |
530
+ | `period` | `string` | Suffix after the price (default `/mo`). |
531
+ | `priceNote` | `string` | Small line under the amount. |
532
+ | `features` | `string[]` | Checklist. |
533
+ | `recommended` | `boolean` | Accent border + ribbon. |
534
+ | `ribbon` | `string` | Ribbon label (default `Recommended`). |
535
+ | `cta` | `string` | Button label. |
536
+ | `ctaVariant` | `string` | Button variant (defaults by recommended/state). |
537
+ | `ctaDisabled` | `boolean` | Disable the CTA (e.g. current plan). |
538
+ | `onCta` | `fn` | CTA click handler. |
539
+
540
+ CSS: `.ds-price` and `.ds-price__*`; grid wrapper `.ds-price-grid`.
541
+
542
+ ## Charts
543
+
544
+ Palette-aware SVG charts. Yellow is the highlight series; everything else stays monochrome. Grid/axis use the `--chart-grid` token, so all four are theme-aware.
545
+
546
+ ```tsx
547
+ import { BarChart, AreaChart, Donut, Sparkline } from '@tollerud/ui'
548
+ ```
549
+
550
+ ```tsx
551
+ <BarChart data={[{ label: 'Mon', value: 12 }, { label: 'Tue', value: 18, accent: true }]} height={180} />
552
+ <AreaChart data={[28, 35, 30, 44, 52]} height={150} />
553
+ <Donut segments={[{ label: 'CPU', value: 40, color: '#E8D500' }, { label: 'Idle', value: 60, color: '#444' }]} size={160} />
554
+ <Sparkline data={[12, 18, 14, 22, 19]} w={84} h={26} color="#E8D500" />
555
+ ```
556
+
557
+ - **BarChart** — `data: { label, value, accent? }[]`, `height`. `accent: true` paints a bar yellow.
558
+ - **AreaChart** — `data: number[]`, `height`. Gradient fill + point markers.
559
+ - **Donut** — `segments: { label, value, color }[]`, `size`. Renders a legend with percentages.
560
+ - **Sparkline** — `data: number[]`, `w`, `h`, `color`. Inline trend line (used in DataTable cells).
561
+
562
+ ## Marketing blocks
563
+
564
+ Full-width page sections used on the Blocks page.
565
+
566
+ ```tsx
567
+ import { HeroBlock, FeatureCard, CTABand } from '@tollerud/ui'
568
+ ```
569
+
570
+ ### HeroBlock
571
+
572
+ A landing hero on the noir glow background. Single-column by default; pass `media` for a two-column layout with a right-hand visual.
573
+
574
+ ```tsx
575
+ <HeroBlock eyebrow="homelab control plane" title="Run your stack like production."
576
+ description="Deploy, monitor and roll back from one keyboard-first console."
577
+ actions={<><button className="tollerud-btn tollerud-btn--terminal tollerud-btn--md">deploy --free</button></>}
578
+ media={<img src="tia.png" alt="" />} />
579
+ ```
580
+
581
+ Props: `eyebrow` (pill text), `title`, `description`, `actions`, `media` (optional right column), `minHeight`, `intense`. Pass `intense` to render `NoirGlowBackground` with loud intensity (requires `@paper-design/shaders-react` in the host app).
582
+
583
+ ### FeatureCard
584
+
585
+ Icon chip + title + copy. Drop several into a `.ds-grid-3`.
586
+
587
+ ```tsx
588
+ <FeatureCard icon="zap" title="Instant deploys"
589
+ description="Push a compose file and watch it roll out with health checks." />
590
+ ```
591
+
592
+ Props: `icon` (`ReactNode` — pass a Lucide icon or custom element), `title`, `description`.
593
+
594
+ ### CTABand
595
+
596
+ A centered closing call-to-action with an optional accent bar.
597
+
598
+ ```tsx
599
+ <CTABand title="Ship your homelab like it matters."
600
+ description="Free for one host. No card, no telemetry, no nonsense."
601
+ actions={<><Button variant="primary" size="lg">Get started</Button></>} />
602
+ ```
603
+
604
+ Props: `title`, `description`, `actions`, `accentBar` (default true).
605
+
606
+ ## Overlays & data
607
+
608
+ ### Toast
609
+
610
+ > ⚠️ **Not yet in the `@tollerud/ui` npm package** — this is a docs-site / roadmap component (see [COMPLETENESS_ROADMAP.md](COMPLETENESS_ROADMAP.md)). Do not import `Toast / useToast / ToastProvider` from `@tollerud/ui` — it will not resolve. Check [SKILL.md](SKILL.md) for what's actually shipped.
611
+
612
+
613
+ Transient feedback via the `useToast()` hook (provided by `ToastProvider` at the app root). Toasts auto-dismiss and stack bottom-right.
614
+
615
+ ```tsx
616
+ const toast = useToast();
617
+ toast({ tone: 'success', title: 'Deployed', message: 'hermes v2.0 is live' });
618
+ ```
619
+
620
+ Tones: `success` · `error` · `info` · `accent`. `title` required; `message` optional.
621
+
622
+ ### Drawer / Sheet
623
+
624
+ > ⚠️ **`Drawer` is not yet in the npm package** (roadmap only — see [COMPLETENESS_ROADMAP.md](COMPLETENESS_ROADMAP.md)). `Sheet` (with `SheetTrigger`/`SheetContent`/etc.) **is** shipped and is the closest available primitive — see [SKILL.md](SKILL.md) for its API.
625
+
626
+
627
+ A side panel for detail views and slide-over forms. Closes on Esc or overlay click.
628
+
629
+ ```tsx
630
+ <Drawer open={open} onClose={() => setOpen(false)} side="right" title="Host details"
631
+ description="emma.tollerud.no"
632
+ footer={<Button variant="primary" size="sm">Connect</Button>}>
633
+ …content…
634
+ </Drawer>
635
+ ```
636
+
637
+ Props: `open`, `onClose`, `side` (`right` | `left`, default right), `title`, `description`, `children`, `footer`, `width` (default 380).
638
+
639
+ ### Combobox
640
+
641
+ > ✅ `Combobox` ships in `@tollerud/ui >= 1.0.9`. Import: `import { Combobox } from '@tollerud/ui'`
642
+
643
+
644
+ Searchable single-select with keyboard navigation (↑/↓/Enter/Esc), controlled or uncontrolled.
645
+
646
+ ```tsx
647
+ <Combobox label="Host" value={value} onChange={setValue}
648
+ options={[{ value: 'emma', label: 'emma.tollerud.no' }, …]}
649
+ placeholder="Search…" emptyText="No matches" />
650
+ ```
651
+
652
+ Props: `options: { value, label }[]`, `value`, `onChange`, `label`, `placeholder`, `emptyText`.
653
+
654
+ ### AvatarGroup
655
+
656
+ > ✅ `Avatar` and `AvatarGroup` ship in `@tollerud/ui >= 1.0.9`. Import: `import { Avatar, AvatarGroup } from '@tollerud/ui'`
657
+
658
+
659
+ Stacked avatars with an overflow count and optional presence dots.
660
+
661
+ ```tsx
662
+ <AvatarGroup max={4} size={32} users={[
663
+ { name: 'Tia', status: 'online' },
664
+ { name: 'Emma Pung', src: '/emma.jpg', status: 'warning' },
665
+ ]} />
666
+ ```
667
+
668
+ Props: `users: { name, src?, status? }[]` (`status`: `online` | `offline` | `warning`), `max` (default 4), `size` (default 32).
669
+
670
+ ## Elevation & density
671
+
672
+ **Shadow scale** — four theme-aware tiers (deep + low-spread in dark, soft in light): `--shadow-sm` `--shadow-md` `--shadow-lg` `--shadow-xl`, plus `--shadow-glow` for the yellow interaction glow. Drawers use `--shadow-xl`, popovers `--shadow-lg`. Lean on borders first; reach for a shadow only to lift overlays off the page.
673
+
674
+ **Density** — set `data-density="compact"` on any container to tighten cards, table rows, form rows, panel headers and buttons inside it, without changing the components. Default is comfortable.
675
+
676
+ ```html
677
+ <div data-density="compact"> … dense tables / forms … </div>
678
+ ```
679
+
680
+ ## EmptyState
681
+
682
+ > ⚠️ **Not yet in the `@tollerud/ui` npm package** — this is a docs-site / roadmap component (see [COMPLETENESS_ROADMAP.md](COMPLETENESS_ROADMAP.md)). Do not import `EmptyState` from `@tollerud/ui` — it will not resolve. Check [SKILL.md](SKILL.md) for what's actually shipped.
683
+
684
+
685
+ For any surface with no data yet, no search results, or an error. Pairs an icon, a one-line headline, a calm explanation and up to two actions. A `compact` variant fits inside cards, tables and panels.
686
+
687
+ ```tsx
688
+ <EmptyState
689
+ icon="server"
690
+ title="No hosts connected"
691
+ description="Connect your first machine and Tia will start watching it."
692
+ action={<Button variant="primary" size="sm">Connect a host</Button>}
693
+ />
694
+ <EmptyState compact accent icon="checkCircle" title="All clear" description="No open alerts." />
695
+ ```
696
+
697
+ | Prop | Type | Default | Description |
698
+ |------|------|---------|-------------|
699
+ | `icon` | `string` | `'folder'` | Icon name from the icon set. |
700
+ | `title` | `string` | — | Headline. |
701
+ | `description` | `string` | — | Supporting copy (max ~340px wide). |
702
+ | `action` | `ReactNode` | — | Primary action, usually a `<Button>`. |
703
+ | `secondaryAction` | `ReactNode` | — | Optional second action. |
704
+ | `compact` | `boolean` | `false` | Tighter padding for inline use. |
705
+ | `accent` | `boolean` | `false` | Yellow-tinted surface + border. |
706
+
707
+ CSS class: `.ds-empty` (with `.ds-empty__icon`, `.ds-empty__title`, `.ds-empty__desc`).
708
+
709
+ ## DataTable
710
+
711
+ The config-driven table. Pass `rows` + a `columns` spec and opt into search, a filter, selection with bulk actions, per-row menus, pagination and an empty state. It owns all sort / filter / selection / pagination state internally. Powers the **Data Table** build page and the invoice history on **Billing**.
712
+
713
+ ```tsx
714
+ <DataTable
715
+ rows={hosts}
716
+ rowKey="id"
717
+ columns={[
718
+ { key: 'id', header: 'Host', sortable: true, render: (r) => <HostCell {...r} /> },
719
+ { key: 'status', header: 'Status', sortable: true, render: (r) => <Badge variant={...}>{r.status}</Badge> },
720
+ { key: 'cpu', header: 'CPU', align: 'right', sortable: true },
721
+ ]}
722
+ searchable
723
+ searchKeys={['id', 'ip', 'owner']}
724
+ searchPlaceholder="Search host, ip, owner…"
725
+ filter={{ key: 'region', allLabel: 'All regions' }}
726
+ selectable
727
+ pageSize={5}
728
+ toolbarRight={<Button variant="terminal" size="sm">add_host</Button>}
729
+ bulkActions={[
730
+ { label: 'Restart', icon: 'refresh', variant: 'ghost', onRun: (ids, clear) => { /* … */ clear(); } },
731
+ { label: 'Stop', icon: 'trash', variant: 'destructive', onRun: (ids, clear) => clear() },
732
+ ]}
733
+ rowMenu={(row) => [
734
+ { label: 'Connect (SSH)', icon: 'external', onSelect: () => {} },
735
+ { sep: true },
736
+ { label: 'Stop host', icon: 'trash', onSelect: () => {} },
737
+ ]}
738
+ emptyState={<EmptyState icon="search" title="No hosts found" description="Try clearing your filters." />}
739
+ />
740
+ ```
741
+
742
+ | Prop | Type | Default | Description |
743
+ |------|------|---------|-------------|
744
+ | `rows` | `object[]` | `[]` | Row data. |
745
+ | `rowKey` | `string` | `'id'` | Field used as the unique key + selection id. |
746
+ | `columns` | `Column[]` | `[]` | Column spec — see below. |
747
+ | `searchable` | `boolean` | `false` | Show the search input. |
748
+ | `searchKeys` | `string[]` | all column keys | Which fields the search matches against. |
749
+ | `searchPlaceholder` | `string` | `'Search…'` | Search input placeholder. |
750
+ | `filter` | `{ key, options?, allLabel? }` | — | Segmented filter on one field; `options` defaults to the field's distinct values. |
751
+ | `selectable` | `boolean` | `false` | Row checkboxes + select-all. |
752
+ | `pageSize` | `number` | `8` | Rows per page. |
753
+ | `bulkActions` | `BulkAction[]` | `[]` | Buttons shown when rows are selected; `onRun(ids, clear)`. |
754
+ | `rowMenu` | `(row) => MenuItem[]` | — | Per-row ⋮ dropdown (same item shape as `DropdownMenu`). |
755
+ | `toolbarRight` | `ReactNode` | — | Content pinned to the right of the toolbar (e.g. an add button). |
756
+ | `emptyState` | `ReactNode` | default text | Shown when no rows match. |
757
+ | `loading` | `boolean` | `false` | Render shimmer skeleton rows instead of data. |
758
+ | `skeletonRows` | `number` | `5` | How many skeleton rows while `loading`. |
759
+
760
+ **Column:** `{ key, header, sortable?, align?: 'left' | 'right', width?, render?: (row) => ReactNode }`. Without `render`, the raw `row[key]` is shown in mono.
761
+
762
+ ## Skeleton
763
+
764
+ ```html
765
+ <div class="tollerud-skeleton h-4 w-48" />
766
+ <div class="tollerud-skeleton h-48 w-full" />
767
+ ```
768
+
769
+ ## Glass Nav
770
+
771
+ ```html
772
+ <nav class="tollerud-glass fixed top-0 left-0 right-0 z-50 h-16 flex items-center px-6">
773
+ ...
774
+ </nav>
775
+ ```
776
+
777
+ ## Grid Background
778
+
779
+ ```html
780
+ <section class="tollerud-grid-bg">
781
+ ...
782
+ </section>
783
+ ```
784
+
785
+ ## Display Heading
786
+
787
+ ```html
788
+ <h1 class="tollerud-display text-[70px]">Dark. Monochrome.</h1>
789
+ <h2 class="tollerud-display--secondary text-[40px]">Yellow where it counts</h2>
790
+ <h3 class="tollerud-display--tertiary text-[28px]">Subtle hierarchy</h3>
791
+ ```
792
+
793
+ ## Container
794
+
795
+ ```jsx
796
+ <Container>
797
+ <p>Content capped at 1100px with 24px padding</p>
798
+ </Container>
799
+ ```
800
+
801
+ ## Textarea
802
+
803
+ Multiline text input with label and error state.
804
+
805
+ ```tsx
806
+ <Textarea
807
+ id="description"
808
+ label="Description"
809
+ placeholder="Enter details..."
810
+ rows={4}
811
+ />
812
+ <Textarea
813
+ label="Bio"
814
+ value="..."
815
+ error="Max 500 characters"
816
+ onChange={...}
817
+ />
818
+ ```
819
+
820
+ | Prop | Type | Default | Description |
821
+ |------|------|---------|-------------|
822
+ | `label` | `string` | — | Label text |
823
+ | `error` | `string` | — | Error message, shows error styling |
824
+ | All native `<textarea>` props | — | — | rows, cols, placeholder, etc. |
825
+
826
+ ## Select
827
+
828
+ Styled native `<select>` dropdown with custom chevron.
829
+
830
+ ```tsx
831
+ <Select
832
+ label="Server"
833
+ placeholder="Select a server"
834
+ options={[
835
+ { value: 'emma', label: 'Emma' },
836
+ { value: 'miriam', label: 'Miriam' },
837
+ { value: 'iris', label: 'Iris' },
838
+ ]}
839
+ />
840
+ <Select label="Status" error="Required">
841
+ <option value="running">Running</option>
842
+ <option value="stopped">Stopped</option>
843
+ </Select>
844
+ ```
845
+
846
+ | Prop | Type | Default | Description |
847
+ |------|------|---------|-------------|
848
+ | `label` | `string` | — | Label text |
849
+ | `error` | `string` | — | Error message |
850
+ | `placeholder` | `string` | — | Disabled placeholder option |
851
+ | `options` | `{value, label}[]` | — | Option items (alternative to children) |
852
+ | All native `<select>` props | — | — | value, onChange, disabled, etc. |
853
+
854
+ ## Checkbox
855
+
856
+ Custom-styled checkbox with checkmark SVG and label.
857
+
858
+ ```tsx
859
+ <Checkbox label="Enable backups" checked={...} onChange={...} />
860
+ <Checkbox label="Send alerts" defaultChecked />
861
+ <Checkbox label="Action" disabled />
862
+ ```
863
+
864
+ | Prop | Type | Default | Description |
865
+ |------|------|---------|-------------|
866
+ | `label` | `string` | — | Label text (wraps input) |
867
+ | All `<input type="checkbox">` props | — | — | checked, defaultChecked, disabled, onChange |
868
+
869
+ ## Switch
870
+
871
+ Toggle switch with `role="switch"` accessibility.
872
+
873
+ ```tsx
874
+ <Switch label="Dark mode" checked={...} onChange={...} />
875
+ <Switch label="Notifications" defaultChecked />
876
+ <Switch label="Beta features" disabled />
877
+ ```
878
+
879
+ | Prop | Type | Default | Description |
880
+ |------|------|---------|-------------|
881
+ | `label` | `string` | — | Label text |
882
+ | All `<input type="checkbox">` props | — | — | checked, defaultChecked, disabled, onChange |
883
+
884
+ ## RadioGroup / Radio
885
+
886
+ Fieldset-based radio group with custom dot indicator.
887
+
888
+ ```tsx
889
+ <RadioGroup label="Deployment target" error={error}>
890
+ <Radio value="staging" label="Staging" name="target" />
891
+ <Radio value="production" label="Production" name="target" />
892
+ <Radio value="canary" label="Canary" disabled name="target" />
893
+ </RadioGroup>
894
+ ```
895
+
896
+ | RadioGroup Prop | Type | Default | Description |
897
+ |-----------------|------|---------|-------------|
898
+ | `label` | `string` | — | Group label (rendered as `<legend>`) |
899
+ | `error` | `string` | — | Error message |
900
+
901
+ | Radio Prop | Type | Default | Description |
902
+ |------------|------|---------|-------------|
903
+ | `label` | `string` | — | Radio label |
904
+ | All `<input type="radio">` props | — | — | value, checked, name, disabled |
905
+
906
+ ## Footer
907
+
908
+ Footer bar with Tollerud monogram and branding link. The component is re-exported from [`@tollerud/footer`](https://www.npmjs.com/package/@tollerud/footer) — install that package directly if you need the footer without the full design system.
909
+
910
+ ```tsx
911
+ <Footer />
912
+ <Footer layout="row" />
913
+ <Footer accent />
914
+ <Footer
915
+ labels={{
916
+ tollerudProject: 'A Tollerud Project',
917
+ allRightsReserved: 'All rights reserved.',
918
+ }}
919
+ unstyled
920
+ className="border-t border-tollerud-yellow/20 bg-black"
921
+ />
922
+ ```
923
+
924
+ | Prop | Type | Default | Description |
925
+ |------|------|---------|-------------|
926
+ | `layout` | `'responsive' \| 'row'` | `'responsive'` | responsive = mobile-first stacking, row = horizontal |
927
+ | `accent` | `boolean` | `false` | Yellow-tinted surface |
928
+ | `unstyled` | `boolean` | `false` | Skip all surface styling |
929
+ | `labels` | `Partial<FooterLabels>` | — | tollerudProject, attribution, allRightsReserved |
930
+ | `className` | `string` | — | Footer element |
931
+ | `style` | `CSSProperties` | — | Footer element inline styles |
932
+ | `classNameInner` | `string` | — | Inner wrapper |
933
+ | `classNameLogo` | `string` | — | SVG monogram |
934
+ | `classNameText` | `string` | — | Text paragraph |
935
+ | `classNameLink` | `string` | — | tollerud.no link |
936
+
937
+ ## State matrix
938
+
939
+ | Component | Default | Hover | Focus | Active/Check | Disabled | Error | Loading |
940
+ |-----------|---------|-------|-------|-------------|----------|-------|---------|
941
+ | Button | ✅ | ✅ | ✅ | ✅ | ✅ | — | ✅ (disabled) |
942
+ | Input | ✅ | — | ✅ | — | ✅ | ✅ | — |
943
+ | Textarea | ✅ | — | ✅ | — | ✅ | ✅ | — |
944
+ | Select | ✅ | — | ✅ | — | ✅ | ✅ | — |
945
+ | Checkbox | ✅ | ✅ | ✅ | ✅ | ✅ | — | — |
946
+ | Switch | ✅ | ✅ | ✅ | ✅ | ✅ | — | — |
947
+ | Radio | ✅ | ✅ | ✅ | ✅ | ✅ | — | — |
948
+ | Badge | ✅ | — | — | — | — | — | — |
949
+ | Card | ✅ | ✅ | — | — | — | — | — |
950
+ | StatusDot | ✅ | — | — | — | — | — | — |
951
+ | Footer | ✅ | — | — | — | — | — | — |