@felloinnovations/brand-kit 0.4.0 → 0.5.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 (363) hide show
  1. package/AGENTS.md +198 -24
  2. package/CHANGELOG.md +197 -0
  3. package/README.md +34 -7
  4. package/design-system/01-how-claude-decides.md +26 -17
  5. package/design-system/02-principles.md +11 -6
  6. package/design-system/03-colour.md +134 -47
  7. package/design-system/04-type.md +45 -23
  8. package/design-system/05-space-and-layout.md +175 -52
  9. package/design-system/06-sizing-profiles.md +4 -4
  10. package/design-system/07-scrolling.md +66 -20
  11. package/design-system/08-choosing-components.md +50 -22
  12. package/design-system/09-charts.md +71 -28
  13. package/design-system/10-patterns.md +33 -10
  14. package/design-system/11-page-recipes.md +110 -23
  15. package/design-system/12-content.md +2 -1
  16. package/design-system/13-motion.md +330 -47
  17. package/design-system/14-accessibility.md +47 -24
  18. package/design-system/15-ai-experiences.md +11 -6
  19. package/design-system/16-creativity.md +1 -1
  20. package/design-system/17-decisions.md +357 -1
  21. package/design-system/18-checklist.md +79 -16
  22. package/design-system/19-public-pages.md +6 -4
  23. package/design-system/20-palettes.md +275 -0
  24. package/design-system/README.md +30 -17
  25. package/design-system/components/ActionList/README.md +96 -10
  26. package/design-system/components/ActionList/preview.html +52 -7
  27. package/design-system/components/ActionList/style.css +64 -0
  28. package/design-system/components/ActivityGrid/README.md +38 -2
  29. package/design-system/components/ActivityGrid/preview.html +1 -1
  30. package/design-system/components/ActivityGrid/style.css +35 -0
  31. package/design-system/components/AppShell/README.md +205 -75
  32. package/design-system/components/AppShell/preview.html +339 -58
  33. package/design-system/components/AppShell/style.css +154 -0
  34. package/design-system/components/Avatar/README.md +37 -10
  35. package/design-system/components/Avatar/preview.html +13 -4
  36. package/design-system/components/Avatar/style.css +14 -0
  37. package/design-system/components/Badge/README.md +41 -19
  38. package/design-system/components/Badge/preview.html +13 -10
  39. package/design-system/components/Badge/style.css +23 -0
  40. package/design-system/components/Banner/README.md +30 -9
  41. package/design-system/components/Banner/preview.html +1 -1
  42. package/design-system/components/Banner/style.css +19 -0
  43. package/design-system/components/Breadcrumb/README.md +58 -46
  44. package/design-system/components/Breadcrumb/preview.html +16 -16
  45. package/design-system/components/Breadcrumb/style.css +17 -0
  46. package/design-system/components/Button/README.md +113 -55
  47. package/design-system/components/Button/preview.html +98 -25
  48. package/design-system/components/Button/style.css +86 -0
  49. package/design-system/components/Card/README.md +158 -49
  50. package/design-system/components/Card/preview.html +141 -19
  51. package/design-system/components/Card/style.css +115 -0
  52. package/design-system/components/CardBoard/README.md +58 -37
  53. package/design-system/components/CardBoard/preview.html +26 -29
  54. package/design-system/components/ChatCard/README.md +115 -29
  55. package/design-system/components/ChatCard/preview.html +149 -32
  56. package/design-system/components/ChatCard/style.css +49 -0
  57. package/design-system/components/ChatShell/README.md +116 -0
  58. package/design-system/components/ChatShell/preview.html +201 -0
  59. package/design-system/components/ChatShell/style.css +86 -0
  60. package/design-system/components/ChatTurn/README.md +78 -35
  61. package/design-system/components/ChatTurn/preview.html +60 -28
  62. package/design-system/components/ChatTurn/style.css +50 -0
  63. package/design-system/components/CheckRow/README.md +141 -0
  64. package/design-system/components/CheckRow/preview.html +140 -0
  65. package/design-system/components/CheckRow/style.css +88 -0
  66. package/design-system/components/Checkbox/README.md +58 -22
  67. package/design-system/components/Checkbox/preview.html +112 -50
  68. package/design-system/components/Checkbox/style.css +45 -0
  69. package/design-system/components/Chip/README.md +78 -47
  70. package/design-system/components/Chip/preview.html +29 -4
  71. package/design-system/components/Chip/style.css +17 -0
  72. package/design-system/components/Combobox/README.md +44 -11
  73. package/design-system/components/Combobox/preview.html +35 -12
  74. package/design-system/components/Combobox/style.css +54 -0
  75. package/design-system/components/Composer/README.md +137 -35
  76. package/design-system/components/Composer/preview.html +115 -40
  77. package/design-system/components/Composer/style.css +81 -0
  78. package/design-system/components/ConfirmDialog/README.md +73 -52
  79. package/design-system/components/ConfirmDialog/preview.html +70 -26
  80. package/design-system/components/ConfirmDialog/style.css +5 -0
  81. package/design-system/components/Cover/preview.html +3 -3
  82. package/design-system/components/Cover/style.css +19 -0
  83. package/design-system/components/DateInput/README.md +77 -12
  84. package/design-system/components/DateInput/preview.html +27 -12
  85. package/design-system/components/DateInput/style.css +38 -0
  86. package/design-system/components/Dialog/README.md +112 -44
  87. package/design-system/components/Dialog/preview.html +141 -18
  88. package/design-system/components/Dialog/style.css +74 -0
  89. package/design-system/components/EmptyState/README.md +42 -12
  90. package/design-system/components/EmptyState/preview.html +11 -11
  91. package/design-system/components/EmptyState/style.css +9 -0
  92. package/design-system/components/EntryShell/README.md +73 -48
  93. package/design-system/components/EntryShell/preview.html +60 -27
  94. package/design-system/components/EntryShell/style.css +19 -0
  95. package/design-system/components/FeatureGrid/README.md +61 -39
  96. package/design-system/components/FeatureGrid/preview.html +2 -2
  97. package/design-system/components/FeatureGrid/style.css +20 -0
  98. package/design-system/components/FileUpload/README.md +33 -3
  99. package/design-system/components/FileUpload/preview.html +18 -4
  100. package/design-system/components/FileUpload/style.css +23 -0
  101. package/design-system/components/FilterBar/README.md +114 -0
  102. package/design-system/components/FilterBar/preview.html +82 -0
  103. package/design-system/components/FilterBar/style.css +22 -0
  104. package/design-system/components/Fold/README.md +84 -26
  105. package/design-system/components/Fold/preview.html +77 -34
  106. package/design-system/components/Fold/style.css +48 -0
  107. package/design-system/components/Footer/README.md +62 -39
  108. package/design-system/components/Footer/preview.html +5 -5
  109. package/design-system/components/Footer/style.css +30 -0
  110. package/design-system/components/FunnelBars/README.md +24 -3
  111. package/design-system/components/FunnelBars/preview.html +5 -5
  112. package/design-system/components/FunnelBars/style.css +13 -0
  113. package/design-system/components/GoalBars/README.md +54 -10
  114. package/design-system/components/GoalBars/preview.html +69 -28
  115. package/design-system/components/GoalBars/style.css +51 -0
  116. package/design-system/components/GoalTicks/README.md +74 -58
  117. package/design-system/components/GoalTicks/preview.html +41 -118
  118. package/design-system/components/GoalTicks/style.css +37 -0
  119. package/design-system/components/GoalTubes/README.md +29 -3
  120. package/design-system/components/GoalTubes/preview.html +6 -6
  121. package/design-system/components/GoalTubes/style.css +23 -0
  122. package/design-system/components/Heatmap/README.md +133 -0
  123. package/design-system/components/Heatmap/preview.html +168 -0
  124. package/design-system/components/Heatmap/style.css +119 -0
  125. package/design-system/components/Hero/README.md +58 -40
  126. package/design-system/components/Hero/preview.html +1 -1
  127. package/design-system/components/Hero/style.css +19 -0
  128. package/design-system/components/HoverCard/README.md +114 -0
  129. package/design-system/components/HoverCard/preview.html +107 -0
  130. package/design-system/components/HoverCard/style.css +59 -0
  131. package/design-system/components/IconButton/README.md +69 -38
  132. package/design-system/components/IconButton/preview.html +77 -43
  133. package/design-system/components/IconButton/style.css +45 -0
  134. package/design-system/components/Input/README.md +36 -11
  135. package/design-system/components/Input/preview.html +1 -1
  136. package/design-system/components/Input/style.css +23 -0
  137. package/design-system/components/KanbanBoard/README.md +93 -45
  138. package/design-system/components/KanbanBoard/preview.html +159 -43
  139. package/design-system/components/KanbanBoard/style.css +53 -0
  140. package/design-system/components/Kbd/README.md +25 -9
  141. package/design-system/components/Kbd/preview.html +6 -2
  142. package/design-system/components/Kbd/style.css +9 -0
  143. package/design-system/components/KitButton/README.md +81 -47
  144. package/design-system/components/KitButton/preview.html +35 -13
  145. package/design-system/components/KitButton/style.css +46 -0
  146. package/design-system/components/KitCard/README.md +75 -44
  147. package/design-system/components/KitCard/preview.html +18 -10
  148. package/design-system/components/KitCard/style.css +15 -0
  149. package/design-system/components/KitInput/README.md +54 -39
  150. package/design-system/components/KitInput/preview.html +7 -7
  151. package/design-system/components/KitInput/style.css +23 -0
  152. package/design-system/components/Leaderboard/README.md +76 -49
  153. package/design-system/components/Leaderboard/preview.html +61 -26
  154. package/design-system/components/Leaderboard/style.css +44 -0
  155. package/design-system/components/ListDetail/README.md +222 -0
  156. package/design-system/components/ListDetail/preview.html +213 -0
  157. package/design-system/components/ListDetail/style.css +148 -0
  158. package/design-system/components/LoadBar/README.md +79 -0
  159. package/design-system/components/LoadBar/preview.html +40 -0
  160. package/design-system/components/LoadBar/style.css +17 -0
  161. package/design-system/components/MarketingNav/README.md +76 -42
  162. package/design-system/components/MarketingNav/preview.html +37 -8
  163. package/design-system/components/MarketingNav/style.css +45 -0
  164. package/design-system/components/Matrix/README.md +129 -0
  165. package/design-system/components/Matrix/preview.html +99 -0
  166. package/design-system/components/Matrix/style.css +84 -0
  167. package/design-system/components/Menu/README.md +117 -41
  168. package/design-system/components/Menu/preview.html +85 -23
  169. package/design-system/components/Menu/style.css +61 -0
  170. package/design-system/components/Meter/README.md +39 -4
  171. package/design-system/components/Meter/preview.html +18 -1
  172. package/design-system/components/Meter/style.css +23 -0
  173. package/design-system/components/MetricTile/README.md +237 -0
  174. package/design-system/components/MetricTile/preview.html +241 -0
  175. package/design-system/components/MetricTile/style.css +129 -0
  176. package/design-system/components/Morph/README.md +106 -0
  177. package/design-system/components/Morph/preview.html +103 -0
  178. package/design-system/components/Morph/style.css +31 -0
  179. package/design-system/components/Motion/README.md +164 -0
  180. package/design-system/components/Motion/preview.html +2537 -0
  181. package/design-system/components/NumberInput/README.md +27 -8
  182. package/design-system/components/NumberInput/preview.html +21 -5
  183. package/design-system/components/NumberInput/style.css +34 -0
  184. package/design-system/components/PaceBar/README.md +75 -0
  185. package/design-system/components/PaceBar/preview.html +67 -0
  186. package/design-system/components/PaceBar/style.css +25 -0
  187. package/design-system/components/Page/README.md +57 -18
  188. package/design-system/components/Page/preview.html +15 -17
  189. package/design-system/components/Page/style.css +7 -0
  190. package/design-system/components/PageBar/README.md +90 -34
  191. package/design-system/components/PageBar/preview.html +60 -19
  192. package/design-system/components/PageBar/style.css +35 -0
  193. package/design-system/components/PageHero/README.md +87 -51
  194. package/design-system/components/PageHero/preview.html +35 -6
  195. package/design-system/components/PageHero/style.css +31 -0
  196. package/design-system/components/Pagination/README.md +70 -49
  197. package/design-system/components/Pagination/preview.html +24 -15
  198. package/design-system/components/Pagination/style.css +12 -0
  199. package/design-system/components/Panel/README.md +109 -51
  200. package/design-system/components/Panel/preview.html +118 -20
  201. package/design-system/components/Panel/style.css +51 -0
  202. package/design-system/components/PasswordInput/README.md +50 -18
  203. package/design-system/components/PasswordInput/preview.html +71 -21
  204. package/design-system/components/PasswordInput/style.css +31 -0
  205. package/design-system/components/Plate/README.md +75 -17
  206. package/design-system/components/Plate/preview.html +40 -7
  207. package/design-system/components/Plate/style.css +37 -0
  208. package/design-system/components/Popover/README.md +70 -49
  209. package/design-system/components/Popover/preview.html +44 -22
  210. package/design-system/components/Popover/style.css +41 -0
  211. package/design-system/components/PricingTable/README.md +69 -49
  212. package/design-system/components/PricingTable/preview.html +21 -21
  213. package/design-system/components/PricingTable/style.css +21 -0
  214. package/design-system/components/ProgressPetals/README.md +31 -3
  215. package/design-system/components/ProgressPetals/preview.html +14 -15
  216. package/design-system/components/ProgressPetals/style.css +27 -0
  217. package/design-system/components/README.md +151 -0
  218. package/design-system/components/Radio/README.md +40 -17
  219. package/design-system/components/Radio/preview.html +16 -1
  220. package/design-system/components/Radio/style.css +21 -0
  221. package/design-system/components/RankBar/README.md +29 -2
  222. package/design-system/components/RankBar/preview.html +14 -12
  223. package/design-system/components/RankBar/style.css +18 -0
  224. package/design-system/components/Ring/README.md +61 -11
  225. package/design-system/components/Ring/preview.html +55 -10
  226. package/design-system/components/Ring/style.css +49 -0
  227. package/design-system/components/ScoreHero/README.md +91 -0
  228. package/design-system/components/ScoreHero/preview.html +104 -0
  229. package/design-system/components/ScoreHero/style.css +32 -0
  230. package/design-system/components/Section/README.md +72 -33
  231. package/design-system/components/Section/preview.html +46 -16
  232. package/design-system/components/Section/style.css +18 -0
  233. package/design-system/components/Segmented/README.md +100 -55
  234. package/design-system/components/Segmented/preview.html +107 -26
  235. package/design-system/components/Segmented/style.css +94 -0
  236. package/design-system/components/SegmentedProgress/README.md +64 -0
  237. package/design-system/components/SegmentedProgress/preview.html +69 -0
  238. package/design-system/components/SegmentedProgress/style.css +11 -0
  239. package/design-system/components/Select/README.md +47 -12
  240. package/design-system/components/Select/preview.html +28 -2
  241. package/design-system/components/Select/style.css +22 -0
  242. package/design-system/components/ShareBar/README.md +69 -0
  243. package/design-system/components/ShareBar/preview.html +72 -0
  244. package/design-system/components/ShareBar/style.css +25 -0
  245. package/design-system/components/SideList/README.md +147 -0
  246. package/design-system/components/SideList/preview.html +139 -0
  247. package/design-system/components/SideList/style.css +94 -0
  248. package/design-system/components/Skeleton/README.md +52 -16
  249. package/design-system/components/Skeleton/preview.html +35 -23
  250. package/design-system/components/Skeleton/style.css +22 -0
  251. package/design-system/components/Slider/README.md +95 -0
  252. package/design-system/components/Slider/preview.html +49 -0
  253. package/design-system/components/Slider/style.css +13 -0
  254. package/design-system/components/Split/README.md +112 -0
  255. package/design-system/components/Split/preview.html +105 -0
  256. package/design-system/components/Split/style.css +30 -0
  257. package/design-system/components/Starter/README.md +21 -9
  258. package/design-system/components/Starter/preview.html +5 -5
  259. package/design-system/components/Stat/README.md +64 -43
  260. package/design-system/components/Stat/preview.html +3 -3
  261. package/design-system/components/Stat/style.css +15 -0
  262. package/design-system/components/StatTiles/README.md +88 -44
  263. package/design-system/components/StatTiles/preview.html +142 -71
  264. package/design-system/components/StatTiles/style.css +64 -0
  265. package/design-system/components/StatusLine/README.md +85 -0
  266. package/design-system/components/StatusLine/preview.html +49 -0
  267. package/design-system/components/StatusLine/style.css +14 -0
  268. package/design-system/components/StatusScreen/README.md +49 -35
  269. package/design-system/components/StatusScreen/preview.html +20 -5
  270. package/design-system/components/StatusScreen/style.css +9 -0
  271. package/design-system/components/StepLine/README.md +97 -0
  272. package/design-system/components/StepLine/preview.html +108 -0
  273. package/design-system/components/StepLine/style.css +62 -0
  274. package/design-system/components/Stepper/README.md +35 -13
  275. package/design-system/components/Stepper/style.css +13 -0
  276. package/design-system/components/StreakCalendar/README.md +22 -1
  277. package/design-system/components/StreakCalendar/preview.html +10 -10
  278. package/design-system/components/StreakCalendar/style.css +12 -0
  279. package/design-system/components/SuggestionChip/README.md +87 -0
  280. package/design-system/components/SuggestionChip/preview.html +42 -0
  281. package/design-system/components/SuggestionChip/style.css +20 -0
  282. package/design-system/components/Switch/README.md +44 -24
  283. package/design-system/components/Switch/preview.html +23 -8
  284. package/design-system/components/Switch/style.css +31 -0
  285. package/design-system/components/Table/README.md +129 -58
  286. package/design-system/components/Table/preview.html +106 -72
  287. package/design-system/components/Table/style.css +104 -0
  288. package/design-system/components/Tabs/README.md +71 -46
  289. package/design-system/components/Tabs/preview.html +56 -24
  290. package/design-system/components/Tabs/style.css +44 -0
  291. package/design-system/components/Testimonial/README.md +48 -32
  292. package/design-system/components/Testimonial/preview.html +14 -14
  293. package/design-system/components/Testimonial/style.css +12 -0
  294. package/design-system/components/TextArea/README.md +27 -11
  295. package/design-system/components/TextArea/preview.html +8 -2
  296. package/design-system/components/TextArea/style.css +12 -0
  297. package/design-system/components/ThemeSwitch/README.md +116 -0
  298. package/design-system/components/ThemeSwitch/preview.html +57 -0
  299. package/design-system/components/ThemeSwitch/style.css +6 -0
  300. package/design-system/components/TickRow/README.md +52 -36
  301. package/design-system/components/TickRow/preview.html +64 -41
  302. package/design-system/components/TickRow/style.css +23 -0
  303. package/design-system/components/TickStrip/README.md +95 -0
  304. package/design-system/components/TickStrip/preview.html +88 -0
  305. package/design-system/components/TickStrip/style.css +42 -0
  306. package/design-system/components/Tile/README.md +79 -18
  307. package/design-system/components/Tile/preview.html +91 -40
  308. package/design-system/components/Tile/style.css +36 -0
  309. package/design-system/components/Timeline/README.md +68 -48
  310. package/design-system/components/Timeline/preview.html +27 -15
  311. package/design-system/components/Timeline/style.css +25 -0
  312. package/design-system/components/Toast/README.md +70 -38
  313. package/design-system/components/Toast/preview.html +43 -8
  314. package/design-system/components/Toast/style.css +25 -0
  315. package/design-system/components/Tooltip/README.md +93 -35
  316. package/design-system/components/Tooltip/preview.html +69 -15
  317. package/design-system/components/Tooltip/style.css +78 -0
  318. package/design-system/components/TopBar/README.md +67 -53
  319. package/design-system/components/TopBar/preview.html +14 -27
  320. package/design-system/components/TopBar/style.css +35 -0
  321. package/design-system/components/TopicRail/README.md +108 -0
  322. package/design-system/components/TopicRail/preview.html +58 -0
  323. package/design-system/components/TopicRail/style.css +48 -0
  324. package/design-system/components/TrendBars/README.md +52 -14
  325. package/design-system/components/TrendBars/preview.html +65 -32
  326. package/design-system/components/TrendBars/style.css +25 -0
  327. package/design-system/components/TrendLine/README.md +164 -0
  328. package/design-system/components/TrendLine/preview.html +373 -0
  329. package/design-system/components/TrendLine/style.css +155 -0
  330. package/design-system/components/Well/README.md +64 -22
  331. package/design-system/components/Well/preview.html +14 -14
  332. package/design-system/components/Well/style.css +22 -0
  333. package/design-system/components/_house.css +324 -0
  334. package/design-system/components/bundle.css +4091 -744
  335. package/design-system/tokens.json +1314 -124
  336. package/dist/fello-core.css +2 -2
  337. package/dist/fello.css +6 -1369
  338. package/dist/fello.scoped.css +7 -0
  339. package/dist/index.d.ts +110 -11
  340. package/dist/index.js +781 -96
  341. package/dist/palettes.d.ts +464 -0
  342. package/dist/palettes.js +497 -0
  343. package/dist/scoped.d.ts +3 -0
  344. package/dist/scoped.js +16 -0
  345. package/dist/toast.d.ts +14 -0
  346. package/dist/toast.js +33 -29
  347. package/dist/tokens.d.ts +1686 -62
  348. package/dist/tokens.js +1668 -61
  349. package/package.json +20 -4
  350. package/scripts/check-palette.mjs +295 -0
  351. package/scripts/token-model.mjs +552 -0
  352. package/tokens/_dark.css +174 -0
  353. package/tokens/_palettes.css +46 -0
  354. package/tokens/_product.css +266 -53
  355. package/tokens/_source.hubl.css +39 -19
  356. package/tokens/design-tokens.json +319 -0
  357. package/tokens/palettes.ts +504 -0
  358. package/tokens/shadcn-compat.css +19 -19
  359. package/tokens/shadcn-compat.hsl.css +76 -0
  360. package/tokens/tokens.css +1292 -62
  361. package/tokens/tokens.md +588 -61
  362. package/tokens/tokens.root.css +1298 -62
  363. package/tokens/tokens.ts +1677 -64
@@ -0,0 +1,104 @@
1
+ /* ---- Table: the wide-table pattern. Records in a card: a sticky head and first column (a shade on its edge once the rows
2
+ slide under it), a fade on the side that hides columns, outer cells on the card's 20 px edge, 8 px above and below every
3
+ cell (40 the row's minimum), numbers right-aligned in fixed columns, totals in the foot, columns that fold by priority
4
+ and a phone fold. The card is a size container: what folds follows the table's width, not the window's. ---- */
5
+ .ws-table { --ws-table-ground: var(--color-bg-page); --ws-table-shade: color-mix(in srgb, var(--ws-shade) 7.8431372549%, transparent); /* the 8-bit step of 8% (20 of 255), as rgba() painted it in this gradient */ --ws-table-num-w: var(--space-4xl); --ws-table-check-w: calc(var(--ws-card-inset) + var(--space-lg) + var(--space-sm)); container: ws-table / inline-size; }
6
+ /* Not clipped (K1): the scroller inside holds the columns and takes the card's corners. */
7
+ .ws-table > :first-child { border-top-left-radius: inherit; border-top-right-radius: inherit; }
8
+ .ws-table > :last-child { border-bottom-left-radius: inherit; border-bottom-right-radius: inherit; }
9
+ /* The scroller: sideways always; up and down once it is given a height (--ws-table-max-h), so the head and the totals stay. */
10
+ .ws-table__scroll { overflow: auto; max-height: var(--ws-table-max-h, none); overscroll-behavior-x: contain; scrollbar-width: thin; }
11
+ /* Its ring would sit under the stuck cells, so a focused scroller rings its card instead, 2 px out. */
12
+ .ws-table:has(> .ws-table__scroll:focus-visible) { outline: var(--ws-focus-width) solid var(--ws-focus-color); outline-offset: var(--ws-focus-offset); }
13
+ @supports selector(:has(*)) { .ws-table > .ws-table__scroll:focus-visible { outline: none; } }
14
+ /* Separate borders: a collapsed border belongs to the table and stays behind when its sticky cell moves. */
15
+ .ws-table table { width: 100%; border-collapse: separate; border-spacing: 0; }
16
+ /* Every cell: one line (columns scroll, rows never squash to a word a line), 8 px above and below, 12 at the sides, 20 at
17
+ the card's edge. A cell that may wrap says so (__wrap). */
18
+ .ws-table th, .ws-table td { padding: var(--space-xs) var(--space-sm); background-color: var(--ws-table-ground); text-align: left; vertical-align: middle; white-space: nowrap; }
19
+ .ws-table tr > :first-child { padding-left: var(--ws-card-inset); }
20
+ .ws-table tr > :last-child { padding-right: var(--ws-card-inset); }
21
+ .ws-table .ws-table__wrap { white-space: normal; min-width: var(--space-9xl); }
22
+ /* Head: 13 / 18 medium muted labels, stuck to the top of the scroller. */
23
+ .ws-table thead th { position: sticky; top: 0; z-index: 2; height: var(--ws-row-h-dense); font-size: var(--ws-text-control); line-height: var(--ws-leading-control); font-weight: 500; color: var(--color-text-muted); border-bottom: var(--ws-divider); }
24
+ .ws-table thead th[aria-sort="ascending"], .ws-table thead th[aria-sort="descending"] { color: var(--color-text-heading); }
25
+ .ws-table__sort { display: inline-flex; align-items: center; gap: var(--space-xxs); border: 0; background: none; padding: 0; font: inherit; color: inherit; cursor: pointer; }
26
+ .ws-table__sort svg { width: var(--ws-icon-sm); height: var(--ws-icon-sm); flex-shrink: 0; }
27
+ .ws-table th[aria-sort="ascending"] .ws-table__sort svg { transform: rotate(180deg); }
28
+ @media (hover: hover) { .ws-table__sort:hover { color: var(--color-text-heading); } }
29
+ /* An unsorted column's chevron only says it sorts: dimmed (--ws-dim), so two columns never both read as sorted; it comes
30
+ up on hover and focus. */
31
+ .ws-table th:not([aria-sort="ascending"], [aria-sort="descending"]) .ws-table__sort svg { opacity: var(--ws-dim); }
32
+ .ws-table th .ws-table__sort:focus-visible svg { opacity: 1; }
33
+ @media (hover: hover) { .ws-table th .ws-table__sort:hover svg { opacity: 1; } }
34
+ /* Body: 40 px rows, a 0.5 px line between them. */
35
+ .ws-table tbody tr, .ws-table tfoot tr { height: var(--ws-row-h-dense); }
36
+ .ws-table tbody th, .ws-table tbody td { font-size: var(--ws-text-body); line-height: calc(var(--space-md) + var(--space-xxs)); font-weight: 400; color: var(--color-text-body); }
37
+ .ws-table tbody tr + tr > * { border-top: var(--ws-divider); }
38
+ .ws-table .ws-table__name { font-weight: 500; color: var(--color-text-heading); }
39
+ /* A second line in a cell (a figure's base, a name's role). */
40
+ .ws-table__sub { display: block; font-size: var(--ws-text-meta); line-height: var(--space-md); font-weight: 400; color: var(--color-text-muted); }
41
+ /* Numbers: right-aligned, tabular, in a fixed column (64, 96 wide) so every row's digits stack; a wider number widens
42
+ the column, it never truncates. */
43
+ .ws-table .ws-table__num { width: var(--ws-table-num-w); text-align: right; font-variant-numeric: tabular-nums; }
44
+ .ws-table .ws-table__num--wide { --ws-table-num-w: var(--space-6xl); }
45
+ /* Totals: the body's rhythm, a line above, 600 navy; they stay at the foot of a scroller that has a height. */
46
+ .ws-table tfoot th, .ws-table tfoot td { position: sticky; bottom: 0; z-index: 1; border-top: var(--ws-divider); font-size: var(--ws-text-body); line-height: calc(var(--space-md) + var(--space-xxs)); font-weight: 600; color: var(--color-text-heading); }
47
+ /* The first column stays while the rest slide under it (and the check column with it). */
48
+ .ws-table tr > :first-child, .ws-table .ws-table__check + * { position: sticky; left: 0; z-index: 1; }
49
+ .ws-table .ws-table__check + * { left: var(--ws-table-check-w); }
50
+ .ws-table thead tr > :first-child, .ws-table thead .ws-table__check + * { z-index: 3; }
51
+ .ws-table tfoot tr > :first-child, .ws-table tfoot .ws-table__check + * { z-index: 2; }
52
+ /* The check column: a fixed width (the sticky name sits after it), the skinned Checkbox on a 24 px target, 40 on touch. */
53
+ .ws-table .ws-table__check { width: var(--ws-table-check-w); padding-right: var(--space-sm); }
54
+ .ws-table__check .ws-check { position: relative; align-items: center; justify-content: center; width: var(--space-lg); height: var(--space-lg); padding: 0; }
55
+ .ws-table__check .ws-check__box { margin-top: 0; }
56
+ @media (pointer: coarse) { .ws-table__check .ws-check::after { content: ""; position: absolute; inset: calc((100% - var(--ws-touch)) / 2); } }
57
+ /* Row acts: small ghost buttons, always in the DOM. The More act stands in for them on a phone. */
58
+ .ws-table .ws-table__acts { text-align: right; }
59
+ .ws-table__acts .ws-btn + .ws-btn { margin-left: var(--space-xxs); }
60
+ .ws-table .ws-table__more { display: none; }
61
+ /* Hover: North's faint navy wash (a colour change, nothing moves); selected: the sunk cream and a 2 px navy mark on the
62
+ row's left edge, so the state is never colour alone. */
63
+ .ws-table tbody tr > * { transition: background-color var(--ws-duration-hover) var(--ws-ease-soft); }
64
+ @media (hover: hover) { .ws-table tbody tr:hover > * { background-image: linear-gradient(var(--ws-hover-faint), var(--ws-hover-faint)); } }
65
+ .ws-table .ws-table__row--selected > *, .ws-table tr[aria-selected="true"] > * { background-color: var(--ws-surface-sunk); }
66
+ .ws-table .ws-table__row--selected > :first-child, .ws-table tr[aria-selected="true"] > :first-child { box-shadow: inset var(--ws-ring) 0 0 var(--color-text-heading); }
67
+ /* Edge cues. The sticky column's shade: the last sticky cell of each row throws a soft shade over the rows sliding under
68
+ it, only once they have moved (a scroll-driven fade where the browser has one; a hairline that is always there where it
69
+ has not). The far edge: while columns are hidden past it the rows fade out there, and the fade lifts at the end. */
70
+ .ws-table tr > :first-child:not(.ws-table__check)::after, .ws-table .ws-table__check + *::after { content: ""; position: absolute; top: 0; bottom: 0; left: 100%; width: var(--space-xs); background: linear-gradient(to right, var(--ws-table-shade), transparent); opacity: 0; pointer-events: none; }
71
+ /* In dark a navy or black shade vanishes into the dark ground (8 % black moved it 1 step), so the edge reads the dark
72
+ theme's way, as a lit rim: the card edge fading out. */
73
+ [data-theme="dark"] .ws-table { --ws-table-shade: var(--ws-edge-card); }
74
+ @media (prefers-color-scheme: dark) { [data-theme="system"] .ws-table { --ws-table-shade: var(--ws-edge-card); } }
75
+ @supports (animation-timeline: scroll()) {
76
+ .ws-table__scroll { scroll-timeline: --ws-table-x inline; mask-image: linear-gradient(to left, transparent, var(--ws-mask) var(--space-lg)); mask-size: calc(100% + var(--space-lg)) 100%; mask-repeat: no-repeat; animation: ws-table-end linear both; animation-timeline: scroll(self inline); animation-range: calc(100% - var(--space-lg)) 100%; }
77
+ .ws-table__scroll tr > :first-child:not(.ws-table__check)::after, .ws-table__scroll .ws-table__check + *::after { animation: ws-table-shade linear both; animation-timeline: --ws-table-x; animation-range: 0 var(--space-lg); }
78
+ }
79
+ @supports not (animation-timeline: scroll()) {
80
+ .ws-table__scroll tr > :first-child:not(.ws-table__check), .ws-table__scroll .ws-table__check + * { box-shadow: inset calc(var(--ws-hairline) * -1) 0 0 var(--ws-edge); }
81
+ /* The hairline outranks the selected mark there; a selected row's stuck name carries both. */
82
+ .ws-table__scroll .ws-table__row--selected > :first-child:not(.ws-table__check), .ws-table__scroll tr[aria-selected="true"] > :first-child:not(.ws-table__check) { box-shadow: inset var(--ws-ring) 0 0 var(--color-text-heading), inset calc(var(--ws-hairline) * -1) 0 0 var(--ws-edge); }
83
+ }
84
+ @keyframes ws-table-shade { from { opacity: 0; } to { opacity: 1; } }
85
+ @keyframes ws-table-end { from { mask-position: calc(var(--space-lg) * -1) 0; } to { mask-position: 0 0; } }
86
+ /* Column priority and the phone fold. A low-priority column (__low) folds away when the table is narrower than 48rem;
87
+ the row's fold line (__fold, under its name) then says what it held. */
88
+ .ws-table__fold { display: none; font-size: var(--ws-text-meta); line-height: var(--space-md); font-weight: 400; color: var(--color-text-muted); }
89
+ @container ws-table (max-width: 48rem) {
90
+ .ws-table .ws-table__low { display: none; }
91
+ .ws-table .ws-table__fold { display: block; }
92
+ }
93
+ /* Under 30rem the row acts fold into one More act, and a name that would push the rest off the card wraps at 40 % of
94
+ the table, so the stuck columns never leave the sliding ones a sliver (a name that fits keeps its one line). */
95
+ @container ws-table (max-width: 30rem) {
96
+ .ws-table .ws-table__acts > :not(.ws-table__more) { display: none; }
97
+ .ws-table .ws-table__more { display: inline-flex; }
98
+ .ws-table .ws-table__name { min-width: 40cqi; white-space: normal; }
99
+ }
100
+ /* Loading: a ghost bar in each cell, in the chart colours' sweep (add ws-shimmer), the head shown at once. */
101
+ .ws-table__placeholder { height: calc(var(--space-xs) + var(--space-3xs)); border-radius: var(--ws-radius-bar); background: var(--ws-track); }
102
+ .ws-table__placeholder.ws-shimmer { background: var(--ws-shimmer); background-size: 300% 100%; }
103
+ /* Empty: the same card, a sentence and the act that clears the filter. */
104
+ .ws-table__empty { display: flex; flex-direction: column; align-items: center; gap: var(--space-sm); padding: var(--space-lg) var(--ws-card-inset); text-align: center; }
@@ -8,81 +8,106 @@ The underline tab row: five or more sections of a settings or detail page, read
8
8
 
9
9
  ## Not for
10
10
 
11
- - Two to four views of the same data: use `Segmented` (the pill, navy-thumb switch), which stays the rule for that range.
11
+ - Two to four views of the same data: use `Segmented`.
12
12
  - Moving between whole pages of a product: use `AppShell`'s nav or `TopBar`'s links.
13
13
  - A one-off disclosure that opens and closes in place: use `Fold`.
14
14
 
15
15
  ## Anatomy
16
16
 
17
- - Row: items 32 px tall (`--ws-control-h`), 14 px medium words, a hairline (`--ws-edge-slot`, the same tone as a day
18
- cell's or a chart row's hairline) under the whole row.
19
- - Rest: `--color-text-body` (grey-7).
20
- - Current: `--color-text-heading` (navy), a 2 px navy underline (`--ws-ring`) sitting just under the item, above the
21
- row's own hairline.
22
- - A count: a small `ws-chip` beside the label when a section has a number worth surfacing (unread items, one upcoming
23
- showing).
17
+ - Row: tabs 32 px tall (`--ws-control-h`, 40 on touch), 14 / 20 medium words, 24 px apart, a 1 px hairline
18
+ (`--ws-edge-slot`) under the whole row at the column's width.
19
+ - The first tab's words sit on the content edge, the same edge as the card's head and body (K7). Each tab's box bleeds
20
+ 6 px past its words, and the row 6 px past the column, so the focus ring has room and no word moves.
21
+ - Rest: `--color-text-body`. Current: navy words and a 2 px navy underline under the words, over the hairline.
22
+ - A count: a small `ws-chip` after the label (18 tall, 11 px).
23
+ - Overflow: the row scrolls sideways with no scrollbar and fades 32 px at the edge that hides tabs (nothing fades when
24
+ every tab fits).
24
25
  - The whole row is `role="tablist"`; each item `role="tab"` with `aria-selected` and a matching `role="tabpanel"`.
25
26
 
26
27
  ## Variants and sizes
27
28
 
28
29
  | Variant | Where |
29
30
  | --- | --- |
30
- | `underline` (this component) | Five-plus sections of one thing; matches the brand kit's own `underline` Tabs variant |
31
- | `pill` | The kit's other Tabs variant is this product's `Segmented`, used for 2 to 4 views; it is a different component here, not a Tabs prop |
31
+ | The row (this component) | five or more sections of one thing |
32
+ | `ws-tabs-fold` | the row on a desktop and a `Select` of the same sections on a phone (under 40rem): for a row too long to scroll comfortably on a phone |
32
33
 
33
34
  ## States
34
35
 
35
- - **Default**: grey-7 words, no mark.
36
- - **Hover**: words move toward `--color-text-heading` (no underline until selected).
37
- - **Focus**: the kit's `--shadow-focus-ring`; arrow keys move focus along the tablist per the ARIA tabs pattern.
38
- - **Selected**: navy words, the 2 px underline.
39
- - **With a count**: the chip sits after the label, never replacing it.
40
- - **Narrow (360 px and under)**: the row scrolls horizontally with no visible scrollbar; the hairline still runs the
41
- row's full scrollable width so it never looks cut off.
42
- - **Disabled** (a section not yet available): 40% opacity, `aria-disabled`, not reachable by click or arrow key.
36
+ - **Default:** grey words, no mark.
37
+ - **Hover (a mouse only):** the words turn navy (no underline until selected).
38
+ - **Focus:** the kit's ring, drawn inside the tab's box (the row scrolls, so a ring outside it would be cut).
39
+ - **Selected:** navy words, the 2 px underline.
40
+ - **With a count:** the chip sits after the label, never replacing it.
41
+ - **Disabled** (a section not yet available): 40%, `aria-disabled="true"`, skipped by click and arrow keys.
42
+ - **Long row:** it scrolls sideways and fades at the hidden edge; the page brings the chosen tab into view.
43
+ - **Narrow:** as long; on a phone, `ws-tabs-fold` shows the `Select` instead.
44
+ - **Touch:** tabs 40 tall; the row swipes.
45
+ - **Dark:** the hairline, the words and the underline take their dark values.
43
46
 
44
47
  ## Behaviour
45
48
 
46
- - Keyboard: `role="tablist"`; left/right arrow (or up/down if the brief calls for a vertical row, not used here) moves
47
- selection, Home/End jump to the first/last tab; only the selected tab is in the Tab order (`tabindex="-1"` on the
48
- rest), matching the brief's note that hand-rolled tabs miss this and the kit's Radix `Tabs` gets it right.
49
- Activation is automatic on arrow move (select-on-focus), the common pattern for this kind of tab.
50
- - Pointer: a click selects the tab under it.
51
- - Touch: the row's items carry the 24 px minimum for words that act; the scrollable row allows a swipe past the fold.
52
- - Motion: the underline can glide between tabs on `--ws-ease-soft` as a shared layout id, the same technique as the
53
- sidebar's current wash and `Segmented`'s thumb; reduced motion shows it landing without the glide.
49
+ - Keyboard: left and right arrows move and choose, Home and End jump to the ends; only the chosen tab is in the Tab order
50
+ (`tabindex="-1"` on the rest). Activation follows focus.
51
+ - On a choice the page calls `tab.scrollIntoView({ inline: 'nearest', block: 'nearest' })`, so the chosen tab is never
52
+ off screen (K7); `scroll-padding-inline` keeps it clear of the fade.
53
+ - With `ws-tabs-fold`, the page keeps the row and the `Select` on the same section.
54
+ - Motion: the underline slides to the chosen tab and takes its width (North's tab spring, .4 / .12, the department
55
+ card underline), plain CSS with no script and no new markup. The chosen tab is a CSS anchor; the row's `::after` is
56
+ the travelling underline, its two ends easing on motion's own `spring(.4, .12)` written out as `linear()` (500 ms;
57
+ `--ws-spring-tab-duration` and `--ws-spring-tab`). At rest the chosen tab draws its own underline (the 0.4 look); while
58
+ the row is pointed at or holds focus the travelling one takes its place, so a choice made then slides. Without CSS
59
+ anchor positioning, or under reduced motion, it lands at once. The edge fade follows the scroll and is not motion.
60
+ Try it in the preview: click a tab.
54
61
 
55
62
  ## Content
56
63
 
57
- - Section names: sentence case, one or two words, naming the section not instructing ("Overview", "Showings", not
58
- "View the showings").
64
+ - Section names: sentence case, one or two words, naming the section ("Overview", "Showings").
59
65
  - No em dashes.
60
- - A count chip is a bare number (occasionally with a short word, "1 new"), never a sentence.
66
+ - A count chip is a bare number (or a short word, "1 new"), never a sentence.
61
67
 
62
68
  ## Props
63
69
 
64
70
  | Name | Type | Meaning |
65
71
  | --- | --- | --- |
66
- | `items` | `{ id, label, count?, disabled? }[]` | The tabs, in order |
67
- | `value` | `string` | The selected tab's id |
68
- | `onChange` | `(id: string) => void` | Called when selection moves |
69
- | `panels` | `Record<string, ReactNode>` | Each tab's content, rendered for the selected id |
72
+ | `items` | `{ id, label, count?, disabled? }[]` | the tabs, in order |
73
+ | `value` | `string` | the chosen tab's id |
74
+ | `onChange` | `(id: string) => void` | called when the choice moves |
75
+ | `panels` | `Record<string, ReactNode>` | each tab's content |
76
+ | `foldOnPhone` | `boolean` | renders `ws-tabs-fold` with its `Select` |
70
77
 
71
78
  ## Rules
72
79
 
73
- - **Underline tabs for five-plus sections; `Segmented` for two to four views**: the two components are never
74
- interchangeable fixes for the same job (`08-choosing-components.md`, "Switch between a few views" vs this piece).
75
- - **No tab row of its own styles** (the system's own rule): every tab row in a product built on this system is this
76
- component, never a hand-drawn set of buttons with a bottom border.
77
- - **The hairline under the row is `--ws-edge-slot`**, matching a day cell's and a chart row's fine edge rather than the
78
- card edge tone (`--ws-edge-card`), since a tab row sits inside content rather than bounding a surface.
79
- - **Source**: Intentional addition, decided from the system (26 Oct 2026): five-plus sections of a settings or detail
80
- page need a row `Segmented` was never meant to hold. Derived from the kit's own underline Tabs variant (Radix-based,
81
- matching this product's look exactly) and the card's foot hairline tone for the row's underline.
80
+ - Underline tabs for five-plus sections; `Segmented` for two to four views.
81
+ - No tab row of its own styles: every tab row is this component.
82
+ - The first tab's words sit on the content edge (K7: the 0.4.0 row put them 4 px in).
83
+ - A tab row never hides its overflow silently: it fades at the hidden edge and the chosen tab is always in view; more
84
+ than four tabs on a phone fold into a `Select`.
85
+ - The hairline under the row is `--ws-edge-slot`, a content line, not a surface edge.
82
86
 
83
87
  ## Source
84
88
 
85
- **Derived from**: the kit's own underline `Tabs` variant (Radix-based), restyled with the product's navy-current and
86
- `--ws-edge-slot` hairline tokens in place of the kit's own border colour, and the system's split between underline
87
- tabs (five-plus sections) and the pill `Segmented` switch (two to four views). **Precedent**: an intentional addition
88
- decided from the system, 26 Oct 2026.
89
+ **Derived from**: the kit's underline Tabs (26 Oct 2026), the Revenue OS audit's K7 (the row hid its overflow and its
90
+ chosen tab, and its first tab sat 4 px in), North's tab underline spring (app.js) and its focus ring.
91
+
92
+ ## Markup
93
+
94
+ ```html
95
+ <div role="tablist" aria-label="Lead" class="ws-tabs">
96
+ <button type="button" role="tab" id="t-overview" aria-selected="true" aria-controls="p-overview" class="ws-tabs__tab">Overview</button>
97
+ <button type="button" role="tab" id="t-showings" aria-selected="false" aria-controls="p-showings" tabindex="-1" class="ws-tabs__tab">Showings <span class="ws-chip ws-chip--sunk">1</span></button>
98
+ </div>
99
+ <div id="p-overview" role="tabpanel" aria-labelledby="t-overview">...</div>
100
+
101
+ <div class="ws-tabs-fold">
102
+ <div role="tablist" aria-label="Account" class="ws-tabs">...</div>
103
+ <span class="ws-select-wrap ws-tabs-fold__select">
104
+ <select class="ws-select" aria-label="Section"><option selected>Overview</option><option>Deals</option></select>
105
+ <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6l4 4 4-4"/></svg>
106
+ </span>
107
+ </div>
108
+ ```
109
+
110
+ ## Styles
111
+
112
+ `design-system/components/Tabs/style.css`: `ws-tabs`, `ws-tabs__tab`, `ws-tabs-fold` and `__select`. The edge fade is a
113
+ scroll-driven animation (`ws-scroll-edges`, shared with `Segmented`); where a browser has none, the row scrolls without it.
@@ -1,4 +1,4 @@
1
- <!-- @dsCard group="Navigation" height=234 -->
1
+ <!-- @dsCard group="Navigation" height=420 -->
2
2
  <!doctype html>
3
3
  <html>
4
4
  <head>
@@ -8,37 +8,69 @@
8
8
  <link rel="stylesheet" href="../bundle.css">
9
9
  </head>
10
10
  <body>
11
- <div class="ws-stage ws-stage--grey ws-stage--column" style="gap: var(--ws-gap-component);">
11
+ <div class="ws-stage ws-stage--column">
12
12
 
13
13
  <div class="ws-stack">
14
- <span class="ws-muted">6 sections, one with a count, the current tab navy with the underline, a hairline under the row</span>
15
- <div role="tablist" aria-label="Lead" class="ws-tabs">
16
- <button type="button" role="tab" id="t-overview" aria-selected="true" aria-controls="p-overview" class="ws-tabs__tab">Overview</button>
17
- <button type="button" role="tab" id="t-activity" aria-selected="false" aria-controls="p-activity" class="ws-tabs__tab" tabindex="-1">Activity</button>
18
- <button type="button" role="tab" id="t-showings" aria-selected="false" aria-controls="p-showings" class="ws-tabs__tab" tabindex="-1">
19
- Showings <span class="ws-chip ws-chip--sunk">1</span>
20
- </button>
21
- <button type="button" role="tab" id="t-calls" aria-selected="false" aria-controls="p-calls" class="ws-tabs__tab" tabindex="-1">Calls</button>
22
- <button type="button" role="tab" id="t-notes" aria-selected="false" aria-controls="p-notes" class="ws-tabs__tab" tabindex="-1">Notes</button>
23
- <button type="button" role="tab" id="t-offer" aria-selected="false" aria-controls="p-offer" class="ws-tabs__tab" tabindex="-1">Offer</button>
14
+ <span class="ws-muted">Six sections, one with a count, one not ready yet; the first tab's words on the content edge. More than four on a phone fold into a Select. Click a tab: the underline slides to it on North's tab spring</span>
15
+ <div class="ws-card ws-card--inset">
16
+ <div class="ws-tabs-fold">
17
+ <div role="tablist" aria-label="Lead" class="ws-tabs">
18
+ <button type="button" role="tab" id="t-overview" aria-selected="true" aria-controls="p-overview" class="ws-tabs__tab">Overview</button>
19
+ <button type="button" role="tab" id="t-activity" aria-selected="false" aria-controls="p-overview" class="ws-tabs__tab" tabindex="-1">Activity</button>
20
+ <button type="button" role="tab" id="t-showings" aria-selected="false" aria-controls="p-overview" class="ws-tabs__tab" tabindex="-1">Showings <span class="ws-chip ws-chip--sunk">1</span></button>
21
+ <button type="button" role="tab" id="t-calls" aria-selected="false" aria-controls="p-overview" class="ws-tabs__tab" tabindex="-1">Calls</button>
22
+ <button type="button" role="tab" id="t-notes" aria-selected="false" aria-controls="p-overview" class="ws-tabs__tab" tabindex="-1">Notes</button>
23
+ <button type="button" role="tab" id="t-offer" aria-selected="false" aria-disabled="true" aria-controls="p-overview" class="ws-tabs__tab" tabindex="-1">Offer</button>
24
+ </div>
25
+ <span class="ws-select-wrap ws-tabs-fold__select">
26
+ <select class="ws-select" aria-label="Section" aria-controls="p-overview">
27
+ <option selected>Overview</option><option>Activity</option><option>Showings (1)</option><option>Calls</option><option>Notes</option><option disabled>Offer</option>
28
+ </select>
29
+ <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6l4 4 4-4"/></svg>
30
+ </span>
31
+ </div>
32
+ <p id="p-overview" role="tabpanel" aria-labelledby="t-overview" class="ws-body" style="padding-top: var(--space-sm);">New lead 6 days ago, referred by Dana Alvarez.</p>
24
33
  </div>
25
- <div id="p-overview" role="tabpanel" aria-labelledby="t-overview" class="ws-body" style="padding-top: var(--space-sm);">New lead 6 days ago, referred by Dana Alvarez.</div>
26
34
  </div>
27
35
 
28
- <div class="ws-stack" style="width: 360px; max-width: 100%;">
29
- <span class="ws-muted">360 px: the row scrolls sideways with no visible scrollbar, the hairline keeps its width</span>
30
- <div role="tablist" aria-label="Lead, narrow" class="ws-tabs">
31
- <button type="button" role="tab" aria-selected="true" class="ws-tabs__tab">Overview</button>
32
- <button type="button" role="tab" aria-selected="false" class="ws-tabs__tab" tabindex="-1">Activity</button>
33
- <button type="button" role="tab" aria-selected="false" class="ws-tabs__tab" tabindex="-1">
34
- Showings <span class="ws-chip ws-chip--sunk">1</span>
35
- </button>
36
- <button type="button" role="tab" aria-selected="false" class="ws-tabs__tab" tabindex="-1">Calls</button>
37
- <button type="button" role="tab" aria-selected="false" class="ws-tabs__tab" tabindex="-1">Notes</button>
38
- <button type="button" role="tab" aria-selected="false" class="ws-tabs__tab" tabindex="-1">Offer</button>
36
+ <div class="ws-stack" style="width: 300px; max-width: 100%;">
37
+ <span class="ws-muted">Too wide for its column: it scrolls sideways and fades at the edge that hides tabs</span>
38
+ <div role="tablist" aria-label="Pipeline stage" class="ws-tabs">
39
+ <button type="button" role="tab" aria-selected="false" class="ws-tabs__tab" tabindex="-1">Upcoming demos</button>
40
+ <button type="button" role="tab" aria-selected="true" class="ws-tabs__tab">Quote sent</button>
41
+ <button type="button" role="tab" aria-selected="false" class="ws-tabs__tab" tabindex="-1">Signed</button>
42
+ <button type="button" role="tab" aria-selected="false" class="ws-tabs__tab" tabindex="-1">Closed won</button>
43
+ <button type="button" role="tab" aria-selected="false" class="ws-tabs__tab" tabindex="-1">Closed lost</button>
44
+ </div>
45
+ </div>
46
+
47
+ <div class="ws-stack">
48
+ <span class="ws-muted">The fold: the row on a desktop, a Select of the same sections on a phone</span>
49
+ <div class="ws-tabs-fold">
50
+ <div role="tablist" aria-label="Account" class="ws-tabs">
51
+ <button type="button" role="tab" aria-selected="true" class="ws-tabs__tab">Overview</button>
52
+ <button type="button" role="tab" aria-selected="false" class="ws-tabs__tab" tabindex="-1">Deals</button>
53
+ <button type="button" role="tab" aria-selected="false" class="ws-tabs__tab" tabindex="-1">Contacts</button>
54
+ <button type="button" role="tab" aria-selected="false" class="ws-tabs__tab" tabindex="-1">Usage</button>
55
+ <button type="button" role="tab" aria-selected="false" class="ws-tabs__tab" tabindex="-1">Billing</button>
56
+ </div>
57
+ <span class="ws-select-wrap ws-tabs-fold__select">
58
+ <select class="ws-select" aria-label="Section">
59
+ <option selected>Overview</option><option>Deals</option><option>Contacts</option><option>Usage</option><option>Billing</option>
60
+ </select>
61
+ <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6l4 4 4-4"/></svg>
62
+ </span>
39
63
  </div>
40
64
  </div>
41
65
 
42
66
  </div>
67
+ <script>
68
+ /* The demo's wiring: a click chooses the tab (an app keeps the choice; the underline slides on its own). */
69
+ document.addEventListener('click', (e) => {
70
+ const tab = e.target.closest('.ws-tabs__tab');
71
+ if (!tab || tab.getAttribute('aria-disabled') === 'true') return;
72
+ for (const t of tab.closest('.ws-tabs').querySelectorAll('.ws-tabs__tab')) { t.setAttribute('aria-selected', String(t === tab)); t.tabIndex = t === tab ? 0 : -1; }
73
+ });
74
+ </script>
43
75
  </body>
44
76
  </html>
@@ -0,0 +1,44 @@
1
+ /* ---- Tabs: a page's own sections, words on a hairline, the current one navy with a 2 px underline under its words.
2
+ The first tab's words sit on the content edge (K7); each tab's box bleeds 6 px past its words (and the row 6 px past
3
+ the column) so the inset focus ring has room without moving a word. The row scrolls sideways when it is too wide,
4
+ fading at the edge that hides tabs (a scroll-driven fade: nothing shows when it all fits), and a page brings the
5
+ chosen tab into view (scrollIntoView, inline: nearest). On a phone a long row can fold into a Select (.ws-tabs-fold). ---- */
6
+ .ws-tabs { display: flex; align-items: stretch; gap: var(--space-lg); margin-inline: calc(var(--space-xxs-plus) * -1); padding-inline: var(--space-xxs-plus); overflow-x: auto; scrollbar-width: none; scroll-padding-inline: var(--space-xl); background: linear-gradient(in srgb, var(--ws-edge-slot), var(--ws-edge-slot)) bottom / 100% var(--ws-line) no-repeat content-box; }
7
+ .ws-tabs::-webkit-scrollbar { display: none; }
8
+ @supports (animation-timeline: scroll()) {
9
+ .ws-tabs { mask-image: linear-gradient(90deg, transparent, currentColor var(--ws-fade-start), currentColor calc(100% - var(--ws-fade-end)), transparent); animation: ws-scroll-edges linear; animation-timeline: scroll(self inline); }
10
+ }
11
+ .ws-tabs__tab { position: relative; display: inline-flex; flex-shrink: 0; align-items: center; gap: var(--space-xxs-plus); height: var(--ws-control-h); margin-inline: calc(var(--space-xxs-plus) * -1); padding: 0 var(--space-xxs-plus); border: 0; border-radius: var(--ws-radius-control); background: none; font-size: var(--ws-text-body); line-height: calc(var(--space-md) + var(--space-xxs)); font-weight: 500; color: var(--color-text-body); white-space: nowrap; cursor: pointer; }
12
+ .ws-tabs__tab:focus-visible { outline-offset: calc(var(--ws-focus-width) * -1); }
13
+ @media (hover: hover) { .ws-tabs__tab:hover { color: var(--color-text-heading); } }
14
+ .ws-tabs__tab[aria-selected="true"] { color: var(--color-text-heading); }
15
+ .ws-tabs__tab[aria-selected="true"]::after { content: ""; position: absolute; left: var(--space-xxs-plus); right: var(--space-xxs-plus); bottom: 0; height: var(--ws-ring); background: var(--color-text-heading); border-radius: var(--ws-radius-bar); }
16
+ .ws-tabs__tab[aria-disabled="true"] { opacity: 0.4; cursor: default; }
17
+ .ws-tabs__tab .ws-chip { height: calc(var(--space-md) + var(--space-3xs)); padding: 0 var(--space-xxs-plus); font-size: var(--ws-text-micro); line-height: var(--ws-leading-micro); }
18
+ /* The fold: on a phone the row gives way to a Select of the same sections (the page keeps the two in step). */
19
+ .ws-tabs-fold__select { display: none; }
20
+ @media (max-width: 40rem) {
21
+ .ws-tabs-fold > .ws-tabs { display: none; }
22
+ .ws-tabs-fold > .ws-tabs-fold__select { display: flex; }
23
+ }
24
+
25
+ /* ---- Motion: the underline slides to the chosen tab on North's tab spring (.4 / .12, the department card underline,
26
+ app.js@754407; north-motion 3) instead of jumping. The chosen tab is the anchor (CSS anchor positioning) and the
27
+ row's ::after is a travelling underline, under the tab's words (6 in from its box), its two ends easing on motion's
28
+ own spring(.4, .12) written out as linear() (500 ms). At rest the chosen tab draws its own underline, so a resting row
29
+ is the 0.4 row pixel for pixel; while the row is pointed at or holds focus the travelling one takes its place (the
30
+ same bar, at once), so a choice made then slides. Only a real choice moves it; a resize or a late font, away from
31
+ the pointer, lands it at once. No new markup. Without anchor positioning, or under reduced motion, the underline
32
+ moves at once. The spring is the global --ws-spring-tab over --ws-spring-tab-duration. ---- */
33
+ @supports (anchor-name: --a) {
34
+ .ws-tabs { position: relative; anchor-scope: --ws-tabs-current; }
35
+ .ws-tabs::after { content: ""; position: absolute; position-anchor: --ws-tabs-current; left: calc(anchor(left) + var(--space-xxs-plus)); right: calc(anchor(right) + var(--space-xxs-plus)); bottom: anchor(bottom); height: var(--ws-ring); border-radius: var(--ws-radius-bar); background: var(--color-text-heading); opacity: 0; pointer-events: none; }
36
+ .ws-tabs__tab[aria-selected="true"] { anchor-name: --ws-tabs-current; }
37
+ }
38
+ @media (prefers-reduced-motion: no-preference) {
39
+ .ws-tabs__tab { transition: color var(--ws-duration-hover) var(--ws-ease-soft); }
40
+ @supports (anchor-name: --a) {
41
+ .ws-tabs:is(:hover, :focus-within)::after { opacity: 1; transition: left var(--ws-spring-tab-duration) var(--ws-spring-tab), right var(--ws-spring-tab-duration) var(--ws-spring-tab); }
42
+ .ws-tabs:is(:hover, :focus-within) .ws-tabs__tab[aria-selected="true"]::after { opacity: 0; }
43
+ }
44
+ }
@@ -1,6 +1,7 @@
1
1
  The kit's marketing piece: for a public page, never a product screen.
2
2
 
3
- A quoted customer, their initials and their role, a `KitCard` built to carry proof rather than product state.
3
+ A quoted customer, their initials and their role: a figure on the kit's card, built to carry proof rather than
4
+ product state.
4
5
 
5
6
  ## Use it when
6
7
 
@@ -15,34 +16,33 @@ A quoted customer, their initials and their role, a `KitCard` built to carry pro
15
16
 
16
17
  ## Anatomy
17
18
 
18
- - Card: `KitCard`, `surface="white"`, `--space-lg` (24px) padding.
19
- - Quote: `--font-size-md` (16px), 24px line height, `--color-text-primary`, no quotation marks drawn by the
20
- component (the text itself may open with one if it reads naturally).
21
- - Avatar: a 36px circle (`--ws-control-h-md`, the nearest token to this diameter; see Source) with the person's
22
- initials, `--color-gray-1` fill, `--color-brand-navy` text.
23
- - Name: `--font-size-sm` (14px) medium, `--color-brand-navy`.
24
- - Role: `--font-size-2xs` (12px), `--color-text-muted`, directly under the name.
19
+ - Card (`.ws-testimonial`, a `<figure>`): 4px corners, a 1px `--color-border-default` edge, `--color-surface`,
20
+ `--space-lg` (24px) padding, a column.
21
+ - Quote (`__quote`, a `<blockquote>`): 16/24 `--color-text-primary`, no quotation marks drawn by the component.
22
+ - Person (`__who`, a `<figcaption>`): on the card's foot, at least `--space-md` (16px) under the quote; the avatar and
23
+ the words `--space-sm` (12px) apart, centred on each other.
24
+ - Avatar (`__avatar`): a 36px disc (`--ws-control-h-md`), `--ws-surface-sunk` fill, 12px semibold initials in
25
+ `--color-text-heading`.
26
+ - Name (`__name`): 14/20 medium `--color-text-heading`. Role (`__role`): 12/16 `--color-text-muted`, under the name.
27
+ - Row (`.ws-testimonial-row`): a grid of columns at least 280px wide, `--layout-col-gap` (24px) apart, the cards equal
28
+ height in each row; a third card that wraps takes the next row's first column at the same width, never the full row.
25
29
 
26
30
  ## Variants and sizes
27
31
 
28
- One layout. Shown two up, but the card has no fixed width of its own; it takes whatever column a page gives it.
29
- In fello.css: `.ws-testimonial` (`__quote`, `__who`, `__avatar`, `__name`, `__role`); two or three side by side sit in
30
- `.ws-testimonial-row`, which wraps them to one column when they no longer fit.
32
+ One layout. The card has no fixed width of its own; it takes the column the row gives it.
31
33
 
32
34
  ## States
33
35
 
34
36
  - **Default:** as above.
35
- - **Hover / focus / selected / pressed:** not applicable; a testimonial is read-only, never a link or a button.
36
- - **Pending / disabled / error:** not applicable.
37
+ - **Hover / focus / selected / pressed / pending / disabled / error:** not applicable; a testimonial is read-only.
37
38
  - **Empty:** not applicable; a testimonial without a real quote and a real name is not shown.
38
- - **Long label:** a longer quote wraps across more lines; the name and role stay pinned under it rather than
39
- being pushed off by a fixed card height.
40
- - **Narrow:** the avatar and name wrap to a tidy row down to a fairly narrow column before the role would need to
41
- wrap under the name as well.
39
+ - **Long label:** a longer quote wraps; the person stays on the card's foot, level with the person on the next card.
40
+ - **Narrow:** the cards stack; the avatar and the words stay in a row, and a long role wraps under the name.
42
41
 
43
42
  ## Behaviour
44
43
 
45
44
  - Keyboard / pointer / touch: nothing interactive; the card is static content.
45
+ - Screen readers: the figure, blockquote and figcaption tie the words to the person who said them.
46
46
  - Motion: none.
47
47
 
48
48
  ## Content
@@ -53,7 +53,7 @@ In fello.css: `.ws-testimonial` (`__quote`, `__who`, `__avatar`, `__name`, `__ro
53
53
 
54
54
  ## Props
55
55
 
56
- Not a kit export; composed from `KitCard`. A reusable version would take:
56
+ Not a kit export. A reusable version would take:
57
57
 
58
58
  | Prop | Type | Meaning |
59
59
  | --- | --- | --- |
@@ -64,20 +64,36 @@ Not a kit export; composed from `KitCard`. A reusable version would take:
64
64
 
65
65
  ## Rules
66
66
 
67
- - **Intentional addition, decided from the system (26 Oct 2026):** the kit ships `Card` but no testimonial
68
- pattern; this composes one from it rather than adding a new surface.
69
- - The card stays at the kit's 4px radius ceiling rather than a softer corner, which the kit's own doc comment
70
- calls out by name: the testimonial card had already moved to 4px, "tighter than feature cards"
71
- (`dist/index.d.ts`, doc comment on `cardVariants`). This component is exactly that testimonial card.
72
- - The avatar shows initials rather than a photo, matching the system's own rule for people without a photo ("neutral
73
- discs, navy initials"), so a marketing page and the product agree on how to represent a person with no image.
74
- - No 36px avatar token exists in `tokens/tokens.md`; `--ws-control-h-md` happens to equal 36px and is reused here
75
- for the diameter rather than writing a raw `36px` (see Source for the value this could not otherwise find a
76
- token for).
67
+ - **The person sits on the card's foot** (4 Nov 2026): side by side, a short quote left its person higher than the
68
+ next card's.
69
+ - **A wrapped card keeps its column** (4 Nov 2026): the row was a wrapping flex row, so a third card on a tablet ran
70
+ the full width under the other two. It is a grid now.
71
+ - **A figure, not a div** (4 Nov 2026): `<figure>`, `<blockquote>` and `<figcaption>`, so the quote and its source are
72
+ one thing to a screen reader.
73
+ - **The avatar is the house disc**: `--ws-surface-sunk` with heading-colour initials, the system's "neutral discs, navy
74
+ initials", in both themes (it was `--color-gray-1`, which stayed near white in dark).
75
+ - The card stays at the kit's 4px radius ceiling, the testimonial card's own "tighter than feature cards" corner.
77
76
 
78
77
  ## Source
79
78
 
80
- **Derived from**: the kit's own `Card` (no dedicated testimonial pattern), its documented 4px "tighter than feature
81
- cards" testimonial radius, and the system's neutral-disc initials-avatar rule for a person with no photo; the 36px
82
- avatar reuses `--ws-control-h-md` as the nearest existing step, since no avatar-size token this large exists.
83
- **Precedent**: an intentional addition decided from the system, 26 Oct 2026.
79
+ **Derived from**: the kit's own card, its 4px testimonial radius, the house neutral disc and the dark-safe surface
80
+ names (D12). **Precedent**: an intentional addition decided from the system, 26 Oct 2026.
81
+
82
+ ## Markup
83
+
84
+ ```html
85
+ <div class="ws-testimonial-row">
86
+ <figure class="ws-testimonial">
87
+ <blockquote class="ws-testimonial__quote">Our team closes a cold call in under six minutes now.</blockquote>
88
+ <figcaption class="ws-testimonial__who">
89
+ <span class="ws-testimonial__avatar" aria-hidden="true">PN</span>
90
+ <div class="ws-testimonial__person">
91
+ <div class="ws-testimonial__name">Priya Nair</div>
92
+ <div class="ws-testimonial__role">Team lead, Nair Realty Group</div>
93
+ </div>
94
+ </figcaption>
95
+ </figure>
96
+ </div>
97
+ ```
98
+
99
+ Styles: the `Testimonial` block (`components/Testimonial/style.css`). Nothing else.
@@ -1,6 +1,6 @@
1
- <!-- @dsCard group="Marketing" height=202 -->
1
+ <!-- @dsCard group="Marketing" height=198 -->
2
2
  <!doctype html>
3
- <html>
3
+ <html lang="en">
4
4
  <head>
5
5
  <meta charset="utf-8">
6
6
  <title>Testimonial</title>
@@ -11,27 +11,27 @@
11
11
  <div class="ws-stage ws-stage--grey">
12
12
  <div class="ws-testimonial-row">
13
13
 
14
- <div class="ws-testimonial">
15
- <p class="ws-testimonial__quote">Our team closes a cold call in under six minutes now. We book more appointments with fewer calls, not more of them.</p>
16
- <div class="ws-testimonial__who">
14
+ <figure class="ws-testimonial">
15
+ <blockquote class="ws-testimonial__quote">Our team closes a cold call in under six minutes now. We book more appointments with fewer calls, not more of them.</blockquote>
16
+ <figcaption class="ws-testimonial__who">
17
17
  <span class="ws-testimonial__avatar" aria-hidden="true">PN</span>
18
- <div>
18
+ <div class="ws-testimonial__person">
19
19
  <div class="ws-testimonial__name">Priya Nair</div>
20
20
  <div class="ws-testimonial__role">Team lead, Nair Realty Group</div>
21
21
  </div>
22
- </div>
23
- </div>
22
+ </figcaption>
23
+ </figure>
24
24
 
25
- <div class="ws-testimonial">
26
- <p class="ws-testimonial__quote">It drafts our weekly posts from our own wins. I approve one in under a minute instead of starting from a blank page every Friday.</p>
27
- <div class="ws-testimonial__who">
25
+ <figure class="ws-testimonial">
26
+ <blockquote class="ws-testimonial__quote">It drafts our weekly posts from our own wins. I approve one in under a minute instead of starting from a blank page every Friday, and the team actually reads them.</blockquote>
27
+ <figcaption class="ws-testimonial__who">
28
28
  <span class="ws-testimonial__avatar" aria-hidden="true">DS</span>
29
- <div>
29
+ <div class="ws-testimonial__person">
30
30
  <div class="ws-testimonial__name">Dana Soto</div>
31
31
  <div class="ws-testimonial__role">Broker, Soto Home Group</div>
32
32
  </div>
33
- </div>
34
- </div>
33
+ </figcaption>
34
+ </figure>
35
35
 
36
36
  </div>
37
37
  </div>