@felloinnovations/brand-kit 0.4.0 → 0.5.0
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 +198 -24
- package/CHANGELOG.md +197 -0
- package/README.md +34 -7
- package/design-system/01-how-claude-decides.md +26 -17
- package/design-system/02-principles.md +11 -6
- package/design-system/03-colour.md +134 -47
- package/design-system/04-type.md +45 -23
- package/design-system/05-space-and-layout.md +175 -52
- package/design-system/06-sizing-profiles.md +4 -4
- package/design-system/07-scrolling.md +66 -20
- package/design-system/08-choosing-components.md +50 -22
- package/design-system/09-charts.md +71 -28
- package/design-system/10-patterns.md +33 -10
- package/design-system/11-page-recipes.md +110 -23
- package/design-system/12-content.md +2 -1
- package/design-system/13-motion.md +330 -47
- package/design-system/14-accessibility.md +47 -24
- package/design-system/15-ai-experiences.md +11 -6
- package/design-system/16-creativity.md +1 -1
- package/design-system/17-decisions.md +357 -1
- package/design-system/18-checklist.md +79 -16
- package/design-system/19-public-pages.md +6 -4
- package/design-system/20-palettes.md +275 -0
- package/design-system/README.md +30 -17
- package/design-system/components/ActionList/README.md +96 -10
- package/design-system/components/ActionList/preview.html +52 -7
- package/design-system/components/ActionList/style.css +64 -0
- package/design-system/components/ActivityGrid/README.md +38 -2
- package/design-system/components/ActivityGrid/preview.html +1 -1
- package/design-system/components/ActivityGrid/style.css +35 -0
- package/design-system/components/AppShell/README.md +205 -75
- package/design-system/components/AppShell/preview.html +339 -58
- package/design-system/components/AppShell/style.css +154 -0
- package/design-system/components/Avatar/README.md +37 -10
- package/design-system/components/Avatar/preview.html +13 -4
- package/design-system/components/Avatar/style.css +14 -0
- package/design-system/components/Badge/README.md +41 -19
- package/design-system/components/Badge/preview.html +13 -10
- package/design-system/components/Badge/style.css +23 -0
- package/design-system/components/Banner/README.md +30 -9
- package/design-system/components/Banner/preview.html +1 -1
- package/design-system/components/Banner/style.css +19 -0
- package/design-system/components/Breadcrumb/README.md +58 -46
- package/design-system/components/Breadcrumb/preview.html +16 -16
- package/design-system/components/Breadcrumb/style.css +17 -0
- package/design-system/components/Button/README.md +113 -55
- package/design-system/components/Button/preview.html +98 -25
- package/design-system/components/Button/style.css +86 -0
- package/design-system/components/Card/README.md +158 -49
- package/design-system/components/Card/preview.html +141 -19
- package/design-system/components/Card/style.css +115 -0
- package/design-system/components/CardBoard/README.md +58 -37
- package/design-system/components/CardBoard/preview.html +26 -29
- package/design-system/components/ChatCard/README.md +115 -29
- package/design-system/components/ChatCard/preview.html +149 -32
- package/design-system/components/ChatCard/style.css +49 -0
- package/design-system/components/ChatShell/README.md +116 -0
- package/design-system/components/ChatShell/preview.html +201 -0
- package/design-system/components/ChatShell/style.css +86 -0
- package/design-system/components/ChatTurn/README.md +78 -35
- package/design-system/components/ChatTurn/preview.html +60 -28
- package/design-system/components/ChatTurn/style.css +50 -0
- package/design-system/components/CheckRow/README.md +141 -0
- package/design-system/components/CheckRow/preview.html +140 -0
- package/design-system/components/CheckRow/style.css +88 -0
- package/design-system/components/Checkbox/README.md +58 -22
- package/design-system/components/Checkbox/preview.html +112 -50
- package/design-system/components/Checkbox/style.css +45 -0
- package/design-system/components/Chip/README.md +78 -47
- package/design-system/components/Chip/preview.html +29 -4
- package/design-system/components/Chip/style.css +17 -0
- package/design-system/components/Combobox/README.md +44 -11
- package/design-system/components/Combobox/preview.html +35 -12
- package/design-system/components/Combobox/style.css +54 -0
- package/design-system/components/Composer/README.md +137 -35
- package/design-system/components/Composer/preview.html +115 -40
- package/design-system/components/Composer/style.css +81 -0
- package/design-system/components/ConfirmDialog/README.md +73 -52
- package/design-system/components/ConfirmDialog/preview.html +70 -26
- package/design-system/components/ConfirmDialog/style.css +5 -0
- package/design-system/components/Cover/preview.html +3 -3
- package/design-system/components/Cover/style.css +19 -0
- package/design-system/components/DateInput/README.md +77 -12
- package/design-system/components/DateInput/preview.html +27 -12
- package/design-system/components/DateInput/style.css +38 -0
- package/design-system/components/Dialog/README.md +112 -44
- package/design-system/components/Dialog/preview.html +141 -18
- package/design-system/components/Dialog/style.css +74 -0
- package/design-system/components/EmptyState/README.md +42 -12
- package/design-system/components/EmptyState/preview.html +11 -11
- package/design-system/components/EmptyState/style.css +9 -0
- package/design-system/components/EntryShell/README.md +73 -48
- package/design-system/components/EntryShell/preview.html +60 -27
- package/design-system/components/EntryShell/style.css +19 -0
- package/design-system/components/FeatureGrid/README.md +61 -39
- package/design-system/components/FeatureGrid/preview.html +2 -2
- package/design-system/components/FeatureGrid/style.css +20 -0
- package/design-system/components/FileUpload/README.md +33 -3
- package/design-system/components/FileUpload/preview.html +18 -4
- package/design-system/components/FileUpload/style.css +23 -0
- package/design-system/components/FilterBar/README.md +114 -0
- package/design-system/components/FilterBar/preview.html +82 -0
- package/design-system/components/FilterBar/style.css +22 -0
- package/design-system/components/Fold/README.md +84 -26
- package/design-system/components/Fold/preview.html +77 -34
- package/design-system/components/Fold/style.css +48 -0
- package/design-system/components/Footer/README.md +62 -39
- package/design-system/components/Footer/preview.html +5 -5
- package/design-system/components/Footer/style.css +30 -0
- package/design-system/components/FunnelBars/README.md +24 -3
- package/design-system/components/FunnelBars/preview.html +5 -5
- package/design-system/components/FunnelBars/style.css +13 -0
- package/design-system/components/GoalBars/README.md +54 -10
- package/design-system/components/GoalBars/preview.html +69 -28
- package/design-system/components/GoalBars/style.css +51 -0
- package/design-system/components/GoalTicks/README.md +74 -58
- package/design-system/components/GoalTicks/preview.html +41 -118
- package/design-system/components/GoalTicks/style.css +37 -0
- package/design-system/components/GoalTubes/README.md +29 -3
- package/design-system/components/GoalTubes/preview.html +6 -6
- package/design-system/components/GoalTubes/style.css +23 -0
- package/design-system/components/Heatmap/README.md +133 -0
- package/design-system/components/Heatmap/preview.html +168 -0
- package/design-system/components/Heatmap/style.css +119 -0
- package/design-system/components/Hero/README.md +58 -40
- package/design-system/components/Hero/preview.html +1 -1
- package/design-system/components/Hero/style.css +19 -0
- package/design-system/components/HoverCard/README.md +114 -0
- package/design-system/components/HoverCard/preview.html +107 -0
- package/design-system/components/HoverCard/style.css +59 -0
- package/design-system/components/IconButton/README.md +69 -38
- package/design-system/components/IconButton/preview.html +77 -43
- package/design-system/components/IconButton/style.css +45 -0
- package/design-system/components/Input/README.md +36 -11
- package/design-system/components/Input/preview.html +1 -1
- package/design-system/components/Input/style.css +23 -0
- package/design-system/components/KanbanBoard/README.md +93 -45
- package/design-system/components/KanbanBoard/preview.html +159 -43
- package/design-system/components/KanbanBoard/style.css +53 -0
- package/design-system/components/Kbd/README.md +25 -9
- package/design-system/components/Kbd/preview.html +6 -2
- package/design-system/components/Kbd/style.css +9 -0
- package/design-system/components/KitButton/README.md +81 -47
- package/design-system/components/KitButton/preview.html +35 -13
- package/design-system/components/KitButton/style.css +46 -0
- package/design-system/components/KitCard/README.md +75 -44
- package/design-system/components/KitCard/preview.html +18 -10
- package/design-system/components/KitCard/style.css +15 -0
- package/design-system/components/KitInput/README.md +54 -39
- package/design-system/components/KitInput/preview.html +7 -7
- package/design-system/components/KitInput/style.css +23 -0
- package/design-system/components/Leaderboard/README.md +76 -49
- package/design-system/components/Leaderboard/preview.html +61 -26
- package/design-system/components/Leaderboard/style.css +44 -0
- package/design-system/components/ListDetail/README.md +222 -0
- package/design-system/components/ListDetail/preview.html +213 -0
- package/design-system/components/ListDetail/style.css +148 -0
- package/design-system/components/LoadBar/README.md +79 -0
- package/design-system/components/LoadBar/preview.html +40 -0
- package/design-system/components/LoadBar/style.css +17 -0
- package/design-system/components/MarketingNav/README.md +76 -42
- package/design-system/components/MarketingNav/preview.html +37 -8
- package/design-system/components/MarketingNav/style.css +45 -0
- package/design-system/components/Matrix/README.md +129 -0
- package/design-system/components/Matrix/preview.html +99 -0
- package/design-system/components/Matrix/style.css +84 -0
- package/design-system/components/Menu/README.md +117 -41
- package/design-system/components/Menu/preview.html +85 -23
- package/design-system/components/Menu/style.css +61 -0
- package/design-system/components/Meter/README.md +39 -4
- package/design-system/components/Meter/preview.html +18 -1
- package/design-system/components/Meter/style.css +23 -0
- package/design-system/components/MetricTile/README.md +237 -0
- package/design-system/components/MetricTile/preview.html +241 -0
- package/design-system/components/MetricTile/style.css +129 -0
- package/design-system/components/Morph/README.md +106 -0
- package/design-system/components/Morph/preview.html +103 -0
- package/design-system/components/Morph/style.css +31 -0
- package/design-system/components/Motion/README.md +164 -0
- package/design-system/components/Motion/preview.html +2537 -0
- package/design-system/components/NumberInput/README.md +27 -8
- package/design-system/components/NumberInput/preview.html +21 -5
- package/design-system/components/NumberInput/style.css +34 -0
- package/design-system/components/PaceBar/README.md +75 -0
- package/design-system/components/PaceBar/preview.html +67 -0
- package/design-system/components/PaceBar/style.css +25 -0
- package/design-system/components/Page/README.md +57 -18
- package/design-system/components/Page/preview.html +15 -17
- package/design-system/components/Page/style.css +7 -0
- package/design-system/components/PageBar/README.md +90 -34
- package/design-system/components/PageBar/preview.html +60 -19
- package/design-system/components/PageBar/style.css +35 -0
- package/design-system/components/PageHero/README.md +87 -51
- package/design-system/components/PageHero/preview.html +35 -6
- package/design-system/components/PageHero/style.css +31 -0
- package/design-system/components/Pagination/README.md +70 -49
- package/design-system/components/Pagination/preview.html +24 -15
- package/design-system/components/Pagination/style.css +12 -0
- package/design-system/components/Panel/README.md +109 -51
- package/design-system/components/Panel/preview.html +118 -20
- package/design-system/components/Panel/style.css +51 -0
- package/design-system/components/PasswordInput/README.md +50 -18
- package/design-system/components/PasswordInput/preview.html +71 -21
- package/design-system/components/PasswordInput/style.css +31 -0
- package/design-system/components/Plate/README.md +75 -17
- package/design-system/components/Plate/preview.html +40 -7
- package/design-system/components/Plate/style.css +37 -0
- package/design-system/components/Popover/README.md +70 -49
- package/design-system/components/Popover/preview.html +44 -22
- package/design-system/components/Popover/style.css +41 -0
- package/design-system/components/PricingTable/README.md +69 -49
- package/design-system/components/PricingTable/preview.html +21 -21
- package/design-system/components/PricingTable/style.css +21 -0
- package/design-system/components/ProgressPetals/README.md +31 -3
- package/design-system/components/ProgressPetals/preview.html +14 -15
- package/design-system/components/ProgressPetals/style.css +27 -0
- package/design-system/components/README.md +151 -0
- package/design-system/components/Radio/README.md +40 -17
- package/design-system/components/Radio/preview.html +16 -1
- package/design-system/components/Radio/style.css +21 -0
- package/design-system/components/RankBar/README.md +29 -2
- package/design-system/components/RankBar/preview.html +14 -12
- package/design-system/components/RankBar/style.css +18 -0
- package/design-system/components/Ring/README.md +61 -11
- package/design-system/components/Ring/preview.html +55 -10
- package/design-system/components/Ring/style.css +49 -0
- package/design-system/components/ScoreHero/README.md +91 -0
- package/design-system/components/ScoreHero/preview.html +104 -0
- package/design-system/components/ScoreHero/style.css +32 -0
- package/design-system/components/Section/README.md +72 -33
- package/design-system/components/Section/preview.html +46 -16
- package/design-system/components/Section/style.css +18 -0
- package/design-system/components/Segmented/README.md +100 -55
- package/design-system/components/Segmented/preview.html +107 -26
- package/design-system/components/Segmented/style.css +94 -0
- package/design-system/components/SegmentedProgress/README.md +64 -0
- package/design-system/components/SegmentedProgress/preview.html +69 -0
- package/design-system/components/SegmentedProgress/style.css +11 -0
- package/design-system/components/Select/README.md +47 -12
- package/design-system/components/Select/preview.html +28 -2
- package/design-system/components/Select/style.css +22 -0
- package/design-system/components/ShareBar/README.md +69 -0
- package/design-system/components/ShareBar/preview.html +72 -0
- package/design-system/components/ShareBar/style.css +25 -0
- package/design-system/components/SideList/README.md +147 -0
- package/design-system/components/SideList/preview.html +139 -0
- package/design-system/components/SideList/style.css +94 -0
- package/design-system/components/Skeleton/README.md +52 -16
- package/design-system/components/Skeleton/preview.html +35 -23
- package/design-system/components/Skeleton/style.css +22 -0
- package/design-system/components/Slider/README.md +95 -0
- package/design-system/components/Slider/preview.html +49 -0
- package/design-system/components/Slider/style.css +13 -0
- package/design-system/components/Split/README.md +112 -0
- package/design-system/components/Split/preview.html +105 -0
- package/design-system/components/Split/style.css +30 -0
- package/design-system/components/Starter/README.md +21 -9
- package/design-system/components/Starter/preview.html +5 -5
- package/design-system/components/Stat/README.md +64 -43
- package/design-system/components/Stat/preview.html +3 -3
- package/design-system/components/Stat/style.css +15 -0
- package/design-system/components/StatTiles/README.md +88 -44
- package/design-system/components/StatTiles/preview.html +142 -71
- package/design-system/components/StatTiles/style.css +64 -0
- package/design-system/components/StatusLine/README.md +85 -0
- package/design-system/components/StatusLine/preview.html +49 -0
- package/design-system/components/StatusLine/style.css +14 -0
- package/design-system/components/StatusScreen/README.md +49 -35
- package/design-system/components/StatusScreen/preview.html +20 -5
- package/design-system/components/StatusScreen/style.css +9 -0
- package/design-system/components/StepLine/README.md +97 -0
- package/design-system/components/StepLine/preview.html +108 -0
- package/design-system/components/StepLine/style.css +62 -0
- package/design-system/components/Stepper/README.md +35 -13
- package/design-system/components/Stepper/style.css +13 -0
- package/design-system/components/StreakCalendar/README.md +22 -1
- package/design-system/components/StreakCalendar/preview.html +10 -10
- package/design-system/components/StreakCalendar/style.css +12 -0
- package/design-system/components/SuggestionChip/README.md +87 -0
- package/design-system/components/SuggestionChip/preview.html +42 -0
- package/design-system/components/SuggestionChip/style.css +20 -0
- package/design-system/components/Switch/README.md +44 -24
- package/design-system/components/Switch/preview.html +23 -8
- package/design-system/components/Switch/style.css +31 -0
- package/design-system/components/Table/README.md +129 -58
- package/design-system/components/Table/preview.html +106 -72
- package/design-system/components/Table/style.css +104 -0
- package/design-system/components/Tabs/README.md +71 -46
- package/design-system/components/Tabs/preview.html +56 -24
- package/design-system/components/Tabs/style.css +44 -0
- package/design-system/components/Testimonial/README.md +48 -32
- package/design-system/components/Testimonial/preview.html +14 -14
- package/design-system/components/Testimonial/style.css +12 -0
- package/design-system/components/TextArea/README.md +27 -11
- package/design-system/components/TextArea/preview.html +8 -2
- package/design-system/components/TextArea/style.css +12 -0
- package/design-system/components/ThemeSwitch/README.md +116 -0
- package/design-system/components/ThemeSwitch/preview.html +57 -0
- package/design-system/components/ThemeSwitch/style.css +6 -0
- package/design-system/components/TickRow/README.md +52 -36
- package/design-system/components/TickRow/preview.html +64 -41
- package/design-system/components/TickRow/style.css +23 -0
- package/design-system/components/TickStrip/README.md +95 -0
- package/design-system/components/TickStrip/preview.html +88 -0
- package/design-system/components/TickStrip/style.css +42 -0
- package/design-system/components/Tile/README.md +79 -18
- package/design-system/components/Tile/preview.html +91 -40
- package/design-system/components/Tile/style.css +36 -0
- package/design-system/components/Timeline/README.md +68 -48
- package/design-system/components/Timeline/preview.html +27 -15
- package/design-system/components/Timeline/style.css +25 -0
- package/design-system/components/Toast/README.md +70 -38
- package/design-system/components/Toast/preview.html +43 -8
- package/design-system/components/Toast/style.css +25 -0
- package/design-system/components/Tooltip/README.md +93 -35
- package/design-system/components/Tooltip/preview.html +69 -15
- package/design-system/components/Tooltip/style.css +78 -0
- package/design-system/components/TopBar/README.md +67 -53
- package/design-system/components/TopBar/preview.html +14 -27
- package/design-system/components/TopBar/style.css +35 -0
- package/design-system/components/TopicRail/README.md +108 -0
- package/design-system/components/TopicRail/preview.html +58 -0
- package/design-system/components/TopicRail/style.css +48 -0
- package/design-system/components/TrendBars/README.md +52 -14
- package/design-system/components/TrendBars/preview.html +65 -32
- package/design-system/components/TrendBars/style.css +25 -0
- package/design-system/components/TrendLine/README.md +164 -0
- package/design-system/components/TrendLine/preview.html +373 -0
- package/design-system/components/TrendLine/style.css +155 -0
- package/design-system/components/Well/README.md +64 -22
- package/design-system/components/Well/preview.html +14 -14
- package/design-system/components/Well/style.css +22 -0
- package/design-system/components/_house.css +324 -0
- package/design-system/components/bundle.css +4091 -744
- package/design-system/tokens.json +1314 -124
- package/dist/fello-core.css +2 -2
- package/dist/fello.css +6 -1369
- package/dist/fello.scoped.css +7 -0
- package/dist/index.d.ts +110 -11
- package/dist/index.js +781 -96
- package/dist/palettes.d.ts +464 -0
- package/dist/palettes.js +497 -0
- package/dist/scoped.d.ts +3 -0
- package/dist/scoped.js +16 -0
- package/dist/toast.d.ts +14 -0
- package/dist/toast.js +33 -29
- package/dist/tokens.d.ts +1686 -62
- package/dist/tokens.js +1668 -61
- package/package.json +20 -4
- package/scripts/check-palette.mjs +295 -0
- package/scripts/token-model.mjs +552 -0
- package/tokens/_dark.css +174 -0
- package/tokens/_palettes.css +46 -0
- package/tokens/_product.css +266 -53
- package/tokens/_source.hubl.css +39 -19
- package/tokens/design-tokens.json +319 -0
- package/tokens/palettes.ts +504 -0
- package/tokens/shadcn-compat.css +19 -19
- package/tokens/shadcn-compat.hsl.css +76 -0
- package/tokens/tokens.css +1292 -62
- package/tokens/tokens.md +588 -61
- package/tokens/tokens.root.css +1298 -62
- package/tokens/tokens.ts +1677 -64
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The kit's tokens as data, shared by scripts/generate-tokens.mjs (which writes every token file) and
|
|
3
|
+
* scripts/check-palette.mjs (which checks a palette against the contract). No side effects: it reads the sources.
|
|
4
|
+
*
|
|
5
|
+
* tokens/_source.hubl.css the kit (HubL interpolation resolved from design-tokens.json)
|
|
6
|
+
* tokens/_product.css the product layer (ws-*)
|
|
7
|
+
* tokens/_dark.css the dark theme: night, and the navy ground on top of it
|
|
8
|
+
* tokens/_palettes.css the built-in palettes, in the same form an app writes its own
|
|
9
|
+
* tokens/design-tokens.json metadata, and the palette contract (group "palette")
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
14
|
+
|
|
15
|
+
export const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
16
|
+
export const read = (p) => readFileSync(join(root, p), 'utf8');
|
|
17
|
+
|
|
18
|
+
export const meta = JSON.parse(read('tokens/design-tokens.json'));
|
|
19
|
+
|
|
20
|
+
/* The three marketer-tunable brand colours are HubL expressions in the source (`{{ theme.brand.salmon.color }}`):
|
|
21
|
+
outside HubSpot they resolve to the Figma default the JSON records against the same themePath. */
|
|
22
|
+
const byThemePath = Object.fromEntries(meta.colors.brand.filter((t) => t.themePath).map((t) => [t.themePath, t.value]));
|
|
23
|
+
|
|
24
|
+
/* Tokens the shared kit owns. Everything else in the source is website page furniture, felix-theme's. */
|
|
25
|
+
const KEEP = [
|
|
26
|
+
/^--color-/, /^--gradient-/, /^--font-body$/, /^--font-size-/, /^--letter-spacing-overline$/,
|
|
27
|
+
/^--space-(3xs|xxs|xxs-plus|xs|xs-plus|sm|sm-plus|md|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl)$/,
|
|
28
|
+
/^--radius-/, /^--z-/, /^--shadow-/, /^--transition-/, /^--layout-(canvas-max|content-max|margin|col-gap)$/,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const stripComments = (css) => css.replace(/\{#[\s\S]*?#\}/g, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
32
|
+
|
|
33
|
+
/** The declarations of one block, in order. Values are multi-line (shadow stacks, gradients), so not line-based. */
|
|
34
|
+
export function decls(block, keep, where) {
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const m of block.matchAll(/(--[a-z0-9-]+)\s*:\s*([^;]+);/gi)) {
|
|
37
|
+
const name = m[1];
|
|
38
|
+
if (keep && !keep.some((re) => re.test(name))) continue;
|
|
39
|
+
const value = m[2]
|
|
40
|
+
.replace(/\{\{\s*([a-z0-9_.]+)\s*\}\}/gi, (raw, path) => {
|
|
41
|
+
const hit = byThemePath[path];
|
|
42
|
+
if (!hit) throw new Error(`No JSON value for HubL path ${path} (token ${name}, ${where})`);
|
|
43
|
+
return hit;
|
|
44
|
+
})
|
|
45
|
+
.replace(/\s+/g, ' ')
|
|
46
|
+
.trim();
|
|
47
|
+
out.push([name, value]);
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseRoot(css, keep = KEEP) {
|
|
53
|
+
const body = stripComments(css);
|
|
54
|
+
// Only the top-level :root block. The trailing @media override is website section rhythm, not shipped.
|
|
55
|
+
const media = body.indexOf('@media');
|
|
56
|
+
return decls(body.slice(body.indexOf(':root'), media === -1 ? undefined : media), keep, ':root');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The declarations of the block whose selector is exactly `selector`. */
|
|
60
|
+
function parseBlock(css, selector, file) {
|
|
61
|
+
const body = stripComments(css);
|
|
62
|
+
const at = body.indexOf(`${selector} {`);
|
|
63
|
+
if (at === -1) throw new Error(`${file} has no "${selector} {" block`);
|
|
64
|
+
return decls(body.slice(at + selector.length + 2, body.indexOf('}', at)), null, selector);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* ---- Springs as CSS (D7): Seth's presets (motion.springs) sampled into linear() curves ----
|
|
68
|
+
The motion library's own spring (North app.js, KN and N5): from {visualDuration, bounce} a damping ratio of
|
|
69
|
+
1 - bounce (at least .05) and an undamped frequency of 2 pi / (visualDuration x 1.2); it runs from 0 to 1 until it
|
|
70
|
+
is within .005 of 1 and slower than .01 a second, checked every 50 ms (El), which is the CSS duration. The curve is
|
|
71
|
+
that spring sampled evenly, as few points as keep it within SPRING_FIT of the solver everywhere (overshoot kept).
|
|
72
|
+
generate-tokens re-reads each emitted curve and stops when it strays past SPRING_CHECK. */
|
|
73
|
+
export const SPRING_FIT = 0.005;
|
|
74
|
+
export const SPRING_CHECK = 0.01;
|
|
75
|
+
/** The spring's value at t ms (0 to 1, past 1 on an overshoot) and the ms it takes to settle. */
|
|
76
|
+
export function springSolver({ visualDuration, bounce = 0 }) {
|
|
77
|
+
const zeta = bounce < 0 ? 1 / Math.max(1 + bounce, 0.05) : Math.max(1 - bounce, 0.05);
|
|
78
|
+
const w = (2 * Math.PI) / (visualDuration * 1.2) / 1000; // rad per ms
|
|
79
|
+
let value, speed; // speed in units per ms
|
|
80
|
+
if (zeta < 1) {
|
|
81
|
+
const wd = w * Math.sqrt(1 - zeta * zeta), m = zeta * w, A = m / wd;
|
|
82
|
+
value = (t) => 1 - Math.exp(-m * t) * (A * Math.sin(wd * t) + Math.cos(wd * t));
|
|
83
|
+
speed = (t) => Math.exp(-m * t) * ((m * A + wd) * Math.sin(wd * t) + (m - A * wd) * Math.cos(wd * t));
|
|
84
|
+
} else {
|
|
85
|
+
value = (t) => 1 - Math.exp(-w * t) * (1 + w * t);
|
|
86
|
+
speed = (t) => Math.exp(-w * t) * w * w * t;
|
|
87
|
+
}
|
|
88
|
+
const settled = (t) => Math.abs(speed(t) * 1000) <= 0.01 && Math.abs(1 - value(t)) <= 0.005;
|
|
89
|
+
let duration = 0;
|
|
90
|
+
while (!settled(duration) && duration < 20000) duration += 50;
|
|
91
|
+
return { value, duration };
|
|
92
|
+
}
|
|
93
|
+
/** A linear() curve's value at progress p (0 to 1), its stops evenly spaced. */
|
|
94
|
+
export const linearAt = (stops, p) => {
|
|
95
|
+
const f = Math.min(Math.max(p, 0), 1) * (stops.length - 1), i = Math.min(Math.floor(f), stops.length - 2);
|
|
96
|
+
return stops[i] + (stops[i + 1] - stops[i]) * (f - i);
|
|
97
|
+
};
|
|
98
|
+
/** The furthest the stops stray from the spring, sampled every quarter millisecond. */
|
|
99
|
+
export function springError(stops, { value, duration }) {
|
|
100
|
+
let worst = 0;
|
|
101
|
+
for (let t = 0; t <= duration; t += 0.25) worst = Math.max(worst, Math.abs(linearAt(stops, t / duration) - value(t)));
|
|
102
|
+
return worst;
|
|
103
|
+
}
|
|
104
|
+
export function springCurve(preset) {
|
|
105
|
+
const s = springSolver(preset);
|
|
106
|
+
const sample = (n) => Array.from({ length: n }, (_, i) => (i === n - 1 ? 1 : Math.round(s.value((i / (n - 1)) * s.duration) * 1e4) / 1e4));
|
|
107
|
+
let n = 8;
|
|
108
|
+
while (springError(sample(n), s) > SPRING_FIT) n++;
|
|
109
|
+
return { duration: s.duration, css: `linear(${sample(n).join(', ')})` };
|
|
110
|
+
}
|
|
111
|
+
export const springs = Object.entries(meta.motion.springs).filter(([k]) => !k.startsWith('$'));
|
|
112
|
+
export const springName = (k) => `--ws-spring-${k.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`;
|
|
113
|
+
const springTokens = springs.flatMap(([k, preset]) => {
|
|
114
|
+
const { duration, css } = springCurve(preset);
|
|
115
|
+
return [[springName(k), css], [`${springName(k)}-duration`, `${duration}ms`]];
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
export const kit = parseRoot(read('tokens/_source.hubl.css'));
|
|
119
|
+
if (kit.length < 80) throw new Error(`Only parsed ${kit.length} kit tokens, source changed shape`);
|
|
120
|
+
export const product = [...parseRoot(read('tokens/_product.css'), [/^--ws-/]), ...springTokens];
|
|
121
|
+
if (product.length < 100) throw new Error(`Only parsed ${product.length} product tokens, source changed shape`);
|
|
122
|
+
export const tokens = [...kit, ...product];
|
|
123
|
+
const dupes = tokens.map(([n]) => n).filter((n, i, all) => all.indexOf(n) !== i);
|
|
124
|
+
if (dupes.length) throw new Error(`Declared twice: ${dupes.join(', ')}`);
|
|
125
|
+
export const known = new Set(tokens.map(([n]) => n));
|
|
126
|
+
|
|
127
|
+
const darkCss = read('tokens/_dark.css');
|
|
128
|
+
export const night = parseBlock(darkCss, ':root[data-theme="dark"]', 'tokens/_dark.css');
|
|
129
|
+
export const navy = parseBlock(darkCss, ':root[data-theme="dark"][data-ground="navy"]', 'tokens/_dark.css');
|
|
130
|
+
for (const [n] of [...night, ...navy]) if (!known.has(n)) throw new Error(`tokens/_dark.css sets ${n}, which no light source declares`);
|
|
131
|
+
|
|
132
|
+
/* ---- The palette contract (design-tokens.json "palette"): the tokens an app may set. Everything else is derived. ---- */
|
|
133
|
+
export const contract = (meta.palette?.tokens ?? []).filter((t) => !t.name.startsWith('$'));
|
|
134
|
+
export const CONTRACT = new Set(contract.map((t) => t.name));
|
|
135
|
+
for (const n of CONTRACT) if (!known.has(n)) throw new Error(`design-tokens.json palette lists ${n}, which is not a token`);
|
|
136
|
+
|
|
137
|
+
/* ---- Theme maps: the raw values (var(), color-mix()) in light, night and the navy ground ---- */
|
|
138
|
+
export const themeMaps = (over = {}) => {
|
|
139
|
+
const light = { ...Object.fromEntries(tokens), ...(over.light || {}) };
|
|
140
|
+
const dark = { ...Object.fromEntries(tokens), ...Object.fromEntries(night), ...(over.dark || {}) };
|
|
141
|
+
// the navy ground sits on top of whatever dark the palette gives, and wins where it sets a token
|
|
142
|
+
const navyMap = { ...dark, ...Object.fromEntries(navy) };
|
|
143
|
+
return { light, dark, navy: navyMap };
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/* ---- Colour: parsing, color-mix, WCAG contrast, CIEDE2000 ---- */
|
|
147
|
+
const NAMED = { transparent: [0, 0, 0, 0], white: [255, 255, 255, 1], black: [0, 0, 0, 1] };
|
|
148
|
+
/** A colour as [r, g, b, a] (0..255, alpha 0..1), or null when it is not a plain colour. */
|
|
149
|
+
export function parseColour(c) {
|
|
150
|
+
c = c.trim().toLowerCase();
|
|
151
|
+
if (c in NAMED) return NAMED[c];
|
|
152
|
+
let m = c.match(/^#([0-9a-f]{3,8})$/);
|
|
153
|
+
if (m) {
|
|
154
|
+
let h = m[1];
|
|
155
|
+
if (h.length === 3 || h.length === 4) h = [...h].map((x) => x + x).join('');
|
|
156
|
+
if (h.length !== 6 && h.length !== 8) return null;
|
|
157
|
+
const v = [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16));
|
|
158
|
+
return [...v, h.length === 8 ? parseInt(h.slice(6), 16) / 255 : 1];
|
|
159
|
+
}
|
|
160
|
+
m = c.match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/);
|
|
161
|
+
if (m) {
|
|
162
|
+
const a = m[4] === undefined ? 1 : m[4].endsWith('%') ? parseFloat(m[4]) / 100 : parseFloat(m[4]);
|
|
163
|
+
return [+m[1], +m[2], +m[3], a];
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
export const hex = ([r, g, b]) => `#${[r, g, b].map((x) => Math.round(x).toString(16).padStart(2, '0')).join('')}`;
|
|
168
|
+
export const formatColour = (c) => (c[3] === 0 && !c[0] && !c[1] && !c[2] ? 'transparent' : c[3] === 1 ? hex(c) : `rgba(${c.slice(0, 3).map(Math.round).join(', ')}, ${+c[3].toFixed(3)})`);
|
|
169
|
+
|
|
170
|
+
/* color-mix(in srgb, a p%, b): CSS mixes premultiplied, so mixing toward transparent keeps the colour and lowers alpha. */
|
|
171
|
+
export function mix(a, p, b, q = 100 - p) {
|
|
172
|
+
const [A, B] = [parseColour(a), parseColour(b)];
|
|
173
|
+
if (!A || !B) throw new Error(`color-mix input ${!A ? a : b} is not a colour`);
|
|
174
|
+
const total = p + q;
|
|
175
|
+
const [pa, pb] = [p / total, q / total];
|
|
176
|
+
const alpha = A[3] * pa + B[3] * pb;
|
|
177
|
+
if (!alpha) return 'transparent';
|
|
178
|
+
const ch = [0, 1, 2].map((i) => Math.round((A[i] * A[3] * pa + B[i] * B[3] * pb) / alpha));
|
|
179
|
+
return formatColour([...ch, alpha * Math.min(1, total / 100)]);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const MIX = /color-mix\(\s*in srgb,\s*(#[0-9a-f]+|transparent|rgba?\([^()]*\))(?:\s+([\d.]+)%)?\s*,\s*(#[0-9a-f]+|transparent|rgba?\([^()]*\))(?:\s+([\d.]+)%)?\s*\)/gi;
|
|
183
|
+
/** Every color-mix() reduced, innermost first, until none is left (a mix may take another mix as its partner). */
|
|
184
|
+
const mixes = (value) => {
|
|
185
|
+
for (let prev; prev !== value; ) {
|
|
186
|
+
prev = value;
|
|
187
|
+
value = value.replace(MIX, (_, a, p, b, q) => {
|
|
188
|
+
const P = p !== undefined ? +p : q !== undefined ? 100 - +q : 50;
|
|
189
|
+
const Q = q !== undefined ? +q : 100 - P;
|
|
190
|
+
return mix(a, P, b, Q);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return value;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/** A resolver for one theme map: var(), color-mix() and calc() on one unit reduced to what they paint. */
|
|
197
|
+
export function resolverFor(src) {
|
|
198
|
+
const resolve = (value, seen = []) =>
|
|
199
|
+
mixes(
|
|
200
|
+
value.replace(/var\(\s*(--[a-z0-9-]+)\s*(?:,\s*([^()]*))?\)/g, (_, n, fallback) => {
|
|
201
|
+
if (!(n in src)) {
|
|
202
|
+
if (fallback !== undefined) return resolve(fallback, seen);
|
|
203
|
+
throw new Error(`var(${n}) is not a token`);
|
|
204
|
+
}
|
|
205
|
+
if (seen.includes(n)) throw new Error(`Alias cycle: ${[...seen, n].join(' -> ')}`);
|
|
206
|
+
return resolve(src[n], [...seen, n]);
|
|
207
|
+
})
|
|
208
|
+
)
|
|
209
|
+
.replace(/calc\(([^()]*)\)/g, (_, expr) => {
|
|
210
|
+
const units = new Set(expr.match(/[a-z%]+/gi));
|
|
211
|
+
if (units.size !== 1) throw new Error(`calc(${expr}) mixes units`);
|
|
212
|
+
const [unit] = units;
|
|
213
|
+
const math = expr.replaceAll(unit, '');
|
|
214
|
+
if (!/^[\d.\s+*/-]+$/.test(math)) throw new Error(`calc(${expr}) is not plain arithmetic`);
|
|
215
|
+
return `${+Function(`return (${math})`)().toFixed(4)}${unit}`;
|
|
216
|
+
})
|
|
217
|
+
.replace(/#[0-9a-f]{3,8}\b/gi, (h) => h.toLowerCase())
|
|
218
|
+
.replace(/\s+/g, ' ')
|
|
219
|
+
.trim();
|
|
220
|
+
return resolve;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Every theme of a palette (or the kit, with no palette) resolved: { light, dark, navy } of name -> literal. */
|
|
224
|
+
export function resolved(over) {
|
|
225
|
+
const maps = themeMaps(over);
|
|
226
|
+
const out = {};
|
|
227
|
+
for (const [theme, map] of Object.entries(maps)) {
|
|
228
|
+
const r = resolverFor(map);
|
|
229
|
+
out[theme] = Object.fromEntries(
|
|
230
|
+
Object.entries(map).map(([n, v]) => {
|
|
231
|
+
try {
|
|
232
|
+
return [n, r(v)];
|
|
233
|
+
} catch {
|
|
234
|
+
return [n, v]; // a value the resolver cannot reduce (a gradient over a mix) stays as written
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** An opaque colour for contrast: a translucent one is composited over `ground` (the colour it sits on). */
|
|
243
|
+
export function opaque(c, ground = '#ffffff') {
|
|
244
|
+
const C = parseColour(c);
|
|
245
|
+
if (!C) throw new Error(`${c} is not a colour`);
|
|
246
|
+
if (C[3] === 1) return C;
|
|
247
|
+
const G = opaque(ground);
|
|
248
|
+
return [0, 1, 2].map((i) => C[i] * C[3] + G[i] * (1 - C[3])).concat(1);
|
|
249
|
+
}
|
|
250
|
+
const lin = (v) => ((v /= 255) <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);
|
|
251
|
+
const luminance = (c) => {
|
|
252
|
+
const [r, g, b] = (Array.isArray(c) ? c : opaque(c)).map(lin);
|
|
253
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
254
|
+
};
|
|
255
|
+
/** WCAG 2.x contrast of `fg` on `bg` (a translucent fg is composited on bg; a translucent bg on white). */
|
|
256
|
+
export function ratio(fg, bg) {
|
|
257
|
+
const B = opaque(bg);
|
|
258
|
+
const F = opaque(fg, hex(B));
|
|
259
|
+
const [x, y] = [luminance(F), luminance(B)].sort((m, n) => n - m);
|
|
260
|
+
return (x + 0.05) / (y + 0.05);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** CIEDE2000 colour difference of two colours (each composited on `ground` if translucent). */
|
|
264
|
+
export function deltaE(a, b, ground = '#ffffff') {
|
|
265
|
+
const lab = (c) => {
|
|
266
|
+
const [r, g, bl] = opaque(c, ground).map(lin);
|
|
267
|
+
const xyz = [
|
|
268
|
+
(0.4124564 * r + 0.3575761 * g + 0.1804375 * bl) / 0.95047,
|
|
269
|
+
0.2126729 * r + 0.7151522 * g + 0.072175 * bl,
|
|
270
|
+
(0.0193339 * r + 0.119192 * g + 0.9503041 * bl) / 1.08883,
|
|
271
|
+
].map((t) => (t > 216 / 24389 ? Math.cbrt(t) : (24389 / 27 * t + 16) / 116));
|
|
272
|
+
return [116 * xyz[1] - 16, 500 * (xyz[0] - xyz[1]), 200 * (xyz[1] - xyz[2])];
|
|
273
|
+
};
|
|
274
|
+
const [L1, a1, b1] = lab(a), [L2, a2, b2] = lab(b);
|
|
275
|
+
const rad = Math.PI / 180, deg = 180 / Math.PI;
|
|
276
|
+
const C1 = Math.hypot(a1, b1), C2 = Math.hypot(a2, b2), Cm = (C1 + C2) / 2;
|
|
277
|
+
const G = 0.5 * (1 - Math.sqrt(Cm ** 7 / (Cm ** 7 + 25 ** 7)));
|
|
278
|
+
const a1p = (1 + G) * a1, a2p = (1 + G) * a2;
|
|
279
|
+
const C1p = Math.hypot(a1p, b1), C2p = Math.hypot(a2p, b2);
|
|
280
|
+
const h = (bb, ap) => (bb === 0 && ap === 0 ? 0 : (Math.atan2(bb, ap) * deg + 360) % 360);
|
|
281
|
+
const h1p = h(b1, a1p), h2p = h(b2, a2p);
|
|
282
|
+
const dLp = L2 - L1, dCp = C2p - C1p;
|
|
283
|
+
let dhp = C1p * C2p === 0 ? 0 : h2p - h1p;
|
|
284
|
+
if (dhp > 180) dhp -= 360; else if (dhp < -180) dhp += 360;
|
|
285
|
+
const dHp = 2 * Math.sqrt(C1p * C2p) * Math.sin((dhp / 2) * rad);
|
|
286
|
+
const Lpm = (L1 + L2) / 2, Cpm = (C1p + C2p) / 2;
|
|
287
|
+
let hpm = h1p + h2p;
|
|
288
|
+
if (C1p * C2p !== 0) hpm = Math.abs(h1p - h2p) <= 180 ? hpm / 2 : (hpm + (hpm < 360 ? 360 : -360)) / 2;
|
|
289
|
+
const T = 1 - 0.17 * Math.cos((hpm - 30) * rad) + 0.24 * Math.cos(2 * hpm * rad) + 0.32 * Math.cos((3 * hpm + 6) * rad) - 0.2 * Math.cos((4 * hpm - 63) * rad);
|
|
290
|
+
const dTheta = 30 * Math.exp(-(((hpm - 275) / 25) ** 2));
|
|
291
|
+
const Rc = 2 * Math.sqrt(Cpm ** 7 / (Cpm ** 7 + 25 ** 7));
|
|
292
|
+
const Sl = 1 + (0.015 * (Lpm - 50) ** 2) / Math.sqrt(20 + (Lpm - 50) ** 2), Sc = 1 + 0.045 * Cpm, Sh = 1 + 0.015 * Cpm * T;
|
|
293
|
+
const Rt = -Math.sin(2 * dTheta * rad) * Rc;
|
|
294
|
+
return Math.sqrt((dLp / Sl) ** 2 + (dCp / Sc) ** 2 + (dHp / Sh) ** 2 + Rt * (dCp / Sc) * (dHp / Sh));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* ---- Where words sit (D2, D12): the grounds every word token is gated on, and the pairs on their own tints ---- */
|
|
298
|
+
export const GROUNDS = {
|
|
299
|
+
'--color-bg-page': 'cream', '--color-surface': 'white', '--ws-surface-sunk': 'plate', '--ws-tray': 'tray', '--ws-raised': 'raised',
|
|
300
|
+
'--ws-page': 'shell', '--ws-surface-press': 'press', '--ws-well': 'well', '--ws-raised-sunk': 'raised sunk',
|
|
301
|
+
'--ws-tint-accent': 'accent tint', '--ws-tint-needs': 'needs tint', '--ws-tint-live': 'live tint', '--ws-card-tint-blue': 'blue card',
|
|
302
|
+
'--ws-card-tint-done': 'done card', '--ws-chart-foot': 'chart foot', '--ws-sidebar-hover': 'sidebar hover', '--ws-menu-hover': 'menu hover',
|
|
303
|
+
'--color-surface-stripe': 'stripe',
|
|
304
|
+
};
|
|
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' };
|
|
307
|
+
export const WORDS = [
|
|
308
|
+
'--color-text-heading', '--color-text-body', '--color-text-muted', '--color-text-interactive',
|
|
309
|
+
'--color-text-brand-strong', '--color-text-error-strong', '--color-text-success-strong',
|
|
310
|
+
'--ws-word-needs', '--ws-word-positive', '--ws-word-needs-work', '--ws-word-alert', '--ws-word-past-half', '--ws-word-answer',
|
|
311
|
+
];
|
|
312
|
+
export const PAIRS = [
|
|
313
|
+
['--color-text-placeholder', '--ws-field'], ['--ws-field-ink', '--ws-field'], ['--ws-on-act', '--color-text-heading'],
|
|
314
|
+
['--ws-word-needs', '--ws-tint-needs'], ['--ws-word-positive', '--ws-tint-live'], ['--ws-word-needs-work', '--ws-tint-needs-work'],
|
|
315
|
+
['--color-text-success-strong', '--color-feedback-success-bg'], ['--ws-on-act', '--ws-agent-tally'],
|
|
316
|
+
];
|
|
317
|
+
|
|
318
|
+
/* ---- A palette file: `[data-palette="name"] { ... }` for light, the same with [data-theme="dark"] for dark ---- */
|
|
319
|
+
/** Parses palette CSS (tokens/_palettes.css, an app's own file, or what `check-palette --emit` writes) into
|
|
320
|
+
* { name: { light: {..}, dark: {..} } }. A rule's palette is the [data-palette="..."] in its selector or in the
|
|
321
|
+
* @scope around it; it is dark when either names [data-theme="dark"], or [data-theme="system"] inside a
|
|
322
|
+
* prefers-color-scheme: dark @media ("system" outside one is light). A rule under the navy ground is the kit's, not
|
|
323
|
+
* the palette's. Anything else in the file is returned in `other` so the check can say so. */
|
|
324
|
+
export function parsePalettes(css) {
|
|
325
|
+
const palettes = {};
|
|
326
|
+
const other = [];
|
|
327
|
+
const NAME = /\[data-palette\s*=\s*["']?([\w-]+)["']?\s*\]/g;
|
|
328
|
+
const walk = (text, ctx) => {
|
|
329
|
+
for (let i = 0; i < text.length; ) {
|
|
330
|
+
const open = text.indexOf('{', i);
|
|
331
|
+
if (open === -1) return;
|
|
332
|
+
const prelude = text.slice(i, open).replace(/^[\s;]+/, '').trim();
|
|
333
|
+
let depth = 1, j = open + 1;
|
|
334
|
+
for (; j < text.length && depth; j++) depth += text[j] === '{' ? 1 : text[j] === '}' ? -1 : 0;
|
|
335
|
+
const inner = text.slice(open + 1, j - 1);
|
|
336
|
+
i = j;
|
|
337
|
+
if (prelude.startsWith('@media')) walk(inner, { ...ctx, darkOs: ctx.darkOs || /prefers-color-scheme:\s*dark/.test(prelude) });
|
|
338
|
+
else if (prelude.startsWith('@scope')) walk(inner, { ...ctx, scope: `${ctx.scope} ${prelude}` });
|
|
339
|
+
else if (prelude.startsWith('@')) walk(inner, ctx); // @layer, @supports
|
|
340
|
+
else {
|
|
341
|
+
const where = `${ctx.scope} ${prelude}`;
|
|
342
|
+
const names = [...where.matchAll(NAME)].map(([, n]) => n);
|
|
343
|
+
if (!names.length) {
|
|
344
|
+
if (inner.trim()) other.push(prelude);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (/data-ground/.test(where)) continue;
|
|
348
|
+
const dark = /\[data-theme\s*=\s*["']?dark["']?\s*\]/.test(where) || (ctx.darkOs && /\[data-theme\s*=\s*["']?system["']?\s*\]/.test(where));
|
|
349
|
+
for (const name of new Set(names)) {
|
|
350
|
+
const p = (palettes[name] ??= { light: {}, dark: {} });
|
|
351
|
+
for (const [n, v] of decls(inner, null, prelude)) (dark ? p.dark : p.light)[n] = v;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
walk(stripComments(css), { darkOs: false, scope: '' });
|
|
357
|
+
return { palettes, other };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export const builtIns = () => parsePalettes(read('tokens/_palettes.css')).palettes;
|
|
361
|
+
|
|
362
|
+
/* ---- The token stylesheet: every theme and palette on any element (D12, D15) ------------------------------------
|
|
363
|
+
A custom property's var() resolves on the element that declares it, so a token derived from another (a hover from
|
|
364
|
+
the page, an edge from the heading) would keep the :root result inside a container that sets its own palette or
|
|
365
|
+
theme. So every derived token is declared again on each element that can change the context: [data-palette],
|
|
366
|
+
[data-theme] and .fello (the carriers). The contract tokens (the palette's inputs) are never declared again there,
|
|
367
|
+
so a palette set on a container reaches everything inside it.
|
|
368
|
+
|
|
369
|
+
The theme is nearest-wins on any element: a theme root ([data-theme="dark"], "system" on a dark OS, "light", and
|
|
370
|
+
the navy ground beside dark) sets its theme's values on itself, and its derived tokens on the carriers inside it
|
|
371
|
+
through @scope, whose proximity picks the nearest root, so light inside dark and dark inside light both work.
|
|
372
|
+
A palette and a theme set on different elements meet the same way: a palette inside a theme takes the palette's
|
|
373
|
+
values for that theme, and a theme inside a palette takes that theme's palette values. The navy ground wins over a
|
|
374
|
+
palette where it sets a token (it is a brand ground). Browsers without @scope get the theme on the element that
|
|
375
|
+
carries it (the common case: <html data-theme>) and lose only the nesting.
|
|
376
|
+
|
|
377
|
+
Load order never matters (D15): the kit's own contract values sit at no specificity (:where(:root), and every
|
|
378
|
+
[data-palette] element's reset), a palette's rules weigh one attribute or more, and where two rules weigh the same
|
|
379
|
+
@scope proximity decides. So an app's palette, written by `check-palette --emit` in the built-ins' form, nests
|
|
380
|
+
exactly like a built-in in either build, before or after the kit. */
|
|
381
|
+
const LIGHT = Object.fromEntries(tokens);
|
|
382
|
+
const NIGHT = Object.fromEntries(night);
|
|
383
|
+
const NAVY = Object.fromEntries(navy);
|
|
384
|
+
const isExpr = (v) => typeof v === 'string' && /var\(/.test(v);
|
|
385
|
+
/** Whether a token's value (in light, night or navy) reads, through any chain of var(), a contract token or a token a
|
|
386
|
+
* theme changes: only those can differ inside a container, so only those are declared again there. */
|
|
387
|
+
const VARIES = new Set([...CONTRACT, ...Object.keys(NIGHT), ...Object.keys(NAVY)]);
|
|
388
|
+
const memo = new Map();
|
|
389
|
+
const varies = (n, seen = new Set()) => {
|
|
390
|
+
if (memo.has(n)) return memo.get(n);
|
|
391
|
+
if (seen.has(n)) return false;
|
|
392
|
+
seen.add(n);
|
|
393
|
+
const out = [LIGHT[n], NIGHT[n], NAVY[n]].some((v) => [...(v || '').matchAll(/var\(\s*(--[a-z0-9-]+)/g)].some(([, r]) => VARIES.has(r) || varies(r, seen)));
|
|
394
|
+
memo.set(n, out);
|
|
395
|
+
return out;
|
|
396
|
+
};
|
|
397
|
+
/** Non-contract tokens derived in light from something that can change: re-declared on every carrier. */
|
|
398
|
+
const DERIVED = tokens.filter(([n, v]) => !CONTRACT.has(n) && isExpr(v) && varies(n)).map(([n]) => n);
|
|
399
|
+
/** Non-contract tokens a theme sets that a carrier must re-declare under it: the ones every carrier re-declares in
|
|
400
|
+
* light, and the ones the theme derives from something that can change. */
|
|
401
|
+
const readsVarying = (v = '') => [...v.matchAll(/var\(\s*(--[a-z0-9-]+)/g)].some(([, r]) => VARIES.has(r) || varies(r));
|
|
402
|
+
const themeDerived = (block, below = {}) =>
|
|
403
|
+
Object.keys(block).filter((n) => !CONTRACT.has(n) && (DERIVED.includes(n) || readsVarying(below[n]) || readsVarying(block[n])));
|
|
404
|
+
const TD_DARK = themeDerived(NIGHT);
|
|
405
|
+
const TD_NAVY = themeDerived(NAVY, NIGHT);
|
|
406
|
+
const TD_LIGHT = [...new Set([...TD_DARK, ...TD_NAVY])];
|
|
407
|
+
/** Contract tokens the kit derives (var()): a theme root states them again, so they follow its theme. */
|
|
408
|
+
const contractDerived = (map) => [...CONTRACT].filter((n) => isExpr(map[n]));
|
|
409
|
+
|
|
410
|
+
const pick = (names, map) => names.map((n) => [n, map[n]]);
|
|
411
|
+
const decl = (pairs, pad) => pairs.map(([n, v]) => `${pad}${n}: ${v};`).join('\n');
|
|
412
|
+
const rule = (sel, pairs, pad = '', extra = '') => `${pad}${sel} {\n${extra ? `${pad} ${extra}\n` : ''}${decl(pairs, `${pad} `)}\n${pad}}`;
|
|
413
|
+
const darkOs = (css) => `@media (prefers-color-scheme: dark) {\n${css.replace(/^/gm, ' ')}\n}`;
|
|
414
|
+
const list = (s) => s.split(/,\s*/);
|
|
415
|
+
|
|
416
|
+
/** The contract tokens a palette sets, in light or dark. */
|
|
417
|
+
export const ownNames = (p) => [...CONTRACT].filter((n) => n in p.light || n in p.dark);
|
|
418
|
+
|
|
419
|
+
/** A palette's values for `names`: what it says, else the kit's own expression in that theme, so a dark value it
|
|
420
|
+
* leaves out is the kit's dark one (derived from its dark inputs), never its light one (D15). `fellBack` lists those. */
|
|
421
|
+
export function paletteValues(p, names = ownNames(p)) {
|
|
422
|
+
const fellBack = Object.keys(p.light).filter((n) => !(n in p.dark));
|
|
423
|
+
return {
|
|
424
|
+
light: names.map((n) => [n, p.light[n] ?? LIGHT[n]]),
|
|
425
|
+
dark: names.map((n) => [n, p.dark[n] ?? NIGHT[n] ?? LIGHT[n]]),
|
|
426
|
+
fellBack,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** The kit's own values where a palette may meet them: a contract token at no specificity (`zero`), so any palette
|
|
431
|
+
* beats it whichever loads first; every other token as it was (`sel`), so a host's element selector cannot reach it. */
|
|
432
|
+
export const splitRule = (sel, zero, pairs, pad = '', extra = '') => {
|
|
433
|
+
const own = pairs.filter(([n]) => !CONTRACT.has(n)), input = pairs.filter(([n]) => CONTRACT.has(n));
|
|
434
|
+
return [own.length || extra ? rule(sel, own, pad, extra) : '', input.length ? rule(zero, input, pad) : ''].filter(Boolean).join('\n');
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
/** The selectors of one build. Root: tokens.root.css, tokens.css, fello.css. Scoped: fello.scoped.css, where every rule
|
|
438
|
+
* stays inside a .fello island and the theme may come from the host. Both weigh the same (.fello sits in :where()), so
|
|
439
|
+
* a palette, an app's own or a built-in, meets the kit's rules on specificity and proximity alone, in either build. */
|
|
440
|
+
function selectors(mode) {
|
|
441
|
+
const scoped = mode === 'scoped';
|
|
442
|
+
return {
|
|
443
|
+
scoped,
|
|
444
|
+
ROOT: scoped ? '.fello:not(:where(.fello) *)' : ':root',
|
|
445
|
+
ROOT0: scoped ? ':where(.fello):not(:where(.fello) *)' : ':where(:root)',
|
|
446
|
+
// an element in the island, outside @scope (a descendant selector), and inside one (the subject itself anchored: a
|
|
447
|
+
// scoped selector is relative to its root, so a .fello between the two could not be asked for)
|
|
448
|
+
at: (sels) => list(sels).flatMap((s) => (scoped ? [`:where(.fello)${s}`, `:where(.fello) ${s}`] : [s])).join(', '),
|
|
449
|
+
inner: (sels) => list(sels).map((s) => (scoped ? `${s}:where(.fello, .fello *)` : s)).join(', '),
|
|
450
|
+
CARRIERS: scoped ? '[data-palette]:where(.fello *), .fello' : '[data-palette], .fello',
|
|
451
|
+
SELF: scoped ? ':scope:where(.fello, .fello *)' : ':scope',
|
|
452
|
+
RESET: scoped ? ':where([data-palette]):where(.fello, .fello *)' : ':where([data-palette])',
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
const ROOTS = { light: '[data-theme="light"], [data-theme="system"]', dark: '[data-theme="dark"]', navy: '[data-theme="dark"][data-ground="navy"]' };
|
|
456
|
+
const SYSTEM = { dark: '[data-theme="system"]', navy: '[data-theme="system"][data-ground="navy"]' };
|
|
457
|
+
|
|
458
|
+
/* The navy ground over a palette: a brand ground, so where it sets a contract token it wins over a palette's dark value,
|
|
459
|
+
on the same element (three attributes) and inside it (the palette attribute counted twice), whichever loads first,
|
|
460
|
+
down to the next theme root inside it. The kit writes it once for every palette; an app's shipped palette carries
|
|
461
|
+
its own copy, since in fello.css the kit's sits in a layer an unlayered palette always beats. */
|
|
462
|
+
function navyOverCss(P, mode) {
|
|
463
|
+
const { at, inner } = selectors(mode);
|
|
464
|
+
const set = pick([...CONTRACT].filter((n) => n in NAVY), NAVY);
|
|
465
|
+
const over = (roots) =>
|
|
466
|
+
[rule(list(roots).map((r) => at(`${P}${r}`)).join(', '), set), `@scope (${roots}) to ([data-theme]) {\n${rule(inner(`${P}[data-palette]`), set, ' ')}\n}`].join('\n');
|
|
467
|
+
return [over(ROOTS.navy), darkOs(over(SYSTEM.navy))].join('\n');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** One palette's rules: its light values on itself, and each theme's values wherever that theme meets it (on the same
|
|
471
|
+
* element, a theme around it, a theme inside it); the nearest wins. The form the built-ins ship in, and what
|
|
472
|
+
* `check-palette --emit` writes for an app's own palette (with `navy`, its own copy of the navy ground's override),
|
|
473
|
+
* so both nest the same way in either build. */
|
|
474
|
+
export function paletteCss(id, p, mode = 'root', { navy: withNavy = false } = {}) {
|
|
475
|
+
const S = selectors(mode);
|
|
476
|
+
const P = `[data-palette="${id}"]`;
|
|
477
|
+
const v = paletteValues(p);
|
|
478
|
+
if (!v.light.length) return `/* ${id}: sets no contract token */`;
|
|
479
|
+
const forTheme = (vals, roots) =>
|
|
480
|
+
[
|
|
481
|
+
rule(list(roots).map((r) => S.at(`${P}${r}`)).join(', '), vals),
|
|
482
|
+
`@scope (${roots}) {\n${rule(S.inner(P), vals, ' ')}\n}`,
|
|
483
|
+
`@scope (${P}) {\n${rule(S.inner(roots), vals, ' ')}\n}`,
|
|
484
|
+
].join('\n');
|
|
485
|
+
return [`/* ${id} */`, rule(S.at(P), v.light), forTheme(v.light, ROOTS.light), forTheme(v.dark, ROOTS.dark), darkOs(forTheme(v.dark, SYSTEM.dark)), ...(withNavy ? [navyOverCss(P, mode)] : [])].join('\n');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** The token stylesheet after the :root block: `mode` "root" (tokens.root.css, tokens.css, fello.css) or "scoped"
|
|
489
|
+
* (fello.scoped.css, where every rule stays inside a .fello island and a theme or palette may come from the host). */
|
|
490
|
+
export function contextCss(mode, palettes) {
|
|
491
|
+
const { scoped, ROOT, ROOT0, at, CARRIERS, SELF, RESET } = selectors(mode);
|
|
492
|
+
const T = {
|
|
493
|
+
// a light root inside a dark one: the derived tokens come back through @scope (:scope below), the rest here
|
|
494
|
+
light: pick([...new Set([...Object.keys(NIGHT), ...Object.keys(NAVY), ...contractDerived(LIGHT)])].filter((n) => !TD_LIGHT.includes(n)), LIGHT),
|
|
495
|
+
dark: [...night, ...pick(contractDerived(LIGHT).filter((n) => !(n in NIGHT)), LIGHT)],
|
|
496
|
+
navy,
|
|
497
|
+
};
|
|
498
|
+
const TD = { light: pick(TD_LIGHT, LIGHT), dark: pick(TD_DARK, NIGHT), navy: pick(TD_NAVY, NAVY) };
|
|
499
|
+
const scheme = { light: 'color-scheme: light;', dark: 'color-scheme: dark;', navy: '' };
|
|
500
|
+
|
|
501
|
+
/** One theme: its roots (on or in the island), the host's root reaching the island (scoped), the carriers inside. */
|
|
502
|
+
const theme = (name, roots) => {
|
|
503
|
+
const out = [rule(at(roots), T[name], '', scheme[name])];
|
|
504
|
+
if (scoped) out.push(`@scope (${roots}) {\n${splitRule(ROOT, ROOT0, T[name], ' ', scheme[name])}\n}`);
|
|
505
|
+
out.push(`@scope (${roots}) {\n${rule(`${SELF}, ${CARRIERS}`, TD[name], ' ')}\n}`);
|
|
506
|
+
return out.join('\n');
|
|
507
|
+
};
|
|
508
|
+
/* The reset: an element that sets a palette starts from the kit's own contract values for the theme it is in, at no
|
|
509
|
+
specificity. A palette then states only what it changes and still resolves whole inside another palette, and a
|
|
510
|
+
contract token the kit derives (the focus ring from the heading, growth from the past-half tone) recomputes from
|
|
511
|
+
that palette's inputs unless the palette sets it. A palette's own rules always beat it. */
|
|
512
|
+
// a theme's reset holds only what that theme changes: the light one under it already restates the rest
|
|
513
|
+
const THEMED = { light: [...CONTRACT].filter((n) => n in NIGHT || n in NAVY), dark: [...CONTRACT].filter((n) => n in NIGHT) };
|
|
514
|
+
const reset = (name, roots) => `@scope (${roots}) {\n${rule(RESET, pick(THEMED[name], name === 'light' ? LIGHT : NIGHT), ' ')}\n}`;
|
|
515
|
+
|
|
516
|
+
return [
|
|
517
|
+
`/* Derived tokens again on every carrier, so a palette or theme set on any element reaches what derives from it. */`,
|
|
518
|
+
// scoped: the island's own root too when it carries a palette or theme (its block below holds the root literals)
|
|
519
|
+
rule(scoped ? `:where(.fello) [data-palette], :where(.fello)[data-palette], :where(.fello) [data-theme], :where(.fello)[data-theme], :where(.fello) .fello` : '[data-palette], [data-theme], .fello', pick(DERIVED, LIGHT)),
|
|
520
|
+
`/* Every palette element starts from the kit's contract values (light here, each theme's below), at no specificity. */`,
|
|
521
|
+
rule(RESET, pick([...CONTRACT], LIGHT)),
|
|
522
|
+
`/* Light: data-theme="light" (and "system" on a light OS) inside a dark element. */`,
|
|
523
|
+
theme('light', ROOTS.light),
|
|
524
|
+
reset('light', ROOTS.light),
|
|
525
|
+
`/* Night: data-theme="dark", and "system" on a dark OS. */`,
|
|
526
|
+
theme('dark', ROOTS.dark),
|
|
527
|
+
reset('dark', ROOTS.dark),
|
|
528
|
+
darkOs([theme('dark', SYSTEM.dark), reset('dark', SYSTEM.dark)].join('\n')),
|
|
529
|
+
`/* The palettes (tokens/_palettes.css): ${Object.keys(palettes).join(', ')}. */`,
|
|
530
|
+
...Object.entries(palettes).map(([id, p]) => paletteCss(id, p, mode)),
|
|
531
|
+
`/* The navy ground, beside data-theme="dark" (or "system" on a dark OS): over night, and over a palette. */`,
|
|
532
|
+
theme('navy', ROOTS.navy),
|
|
533
|
+
darkOs(theme('navy', SYSTEM.navy)),
|
|
534
|
+
navyOverCss('[data-palette]', mode),
|
|
535
|
+
'',
|
|
536
|
+
].join('\n');
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/* The chart fades, tints and empty cell are declared on the root as the literal they resolve to in light (the
|
|
540
|
+
designer's value), and derive everywhere a palette or theme opens (the carriers): charts draw them as gradient stops
|
|
541
|
+
(SVG gradients, a ring's conic gradient written in the markup), and Chromium blends a gradient whose stop is a
|
|
542
|
+
computed colour (color-mix) in Oklab unless the gradient says "in srgb", which SVG cannot, so the default would move
|
|
543
|
+
a level or four. A palette moves them as it moves everything else. */
|
|
544
|
+
export const ROOT_LITERAL = new Set([
|
|
545
|
+
'--ws-chart-empty', '--ws-chart-navy-fade', '--ws-chart-salmon-fade', '--ws-chart-purple-fade', '--ws-chart-teal-fade',
|
|
546
|
+
'--ws-chart-finished-tint', '--ws-chart-past-half-tint', '--ws-chart-under-half-tint',
|
|
547
|
+
]);
|
|
548
|
+
/** The token list as a root block declares it (tokens.css, tokens.root.css, the scoped island). */
|
|
549
|
+
export function rootList() {
|
|
550
|
+
const light = resolved().light;
|
|
551
|
+
return tokens.map(([n, v]) => [n, ROOT_LITERAL.has(n) ? light[n] : v]);
|
|
552
|
+
}
|