@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
@@ -1,6 +1,6 @@
1
1
  export { cx } from './cx.js';
2
2
  export { useClipboard, writeClipboard } from './useClipboard.js';
3
3
  export type { Clipboard, ClipboardOptions, ClipboardState } from './useClipboard.js';
4
- export type { PhaseTone, Tone } from './tone.js';
4
+ export type { Tone } from './tone.js';
5
5
  export { HeadingLevelProvider, headingTag, nextHeadingLevel, useHeadingLevel } from './headingLevel.js';
6
6
  export type { HeadingLevel, HeadingLevelProviderProps } from './headingLevel.js';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The two colour vocabularies a component is allowed to take as a prop.
2
+ * The one colour vocabulary a component is allowed to take as a prop.
3
3
  *
4
4
  * ONE SPELLING. The package shipped `warn` in Tag and Toaster and `warning` in
5
5
  * StatCard and Callout, and `danger` beside `error`, so the same state had two
@@ -10,16 +10,11 @@
10
10
  * info are states, `brand` is where the reader is, and everything else is
11
11
  * neutral. Identity (a seat, a node, a vendor, a tool) takes neutral colour and
12
12
  * is carried by its name, its glyph and its position.
13
- */
14
- export type Tone = 'neutral' | 'info' | 'success' | 'warning' | 'danger' | 'brand';
15
- /**
16
- * The phase vocabulary, and nothing else.
17
13
  *
18
- * Three phases, because that is the whole of the engine's phase model. A
19
- * per-model or per-worker breakdown is a CHART SERIES SET and takes the data
20
- * ramp (`--color-data-1` and the rest) inside a legend, not this family: a
21
- * reader compares phase marks against each other, so the three are separated
22
- * from one another and from every status hue, which a growing set of series
23
- * could not be.
14
+ * So does a CATEGORY. A phase, a unit or a model is neutral with its word, and
15
+ * inside a figure it is a series its legend names, from the data ramp the
16
+ * application maps it onto. There used to be a second vocabulary here, three
17
+ * phase hues, and it spent three of the hues the states and the series are held
18
+ * apart in on a word the label already said.
24
19
  */
25
- export type PhaseTone = 'phase-onboarding' | 'phase-execute' | 'phase-review';
20
+ export type Tone = 'neutral' | 'info' | 'success' | 'warning' | 'danger' | 'brand';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewlethq/ui",
3
- "version": "0.4.4",
3
+ "version": "0.5.1",
4
4
  "description": "Crewlet shared React components built on @crewlethq/tokens and @crewlethq/icons.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -37,13 +37,13 @@
37
37
  "scripts": {
38
38
  "build": "node scripts/write-barrel.mjs && tsup && tsc -p tsconfig.build.json && node scripts/bundle-css.mjs",
39
39
  "dev": "node scripts/write-barrel.mjs && tsup --watch --onSuccess \"node scripts/bundle-css.mjs\"",
40
- "lint": "node scripts/write-barrel.mjs && eslint src scripts && node scripts/check-css-variables.mjs && node scripts/check-css-literals.mjs src/Avatar src/Layer src/Listbox src/Announcer src/Modal src/Tooltip src/CommandPalette src/VisuallyHidden src/utils src/Count src/StatusDot src/EmptyValue src/Button src/IconButton src/Popover src/Menu src/DataView src/DataTable src/Input src/Tag src/TagGroup src/FilterChip src/EntityChip src/Callout src/Toaster src/Skeleton src/EmptyState src/Meter src/StatCard src/StatGroup src/NewItemsNotice src/PricingCard src/ImageUpload src/Tabs src/SegmentedControl src/PageHeader src/Card src/Section src/Charts src/Layout src/Canvas src/Tree src/TreeCanvas src/TreeGrid src/OrgNode src/OrgLabel src/AddPill src/OrgTable",
40
+ "lint": "node scripts/write-barrel.mjs && eslint src scripts && node scripts/check-css-variables.mjs && node scripts/check-css-literals.mjs src",
41
41
  "typecheck": "node scripts/write-barrel.mjs && tsc -p tsconfig.json --noEmit",
42
42
  "clean": "rm -rf dist src/generated"
43
43
  },
44
44
  "dependencies": {
45
- "@crewlethq/icons": "0.4.4",
46
- "@crewlethq/tokens": "0.4.4"
45
+ "@crewlethq/icons": "0.5.1",
46
+ "@crewlethq/tokens": "0.5.1"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "react": ">=18",
@@ -1,17 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  // Refuses a value a component should have taken from a token, over the folders
3
- // it is given.
3
+ // it is given. The package's own lint runs it over all of src/, so every
4
+ // component keeps these rules at every commit.
4
5
  //
5
- // WHY IT TAKES FOLDERS. Its sibling, check-css-variables.mjs, runs over the
6
- // whole package and holds rules no component may break at any point. This one
7
- // holds the rules a folder OPTS INTO as it is rewritten onto the token layer,
8
- // so a component that has not been rewritten yet does not have to be
9
- // allow-listed to keep the build green. The list of folders in the package's
10
- // lint script is the record of which are done; when the last one lands, the
11
- // list becomes `src` and this paragraph goes with it.
12
- //
13
- // It is also the published bin, `crewlet-css-check`, so a consumer can hold
14
- // its own stylesheets to the same rules.
6
+ // WHY IT TAKES FOLDERS. It is also the published bin, `crewlet-css-check`, so
7
+ // a consumer can hold its own stylesheets to the same rules, and a consumer's
8
+ // stylesheets live wherever its build puts them.
15
9
  //
16
10
  // THE RULES.
17
11
  //
@@ -25,9 +19,27 @@
25
19
  // off the scale is a component that drifts from the one beside it.
26
20
  // 3. No color-mix(). Mixing two tokens produces a third colour nobody
27
21
  // measured, which is how a "warning text" step ended up at 4.41:1.
28
- // 4. An animation or a transition is paired with a reduced-motion rule IN THE
29
- // SAME FILE. The document baseline collapses motion too, but a component
30
- // is also used in an application that does not import the baseline.
22
+ // 4. EVERY MOTION IS PAIRED WITH ITS STOP. A rule that starts an animation or
23
+ // a transition is named again, selector for selector, by a later rule in
24
+ // the same file inside `@media (prefers-reduced-motion: reduce)` that sets
25
+ // it to `none` (or takes the element out with `display: none`), unless it
26
+ // only starts inside `@media (prefers-reduced-motion: no-preference)`.
27
+ // The document baseline collapses motion too, but a component is also
28
+ // used in an application that does not import the baseline.
29
+ // THE SAME SELECTOR, LATER, because that is the one arrangement the
30
+ // cascade guarantees: the two tie on specificity and the stop wins on
31
+ // source order. The rule was "the file mentions prefers-reduced-motion",
32
+ // and three components passed it while still moving for a reader who had
33
+ // asked them not to. DataTable stopped everything under a catch-all,
34
+ // `.crewlet-data-table.crewlet-data-table *`, which is two classes, and
35
+ // its row's transition was two classes and two elements. TreeCanvas
36
+ // stopped `.crewlet-tree-canvas__card--composing`, one class, where the
37
+ // ghost's arrival started on two. And TreeCanvas stopped its node
38
+ // controls ABOVE the rules that started them, so source order handed
39
+ // every tie back to the motion. A stop that looks broader is not one the
40
+ // check can verify, and all three of those looked right. LATER is where
41
+ // a browser places it: declarations written after a block nested in their
42
+ // rule come after that block, so a motion below a nested stop beats it.
31
43
  // 5. A media query's length is one of the breakpoint tokens. A media query
32
44
  // cannot read a custom property, so the number has to be written out; this
33
45
  // is what says it is still the same number.
@@ -53,6 +65,17 @@ const LENGTH_IN_QUERY = /(\d*\.?\d+)px/g;
53
65
  const COLOUR_PROPERTY =
54
66
  /^(color|background|background-color|border(-(top|right|bottom|left))?-color|border|border-(top|right|bottom|left)|outline|outline-color|fill|stroke|box-shadow|text-shadow|caret-color|column-rule-color|text-decoration-color|accent-color|scrollbar-color|--crewlet-[\w-]*)$/;
55
67
 
68
+ /** The declarations that start a motion, and the one each belongs to. */
69
+ const MOTION = new Map([
70
+ ['animation', 'animation'],
71
+ ['animation-name', 'animation'],
72
+ ['transition', 'transition'],
73
+ ['transition-property', 'transition'],
74
+ ]);
75
+
76
+ /** The one query a stop is read inside: exactly this, with nothing else around it. */
77
+ const REDUCE = /^@media\s*\(\s*prefers-reduced-motion\s*:\s*reduce\s*\)$/;
78
+
56
79
  function declarations(body) {
57
80
  const found = [];
58
81
  for (const part of body.split(';')) {
@@ -75,6 +98,123 @@ function withoutTokens(value) {
75
98
  return text;
76
99
  }
77
100
 
101
+ /**
102
+ * One selector, spelled one way: whitespace collapsed, none round a combinator,
103
+ * and one kind of quote. Two spellings of one selector are one selector here,
104
+ * so a stop is never refused for its layout.
105
+ */
106
+ function normalSelector(selector) {
107
+ return selector
108
+ .replace(/\s+/g, ' ')
109
+ .replace(/\s*([>+~])\s*/g, '$1')
110
+ .replace(/"/g, "'")
111
+ .trim();
112
+ }
113
+
114
+ /** A selector list split at its own commas, not the ones inside `:not(…)` or `[…]`. */
115
+ function selectorList(list) {
116
+ const parts = [];
117
+ let depth = 0;
118
+ let from = 0;
119
+ for (let at = 0; at < list.length; at += 1) {
120
+ const char = list[at];
121
+ if (char === '(' || char === '[') depth += 1;
122
+ else if (char === ')' || char === ']') depth -= 1;
123
+ else if (char === ',' && depth === 0) {
124
+ parts.push(list.slice(from, at));
125
+ from = at + 1;
126
+ }
127
+ }
128
+ parts.push(list.slice(from));
129
+ return parts;
130
+ }
131
+
132
+ /**
133
+ * The selectors a block's own declarations apply to, with any nesting
134
+ * resolved, or null for a block that declares for no element: a top-level
135
+ * at-rule, or a keyframe.
136
+ */
137
+ function appliesTo(block) {
138
+ let list = null;
139
+ for (const head of [...block.context, block.selector]) {
140
+ if (/^@keyframes\b/.test(head)) return null;
141
+ if (head.startsWith('@')) continue;
142
+ const parts = selectorList(head);
143
+ list =
144
+ list === null
145
+ ? parts
146
+ : list.flatMap((parent) => parts.map((part) => (part.includes('&') ? part.replaceAll('&', parent) : `${parent} ${part}`)));
147
+ }
148
+ return list === null ? null : list.map(normalSelector);
149
+ }
150
+
151
+ /** The conditional heads a block sits under, its own included when it is one. */
152
+ function conditions(block) {
153
+ return [...block.context, block.selector].filter((head) => head.startsWith('@')).map((head) => head.replace(/\s+/g, ' '));
154
+ }
155
+
156
+ /**
157
+ * Whether a block's declarations reach a browser only when the reader has NOT
158
+ * asked for less motion. Only a plain conjunction counts: under `not`, `or` or
159
+ * a comma list the same block also applies to a reader who has.
160
+ */
161
+ function onlyWithMotion(heads) {
162
+ return heads.some(
163
+ (head) =>
164
+ /^@media\b/.test(head) && /\(\s*prefers-reduced-motion\s*:\s*no-preference\s*\)/.test(head) && !/,|\bor\b|\bnot\b/.test(head),
165
+ );
166
+ }
167
+
168
+ /**
169
+ * Rule 4: every rule that starts a motion, answered selector for selector by a
170
+ * later stop in the same file. Returns the findings.
171
+ */
172
+ function unpairedMotion(file) {
173
+ const starts = [];
174
+ const stops = [];
175
+ for (const block of rules(file.text)) {
176
+ const selectors = appliesTo(block);
177
+ if (selectors === null) continue;
178
+ const heads = conditions(block);
179
+ const reduced = heads.some((head) => /prefers-reduced-motion\s*:\s*reduce/.test(head));
180
+ const isStop = heads.length === 1 && REDUCE.test(heads[0] ?? '');
181
+ for (const { property, value } of declarations(block.body)) {
182
+ const important = /!\s*important\s*$/i.test(value);
183
+ const bare = value.replace(/!\s*important\s*$/i, '').trim().toLowerCase();
184
+ if (isStop && property === 'display' && bare === 'none') {
185
+ for (const family of ['animation', 'transition']) stops.push({ family, selectors, index: block.index, important });
186
+ continue;
187
+ }
188
+ const family = MOTION.get(property);
189
+ if (family === undefined) continue;
190
+ if (isStop && bare === 'none') stops.push({ family, selectors, index: block.index, important });
191
+ // Motion declared for a reader who asked for less is a deliberate
192
+ // alternative, such as a fade in place of a slide, and motion declared
193
+ // only for a reader who did not ask needs no stop.
194
+ if (reduced || onlyWithMotion(heads) || bare === 'none') continue;
195
+ starts.push({ family, property, selectors, index: block.index, important });
196
+ }
197
+ }
198
+
199
+ const problems = [];
200
+ for (const start of starts) {
201
+ for (const selector of start.selectors) {
202
+ const stopped = stops.some(
203
+ (stop) =>
204
+ stop.family === start.family &&
205
+ stop.index > start.index &&
206
+ (stop.important || !start.important) &&
207
+ stop.selectors.includes(selector),
208
+ );
209
+ if (stopped) continue;
210
+ problems.push(
211
+ `${file.where}:${lineOf(file.text, start.index)}: ${start.property} on ${selector} has no reduced-motion stop. Name the same selector in a later rule of this file inside @media (prefers-reduced-motion: reduce) and set ${start.family}: none, or start it inside @media (prefers-reduced-motion: no-preference): a stop naming another selector can lose on specificity, and one above the rule it stops loses on source order`,
212
+ );
213
+ }
214
+ }
215
+ return problems;
216
+ }
217
+
78
218
  /**
79
219
  * The breakpoint steps, from the tokens package's own typed export.
80
220
  *
@@ -94,13 +234,13 @@ export async function findings(roots, base = packageRoot) {
94
234
 
95
235
  for (const file of files) {
96
236
  const at = (index) => `${file.where}:${lineOf(file.text, index)}`;
97
- const hasReducedMotion = file.text.includes('prefers-reduced-motion');
98
237
 
99
- for (const { selector, body, index } of rules(file.text)) {
238
+ for (const { selector, body, index, nestedDeclarations } of rules(file.text)) {
100
239
  const where = at(index);
101
- const inQuery = /@media/.test(selector);
102
240
 
103
- if (inQuery) {
241
+ // Declarations resumed after a nested block carry their block's head
242
+ // again, and the head was read with the block.
243
+ if (/^@media\b/.test(selector) && !nestedDeclarations) {
104
244
  for (const match of selector.matchAll(LENGTH_IN_QUERY)) {
105
245
  if (!allowed.has(Number(match[1]))) {
106
246
  problems.push(
@@ -142,11 +282,7 @@ export async function findings(roots, base = packageRoot) {
142
282
  }
143
283
  }
144
284
 
145
- if (!hasReducedMotion && /(^|[;{\s])(animation|transition)\s*:/.test(file.text)) {
146
- problems.push(
147
- `${file.where}: animates or transitions with no prefers-reduced-motion rule in the file. The document baseline is opt-in, so a component states its own`,
148
- );
149
- }
285
+ problems.push(...unpairedMotion(file));
150
286
  }
151
287
 
152
288
  return { problems, checked: `${files.length} stylesheets in ${roots.length} folder(s)` };
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  // The rules EVERY component in the package keeps, checked over all of src/.
3
3
  //
4
- // Its sibling, check-css-literals.mjs, holds the rules a folder opts into as
5
- // it is rewritten onto the token layer. These are the ones nothing may break
6
- // at any point, because each of them fails silently in production:
4
+ // Its sibling, check-css-literals.mjs, holds the rules about VALUES: which
5
+ // colour, size, radius, duration and depth a component may spell out, and how
6
+ // every motion is stopped. These are the rules about which names a stylesheet
7
+ // reads, what it loads and how it draws text and focus, and each of them fails
8
+ // silently in production:
7
9
  //
8
10
  // 1. A name in a token namespace (--color-*, --font-*, --spacing-* and the
9
11
  // rest of TOKEN_PREFIXES) must be one the @crewlethq/tokens build emits,
@@ -46,14 +46,106 @@ export function readSources(roots, base, extensions) {
46
46
  return files;
47
47
  }
48
48
 
49
- /** Every rule body in a stylesheet, with its selector and its start offset. */
49
+ /**
50
+ * Every block in a stylesheet, in source order, whatever it is nested in.
51
+ *
52
+ * Each is `{ selector, body, index, context, nestedDeclarations }`: the text
53
+ * before its brace (a selector list, or an at-rule's prelude such as
54
+ * `@media (width < 1024px)`), the declarations it makes ITSELF before the
55
+ * first block nested in it, where that text starts, and the heads of the
56
+ * blocks it sits inside, outermost first. A keyframe's `from` or `50%` is a
57
+ * block too, with its `@keyframes` in its context.
58
+ *
59
+ * NESTING-AWARE, because the regex this replaced, `([^{}]+)\{([^{}]*)\}`, saw
60
+ * only the innermost blocks. It never saw an at-rule's prelude, so the literal
61
+ * check's breakpoint rule read not one top-level `@media` query in the package;
62
+ * it could not say which query a rule sat inside, which is the whole of
63
+ * pairing a motion with the reduced-motion rule that stops it; and it never
64
+ * saw the declarations of a rule with a rule nested in it. Its offsets also
65
+ * started after the previous block's brace, so a finding named the line that
66
+ * block closed on rather than the line of the selector.
67
+ *
68
+ * DECLARATIONS AFTER A NESTED BLOCK ARE AN ENTRY OF THEIR OWN, at their own
69
+ * offset, with the block's selector and context and `nestedDeclarations: true`,
70
+ * because that is where a browser puts them: in a nested-declarations rule
71
+ * that matches what its parent matches, at the parent's specificity, AFTER the
72
+ * nested block in source order. Folded into the parent at the parent's offset,
73
+ * an `animation` written below a nested reduced-motion stop read as written
74
+ * above it, and the pairing check passed a motion that beats its own stop. The
75
+ * flag lets a check that reads the HEAD rather than the declarations, such as
76
+ * the breakpoint rule, read each head once.
77
+ */
50
78
  export function rules(text) {
51
79
  const found = [];
52
- const pattern = /([^{}]+)\{([^{}]*)\}/g;
53
- for (const match of text.matchAll(pattern)) {
54
- found.push({ selector: match[1].trim(), body: match[2], index: match.index });
80
+ const open = [];
81
+ // Where the statement being read began: a declaration, or the head of the
82
+ // next block, which is everything since the last `;`, `{` or `}`.
83
+ let statement = 0;
84
+ let quote = null;
85
+ /**
86
+ * A block's own declarations from where they resume to `end`: its body while
87
+ * nothing is nested in it yet, and an entry of their own once something is,
88
+ * since a browser places them after that nested block.
89
+ */
90
+ const declarationsUntil = (frame, end) => {
91
+ const declared = text.slice(frame.from, end);
92
+ if (!frame.nested) {
93
+ frame.body += declared;
94
+ } else if (declared.trim() !== '') {
95
+ found.push({
96
+ selector: frame.selector,
97
+ body: declared,
98
+ index: frame.from + declared.length - declared.trimStart().length,
99
+ context: frame.context,
100
+ nestedDeclarations: true,
101
+ });
102
+ }
103
+ };
104
+ for (let at = 0; at < text.length; at += 1) {
105
+ const char = text[at];
106
+ if (quote !== null) {
107
+ if (char === '\\') at += 1;
108
+ else if (char === quote) quote = null;
109
+ continue;
110
+ }
111
+ if (char === '"' || char === "'") {
112
+ quote = char;
113
+ } else if (char === ';') {
114
+ statement = at + 1;
115
+ } else if (char === '{') {
116
+ const parent = open.at(-1);
117
+ if (parent) {
118
+ declarationsUntil(parent, statement);
119
+ parent.nested = true;
120
+ }
121
+ const head = text.slice(statement, at);
122
+ open.push({
123
+ selector: head.trim(),
124
+ index: statement + head.length - head.trimStart().length,
125
+ context: open.map((each) => each.selector),
126
+ from: at + 1,
127
+ body: '',
128
+ nested: false,
129
+ });
130
+ statement = at + 1;
131
+ } else if (char === '}') {
132
+ const frame = open.pop();
133
+ if (frame) {
134
+ declarationsUntil(frame, at);
135
+ found.push({
136
+ selector: frame.selector,
137
+ body: frame.body,
138
+ index: frame.index,
139
+ context: frame.context,
140
+ nestedDeclarations: false,
141
+ });
142
+ const parent = open.at(-1);
143
+ if (parent) parent.from = at + 1;
144
+ }
145
+ statement = at + 1;
146
+ }
55
147
  }
56
- return found;
148
+ return found.sort((a, b) => a.index - b.index);
57
149
  }
58
150
 
59
151
  export function report(name, problems, checked) {
@@ -1,156 +0,0 @@
1
- /*
2
- * The identity badge.
3
- *
4
- * IT IS NEUTRAL, and that is the whole rule. The system this replaces coloured
5
- * a seat by `PALETTE[hash(name) % 8]`, over the same eight families it also
6
- * spent on phases and event categories, so one amber meant "the execute
7
- * phase", "the decision category" and "the Analyst" at once. A hash of a name
8
- * carries no information: rename the seat and its colour changes, which is the
9
- * proof it never meant anything. Identity is the monogram and the name.
10
- *
11
- * The seeded tints stay for the one case that earns them, a roster somebody
12
- * scans, and their grounds are TOKENS: measured under white initials and held
13
- * at 4.5:1 by the palette suite. Spelled as literals, five of the ten sat
14
- * under that floor while the suite measured ten values nothing painted with.
15
- */
16
-
17
- .crewlet-avatar {
18
- --crewlet-avatar-size: 32px;
19
- display: inline-flex;
20
- align-items: center;
21
- justify-content: center;
22
- flex: none;
23
- width: var(--crewlet-avatar-size);
24
- height: var(--crewlet-avatar-size);
25
- overflow: hidden;
26
- object-fit: cover;
27
- font-family: var(--font-family-sans);
28
- font-size: var(--font-size-xs);
29
- font-weight: var(--font-weight-semibold);
30
- letter-spacing: var(--font-letter-spacing-wide);
31
- /* The document's own leading rather than a tight step of the badge's own.
32
- The box is a fixed square that centres whatever line it holds, so the ink
33
- lands in the same place either way; what the tight step bought was one
34
- more number in a component that already sets four. */
35
- line-height: var(--font-line-height-normal);
36
- text-transform: uppercase;
37
- user-select: none;
38
- box-sizing: border-box;
39
- }
40
-
41
- /*
42
- * The four steps. Each pairs its box with a type step, so two initials sit
43
- * inside the circle at every one of them; a numeric size computes its own,
44
- * because nothing in the scale can answer for an arbitrary box.
45
- */
46
- .crewlet-avatar--xs {
47
- --crewlet-avatar-size: 20px;
48
- font-size: var(--font-size-2xs);
49
- }
50
-
51
- .crewlet-avatar--sm {
52
- --crewlet-avatar-size: 26px;
53
- font-size: var(--font-size-2xs);
54
- }
55
-
56
- .crewlet-avatar--md {
57
- --crewlet-avatar-size: 32px;
58
- font-size: var(--font-size-xs);
59
- }
60
-
61
- .crewlet-avatar--lg {
62
- --crewlet-avatar-size: 40px;
63
- font-size: var(--font-size-sm);
64
- }
65
-
66
- /*
67
- * Shapes. THE SQUARE IS THE DEFAULT, which is the change: the engine's
68
- * identity mark is a rounded square and it is what a reader sees in every seat
69
- * chip, every list row and every header on that product. A circle stays for a
70
- * PERSON, where the round badge is the platform convention a member list
71
- * already speaks.
72
- *
73
- * The square's radius moves with the box rather than sitting at one step. The
74
- * engine draws 4px at 20, 8px at 26 and 12px at 40, which is a roughly
75
- * constant proportion; one radius across the four steps reads as a pill at the
76
- * smallest and as a plain box at the largest. A NUMERIC SIZE OBEYS THE SAME
77
- * LADDER, from `avatarSquareCorner` in the component, which picks the corner
78
- * of the step nearest the box it was given: taking one fixed step there is how
79
- * `size={80}` ended up drawn squarer than `size="lg"` at half its width.
80
- */
81
- .crewlet-avatar--circle {
82
- border-radius: var(--radius-circle);
83
- }
84
-
85
- .crewlet-avatar--square {
86
- border-radius: var(--radius-md);
87
- }
88
-
89
- .crewlet-avatar--square.crewlet-avatar--xs {
90
- border-radius: var(--radius-xs);
91
- }
92
-
93
- .crewlet-avatar--square.crewlet-avatar--lg {
94
- border-radius: var(--radius-lg);
95
- }
96
-
97
- .crewlet-avatar--image {
98
- display: block;
99
- object-fit: cover;
100
- }
101
-
102
- /* ── tones ─────────────────────────────────────────────────────────────── */
103
-
104
- .crewlet-avatar--fallback {
105
- border: 1px solid transparent;
106
- }
107
-
108
- .crewlet-avatar--neutral {
109
- background: var(--color-surface-elevated);
110
- border-color: var(--color-border-default);
111
- color: var(--color-text-secondary);
112
- }
113
-
114
- /*
115
- * The label on the accent fill is the on-accent step, not the on-brand one.
116
- * The themes repaint the brand fill monochrome and leave the accent where it
117
- * is, so on-brand is black in the dark palette and black on the accent
118
- * measures 4.35:1. On-accent is white in every palette and measures 4.83:1.
119
- */
120
- .crewlet-avatar--brand {
121
- background: var(--color-brand-accent);
122
- color: var(--color-text-on-accent);
123
- }
124
-
125
- .crewlet-avatar--seeded {
126
- color: var(--color-text-on-accent);
127
- }
128
-
129
- .crewlet-avatar--tint-0 { background: var(--color-avatar-tint-0); }
130
- .crewlet-avatar--tint-1 { background: var(--color-avatar-tint-1); }
131
- .crewlet-avatar--tint-2 { background: var(--color-avatar-tint-2); }
132
- .crewlet-avatar--tint-3 { background: var(--color-avatar-tint-3); }
133
- .crewlet-avatar--tint-4 { background: var(--color-avatar-tint-4); }
134
- .crewlet-avatar--tint-5 { background: var(--color-avatar-tint-5); }
135
- .crewlet-avatar--tint-6 { background: var(--color-avatar-tint-6); }
136
- .crewlet-avatar--tint-7 { background: var(--color-avatar-tint-7); }
137
- .crewlet-avatar--tint-8 { background: var(--color-avatar-tint-8); }
138
- .crewlet-avatar--tint-9 { background: var(--color-avatar-tint-9); }
139
-
140
- /*
141
- * A HUMAN SEAT IS DRAWN, NOT TINTED. The engine does not run it, which is a
142
- * structural fact rather than a status, so it takes the edge and no fill at
143
- * all. Last, so it wins over whichever tone it is paired with.
144
- *
145
- * The whole border rather than its style alone: the badge is the same person
146
- * whether or not they have uploaded a photo, and an image draws no border of
147
- * its own for a style to change.
148
- */
149
- .crewlet-avatar--dashed {
150
- background: transparent;
151
- /* The same hairline the solid neutral badge draws, so the human seat differs
152
- by its LINE STYLE and its missing fill rather than by a second grey a
153
- reader would have to hold two of in mind. */
154
- border: 1px dashed var(--color-border-default);
155
- color: var(--color-text-secondary);
156
- }