@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.
- package/AGENTS.md +29 -9
- package/CHANGELOG.md +86 -0
- package/README.md +10 -4
- package/design-system/01-how-claude-decides.md +5 -3
- package/design-system/03-colour.md +19 -1
- package/design-system/05-space-and-layout.md +2 -1
- package/design-system/08-choosing-components.md +20 -4
- package/design-system/09-charts.md +6 -3
- package/design-system/11-page-recipes.md +40 -1
- package/design-system/13-motion.md +1 -1
- package/design-system/15-ai-experiences.md +11 -0
- package/design-system/17-decisions.md +41 -0
- package/design-system/components/AppShell/README.md +89 -21
- package/design-system/components/AppShell/preview.html +148 -3
- package/design-system/components/AppShell/style.css +47 -0
- package/design-system/components/Avatar/README.md +6 -3
- package/design-system/components/Avatar/preview.html +2 -1
- package/design-system/components/Avatar/style.css +3 -0
- package/design-system/components/Banner/README.md +9 -5
- package/design-system/components/Banner/preview.html +9 -0
- package/design-system/components/Banner/style.css +3 -0
- package/design-system/components/Button/README.md +29 -2
- package/design-system/components/Button/preview.html +33 -1
- package/design-system/components/Button/style.css +21 -0
- package/design-system/components/Calendar/README.md +148 -0
- package/design-system/components/Calendar/preview.html +449 -0
- package/design-system/components/Calendar/style.css +61 -0
- package/design-system/components/Card/README.md +13 -0
- package/design-system/components/Card/preview.html +23 -0
- package/design-system/components/Card/style.css +19 -0
- package/design-system/components/ChatCard/README.md +74 -3
- package/design-system/components/ChatCard/preview.html +126 -25
- package/design-system/components/ChatCard/style.css +6 -0
- package/design-system/components/Chip/README.md +16 -7
- package/design-system/components/Chip/preview.html +25 -1
- package/design-system/components/Chip/style.css +5 -0
- package/design-system/components/Combobox/README.md +10 -4
- package/design-system/components/Combobox/preview.html +19 -4
- package/design-system/components/Combobox/style.css +7 -1
- package/design-system/components/CommentThread/README.md +138 -0
- package/design-system/components/CommentThread/preview.html +134 -0
- package/design-system/components/CommentThread/style.css +41 -0
- package/design-system/components/DateInput/README.md +38 -4
- package/design-system/components/DateInput/preview.html +50 -2
- package/design-system/components/DateInput/style.css +6 -0
- package/design-system/components/Dialog/README.md +26 -3
- package/design-system/components/Dialog/preview.html +80 -0
- package/design-system/components/Dialog/style.css +28 -3
- package/design-system/components/EmptyState/README.md +1 -1
- package/design-system/components/Fold/README.md +21 -0
- package/design-system/components/Fold/preview.html +44 -1
- package/design-system/components/Fold/style.css +30 -0
- package/design-system/components/InlineEdit/README.md +116 -0
- package/design-system/components/InlineEdit/preview.html +135 -0
- package/design-system/components/InlineEdit/style.css +34 -0
- package/design-system/components/Input/README.md +20 -1
- package/design-system/components/Input/preview.html +24 -0
- package/design-system/components/Input/style.css +13 -1
- package/design-system/components/KanbanBoard/README.md +68 -4
- package/design-system/components/KanbanBoard/preview.html +129 -0
- package/design-system/components/KanbanBoard/style.css +31 -0
- package/design-system/components/PlanTimeline/README.md +144 -0
- package/design-system/components/PlanTimeline/preview.html +109 -0
- package/design-system/components/PlanTimeline/style.css +64 -0
- package/design-system/components/README.md +11 -6
- package/design-system/components/Skeleton/README.md +66 -2
- package/design-system/components/Skeleton/preview.html +83 -1
- package/design-system/components/Starter/README.md +1 -1
- package/design-system/components/Starter/preview.html +1 -1
- package/design-system/components/Stepper/README.md +81 -5
- package/design-system/components/Stepper/preview.html +64 -1
- package/design-system/components/Stepper/style.css +29 -0
- package/design-system/components/StreakCalendar/README.md +1 -0
- package/design-system/components/Table/README.md +7 -3
- package/design-system/components/Table/preview.html +15 -0
- package/design-system/components/Table/style.css +2 -0
- package/design-system/components/TextDiff/README.md +96 -0
- package/design-system/components/TextDiff/preview.html +42 -0
- package/design-system/components/TextDiff/style.css +21 -0
- package/design-system/components/Timeline/README.md +3 -1
- package/design-system/components/TrendLine/README.md +38 -7
- package/design-system/components/TrendLine/preview.html +140 -3
- package/design-system/components/TrendLine/style.css +12 -2
- package/design-system/components/bundle.css +483 -7
- package/design-system/tokens.json +15 -1
- package/dist/fello-core.css +2 -2
- package/dist/fello.css +2 -2
- package/dist/fello.scoped.css +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/scoped.js +2 -2
- package/dist/tokens.d.ts +8 -0
- package/dist/tokens.js +8 -0
- package/package.json +7 -2
- package/scripts/check-drift.mjs +325 -0
- package/scripts/token-model.mjs +2 -1
- package/tokens/_dark.css +2 -0
- package/tokens/_product.css +2 -0
- package/tokens/palettes.ts +1 -1
- package/tokens/shadcn-compat.css +1 -1
- package/tokens/shadcn-compat.hsl.css +1 -1
- package/tokens/tokens.css +14 -1
- package/tokens/tokens.md +53 -46
- package/tokens/tokens.root.css +14 -1
- 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.
|
|
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
|
+
}
|
package/scripts/token-model.mjs
CHANGED
|
@@ -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));
|
package/tokens/_product.css
CHANGED
|
@@ -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
|
package/tokens/palettes.ts
CHANGED
|
@@ -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
|
-
|
|
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 = [
|
package/tokens/shadcn-compat.css
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|