@felloinnovations/brand-kit 0.5.0 → 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 (124) hide show
  1. package/AGENTS.md +29 -9
  2. package/CHANGELOG.md +110 -0
  3. package/README.md +14 -6
  4. package/design-system/01-how-claude-decides.md +5 -3
  5. package/design-system/03-colour.md +22 -3
  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 +7 -4
  9. package/design-system/10-patterns.md +1 -1
  10. package/design-system/11-page-recipes.md +40 -1
  11. package/design-system/13-motion.md +4 -4
  12. package/design-system/15-ai-experiences.md +11 -0
  13. package/design-system/17-decisions.md +51 -0
  14. package/design-system/components/AppShell/README.md +89 -21
  15. package/design-system/components/AppShell/preview.html +148 -3
  16. package/design-system/components/AppShell/style.css +47 -0
  17. package/design-system/components/Avatar/README.md +6 -3
  18. package/design-system/components/Avatar/preview.html +2 -1
  19. package/design-system/components/Avatar/style.css +3 -0
  20. package/design-system/components/Banner/README.md +9 -5
  21. package/design-system/components/Banner/preview.html +9 -0
  22. package/design-system/components/Banner/style.css +3 -0
  23. package/design-system/components/Button/README.md +29 -2
  24. package/design-system/components/Button/preview.html +33 -1
  25. package/design-system/components/Button/style.css +21 -0
  26. package/design-system/components/Calendar/README.md +148 -0
  27. package/design-system/components/Calendar/preview.html +449 -0
  28. package/design-system/components/Calendar/style.css +61 -0
  29. package/design-system/components/Card/README.md +13 -0
  30. package/design-system/components/Card/preview.html +23 -0
  31. package/design-system/components/Card/style.css +19 -0
  32. package/design-system/components/ChatCard/README.md +74 -3
  33. package/design-system/components/ChatCard/preview.html +126 -25
  34. package/design-system/components/ChatCard/style.css +6 -0
  35. package/design-system/components/CheckRow/README.md +1 -1
  36. package/design-system/components/CheckRow/preview.html +1 -1
  37. package/design-system/components/CheckRow/style.css +1 -1
  38. package/design-system/components/Chip/README.md +16 -7
  39. package/design-system/components/Chip/preview.html +25 -1
  40. package/design-system/components/Chip/style.css +5 -0
  41. package/design-system/components/Combobox/README.md +10 -4
  42. package/design-system/components/Combobox/preview.html +19 -4
  43. package/design-system/components/Combobox/style.css +7 -1
  44. package/design-system/components/CommentThread/README.md +138 -0
  45. package/design-system/components/CommentThread/preview.html +134 -0
  46. package/design-system/components/CommentThread/style.css +41 -0
  47. package/design-system/components/DateInput/README.md +38 -4
  48. package/design-system/components/DateInput/preview.html +50 -2
  49. package/design-system/components/DateInput/style.css +6 -0
  50. package/design-system/components/Dialog/README.md +26 -3
  51. package/design-system/components/Dialog/preview.html +80 -0
  52. package/design-system/components/Dialog/style.css +28 -3
  53. package/design-system/components/EmptyState/README.md +1 -1
  54. package/design-system/components/Fold/README.md +21 -0
  55. package/design-system/components/Fold/preview.html +44 -1
  56. package/design-system/components/Fold/style.css +30 -0
  57. package/design-system/components/InlineEdit/README.md +116 -0
  58. package/design-system/components/InlineEdit/preview.html +135 -0
  59. package/design-system/components/InlineEdit/style.css +34 -0
  60. package/design-system/components/Input/README.md +20 -1
  61. package/design-system/components/Input/preview.html +24 -0
  62. package/design-system/components/Input/style.css +13 -1
  63. package/design-system/components/KanbanBoard/README.md +69 -5
  64. package/design-system/components/KanbanBoard/preview.html +129 -0
  65. package/design-system/components/KanbanBoard/style.css +32 -1
  66. package/design-system/components/ListDetail/preview.html +1 -1
  67. package/design-system/components/Matrix/README.md +1 -1
  68. package/design-system/components/Matrix/preview.html +1 -1
  69. package/design-system/components/Motion/README.md +2 -2
  70. package/design-system/components/Motion/preview.html +8 -8
  71. package/design-system/components/PlanTimeline/README.md +144 -0
  72. package/design-system/components/PlanTimeline/preview.html +109 -0
  73. package/design-system/components/PlanTimeline/style.css +64 -0
  74. package/design-system/components/README.md +12 -7
  75. package/design-system/components/ScoreHero/README.md +2 -2
  76. package/design-system/components/ScoreHero/style.css +1 -1
  77. package/design-system/components/ShareBar/README.md +1 -1
  78. package/design-system/components/ShareBar/style.css +1 -1
  79. package/design-system/components/Skeleton/README.md +82 -17
  80. package/design-system/components/Skeleton/preview.html +84 -2
  81. package/design-system/components/Skeleton/style.css +9 -14
  82. package/design-system/components/Starter/README.md +1 -1
  83. package/design-system/components/Starter/preview.html +1 -1
  84. package/design-system/components/Stepper/README.md +81 -5
  85. package/design-system/components/Stepper/preview.html +64 -1
  86. package/design-system/components/Stepper/style.css +29 -0
  87. package/design-system/components/StreakCalendar/README.md +1 -0
  88. package/design-system/components/Table/README.md +8 -4
  89. package/design-system/components/Table/preview.html +16 -1
  90. package/design-system/components/Table/style.css +3 -1
  91. package/design-system/components/TextDiff/README.md +96 -0
  92. package/design-system/components/TextDiff/preview.html +42 -0
  93. package/design-system/components/TextDiff/style.css +21 -0
  94. package/design-system/components/TickStrip/README.md +1 -1
  95. package/design-system/components/TickStrip/preview.html +1 -1
  96. package/design-system/components/TickStrip/style.css +1 -1
  97. package/design-system/components/Timeline/README.md +4 -2
  98. package/design-system/components/TrendLine/README.md +38 -7
  99. package/design-system/components/TrendLine/preview.html +140 -3
  100. package/design-system/components/TrendLine/style.css +12 -2
  101. package/design-system/components/Well/README.md +1 -1
  102. package/design-system/components/Well/style.css +2 -2
  103. package/design-system/components/_house.css +1 -1
  104. package/design-system/components/bundle.css +501 -30
  105. package/design-system/tokens.json +17 -7
  106. package/dist/fello-core.css +2 -2
  107. package/dist/fello.css +2 -2
  108. package/dist/fello.scoped.css +2 -2
  109. package/dist/index.d.ts +1 -1
  110. package/dist/scoped.js +2 -2
  111. package/dist/tokens.d.ts +16 -8
  112. package/dist/tokens.js +16 -8
  113. package/package.json +7 -2
  114. package/scripts/check-drift.mjs +325 -0
  115. package/scripts/token-model.mjs +2 -1
  116. package/tokens/_dark.css +3 -2
  117. package/tokens/_product.css +13 -11
  118. package/tokens/palettes.ts +1 -1
  119. package/tokens/shadcn-compat.css +1 -1
  120. package/tokens/shadcn-compat.hsl.css +1 -1
  121. package/tokens/tokens.css +23 -15
  122. package/tokens/tokens.md +56 -50
  123. package/tokens/tokens.root.css +23 -15
  124. package/tokens/tokens.ts +17 -9
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)";
@@ -195,8 +196,8 @@ declare const tokens: {
195
196
  readonly "--ws-rating-strong": "var(--color-stat-positive)";
196
197
  readonly "--ws-rating-middle": "var(--color-brand-mid-blue)";
197
198
  readonly "--ws-rating-needs-work": "var(--ws-chart-needs-work)";
198
- readonly "--ws-shimmer-ground": "color-mix(in srgb, var(--ws-tray) 96%, #d4700c)";
199
- readonly "--ws-shimmer": "linear-gradient(100deg in srgb, var(--ws-shimmer-ground) 0 34%, color-mix(in srgb, var(--ws-chart-under-half) 20%, rgb(243.75 247.75 247)) 44%, color-mix(in srgb, var(--ws-chart-past-half) 15%, rgb(249 244.8823529412 251.6470588235)) 50%, color-mix(in srgb, var(--ws-chart-finished) 15%, rgb(250.1764705882 252 251.6470588235)) 56%, var(--ws-shimmer-ground) 66% 100%)";
199
+ readonly "--ws-shimmer-ground": "var(--ws-ghost)";
200
+ readonly "--ws-shimmer": "linear-gradient(100deg in srgb, var(--ws-shimmer-ground) 0 34%, color-mix(in srgb, var(--ws-shimmer-ground) 30%, var(--color-bg-page)) 50%, var(--ws-shimmer-ground) 66% 100%)";
200
201
  readonly "--ws-card-tint-blue": "color-mix(in srgb, var(--ws-rating-middle) 10%, var(--color-surface))";
201
202
  readonly "--ws-card-tint-done": "color-mix(in srgb, var(--ws-chart-finished) 10%, var(--color-surface))";
202
203
  readonly "--ws-scrim": "color-mix(in srgb, var(--color-text-heading) 35%, 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)";
@@ -624,8 +627,8 @@ declare const themes: {
624
627
  readonly "--ws-rating-strong": "#098486";
625
628
  readonly "--ws-rating-middle": "#495883";
626
629
  readonly "--ws-rating-needs-work": "#f370a4";
627
- readonly "--ws-shimmer-ground": "#ece8e4";
628
- readonly "--ws-shimmer": "linear-gradient(100deg in srgb, #ece8e4 0 34%, #f6ddd8 44%, #e4dcf3 50%, #d6eaea 56%, #ece8e4 66% 100%)";
630
+ readonly "--ws-shimmer-ground": "#e3dfdb";
631
+ readonly "--ws-shimmer": "linear-gradient(100deg in srgb, #e3dfdb 0 34%, #f6f3f0 50%, #e3dfdb 66% 100%)";
629
632
  readonly "--ws-card-tint-blue": "#edeef3";
630
633
  readonly "--ws-card-tint-done": "#e6f3f3";
631
634
  readonly "--ws-scrim": "rgba(9, 53, 85, 0.35)";
@@ -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";
@@ -1048,8 +1053,8 @@ declare const themes: {
1048
1053
  readonly "--ws-rating-strong": "#3bb8b2";
1049
1054
  readonly "--ws-rating-middle": "#93a6dc";
1050
1055
  readonly "--ws-rating-needs-work": "#f370a4";
1051
- readonly "--ws-shimmer-ground": "#1a2631";
1052
- readonly "--ws-shimmer": "linear-gradient(100deg, #1a2631 0 34%, #3a3137 44%, #2c334b 50%, #1f3a43 56%, #1a2631 66% 100%)";
1056
+ readonly "--ws-shimmer-ground": "#273544";
1057
+ readonly "--ws-shimmer": "linear-gradient(100deg in srgb, #273544 0 34%, #354250 50%, #273544 66% 100%)";
1053
1058
  readonly "--ws-card-tint-blue": "#1b2637";
1054
1059
  readonly "--ws-card-tint-done": "rgba(17, 40, 47, 1)";
1055
1060
  readonly "--ws-scrim": "rgba(2, 6, 10, 0.62)";
@@ -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)";
@@ -1472,8 +1479,8 @@ declare const themes: {
1472
1479
  readonly "--ws-rating-strong": "#3bb8b2";
1473
1480
  readonly "--ws-rating-middle": "#93a6dc";
1474
1481
  readonly "--ws-rating-needs-work": "#f370a4";
1475
- readonly "--ws-shimmer-ground": "#20354b";
1476
- readonly "--ws-shimmer": "linear-gradient(100deg, #20354b 0 34%, #3f3e4d 44%, #314062 50%, #244759 56%, #20354b 66% 100%)";
1482
+ readonly "--ws-shimmer-ground": "#273544";
1483
+ readonly "--ws-shimmer": "linear-gradient(100deg in srgb, #273544 0 34%, #364351 50%, #273544 66% 100%)";
1477
1484
  readonly "--ws-card-tint-blue": "#142a46";
1478
1485
  readonly "--ws-card-tint-done": "#062d41";
1479
1486
  readonly "--ws-scrim": "rgba(0, 8, 15, 0.62)";
@@ -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)",
@@ -198,8 +199,8 @@ var tokens = {
198
199
  "--ws-rating-strong": "var(--color-stat-positive)",
199
200
  "--ws-rating-middle": "var(--color-brand-mid-blue)",
200
201
  "--ws-rating-needs-work": "var(--ws-chart-needs-work)",
201
- "--ws-shimmer-ground": "color-mix(in srgb, var(--ws-tray) 96%, #d4700c)",
202
- "--ws-shimmer": "linear-gradient(100deg in srgb, var(--ws-shimmer-ground) 0 34%, color-mix(in srgb, var(--ws-chart-under-half) 20%, rgb(243.75 247.75 247)) 44%, color-mix(in srgb, var(--ws-chart-past-half) 15%, rgb(249 244.8823529412 251.6470588235)) 50%, color-mix(in srgb, var(--ws-chart-finished) 15%, rgb(250.1764705882 252 251.6470588235)) 56%, var(--ws-shimmer-ground) 66% 100%)",
202
+ "--ws-shimmer-ground": "var(--ws-ghost)",
203
+ "--ws-shimmer": "linear-gradient(100deg in srgb, var(--ws-shimmer-ground) 0 34%, color-mix(in srgb, var(--ws-shimmer-ground) 30%, var(--color-bg-page)) 50%, var(--ws-shimmer-ground) 66% 100%)",
203
204
  "--ws-card-tint-blue": "color-mix(in srgb, var(--ws-rating-middle) 10%, var(--color-surface))",
204
205
  "--ws-card-tint-done": "color-mix(in srgb, var(--ws-chart-finished) 10%, var(--color-surface))",
205
206
  "--ws-scrim": "color-mix(in srgb, var(--color-text-heading) 35%, 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)",
@@ -624,8 +627,8 @@ var themes = {
624
627
  "--ws-rating-strong": "#098486",
625
628
  "--ws-rating-middle": "#495883",
626
629
  "--ws-rating-needs-work": "#f370a4",
627
- "--ws-shimmer-ground": "#ece8e4",
628
- "--ws-shimmer": "linear-gradient(100deg in srgb, #ece8e4 0 34%, #f6ddd8 44%, #e4dcf3 50%, #d6eaea 56%, #ece8e4 66% 100%)",
630
+ "--ws-shimmer-ground": "#e3dfdb",
631
+ "--ws-shimmer": "linear-gradient(100deg in srgb, #e3dfdb 0 34%, #f6f3f0 50%, #e3dfdb 66% 100%)",
629
632
  "--ws-card-tint-blue": "#edeef3",
630
633
  "--ws-card-tint-done": "#e6f3f3",
631
634
  "--ws-scrim": "rgba(9, 53, 85, 0.35)",
@@ -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",
@@ -1048,8 +1053,8 @@ var themes = {
1048
1053
  "--ws-rating-strong": "#3bb8b2",
1049
1054
  "--ws-rating-middle": "#93a6dc",
1050
1055
  "--ws-rating-needs-work": "#f370a4",
1051
- "--ws-shimmer-ground": "#1a2631",
1052
- "--ws-shimmer": "linear-gradient(100deg, #1a2631 0 34%, #3a3137 44%, #2c334b 50%, #1f3a43 56%, #1a2631 66% 100%)",
1056
+ "--ws-shimmer-ground": "#273544",
1057
+ "--ws-shimmer": "linear-gradient(100deg in srgb, #273544 0 34%, #354250 50%, #273544 66% 100%)",
1053
1058
  "--ws-card-tint-blue": "#1b2637",
1054
1059
  "--ws-card-tint-done": "rgba(17, 40, 47, 1)",
1055
1060
  "--ws-scrim": "rgba(2, 6, 10, 0.62)",
@@ -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)",
@@ -1472,8 +1479,8 @@ var themes = {
1472
1479
  "--ws-rating-strong": "#3bb8b2",
1473
1480
  "--ws-rating-middle": "#93a6dc",
1474
1481
  "--ws-rating-needs-work": "#f370a4",
1475
- "--ws-shimmer-ground": "#20354b",
1476
- "--ws-shimmer": "linear-gradient(100deg, #20354b 0 34%, #3f3e4d 44%, #314062 50%, #244759 56%, #20354b 66% 100%)",
1482
+ "--ws-shimmer-ground": "#273544",
1483
+ "--ws-shimmer": "linear-gradient(100deg in srgb, #273544 0 34%, #364351 50%, #273544 66% 100%)",
1477
1484
  "--ws-card-tint-blue": "#142a46",
1478
1485
  "--ws-card-tint-done": "#062d41",
1479
1486
  "--ws-scrim": "rgba(0, 8, 15, 0.62)",
@@ -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.0",
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 */
@@ -109,7 +110,6 @@
109
110
  --ws-chart-alert: #FF5470; /* was #FF5A5A (gtm --color-alert dark), CIEDE2000 6.1 from salmon: a chart could not tell the two apart. A step toward crimson, 12.3 from salmon and 11.9 from the pink; words 4.63 on press (D15) */
110
111
  --ws-word-needs-work: var(--ws-chart-needs-work); /* derived: Seth's pink itself clears AA on every night ground (5.26 on press) */
111
112
  --ws-tint-needs-work: color-mix(in srgb, var(--ws-chart-needs-work) 18%, var(--color-bg-page)); /* derived, the tint-needs formula */
112
- --ws-shimmer: linear-gradient(100deg, var(--ws-track) 0 34%, color-mix(in srgb, var(--ws-chart-under-half) 14%, var(--ws-track)) 44%, color-mix(in srgb, var(--ws-chart-past-half) 14%, var(--ws-track)) 50%, color-mix(in srgb, var(--color-stat-positive) 14%, var(--ws-track)) 56%, var(--ws-track) 66% 100%); /* derived: the chart colours at 14% over the dark track; still frames plain, as in light */
113
113
  /* Promoted at integration (0.5.0): the packages' scoped dark values, now global. */
114
114
  --ws-rating-middle: var(--color-text-interactive); /* the mid blue was 2.5:1 on night, under 3:1 for a mark; its words' own dark step */
115
115
  --ws-raised-sunk: var(--ws-surface-press); /* the page's sunk is 1/255 off raised in dark */
@@ -119,7 +119,7 @@
119
119
  --ws-agent-tally: var(--color-text-heading); /* the act itself in dark (WP-controls) */
120
120
  --ws-mark-muted: color-mix(in srgb, var(--color-text-muted) 79.8%, var(--color-bg-page)); /* derived: the muted words toward the page, 3.3:1 on the tray, 4.0 on night, a mark (was #6B7A8A) */
121
121
  --ws-control-off: color-mix(in srgb, var(--color-text-muted) 50%, #7a6a59); /* #7F7F7F, the off track as in light */
122
- --ws-shimmer-ground: var(--ws-track);
122
+ --ws-shimmer: linear-gradient(100deg in srgb, var(--ws-shimmer-ground) 0 34%, color-mix(in srgb, var(--ws-shimmer-ground) 93%, var(--ws-ink)) 50%, var(--ws-shimmer-ground) 66% 100%); /* derived (0.5.1): the ghost (the night's #273544, 1.42:1 on the page, 1.15:1 on the tray) with a lighter pass of 7% ink, #354250; on the navy ground #364351 */
123
123
  --ws-inverse: var(--color-bg-dark); /* a brand pair in dark: the navy message keeps the navy and white (D12) */
124
124
  --ws-on-inverse: var(--color-white);
125
125
  --ws-chart-rest: var(--ws-edge-card);
@@ -144,6 +144,7 @@
144
144
  --ws-tint-accent: color-mix(in srgb, var(--color-bg-page) 94%, var(--color-white));
145
145
  --ws-tint-needs: color-mix(in srgb, var(--ws-chart-under-half) 18%, var(--color-bg-page));
146
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 */
147
148
  --ws-card-tint-blue: color-mix(in srgb, var(--color-brand-mid-blue) 25%, var(--color-bg-page));
148
149
  --ws-card-tint-done: var(--ws-tint-live);
149
150
  --ws-track: color-mix(in srgb, var(--color-bg-page) 88%, var(--color-white));