Skip to content

Commit 42dc418

Browse files
refactor(tokens): DLT-3013 convert color system from HSL to OKLCH (#1060)
1 parent 28cb1de commit 42dc418

File tree

160 files changed

+2267
-1404
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

160 files changed

+2267
-1404
lines changed

.claude/rules/css-utilities.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ Color utilities use semantic tokens, not base palette stops:
2525

2626
Prefer semantic equivalents over base color utilities (e.g., `d-fc-critical` instead of `d-fc-red-600`). The ESLint rule `deprecated-base-color-classes` flags base color utility usage.
2727

28+
Generated color utilities use OKLCH relative color syntax for opacity support:
29+
30+
```css
31+
.d-fc-primary { color: oklch(from var(--dt-color-foreground-primary) l c h / var(--fco, alpha)) !important; }
32+
```
33+
34+
The `alpha` keyword preserves the source color's opacity when no opacity utility is applied. Opacity utilities (e.g., `d-fco50`) set the `--fco` / `--bgo` / `--bco` custom property to override it.
35+
36+
HSL channel variables (`--dt-color-*-h`, `-s`, `-l`, `-hsl`, `-hsla`) no longer exist.
37+
2838
## Token References — Mandatory
2939

3040
ALWAYS use `var(--dt-*)` custom properties. Never hardcode raw values.

.claude/rules/design-tokens.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ These produce CSS custom properties: `--dt-color-foreground-primary`, `--dt-spac
2424
All base colors use a standard 12-stop scale: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950, 1000.
2525
Irregular stops (250, 350, 425, etc.) were removed in the Feb 2026 migration. Do not create tokens referencing irregular stops.
2626

27+
## Color Space
28+
29+
Base color token values use OKLCH format: `oklch(L C H)` (e.g., `oklch(0.6464 0.1985 289.97)`). Previous hex/HSL values were refactored to OKLCH in Feb 2026. HSL channel decomposition variables (`-h`, `-s`, `-l`, `-hsl`, `-hsla` suffixes) no longer exist — do not reference them.
30+
2731
## Dark Mode
2832

2933
Values in `dark.json` override `default.json`. When adding/editing a token, ensure `dark.json` has the corresponding override with the appropriate dark palette reference.

.claude/skills/component.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export * from './<name>_constants';
3131
```
3232

3333
**Optional files for complex components:**
34+
3435
- **`utils.js`** — Utility/helper functions specific to this component
3536
- **`validators.js`** — Custom prop or value validators
3637
- **`modules/`** — Directory for sub-components (e.g. emoji_picker has `emoji_search.vue`, `emoji_selector.vue`)

.claude/skills/dt-migrate.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,25 @@ description: "Run Dialtone migration tools for token and utility renames. Use '/
1111
| `color-stops` | Renames irregular color stops (250, 350, 425, etc.) to standard 12-stop scale | `npx dialtone-migration-helper` → "color stops" |
1212
| `base-to-semantic` | Replaces base color utilities and CSS tokens with semantic equivalents | `npx dialtone-migration-helper` → "base to semantic" |
1313
| `space-to-size` | Renames `var(--dt-space-*)` to `var(--dt-size-*)` | `npx dialtone-migration-helper` → "space to size" |
14+
| `hsl-to-oklch` | Migrates consumer HSL channel variable patterns to OKLCH relative color syntax or plain `var()` | `npx dialtone-migration-helper` → "hsl to oklch" |
1415

1516
## Usage
1617

1718
### `/dt-migrate`
19+
1820
List all available migrations with descriptions.
1921

2022
### `/dt-migrate <name>`
23+
2124
Run the specified migration:
25+
2226
1. Confirm the target directory with the user (default: `./src`)
2327
2. Run `npx dialtone-migration-helper --cwd <dir>` and select the named config
2428
3. Report the number of files changed and matches replaced
2529
4. Suggest running linters after migration to catch remaining manual fixes
2630

2731
### `/dt-migrate <name> --dry-run`
32+
2833
Preview changes without applying them.
2934

3035
### `/dt-migrate color-stops --merge-from staging`
@@ -55,6 +60,7 @@ node scripts/merge-migrate-color-stops.mjs --merge-from staging --dry-run --verb
5560
> Note: `scripts/merge-migrate-color-stops.mjs` is a temporary script for the staging→next migration period. Delete it once the migration is complete.
5661
5762
## Migration Helper Location
63+
5864
Configs: `packages/dialtone-css/lib/build/js/dialtone_migration_helper/configs/`
5965
Tests: `packages/dialtone-css/lib/build/js/dialtone_migration_helper/tests/`
6066

.claude/skills/token.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,4 @@ After creating or updating tokens:
5959
- If tokens are edited manually (not via Figma sync), `sync:tokens-to-figma` may be needed to push changes back to Figma
6060
- The `$metadata.json` file defines the build order for 137 token sets — do not modify this without understanding the dependency chain
6161
- Component tokens should reference semantic tokens, not base palette tokens directly, to ensure theme compatibility
62+
- Base color token values use OKLCH format (`oklch(L C H)`), not hex or HSL. HSL channel decomposition (`-h`, `-s`, `-l` suffix variables) was removed — tokens output only the base variable now.

.claude/skills/utility.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,4 @@ After creating or updating a utility:
7070
- The CSS package ships as one monolithic file — every utility is included regardless of usage
7171
- PurgeCSS (shipped as a Dialtone PostCSS plugin) is the recommended approach for tree-shaking unused utilities in consuming projects
7272
- When adding utilities that pair with component styles, coordinate with the component skill to ensure consistency
73+
- Color utilities are generated using OKLCH relative color syntax (`oklch(from var(...) l c h / var(--opacity-var, alpha))`). When debugging color utility output, expect this format rather than raw `var()` references. See `postcss/dialtone-generators.cjs` for the generation logic.
Lines changed: 67 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,62 @@
11
<template>
2-
<dt-stack as="aside">
3-
<dt-stack v-if="stops.length" as="header" direction="row" justify="between">
4-
<h4
5-
class="d-docsite--header-3 d-tt-capitalize"
2+
<dt-stack as="aside" gap="300">
3+
<dt-stack v-if="stops.length" as="header" direction="row" justify="between" align="baseline">
4+
<dt-text
5+
as="h4"
6+
kind="headline"
7+
size="lg"
8+
class="d-tt-capitalize"
9+
text-box-trim="start"
610
tabindex="-1"
7-
v-text="colorName"
8-
/>
11+
>
12+
{{ colorName }}
13+
</dt-text>
14+
<dt-text
15+
v-dt-tooltip="`Lightness Contrast (APCA) against either pure white or black. 60 is considered AA accessible.`"
16+
as="abbr"
17+
tabindex="0"
18+
text-box-trim="start"
19+
class="d-px12 d-td-dotted d-c-help"
20+
>
21+
LC
22+
</dt-text>
923
</dt-stack>
10-
<div
11-
v-for="(stop, index) in stops"
12-
:key="`${colorName}-${index}`"
13-
:class="[
14-
'd-d-flex d-jc-space-between d-ai-center d-px12 d-py8 d-code--sm',
15-
{
16-
'd-btr4': index === 0,
17-
'd-bbr4': index === (stops.length - 1),
18-
},
19-
]"
20-
:style="`background-color: ${stop.value}`"
21-
>
22-
<div :class="fontColorClass(stop.primaryContrast, stop.invertedContrast)">
23-
<strong v-text="`var(--dt-color-${colorName}-${stop.stop})`" />
24-
<br>
25-
<span v-text="stop.value" />
26-
</div>
27-
<dt-stack class="d-fs-100 d-lh2 d-fw-bold d-bar-sm d-px4 py2">
28-
<span
29-
v-if="stop.primaryContrast >= minAAContrastRatio"
30-
:class="fontColorMap[mode].primary"
31-
v-text="formattedContrast(stop.primaryContrast)"
32-
/>
33-
<span
34-
v-if="stop.invertedContrast >= minAAContrastRatio"
35-
:class="fontColorMap[mode].inverted"
36-
v-text="formattedContrast(stop.invertedContrast)"
37-
/>
24+
<dt-stack>
25+
<dt-stack
26+
v-for="(stop, index) in stops"
27+
:key="`${colorName}-${index}`"
28+
direction="row"
29+
align="center"
30+
justify="space-between"
31+
:class="[
32+
'd-px12 d-py8 d-text-code--xs',
33+
{
34+
'd-btr4': index === 0,
35+
'd-bbr4': index === (stops.length - 1),
36+
},
37+
]"
38+
:style="`background-color: ${stop.value}`"
39+
>
40+
<dt-stack gap="300" :class="fontColorClass(stop.lightness)">
41+
<dt-text as="strong" class="d-us-all">
42+
{{ `var(--dt-color-${colorName}-${stop.stop})` }}
43+
</dt-text>
44+
<dt-text class="d-o75 d-us-all">
45+
{{ stop.value }}
46+
</dt-text>
47+
</dt-stack>
48+
<dt-text
49+
strength="bold"
50+
:class="fontColorClass(stop.lightness)"
51+
>
52+
{{ formattedContrast(activeContrast(stop)) }}
53+
</dt-text>
3854
</dt-stack>
39-
</div>
55+
</dt-stack>
4056
</dt-stack>
4157
</template>
4258

4359
<script setup>
44-
const minAAContrastRatio = 4;
45-
const minAAAContrastRatio = 7;
46-
47-
// Using neutral colors instead of primary, primary-inverted to make it easier to
48-
// implement this, as the primary color change based on theme, it was being pretty complex
49-
// to keep the text colors matching.
50-
const fontColorMap = {
51-
light: {
52-
primary: 'd-fc-neutral-black',
53-
inverted: 'd-fc-neutral-white',
54-
},
55-
dark: {
56-
primary: 'd-fc-neutral-white',
57-
inverted: 'd-fc-neutral-black',
58-
},
59-
};
60-
6160
const props = defineProps({
6261
stops: {
6362
type: Array,
@@ -73,13 +72,23 @@ const props = defineProps({
7372
},
7473
});
7574
76-
function fontColorClass (primaryContrast, invertedContrast) {
77-
return primaryContrast > invertedContrast
78-
? fontColorMap[props.mode].primary
79-
: fontColorMap[props.mode].inverted;
75+
const LIGHTNESS_THRESHOLD = 0.65;
76+
77+
function fontColorClass (lightness) {
78+
return lightness >= LIGHTNESS_THRESHOLD
79+
? 'd-fc-neutral-black'
80+
: 'd-fc-neutral-white';
81+
}
82+
function activeContrast (stop) {
83+
const useBlackText = stop.lightness >= LIGHTNESS_THRESHOLD;
84+
// In light mode: primary = black contrast, inverted = white contrast
85+
// In dark mode: primary = white contrast, inverted = black contrast
86+
if (props.mode === 'light') {
87+
return useBlackText ? stop.primaryContrast : stop.invertedContrast;
88+
}
89+
return useBlackText ? stop.invertedContrast : stop.primaryContrast;
8090
}
8191
function formattedContrast (contrast) {
82-
const contrastGrade = contrast >= minAAAContrastRatio ? 'AAA' : (contrast >= minAAContrastRatio ? 'AA' : 'A');
83-
return `${contrastGrade} ${contrast}`;
92+
return `${Math.ceil(contrast)}`;
8493
}
8594
</script>

apps/dialtone-documentation/docs/.vuepress/common/utilities.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,10 @@ export function extractUtilityClasses (utilityClassDocs, prefix) {
7171
}
7272

7373
export function extractCSSVariableName (propValue) {
74-
const variable = Object.values(propValue.values)[0].value;
75-
if (!variable.startsWith('var(')) return;
76-
return variable.replace('var(', '').replace(/(-[hsla])?\).*/, '');
74+
const value = Object.values(propValue.values)[0].value;
75+
const match = value.match(/var\((--[\w-]+)\)/);
76+
if (!match) return;
77+
return match[1].replace(/(-(h|s|c|l|a|hsl|hsla|oklch|oklcha))$/, '');
7778
}
7879

7980
/**

apps/dialtone-documentation/docs/.vuepress/theme/assets/less/dialtone-docs.less

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,7 @@ code {
558558

559559
&:hover,
560560
.dialtone-icon-card[data-selected='yes'] & {
561-
background-color: hsl(var(--dt-color-neutral-black-h), var(--dt-color-neutral-black-s), var(--dt-color-neutral-black-l), 0.8);
561+
background-color: oklch(from var(--dt-color-neutral-black) l c h / 0.8);
562562
}
563563

564564
.dialtone-icon-card__name {
@@ -571,7 +571,7 @@ code {
571571

572572
&:hover,
573573
.dialtone-icon-card[data-selected='yes'] & {
574-
background-color: hsl(0deg, 0%, 91.37%, 0.4);
574+
background-color: oklch(0.934 0 0deg / 0.4);
575575
}
576576

577577
.dialtone-icon-card__name {
@@ -584,7 +584,7 @@ code {
584584

585585
&:hover,
586586
.dialtone-icon-card[data-selected='yes'] & {
587-
background-color: hsl(var(--dt-color-black-900-h), var(--dt-color-black-900-s), var(--dt-color-black-900-l), 0.8);
587+
background-color: oklch(from var(--dt-color-black-900) l c h / 0.8);
588588
}
589589

590590
.dialtone-icon-card__name {
@@ -710,6 +710,7 @@ code {
710710
.dialtone-page-title {
711711
font: var(--dt-text-headline-3xl);
712712
margin-block-end: var(--dt-size-400);
713+
text-wrap: pretty;
713714
}
714715

715716
// ============================================================================

0 commit comments

Comments
 (0)