@crewlethq/ui 0.4.4 → 0.5.1

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 (231) hide show
  1. package/README.md +888 -24
  2. package/dist/Accordion/index.js +2 -2
  3. package/dist/AddPill/index.js +1 -1
  4. package/dist/{AddPill-DFWXYLIQ.css → AddPill-IJQYCY6C.css} +9 -0
  5. package/dist/AppShell/AppShell.d.ts +26 -5
  6. package/dist/AppShell/index.js +3 -3
  7. package/dist/{AppShell-NVE6LHNI.css → AppShell-XARELGIH.css} +178 -25
  8. package/dist/Avatar/Avatar.d.ts +85 -40
  9. package/dist/Avatar/AvatarStack.d.ts +76 -0
  10. package/dist/Avatar/index.d.ts +5 -2
  11. package/dist/Avatar/index.js +1 -1
  12. package/dist/Avatar-XID7Z74C.css +200 -0
  13. package/dist/AvatarStack-GEJBNV7E.css +72 -0
  14. package/dist/BrandLockup/index.js +1 -1
  15. package/dist/{BrandLockup-6ISDEHPJ.css → BrandLockup-JKGXFCZF.css} +25 -19
  16. package/dist/Button/Button.d.ts +7 -2
  17. package/dist/Button/index.js +1 -1
  18. package/dist/{Button-D4J6B4MA.css → Button-X4B2TJAQ.css} +50 -19
  19. package/dist/Callout/index.js +1 -1
  20. package/dist/Canvas/index.js +4 -4
  21. package/dist/Card/Card.d.ts +9 -5
  22. package/dist/Card/index.js +1 -1
  23. package/dist/{Card-RLR254LD.css → Card-GLP7SUV6.css} +25 -18
  24. package/dist/Charts/BarList.d.ts +18 -1
  25. package/dist/Charts/ChartTooltip.d.ts +68 -0
  26. package/dist/Charts/StackedBar.d.ts +25 -7
  27. package/dist/Charts/StackedColumns.d.ts +112 -0
  28. package/dist/Charts/TimeSeries.d.ts +33 -2
  29. package/dist/Charts/dataColor.d.ts +22 -14
  30. package/dist/Charts/index.d.ts +6 -0
  31. package/dist/Charts/index.js +1 -1
  32. package/dist/Charts/useChartHover.d.ts +56 -0
  33. package/dist/Charts-B3YEF7R4.css +742 -0
  34. package/dist/Checkbox/index.js +1 -1
  35. package/dist/{Checkbox-VGW26YLQ.css → Checkbox-BOXQ747I.css} +2 -2
  36. package/dist/CodeBlock/index.js +4 -4
  37. package/dist/Combobox/index.js +5 -5
  38. package/dist/CommandPalette/CommandPalette.d.ts +97 -6
  39. package/dist/CommandPalette/index.d.ts +1 -1
  40. package/dist/CommandPalette/index.js +9 -7
  41. package/dist/CommandPalette-C5AMKGN2.css +259 -0
  42. package/dist/Copyable/index.js +3 -3
  43. package/dist/DataTable/index.js +7 -7
  44. package/dist/{DataTable-37UWTRN6.css → DataTable-OLHPF7D4.css} +35 -38
  45. package/dist/DataView/index.js +16 -16
  46. package/dist/{DataView-256YF32J.css → DataView-N6JX464S.css} +5 -4
  47. package/dist/DateTimePicker/index.js +5 -5
  48. package/dist/DiffList/index.js +1 -1
  49. package/dist/Disclosure/index.js +2 -2
  50. package/dist/EntityChip/EntityChip.d.ts +9 -12
  51. package/dist/EntityChip/index.d.ts +1 -1
  52. package/dist/EntityChip/index.js +2 -2
  53. package/dist/ErrorBoundary/index.js +2 -2
  54. package/dist/{ErrorBoundary-ME36CQTA.css → ErrorBoundary-U2PA3LY2.css} +4 -4
  55. package/dist/IconButton/index.js +1 -1
  56. package/dist/{IconButton-VQGNMJDI.css → IconButton-TVEUWK6L.css} +2 -2
  57. package/dist/ImageUpload/ImageUpload.d.ts +10 -7
  58. package/dist/ImageUpload/index.js +2 -2
  59. package/dist/{ImageUpload-HKFIZR6D.css → ImageUpload-YCHJMUGX.css} +24 -10
  60. package/dist/Input/index.js +2 -2
  61. package/dist/Kbd/Kbd.d.ts +14 -4
  62. package/dist/Kbd/index.js +1 -1
  63. package/dist/Kbd-JPLDVJIP.css +71 -0
  64. package/dist/Layer/index.js +1 -1
  65. package/dist/Link/index.js +1 -1
  66. package/dist/List/index.js +1 -1
  67. package/dist/{List-XTHESXLR.css → List-MYJIV6MP.css} +12 -2
  68. package/dist/ListInput/index.js +6 -6
  69. package/dist/Listbox/index.js +2 -2
  70. package/dist/Listbox/useListbox.d.ts +2 -0
  71. package/dist/{Listbox-ZXYYNN6U.css → Listbox-6WJNJQVQ.css} +32 -7
  72. package/dist/Menu/index.js +4 -4
  73. package/dist/Meter/Meter.d.ts +103 -16
  74. package/dist/Meter/index.d.ts +2 -2
  75. package/dist/Meter/index.js +1 -1
  76. package/dist/{Meter-SKOOXQBC.css → Meter-5VCSFMZW.css} +46 -7
  77. package/dist/Modal/index.js +4 -4
  78. package/dist/{Modal-XYQJ4BL6.css → Modal-JAYBS2KS.css} +13 -4
  79. package/dist/OrgLabel/OrgLabel.d.ts +60 -24
  80. package/dist/OrgLabel/index.d.ts +1 -1
  81. package/dist/OrgLabel/index.js +2 -1
  82. package/dist/{OrgLabel-D7U6QJZD.css → OrgLabel-CR233G34.css} +9 -18
  83. package/dist/OrgNode/index.js +3 -2
  84. package/dist/OrgTable/OrgTable.d.ts +2 -2
  85. package/dist/OrgTable/index.js +8 -7
  86. package/dist/Popover/index.js +2 -2
  87. package/dist/PricingCard/index.js +3 -3
  88. package/dist/{PricingCard-JFI7RYXP.css → PricingCard-7EFXRSD2.css} +4 -5
  89. package/dist/SearchTrigger/SearchTrigger.d.ts +23 -4
  90. package/dist/SearchTrigger/index.d.ts +1 -1
  91. package/dist/SearchTrigger/index.js +1 -1
  92. package/dist/SearchTrigger-Z5D4AAY7.css +112 -0
  93. package/dist/SegmentedControl/index.js +2 -2
  94. package/dist/SegmentedMeter/SegmentedMeter.d.ts +107 -0
  95. package/dist/SegmentedMeter/index.d.ts +3 -0
  96. package/dist/SegmentedMeter/index.js +2 -0
  97. package/dist/SegmentedMeter-KQNML2TW.css +109 -0
  98. package/dist/Select/index.js +3 -3
  99. package/dist/Shortcut/index.js +2 -2
  100. package/dist/SidebarNav/SidebarNav.d.ts +85 -25
  101. package/dist/SidebarNav/index.d.ts +1 -1
  102. package/dist/SidebarNav/index.js +1 -1
  103. package/dist/SidebarNav-X5NLZNWA.css +424 -0
  104. package/dist/StatCard/StatCard.d.ts +59 -12
  105. package/dist/StatCard/index.d.ts +1 -1
  106. package/dist/StatCard/index.js +1 -1
  107. package/dist/StatCard-KPAFD4E2.css +204 -0
  108. package/dist/StatGroup/index.js +1 -1
  109. package/dist/{StatGroup-TGB6AEKM.css → StatGroup-HVG6O62W.css} +8 -9
  110. package/dist/StatusDot/StatusDot.d.ts +24 -6
  111. package/dist/StatusDot/index.js +1 -1
  112. package/dist/StatusDot-7OMCQCZF.css +186 -0
  113. package/dist/Stepper/Stepper.d.ts +78 -0
  114. package/dist/Stepper/index.d.ts +3 -0
  115. package/dist/Stepper/index.js +4 -0
  116. package/dist/Stepper-HW4JM3YH.css +176 -0
  117. package/dist/Tabs/index.js +1 -1
  118. package/dist/{Tabs-MJ2E3VPK.css → Tabs-XCNPHA7B.css} +46 -26
  119. package/dist/Tag/Tag.d.ts +29 -22
  120. package/dist/Tag/index.js +2 -2
  121. package/dist/{Tag-VNZEYLB6.css → Tag-GBE7LQJP.css} +82 -83
  122. package/dist/TagsInput/index.js +7 -7
  123. package/dist/ThemeSwitcher/ThemeSwitcher.d.ts +53 -21
  124. package/dist/ThemeSwitcher/index.d.ts +3 -3
  125. package/dist/ThemeSwitcher/index.js +4 -3
  126. package/dist/ThemeSwitcher/preferences.d.ts +19 -5
  127. package/dist/ThemeToggle/ThemeToggle.d.ts +40 -0
  128. package/dist/ThemeToggle/index.d.ts +2 -0
  129. package/dist/ThemeToggle/index.js +5 -0
  130. package/dist/TimeWindowPicker/index.js +8 -8
  131. package/dist/Toaster/index.js +2 -2
  132. package/dist/Toolbar/index.js +2 -2
  133. package/dist/{Toolbar-IP6LXN45.css → Toolbar-HUOSMNTH.css} +3 -1
  134. package/dist/Tooltip/index.js +2 -2
  135. package/dist/Tree/index.d.ts +2 -2
  136. package/dist/Tree/index.js +1 -1
  137. package/dist/Tree/layout.d.ts +103 -28
  138. package/dist/TreeCanvas/TreeCanvas.d.ts +60 -14
  139. package/dist/TreeCanvas/index.d.ts +1 -1
  140. package/dist/TreeCanvas/index.js +6 -6
  141. package/dist/TreeCanvas/motion.d.ts +3 -2
  142. package/dist/{TreeCanvas-WBNQPHC7.css → TreeCanvas-HVOV4SQN.css} +154 -30
  143. package/dist/TreeGrid/index.js +4 -4
  144. package/dist/{chunk-F6OE5FH3.js → chunk-2FPENTCF.js} +1 -1
  145. package/dist/chunk-4AK52VHB.js +88 -0
  146. package/dist/{chunk-7EM5T4ME.js → chunk-4IQD3HUQ.js} +2 -2
  147. package/dist/{chunk-YODNNSZL.js → chunk-4PZ6FJKW.js} +43 -13
  148. package/dist/{chunk-F27EPQ7D.js → chunk-4UQ7SMPK.js} +1 -1
  149. package/dist/chunk-5QWIJSQM.js +73 -0
  150. package/dist/{chunk-KIC7BZYJ.js → chunk-6MU7HNB4.js} +6 -5
  151. package/dist/{chunk-S2XHHEW4.js → chunk-74KMX7YH.js} +4 -4
  152. package/dist/{chunk-WNI2WSJF.js → chunk-7KBNFFM3.js} +1 -1
  153. package/dist/{chunk-EFSG7HNU.js → chunk-AUS5FNQE.js} +1 -1
  154. package/dist/{chunk-CMEE37QJ.js → chunk-BMYPGGPV.js} +4 -4
  155. package/dist/{chunk-H4FBTAYS.js → chunk-BNCCEER3.js} +8 -8
  156. package/dist/chunk-C5WJUZVR.js +56 -0
  157. package/dist/{chunk-5WS6L6FA.js → chunk-CAIHUYXV.js} +9 -9
  158. package/dist/{chunk-5KSRXHOW.js → chunk-CQY5NTRS.js} +1 -1
  159. package/dist/{chunk-MW356IVK.js → chunk-D6EVJUGU.js} +2 -2
  160. package/dist/{chunk-7PEFUMQJ.js → chunk-DHPUMOTX.js} +2 -2
  161. package/dist/{chunk-HZ5XZNPS.js → chunk-DS2C73RJ.js} +3 -3
  162. package/dist/{chunk-GKHAKGOF.js → chunk-DSUY5F4E.js} +3 -3
  163. package/dist/{chunk-Q4ZPPART.js → chunk-EFYPAJT3.js} +3 -3
  164. package/dist/{chunk-PATXSJGR.js → chunk-ERVO6OUF.js} +17 -17
  165. package/dist/{chunk-BZYDZBAN.js → chunk-ETYEBKR4.js} +3 -3
  166. package/dist/{chunk-CC67MUUO.js → chunk-FD5UEN6X.js} +30 -30
  167. package/dist/{chunk-5SDBMKUO.js → chunk-FIFNP6PL.js} +3 -3
  168. package/dist/{chunk-7HPRF5XN.js → chunk-FUF52K7E.js} +98 -16
  169. package/dist/{chunk-5KPQ6D7N.js → chunk-FZVJDJFN.js} +2 -2
  170. package/dist/chunk-IA232OTU.js +156 -0
  171. package/dist/{chunk-2EOKFKHX.js → chunk-IG27QX7Q.js} +7 -7
  172. package/dist/chunk-IKB6HFGK.js +55 -0
  173. package/dist/{chunk-T43A26SD.js → chunk-IPVQKAHO.js} +6 -6
  174. package/dist/{chunk-3EYF6HKB.js → chunk-IVFJD7WY.js} +28 -9
  175. package/dist/{chunk-XJWXPRSK.js → chunk-JGRNTBF2.js} +11 -11
  176. package/dist/chunk-K3OWD7BJ.js +121 -0
  177. package/dist/{chunk-6KCO2D5C.js → chunk-KD4H66L6.js} +7 -20
  178. package/dist/{chunk-SFZOKW2G.js → chunk-KJ34ZBBI.js} +8 -3
  179. package/dist/{chunk-AQTBF4EB.js → chunk-LFHCXZO5.js} +5 -5
  180. package/dist/{chunk-LWKJQTOM.js → chunk-MICH6QSJ.js} +1 -1
  181. package/dist/chunk-N3DPO6HG.js +226 -0
  182. package/dist/{chunk-S4GZWSKR.js → chunk-OM5E3TTK.js} +1 -1
  183. package/dist/{chunk-3QRYCPXQ.js → chunk-OPKIKM35.js} +6 -6
  184. package/dist/{chunk-5EKIWVOR.js → chunk-PGRPNDWM.js} +2 -2
  185. package/dist/{chunk-FSOW3ARP.js → chunk-PQNILU27.js} +9 -9
  186. package/dist/{chunk-QNCVHRZL.js → chunk-QVAFL2LE.js} +1 -1
  187. package/dist/{chunk-Q3B66TJM.js → chunk-QY7UU5ZB.js} +1 -1
  188. package/dist/{chunk-ITHXJHBB.js → chunk-R25RGPUB.js} +1 -0
  189. package/dist/{chunk-KLJCCMF3.js → chunk-R6735B37.js} +2 -2
  190. package/dist/{chunk-3ZP55QZE.js → chunk-R6FBDTP3.js} +4 -4
  191. package/dist/{chunk-7FZ53WDJ.js → chunk-RIAZM6L3.js} +1 -1
  192. package/dist/chunk-RJM4QCGG.js +550 -0
  193. package/dist/{chunk-NTRZGNEX.js → chunk-S3BCVWTU.js} +215 -46
  194. package/dist/chunk-S7KW47IQ.js +121 -0
  195. package/dist/{chunk-KNYYCWMK.js → chunk-SJQ546ID.js} +4 -4
  196. package/dist/{chunk-D35VTV2Z.js → chunk-SV7QAONL.js} +1 -1
  197. package/dist/{chunk-IAJLHVUA.js → chunk-TIWAHZZ6.js} +1 -1
  198. package/dist/{chunk-2PRH365R.js → chunk-UA7O3KKJ.js} +1 -1
  199. package/dist/{chunk-NPZY6YYL.js → chunk-UG5ZMAXW.js} +6 -6
  200. package/dist/{chunk-NDCATSW7.js → chunk-V34H2YCY.js} +1 -1
  201. package/dist/chunk-WIATRXEJ.js +22 -0
  202. package/dist/{chunk-5C5YMZQU.js → chunk-WOFOMTGB.js} +16 -10
  203. package/dist/{chunk-QYOFACSP.js → chunk-WSOFKOLP.js} +3 -3
  204. package/dist/{chunk-MNAXYY3R.js → chunk-WUVZDFHS.js} +31 -11
  205. package/dist/{chunk-DB2VDLVR.js → chunk-YIMOT5OO.js} +10 -10
  206. package/dist/{chunk-2ZQMA2WB.js → chunk-ZPSOF6VL.js} +2 -2
  207. package/dist/generated/index.d.ts +3 -0
  208. package/dist/generated/index.js +70 -66
  209. package/dist/index.js +70 -66
  210. package/dist/styles.css +2538 -741
  211. package/dist/utils/index.d.ts +1 -1
  212. package/dist/utils/tone.d.ts +7 -12
  213. package/package.json +4 -4
  214. package/scripts/check-css-literals.mjs +159 -23
  215. package/scripts/check-css-variables.mjs +5 -3
  216. package/scripts/css-source.mjs +97 -5
  217. package/dist/Avatar-P2KRQM2B.css +0 -156
  218. package/dist/Charts-D7G2BSXH.css +0 -277
  219. package/dist/CommandPalette-CSJDUCQA.css +0 -115
  220. package/dist/Kbd-W66MXQI5.css +0 -61
  221. package/dist/SearchTrigger-QXOYAUUY.css +0 -79
  222. package/dist/SidebarNav-QGSFKZYA.css +0 -319
  223. package/dist/StatCard-KYPKZUWX.css +0 -111
  224. package/dist/StatusDot-HA2L2FSX.css +0 -84
  225. package/dist/chunk-22O7LAW2.js +0 -41
  226. package/dist/chunk-AJNCATWZ.js +0 -144
  227. package/dist/chunk-F4PJZ4ED.js +0 -245
  228. package/dist/chunk-PNFASD6F.js +0 -98
  229. package/dist/chunk-PZSWF74H.js +0 -15
  230. package/dist/chunk-R66KLW7S.js +0 -143
  231. /package/dist/{chunk-ZCR6YGQ4.js → chunk-PKIN4IMB.js} +0 -0
package/README.md CHANGED
@@ -6,16 +6,16 @@ Crewlet shared React components. Built on top of `@crewlethq/tokens` (CSS variab
6
6
 
7
7
  ```tsx
8
8
  import '@crewlethq/tokens/css'; // the variables, once at the application entry
9
- import '@crewlethq/tokens/css/themes'; // light, dark and follow-the-system
9
+ import '@crewlethq/tokens/css/themes'; // dark, light and follow-the-system
10
10
  import '@crewlethq/tokens/css/density'; // compact and comfortable, optional
11
- import '@crewlethq/tokens/css/fonts'; // self-hosted Inter and JetBrains Mono
11
+ import '@crewlethq/tokens/css/fonts'; // self-hosted Geist and Geist Mono
12
12
  import '@crewlethq/ui/styles.css'; // every component stylesheet, in one file
13
13
  import { Button, Card } from '@crewlethq/ui';
14
- import { Book2Glyph } from '@crewlethq/icons/glyphs';
14
+ import { BookOpenGlyph } from '@crewlethq/icons/glyphs';
15
15
 
16
16
  <Card>
17
17
  <Card.Body>
18
- <Button variant="primary" leadingIcon={<Book2Glyph />}>
18
+ <Button variant="primary" leadingIcon={<BookOpenGlyph />}>
19
19
  Open documentation
20
20
  </Button>
21
21
  <Button variant="outline">Cancel</Button>
@@ -40,16 +40,18 @@ import { Button } from '@crewlethq/ui/Button'; // 8 KB of CSS, not 182 KB
40
40
 
41
41
  | Area | Components |
42
42
  | ---- | ---------- |
43
- | Actions | `Button` and `ButtonLink` (variants `primary`, `secondary`, `outline`, `tertiary`, `accent`, `danger`; sizes `small`, `medium`, `large`; shapes `square`, `pill`), `IconButton`, `Menu`, `Link`, `Copyable`, `CopyButton`, `Kbd` |
43
+ | Actions | `Button` and `ButtonLink` (variants `primary`, `secondary`, `outline`, `ghost`, `accent`, `danger`; sizes `small`, `medium`, `large`; shapes `square`, `pill`), `IconButton`, `Menu`, `Link`, `Copyable`, `CopyButton`, `Kbd` |
44
44
  | Content and text | `Text` (the eight registers), `Prose`, `InlineCode`, `List` and `ListItem`, `DescriptionList`, `Timeline`, `RelativeTime` |
45
45
  | Forms | `Input`, `Textarea`, `Label`, `FormField`, `FormRow`, `Checkbox`, `Select`, `Tag`, `TagsInput`, `DateTimePicker`, `TimeWindowPicker`, `ImageUpload` |
46
- | Data display | `DataTable`, `CopyableCell`, `Table`, `StatCard`, `PricingCard`, `Avatar`, `CodeBlock`, `Skeleton`, `Eyebrow` |
46
+ | Data display | `DataTable`, `CopyableCell`, `Table`, `StatCard`, `Meter`, `SegmentedMeter`, `Stepper`, `PricingCard`, `Avatar`, `AvatarStack`, `EntityChip`, `CodeBlock`, `Skeleton`, `Eyebrow` |
47
47
  | List screens | `DataView`, and its parts on their own: `DataViewToolbar`, `FilterAxisBar`, `FilterAxisChip`, plus the filter model (`filterDefsFromColumns`, `applyColumnFilters`, `filterPredicate`, `blankFilterValue`) |
48
48
  | Feedback and overlays | `Callout`, `Toaster`, `Modal` (with `variant="sheet"`), `ConfirmModal`, `CommandPalette`, `Tooltip`, `Popover`, `Announcer` |
49
+ | Charts | `Legend`, `BarList`, `StackedBar`, `StackedColumns`, `ActivityStrip`, `TimeSeries`, `Sparkline`, the reading layer they share (`ChartTooltip`, `useChartHover`), and the ramp (`DATA_COLORS`, `DATA_COLOR_OTHER`, `dataColor`) |
49
50
  | Marks | `Count`, `StatusDot`, `EmptyValue`, `VisuallyHidden` |
50
51
  | Hooks and seams | `useModalLayer`, `usePopupLayer`, `LayerHost`, `useListbox`, `useOptionKeys`, `useClipboard`, `useAnnouncer`, `useNow`, `formatRelative`, `HeadingLevelProvider`, `cx` |
51
52
  | Layout | `Container`, `Section`, `Card`, `Tabs`, `Accordion`, `Disclosure` |
52
53
  | Application shell | `AppShell` with `AppShell.Rail`, `AppShell.Topbar` and `useAppShell`, `SidebarNav` and `NavItem`, `BrandLockup`, `SearchTrigger`, `PageHeader`, `Toolbar`, `Stack`, `Inline`, `Spacer`, `AutoGrid`, `ErrorBoundary`, `useShortcut`, `useFullscreen` |
54
+ | Preferences | `ThemeToggle`, `ThemeSwitcher`, `DensitySwitcher`, `useThemePreference`, `useDensityPreference`, `useSystemTheme`, `resolveTheme`, `applyStoredPreferences` |
53
55
 
54
56
  Storybook stories live under `apps/storybook/src/stories`.
55
57
 
@@ -61,40 +63,117 @@ land at once.
61
63
 
62
64
  ## Theming hooks
63
65
 
64
- - **Light and dark palettes.** Import `@crewlethq/tokens/css/themes` and set
65
- `data-theme="light"` or `data-theme="dark"` on `<html>`. With the attribute
66
- absent the document follows the system. Every component reads the canonical
67
- `--color-*` variables, so no provider is needed.
66
+ - **Dark and light palettes.** Import `@crewlethq/tokens/css/themes` and set
67
+ `data-theme="dark"` or `data-theme="light"` on `<html>`. With the attribute
68
+ absent the document follows the system, and is dark in a browser that
69
+ reports no preference: the palette is dark first. `ThemeSwitcher` writes the
70
+ attribute for an explicit choice and removes it for "Follow the system";
71
+ `ThemeToggle` flips the palette actually on the screen and writes the
72
+ explicit opposite. Every component reads the canonical `--color-*`
73
+ variables, so no provider is needed. Every stored control on one
74
+ `storageKey` shows one value; an application that owns the preference holds
75
+ it with one `useThemePreference` and passes `value` and `onChange` to each
76
+ control, which then writes nothing of its own.
68
77
  - **Density.** Import `@crewlethq/tokens/css/density` and set
69
78
  `data-density="compact"` or `"comfortable"` on `<html>`. Every spacing and
70
79
  size token scales with it, and the small control and row steps floor at 24px
71
80
  so no density takes a pointer target under the size a finger can hit.
72
- - **Accent colour.** The selected, active and focus states read `--color-brand-accent` and its companions from `@crewlethq/tokens`: `--color-brand-accent-rgb` (the same colour as a comma-separated `r, g, b` triple, for translucent tints), `--color-brand-accent-hover`, `--color-brand-accent-active`, `--color-brand-accent-soft` and `--color-brand-accent-soft-strong`. Rebind them together to retint the components. Every overlay portals into the nearest `LayerHost`, or into `<body>` when
73
- there is none, so declare the override on `<html>` for those panels to follow
74
- it.
81
+ - **Surfaces.** The components stand on the four rungs `@crewlethq/tokens`
82
+ defines, lowest first the frame, the sheet, the card and raised. `AppShell`
83
+ paints the window and its rail on `--color-surface-frame` and floats the
84
+ screen, its top bar and its one scroller on a sheet of
85
+ `--color-surface-background`, inset from the frame by `--size-shell-inset`
86
+ inside the `--color-border-default` hairline at `--radius-sheet`; a card, a popover, a menu and a
87
+ dialog body are `--color-surface-subtle`, the card rung, and a `Card` is
88
+ found by the `--color-border-default` hairline it draws round itself rather
89
+ than by its fill or a shadow: the card is flat, and only an `elevated` one
90
+ lifts. Rebind a rung on `<html>` and everything standing on it follows.
91
+ - **Accent colour.** The primary `Button`, the selected, active and focus
92
+ states and the rail's attention count read `--color-brand-accent` and its
93
+ companions from `@crewlethq/tokens`: `--color-brand-accent-rgb` (the same
94
+ colour as a comma-separated `r, g, b` triple, for translucent tints),
95
+ `--color-brand-accent-hover`, `--color-brand-accent-active`,
96
+ `--color-brand-accent-ink`, `--color-brand-accent-soft`,
97
+ `--color-brand-accent-soft-strong` and `--color-focus`. The accent is PER
98
+ PALETTE (`#7c56ff` dark, `#6b45f0` light), so rebind the set together, and
99
+ under the same three selectors the theme layer paints it with: an override
100
+ on `:root` alone is beaten by `:root[data-theme="light"]` in light. Declare
101
+ it on the root rather than on `<body>` or a wrapper: `--shadow-focus`,
102
+ `--shadow-selection` and `--shadow-glow` are composed from the accent where
103
+ the tokens declare them, on the root, so they follow only an override
104
+ declared there.
75
105
 
76
106
  ```css
77
- /* Retint every accent state to the brand slate. */
78
- body {
79
- --color-brand-accent: var(--color-brand-slate);
80
- --color-brand-accent-hover: var(--color-brand-slate);
81
- --color-brand-accent-active: var(--color-brand-slate);
82
- --color-brand-accent-rgb: 60, 64, 82;
83
- --color-brand-accent-soft: rgba(60, 64, 82, 0.10);
84
- --color-brand-accent-soft-strong: rgba(60, 64, 82, 0.32);
107
+ /* Retint every accent state to an indigo, in both palettes. The hover and the
108
+ press step DARKER, so the white label gains contrast as the button is used.
109
+ Import it after @crewlethq/tokens/css/themes: each block here weighs the
110
+ same as the one it overrides, so the later one wins. */
111
+ :root {
112
+ --color-brand-accent: #5469d4;
113
+ --color-brand-accent-hover: #4a5cc2;
114
+ --color-brand-accent-active: #3e50b8;
115
+ --color-brand-accent-ink: #8b9ff5;
116
+ --color-brand-accent-rgb: 84, 105, 212;
117
+ --color-brand-accent-soft: rgba(84, 105, 212, 0.18);
118
+ --color-brand-accent-soft-strong: rgba(84, 105, 212, 0.32);
119
+ --color-focus: #8b9ff5;
120
+ }
121
+ @media (prefers-color-scheme: light) {
122
+ :root:not([data-theme="dark"]) {
123
+ --color-brand-accent: #5469d4;
124
+ --color-brand-accent-hover: #4a5cc2;
125
+ --color-brand-accent-active: #3e50b8;
126
+ --color-brand-accent-ink: #3e50b8;
127
+ --color-brand-accent-rgb: 84, 105, 212;
128
+ --color-brand-accent-soft: rgba(84, 105, 212, 0.14);
129
+ --color-brand-accent-soft-strong: rgba(84, 105, 212, 0.32);
130
+ --color-focus: #5469d4;
131
+ }
132
+ }
133
+ :root[data-theme="light"] {
134
+ --color-brand-accent: #5469d4;
135
+ --color-brand-accent-hover: #4a5cc2;
136
+ --color-brand-accent-active: #3e50b8;
137
+ --color-brand-accent-ink: #3e50b8;
138
+ --color-brand-accent-rgb: 84, 105, 212;
139
+ --color-brand-accent-soft: rgba(84, 105, 212, 0.14);
140
+ --color-brand-accent-soft-strong: rgba(84, 105, 212, 0.32);
141
+ --color-focus: #5469d4;
85
142
  }
86
143
  ```
87
144
 
88
- - **Component variables.** A few components expose their own `--crewlet-*` variables for values that are not tokens, for example `--crewlet-data-table-active-sort-color`, `--crewlet-data-table-row-hover-bg` and `--crewlet-data-table-archived-tint-rgb` on the compact `DataTable`, and `--crewlet-avatar-tint-fg` for the initials on a tinted `Avatar`. Override them from a rule that targets the component's root.
145
+ That is the accent this kit shipped before 0.5.0, and it clears every floor
146
+ the violet is held to. Hold a retint of your own to the same floors by
147
+ appending it to the theme layer the palette suite reads:
148
+ `runPalette({ tokens, themes: themes + override })` from
149
+ `@crewlethq/tokens/test/palette`.
150
+
151
+ - **Component variables.** A few components expose their own `--crewlet-*` variables for values that are not tokens, for example `--crewlet-data-table-active-sort-color`, `--crewlet-data-table-row-hover-bg` and `--crewlet-data-table-archived-rail` on the compact `DataTable`, `--crewlet-avatar-stack-ground` for the cut-out round each badge of an `AvatarStack` (the sheet, `--color-surface-background`, by default, which is where the design draws every stack; on a card or any other surface, set it to that surface), `--crewlet-spark-ground` for the ring a `Sparkline`'s current point is cut out of the ground by (the card, `--color-surface-subtle`, by default, where a sparkline stands beside its number), `--crewlet-statcard-trend-width` for the figure at the end of a `StatCard`'s value line (96px), `--crewlet-bar-list-label-width` (128px) and `--crewlet-bar-list-value-room` (`6ch` and a gap) for the words' column and the value's room in a `BarList layout="beside"`, and `--crewlet-search-trigger-width` for a `SearchTrigger variant="toolbar"` at rest (200px). Override them from a rule that targets the component's root.
89
152
 
90
153
  The component stylesheets read only variables that `@crewlethq/tokens` emits or
91
154
  that the components declare themselves, and the same check refuses a glyph
92
155
  drawn as a ligature, anything loaded from a host, a theme painted on a body
93
156
  class, the decoration step used as text, and a focus rule that turns the
94
- outline off without putting one back. `npm run lint` in this package runs it,
95
- and `npx crewlet-css-check <folder>` runs it and the literal check over an
157
+ outline off without putting one back. A second, literal check refuses a colour,
158
+ font size, radius, duration or z-index spelled out where a token belongs, a
159
+ `color-mix()`, a media query off the breakpoint steps and a hover overlay drawn
160
+ on a pseudo element. `npm run lint` in this package runs both over every
161
+ stylesheet in `src`, and `npx crewlet-css-check <folder>` runs both over an
96
162
  application's own stylesheets.
97
163
 
164
+ The literal check also pairs every motion with a stop that WINS. A rule that
165
+ starts an animation or a transition is named again, selector for selector, by a
166
+ later rule in the same file inside `@media (prefers-reduced-motion: reduce)`
167
+ that sets it to `none` (or takes the element out with `display: none`), unless
168
+ the motion only starts inside `@media (prefers-reduced-motion: no-preference)`.
169
+ The same selector, later, is the one arrangement the cascade guarantees: the
170
+ two tie on specificity and the stop wins on source order. A stop that names a
171
+ broader selector, a shorter one, or sits above the rule it stops is refused,
172
+ because each of those can lose, and each of them did in this package. "Later"
173
+ is where a browser places it: in nested CSS, declarations written after a
174
+ block nested in their rule come after that block, so a motion written below a
175
+ nested stop beats it and is refused.
176
+
98
177
  ## Overlays
99
178
 
100
179
  Every overlay in this package is on ONE layer stack, so a press of Escape
@@ -112,6 +191,791 @@ places:
112
191
  - A surface that pans or zooms dispatches `LAYER_REPOSITION_EVENT` on its host
113
192
  when the content beneath it moves, and every panel anchored inside follows.
114
193
 
194
+ ## Breaking changes in 0.5.0
195
+
196
+ 0.5.0 draws the kit in the approved visual system: Geist, a violet accent that
197
+ is the primary action, the four state hues and four chart series fitted to the
198
+ palette suite's floors, Lucide stroke glyphs, and the application shell floated
199
+ on a sheet. `@crewlethq/tokens` and `@crewlethq/icons` move with it, and each
200
+ of their READMEs carries its own "Breaking changes in 0.5.0" for the tokens and
201
+ the glyphs; upgrade the three packages together, as always. Below, every change
202
+ a consumer can see is explained where it happens, and each paragraph whose
203
+ change needs an edit ends in a **What to change** table, `Was` on the left and
204
+ `Is` on the right. A paragraph with no table needs no edit.
205
+
206
+ **The primary action is the violet accent, and it darkens as it is used.**
207
+ `@crewlethq/tokens` makes the accent a per-palette violet (`#7c56ff` in dark,
208
+ `#6b45f0` in light) and removes the monochrome `--color-brand-primary` family
209
+ and `--color-text-on-brand`. `Button`'s `primary` and `accent` variants already
210
+ painted the accent, so they change colour and nothing else: the fill is the
211
+ new violet, the label is white in both palettes, and hover and press each step
212
+ DARKER rather than brighter, because a brighter fill is a step toward a white
213
+ label (the approved design's brightening hover measured 4.18:1). The focus
214
+ ring every component draws follows the accent too.
215
+
216
+ **The rail's current row is raised, and the attention count is the accent.**
217
+ `SidebarNav` drew the row the reader is on in the accent's soft tint with the
218
+ accent ink. With the accent spent on the primary action that would read as a
219
+ second primary on every screen, so the current row now stands on
220
+ `--color-surface-elevated` with a `--color-border-default` hairline drawn
221
+ round it inside the row, in the primary ink. The hairline is an
222
+ OUTLINE rather than an inset shadow, so it survives forced-colors mode, which
223
+ drops shadows and backgrounds; a focused current row draws the focus ring in
224
+ its place. A `NavItem`'s `badge` (it was `badgeTone="attention"`, see the
225
+ rail's rows below) is the one hue in the chrome, and it is now the accent's
226
+ fill with the on-accent label rather than the warning tint and ink: a count of what is waiting on the reader asks them
227
+ to act, which is what the accent means, and warning is kept for state. The
228
+ indent guide beside a nested run steps up to `--color-border-strong` on the
229
+ reader's path rather than taking the accent.
230
+
231
+ **A resting rail row is the secondary ink again.** 0.4 drew every row at
232
+ `--color-text-primary` and its glyph on the decoration step. A row at rest now
233
+ takes `--color-text-secondary`, as the approved design draws it, and a hovered
234
+ row and the current one lift to `--color-text-primary`, so the step between
235
+ them says which row is which before the fill does. The glyph has no colour of
236
+ its own any more: it is drawn in the row's ink on every row, where it used to
237
+ be `--color-text-muted` until the row was hovered or current. Nothing in the
238
+ API changes; a stylesheet of your own that set `.crewlet-nav-item__icon svg`'s
239
+ colour to match the old ramp can drop the rule.
240
+
241
+ | Was | Is |
242
+ |---|---|
243
+ | `--color-brand-primary`, `-hover`, `-active` in a stylesheet of your own | `--color-brand-accent`, `-hover`, `-active` |
244
+ | `--color-text-on-brand` | `--color-text-on-accent` |
245
+ | an accent override declared on `:root` or `body` alone | the same values under the three theme selectors, as above |
246
+ | a stylesheet tinting `.crewlet-nav-item__row[aria-current='page']` with the accent | drop it: the row is raised with a hairline, and the hairline is its `outline` |
247
+ | a stylesheet relying on `.crewlet-nav-item__badge--attention` being the warning tint | `.crewlet-nav-item__badge`, which is `--color-brand-accent` with `--color-text-on-accent` |
248
+
249
+ **The chart ramp is four series, and a meter's ordinary reading is the first of
250
+ them.** `@crewlethq/tokens` fits the state and chart hues to the approved design
251
+ (info is working, warning needs a person, danger is stopped, success is done)
252
+ and removes `--color-data-5`. `DATA_COLORS` has four entries and `dataColor(4)`
253
+ is `DATA_COLOR_OTHER`, the residual, so every chart that colours by index
254
+ (`BarList`, `Legend`, `StackedBar`, `TimeSeries`) draws its fifth series and
255
+ beyond as "the rest". A series hue is legal only inside a figure that names it:
256
+ a legend for two series or more, the label for one. That is what a `Meter` is,
257
+ so its ordinary reading, the tone that is not a state, is `quantity`, painted in
258
+ `--color-data-1`: the accent it used to take means "act here", and a bar is not
259
+ something to act on. The three state tones and `neutral` are unchanged. The
260
+ track is the raised rung, `--color-surface-elevated`, rather than the inset
261
+ well: the palette suite measures a fill as a mark on the opaque rungs, and on
262
+ the well the light warning and success fills measured 2.84:1 and 2.86:1. So a
263
+ meter is drawn on a card, which is where the design puts every one: the palette
264
+ holds raised a visible step above the card, and on the raised rung itself an
265
+ opaque track has no edge, so the unfilled remainder, and with it the extent of
266
+ the bar, would disappear. Do not draw a meter on a raised surface.
267
+
268
+ **A chart can be read mark by mark, by pointer and by keyboard, and
269
+ `StackedColumns` is new.** `TimeSeries` stands a crosshair on the nearest
270
+ instant under the pointer and reads every series there in a tooltip;
271
+ `StackedColumns` reads a column's every part and its total; `StackedBar` reads
272
+ the part under the pointer with its value, its share and the whole, and gains
273
+ an optional `format` for those values and its default sentence, which is now
274
+ also the bar's accessible name. `BarList` takes no reading, because every row
275
+ already prints its value beside its bar. Each plot is one
276
+ tab stop, `←`/`→` walk its marks (`Home` and `End` jump to the ends, and the
277
+ first `←` opens on the newest), and `Escape` closes the reading, stopping there
278
+ only when it closed one, so a chart inside a `Modal` does not close the modal.
279
+ The tooltip is the live region, so a screen reader hears each reading, and its
280
+ words are text tokens only: a series' hue is its swatch, never the colour of a
281
+ word. Its ground is `--color-surface-glass` frosted by `--blur-sm`, not the
282
+ card: it stands over the marks beside the one it reads, and the card, a 5%
283
+ wash in the marketing palette, let every data hue through it, where the glass
284
+ keeps every word over 4.5:1 on each of them. It is placed by a React style
285
+ property (`left`) and never by a style element, so it works under a strict
286
+ Content-Security-Policy. `ChartTooltip` and
287
+ `useChartHover` are exported for a figure of your own.
288
+
289
+ `StackedColumns` takes `series` (`{ id, name, color? }`, stacked first at the
290
+ baseline) and `buckets` (`{ t, values }`, `values` keyed by series id). A
291
+ series' colour is its place in `series`, and a filter is `hidden` (ids), never
292
+ a shorter list, so hiding one series never repaints the others. It draws its
293
+ OWN legend, from `series` in the same colours with the hidden ones left out,
294
+ because a stacked figure is never drawn without one and a `Legend` written
295
+ beside it by hand was a second copy of the series to keep in step. `legend`
296
+ says where: `below` (the default) under the dates, `head` at the end of the
297
+ chart's head row, top right, as the design's Spend card draws it, beside the
298
+ title and sentence passed as `head`; or `none`, for a screen that names the
299
+ series itself. The
300
+ scale is round (`niceScale`: about four steps of 1, 2, 2.5 or 5), the parts of a
301
+ column stand 2px apart with the gap taken out of the parts, and only the value
302
+ end is rounded, at `--radius-xs`.
303
+
304
+ Marks change shape with no API change. A `BarList` bar and its track, and a
305
+ `StackedBar`'s track, are rounded at the VALUE END only (the inline end, at
306
+ `--radius-xs`) and square on the baseline, where they were pills. A
307
+ `StackedBar`'s parts stand 2px apart and grow by their value rather than
308
+ taking a percentage width. A `TimeSeries` line is 2px (it was 1.75, which a
309
+ `Sparkline` keeps). A stylesheet of your own that set a part's `width` or
310
+ relied on the pill ends should drop it.
311
+
312
+ `BarList` takes a `layout`. `stacked`, the default, is the list as it was: the
313
+ label and its value on a line, a thin bar across the row under them. `beside`
314
+ is the design's ranked figure, the Home screen's "Tokens by team" and Spend's
315
+ "By model": the label over its `sub` (at `--font-size-2xs`) in a column of their own
316
+ (`--crewlet-bar-list-label-width`, 128px, so every bar starts on one
317
+ baseline), then a 12px bar rounded at its value end, then the value at the
318
+ BAR's end in the primary ink, in a 30px row (`--size-control-md`). It has no
319
+ track. A bar is its share of the SCALE, the plot less the room kept for the
320
+ longest bar's value (`--crewlet-bar-list-value-room`, `6ch` and the gap), so
321
+ the longest value still fits and every bar is measured against one length.
322
+ The parts of a row are now separated by real spaces, in both layouts, so a
323
+ row that is a link or a button is named "planner 180" rather than
324
+ "planner180".
325
+
326
+ **A failed row in a `List` marks itself with the danger ink.** The rail and the
327
+ glyph of a `tone="danger"` item were the danger fill, which is measured to 3:1
328
+ on the opaque surfaces alone, and a failed row can also be the selected one, on
329
+ the accent tint. Both take `--color-feedback-danger-ink` now, the step the
330
+ palette suite holds on every row ground, as `DataTable`'s rails already did.
331
+
332
+ | Was | Is |
333
+ |---|---|
334
+ | `var(--color-data-5)`, or `dataColor(4)` read as a fifth hue | `DATA_COLOR_OTHER`: fold a fifth series into the rest, or split the figure |
335
+ | `<Meter tone="brand">`, the `MeterTone` `'brand'`, `meterTone()` or `progressTone()` answering `'brand'` | `'quantity'` |
336
+ | a stylesheet targeting `.crewlet-meter__fill[data-tone='brand']` | `.crewlet-meter__fill[data-tone='quantity']` |
337
+ | a stylesheet relying on `.crewlet-meter__track` being `--color-surface-inset` | it is `--color-surface-elevated` |
338
+ | a stylesheet overriding the `List` danger rail or glyph with `--color-feedback-danger` | `--color-feedback-danger-ink`, or nothing |
339
+
340
+ **A phase is a category, and a category has no colour.** `@crewlethq/tokens`
341
+ removes the phase family (`--color-phase-onboarding`, `-execute`, `-review`
342
+ and their `-ink` and `-soft` steps), so `Tag` and `StatusDot` lose the three
343
+ phase tones they painted with it, and the `PhaseTone` type is gone. A phase is
344
+ drawn as the neutral tag, or the neutral dot, with its word: beside a state
345
+ badge the state is the one coloured thing on the row, and the phase is the
346
+ word that says where the work is. The phase tag's uppercase, tracked-open
347
+ register goes with its hue, so every tag is set as the word it says. Inside a
348
+ figure a phase is a series: map each phase to an index of `DATA_COLORS` once,
349
+ in your own code, and pass `dataColor(index)` as its colour in every figure
350
+ and in the `Legend` that names it.
351
+
352
+ | Was | Is |
353
+ |---|---|
354
+ | `<Tag variant="phase-onboarding">`, `"phase-execute"` or `"phase-review"` | `<Tag>` (neutral) with the phase's word |
355
+ | `<StatusDot tone="phase-onboarding">`, `"phase-execute"` or `"phase-review"` | `<StatusDot>` (neutral) beside the phase's word |
356
+ | the `PhaseTone` type, or `TagVariant` read as including it | `Tone`; `TagVariant` is `Tone` |
357
+ | a stylesheet targeting `.crewlet-tag--phase-*` or `.crewlet-status-dot--phase-*` | `.crewlet-tag--neutral` or the bare `.crewlet-status-dot`; neither class is drawn any more |
358
+ | `var(--color-phase-*)` in a stylesheet of your own | the neutral text steps for a word, or the series you map the phase to inside a figure |
359
+
360
+ **The shell is tighter, and the drawer takes over at 1024px.** `@crewlethq/tokens`
361
+ moves the shell to the approved design's geometry: the rail is 236px rather
362
+ than 280px and the top bar 52px rather than 64px, and `--breakpoint-shell` is
363
+ 1024px rather than 900px, derived as the rail, the 8px inset between the frame
364
+ and the sheet, and the 780px sheet the two-pane Inbox needs. `AppShell` switches
365
+ to its drawer STRICTLY UNDER the step, `(width < 1024px)` in its stylesheet and in
366
+ its `matchMedia`, because at exactly 1024px the wide layout fits; it used to
367
+ switch at `(max-width: 900px)`, a pixel before the arithmetic said it had to.
368
+ Everything that changes shape with the shell moves with it and asks the same
369
+ question: `Modal`'s side sheet goes full width, `Toolbar` folds into its
370
+ overflow, a `SearchTrigger` in a bar drops its label and its hint, and `StatGroup` goes to
371
+ two columns. `AppShell`'s suite holds every one of them to that query and
372
+ asserts the sum.
373
+
374
+ The pieces that stand at the bar's height follow it down. `Modal`'s side-sheet
375
+ head is 52px with 4px over 24px insets rather than 64px with 16px over 24px:
376
+ the head centres what it holds, so the bar's height is the room round its
377
+ buttons, and the 16px padding would have stood it 12px past the bar behind it.
378
+ `BrandLockup`'s block padding is 4px rather than 8px, so the mark and the
379
+ company's line still come in under the rail's head, 47.95px at the normal
380
+ density and 49.07px at comfortable against 52px.
381
+
382
+ **The document's line is 13px on 1.45.** The baseline sets the body at the
383
+ compact step on the new body leading, and the three components drawn inside a
384
+ line of the document read that leading rather than one of their own: `Kbd`, the
385
+ initials in `Avatar`, and both lines of `BrandLockup`. `StatusDot` is 7px, the
386
+ design's mark, rather than 6px.
387
+
388
+ | Was | Is |
389
+ |---|---|
390
+ | a stylesheet of your own switching with the shell at `(max-width: 900px)` | `(width < 1024px)`, which is what `AppShell` asks |
391
+ | a literal `280px` or `64px` lined up with the rail or the bar | `var(--size-shell-rail)` or `var(--size-shell-topbar)` |
392
+ | a stylesheet overriding `.crewlet-modal__header--sheet` padding | the head is 52px with 4px over 24px insets; override `--size-shell-topbar` or the rule itself |
393
+ | a stylesheet overriding `.crewlet-brand` padding | its block padding is `--spacing-1` |
394
+ | a layout that reserved 6px for a `StatusDot` | 7px |
395
+
396
+ **The screen floats on a sheet, and the rail's rows are the design's.**
397
+ `AppShell` paints the window on the frame and draws everything the reader
398
+ reads (the bar, a banner, the one scroller, a footer) on ONE sheet beside the
399
+ rail: `--color-surface-background` inside the `--color-border-default`
400
+ hairline, rounded at `--radius-sheet` (14px) and held `--size-shell-inset`
401
+ (8px) off the frame on its top, right and bottom, the rail being the gap on its
402
+ left. It is one element because the corner has to clip the bar and the scroller
403
+ alike and the hairline has to run round both. The rail draws no right border
404
+ any more: the sheet's own hairline is the edge between them. The rail's head
405
+ stands the inset plus the bar's height tall, with the inset above it, so the
406
+ lockup is centred on the line the bar's title is on. Under the shell step the
407
+ sheet is the whole window, its inset, corner and hairline collapsed to nothing,
408
+ and the drawer draws the right border the wide rail does not. A shell with no
409
+ rail is one column (`crewlet-app-shell--no-rail`), its sheet inset on the left
410
+ as well; it used to be dropped into the empty rail's 236px cell.
411
+
412
+ `SidebarNav` draws the approved rail. A row is 30px, the new
413
+ `--size-nav-row` (it was `--size-row-md`, 36px), with 10px between its glyph and
414
+ its word, and the rows of a group stand 2px apart rather than flush. A group's
415
+ name is a sentence-case word at the caption step in the tertiary ink rather
416
+ than an uppercase micro-label, on a heading that stands at the 24px target
417
+ floor, and `NavGroup` takes an `action` (`{ label, icon, onClick }`): a control
418
+ at the heading's end, the add beside "Projects", named by its label and
419
+ reached from the keyboard between the row above and the group's first row. A
420
+ figure at the end of a row is one of two props now, where it was a `badge` of
421
+ any element with a `badgeTone`: `count` is how many of something the
422
+ destination holds, a quiet tabular figure in the tertiary ink with an optional
423
+ `mark` before it, and `badge` is how many things are waiting on the reader, the
424
+ one filled pill in the chrome, in the accent. Both are values (`{ value, label }`),
425
+ the rail paints them, and both are read as the end of the row's name with their
426
+ words: "Inbox, 5 unread", "Triage, 12 open, 2 unread". `lead` is a short key
427
+ drawn as a chip in the glyph's place, a project's `ENG` on the raised rung in
428
+ mono, and read as the start of the name: "ENG Core platform".
429
+
430
+ The rail's foot never cuts what a row says. An `AppShell.RailRow`'s words took
431
+ whatever its mark and its `trailing` value left them and ended in an ellipsis,
432
+ so a pill beside the engine's status left "engine conn…". The words and the
433
+ value now share one wrapping line: the value stands at the end of the words'
434
+ line while both fit, and drops to the start of a line under them when they do
435
+ not, and words wider than the rail wrap. A `Button` dropped into the foot wraps
436
+ its label the same way. `detail` is new: a second line under the words, which
437
+ makes the row the design's status block ("Engine healthy" over "3 nodes ·
438
+ config epoch 42"), on `--color-surface-subtle` inside the
439
+ `--color-border-default` hairline at `--radius-lg`, the words in the primary
440
+ ink at the medium weight and the detail at `--font-size-2xs` in the tertiary
441
+ ink. The row keeps its inline pad, so its mark stays on the nav glyphs' line.
442
+ A row with no `detail` is drawn as before. The row's content is now wrapped in
443
+ `.crewlet-app-shell__rail-row-body`, holding `.crewlet-app-shell__rail-row-text`
444
+ (the label and the detail) and the trailing value.
445
+
446
+ | Was | Is |
447
+ |---|---|
448
+ | a stylesheet relying on `.crewlet-app-shell__rail-row-label` truncating, or on `.crewlet-app-shell__rail-row-trailing` being a direct child of the row pushed to its end by `margin-left: auto` | the label wraps; the trailing value is inside `.crewlet-app-shell__rail-row-body` and moves under the words when both do not fit |
449
+ | a stylesheet targeting `.crewlet-app-shell__column` | `.crewlet-app-shell__sheet` |
450
+ | a stylesheet relying on `.crewlet-app-shell` being `--color-surface-background` | it is `--color-surface-frame`; the sheet is `--color-surface-background` |
451
+ | a stylesheet relying on the rail's `border-right` | the sheet's hairline is the edge; the rail draws one only as the narrow drawer |
452
+ | a stylesheet relying on `.crewlet-app-shell__rail-head` standing at `--crewlet-app-shell-topbar` | it stands `--size-shell-inset` plus that, with the inset as its top padding, and at the bar's height under the step |
453
+ | `<NavItem badge={<Count value={3} label="need a person" />} badgeTone="attention">` | `<NavItem badge={{ value: 3, label: 'need a person' }}>` |
454
+ | `<NavItem badge={<Count value="4 live" />}>`, a badge with no tone | `<NavItem count={{ value: 4, label: 'live' }}>` |
455
+ | the `NavBadgeTone` type, or `badgeTone` | gone: `badge` is the accent pill and `count` the quiet figure |
456
+ | a stylesheet targeting `.crewlet-nav-item__badge--attention` | `.crewlet-nav-item__badge`, which is always the accent pill; the quiet figure is `.crewlet-nav-item__count` |
457
+ | a stylesheet styling what was passed into `.crewlet-nav-item__badge` | nothing is passed in: the figure is drawn by the rail, `aria-hidden`, and its words are read at the end of `.crewlet-nav-item__label` |
458
+ | a layout that counted on a rail row standing 36px | `--size-nav-row`, 30px |
459
+ | a stylesheet relying on `.crewlet-nav-group__label` carrying the heading's padding, uppercase and tracked | the padding is on `.crewlet-nav-group__head`; the label is sentence case at `--font-size-xs` |
460
+
461
+ **A working dot breathes rather than fades, and every stop is one that wins.**
462
+ A pulsing `StatusDot` keeps its fill and breathes a halo out round it, in its
463
+ tone's own soft step: out to 5px and back, once every `--motion-duration-breath`
464
+ (2.2s), the approved design's working pulse. It used to fade the dot itself to
465
+ 0.35 and back every 1.8s, and at the bottom of that fade every fill measured
466
+ 1.35:1 to 2.17:1 against its ground, under the 3:1 a mark clears. The design
467
+ casts the halo from the dot itself, where it would touch the dot for the whole
468
+ round and become its ground: on their own soft step the light info, success and
469
+ warning fills measure 2.65:1 to 2.95:1, the same fault at a smaller size. So the
470
+ halo is a `box-shadow` cast by the dot's `::after`, a ring box 2px outside it
471
+ (the offset the `Avatar` state ring stands at), and a shadow is never painted
472
+ inside the box that casts it: 2px of the real ground always stands between the
473
+ dot and its halo, and the halo spreads 3px beyond that, to the design's 5px. It
474
+ takes no room and moves nothing beside the dot. The pulsing dot is the ring's
475
+ containing block through a `:where()` rule, which weighs nothing, so a
476
+ `position` of your own on it still wins. The neutral dot, whose family has no
477
+ soft step, breathes in `--color-surface-pressed`. Every halo stands at least
478
+ dE 3 off every surface, the floor the palette suite holds every "can a reader
479
+ notice it" difference to. Under `prefers-reduced-motion: reduce` the pulse is
480
+ held: the dot rests at its fill with no halo, and the word beside it still says
481
+ the work is under way.
482
+
483
+ Because the halo takes no room, a word set beside a pulsing dot at an ordinary
484
+ mark's 4px was struck by it at every peak. `StatusDot` takes that word now, as
485
+ its `children`: `<StatusDot tone="info" pulse>Reviewing !231</StatusDot>` is one
486
+ line, `.crewlet-status` (which then takes `className`), the hidden dot and the
487
+ read word 7px apart, which is the halo's 5px reach and the 2px of ground that
488
+ stands between the dot and its halo again on the far side. The gap is fixed at
489
+ every density, like the dot and its halo, and the same for every tone, pulsing
490
+ or not, so a column of statuses starts its words on one line. The word is one
491
+ line and ends in an ellipsis rather than the dot giving way. A dot with no
492
+ children is drawn exactly as before.
493
+
494
+ The literal check behind `crewlet-css-check` used to accept any stylesheet that
495
+ MENTIONED `prefers-reduced-motion`, and three components here passed it while
496
+ still moving for a reader who had asked them not to. `DataTable` stopped its
497
+ motion with one catch-all, `.crewlet-data-table.crewlet-data-table *`, and its
498
+ row's own hover transition outranked it, so the row still eased; `TreeCanvas`
499
+ stopped the ghost card of a node being added by its own class, one class short
500
+ of the rule that starts its arrival; and its `node` appearance stopped its quiet
501
+ controls in a rule above the ones that fade them in, so source order handed
502
+ every tie back to the fade. Each is now stopped by name, after the rule it
503
+ stops, and the check refuses anything else (see "Theming hooks" above).
504
+ `DataTable` stops exactly what its own stylesheet starts, so motion of your own
505
+ inside a table is stopped by your own stylesheet rather than by the table's.
506
+ The check also reads a top-level media query now: it read only rule selectors,
507
+ so a query outside every block was never checked against the breakpoint scale.
508
+ In nested CSS it reads declarations written after a nested block where a
509
+ browser places them, after that block, so a motion written below a nested stop
510
+ is refused rather than read as coming first.
511
+
512
+ | Was | Is |
513
+ |---|---|
514
+ | a stylesheet relying on a pulsing `StatusDot` fading, or setting its `opacity` | the dot never fades; the pulse is a `box-shadow` halo on its `::after` |
515
+ | a `::after` of your own on a pulsing `StatusDot` | the pulse draws its ring there; decorate a wrapper instead |
516
+ | a pulsing `StatusDot` left `position: static` | it is `position: relative` at no specificity, as the ring's containing block; a `position` of your own still wins, and any but `static` keeps the ring on the dot |
517
+ | motion of your own inside a `DataTable`, left to the table's reduced-motion catch-all | a stop in your own stylesheet: the table no longer stops what it did not start |
518
+ | a stylesheet `crewlet-css-check` passed because it mentioned `prefers-reduced-motion` | name each selector that starts a motion again, in a later `@media (prefers-reduced-motion: reduce)` rule, set to `none` |
519
+ | a top-level `@media` query of your own at a length off the breakpoint scale | one of the `--breakpoint-*` steps; the check reads the query now |
520
+
521
+ **Every glyph is a Lucide stroke.** `@crewlethq/icons` replaces the Material
522
+ Symbols set with Lucide drawings, stroked at the approved design's 1.75 on the
523
+ 24 grid with round caps and joins, so every glyph a component draws changes
524
+ drawing, and the names a component imported change with them; the package's
525
+ README has the full table. Nothing here takes a glyph by name, so a component's
526
+ own glyphs need no edit from you. Two kinds of stylesheet do.
527
+
528
+ A rule that sets `fill` on an SVG it does not own now paints the INSIDE of a
529
+ stroke drawing, which turns a closed shape solid and an open one into a smear.
530
+ `AddPill` and `OrgLabel` both had one, to take a brand mark's own fill away,
531
+ and both now leave a `.crewlet-glyph` alone and give it their ink through its
532
+ `color`, which the stroke follows. A glyph's weight is
533
+ `--crewlet-glyph-stroke` on an ancestor, which is the one way to move it.
534
+
535
+ The `Toaster`'s status glyphs were Material's FILLED drawings, a solid disc or
536
+ triangle with the mark knocked out. A stroke set has no such drawing, so a toast
537
+ draws the outline, in the same tone, beside the same title word.
538
+
539
+ | Was | Is |
540
+ |---|---|
541
+ | a glyph passed to a component under its Material name (`icon={<CloseGlyph />}`) | its Lucide successor from `@crewlethq/icons`' table (`icon={<XGlyph />}`) |
542
+ | a stylesheet of your own setting `fill` on the SVGs inside a component, or on `.crewlet-glyph` | `color` on the element; the glyph's stroke is `currentColor` |
543
+ | a stylesheet setting `stroke-width` on `.crewlet-glyph` | `--crewlet-glyph-stroke` on an ancestor |
544
+ | a toast relying on the filled status glyph | the outline, in the same `--crewlet-toast-mark` tone |
545
+
546
+ **An agent is a squircle and a person a circle, and a state is a ring.**
547
+ `Avatar` takes a `kind` in place of its `shape` and its `variant`: `agent`, the
548
+ default, is a squircle with its initials in the mono face, and `human` is a
549
+ circle with them in the sans face. That outline is the one cue telling the two
550
+ apart. There were two, a square or a circle and a solid or a dashed edge, and
551
+ nothing stopped them disagreeing; the dashed edge is gone, from the badge and
552
+ from everything that drew it (`EntityChip`, the dashed ring `OrgLabel` drew
553
+ round a glyph and the dashed card `TreeCanvas` drew for a person's seat). An
554
+ agent's corner is 0.29 of its box, the approved design's 7px at 24 and 9px at 30
555
+ as one proportion, and a numeric size now goes through the same rule as a step
556
+ by setting the same `--crewlet-avatar-size`; where the browser draws
557
+ `corner-shape: squircle` it is a whole squircle, at the circle token, which
558
+ stands as deep at the diagonal as that corner does. The neutral badge is the
559
+ design's: the raised rung, the strong hairline and the secondary ink.
560
+
561
+ A name of ONE word now gives two initials too, as the design's org chart draws
562
+ its seats (`getInitials`, which the badge uses). Words split on whitespace,
563
+ hyphens, underscores and dots as before and give the first letter of each of
564
+ the first two; a single word gives the first letters of its first two case
565
+ humps where its case says it is two words run together (`DevRel` is DR,
566
+ `McCall` MC), and its first two characters otherwise (`CEO` is CE, `SWE` SW,
567
+ `PM` PM, `Acme` AC). One letter each made the CEO and the CTO the same `C`.
568
+
569
+ | Was | Is |
570
+ |---|---|
571
+ | a test or a layout that counted on a one-word name giving one initial (`Acme` as `A`) | two: `AC`, and `DR` for `DevRel` |
572
+
573
+ The `brand` tone is gone, because the accent means where the reader is, the
574
+ primary action and focus, and a badge filled with it was identity drawn in that
575
+ colour. A new `ring` draws a state instead: a 1.5px outline 2px outside the
576
+ badge in `info`, `warning`, `danger` or `success`'s own fill, or the accent for
577
+ `brand`, which means selected. The gap is transparent, so the ring reads on any
578
+ ground, and it is an outline, so forced-colors mode keeps it; in that mode every
579
+ ring is one system colour, so say the state in words beside the badge. An image
580
+ keeps its kind's outline and its ring.
581
+
582
+ `AvatarStack` is new: badges that overlap by 6px, each cut out of the one before
583
+ by a 2px ring of `--crewlet-avatar-stack-ground` (the sheet by default, where
584
+ the design draws every stack; a stack on a card sets it to the card's rung),
585
+ with the members past `max` (four by default) counted in a pill. Its badges are
586
+ the `sm` step by default rather than the design's 22px: the design sets their
587
+ initials at 9px, under this kit's 11px floor, and `sm` is the smallest step at
588
+ which the overlap hides no more of two initials than the design's own stack
589
+ does. It is one image to a screen reader, named for how many of each kind and
590
+ every name, "2 agents and 1 person: Jane Founder, CTO, SWE", or by a `label` of
591
+ your own, or silent with `decorative` where the words beside it already say it.
592
+
593
+ `OrgLabel`, and so `OrgNodeLabel` and `OrgTableName`, lead a seat with its
594
+ badge: `avatar={{ name, kind }}` in place of `icon`, read by nobody, since the
595
+ name is beside it. It is the approved org chart's 26px badge (the `sm` step) in
596
+ a node and the smallest step, 20px, in a row, larger than a container's glyph in
597
+ either, so a seat never reads as a unit. `ImageUpload` takes the picture's
598
+ `kind` in place of its `shape`; `agent` is the squircle and is what a company's
599
+ mark takes too, since the circle is a person's.
600
+
601
+ `TreeCanvas`'s `cardOutline` is removed, with the
602
+ `.crewlet-tree-canvas__card--outline` class it set. It drew a dashed edge round
603
+ the card of a seat a person holds, which was the same second cue drawn in a
604
+ card's frame, and the approved org chart draws a person's seat on the same solid
605
+ card as an agent's, with the badge's circle saying who holds it. For the same
606
+ reason a `Card` of the `dashed` variant no longer stands for a person's seat. A
607
+ dashed edge stands for a person nowhere in the kit now: it marks a place nothing
608
+ fills yet, which is what that `Card` variant, a unit's missing lead and the
609
+ ghost of a node being added all draw.
610
+
611
+ | Was | Is |
612
+ |---|---|
613
+ | `<Avatar shape="square">`, or no `shape` | `<Avatar>`, or `kind="agent"` |
614
+ | `<Avatar shape="circle">` | `<Avatar kind="human">` |
615
+ | `<Avatar variant="dashed">` | `<Avatar kind="human">` |
616
+ | `<Avatar variant="solid">` | drop it |
617
+ | `<Avatar tone="brand">` for the reader's own badge or a selected one | `<Avatar ring="brand">`, on the neutral badge |
618
+ | the `AvatarShape` and `AvatarVariant` types | `AvatarKind`; `AvatarTone` is `'neutral' \| 'seeded'` |
619
+ | `avatarSquareCorner(px)`, which answered a radius token | `avatarCorner(px)`, which answers the corner in px (`px * AVATAR_CORNER_RATIO`) |
620
+ | a stylesheet targeting `.crewlet-avatar--square` or `.crewlet-avatar--circle` | `.crewlet-avatar--agent` or `.crewlet-avatar--human` |
621
+ | a stylesheet targeting `.crewlet-avatar--dashed` or `.crewlet-avatar--brand` | nothing is drawn with either any more |
622
+ | a stylesheet relying on the neutral badge's `--color-border-default` hairline | it is `--color-border-strong` |
623
+ | a stylesheet reading a numeric badge's inline `width` and `height` | its inline `--crewlet-avatar-size` |
624
+ | `<EntityChip variant="human">`, and the `EntityChipVariant` type | `<EntityChip kind="human">`, typed `AvatarKind` |
625
+ | `<OrgNodeLabel iconRing="dashed" icon={…}>`, or the same on `OrgLabel` or `OrgTableName` | `avatar={{ name, kind: 'human' }}` in place of `icon` |
626
+ | `iconRing="none"` | drop it |
627
+ | a stylesheet targeting `.crewlet-org-label__icon--dashed` | the badge inside the zone, `.crewlet-org-label__icon .crewlet-avatar--human` |
628
+ | `interface … extends OrgLabelContent`, `OrgLabelNodeProps`, `OrgLabelRowProps` or `OrgTableNameProps` | an intersection: each is a type now, over `OrgLabelMark` (a glyph or a badge, never both) |
629
+ | `<TreeCanvas cardOutline={…}>` | drop it: a person's seat is the same solid card as an agent's, and its badge's circle says who holds it |
630
+ | a stylesheet targeting `.crewlet-tree-canvas__card--outline` | nothing is drawn with it any more |
631
+ | `<Card variant="dashed">` round a seat a person holds | the card it would take for an agent's seat; the badge's circle says who holds it |
632
+ | `<ImageUpload shape="square">`, or no `shape` | `<ImageUpload>`, or `kind="agent"` |
633
+ | `<ImageUpload shape="circle">` | `<ImageUpload kind="human">` |
634
+ | a stylesheet targeting `.crewlet-image-upload--square` or `.crewlet-image-upload--circle` | `.crewlet-image-upload--agent` or `.crewlet-image-upload--human` |
635
+
636
+ **The kit's primitives are the approved design's.** A control stands at the
637
+ design's control rhythm: `@crewlethq/tokens` moves `--size-control-md` to 30px
638
+ and `--size-control-sm` to 26px, so a medium `Button` is 30px and a small one
639
+ 26px, and so is every icon button, field, select and tab on the same line,
640
+ which read the same steps. `secondary` is the card's own ground inside the
641
+ STRONG hairline, the design's button: a button stands on the very card it is
642
+ filled with, so its boundary is the whole of it, and the default hairline is
643
+ the panel's own divider. It steps up to the raised rung and
644
+ `--color-border-strong-hover` under the pointer. `danger` holds the same weight
645
+ until it is pointed at, and `IconButton`'s `secondary` square follows. The
646
+ borderless button is `ghost`, the word `IconButton`'s borderless square already
647
+ used, so a toolbar holding both is written in one vocabulary; `tertiary` is
648
+ gone. `Menu`'s `triggerVariant`, `ConfirmModal`'s `cancelVariant` and
649
+ `CopyButton`'s `variant` default to it.
650
+
651
+ **The card is flat.** A `Card` is the card rung inside the default hairline at
652
+ the 12px corner, and nothing else, in both palettes: the `default` variant no
653
+ longer casts `--shadow-xs` or draws a rim of light on its `::after`, and an
654
+ interactive card answers the pointer with its hairline rather than lifting.
655
+ What finds a card on the sheet is the pair the palette suite holds, the rung's
656
+ step and the hairline's. `elevated`, which genuinely stands over the page, is
657
+ the one that lifts. Every copy of the card recipe follows (`StatCard`,
658
+ `StatGroup`, `ErrorBoundary` and `PricingCard`), and in following it drops a
659
+ defect: each carried `box-shadow: var(--shadow-xs), var(--shadow-hairline)`,
660
+ and `--shadow-hairline` is `none` in the light palette, which makes the whole
661
+ list invalid there, so every one of them drew no shadow at all in light and two
662
+ in dark. A selected `PricingCard` draws `--shadow-selection` alone, and the
663
+ glow beside it where it has a discount. The scan that caught this in `Card`
664
+ alone now reads every stylesheet in the package.
665
+
666
+ **A `Tag` is the design's pill.** The default is 22px with fully round ends,
667
+ the 12px caption step at the medium weight, 9px inside each end and 6px between
668
+ a dot or a glyph and its word. `xs` is the design's 18px count pill at the 11px
669
+ step and `md` a small control's 26px. The neutral tag is the raised rung under
670
+ the secondary ink, the design's quiet pill, rather than the inset overlay, and
671
+ it is drawn inside the strong hairline: its fill IS a surface, so on a raised
672
+ ground (a palette's lead block, a raised card, the rail's current row) a
673
+ borderless one vanished into loose words. Under the pointer an acting neutral
674
+ tag takes the strong step once more. Every state keeps its soft tint under its
675
+ ink and no resting boundary. A tag that acts still clears 24px.
676
+
677
+ **A `Kbd` is the design's keycap, and a chord is one cap.** The cap is the
678
+ raised rung inside the strong hairline, flat on every side, in the mono face at
679
+ the 11px step and the tertiary ink; the heavier bottom edge said "raised" a
680
+ second time and stood the cap a pixel taller than its line. `subtle` gives up
681
+ the fill and takes the default hairline. `keys` draws the whole shortcut on ONE
682
+ cap, as the design draws every shortcut it shows: `⌘K` on an Apple platform,
683
+ where the menus run a chord's symbols together, and `Ctrl+K` everywhere else,
684
+ where they join the names with a plus. Each key is a `kbd` inside the cap, and
685
+ the one sentence a screen reader is given is unchanged.
686
+
687
+ **A segmented row is a raised well with a card chip in it.** `Tabs` with
688
+ `variant="pill"` and `SegmentedControl` draw their well on the raised rung inside
689
+ the default hairline, where the inset overlay stood almost nowhere off a card
690
+ and the chips floated. The chip that is on is the card's own ground in the
691
+ primary ink, lifted by the default hairline and `--shadow-xs`; the others are
692
+ the secondary ink on nothing. A chip is 24px with 10px inside each end and the
693
+ `--radius-sm` corner, so the well with its 1px boundary and 2px inset stands at
694
+ exactly the 30px of the button beside it.
695
+
696
+ **`SearchTrigger` is the rail's field by default.** The approved design puts
697
+ search in the sidebar: the full width of the rail, 34px tall (the medium
698
+ control and one scale step), on the sheet's ground one rung above the frame,
699
+ with its label and its `⌘K` cap at every width, because under the shell
700
+ breakpoint the rail is a drawer as wide as it ever was. The field a screen
701
+ draws in its own bar is `variant="toolbar"`: the card's ground at the control
702
+ height, 200px at rest, and still the form that folds to its glyph under the
703
+ shell breakpoint.
704
+
705
+ **A `StatCard` is the design's tile.** Its label is set in sentence case at the
706
+ 12px caption step, as the caller wrote it, where it was uppercased and tracked
707
+ open in the column-head register. The value is `--font-size-display`, the
708
+ screen's one display number, in tabular figures. Two slots are new. `trend`
709
+ stands a small figure at the END of the value's line, such as a `Sparkline`,
710
+ a `Meter` or a `SegmentedMeter`, or a small control that acts on the number,
711
+ in a slot `--crewlet-statcard-trend-width` wide (96px); a figure fills it and a control
712
+ keeps its own width at its end. `delta` heads the second line with a change
713
+ against an earlier reading, `{ value, polarity }`, drawn in the ink of whether
714
+ it was WANTED (`good` the success ink, `bad` the danger ink, `neutral` the
715
+ secondary ink) rather than of its direction, which its sign already says:
716
+ "+12% vs previous 7 days". Neither is drawn while the tile is loading. The
717
+ second line reserves exactly one line of its own leading, where a reserve in
718
+ `em` was a line shorter than the one a sub draws, and a value too long for its
719
+ tile now ends in an ellipsis rather than being cut mid-digit. `subTone` is new:
720
+ the second line in a state's ink (`success`, `warning`, `danger` or `info`, the
721
+ `-ink` step), for a line that reports a state rather than a fact, as the
722
+ design's "Oldest waiting 2h 10m" is a person kept waiting and takes the warning
723
+ ink. It paints the line and never the value, a `delta` in the line keeps its
724
+ own polarity's ink, and the default, `neutral`, is the quiet ink as before.
725
+
726
+ **A `Sparkline` is the neutral, and `current` marks now in the accent.** The
727
+ line's default colour is `DATA_COLOR_OTHER`, the residual neutral, at the
728
+ design's 1.75 stroke: a shape beside a number is named by that number and not by
729
+ a legend, so a series hue has nothing to be named by. `current` marks the last
730
+ value with one accent point, cut out of its ground by a 2px ring of
731
+ `--crewlet-spark-ground`. The point is its own element over the plot, since the
732
+ plot is stretched to its box and a circle drawn inside it would be an ellipse,
733
+ so the figure's root is a `div.crewlet-spark` holding the plot's
734
+ `svg.crewlet-spark__plot`.
735
+
736
+ **A `Meter`'s track is 6px** with round ends at both sizes, the design's meter.
737
+
738
+ | Was | Is |
739
+ |---|---|
740
+ | `<Button variant="tertiary">` or `<ButtonLink variant="tertiary">`, the `ButtonVariant` `'tertiary'` | `variant="ghost"`, `'ghost'` |
741
+ | `<Menu triggerVariant="tertiary">`, `<ConfirmModal cancelVariant="tertiary">`, `<CopyButton variant="tertiary">` | `"ghost"`, which is the default of all three, as `tertiary` was |
742
+ | a stylesheet targeting `.crewlet-btn--tertiary` | `.crewlet-btn--ghost` |
743
+ | a stylesheet relying on the `--color-border-default` boundary of a `secondary` or `danger` `Button`, or a `secondary` `IconButton` | it is `--color-border-strong`, and `--color-border-strong-hover` under the pointer |
744
+ | a layout lined up with a 32px or 28px control | 30px and 26px; read `--size-control-md` and `--size-control-sm` rather than a number |
745
+ | a stylesheet relying on a `default` `Card`'s shadow or its `::after` rim, or on an interactive card lifting on hover | the card is flat; `variant="elevated"` is the one that lifts |
746
+ | a stylesheet relying on the shadow of a `StatCard`, a `StatGroup`, an `ErrorBoundary` or a `PricingCard`, or the rim on a selected `PricingCard` | each is flat; a selected `PricingCard` draws `--shadow-selection` (and `--shadow-glow` with a discount) |
747
+ | a `Tag` measured at 20px with a 4px corner and an 11px label | 22px, `--radius-pill` and `--font-size-xs`; `size="xs"` is 18px at `--font-size-2xs`, `size="md"` is 26px |
748
+ | a stylesheet relying on the neutral `Tag`'s `--color-surface-inset` fill, or on its transparent boundary | it is `--color-surface-elevated` inside a 1px `--color-border-strong` boundary, and `--color-border-strong-hover` under the pointer |
749
+ | a stylesheet relying on `<Kbd keys>` drawing one `.crewlet-kbd` per key inside `.crewlet-kbd-keys` | one `.crewlet-kbd` cap holds a `kbd.crewlet-kbd__key` per key; `.crewlet-kbd-keys` is gone |
750
+ | a stylesheet relying on a `Kbd`'s 2px bottom edge, its `--color-border-default` boundary or its inset fill | a 1px `--color-border-strong` boundary on `--color-surface-elevated`; `subtle` is `--color-border-default` and no fill |
751
+ | a stylesheet relying on a pill `Tabs` or `SegmentedControl` well on `--color-surface-inset` with no boundary | it is `--color-surface-elevated` inside a 1px `--color-border-default` boundary |
752
+ | a pill chip measured at 26px with 12px side pads and the `--radius-chip` corner, or in the tertiary ink | 24px, 10px and `--radius-sm`, in `--color-text-secondary` |
753
+ | `<SearchTrigger>` in a top bar or a toolbar | `<SearchTrigger variant="toolbar">`; without it, it is the rail's full-width field |
754
+ | `--crewlet-search-trigger-width` set on a `SearchTrigger` | the same property on the `toolbar` form, which is the only one it sizes; its default is 200px rather than 220px |
755
+ | a stylesheet selecting `.crewlet-statcard > .crewlet-statcard__value` | the value is inside `.crewlet-statcard__reading`, beside `.crewlet-statcard__trend` |
756
+ | a `StatCard` label written for the uppercase register | write it in sentence case; it is drawn as written |
757
+ | a `StatCard` value measured at `--font-size-2xl` | `--font-size-display` |
758
+ | a `Sparkline` relying on the accent as its default colour | `DATA_COLOR_OTHER`; pass `color` for another |
759
+ | a stylesheet selecting the `Sparkline` root as an `svg` | the root is `div.crewlet-spark`; the plot is `svg.crewlet-spark__plot` inside it |
760
+ | a `Meter` track measured at 4px | 6px |
761
+
762
+ **`SegmentedMeter` is new: the design's state bar.** A whole divided into the
763
+ states its parts are in, `segments: { id, value, tone, label }[]` against a
764
+ `total`: how many seats are working (`info`), waiting on a person (`warning`),
765
+ stopped (`danger`) and done (`success`), or how much of a project is done,
766
+ active and still to do. Each part is its share of the whole, 2px from the
767
+ next, in its tone's fill step; what the parts leave of `total` is the
768
+ remainder, drawn last in `--color-border-strong`, which stands at least dE
769
+ 7.99 off every rung, so the whole bar is seen on any ground. There is no
770
+ neutral part, because the quiet part of a whole IS the remainder. The bar is
771
+ 8px, the figure at the end of a stat tile's value line (the `trend` slot takes
772
+ it), and `size="compact"` is the design's 6px progress bar under a project's
773
+ name. A part of 0 is not drawn at all, so it leaves no gap behind it, and no
774
+ part is ever narrower than the kit's 7px status dot, or than an equal share of
775
+ the bar where so many parts would not fit at 7px. It is one image to a screen
776
+ reader, named for every part it draws and the whole, "4 working, 1 waiting,
777
+ 1 stopped, 1 idle of 7", with `remainderLabel` naming the remainder;
778
+ `segmentedMeterLabel()` answers the same words, `label` replaces them for a
779
+ figure with a unit, and `decorative` silences a bar whose figures are written
780
+ beside it. A total under its parts draws the parts against their own sum and
781
+ keeps the figures it was given in the name, as a `Meter` past its maximum
782
+ does; a negative or non-finite count, and two parts with one id, are refused
783
+ with a `RangeError` that names them.
784
+
785
+ **`Stepper` is new: where a sequence has got to.** The design's step row, an
786
+ ordered list of `steps: { id, label }[]` with `current` naming the step under
787
+ way: the steps before it are done, in the secondary ink behind a check and
788
+ said as "Done" before their words (`doneLabel` for another language); the
789
+ current one is a pill of its tone's soft tint under its ink with the tone's
790
+ dot, `info` by default (`warning`, `danger`, `success` and `brand` for a step
791
+ parked, stopped, finished or where the reader is), and carries
792
+ `aria-current="step"`; the steps after it are the tertiary ink. A 10px rule in
793
+ `--color-border-strong` joins two steps. `current={null}` is a finished
794
+ sequence, every step done and none current, which is how a turn's history is
795
+ drawn. `pulse` breathes the current dot with `StatusDot`'s own pulse, held
796
+ still under a reduced-motion preference. The tone is drawn and never spoken,
797
+ so a step whose state is not ordinary work says so in its own words ("Review ·
798
+ sent back"). A `current` that names none of the steps, and two steps with one
799
+ id, are refused with a `RangeError` that names them.
800
+
801
+ **A `Meter`, and both new figures, are drawn in forced-colors mode.** The mode
802
+ repaints every author background to `Canvas`, and a meter's track and fill are
803
+ nothing but backgrounds, so the whole bar used to disappear for the reader who
804
+ asked for more contrast. A system colour an author writes is kept, so in that
805
+ mode a `Meter`'s fill and a `SegmentedMeter`'s parts are `CanvasText`, the
806
+ track and the remainder `GrayText`, and a `Stepper`'s current step is
807
+ `HighlightText` on `Highlight`, the system's own drawing of a selection, with
808
+ the rule between two steps in `GrayText`. The hues go, as every hue does in
809
+ that mode; the lengths and the current step stay.
810
+
811
+ **`CommandPalette` takes scopes, a lead and a row's meta.** The design's ⌘K.
812
+ `scopes: { id, label, count? }[]` with a controlled `scope` / `onScopeChange`
813
+ draws a row of tabs under the field (the chosen one in the primary ink over a
814
+ 2px bar, a `count` as a `Count`), and they come as a SET: the types refuse
815
+ one without the others, and without `groupsScope`, which names the scope
816
+ `groups` was computed for. Until it is the chosen scope the rows are withheld,
817
+ the list is `aria-busy` and says `pendingMessage` ("Searching…"), so a
818
+ search still answering the scope the reader left never offers its rows, or
819
+ lets Enter open one, under the new one. Focus never leaves the field: Tab and
820
+ Shift+Tab step the scopes, and so do ← and → while the field is empty; a
821
+ press on a tab picks it without taking focus; the results are the chosen
822
+ tab's `tabpanel`, the field is described by that tab, and a change is said in
823
+ a hidden status line. `lead` is what the surface says before its rows, an
824
+ answer drawn on the raised rung: a polite live region OUTSIDE the listbox,
825
+ mounted empty so its first words are read, never an option (Enter still
826
+ takes the highlighted row), and scrolled with the rows in the one scroller.
827
+ Anything a reader should act on belongs in a row. A row's `hint` now runs on
828
+ its label's line in the tertiary ink rather than at the row's end, which is
829
+ kept for the new `meta`: a short fact or a `Kbd` naming an accelerator the
830
+ application binds on the palette's `onKeyDown`. `footer` is the key legend,
831
+ at the `xs` step on the sheet rung. The surface is the design's 720px wide
832
+ and at most `min(74dvh, 680px)` tall, the artboard's 664 of 900 with a lead
833
+ over five rows; it was 620 by `min(60dvh, 520px)`, sized for a list alone.
834
+ The field row is the design's too: a search glyph always leads the query, in
835
+ the field's own leading slot, and `escapeHint` draws an `Esc` keycap at its
836
+ end (off by default, since it names a key and only the application knows
837
+ whether every reader of its palette has one). Both take no pointer, so a press
838
+ anywhere on the row lands in the text, and both are hidden from assistive
839
+ technology: the glyph restates the field's name and the keycap what a
840
+ dialog's Escape does. Nothing here is renamed: a palette without scopes
841
+ behaves as before, Tab included.
842
+
843
+ | Was | Is |
844
+ |---|---|
845
+ | a stylesheet targeting a palette row's hint as `.crewlet-listbox__hint` | `.crewlet-palette__hint`, on the label's line; the row's end is `.crewlet-palette__meta` |
846
+ | a layout or a test that counted on the palette being 620px wide and at most `min(60dvh, 520px)` tall | 720px and `min(74dvh, 680px)`; `--crewlet-modal-width` on the palette still sets the width |
847
+
848
+ **The row under the arrows is raised, not violet.** Every listbox row (a
849
+ `Select`, a `Combobox`, a `TagsInput` and a `CommandPalette` alike) drew its
850
+ highlighted and hovered row in the accent's soft tint with the accent ink.
851
+ The accent is the primary action's fill and the focus ring now, so that row
852
+ read as a second primary on every surface that offers a list. It now stands
853
+ on `--color-surface-elevated`, the raised rung @crewlethq/tokens holds dE 3 off
854
+ the card every list opens on, and its label lifts from the register's
855
+ secondary step to `--color-text-primary`, as the approved palette draws it.
856
+ A palette row's `hint` stays in the tertiary ink on the highlighted row
857
+ rather than taking the row's colour. What the accent still marks in a list is
858
+ a choice made, a multi-select's tick. Nothing is renamed.
859
+
860
+ | Was | Is |
861
+ |---|---|
862
+ | a test or a stylesheet reading `--color-brand-accent-soft` / `-ink` off a highlighted `.crewlet-listbox__option` | `--color-surface-elevated` and `--color-text-primary` |
863
+
864
+ **A highlighted listbox row is outlined in forced-colors mode, and a chord is
865
+ not Enter.** The mode repaints backgrounds to `Canvas`, which took the
866
+ highlight of every `Select`, `Combobox` and `CommandPalette` with it; the
867
+ highlighted row now carries a 2px `Highlight` outline inside its box. Enter
868
+ held with Command, Control or Option no longer takes the highlighted option:
869
+ it is left to whoever owns the chord, so a palette's ⌘Enter does not also
870
+ open the row. And a modal's focus trap no longer moves a Tab a control has
871
+ already taken (`preventDefault`): a `Combobox` with `tabCommits` that was the
872
+ last stop in a dialog took the completion and then lost focus to the first
873
+ stop. `useListbox` brings the HIGHLIGHTED row into view by its id rather than
874
+ the first `[aria-selected="true"]` in its scroller, which in a multi-select is
875
+ the first chosen row and in a palette could be a tab in the lead; `TagsInput`
876
+ drops the second reveal it kept to undo that.
877
+
878
+ | Was | Is |
879
+ |---|---|
880
+ | ⌘Enter, Ctrl+Enter or ⌥Enter relied on to take the highlighted option of a `Select`, a `Combobox`, a `CommandPalette` or a `useListbox` of your own | bind the chord yourself, on the surface's `onKeyDown`; plain Enter takes the option as before |
881
+ | a forced-colors stylesheet of your own outlining a highlighted listbox row | drop it: the row draws a 2px `Highlight` outline itself |
882
+
883
+ **`ThemeToggle` is new, and the preference controls can be controlled.** The
884
+ design's rail foot carries one button beside the reader's identity row rather
885
+ than the three-way row. `ThemeToggle` resolves the preference against the
886
+ platform, through the same `(prefers-color-scheme: light)` query the theme
887
+ layer answers (so a browser reporting no preference is dark, as it is
888
+ painted), and a press writes the EXPLICIT opposite: on a dark system a reader
889
+ following it gets `light`, and a second press `dark`, never `system`, which
890
+ repaints nothing. It is named for what a press does ("Switch to the light
891
+ theme", `toLightLabel` / `toDarkLabel`) and draws the palette it goes to, a
892
+ sun on a dark page and a moon on a light one; it carries no `aria-pressed`,
893
+ because neither palette is the other's "on". `ThemeSwitcher`,
894
+ `DensitySwitcher` and `ThemeToggle` take `value` / `onChange`: given a
895
+ `value`, a control draws it, reports a pick and writes NOTHING, neither the
896
+ root attribute nor storage, and the types refuse a `storageKey` beside it.
897
+ Without one a control is stored as before, and `onChange` is told after the
898
+ pick is applied and kept. Stored controls on one key now read ONE value: each
899
+ held its own copy, so a press on a toggle left a settings row showing the
900
+ choice it had replaced, and a choice another tab writes now reaches every
901
+ control and the root. A choice storage refuses (a private window, blocked site
902
+ data) is held for the visit, so the control shows the pick it just made rather
903
+ than re-reading an empty store. `useSystemTheme` and `resolveTheme` are
904
+ exported for an application that draws its own. Nothing is renamed.
905
+
906
+ **`TreeCanvas` boxes a unit round its seats, and its connectors are elbows.**
907
+ The design's org chart. `groups: { id, label, lead?, memberIds }[]` draws a
908
+ DASHED `--color-border-strong` box on `--color-surface-subtle` round a run of
909
+ sibling cards (the seats that report to one lead), with `label` along its top
910
+ in the tertiary ink at 12px and `lead` (the project key chip) AHEAD of it, as
911
+ the design reads a unit: "ENG Engineering · Core", which is also the
912
+ description each member card is given. The edge is dashed because a box round
913
+ cards drawn with a solid edge read as one more, larger card, and it takes the
914
+ strong step because a dash spends half its length on gaps. The box is room the LAYOUT keeps rather than a frame over it:
915
+ `layoutForest` takes `groups` (`{ id, members, inset }`, each member's outline
916
+ widened by the box's padding and header), so a card outside a unit is held a
917
+ gap clear of the unit's box, two boxes never overlap, a short member's
918
+ children start below the box, and `ForestLayout.groups` is each box's
919
+ rectangle, exactly its members' extent plus the padding (`--spacing-3`, the
920
+ label's own inset) and the header's measured height. Members must be
921
+ consecutive children of one card (or consecutive roots), since a rectangle
922
+ round anything else encloses what lies between; anything else is refused by
923
+ name. A member not drawn right now is left out and a group with none drawn is
924
+ not drawn. The box and its label are hidden drawing, like the connectors; each
925
+ member card's node is described by the label (`aria-describedby`), and the
926
+ label rides over the branches on the box's own fill. `connector` gains
927
+ `elbow`, now the default, and `straight`: an elbow is orthogonal, down, a 6px
928
+ corner (`--radius-sm`) drawn as a true arc, across, another corner and down,
929
+ and the run across a parent's children is drawn halfway through the clear
930
+ space between the parent (or its box) and the box its children are in, so it
931
+ never passes through a label. On shared ranks that run is between the RANKS:
932
+ it used to be halfway below a short parent centred in a tall band, inside its
933
+ rank, and a children's row reaching under a taller neighbour ran through that
934
+ neighbour's card. `step` and `curve` are unchanged; a chart that relied on the
935
+ old default asks for `connector="step"`. Connectors are 1.5px, the design's
936
+ weight (the `node` appearance keeps its 3). The design draws them in
937
+ `--color-border-strong`, which measures 1.45:1 on the canvas in dark and 1.43:1
938
+ in light against the 3:1 a connector is held to, so they stay on
939
+ `--color-border-control` (4.40:1 and 4.04:1). A child a fraction of a pixel off
940
+ its parent's centre now gets its two corners rather than a vertical that ended
941
+ beside the child. Nothing is renamed.
942
+
943
+ | Was | Is |
944
+ |---|---|
945
+ | a `TreeCanvas` with no `connector`, relied on for the stepped connector | `connector="step"`; the default is `elbow` |
946
+ | a stylesheet or a test reading a connector's 1px width | 1.5px, still `--color-border-control`; the `node` appearance keeps its 3px |
947
+
948
+ **A `Meter` takes its thresholds, or its verdict.** The `spent` ramp turned at
949
+ three quarters of the maximum for every caller, so an application whose own
950
+ rule warns elsewhere had to compute the tone itself and pass `tone`, which
951
+ names a paint rather than a state. Two props replace that. `thresholds={{ near:
952
+ 0.9 }}` moves the middle step to the given FRACTION of the limit and keeps the
953
+ rest of the ramp: at or past the maximum the bar is `danger`, at or past `near`
954
+ it is `warning`, below that it is the ordinary `quantity`. The fraction is
955
+ compared with `value / max` as given, so `near: 0.9` flips at exactly 90 of
956
+ 100, and one outside (0, 1] is refused with a `RangeError` that names it (at 1
957
+ there is no middle step). `state` is the VERDICT of a consumer that owns the
958
+ rule, such as the engine that enforces a budget: `'ok'`, `'near'` or
959
+ `'refusing'`, painted `quantity`, `warning` and `danger`. Given a state the
960
+ meter derives nothing, whatever the fill; the bar is still drawn at the value
961
+ against the maximum, and an unknown state is refused rather than drawn as the
962
+ ordinary reading. The types refuse a second opinion beside either one: a
963
+ `state` with a `tone`, a `polarity` or `thresholds`, and `thresholds` with a
964
+ `tone` or with `polarity="progress"`, whose ramp has no middle step. The tone is
965
+ drawn and never spoken, so a refusing budget says so in its `valueText`.
966
+ `meterState(fraction, thresholds?)` and `meterStateTone(state)` are the same
967
+ rule for an application drawing its own figure; `meterTone(percent)` is
968
+ `meterState` at `DEFAULT_METER_THRESHOLDS` (`{ near: 0.75 }`), unchanged.
969
+ `MeterProps` is now a union of `MeterVerdictProps`, `MeterRampProps` and
970
+ `MeterThresholdProps` over the shared props, and an interface cannot extend a
971
+ union, so a props type of your own built on it is an intersection. A meter
972
+ given neither prop is drawn exactly as before.
973
+
974
+ | Was | Is |
975
+ |---|---|
976
+ | `interface BudgetMeterProps extends MeterProps { … }` | `type BudgetMeterProps = MeterProps & { … }` |
977
+ | a `tone` computed from your own rule and passed to `Meter` | `state` with your verdict (`'ok'`, `'near'`, `'refusing'`), or `thresholds={{ near }}` for the fraction; `tone` stays for a paint that is not a state |
978
+
115
979
  ## Breaking changes in 0.3.0
116
980
 
117
981
  Every one of these is a change a consumer can see. None of them needs an edit