@felloinnovations/brand-kit 0.5.1 → 0.5.3

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 (104) hide show
  1. package/AGENTS.md +29 -9
  2. package/CHANGELOG.md +86 -0
  3. package/README.md +10 -4
  4. package/design-system/01-how-claude-decides.md +5 -3
  5. package/design-system/03-colour.md +19 -1
  6. package/design-system/05-space-and-layout.md +2 -1
  7. package/design-system/08-choosing-components.md +20 -4
  8. package/design-system/09-charts.md +6 -3
  9. package/design-system/11-page-recipes.md +40 -1
  10. package/design-system/13-motion.md +1 -1
  11. package/design-system/15-ai-experiences.md +11 -0
  12. package/design-system/17-decisions.md +41 -0
  13. package/design-system/components/AppShell/README.md +89 -21
  14. package/design-system/components/AppShell/preview.html +148 -3
  15. package/design-system/components/AppShell/style.css +47 -0
  16. package/design-system/components/Avatar/README.md +6 -3
  17. package/design-system/components/Avatar/preview.html +2 -1
  18. package/design-system/components/Avatar/style.css +3 -0
  19. package/design-system/components/Banner/README.md +9 -5
  20. package/design-system/components/Banner/preview.html +9 -0
  21. package/design-system/components/Banner/style.css +3 -0
  22. package/design-system/components/Button/README.md +29 -2
  23. package/design-system/components/Button/preview.html +33 -1
  24. package/design-system/components/Button/style.css +21 -0
  25. package/design-system/components/Calendar/README.md +148 -0
  26. package/design-system/components/Calendar/preview.html +449 -0
  27. package/design-system/components/Calendar/style.css +61 -0
  28. package/design-system/components/Card/README.md +13 -0
  29. package/design-system/components/Card/preview.html +23 -0
  30. package/design-system/components/Card/style.css +19 -0
  31. package/design-system/components/ChatCard/README.md +74 -3
  32. package/design-system/components/ChatCard/preview.html +126 -25
  33. package/design-system/components/ChatCard/style.css +6 -0
  34. package/design-system/components/Chip/README.md +16 -7
  35. package/design-system/components/Chip/preview.html +25 -1
  36. package/design-system/components/Chip/style.css +5 -0
  37. package/design-system/components/Combobox/README.md +10 -4
  38. package/design-system/components/Combobox/preview.html +19 -4
  39. package/design-system/components/Combobox/style.css +7 -1
  40. package/design-system/components/CommentThread/README.md +138 -0
  41. package/design-system/components/CommentThread/preview.html +134 -0
  42. package/design-system/components/CommentThread/style.css +41 -0
  43. package/design-system/components/DateInput/README.md +38 -4
  44. package/design-system/components/DateInput/preview.html +50 -2
  45. package/design-system/components/DateInput/style.css +6 -0
  46. package/design-system/components/Dialog/README.md +26 -3
  47. package/design-system/components/Dialog/preview.html +80 -0
  48. package/design-system/components/Dialog/style.css +28 -3
  49. package/design-system/components/EmptyState/README.md +1 -1
  50. package/design-system/components/Fold/README.md +21 -0
  51. package/design-system/components/Fold/preview.html +44 -1
  52. package/design-system/components/Fold/style.css +30 -0
  53. package/design-system/components/InlineEdit/README.md +116 -0
  54. package/design-system/components/InlineEdit/preview.html +135 -0
  55. package/design-system/components/InlineEdit/style.css +34 -0
  56. package/design-system/components/Input/README.md +20 -1
  57. package/design-system/components/Input/preview.html +24 -0
  58. package/design-system/components/Input/style.css +13 -1
  59. package/design-system/components/KanbanBoard/README.md +68 -4
  60. package/design-system/components/KanbanBoard/preview.html +129 -0
  61. package/design-system/components/KanbanBoard/style.css +31 -0
  62. package/design-system/components/PlanTimeline/README.md +144 -0
  63. package/design-system/components/PlanTimeline/preview.html +109 -0
  64. package/design-system/components/PlanTimeline/style.css +64 -0
  65. package/design-system/components/README.md +11 -6
  66. package/design-system/components/Skeleton/README.md +66 -2
  67. package/design-system/components/Skeleton/preview.html +83 -1
  68. package/design-system/components/Starter/README.md +1 -1
  69. package/design-system/components/Starter/preview.html +1 -1
  70. package/design-system/components/Stepper/README.md +81 -5
  71. package/design-system/components/Stepper/preview.html +64 -1
  72. package/design-system/components/Stepper/style.css +29 -0
  73. package/design-system/components/StreakCalendar/README.md +1 -0
  74. package/design-system/components/Table/README.md +7 -3
  75. package/design-system/components/Table/preview.html +15 -0
  76. package/design-system/components/Table/style.css +2 -0
  77. package/design-system/components/TextDiff/README.md +96 -0
  78. package/design-system/components/TextDiff/preview.html +42 -0
  79. package/design-system/components/TextDiff/style.css +21 -0
  80. package/design-system/components/Timeline/README.md +3 -1
  81. package/design-system/components/TrendLine/README.md +38 -7
  82. package/design-system/components/TrendLine/preview.html +140 -3
  83. package/design-system/components/TrendLine/style.css +12 -2
  84. package/design-system/components/bundle.css +483 -7
  85. package/design-system/tokens.json +15 -1
  86. package/dist/fello-core.css +2 -2
  87. package/dist/fello.css +2 -2
  88. package/dist/fello.scoped.css +2 -2
  89. package/dist/index.d.ts +1 -1
  90. package/dist/scoped.js +2 -2
  91. package/dist/tokens.d.ts +8 -0
  92. package/dist/tokens.js +8 -0
  93. package/package.json +7 -2
  94. package/scripts/check-drift.mjs +325 -0
  95. package/scripts/token-model.mjs +2 -1
  96. package/tokens/_dark.css +2 -0
  97. package/tokens/_product.css +2 -0
  98. package/tokens/palettes.ts +1 -1
  99. package/tokens/shadcn-compat.css +1 -1
  100. package/tokens/shadcn-compat.hsl.css +1 -1
  101. package/tokens/tokens.css +14 -1
  102. package/tokens/tokens.md +53 -46
  103. package/tokens/tokens.root.css +14 -1
  104. package/tokens/tokens.ts +9 -1
package/dist/tokens.d.ts CHANGED
@@ -122,6 +122,7 @@ declare const tokens: {
122
122
  readonly "--ws-tint-accent": "color-mix(in srgb, var(--color-bg-page) 96%, #4f4c7c)";
123
123
  readonly "--ws-tint-needs": "color-mix(in srgb, var(--ws-chart-under-half) 10%, var(--color-surface))";
124
124
  readonly "--ws-tint-live": "color-mix(in srgb, var(--ws-chart-finished) 12%, var(--color-surface))";
125
+ readonly "--ws-tint-alert": "color-mix(in srgb, var(--ws-chart-alert) 8%, var(--color-surface))";
125
126
  readonly "--ws-edge": "var(--color-gray-stroke)";
126
127
  readonly "--ws-edge-card": "var(--color-gray-3)";
127
128
  readonly "--ws-edge-card-1x": "color-mix(in srgb, var(--ws-edge-card) 50%, transparent)";
@@ -269,6 +270,7 @@ declare const tokens: {
269
270
  readonly "--ws-shadow-menu": "var(--ws-float-rim), 0 16px 32px -16px color-mix(in srgb, var(--ws-shade) 35%, transparent)";
270
271
  readonly "--ws-shadow-tip": "var(--ws-float-rim), 0 16px 32px -20px color-mix(in srgb, var(--ws-shade) 45%, transparent)";
271
272
  readonly "--ws-shadow-float": "var(--ws-float-rim), 0 40px 80px -30px color-mix(in srgb, var(--ws-shade) 30%, transparent), 0 10px 24px -12px color-mix(in srgb, var(--ws-shade) 14%, transparent)";
273
+ readonly "--ws-shadow-rise": "var(--ws-float-rim), 0 -16px 32px -16px color-mix(in srgb, var(--ws-shade) 35%, transparent)";
272
274
  readonly "--ws-shadow-bar": "var(--ws-float-rim), 0 24px 60px -24px color-mix(in srgb, var(--ws-shade) 32%, transparent), 0 2px 10px color-mix(in srgb, var(--ws-shade) 6%, transparent)";
273
275
  readonly "--ws-hover-faint": "color-mix(in srgb, var(--ws-ink) 3.137254902%, transparent)";
274
276
  readonly "--ws-hover": "color-mix(in srgb, var(--ws-ink) 5.0980392157%, transparent)";
@@ -551,6 +553,7 @@ declare const themes: {
551
553
  readonly "--ws-tint-accent": "#f7f4f4";
552
554
  readonly "--ws-tint-needs": "#fff1ef";
553
555
  readonly "--ws-tint-live": "#e1f0f0";
556
+ readonly "--ws-tint-alert": "#f9ebee";
554
557
  readonly "--ws-edge": "#e8e8e8";
555
558
  readonly "--ws-edge-card": "#d3d3d3";
556
559
  readonly "--ws-edge-card-1x": "rgba(211, 211, 211, 0.5)";
@@ -698,6 +701,7 @@ declare const themes: {
698
701
  readonly "--ws-shadow-menu": "0 0 0 0 transparent, 0 16px 32px -16px rgba(9, 53, 85, 0.35)";
699
702
  readonly "--ws-shadow-tip": "0 0 0 0 transparent, 0 16px 32px -20px rgba(9, 53, 85, 0.45)";
700
703
  readonly "--ws-shadow-float": "0 0 0 0 transparent, 0 40px 80px -30px rgba(9, 53, 85, 0.3), 0 10px 24px -12px rgba(9, 53, 85, 0.14)";
704
+ readonly "--ws-shadow-rise": "0 0 0 0 transparent, 0 -16px 32px -16px rgba(9, 53, 85, 0.35)";
701
705
  readonly "--ws-shadow-bar": "0 0 0 0 transparent, 0 24px 60px -24px rgba(9, 53, 85, 0.32), 0 2px 10px rgba(9, 53, 85, 0.06)";
702
706
  readonly "--ws-hover-faint": "rgba(9, 53, 85, 0.031)";
703
707
  readonly "--ws-hover": "rgba(9, 53, 85, 0.051)";
@@ -975,6 +979,7 @@ declare const themes: {
975
979
  readonly "--ws-tint-accent": "#131e28";
976
980
  readonly "--ws-tint-needs": "#2e252a";
977
981
  readonly "--ws-tint-live": "#112a32";
982
+ readonly "--ws-tint-alert": "#2e212c";
978
983
  readonly "--ws-edge": "#1e2a36";
979
984
  readonly "--ws-edge-card": "#273544";
980
985
  readonly "--ws-edge-card-1x": "#273544";
@@ -1122,6 +1127,7 @@ declare const themes: {
1122
1127
  readonly "--ws-shadow-menu": "0 0 0 0.5px rgba(232, 237, 243, 0.08), 0 16px 32px -16px rgba(0, 0, 0, 0.35)";
1123
1128
  readonly "--ws-shadow-tip": "0 0 0 0.5px rgba(232, 237, 243, 0.08), 0 16px 32px -20px rgba(0, 0, 0, 0.45)";
1124
1129
  readonly "--ws-shadow-float": "0 0 0 0.5px rgba(232, 237, 243, 0.08), 0 40px 80px -30px rgba(0, 0, 0, 0.3), 0 10px 24px -12px rgba(0, 0, 0, 0.14)";
1130
+ readonly "--ws-shadow-rise": "0 0 0 0.5px rgba(232, 237, 243, 0.08), 0 -16px 32px -16px rgba(0, 0, 0, 0.35)";
1125
1131
  readonly "--ws-shadow-bar": "0 0 0 0.5px rgba(232, 237, 243, 0.08), 0 24px 60px -24px rgba(0, 0, 0, 0.32), 0 2px 10px rgba(0, 0, 0, 0.06)";
1126
1132
  readonly "--ws-hover-faint": "rgba(232, 237, 243, 0.031)";
1127
1133
  readonly "--ws-hover": "rgba(232, 237, 243, 0.051)";
@@ -1399,6 +1405,7 @@ declare const themes: {
1399
1405
  readonly "--ws-tint-accent": "#11283e";
1400
1406
  readonly "--ws-tint-needs": "#302a3a";
1401
1407
  readonly "--ws-tint-live": "#062d41";
1408
+ readonly "--ws-tint-alert": "#30243d";
1402
1409
  readonly "--ws-edge": "rgba(255, 255, 255, 0.1)";
1403
1410
  readonly "--ws-edge-card": "rgba(255, 255, 255, 0.18)";
1404
1411
  readonly "--ws-edge-card-1x": "rgba(255, 255, 255, 0.18)";
@@ -1546,6 +1553,7 @@ declare const themes: {
1546
1553
  readonly "--ws-shadow-menu": "0 0 0 0.5px rgba(255, 255, 255, 0.08), 0 16px 32px -16px rgba(0, 0, 0, 0.35)";
1547
1554
  readonly "--ws-shadow-tip": "0 0 0 0.5px rgba(255, 255, 255, 0.08), 0 16px 32px -20px rgba(0, 0, 0, 0.45)";
1548
1555
  readonly "--ws-shadow-float": "0 0 0 0.5px rgba(255, 255, 255, 0.08), 0 40px 80px -30px rgba(0, 0, 0, 0.3), 0 10px 24px -12px rgba(0, 0, 0, 0.14)";
1556
+ readonly "--ws-shadow-rise": "0 0 0 0.5px rgba(255, 255, 255, 0.08), 0 -16px 32px -16px rgba(0, 0, 0, 0.35)";
1549
1557
  readonly "--ws-shadow-bar": "0 0 0 0.5px rgba(255, 255, 255, 0.08), 0 24px 60px -24px rgba(0, 0, 0, 0.32), 0 2px 10px rgba(0, 0, 0, 0.06)";
1550
1558
  readonly "--ws-hover-faint": "rgba(255, 255, 255, 0.031)";
1551
1559
  readonly "--ws-hover": "rgba(255, 255, 255, 0.051)";
package/dist/tokens.js CHANGED
@@ -125,6 +125,7 @@ var tokens = {
125
125
  "--ws-tint-accent": "color-mix(in srgb, var(--color-bg-page) 96%, #4f4c7c)",
126
126
  "--ws-tint-needs": "color-mix(in srgb, var(--ws-chart-under-half) 10%, var(--color-surface))",
127
127
  "--ws-tint-live": "color-mix(in srgb, var(--ws-chart-finished) 12%, var(--color-surface))",
128
+ "--ws-tint-alert": "color-mix(in srgb, var(--ws-chart-alert) 8%, var(--color-surface))",
128
129
  "--ws-edge": "var(--color-gray-stroke)",
129
130
  "--ws-edge-card": "var(--color-gray-3)",
130
131
  "--ws-edge-card-1x": "color-mix(in srgb, var(--ws-edge-card) 50%, transparent)",
@@ -272,6 +273,7 @@ var tokens = {
272
273
  "--ws-shadow-menu": "var(--ws-float-rim), 0 16px 32px -16px color-mix(in srgb, var(--ws-shade) 35%, transparent)",
273
274
  "--ws-shadow-tip": "var(--ws-float-rim), 0 16px 32px -20px color-mix(in srgb, var(--ws-shade) 45%, transparent)",
274
275
  "--ws-shadow-float": "var(--ws-float-rim), 0 40px 80px -30px color-mix(in srgb, var(--ws-shade) 30%, transparent), 0 10px 24px -12px color-mix(in srgb, var(--ws-shade) 14%, transparent)",
276
+ "--ws-shadow-rise": "var(--ws-float-rim), 0 -16px 32px -16px color-mix(in srgb, var(--ws-shade) 35%, transparent)",
275
277
  "--ws-shadow-bar": "var(--ws-float-rim), 0 24px 60px -24px color-mix(in srgb, var(--ws-shade) 32%, transparent), 0 2px 10px color-mix(in srgb, var(--ws-shade) 6%, transparent)",
276
278
  "--ws-hover-faint": "color-mix(in srgb, var(--ws-ink) 3.137254902%, transparent)",
277
279
  "--ws-hover": "color-mix(in srgb, var(--ws-ink) 5.0980392157%, transparent)",
@@ -551,6 +553,7 @@ var themes = {
551
553
  "--ws-tint-accent": "#f7f4f4",
552
554
  "--ws-tint-needs": "#fff1ef",
553
555
  "--ws-tint-live": "#e1f0f0",
556
+ "--ws-tint-alert": "#f9ebee",
554
557
  "--ws-edge": "#e8e8e8",
555
558
  "--ws-edge-card": "#d3d3d3",
556
559
  "--ws-edge-card-1x": "rgba(211, 211, 211, 0.5)",
@@ -698,6 +701,7 @@ var themes = {
698
701
  "--ws-shadow-menu": "0 0 0 0 transparent, 0 16px 32px -16px rgba(9, 53, 85, 0.35)",
699
702
  "--ws-shadow-tip": "0 0 0 0 transparent, 0 16px 32px -20px rgba(9, 53, 85, 0.45)",
700
703
  "--ws-shadow-float": "0 0 0 0 transparent, 0 40px 80px -30px rgba(9, 53, 85, 0.3), 0 10px 24px -12px rgba(9, 53, 85, 0.14)",
704
+ "--ws-shadow-rise": "0 0 0 0 transparent, 0 -16px 32px -16px rgba(9, 53, 85, 0.35)",
701
705
  "--ws-shadow-bar": "0 0 0 0 transparent, 0 24px 60px -24px rgba(9, 53, 85, 0.32), 0 2px 10px rgba(9, 53, 85, 0.06)",
702
706
  "--ws-hover-faint": "rgba(9, 53, 85, 0.031)",
703
707
  "--ws-hover": "rgba(9, 53, 85, 0.051)",
@@ -975,6 +979,7 @@ var themes = {
975
979
  "--ws-tint-accent": "#131e28",
976
980
  "--ws-tint-needs": "#2e252a",
977
981
  "--ws-tint-live": "#112a32",
982
+ "--ws-tint-alert": "#2e212c",
978
983
  "--ws-edge": "#1e2a36",
979
984
  "--ws-edge-card": "#273544",
980
985
  "--ws-edge-card-1x": "#273544",
@@ -1122,6 +1127,7 @@ var themes = {
1122
1127
  "--ws-shadow-menu": "0 0 0 0.5px rgba(232, 237, 243, 0.08), 0 16px 32px -16px rgba(0, 0, 0, 0.35)",
1123
1128
  "--ws-shadow-tip": "0 0 0 0.5px rgba(232, 237, 243, 0.08), 0 16px 32px -20px rgba(0, 0, 0, 0.45)",
1124
1129
  "--ws-shadow-float": "0 0 0 0.5px rgba(232, 237, 243, 0.08), 0 40px 80px -30px rgba(0, 0, 0, 0.3), 0 10px 24px -12px rgba(0, 0, 0, 0.14)",
1130
+ "--ws-shadow-rise": "0 0 0 0.5px rgba(232, 237, 243, 0.08), 0 -16px 32px -16px rgba(0, 0, 0, 0.35)",
1125
1131
  "--ws-shadow-bar": "0 0 0 0.5px rgba(232, 237, 243, 0.08), 0 24px 60px -24px rgba(0, 0, 0, 0.32), 0 2px 10px rgba(0, 0, 0, 0.06)",
1126
1132
  "--ws-hover-faint": "rgba(232, 237, 243, 0.031)",
1127
1133
  "--ws-hover": "rgba(232, 237, 243, 0.051)",
@@ -1399,6 +1405,7 @@ var themes = {
1399
1405
  "--ws-tint-accent": "#11283e",
1400
1406
  "--ws-tint-needs": "#302a3a",
1401
1407
  "--ws-tint-live": "#062d41",
1408
+ "--ws-tint-alert": "#30243d",
1402
1409
  "--ws-edge": "rgba(255, 255, 255, 0.1)",
1403
1410
  "--ws-edge-card": "rgba(255, 255, 255, 0.18)",
1404
1411
  "--ws-edge-card-1x": "rgba(255, 255, 255, 0.18)",
@@ -1546,6 +1553,7 @@ var themes = {
1546
1553
  "--ws-shadow-menu": "0 0 0 0.5px rgba(255, 255, 255, 0.08), 0 16px 32px -16px rgba(0, 0, 0, 0.35)",
1547
1554
  "--ws-shadow-tip": "0 0 0 0.5px rgba(255, 255, 255, 0.08), 0 16px 32px -20px rgba(0, 0, 0, 0.45)",
1548
1555
  "--ws-shadow-float": "0 0 0 0.5px rgba(255, 255, 255, 0.08), 0 40px 80px -30px rgba(0, 0, 0, 0.3), 0 10px 24px -12px rgba(0, 0, 0, 0.14)",
1556
+ "--ws-shadow-rise": "0 0 0 0.5px rgba(255, 255, 255, 0.08), 0 -16px 32px -16px rgba(0, 0, 0, 0.35)",
1549
1557
  "--ws-shadow-bar": "0 0 0 0.5px rgba(255, 255, 255, 0.08), 0 24px 60px -24px rgba(0, 0, 0, 0.32), 0 2px 10px rgba(0, 0, 0, 0.06)",
1550
1558
  "--ws-hover-faint": "rgba(255, 255, 255, 0.031)",
1551
1559
  "--ws-hover": "rgba(255, 255, 255, 0.051)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@felloinnovations/brand-kit",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "Fello brand tokens, the house stylesheet (fello.css), React components and the design system guide. One source of truth for every GTM app.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -19,6 +19,7 @@
19
19
  "files": [
20
20
  "dist",
21
21
  "scripts/check-palette.mjs",
22
+ "scripts/check-drift.mjs",
22
23
  "scripts/token-model.mjs",
23
24
  "tokens",
24
25
  "design-system",
@@ -26,6 +27,9 @@
26
27
  "AGENTS.md",
27
28
  "CHANGELOG.md"
28
29
  ],
30
+ "bin": {
31
+ "brand-kit-drift": "scripts/check-drift.mjs"
32
+ },
29
33
  "main": "./dist/index.js",
30
34
  "module": "./dist/index.js",
31
35
  "types": "./dist/index.d.ts",
@@ -76,7 +80,8 @@
76
80
  "hosted": "node scripts/build-hosted.mjs",
77
81
  "check:geometry": "node scripts/check-geometry.mjs",
78
82
  "check:scope": "node scripts/check-scope.mjs",
79
- "check:palette": "node scripts/check-palette.mjs"
83
+ "check:palette": "node scripts/check-palette.mjs",
84
+ "check:drift": "node scripts/check-drift.mjs --selftest"
80
85
  },
81
86
  "dependencies": {
82
87
  "@radix-ui/react-tabs": "^1.1.21",
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * check-drift: the static half of brand-audit as a script, so nothing depends on an agent remembering to grep every
4
+ * file. It walks an app's own source (never node_modules, builds or a vendored copy of the kit) and lists every line
5
+ * that has drifted from the Fello brand kit, by file and line:
6
+ *
7
+ * colour hex, rgb(), hsl(), oklch() literals and Tailwind palette classes (bg-blue-500); each hex is marked as
8
+ * matching a kit token, close to one, or no token
9
+ * tokens an app redefining a kit token (--ws-*, --color-*, ...) or restyling a kit class (.ws-*)
10
+ * type px font sizes, text-[14px], a font-family that is not the kit's
11
+ * radius raw border-radius, rounded-[...]
12
+ * shadow raw box-shadow, shadow-[...]
13
+ * trap text-[var(--x)]: Tailwind reads a bare var as a font size, so the colour silently drops
14
+ * motion raw durations and curves, transition-all, duration-300, animate-*, @keyframes, hover lifts
15
+ * underline a link underlined at rest, or its hover underline taken away (04-type: medium, underlined under the pointer)
16
+ * words em dashes in markup
17
+ * copy a local components/ui file named after a kit component that never touches the kit
18
+ *
19
+ * A line that is right as it is carries `brand-kit-ignore: <why>` on it or on the line above.
20
+ *
21
+ * node check-drift.mjs [dir] report; exit 1 when anything drifted
22
+ * node check-drift.mjs [dir] --json the findings as JSON
23
+ * node check-drift.mjs [dir] --write-baseline <file> record today's counts per file and rule, exit 0
24
+ * node check-drift.mjs [dir] --baseline <file> exit 1 only when a file and rule has more than its baseline
25
+ * (CI for an app mid-migration: nothing new gets in)
26
+ * node check-drift.mjs --selftest plants known drift in a temp folder and proves each is caught
27
+ *
28
+ * In an app: node node_modules/@felloinnovations/brand-kit/scripts/check-drift.mjs src
29
+ */
30
+ import { readFileSync, writeFileSync, readdirSync, statSync, mkdtempSync, mkdirSync, rmSync } from 'node:fs';
31
+ import { join, relative, dirname, extname, basename } from 'node:path';
32
+ import { fileURLToPath } from 'node:url';
33
+ import { tmpdir } from 'node:os';
34
+
35
+ const here = dirname(fileURLToPath(import.meta.url));
36
+
37
+ // ---- The kit's tokens: every name, and the light value of each written as a hex.
38
+ function loadTokens() {
39
+ const css = readFileSync(join(here, '../tokens/tokens.root.css'), 'utf8');
40
+ const names = new Set();
41
+ const byHex = new Map();
42
+ for (const m of css.matchAll(/(--[a-z0-9-]+)\s*:\s*([^;]+);/gi)) {
43
+ names.add(m[1]);
44
+ const hex = m[2].trim().match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
45
+ if (hex && !byHex.has(norm(hex[0]))) byHex.set(norm(hex[0]), m[1]);
46
+ }
47
+ return { names, byHex };
48
+ }
49
+
50
+ function norm(hex) {
51
+ let h = hex.slice(1).toLowerCase();
52
+ if (h.length === 3 || h.length === 4) h = [...h].map((c) => c + c).join('');
53
+ return `#${h.slice(0, 6)}`;
54
+ }
55
+
56
+ const rgb = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
57
+
58
+ /** "matches --x", "close to --x (#abc)" (no channel more than 12 off), or "no token". */
59
+ function classify(hex, byHex) {
60
+ const h = norm(hex);
61
+ if (byHex.has(h)) return `matches ${byHex.get(h)}`;
62
+ const [r, g, b] = rgb(h);
63
+ let best = null;
64
+ for (const [v, name] of byHex) {
65
+ const [r2, g2, b2] = rgb(v);
66
+ const d = Math.max(Math.abs(r - r2), Math.abs(g - g2), Math.abs(b - b2));
67
+ if (d <= 12 && (!best || d < best.d)) best = { d, name, v };
68
+ }
69
+ return best ? `close to ${best.name} (${best.v})` : 'no token';
70
+ }
71
+
72
+ // ---- What to read.
73
+ const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', 'out', '.next', '.nuxt', '.svelte-kit', '.turbo', '.vercel',
74
+ '.git', '.claude', 'coverage', 'storybook-static', '.output']);
75
+ const EXT = new Set(['.css', '.scss', '.sass', '.less', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.html', '.vue', '.svelte', '.astro']);
76
+ const STYLE = new Set(['.css', '.scss', '.sass', '.less']);
77
+ const MARKUP = new Set(['.tsx', '.jsx', '.html', '.vue', '.svelte', '.astro']);
78
+ const SKIP_FILE = /\.(d\.ts|test\.[jt]sx?|spec\.[jt]sx?|stories\.[jt]sx?)$|\.min\.(css|js)$/;
79
+ // A vendored copy of the kit (fello-css.mjs output, the brand-kit's own files) is the kit, not drift.
80
+ const VENDORED = /Generated by scripts\/fello-css\.mjs|generated by scripts\/build-bundle\.mjs/;
81
+
82
+ function* walk(dir) {
83
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
84
+ if (e.isDirectory()) {
85
+ if (!SKIP_DIRS.has(e.name)) yield* walk(join(dir, e.name));
86
+ } else if (EXT.has(extname(e.name)) && !SKIP_FILE.test(e.name)) yield join(dir, e.name);
87
+ }
88
+ }
89
+
90
+ // ---- The rules. Each returns [{ col, value, note }] for one line.
91
+ const TW_HUES = 'slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose';
92
+ const TW_PREFIX = 'bg|text|border|ring|fill|stroke|from|via|to|outline|divide|placeholder|accent|caret|decoration|shadow';
93
+ const KIT_UI = new Set(['button', 'badge', 'card', 'input', 'label', 'skeleton', 'tabs', 'toast', 'toaster', 'checkbox',
94
+ 'radio', 'radio-group', 'switch', 'select', 'textarea', 'dialog', 'tooltip', 'popover', 'avatar', 'table', 'menu',
95
+ 'dropdown-menu', 'slider', 'pagination', 'breadcrumb', 'stepper', 'combobox']);
96
+
97
+ const all = (re, line, fn) => [...line.matchAll(re)].flatMap((m) => fn(m) ?? []);
98
+
99
+ function rules(tokens) {
100
+ return {
101
+ colour: (line, ext) => [
102
+ ...all(/(^|[\s'"`(:,[=])(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3,4}))(?![0-9a-z_-])/gi, line, (m) => {
103
+ if (/(href|to|id)=["'`{]*$/.test(line.slice(0, m.index + m[1].length))) return null;
104
+ if (!STYLE.has(ext) && /^#[0-9a-f]{3,4}$/i.test(m[2]) && !/color|colour|fill|stroke|background|border|#[0-9a-f]{3,4}['"`;)]/i.test(line)) return null;
105
+ return { col: m.index + m[1].length, value: m[2], note: classify(m[2], tokens.byHex) };
106
+ }),
107
+ ...all(/\b(rgba?|hsla?|oklch|oklab|lab|lch)\(([^)]*)\)/g, line, (m) =>
108
+ /var\(|from\s/.test(m[2]) ? null : { col: m.index, value: m[0], note: 'no token' }),
109
+ ...all(new RegExp(`(?<![\\w-])(?:[a-z-]+:)*(?:${TW_PREFIX})-(?:${TW_HUES})-\\d{2,3}(?:\\/\\d+)?\\b`, 'g'), line, (m) =>
110
+ ({ col: m.index, value: m[0], note: 'Tailwind palette, not a kit token' })),
111
+ ],
112
+ tokens: (line) => [
113
+ ...all(/(?<![\w(-])(--(?:ws|color|font|space|radius|shadow|text|size|brand)-[a-z0-9-]+)\s*:/g, line, (m) =>
114
+ tokens.names.has(m[1]) ? { col: m.index, value: m[1], note: 'redefines a kit token (a palette, 20-palettes.md, sets colours)' } : null),
115
+ ...all(/(^|[\s,}>~+])(\.ws-[a-z0-9_-]+)[^{};]*\{/g, line, (m) =>
116
+ ({ col: m.index + m[1].length, value: m[2], note: 'restyles a kit class' })),
117
+ ],
118
+ type: (line) => [
119
+ ...all(/font-size\s*:\s*[\d.]+px/g, line, (m) => ({ col: m.index, value: m[0], note: 'px font size' })),
120
+ ...all(/fontSize\s*:\s*['"]?[\d.]+(px)?['"]?\s*[,}]/g, line, (m) => ({ col: m.index, value: m[0], note: 'px font size' })),
121
+ ...all(/\btext-\[[\d.]+(px|rem)\]/g, line, (m) => ({ col: m.index, value: m[0], note: 'arbitrary font size' })),
122
+ ...all(/font-family\s*:\s*([^;}]+)/g, line, (m) =>
123
+ /var\(|instrument sans|inherit|monospace|ui-monospace/i.test(m[1]) ? null : { col: m.index, value: m[0].trim(), note: 'not the kit face' }),
124
+ ...all(/\bfont-\[['"]?[A-Za-z]/g, line, (m) => ({ col: m.index, value: m[0], note: 'not the kit face' })),
125
+ ],
126
+ radius: (line) => [
127
+ ...all(/border-radius\s*:\s*(?!var|0[;\s]|inherit|50%)[\d.]+(px|rem|em)/g, line, (m) => ({ col: m.index, value: m[0], note: 'raw radius' })),
128
+ ...all(/borderRadius\s*:\s*['"]?\d/g, line, (m) => ({ col: m.index, value: m[0], note: 'raw radius' })),
129
+ ...all(/\brounded(-[a-z]{1,2})?-\[(?!var)[^\]]+\]/g, line, (m) => ({ col: m.index, value: m[0], note: 'raw radius' })),
130
+ ],
131
+ shadow: (line) => [
132
+ ...all(/box-shadow\s*:\s*(?!var|none|inherit|0\s*0\s*0\s*0)([^;}]+)/g, line, (m) => ({ col: m.index, value: m[0].trim(), note: 'raw shadow' })),
133
+ ...all(/boxShadow\s*:\s*['"`](?!var|none)/g, line, (m) => ({ col: m.index, value: m[0], note: 'raw shadow' })),
134
+ ...all(/\bshadow-\[(?!var)[^\]]+\]/g, line, (m) => ({ col: m.index, value: m[0], note: 'raw shadow' })),
135
+ ],
136
+ trap: (line) => all(/\btext-\[var\(--/g, line, (m) => ({ col: m.index, value: m[0], note: 'write text-[color:var(--x)] or text-[length:var(--x)]' })),
137
+ motion: (line, ext) => [
138
+ ...all(/(transition|animation)(-duration)?\s*:\s*(?![^;}]*var\()[^;}]*\b\d*\.?\d+m?s\b[^;}]*/g, line, (m) => ({ col: m.index, value: m[0].trim(), note: 'raw duration (--ws-spring-*, --ws-duration-*)' })),
139
+ ...all(/cubic-bezier\(/g, line, (m) => ({ col: m.index, value: m[0], note: 'raw curve (--ws-ease-*)' })),
140
+ ...(STYLE.has(ext) ? all(/@keyframes\s+[\w-]+/g, line, (m) => ({ col: m.index, value: m[0], note: 'hand-rolled animation' })) : []),
141
+ ...all(/(?<![\w-])(transition-all|duration-\d+|ease-(in|out|in-out|linear)|animate-(spin|ping|pulse|bounce))(?![\w-])/g, line, (m) =>
142
+ ({ col: m.index, value: m[0], note: 'Tailwind motion, not the kit springs' })),
143
+ ...all(/\bhover:(-?translate-[xy]-[\w.[\]-]+|-?scale-[\w.[\]-]+|shadow(-[\w[\]-]+)?)/g, line, (m) =>
144
+ ({ col: m.index, value: m[0], note: 'hover lift (hover is a tint)' })),
145
+ ],
146
+ underline: (line) => /class(Name)?\s*=|\bcn\(|clsx\(/.test(line) ? [
147
+ // A dotted underline is the kit's mark for a cell whose full words show on hover (04-type), not a link.
148
+ ...(/decoration-dotted/.test(line) ? [] : all(/(?<![\w:-])underline(?![\w-])/g, line, (m) => ({ col: m.index, value: 'underline', note: 'underlined at rest' }))),
149
+ ...all(/\bhover:no-underline\b/g, line, (m) => ({ col: m.index, value: m[0], note: 'hover underline taken away' })),
150
+ ] : [],
151
+ words: (line, ext) => (MARKUP.has(ext)
152
+ ? all(/—/g, line, (m) => ({ col: m.index, value: '—', note: 'em dash in UI words' })) : []),
153
+ };
154
+ }
155
+
156
+ /** The lines with comments (/* *\/ across lines, // to the end, <!-- -->) blanked to spaces, so columns hold and a
157
+ * colour or a dash in a comment is not drift. `//` after a colon (a URL) is not a comment. */
158
+ function uncomment(lines) {
159
+ let open = false;
160
+ return lines.map((line) => {
161
+ let out = '';
162
+ let i = 0;
163
+ while (i < line.length) {
164
+ if (open) {
165
+ const end = line.indexOf('*/', i);
166
+ const stop = end < 0 ? line.length : end + 2;
167
+ out += ' '.repeat(stop - i);
168
+ i = stop;
169
+ if (end >= 0) open = false;
170
+ continue;
171
+ }
172
+ const rest = line.slice(i);
173
+ if (rest.startsWith('/*')) { open = true; continue; }
174
+ if (rest.startsWith('<!--')) {
175
+ const end = line.indexOf('-->', i);
176
+ const stop = end < 0 ? line.length : end + 3;
177
+ out += ' '.repeat(stop - i);
178
+ i = stop;
179
+ continue;
180
+ }
181
+ if (rest.startsWith('//') && (i === 0 || /[\s;{}(),]/.test(line[i - 1]))) { out += ' '.repeat(line.length - i); break; }
182
+ out += line[i++];
183
+ }
184
+ return out;
185
+ });
186
+ }
187
+
188
+ function scan(dir, tokens) {
189
+ const R = rules(tokens);
190
+ const findings = [];
191
+ for (const file of walk(dir)) {
192
+ const text = readFileSync(file, 'utf8');
193
+ if (VENDORED.test(text.slice(0, 2000))) continue;
194
+ const rel = relative(dir, file) || basename(file);
195
+ const ext = extname(file);
196
+ const lines = text.split('\n');
197
+ const code = uncomment(lines);
198
+ lines.forEach((raw, i) => {
199
+ if (/brand-kit-ignore/.test(raw) || /brand-kit-ignore/.test(lines[i - 1] ?? '')) return;
200
+ if (raw.length > 2000) return; // a minified or inlined blob
201
+ const line = code[i];
202
+ for (const [rule, fn] of Object.entries(R)) {
203
+ for (const f of fn(line, ext)) findings.push({ file: rel, line: i + 1, col: f.col + 1, rule, value: f.value, note: f.note });
204
+ }
205
+ });
206
+ const name = basename(file, ext).toLowerCase();
207
+ if (/components\/ui\//.test(file) && KIT_UI.has(name) && !/ws-|@felloinnovations\/brand-kit/.test(text)) {
208
+ findings.push({ file: rel, line: 1, col: 1, rule: 'copy', value: basename(file), note: 'a local component the kit has; use the kit\'s class or component' });
209
+ }
210
+ }
211
+ return findings;
212
+ }
213
+
214
+ const key = (f) => `${f.file}::${f.rule}`;
215
+ const countBy = (fs) => fs.reduce((m, f) => m.set(key(f), (m.get(key(f)) ?? 0) + 1), new Map());
216
+
217
+ function report(findings, out = console.log) {
218
+ if (!findings.length) return out('brand-kit drift: nothing found.');
219
+ const files = new Set(findings.map((f) => f.file));
220
+ out(`brand-kit drift: ${findings.length} finding${findings.length === 1 ? '' : 's'} in ${files.size} file${files.size === 1 ? '' : 's'}`);
221
+ const byRule = new Map();
222
+ for (const f of findings) byRule.set(f.rule, (byRule.get(f.rule) ?? 0) + 1);
223
+ const colours = findings.filter((f) => f.rule === 'colour');
224
+ const parts = ['matches', 'close to', 'no token'].map((p) => `${colours.filter((f) => f.note.startsWith(p)).length} ${p === 'matches' ? 'match a token' : p}`);
225
+ for (const [rule, n] of [...byRule].sort((a, b) => b[1] - a[1])) out(` ${rule.padEnd(10)}${String(n).padStart(5)}${rule === 'colour' ? ` (${parts.join(', ')})` : ''}`);
226
+ for (const file of files) {
227
+ out(`\n${file}`);
228
+ for (const f of findings.filter((x) => x.file === file)) out(` ${`${f.line}:${f.col}`.padEnd(9)}${f.rule.padEnd(10)}${f.value} ${f.note}`);
229
+ }
230
+ }
231
+
232
+ function selftest() {
233
+ const dir = mkdtempSync(join(tmpdir(), 'brand-drift-'));
234
+ try {
235
+ mkdirSync(join(dir, 'components/ui'), { recursive: true });
236
+ mkdirSync(join(dir, 'node_modules/x'), { recursive: true });
237
+ writeFileSync(join(dir, 'node_modules/x/a.css'), 'a { color: #123456; }');
238
+ writeFileSync(join(dir, 'vendored.ts'), '/* Generated by scripts/fello-css.mjs */\nexport const css = `.ws-btn { color: #f00 }`;');
239
+ writeFileSync(join(dir, 'app.css'), [
240
+ '.a { color: #093555; }', // matches --color-brand-navy
241
+ '.b { color: #0b3757; }', // close to it
242
+ '.c { color: #12ab34; }', // no token
243
+ '.d { font-size: 13px; border-radius: 6px; box-shadow: 0 1px 2px rgba(0,0,0,.1); }',
244
+ '.e { transition: opacity 200ms ease; }',
245
+ '@keyframes spin { to { transform: rotate(1turn) } }',
246
+ ':root { --ws-ink: red; --my-own: 1px; }',
247
+ '.ws-btn { padding: 0 }',
248
+ '.f { color: #abcdef; } /* brand-kit-ignore: a third-party embed */',
249
+ '.g { color: var(--ws-ink); transition: opacity var(--ws-duration-reveal); }',
250
+ ].join('\n'));
251
+ writeFileSync(join(dir, 'page.tsx'), [
252
+ 'export const P = () => <a href="#add" className="text-blue-500 underline hover:-translate-y-1 text-[var(--ws-ink)] duration-300">Hi — there</a>;',
253
+ '// a comment with — is fine',
254
+ 'const ok = <div className="hover:underline text-[color:var(--ws-ink)]" />;',
255
+ '{/* a block comment that runs on',
256
+ ' with a — and #ff0000 inside */}',
257
+ 'const dotted = <span className="underline decoration-dotted" />;',
258
+ 'const url = "https://example.com/a"; // #ff0000 in a line comment',
259
+ ].join('\n'));
260
+ writeFileSync(join(dir, 'components/ui/button.tsx'), 'export const Button = () => <button className="px-4" />;');
261
+ writeFileSync(join(dir, 'components/ui/card.tsx'), 'export const Card = () => <div className="ws-card" />;');
262
+ const fs = scan(dir, loadTokens());
263
+ const n = (rule, pred = () => true) => fs.filter((f) => f.rule === rule && pred(f)).length;
264
+ const expect = [
265
+ ['colour matches a token', n('colour', (f) => f.note === 'matches --color-brand-navy'), 1],
266
+ ['colour close to a token', n('colour', (f) => f.note.startsWith('close to --color-brand-navy')), 1],
267
+ ['colour with no token (hex, rgba)', n('colour', (f) => f.note === 'no token'), 2],
268
+ ['Tailwind palette class', n('colour', (f) => f.value === 'text-blue-500'), 1],
269
+ ['#add in an href is not a colour', n('colour', (f) => f.value === '#add'), 0],
270
+ ['type', n('type'), 1],
271
+ ['radius', n('radius'), 1],
272
+ ['shadow', n('shadow'), 1],
273
+ ['motion (raw duration, keyframes, duration-300, hover lift)', n('motion'), 4],
274
+ ['a kit token redefined, an own token left alone', n('tokens', (f) => f.value === '--ws-ink'), 1],
275
+ ['a kit class restyled', n('tokens', (f) => f.value === '.ws-btn'), 1],
276
+ ['the trap', n('trap'), 1],
277
+ ['underlined at rest, hover underline fine', n('underline'), 1],
278
+ ['em dash in markup, not in a comment', n('words'), 1],
279
+ ['a local kit component copy, not one that wears ws-', n('copy'), 1],
280
+ ['node_modules, vendored kit and ignored lines skipped', fs.filter((f) => /node_modules|vendored/.test(f.file) || f.value === '#abcdef').length, 0],
281
+ ];
282
+ let bad = 0;
283
+ for (const [what, got, want] of expect) {
284
+ const ok = got === want;
285
+ if (!ok) bad++;
286
+ console.log(`${ok ? 'ok ' : 'FAIL'} ${what}: ${got}${ok ? '' : ` (want ${want})`}`);
287
+ }
288
+ if (bad) { report(fs); process.exit(1); }
289
+ console.log('check-drift selftest: every planted drift caught.');
290
+ } finally {
291
+ rmSync(dir, { recursive: true, force: true });
292
+ }
293
+ }
294
+
295
+ // ---- Run.
296
+ const args = process.argv.slice(2);
297
+ const flag = (name) => { const i = args.indexOf(name); return i < 0 ? null : (args.splice(i, 2)[1] ?? ''); };
298
+ if (args.includes('--selftest')) selftest();
299
+ else run();
300
+
301
+ function run() {
302
+ const json = args.includes('--json') && args.splice(args.indexOf('--json'), 1);
303
+ const writeBase = flag('--write-baseline');
304
+ const base = flag('--baseline');
305
+ const dir = args[0] ?? '.';
306
+ if (!statSync(dir, { throwIfNoEntry: false })?.isDirectory()) { console.error(`check-drift: ${dir} is not a folder`); process.exit(2); }
307
+
308
+ let findings = scan(dir, loadTokens());
309
+ if (writeBase) {
310
+ writeFileSync(writeBase, `${JSON.stringify({ version: 1, counts: Object.fromEntries([...countBy(findings)].sort()) }, null, 2)}\n`);
311
+ console.log(`check-drift: baseline of ${findings.length} findings written to ${writeBase}`);
312
+ return;
313
+ }
314
+ if (base) {
315
+ const was = JSON.parse(readFileSync(base, 'utf8')).counts ?? {};
316
+ const now = countBy(findings);
317
+ const grew = new Set([...now].filter(([k, v]) => v > (was[k] ?? 0)).map(([k]) => k));
318
+ findings = findings.filter((f) => grew.has(key(f)));
319
+ if (!json) console.log(grew.size ? `check-drift: ${grew.size} file and rule pair${grew.size === 1 ? '' : 's'} drifted past ${base}` : `check-drift: nothing new past ${base}.`);
320
+ }
321
+ if (json) console.log(JSON.stringify(findings, null, 2));
322
+ else if (!base || findings.length) report(findings);
323
+ // exitCode, not exit(): a piped stdout is written after this line returns.
324
+ process.exitCode = findings.length ? 1 : 0;
325
+ }
@@ -303,7 +303,7 @@ export const GROUNDS = {
303
303
  '--color-surface-stripe': 'stripe',
304
304
  };
305
305
  // Reported, not gated: grounds that carry only their own words (a PAIR below), or a frame under the pointer.
306
- export const REPORT = { '--ws-tint-needs-work': 'needs-work tint', '--color-feedback-success-bg': 'success', '--ws-well-hover': 'well hover' };
306
+ export const REPORT = { '--ws-tint-needs-work': 'needs-work tint', '--ws-tint-alert': 'alert tint', '--color-feedback-success-bg': 'success', '--ws-well-hover': 'well hover' };
307
307
  export const WORDS = [
308
308
  '--color-text-heading', '--color-text-body', '--color-text-muted', '--color-text-interactive',
309
309
  '--color-text-brand-strong', '--color-text-error-strong', '--color-text-success-strong',
@@ -312,6 +312,7 @@ export const WORDS = [
312
312
  export const PAIRS = [
313
313
  ['--color-text-placeholder', '--ws-field'], ['--ws-field-ink', '--ws-field'], ['--ws-on-act', '--color-text-heading'],
314
314
  ['--ws-word-needs', '--ws-tint-needs'], ['--ws-word-positive', '--ws-tint-live'], ['--ws-word-needs-work', '--ws-tint-needs-work'],
315
+ ['--ws-word-alert', '--ws-tint-alert'],
315
316
  ['--color-text-success-strong', '--color-feedback-success-bg'], ['--ws-on-act', '--ws-agent-tally'],
316
317
  ];
317
318
 
package/tokens/_dark.css CHANGED
@@ -35,6 +35,7 @@
35
35
  --ws-tint-accent: var(--color-surface);
36
36
  --ws-tint-needs: color-mix(in srgb, var(--ws-chart-under-half) 13.05%, var(--color-bg-page)); /* #2E252A */
37
37
  --ws-tint-live: color-mix(in srgb, var(--ws-chart-finished) 12.1%, var(--color-bg-page)); /* was #0E2931 */
38
+ --ws-tint-alert: color-mix(in srgb, var(--ws-chart-alert) 13.05%, var(--color-bg-page)); /* derived (#15), the night tint-needs formula */
38
39
  --ws-card-tint-blue: color-mix(in srgb, var(--color-bg-page) 90%, #879bf4); /* #1B2637: the page toward the mid blue */
39
40
  --ws-card-tint-done: color-mix(in srgb, var(--ws-chart-finished) 10.4%, var(--color-bg-page)); /* was #10262B */
40
41
  --ws-track: color-mix(in srgb, var(--ws-tray) 80%, #0a1215); /* #1A2631: the tray, darker */
@@ -143,6 +144,7 @@
143
144
  --ws-tint-accent: color-mix(in srgb, var(--color-bg-page) 94%, var(--color-white));
144
145
  --ws-tint-needs: color-mix(in srgb, var(--ws-chart-under-half) 18%, var(--color-bg-page));
145
146
  --ws-tint-live: color-mix(in srgb, var(--ws-chart-finished) 13.1%, var(--color-bg-page)); /* was the light teal 18% (#032D41) */
147
+ --ws-tint-alert: color-mix(in srgb, var(--ws-chart-alert) 18%, var(--color-bg-page)); /* derived (#15), the navy tint-needs formula */
146
148
  --ws-card-tint-blue: color-mix(in srgb, var(--color-brand-mid-blue) 25%, var(--color-bg-page));
147
149
  --ws-card-tint-done: var(--ws-tint-live);
148
150
  --ws-track: color-mix(in srgb, var(--color-bg-page) 88%, var(--color-white));
@@ -34,6 +34,7 @@
34
34
  --ws-tint-accent: color-mix(in srgb, var(--color-bg-page) 96%, #4f4c7c); /* #f7f4f4: the zebra row, the page a step cooler */
35
35
  --ws-tint-needs: color-mix(in srgb, var(--ws-chart-under-half) 10%, var(--color-surface));
36
36
  --ws-tint-live: color-mix(in srgb, var(--ws-chart-finished) 12%, var(--color-surface));
37
+ --ws-tint-alert: color-mix(in srgb, var(--ws-chart-alert) 8%, var(--color-surface)); /* an error status's ground (#15): the alert red at 8%, the tint-needs formula */
37
38
  --ws-edge: var(--color-gray-stroke);
38
39
  --ws-edge-card: var(--color-gray-3); /* the designer's border/card */
39
40
  --ws-edge-card-1x: color-mix(in srgb, var(--ws-edge-card) 50%, transparent); /* the card edge on a screen under 1.5 dppx, where 0.5 px rounds up to 1 */
@@ -282,6 +283,7 @@
282
283
  --ws-shadow-menu: var(--ws-float-rim), 0 16px 32px -16px color-mix(in srgb, var(--ws-shade) 35%, transparent);
283
284
  --ws-shadow-tip: var(--ws-float-rim), 0 16px 32px -20px color-mix(in srgb, var(--ws-shade) 45%, transparent);
284
285
  --ws-shadow-float: var(--ws-float-rim), 0 40px 80px -30px color-mix(in srgb, var(--ws-shade) 30%, transparent), 0 10px 24px -12px color-mix(in srgb, var(--ws-shade) 14%, transparent);
286
+ --ws-shadow-rise: var(--ws-float-rim), 0 -16px 32px -16px color-mix(in srgb, var(--ws-shade) 35%, transparent); /* the menu's drop cast upward: a drawer that rises from the bottom of a pane */
285
287
  --ws-shadow-bar: var(--ws-float-rim), 0 24px 60px -24px color-mix(in srgb, var(--ws-shade) 32%, transparent), 0 2px 10px color-mix(in srgb, var(--ws-shade) 6%, transparent); /* the centred chat bar (North) */
286
288
  /* Seth's hover washes (North): a list row .03, a nav or menu row .05, an icon button .06. */
287
289
  /* Each alpha is the 8-bit step rgba(.03 / .05 / .06) always painted (8, 13 and 15 of 255), so a wash laid as a
@@ -1,7 +1,7 @@
1
1
  /* Generated by scripts/generate-tokens.mjs. Do not edit.
2
2
  Source: Figma Marketing - Design System 3.0 (MmOx1GHtbMge4lSLp1kcs8), captured 2026-06-11,
3
3
  tokens/_product.css (the product layer), tokens/_dark.css (the dark theme) and tokens/_palettes.css (the palettes).
4
- 422 tokens: 111 kit, 311 product layer; dark: 98 night, 42 navy ground. */
4
+ 424 tokens: 111 kit, 313 product layer; dark: 99 night, 43 navy ground. */
5
5
 
6
6
  /** The palette contract (D15): the only tokens a palette sets, each with its group and one-line meaning. */
7
7
  export const contract = [
@@ -1,7 +1,7 @@
1
1
  /* Generated by scripts/generate-tokens.mjs. Do not edit.
2
2
  Source: Figma Marketing - Design System 3.0 (MmOx1GHtbMge4lSLp1kcs8), captured 2026-06-11,
3
3
  tokens/_product.css (the product layer), tokens/_dark.css (the dark theme) and tokens/_palettes.css (the palettes).
4
- 422 tokens: 111 kit, 311 product layer; dark: 98 night, 42 navy ground. */
4
+ 424 tokens: 111 kit, 313 product layer; dark: 99 night, 43 navy ground. */
5
5
 
6
6
  /* Maps shadcn's semantic names onto Fello brand tokens. Import AFTER
7
7
  tokens.css (or fello.css). Existing shadcn components then render in brand
@@ -1,7 +1,7 @@
1
1
  /* Generated by scripts/generate-tokens.mjs. Do not edit.
2
2
  Source: Figma Marketing - Design System 3.0 (MmOx1GHtbMge4lSLp1kcs8), captured 2026-06-11,
3
3
  tokens/_product.css (the product layer), tokens/_dark.css (the dark theme) and tokens/_palettes.css (the palettes).
4
- 422 tokens: 111 kit, 311 product layer; dark: 98 night, 42 navy ground. */
4
+ 424 tokens: 111 kit, 313 product layer; dark: 99 night, 43 navy ground. */
5
5
 
6
6
  /* shadcn's names as bare HSL channels, for a host whose Tailwind config writes
7
7
  hsl(var(--primary)) (Tailwind v3 shadcn, Campfire). Literal values, so the dark