diff --git a/.changeset/green-planes-shine.md b/.changeset/green-planes-shine.md new file mode 100644 index 00000000000..9c6f0665cf3 --- /dev/null +++ b/.changeset/green-planes-shine.md @@ -0,0 +1,5 @@ +--- +'@kaizen/components': patch +--- + +add semantic colours diff --git a/.changeset/semantic-colour-tokens.md b/.changeset/semantic-colour-tokens.md new file mode 100644 index 00000000000..c4fbf38d258 --- /dev/null +++ b/.changeset/semantic-colour-tokens.md @@ -0,0 +1,9 @@ +--- +'@kaizen/design-tokens': minor +'@kaizen/tailwind': minor +'@kaizen/components': patch +--- + +feat: add semantic colour tokens (background, text, foreground, border) and expand the gray primitive ramp with `gray-550` and `gray-700`. Exposes the tokens as CSS variables (inlined into `variables.css`), SCSS, JS exports, and Tailwind preset utilities. + +`@kaizen/tailwind` depends on `@kaizen/design-tokens` (a regular runtime dependency, installed transitively): the semantic Tailwind utilities emit `var(--)` references whose definitions live in `@kaizen/design-tokens`. Consumers using the Tailwind preset do not need to install `@kaizen/design-tokens` themselves — they only need to load its CSS variables once at runtime (handled by `KaizenProvider`). diff --git a/.claude/skills/semantic-border-migration/SKILL.md b/.claude/skills/semantic-border-migration/SKILL.md new file mode 100644 index 00000000000..e25878a3caf --- /dev/null +++ b/.claude/skills/semantic-border-migration/SKILL.md @@ -0,0 +1,104 @@ +--- +name: semantic-border-migration +description: Migrate a component's border styles to the new semantic border tokens. Runs the migrateBorderColorsToSemanticTokens codemod first, then falls back to a guided manual migration only after the user confirms the codemod was incomplete. Use when asked to migrate borders to semantic tokens, apply border semantic tokens, or run the border token migration for a component. +--- + +## Semantic Border Migration + +Use this skill to migrate a component's borders from hard-coded **primitive** +colors — CSS `var(--color-gray-500)`, SCSS `$color-gray-500`, or Tailwind +`border-gray-500` — to the new **semantic +border tokens** (`--border-primary`, `--border-secondary`, `--border-secondary_alt`, +`--border-tertiary`, `--border-brand`, `--border-brand_alt`, `--border-error`, +`--border-error_subtle`). + +This runs in **two stages with a hard gate between them**. Stage 1 is the +deterministic codemod. Stage 2 is a guided manual migration that you only enter +**after the user has verified Stage 1 failed or is incomplete**. Never jump +straight to Stage 2, and never run Stage 2 automatically off the back of Stage 1. + +## Preconditions + +- Package manager: `pnpm`. Repo uses Changesets + Buildkite/GitHub Actions CI. +- `@kaizen/components` is installed (ships the `kaizen-codemod` bin). +- Know the **target component / directory** to migrate. +- If the consumer repo sets a Tailwind `prefix`, know its value (e.g. `goals-` + or `EP:`) so the codemod preserves it. + +## Reference + +- Codemod: [`migrateBorderColorsToSemanticTokens`](../../../packages/components/codemods/migrateBorderColorsToSemanticTokens/README.md) — the single source of truth for the confident mapping table and CLI usage. +- Semantic token definitions: [`semanticColorTokens.ts`](../../../packages/design-tokens/src/js/semanticColorTokens.ts) (the `border` group). + +--- + +## Stage 1 — Attempt the codemod (deterministic) + +1. **Confirm the target** directory/component with the user, and the Tailwind + prefix if any. +2. **Run the codemod:** + ```sh + pnpm kaizen-codemod migrateBorderColorsToSemanticTokens + ``` + If there is a Tailwind prefix, pass it via the env var: + ```sh + KAIZEN_TW_PREFIX=goals- pnpm kaizen-codemod migrateBorderColorsToSemanticTokens + ``` +3. **Tidy rewrites:** run prettier/lint over the changed files (e.g. + `pnpm prettier --write `), since AST/text rewrites may need reformatting. +4. **Surface the report.** The codemod prints a **`✅ Converted N`** count and, + crucially, a **`⚠️ SKIPPED`** list of border colors it could not confidently + map (`file:line — detail`). Show this to the user, along with the diff. +5. **STOP and ask the user to verify.** Explicitly ask: + + > Did Stage 1 fully migrate this component's borders, or are there skipped / + > incomplete / ambiguous cases still to resolve? + + Do **not** proceed. Only continue to Stage 2 if the user confirms Stage 1 is + incomplete or failed. If the user says it's complete, verify (below) and stop. + +--- + +## Stage 2 — Guided manual fallback (only after user verification) + +Enter this stage **only** when the user has confirmed Stage 1 left work behind. +Work through the codemod's `SKIPPED` report plus anything the user flagged. + +1. **Resolve each skipped border color.** For each entry, decide the correct + semantic border token using the intent of the border and the mappings in + [`semanticColorTokens.ts`](../../../packages/design-tokens/src/js/semanticColorTokens.ts). + Apply the change by hand (CSS `var(--color-*)` → `var(--border-*)`; SCSS + `$color-*` → `$border-*`, importing `@kaizen/design-tokens/sass/semantic` if + the `$border-*` var isn't already available; Tailwind `border-*` → the + semantic utility, preserving any prefix/variant). +2. **Handle what the codemod can't:** + - **No direct equivalent** (yellow / orange / green / purple borders, alpha / + `rgb()` usages, raw hex). These often need a **design decision** — do not + force a mapping. Propose the closest semantic token and flag it for + designer/user sign-off rather than guessing silently. + - **Dynamic / computed class names** (template literals, variables) the + codemod skipped. + - **Unusual selectors or non-standard files** not covered by the walk. +3. **Record decisions** for ambiguous cases (what you chose and why) so reviewers + can check them. + +--- + +## Verification (either stage) + +- **Tests:** `pnpm --filter @kaizen/components test` (or scope to the component). +- **Build:** `pnpm build`. +- **Visual:** Storybook + Chromatic to confirm no border regressions on the + migrated component. +- **Summarise** what was auto-migrated (Stage 1), what was hand-migrated + (Stage 2), and anything deferred for design sign-off. + +## Do / Don't + +- **Do** keep the change scoped to one component/directory and add a changeset if + it ships to consumers. +- **Do** preserve Tailwind prefixes and variant chains exactly. +- **Don't** enter Stage 2 without explicit user verification that Stage 1 is + incomplete. +- **Don't** guess a semantic token for a color with no confident mapping — surface + it for a design decision. diff --git a/.claude/skills/semantic-colours-migration/SKILL.md b/.claude/skills/semantic-colours-migration/SKILL.md new file mode 100644 index 00000000000..4b32806bfc0 --- /dev/null +++ b/.claude/skills/semantic-colours-migration/SKILL.md @@ -0,0 +1,126 @@ +--- +name: semantic-colours-migration +description: Migrate a component's colours (background, text, foreground/icon, and border) to the new semantic colour tokens. Runs the migratePrimitivesToSemanticTokens codemod first (deterministic), then falls back to a guided manual migration only after the user confirms the codemod was incomplete. Use when asked to migrate colours to semantic tokens, apply semantic colour tokens, or run the semantic colour token migration for a component. +--- + +## Semantic Colours Migration + +Use this skill to migrate a component's colours from hard-coded **primitives** — +CSS `var(--color-blue-500)`, SCSS `$color-blue-500`, or Tailwind `bg-blue-500` — +to the new **semantic colour tokens** across all four groups: + +- **background** → `--bg-*` (e.g. `bg-brand-solid`, `bg-tertiary`) +- **text** → `--text-*` (e.g. `text-primary`, `text-secondary`) +- **foreground** (icons / glyphs) → `--fg-*` (e.g. `fg-tertiary`, `fg-brand-primary`) +- **border** → `--border-*` (e.g. `border-secondary`, `border-brand`) + +This runs in **two stages with a hard gate between them**. Stage 1 is the +deterministic codemod. Stage 2 is a guided manual migration that you only enter +**after the user has verified Stage 1 failed or is incomplete**. Never jump +straight to Stage 2, and never run Stage 2 automatically off the back of Stage 1. + +> **Borders are handled here.** This skill and its codemod fully cover the +> `border-*` group alongside background/text/foreground — do **not** hand off to +> the separate `/semantic-border-migration` skill or run the +> `migrateBorderColorsToSemanticTokens` codemod. Those were the reference/basis +> this skill and codemod were built from; treat them as prior art only. + +## Preconditions + +- Package manager: `pnpm`. Repo uses Changesets + Buildkite/GitHub Actions CI. +- `@kaizen/components` is installed (ships the `kaizen-codemod` bin). +- Know the **target component / directory** to migrate. +- If the consumer repo sets a Tailwind `prefix`, know its value (e.g. `goals-` + or `EP:`) so the codemod preserves it. + +## Reference + +- Codemod: [`migratePrimitivesToSemanticTokens`](../../../packages/components/codemods/migratePrimitivesToSemanticTokens/README.md) — the single source of truth for the confident mapping, group-context rules, and CLI usage. +- Semantic token definitions: [`semanticColorTokens.ts`](../../../packages/design-tokens/src/js/semanticColorTokens.ts) (the `background`, `text`, `foreground`, `border` groups). +- Known follow-ups / outliers: [`TODO.md`](../../../packages/components/codemods/migratePrimitivesToSemanticTokens/TODO.md). +- Prior art (reference only — **not** to be run from this skill): the border-only [`/semantic-border-migration`](../semantic-border-migration/SKILL.md) skill and [`migrateBorderColorsToSemanticTokens`](../../../packages/components/codemods/migrateBorderColorsToSemanticTokens/README.md) codemod, whose two-stage design and border mappings this skill/codemod are built on and fully supersede. + +--- + +## Stage 1 — Attempt the codemod (deterministic) + +Stage 1 is **deterministic**: it only applies confident 1:1 primitive→semantic +mappings and reports everything else. Do not hand-edit colours in this stage. + +1. **Confirm the target** directory/component with the user, and the Tailwind + prefix if any. +2. **Run the codemod:** + ```sh + pnpm kaizen-codemod migratePrimitivesToSemanticTokens + ``` + If there is a Tailwind prefix, pass it via the env var: + ```sh + KAIZEN_TW_PREFIX=goals- pnpm kaizen-codemod migratePrimitivesToSemanticTokens + ``` +3. **Tidy rewrites:** run prettier/lint over the changed files (e.g. + `pnpm prettier --write `), since AST/text rewrites may need reformatting. +4. **Surface the report.** The codemod prints a **`✅ Converted N`** count and, + crucially, a **`⚠️ SKIPPED`** list of colours it could not confidently map + (`file:line — detail`). Show this to the user, along with the diff. +5. **STOP and ask the user to verify.** Explicitly ask: + + > Did Stage 1 fully migrate this component's colours, or are there skipped / + > incomplete / ambiguous cases still to resolve? + + Do **not** proceed. Only continue to Stage 2 if the user confirms Stage 1 is + incomplete or failed. If the user says it's complete, verify (below) and stop. + +--- + +## Stage 2 — Guided manual fallback (only after user verification) + +Enter this stage **only** when the user has confirmed Stage 1 left work behind. +Work through the codemod's `SKIPPED` report plus anything the user flagged, and +**report each missed instance back to the user** with the decision you propose. + +1. **Resolve each skipped colour.** For each entry, decide the correct semantic + token using the intent of the surface (background / text / icon / border) and + the mappings in + [`semanticColorTokens.ts`](../../../packages/design-tokens/src/js/semanticColorTokens.ts). + Apply the change by hand (CSS `var(--color-*)` → `var(--)`; SCSS + `$color-*` → `$`, importing `@kaizen/design-tokens/sass/semantic-color` + if the semantic var isn't already available; Tailwind primitive utility → the + semantic utility, preserving any prefix/variant). +2. **Handle what the codemod can't (report, don't guess):** + - **Colliding primitives** — one primitive backing several tokens in a group + (e.g. `gray-200` → `bg-secondary` _or_ `bg-secondary_hover`). Pick the token + that matches the **intent** (resting vs hover/`_alt` state) and note why. + - **No direct equivalent** (colours with no mapping, alpha / `rgb()` usages, + raw hex, `null` tokens not yet signed off). These often need a **design + decision** — propose the closest semantic token and flag it for + designer/user sign-off rather than forcing a mapping. + - **Dynamic / computed class names** (template literals, variables) the codemod + skipped. + - **Out-of-scope properties** (`box-shadow`, `outline-color`, gradients) the + codemod does not touch. +3. **Report the missed instances back.** Summarise every skipped case as + `file:line — original → proposed (rationale / needs sign-off)` so the user and + reviewers can check each decision. + +--- + +## Verification (either stage) + +- **Tests:** `pnpm --filter @kaizen/components test` (or scope to the component). +- **Build:** `pnpm build`. +- **Visual:** Storybook + Chromatic to confirm no colour regressions on the + migrated component (check background, text, icon, and border surfaces, and + hover/focus states). +- **Summarise** what was auto-migrated (Stage 1), what was hand-migrated + (Stage 2), and anything deferred for design sign-off. + +## Do / Don't + +- **Do** keep the change scoped to one component/directory and add a changeset if + it ships to consumers. +- **Do** preserve Tailwind prefixes and variant chains exactly. +- **Do** report every skipped instance in Stage 2 with a proposed mapping. +- **Don't** enter Stage 2 without explicit user verification that Stage 1 is + incomplete. +- **Don't** guess a semantic token for a colliding or unmapped colour — surface + it for a design decision. diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index db4f3f22fd1..970f976c722 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -16,7 +16,13 @@ runs: - name: Cache pnpm store uses: actions/setup-node@v4 with: - node-version-file: package.json + # Pin to .nvmrc (single version) instead of the package.json `engines` + # range. A range lets setup-node float to the latest available Node, + # which after the 2026-06-18 security batch is a poisoned release + # (24.17.0 / 26.3.1) that makes node-fetch@2 throw a false + # ERR_STREAM_PREMATURE_CLOSE — breaking `changeset version`'s GitHub + # GraphQL changelog fetch. See nodejs/node#63989. + node-version-file: .nvmrc registry-url: https://npm.pkg.github.com/ cache: 'pnpm' diff --git a/.stylelintrc-css.mjs b/.stylelintrc-css.mjs index a5cd4c48d32..7eab76ceba8 100644 --- a/.stylelintrc-css.mjs +++ b/.stylelintrc-css.mjs @@ -4,6 +4,17 @@ export default { extends: 'stylelint-config-standard', rules: { 'selector-class-pattern': null, + // Semantic colour tokens use a single `_` to introduce a state/variant + // segment (e.g. `--border-brand_alt`, `--bg-secondary_hover`, + // `--text-secondary_on-brand`). Allow one such underscore-separated variant + // group while still enforcing kebab-case within each segment. + 'custom-property-pattern': [ + '^([a-z][a-z0-9]*)(-[a-z0-9]+)*(_[a-z0-9]+(-[a-z0-9]+)*)?$', + { + message: (name) => + `Expected custom property name "${name}" to be kebab-case with an optional _variant suffix`, + }, + ], 'color-function-notation': ['modern', { ignore: ['with-var-inside'] }], 'font-family-no-missing-generic-family-keyword': [ true, diff --git a/.stylelintrc-scss.json b/.stylelintrc-scss.json index db6c98edfdc..0c0ecffd6e9 100644 --- a/.stylelintrc-scss.json +++ b/.stylelintrc-scss.json @@ -18,6 +18,7 @@ ], "block-no-empty": true, "color-function-notation": "legacy", + "custom-property-pattern": "^([a-z][a-z0-9]*)(-[a-z0-9]+)*(_[a-z0-9]+(-[a-z0-9]+)*)?$", "comment-empty-line-before": [ "always", { diff --git a/packages/components/codemods/README.md b/packages/components/codemods/README.md index ea16972860d..950ab7ec10b 100644 --- a/packages/components/codemods/README.md +++ b/packages/components/codemods/README.md @@ -25,6 +25,10 @@ pnpm kaizen-codemod src migrateWellVariantToColor ## Available codemods +### `migrateBorderColorsToSemanticTokens` + +Migrates hard-coded primitive border colors to the new semantic border tokens across CSS, SCSS, and Tailwind, via three transformers dispatched by file extension: CSS custom properties (`var(--color-gray-500)` → `var(--border-secondary)`), SCSS variables (`$color-gray-500` → `$border-secondary`, adding the `@kaizen/design-tokens/sass/semantic` import when needed), and Tailwind utilities (`border-gray-500` → `border-secondary`). Only confident border-context mappings are rewritten; ambiguous/unmapped colors and alpha/`rgb()` usages (e.g. `rgba($color-gray-600-rgb, 0.1)`) are skipped and reported. Honours a consumer Tailwind `prefix` via `KAIZEN_TW_PREFIX`. See the [README](./migrateBorderColorsToSemanticTokens/README.md), and pair it with the `/semantic-border-migration` skill for the stage-2 fallback. + Released in `1.80.6` ### `runV1Codemods` diff --git a/packages/components/codemods/migrateBorderColorsToSemanticTokens/README.md b/packages/components/codemods/migrateBorderColorsToSemanticTokens/README.md new file mode 100644 index 00000000000..9c6fae5f8aa --- /dev/null +++ b/packages/components/codemods/migrateBorderColorsToSemanticTokens/README.md @@ -0,0 +1,162 @@ +# `migrateBorderColorsToSemanticTokens` + +Migrates hard-coded **primitive border colors** to the new **semantic border +tokens** across CSS, SCSS, and Tailwind. It is the deterministic first stage of +the border migration; the companion [`/semantic-border-migration`](../../../../.claude/skills/semantic-border-migration/SKILL.md) +skill runs this codemod and then guides a manual fallback for anything it +deliberately skips. + +## Prerequisites + +- Install `@kaizen/components` (the `kaizen-codemod` bin ships with the package). + +## Usage + +``` +pnpm kaizen-codemod {DIR} migrateBorderColorsToSemanticTokens +``` + +- `{DIR}` — directory to scan (e.g. `src`, or a single component folder to scope + to one component). `node_modules` is excluded. +- After it runs, **run prettier/lint** to tidy any rewritten lines. +- Review the printed **`SKIPPED`** report and resolve the remaining cases with + the `/semantic-border-migration` skill (stage 2). + +### Tailwind prefix + +If your repo sets a Tailwind `prefix`, tell the codemod so prefixed border +utilities are matched and the prefix is **preserved** on rewrite. The codemod +resolves the prefix from (in order): a `--prefix=` arg, the `KAIZEN_TW_PREFIX` +env var, or a `tailwind.config.*` in the current directory. + +``` +# utility prefix (Tailwind `prefix: 'goals-'`) +KAIZEN_TW_PREFIX=goals- pnpm kaizen-codemod src migrateBorderColorsToSemanticTokens +# goals-border-gray-500 → goals-border-secondary + +# colon namespace prefix (e.g. Employee Profile) +KAIZEN_TW_PREFIX=EP: pnpm kaizen-codemod src migrateBorderColorsToSemanticTokens +# EP:border-gray-500 → EP:border-secondary +``` + +Stacked variants (`hover:`, `md:`) are always preserved automatically. + +## How it works + +The codemod walks `{DIR}` (excluding `node_modules`) and dispatches each file to +one of three transformers by extension. They share the confident mapping and +skip/report rules, but each handles the reference styles native to its language: + +| Files | Transformer | Reference styles handled | +| --------------- | -------------------------------- | ----------------------------------------------------------------------- | +| `.css` | `transformCssBorderColors` | CSS custom property `var(--color-*)` | +| `.scss` | `transformScssBorderColors` | SCSS variable `$color-*` **and** CSS custom property `var(--color-*)` | +| `.tsx/.jsx/.ts` | `transformTailwindBorderClasses` | Tailwind border utilities in `className`/`class` and `clsx`-style calls | + +CSS/SCSS use a `postcss` AST (`postcss-scss` for `.scss`); Tailwind uses the +TypeScript compiler API. Only file writes happen for files with at least one +conversion. + +## What it converts + +Only **confident 1:1 mappings in a border context** are rewritten. Anything +else is left untouched and reported for stage 2. + +| Primitive | Semantic token | CSS / SCSS `var(--color-*)` | SCSS `$color-*` | Tailwind `border-*` | +| ---------- | ---------------------- | --------------------------------------------------- | --------------------------------------- | -------------------------------------- | +| `gray-600` | `border-primary` | `var(--color-gray-600)` → `var(--border-primary)` | `$color-gray-600` → `$border-primary` | `border-gray-600` → `border-primary` | +| `gray-500` | `border-secondary` | `var(--color-gray-500)` → `var(--border-secondary)` | `$color-gray-500` → `$border-secondary` | `border-gray-500` → `border-secondary` | +| `gray-200` | `border-secondary_alt` | … | … | … | +| `gray-300` | `border-tertiary` | … | … | … | +| `blue-500` | `border-brand` | … | … | … | +| `blue-300` | `border-brand_alt` | … | … | … | +| `red-500` | `border-error` | … | … | … | +| `red-300` | `border-error_subtle` | … | … | … | + +**Border context** = the `border` shorthand, per-side/logical shorthands +(`border-top`, `border-inline-start`, …), the `*-color` longhands, or a custom +property whose name contains `border` (e.g. `--border-color`). Non-border uses +of the same primitive (e.g. `background: var(--color-gray-500)` or +`background: $color-gray-500`) are never touched. + +### SCSS specifics + +- Consumer SCSS usually references Kaizen colors as **SCSS variables** + (`$color-gray-500`, imported from `@kaizen/design-tokens/sass/color`), so those + are matched in addition to raw `var(--color-*)`. Solid `$color-*` borders are + rewritten to the semantic SCSS variable (`$border-secondary`, …). +- Because `$border-*` is defined in `@kaizen/design-tokens/sass/semantic`, the + codemod **adds that import** when it introduces a `$border-*` variable and the + file doesn't already import it — cloning the style (`~` prefix / quotes) of an + existing kaizen sass import so the addition matches your convention. +- **Namespaced** refs (`@use` → `color.$color-*`) are left for stage 2 rather + than rewritten with an assumed namespace. + +### What is skipped (reported, never guessed) + +- Colors with no confident semantic border equivalent (`white`, `yellow`, + `orange`, `green`, `purple`, `blue-400`, `gray-400`, raw hex …). +- **Alpha / `rgb()` / `rgba()` usages**, e.g. `rgba($color-gray-600-rgb, 0.1)` or + the `$color-*-rgb` helpers — the semantic tokens are solid colors, so these + need a design decision. +- Dynamic/computed Tailwind class names (template literals, variables). + +## Examples + +**CSS** + +```css +/* before */ +.card { + border: var(--border-width-1) solid var(--color-gray-500); +} +.error { + --border-color: var(--color-red-500); +} +/* after */ +.card { + border: var(--border-width-1) solid var(--border-secondary); +} +.error { + --border-color: var(--border-error); +} +``` + +**SCSS** (note the injected semantic import and the skipped alpha border) + +```scss +/* before */ +@import '~@kaizen/design-tokens/sass/color'; +.card { + border: 1px solid $color-gray-500; +} +.footer { + border-top: 1px solid rgba($color-gray-600-rgb, 0.1); /* skipped — design decision */ +} +/* after */ +@import '~@kaizen/design-tokens/sass/color'; +@import '~@kaizen/design-tokens/sass/semantic'; +.card { + border: 1px solid $border-secondary; +} +.footer { + border-top: 1px solid rgba($color-gray-600-rgb, 0.1); /* left for stage 2 */ +} +``` + +**Tailwind** + +```tsx +// before +
+// after +
+``` + +## Source of truth + +Mappings mirror the `border` group in +[`packages/design-tokens/src/js/semanticColorTokens.ts`](../../../design-tokens/src/js/semanticColorTokens.ts) +and live in [`borderTokenMap.ts`](./borderTokenMap.ts). The CSS and SCSS +transformers share the postcss engine in +[`borderColorPostcss.ts`](./borderColorPostcss.ts). diff --git a/packages/components/codemods/migrateBorderColorsToSemanticTokens/borderColorPostcss.ts b/packages/components/codemods/migrateBorderColorsToSemanticTokens/borderColorPostcss.ts new file mode 100644 index 00000000000..2fc8258efbe --- /dev/null +++ b/packages/components/codemods/migrateBorderColorsToSemanticTokens/borderColorPostcss.ts @@ -0,0 +1,137 @@ +import postcss from 'postcss' +import { + BORDER_COLOR_MAP, + COLOR_VAR_REFERENCE, + SCSS_COLOR_VAR_REFERENCE, + hasAlphaColorUsage, + isBorderColorProperty, + referencesPrimitiveColor, +} from './borderTokenMap' + +/** + * Shared postcss engine for the CSS and SCSS border-color transformers. Both + * public transformers ({@link transformCssBorderColors}, + * {@link transformScssBorderColors}) parse with their own syntax and delegate + * the declaration rewriting here so the border-context rules, confident mapping, + * and skip/report behaviour stay identical across CSS and SCSS. + */ + +export type ConvertedToken = { + line: number + property: string + from: string + to: string +} + +export type SkippedToken = { + line: number + property: string + value: string +} + +export type BorderColorResult = { + converted: ConvertedToken[] + skipped: SkippedToken[] +} + +/** Path to the semantic sass partial that defines the `$border-*` variables. */ +const SEMANTIC_SASS_MODULE = '@kaizen/design-tokens/sass/semantic' + +/** + * Ensures the semantic sass partial is imported once a `$border-*` variable has + * been introduced, otherwise the rewritten SCSS would reference an undefined + * variable and fail to compile. Clones the style (quote + `~` prefix) of an + * existing `@kaizen/design-tokens/sass/*` import so the added line matches the + * file's convention; falls back to a plain `@import` when no kaizen import + * exists. No-op when the semantic partial is already imported/used. + */ +const ensureSemanticImport = (root: postcss.Root): void => { + let kaizenImport: postcss.AtRule | undefined + let alreadyImported = false + + root.walkAtRules((atRule) => { + if (atRule.name !== 'import' && atRule.name !== 'use') return + if (atRule.params.includes('design-tokens/sass/semantic')) alreadyImported = true + if (!kaizenImport && atRule.params.includes('design-tokens/sass/')) kaizenImport = atRule + }) + + if (alreadyImported) return + + if (kaizenImport) { + const params = kaizenImport.params.replace(/(design-tokens\/sass\/)[a-z-]+/i, `$1semantic`) + // Clone the existing import's own-line formatting so the addition sits on its + // own line rather than being appended to the source import. + kaizenImport.cloneAfter({ params, raws: { ...kaizenImport.raws, before: '\n' } }) + return + } + + root.prepend(postcss.atRule({ name: 'import', params: `'${SEMANTIC_SASS_MODULE}'` })) +} + +/** + * Walks every declaration in `root`, rewriting hard-coded primitive border + * colors to the new semantic border tokens. When `scss` is true, SCSS + * `$color-*` variables are also rewritten (to `$border-*`) and the semantic sass + * import is injected if needed. + * + * Only confident 1:1 mappings in a border context are rewritten (see + * {@link BORDER_COLOR_MAP}). Unmapped border colors, and alpha/`rgb()`/`rgba()` + * usages such as `rgba($color-gray-600-rgb, 0.1)`, are left untouched and + * returned in `skipped` for the stage-2 fallback. Non-border usages of the same + * primitives (e.g. `background: var(--color-gray-500)`) are never touched. + */ +export const rewriteBorderColors = ( + root: postcss.Root, + { scss = false }: { scss?: boolean } = {}, +): BorderColorResult => { + const converted: ConvertedToken[] = [] + const skipped: SkippedToken[] = [] + let introducedScssSemantic = false + + root.walkDecls((decl) => { + if (!isBorderColorProperty(decl.prop)) return + if (!referencesPrimitiveColor(decl.value)) return + + const line = decl.source?.start?.line ?? 0 + + // Alpha / rgb() usages are solid-color-only in the semantic set — flag for a + // design decision (stage 2) instead of guessing, and don't touch the value. + if (hasAlphaColorUsage(decl.value)) { + skipped.push({ line, property: decl.prop, value: decl.value.trim() }) + return + } + + // CSS custom property references: var(--color-X) → var(--border-Y). + let value = decl.value.replace(COLOR_VAR_REFERENCE, (match, primitive: string) => { + const semantic = BORDER_COLOR_MAP[primitive] + if (semantic) { + const replacement = `var(--${semantic})` + converted.push({ line, property: decl.prop, from: match, to: replacement }) + return replacement + } + skipped.push({ line, property: decl.prop, value: match }) + return match + }) + + // SCSS variable references: $color-X → $border-Y (SCSS files only). + if (scss) { + value = value.replace(SCSS_COLOR_VAR_REFERENCE, (match, primitive: string) => { + const semantic = BORDER_COLOR_MAP[primitive] + if (semantic) { + const replacement = `$${semantic}` + converted.push({ line, property: decl.prop, from: match, to: replacement }) + introducedScssSemantic = true + return replacement + } + skipped.push({ line, property: decl.prop, value: match }) + return match + }) + } + + decl.value = value + }) + + if (introducedScssSemantic) ensureSemanticImport(root) + + return { converted, skipped } +} diff --git a/packages/components/codemods/migrateBorderColorsToSemanticTokens/borderTokenMap.ts b/packages/components/codemods/migrateBorderColorsToSemanticTokens/borderTokenMap.ts new file mode 100644 index 00000000000..4f6ae8ce686 --- /dev/null +++ b/packages/components/codemods/migrateBorderColorsToSemanticTokens/borderTokenMap.ts @@ -0,0 +1,95 @@ +/** + * Confident primitive → semantic border-token mappings for the border codemod. + * + * Source of truth: `packages/design-tokens/src/js/semanticColorTokens.ts` (the + * `border` group). That file maps a semantic token → the primitive it paints + * with; here we invert it (primitive → semantic) because the codemod rewrites + * hard-coded primitives to the new semantic token. + * + * We ONLY include mappings that are safe to apply automatically in a border + * context. Ambiguous or unmapped primitives (e.g. `gray-400`, `blue-400`, + * `white`, and any `yellow`/`orange`/`green`/`purple` used on a border) have no + * confident 1:1 semantic border token, so they are intentionally omitted — the + * codemod skips and reports them for the `/semantic-border-migration` stage-2 + * fallback rather than guessing. + */ + +/** primitive color-token name (e.g. `gray-500`) → semantic border-token name (e.g. `border-secondary`) */ +export const BORDER_COLOR_MAP: Record = { + 'gray-600': 'border-primary', + 'gray-500': 'border-secondary', + 'gray-200': 'border-secondary_alt', + 'gray-300': 'border-tertiary', + 'blue-500': 'border-brand', + 'blue-300': 'border-brand_alt', + 'red-500': 'border-error', + 'red-300': 'border-error_subtle', +} + +/** + * Tailwind border utility (e.g. `border-gray-500`) → semantic border utility + * (e.g. `border-secondary`). Derived from {@link BORDER_COLOR_MAP}; the semantic + * token name already starts with `border-`, so it doubles as the utility class. + */ +export const TAILWIND_BORDER_MAP: Record = Object.fromEntries( + Object.entries(BORDER_COLOR_MAP).map(([primitive, semantic]) => [ + `border-${primitive}`, + semantic, + ]), +) + +/** + * Matches CSS properties that can carry a border *color*: the `border` + * shorthand, per-side/logical shorthands, and the explicit `*-color` longhands. + * Deliberately excludes `border-width`/`border-style`/`border-radius`/ + * `border-image`/`border-spacing`/`border-collapse`, which never hold a color. + */ +const BORDER_COLOR_PROPERTY = + /^border(-(top|right|bottom|left|block|inline)(-(start|end))?)?(-color)?$/ + +/** + * True when a declaration property can hold a border color — either a standard + * border color property, or a custom property whose name mentions `border` + * (e.g. `--border-color`, `--border-hover-color`). + */ +export const isBorderColorProperty = (property: string): boolean => { + const prop = property.trim().toLowerCase() + if (prop.startsWith('--')) return prop.includes('border') + return BORDER_COLOR_PROPERTY.test(prop) +} + +/** Matches a `var(--color-)` reference and captures the primitive name. */ +export const COLOR_VAR_REFERENCE = /var\(\s*--color-([a-z0-9-]+)\s*\)/gi + +/** + * Matches a solid `$color--` SCSS variable reference (the form + * consumers get from `@import '@kaizen/design-tokens/sass/color'`) and captures + * the primitive name. Deliberately does NOT match the `-rgb`/`-id` companions + * (`$color-gray-600-rgb`) — those are alpha/identifier helpers with no solid + * semantic border equivalent. A leading `\w`/`.`/`-` is disallowed so namespaced + * (`color.$color-…`) refs are left for stage 2 rather than mis-rewritten. + */ +export const SCSS_COLOR_VAR_REFERENCE = /(? + /var\(\s*--color-/i.test(value) || /(? + /\brgba?\(/i.test(value) || /\$color-[a-z0-9-]+-rgb\b/i.test(value) + +/** + * Heuristic for a Tailwind border-*color* utility so unmapped ones can be + * reported (skipped) rather than silently ignored. Matches + * `border--` and arbitrary values like `border-[var(--color-…)]`. + * Excludes structural utilities (`border-2`, `border-t`, `border-solid`) and + * already-semantic classes (`border-primary`). + */ +export const TAILWIND_BORDER_COLOR_UTILITY = + /^border-(gray|blue|red|green|yellow|orange|purple|teal|pink|white|black)(-\d+)?$|^border-\[.+\]$/i diff --git a/packages/components/codemods/migrateBorderColorsToSemanticTokens/index.ts b/packages/components/codemods/migrateBorderColorsToSemanticTokens/index.ts new file mode 100644 index 00000000000..c7df4bc8f81 --- /dev/null +++ b/packages/components/codemods/migrateBorderColorsToSemanticTokens/index.ts @@ -0,0 +1,109 @@ +import fs from 'fs' +import path from 'path' +import ts from 'typescript' +import { transformCssBorderColors } from './transformCssBorderColors' +import { transformScssBorderColors } from './transformScssBorderColors' +import { transformTailwindBorderClasses } from './transformTailwindBorderClasses' + +type SkipReport = { file: string; line: number; detail: string } + +/** + * Resolves the consumer's Tailwind prefix. + * + * The `kaizen-codemod` wrapper only forwards the target dir, so the prefix is + * taken from (in priority order): a `--prefix=` CLI arg (when run directly via + * tsx), the `KAIZEN_TW_PREFIX` env var, or a best-effort read of a + * `tailwind.config.*` in the current working directory. + */ +const resolveTailwindPrefix = (): string => { + const argPrefix = process.argv.find((arg) => arg.startsWith('--prefix=')) + if (argPrefix) return argPrefix.slice('--prefix='.length) + if (process.env.KAIZEN_TW_PREFIX) return process.env.KAIZEN_TW_PREFIX + + const configName = [ + 'tailwind.config.ts', + 'tailwind.config.js', + 'tailwind.config.cjs', + 'tailwind.config.mjs', + ].find((name) => fs.existsSync(path.join(process.cwd(), name))) + if (configName) { + try { + const contents = fs.readFileSync(path.join(process.cwd(), configName), 'utf8') + const match = /prefix:\s*['"`]([^'"`]+)['"`]/.exec(contents) + if (match) return match[1] + } catch { + // best effort only — fall through to no prefix + } + } + return '' +} + +const traverseDir = (dir: string, onFile: (filePath: string) => void): void => { + if (dir.includes('node_modules')) return + for (const entry of fs.readdirSync(dir)) { + const fullPath = path.join(dir, entry) + if (fs.statSync(fullPath).isDirectory()) { + traverseDir(fullPath, onFile) + } else { + onFile(fullPath) + } + } +} + +const run = (): void => { + console.log(' ~(-_- ~) Running border colors → semantic tokens transformer (~ -_-)~') + const targetDir = process.argv[2] + if (!targetDir || targetDir.startsWith('--')) { + console.error('Usage: kaizen-codemod migrateBorderColorsToSemanticTokens') + process.exit(1) + } + + const prefix = resolveTailwindPrefix() + if (prefix) console.log(`Using Tailwind prefix: "${prefix}"`) + + let convertedCount = 0 + const skips: SkipReport[] = [] + + traverseDir(targetDir, (filePath) => { + const relative = path.relative(process.cwd(), filePath) + + if (filePath.endsWith('.scss') || filePath.endsWith('.css')) { + const source = fs.readFileSync(filePath, 'utf8') + const { code, converted, skipped } = filePath.endsWith('.scss') + ? transformScssBorderColors(source) + : transformCssBorderColors(source) + if (converted.length > 0) fs.writeFileSync(filePath, code, 'utf8') + convertedCount += converted.length + skipped.forEach((s) => + skips.push({ file: relative, line: s.line, detail: `${s.property}: ${s.value}` }), + ) + return + } + + if (/\.(tsx|jsx|ts)$/.test(filePath)) { + const source = fs.readFileSync(filePath, 'utf8') + const scriptKind = filePath.endsWith('.ts') ? ts.ScriptKind.TS : ts.ScriptKind.TSX + const { code, converted, skipped } = transformTailwindBorderClasses(source, { + prefix, + scriptKind, + }) + if (converted.length > 0) fs.writeFileSync(filePath, code, 'utf8') + convertedCount += converted.length + skipped.forEach((s) => skips.push({ file: relative, line: s.line, detail: s.token })) + } + }) + + console.log('') + console.log(`✅ Converted ${convertedCount} border color token(s) to semantic tokens.`) + + if (skips.length > 0) { + console.log('') + console.log(`⚠️ SKIPPED ${skips.length} border color(s) with no confident semantic mapping.`) + console.log(' Resolve these with the /semantic-border-migration skill (stage 2):') + skips.forEach((s) => console.log(` - ${s.file}:${s.line} — ${s.detail}`)) + } else { + console.log('No ambiguous border colors detected — stage 2 may not be needed.') + } +} + +run() diff --git a/packages/components/codemods/migrateBorderColorsToSemanticTokens/transformCssBorderColors.spec.ts b/packages/components/codemods/migrateBorderColorsToSemanticTokens/transformCssBorderColors.spec.ts new file mode 100644 index 00000000000..5d79c871c80 --- /dev/null +++ b/packages/components/codemods/migrateBorderColorsToSemanticTokens/transformCssBorderColors.spec.ts @@ -0,0 +1,58 @@ +import { transformCssBorderColors } from './transformCssBorderColors' + +describe('transformCssBorderColors()', () => { + it('swaps only the color token in a border shorthand', () => { + const input = `.a { + border: var(--border-solid-border-width) var(--border-solid-border-style) var(--color-gray-500); +}` + const { code, converted } = transformCssBorderColors(input) + expect(code).toContain( + 'border: var(--border-solid-border-width) var(--border-solid-border-style) var(--border-secondary);', + ) + expect(converted).toHaveLength(1) + expect(converted[0]).toMatchObject({ to: 'var(--border-secondary)' }) + }) + + it('rewrites border-color longhand', () => { + const { code } = transformCssBorderColors('.a { border-color: var(--color-gray-600); }') + expect(code).toContain('border-color: var(--border-primary);') + }) + + it('rewrites a border-related custom property', () => { + const { code } = transformCssBorderColors('.a { --border-color: var(--color-blue-500); }') + expect(code).toContain('--border-color: var(--border-brand);') + }) + + it('rewrites per-side and logical border color properties', () => { + const { code } = transformCssBorderColors( + '.a { border-bottom-color: var(--color-red-500); border-inline-start: 1px solid var(--color-red-300); }', + ) + expect(code).toContain('border-bottom-color: var(--border-error);') + expect(code).toContain('border-inline-start: 1px solid var(--border-error_subtle);') + }) + + it('leaves non-border usages of the same primitive untouched', () => { + const input = '.a { background: var(--color-gray-500); color: var(--color-gray-600); }' + const { code, converted } = transformCssBorderColors(input) + expect(code).toBe(input) + expect(converted).toHaveLength(0) + }) + + it('skips and reports border colors with no confident semantic mapping', () => { + const { code, converted, skipped } = transformCssBorderColors( + '.a { border-color: var(--color-yellow-700); }', + ) + expect(code).toContain('border-color: var(--color-yellow-700);') + expect(converted).toHaveLength(0) + expect(skipped).toHaveLength(1) + expect(skipped[0]).toMatchObject({ property: 'border-color', value: 'var(--color-yellow-700)' }) + }) + + it('does not touch border-width / border-style / border-radius', () => { + const input = + '.a { border-width: var(--color-gray-500); border-radius: var(--color-gray-500); }' + // These properties never carry a color; even if a color var appears we leave it. + const { converted } = transformCssBorderColors(input) + expect(converted).toHaveLength(0) + }) +}) diff --git a/packages/components/codemods/migrateBorderColorsToSemanticTokens/transformCssBorderColors.ts b/packages/components/codemods/migrateBorderColorsToSemanticTokens/transformCssBorderColors.ts new file mode 100644 index 00000000000..21918913540 --- /dev/null +++ b/packages/components/codemods/migrateBorderColorsToSemanticTokens/transformCssBorderColors.ts @@ -0,0 +1,27 @@ +import postcss from 'postcss' +import { + rewriteBorderColors, + type BorderColorResult, + type ConvertedToken, + type SkippedToken, +} from './borderColorPostcss' + +export type { ConvertedToken, SkippedToken } + +export type CssTransformResult = BorderColorResult & { code: string } + +/** + * Rewrites hard-coded primitive border colors to the new semantic border tokens + * in plain **CSS**, matching CSS custom property references: + * `border-color: var(--color-gray-500)` → `border-color: var(--border-secondary)` + * + * SCSS `$variable` references are handled by {@link transformScssBorderColors}. + * Only confident 1:1 mappings in a border context are rewritten; unmapped and + * alpha/`rgb()` border colors are left untouched and returned in `skipped` for + * the stage-2 fallback. + */ +export const transformCssBorderColors = (source: string): CssTransformResult => { + const root = postcss.parse(source) + const { converted, skipped } = rewriteBorderColors(root, { scss: false }) + return { code: root.toString(), converted, skipped } +} diff --git a/packages/components/codemods/migrateBorderColorsToSemanticTokens/transformScssBorderColors.spec.ts b/packages/components/codemods/migrateBorderColorsToSemanticTokens/transformScssBorderColors.spec.ts new file mode 100644 index 00000000000..86a238346e4 --- /dev/null +++ b/packages/components/codemods/migrateBorderColorsToSemanticTokens/transformScssBorderColors.spec.ts @@ -0,0 +1,86 @@ +import { transformScssBorderColors } from './transformScssBorderColors' + +describe('transformScssBorderColors()', () => { + it('rewrites SCSS $color-* variables to the semantic $border-* variable', () => { + const input = `@import "~@kaizen/design-tokens/sass/color"; +.a { + border: 1px solid $color-gray-500; +}` + const { code, converted } = transformScssBorderColors(input) + expect(code).toContain('border: 1px solid $border-secondary;') + expect(converted).toHaveLength(1) + expect(converted[0]).toMatchObject({ from: '$color-gray-500', to: '$border-secondary' }) + }) + + it('rewrites CSS custom property references inside SCSS too', () => { + const input = `.a { + &:hover { border-color: var(--color-gray-500); } +}` + const { code } = transformScssBorderColors(input) + expect(code).toContain('border-color: var(--border-secondary);') + }) + + it('preserves nested rules and unrelated $variables', () => { + const input = `$x: 1; +.a { + &:hover { border-color: $color-gray-600; } +}` + const { code } = transformScssBorderColors(input) + expect(code).toContain('border-color: $border-primary;') + expect(code).toContain('$x: 1;') + }) + + it('adds the semantic sass import (matching existing import style) when introducing a $border-* var', () => { + const input = `@import "~@kaizen/design-tokens/sass/color"; +.a { + border-color: $color-gray-600; +}` + const { code } = transformScssBorderColors(input) + expect(code).toContain('@import "~@kaizen/design-tokens/sass/semantic";') + expect(code).toContain('border-color: $border-primary;') + }) + + it('prepends a plain semantic import when no kaizen sass import exists', () => { + const { code } = transformScssBorderColors('.a { border-color: $color-blue-500; }') + expect(code).toContain("@import '@kaizen/design-tokens/sass/semantic';") + expect(code).toContain('border-color: $border-brand;') + }) + + it('does not duplicate the semantic import when already present', () => { + const input = `@import "~@kaizen/design-tokens/sass/color"; +@import "~@kaizen/design-tokens/sass/semantic"; +.a { + border-color: $color-gray-600; +}` + const { code } = transformScssBorderColors(input) + expect(code.match(/sass\/semantic/g)).toHaveLength(1) + }) + + it('skips and reports rgba()/alpha border colors for a design decision', () => { + const input = `@import "~@kaizen/design-tokens/sass/color"; +.footer { + border-top: 1px solid rgba($color-gray-600-rgb, 0.1); +}` + const { code, converted, skipped } = transformScssBorderColors(input) + expect(code).toContain('border-top: 1px solid rgba($color-gray-600-rgb, 0.1);') + expect(code).not.toContain('$border-') + expect(converted).toHaveLength(0) + expect(skipped).toHaveLength(1) + expect(skipped[0]).toMatchObject({ property: 'border-top' }) + }) + + it('skips and reports unmapped $color-* border variables', () => { + const input = '.a { border-color: $color-gray-400; }' + const { code, converted, skipped } = transformScssBorderColors(input) + expect(code).toContain('border-color: $color-gray-400;') + expect(converted).toHaveLength(0) + expect(skipped).toHaveLength(1) + }) + + it('leaves non-border $color-* usages untouched', () => { + const input = '.a { background: $color-gray-500; }' + const { code, converted } = transformScssBorderColors(input) + expect(code).toBe(input) + expect(converted).toHaveLength(0) + }) +}) diff --git a/packages/components/codemods/migrateBorderColorsToSemanticTokens/transformScssBorderColors.ts b/packages/components/codemods/migrateBorderColorsToSemanticTokens/transformScssBorderColors.ts new file mode 100644 index 00000000000..8a0cdca0f80 --- /dev/null +++ b/packages/components/codemods/migrateBorderColorsToSemanticTokens/transformScssBorderColors.ts @@ -0,0 +1,31 @@ +import scssSyntax from 'postcss-scss' +import { + rewriteBorderColors, + type BorderColorResult, + type ConvertedToken, + type SkippedToken, +} from './borderColorPostcss' + +export type { ConvertedToken, SkippedToken } + +export type ScssTransformResult = BorderColorResult & { code: string } + +/** + * Rewrites hard-coded primitive border colors to the new semantic border tokens + * in **SCSS**, covering both reference styles a `.scss` file can use: + * - SCSS variable (Kaizen sass): `border: 1px solid $color-gray-500` → `$border-secondary` + * - CSS custom property: `border-color: var(--color-gray-500)` → `var(--border-secondary)` + * + * When a `$border-*` variable is introduced, the `@kaizen/design-tokens/sass/semantic` + * import is added (matching the file's existing import style) so the output still + * compiles. Only confident 1:1 mappings in a border context are rewritten; + * unmapped border colors and alpha/`rgb()`/`rgba()` usages such as + * `rgba($color-gray-600-rgb, 0.1)` are left untouched and returned in `skipped` + * for the stage-2 fallback. Nested rules, `@layer`, and other SCSS constructs + * are preserved by the SCSS parser. + */ +export const transformScssBorderColors = (source: string): ScssTransformResult => { + const root = scssSyntax.parse(source) + const { converted, skipped } = rewriteBorderColors(root, { scss: true }) + return { code: root.toString(), converted, skipped } +} diff --git a/packages/components/codemods/migrateBorderColorsToSemanticTokens/transformTailwindBorderClasses.spec.ts b/packages/components/codemods/migrateBorderColorsToSemanticTokens/transformTailwindBorderClasses.spec.ts new file mode 100644 index 00000000000..95d6e7b41bb --- /dev/null +++ b/packages/components/codemods/migrateBorderColorsToSemanticTokens/transformTailwindBorderClasses.spec.ts @@ -0,0 +1,52 @@ +import { transformTailwindBorderClasses } from './transformTailwindBorderClasses' + +describe('transformTailwindBorderClasses()', () => { + it('rewrites a named border color utility in a className attribute', () => { + const input = `const A = () =>
` + const { code, converted } = transformTailwindBorderClasses(input) + expect(code).toContain('className="border border-secondary p-2"') + expect(converted).toEqual([{ line: 1, from: 'border-gray-500', to: 'border-secondary' }]) + }) + + it('rewrites an arbitrary-value border utility', () => { + const input = `const A = () =>
` + const { code } = transformTailwindBorderClasses(input) + expect(code).toContain('className="border-brand"') + }) + + it('rewrites classes inside a clsx call, including object keys', () => { + const input = `const A = () =>
` + const { code } = transformTailwindBorderClasses(input) + expect(code).toContain("'border-error'") + expect(code).toContain("'border-primary'") + }) + + it('preserves a Tailwind utility prefix (goals-)', () => { + const input = `const A = () =>
` + const { code, converted } = transformTailwindBorderClasses(input, { prefix: 'goals-' }) + expect(code).toContain('className="goals-border-secondary"') + expect(converted[0]).toMatchObject({ to: 'goals-border-secondary' }) + }) + + it('preserves a colon namespace prefix (EP:) and stacked variants', () => { + const input = `const A = () =>
` + const { code } = transformTailwindBorderClasses(input) + expect(code).toContain('EP:border-secondary') + expect(code).toContain('hover:border-brand') + }) + + it('skips dynamic class names and reports unmapped border colors', () => { + const input = `const A = () =>
` + const { code, converted, skipped } = transformTailwindBorderClasses(input) + expect(converted).toHaveLength(0) + expect(code).toContain('border-yellow-700') + expect(skipped).toEqual([{ line: 1, token: 'border-yellow-700' }]) + }) + + it('leaves non-className strings untouched', () => { + const input = `const label = "border-gray-500"` + const { code, converted } = transformTailwindBorderClasses(input) + expect(code).toBe(input) + expect(converted).toHaveLength(0) + }) +}) diff --git a/packages/components/codemods/migrateBorderColorsToSemanticTokens/transformTailwindBorderClasses.ts b/packages/components/codemods/migrateBorderColorsToSemanticTokens/transformTailwindBorderClasses.ts new file mode 100644 index 00000000000..82edc46f7bf --- /dev/null +++ b/packages/components/codemods/migrateBorderColorsToSemanticTokens/transformTailwindBorderClasses.ts @@ -0,0 +1,170 @@ +import ts from 'typescript' +import { + BORDER_COLOR_MAP, + TAILWIND_BORDER_COLOR_UTILITY, + TAILWIND_BORDER_MAP, +} from './borderTokenMap' + +export type ConvertedClass = { line: number; from: string; to: string } +export type SkippedClass = { line: number; token: string } + +export type TailwindTransformResult = { + code: string + converted: ConvertedClass[] + skipped: SkippedClass[] +} + +/** JSX attributes whose string value is a class list. */ +const CLASSNAME_ATTRIBUTES = new Set(['className', 'class']) +/** Call expressions whose string arguments are class names. */ +const CLASSNAME_FUNCTIONS = new Set(['clsx', 'classnames', 'classNames', 'cx', 'cn']) +/** Matches an arbitrary-value border utility: `border-[var(--color-)]`. */ +const ARBITRARY_BORDER_VALUE = /^border-\[var\(\s*--color-([a-z0-9-]+)\s*\)\]$/i + +type TokenResult = + | { status: 'converted'; token: string } + | { status: 'skipped' } + | { status: 'unchanged' } + +/** + * Transforms a single class token, preserving any variant chain (`hover:`, + * `md:`, or a namespace prefix like `EP:`) and any configured utility prefix + * (Tailwind `prefix`, like `goals-`). Only the utility portion is matched + * against the border maps; the prefix/variant is re-attached verbatim. + */ +const transformToken = (token: string, prefix: string): TokenResult => { + // Split off the variant chain: everything up to and including the last colon. + // This handles stacked variants (`hover:md:`) and colon-style prefixes (`EP:`). + const lastColon = token.lastIndexOf(':') + const variant = lastColon >= 0 ? token.slice(0, lastColon + 1) : '' + let utility = lastColon >= 0 ? token.slice(lastColon + 1) : token + + // Strip a configured dash-style utility prefix (e.g. `goals-`) so we can match + // the bare utility, then re-apply it. Colon-style prefixes are already handled + // by the variant split above. + let utilityPrefix = '' + if (prefix && !prefix.endsWith(':') && utility.startsWith(prefix)) { + utilityPrefix = prefix + utility = utility.slice(prefix.length) + } + + const rebuild = (mapped: string): string => `${variant}${utilityPrefix}${mapped}` + + // Named utility, e.g. `border-gray-500` → `border-secondary`. + const mapped = TAILWIND_BORDER_MAP[utility] + if (mapped) return { status: 'converted', token: rebuild(mapped) } + + // Arbitrary value, e.g. `border-[var(--color-gray-500)]` → `border-secondary`. + const arbitrary = ARBITRARY_BORDER_VALUE.exec(utility) + if (arbitrary) { + const semantic = BORDER_COLOR_MAP[arbitrary[1]] + if (semantic) return { status: 'converted', token: rebuild(semantic) } + return { status: 'skipped' } + } + + // A border color utility we can't confidently map — report for stage 2. + if (TAILWIND_BORDER_COLOR_UTILITY.test(utility)) return { status: 'skipped' } + + return { status: 'unchanged' } +} + +const transformClassValue = ( + text: string, + prefix: string, + line: number, + converted: ConvertedClass[], + skipped: SkippedClass[], +): { value: string; changed: boolean } => { + let changed = false + const value = text + .split(/(\s+)/) // keep whitespace separators so spacing is preserved + .map((part) => { + if (part.trim() === '') return part + const result = transformToken(part, prefix) + if (result.status === 'converted') { + converted.push({ line, from: part, to: result.token }) + changed = true + return result.token + } + if (result.status === 'skipped') skipped.push({ line, token: part }) + return part + }) + .join('') + return { value, changed } +} + +/** + * Rewrites Tailwind border color utilities in `className`/`class` attributes and + * `clsx`-style calls to the new semantic border utilities, honouring a consumer + * Tailwind `prefix` (e.g. `goals-` or `EP:`). + * + * Edits are spliced back into the original source by node position, so file + * formatting is otherwise preserved (run prettier afterwards regardless). + * Dynamic/computed class names and unmapped border colors are left untouched; + * unmapped border colors are returned in `skipped` for the stage-2 fallback. + */ +export const transformTailwindBorderClasses = ( + source: string, + { + prefix = '', + scriptKind = ts.ScriptKind.TSX, + }: { prefix?: string; scriptKind?: ts.ScriptKind } = {}, +): TailwindTransformResult => { + const sourceFile = ts.createSourceFile( + 'temp.tsx', + source, + ts.ScriptTarget.Latest, + true, + scriptKind, + ) + + const converted: ConvertedClass[] = [] + const skipped: SkippedClass[] = [] + // Inner-text edits keyed by start position to avoid processing a literal twice + // (e.g. a clsx call that also lives inside a className attribute). + const edits = new Map() + + const collectFromStringLiteral = (node: ts.StringLiteralLike): void => { + const start = node.getStart(sourceFile) + if (edits.has(start)) return + const line = sourceFile.getLineAndCharacterOfPosition(start).line + 1 + const { value, changed } = transformClassValue(node.text, prefix, line, converted, skipped) + if (changed) { + // Replace the inner text only, leaving the surrounding quotes/backticks intact. + edits.set(start, { start: start + 1, end: node.getEnd() - 1, text: value }) + } else { + edits.set(start, { start, end: start, text: '' }) // mark as seen, no-op + } + } + + const collectStrings = (node: ts.Node): void => { + if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) { + collectFromStringLiteral(node) + return + } + ts.forEachChild(node, collectStrings) + } + + const visit = (node: ts.Node): void => { + if (ts.isJsxAttribute(node) && CLASSNAME_ATTRIBUTES.has(node.name.getText(sourceFile))) { + if (node.initializer) collectStrings(node.initializer) + } + if (ts.isCallExpression(node) && CLASSNAME_FUNCTIONS.has(node.expression.getText(sourceFile))) { + node.arguments.forEach(collectStrings) + } + ts.forEachChild(node, visit) + } + visit(sourceFile) + + // Apply edits back-to-front so earlier offsets stay valid. + const realEdits = Array.from(edits.values()) + .filter((edit) => edit.end > edit.start) + .sort((a, b) => b.start - a.start) + + let code = source + for (const edit of realEdits) { + code = code.slice(0, edit.start) + edit.text + code.slice(edit.end) + } + + return { code, converted, skipped } +} diff --git a/packages/components/codemods/migratePrimitivesToSemanticTokens/README.md b/packages/components/codemods/migratePrimitivesToSemanticTokens/README.md new file mode 100644 index 00000000000..006701da9fc --- /dev/null +++ b/packages/components/codemods/migratePrimitivesToSemanticTokens/README.md @@ -0,0 +1,195 @@ +# `migratePrimitivesToSemanticTokens` + +Migrates hard-coded **primitive colors** to the new **semantic colour tokens** +across all four semantic groups — **background**, **text**, **foreground** +(icons / glyphs), and **border** — in CSS, SCSS, and Tailwind. It is the +deterministic first stage of the semantic-colour migration; the companion +[`/semantic-colours-migration`](../../../../.claude/skills/semantic-colours-migration/SKILL.md) +skill runs this codemod and then guides a manual fallback for anything it +deliberately skips. + +> **Borders are included here.** This codemod handles the `border-*` group +> itself, alongside `bg-*`, `text-*`, and `fg-*` — there is no need to also run +> the older border-only +> [`migrateBorderColorsToSemanticTokens`](../migrateBorderColorsToSemanticTokens/README.md) +> codemod. That codemod is **prior art / reference only**: its architecture and +> border mappings are the basis this one was built on and fully supersedes. + +## Prerequisites + +- Install `@kaizen/components` (the `kaizen-codemod` bin ships with the package). + +## Usage + +``` +pnpm kaizen-codemod {DIR} migratePrimitivesToSemanticTokens +``` + +- `{DIR}` — directory to scan (e.g. `src`, or a single component folder to scope + to one component). `node_modules` is excluded. +- After it runs, **run prettier/lint** to tidy any rewritten lines. +- Review the printed **`SKIPPED`** report and resolve the remaining cases with + the `/semantic-colours-migration` skill (stage 2). + +### Tailwind prefix + +If your repo sets a Tailwind `prefix`, tell the codemod so prefixed utilities +are matched and the prefix is **preserved** on rewrite. The codemod resolves the +prefix from (in order): a `--prefix=` arg, the `KAIZEN_TW_PREFIX` env var, or a +`tailwind.config.*` in the current directory. + +``` +# utility prefix (Tailwind `prefix: 'goals-'`) +KAIZEN_TW_PREFIX=goals- pnpm kaizen-codemod src migratePrimitivesToSemanticTokens +# goals-bg-blue-500 → goals-bg-brand-solid + +# colon namespace prefix (e.g. Employee Profile) +KAIZEN_TW_PREFIX=EP: pnpm kaizen-codemod src migratePrimitivesToSemanticTokens +# EP:bg-blue-500 → EP:bg-brand-solid +``` + +Stacked variants (`hover:`, `md:`) are always preserved automatically. + +## How it works + +The codemod walks `{DIR}` (excluding `node_modules`) and dispatches each file to +one of three transformers by extension. They share the confident mapping, +group-context detection, and skip/report rules, but each handles the reference +styles native to its language: + +| Files | Transformer | Reference styles handled | +| --------------- | ---------------------------------- | ---------------------------------------------------------------------- | +| `.css` | `transformCssSemanticColors` | CSS custom property `var(--color-*)` | +| `.scss` | `transformScssSemanticColors` | SCSS variable `$color-*` **and** CSS custom property `var(--color-*)` | +| `.tsx/.jsx/.ts` | `transformTailwindSemanticClasses` | Tailwind color utilities in `className`/`class` and `clsx`-style calls | + +CSS/SCSS use a `postcss` AST (`postcss-scss` for `.scss`); Tailwind uses the +TypeScript compiler API. Only files with at least one conversion are written. + +### The group is chosen by context + +The same primitive maps to a **different** semantic token depending on the +surface it paints. The codemod picks the group from the CSS property or the +Tailwind utility root, then applies that group's map: + +| Group | CSS/SCSS property | Tailwind root | Semantic prefix | +| ------------ | ------------------------------------------------------------- | ------------------ | --------------- | +| `background` | `background`, `background-color`, `--*background*` / `--*bg*` | `bg-` | `bg-*` | +| `text` | `color`, `--*text*` | `text-` | `text-*` | +| `foreground` | `fill`, `stroke`, `--*icon*` / `--*fill*` / `--*stroke*` | `fill-`, `stroke-` | `fg-*` | +| `border` | `border` / per-side / logical / `*-color`, `--*border*` | `border-` | `border-*` | + +So `var(--color-blue-500)` becomes `var(--bg-brand-solid)` on `background`, +`var(--text-brand-primary)` on `color`, `var(--fg-brand-primary)` on `fill`, and +`var(--border-brand)` on a border. Tailwind `fill-`/`stroke-` primitives map to +the `fg-*` utility (the foreground token name is the utility). + +Colour references in properties with **no confident group** (`box-shadow`, +`outline-color`, …) are left untouched and **not** reported — they are out of +scope. + +## What it converts + +Only **confident 1:1 mappings within a group** are rewritten. A primitive is +confident only if it backs exactly **one** semantic token in that group. + +The maps are derived by inverting the four groups in +[`semanticColorTokens.ts`](../../../design-tokens/src/js/semanticColorTokens.ts) +and **dropping collisions** — see [`semanticTokenMap.ts`](./semanticTokenMap.ts). +`semanticTokenMap.spec.ts` asserts the mirror never drifts from that source. + +Selected confident mappings (see the source for the full set): + +| Group | Primitive | Semantic token | +| ------------ | ------------ | -------------------- | +| `background` | `gray-300` | `bg-tertiary` | +| `background` | `blue-500` | `bg-brand-solid` | +| `background` | `red-400` | `bg-error-solid` | +| `text` | `purple-800` | `text-primary` | +| `text` | `gray-700` | `text-secondary` | +| `text` | `blue-500` | `text-brand-primary` | +| `foreground` | `gray-500` | `fg-tertiary` | +| `foreground` | `blue-500` | `fg-brand-primary` | +| `foreground` | `red-500` | `fg-error-primary` | +| `border` | `gray-600` | `border-primary` | +| `border` | `blue-500` | `border-brand` | + +### SCSS specifics + +- Consumer SCSS usually references Kaizen colors as **SCSS variables** + (`$color-gray-500`, imported from `@kaizen/design-tokens/sass/color`), so those + are matched in addition to raw `var(--color-*)`. Solid `$color-*` colours are + rewritten to the semantic SCSS variable (`$text-secondary`, `$bg-brand-solid`, …). +- Because the semantic `$*` variables are defined in + `@kaizen/design-tokens/sass/semantic-color`, the codemod **adds that import** + when it introduces a semantic variable and the file doesn't already import it — + cloning the style (`~` prefix / quotes) of an existing kaizen sass import. +- **Namespaced** refs (`@use` → `color.$color-*`) are left for stage 2 rather + than rewritten with an assumed namespace. + +### What is skipped (reported, never guessed) + +- **Colliding primitives** — a primitive that backs more than one token in a + group (e.g. `gray-200` → `bg-secondary` _and_ `bg-secondary_hover`; `white` → + `bg-primary` _and_ `bg-overlay`; `purple-800` → four `fg-*` tokens). No + confident 1:1 mapping. +- Primitives with **no mapping** in the group (e.g. an `orange-*` text colour). +- **`null` (unmapped) semantic tokens** — never a target; the primitive is only + reported if used in-group. +- **Alpha / `rgb()` / `rgba()` usages** (e.g. `rgba($color-gray-600-rgb, 0.1)`) — + the semantic tokens are solid colors, so these need a design decision. +- Dynamic/computed Tailwind class names (template literals, variables). + +## Examples + +**CSS** + +```css +/* before */ +.card { + background-color: var(--color-blue-500); + color: var(--color-gray-700); + border: 1px solid var(--color-gray-500); +} +/* after */ +.card { + background-color: var(--bg-brand-solid); + color: var(--text-secondary); + border: 1px solid var(--border-secondary); +} +``` + +**SCSS** (note the injected semantic import) + +```scss +/* before */ +@import '~@kaizen/design-tokens/sass/color'; +.card { + color: $color-gray-700; +} +/* after */ +@import '~@kaizen/design-tokens/sass/color'; +@import '~@kaizen/design-tokens/sass/semantic-color'; +.card { + color: $text-secondary; +} +``` + +**Tailwind** + +```tsx +// before +
+// after +
+``` + +## Source of truth + +Mappings mirror the four groups in +[`packages/design-tokens/src/js/semanticColorTokens.ts`](../../../design-tokens/src/js/semanticColorTokens.ts) +and are derived (inverted, collisions dropped) in +[`semanticTokenMap.ts`](./semanticTokenMap.ts). The CSS and SCSS transformers +share the postcss engine in [`semanticColorPostcss.ts`](./semanticColorPostcss.ts). + +See [`TODO.md`](./TODO.md) for known follow-ups and outliers still to review. diff --git a/packages/components/codemods/migratePrimitivesToSemanticTokens/TODO.md b/packages/components/codemods/migratePrimitivesToSemanticTokens/TODO.md new file mode 100644 index 00000000000..4386d327f2d --- /dev/null +++ b/packages/components/codemods/migratePrimitivesToSemanticTokens/TODO.md @@ -0,0 +1,54 @@ +# `migratePrimitivesToSemanticTokens` — follow-ups + +Known gaps and things to revisit. This codemod only applies **confident 1:1** +mappings today; the items below are deliberately deferred. + +- [ ] **Update once `null` semantic tokens are defined.** Many semantic tokens + are still `null` in + [`semanticColorTokens.ts`](../../../design-tokens/src/js/semanticColorTokens.ts) + (e.g. `bg-primary_alt`, `text-secondary_hover`, `fg-brand-secondary`). + They are intentionally excluded from the maps. When a designer signs off a + value, the mirror in [`semanticTokenMap.ts`](./semanticTokenMap.ts) must be + updated (the drift test in `semanticTokenMap.spec.ts` will flag it) and any + new collisions re-checked. + +- [ ] **Make the codemod runnable across package managers (pnpm / yarn / devbox).** + Usage is currently documented as `pnpm kaizen-codemod …`, and the + `kaizen-codemod` bin shells out via `npx tsx@latest`. Confirm and document + equivalents for `yarn kaizen-codemod …` / `yarn dlx`, and `devbox run …`, + and make sure the `bin/codemod.sh` path resolution and Tailwind-prefix + detection work under each (not just pnpm workspaces). + +- [ ] **Review the Divider component outlier.** Divider uses a hard-coded + primitive in a way that may not fit the `border-*` / `bg-*` group heuristic + cleanly. Confirm what surface its line paints and whether the codemod maps + it correctly (or should skip it). + +- [ ] **Review interaction states (hover / focus / `_alt`).** State-specific + semantic tokens (`bg-secondary_hover`, `text-brand-secondary_hover`, + `bg-brand-solid_hover`, the `_alt` variants) are only auto-applied where + the primitive is unambiguous. Several state pairs collide (e.g. `gray-200` + backs both `bg-secondary` and `bg-secondary_hover`) and are skipped — + confirm the codemod isn't mapping a resting colour where a hover colour was + intended, and vice versa. + +- [ ] **Fix SCSS stripping comments.** Confirm and fix cases where the + `postcss-scss` round-trip drops or mangles comments (inline `//` and block + `/* */`) in rewritten `.scss` files. Rewrites should preserve surrounding + comments verbatim; investigate `toString()` output and add a regression + test in `transformScssSemanticColors.spec.ts`. + +- [ ] **Decide Tailwind foreground scope (`fill-*`/`stroke-*` → `fg-*`).** The + codemod currently maps Tailwind `fill-*` and `stroke-*` primitives to the + `fg-*` semantic utility, and deliberately leaves `text-*` for the text + group (so icon colour expressed via `text-*` on `currentColor` is treated + as text, not foreground). Confirm this matches how consumers actually paint + icons in Tailwind — if icon colour commonly comes through `text-*`, the + foreground group may need a different (or additional) Tailwind path. + +- [ ] **Review behaviour across different JS applications.** Verify parity when + the same styles arrive via different pipelines — e.g. SCSS parsed/inlined + inside JS (CSS-in-JS, `.module.scss` imports) versus plain Tailwind classes + in `.tsx`. The Tailwind transformer only inspects `className`/`class` + attributes and `clsx`-style calls; styles reaching JS by other means may + need a dedicated path. diff --git a/packages/components/codemods/migratePrimitivesToSemanticTokens/index.ts b/packages/components/codemods/migratePrimitivesToSemanticTokens/index.ts new file mode 100644 index 00000000000..ffbb60e359f --- /dev/null +++ b/packages/components/codemods/migratePrimitivesToSemanticTokens/index.ts @@ -0,0 +1,109 @@ +import fs from 'fs' +import path from 'path' +import ts from 'typescript' +import { transformCssSemanticColors } from './transformCssSemanticColors' +import { transformScssSemanticColors } from './transformScssSemanticColors' +import { transformTailwindSemanticClasses } from './transformTailwindSemanticClasses' + +type SkipReport = { file: string; line: number; detail: string } + +/** + * Resolves the consumer's Tailwind prefix. + * + * The `kaizen-codemod` wrapper only forwards the target dir, so the prefix is + * taken from (in priority order): a `--prefix=` CLI arg (when run directly via + * tsx), the `KAIZEN_TW_PREFIX` env var, or a best-effort read of a + * `tailwind.config.*` in the current working directory. + */ +const resolveTailwindPrefix = (): string => { + const argPrefix = process.argv.find((arg) => arg.startsWith('--prefix=')) + if (argPrefix) return argPrefix.slice('--prefix='.length) + if (process.env.KAIZEN_TW_PREFIX) return process.env.KAIZEN_TW_PREFIX + + const configName = [ + 'tailwind.config.ts', + 'tailwind.config.js', + 'tailwind.config.cjs', + 'tailwind.config.mjs', + ].find((name) => fs.existsSync(path.join(process.cwd(), name))) + if (configName) { + try { + const contents = fs.readFileSync(path.join(process.cwd(), configName), 'utf8') + const match = /prefix:\s*['"`]([^'"`]+)['"`]/.exec(contents) + if (match) return match[1] + } catch { + // best effort only — fall through to no prefix + } + } + return '' +} + +const traverseDir = (dir: string, onFile: (filePath: string) => void): void => { + if (dir.includes('node_modules')) return + for (const entry of fs.readdirSync(dir)) { + const fullPath = path.join(dir, entry) + if (fs.statSync(fullPath).isDirectory()) { + traverseDir(fullPath, onFile) + } else { + onFile(fullPath) + } + } +} + +const run = (): void => { + console.log(' ~(-_- ~) Running primitives → semantic tokens transformer (~ -_-)~') + const targetDir = process.argv[2] + if (!targetDir || targetDir.startsWith('--')) { + console.error('Usage: kaizen-codemod migratePrimitivesToSemanticTokens') + process.exit(1) + } + + const prefix = resolveTailwindPrefix() + if (prefix) console.log(`Using Tailwind prefix: "${prefix}"`) + + let convertedCount = 0 + const skips: SkipReport[] = [] + + traverseDir(targetDir, (filePath) => { + const relative = path.relative(process.cwd(), filePath) + + if (filePath.endsWith('.scss') || filePath.endsWith('.css')) { + const source = fs.readFileSync(filePath, 'utf8') + const { code, converted, skipped } = filePath.endsWith('.scss') + ? transformScssSemanticColors(source) + : transformCssSemanticColors(source) + if (converted.length > 0) fs.writeFileSync(filePath, code, 'utf8') + convertedCount += converted.length + skipped.forEach((s) => + skips.push({ file: relative, line: s.line, detail: `${s.property}: ${s.value}` }), + ) + return + } + + if (/\.(tsx|jsx|ts)$/.test(filePath)) { + const source = fs.readFileSync(filePath, 'utf8') + const scriptKind = filePath.endsWith('.ts') ? ts.ScriptKind.TS : ts.ScriptKind.TSX + const { code, converted, skipped } = transformTailwindSemanticClasses(source, { + prefix, + scriptKind, + }) + if (converted.length > 0) fs.writeFileSync(filePath, code, 'utf8') + convertedCount += converted.length + skipped.forEach((s) => skips.push({ file: relative, line: s.line, detail: s.token })) + } + }) + + console.log('') + console.log(`✅ Converted ${convertedCount} primitive color token(s) to semantic tokens.`) + + if (skips.length > 0) { + console.log('') + console.log(`⚠️ SKIPPED ${skips.length} color(s) with no confident semantic mapping.`) + console.log(' Resolve these with the /semantic-colours-migration skill (stage 2):') + skips.forEach((s) => console.log(` - ${s.file}:${s.line} — ${s.detail}`)) + } else { + console.log('No ambiguous colors detected — stage 2 may not be needed.') + } +} + +run() diff --git a/packages/components/codemods/migratePrimitivesToSemanticTokens/semanticColorPostcss.ts b/packages/components/codemods/migratePrimitivesToSemanticTokens/semanticColorPostcss.ts new file mode 100644 index 00000000000..cc048cad06b --- /dev/null +++ b/packages/components/codemods/migratePrimitivesToSemanticTokens/semanticColorPostcss.ts @@ -0,0 +1,145 @@ +import postcss from 'postcss' +import { + COLOR_VAR_REFERENCE, + PRIMITIVE_TO_SEMANTIC, + SCSS_COLOR_VAR_REFERENCE, + groupForProperty, + hasAlphaColorUsage, + referencesPrimitiveColor, +} from './semanticTokenMap' + +/** + * Shared postcss engine for the CSS and SCSS semantic-colour transformers. Both + * public transformers ({@link transformCssSemanticColors}, + * {@link transformScssSemanticColors}) parse with their own syntax and delegate + * declaration rewriting here so the group-context rules, confident mapping, and + * skip/report behaviour stay identical across CSS and SCSS. + */ + +export type ConvertedToken = { + line: number + property: string + from: string + to: string +} + +export type SkippedToken = { + line: number + property: string + value: string +} + +export type SemanticColorResult = { + converted: ConvertedToken[] + skipped: SkippedToken[] +} + +/** Path to the semantic sass partial that defines the `$bg-*`/`$text-*`/`$fg-*`/`$border-*` variables. */ +const SEMANTIC_SASS_MODULE = '@kaizen/design-tokens/sass/semantic-color' + +/** + * Ensures the semantic sass partial is imported once a semantic variable has + * been introduced, otherwise the rewritten SCSS would reference an undefined + * variable and fail to compile. Clones the style (quote + `~` prefix) of an + * existing `@kaizen/design-tokens/sass/*` import so the added line matches the + * file's convention; falls back to a plain `@import` when no kaizen import + * exists. No-op when the semantic partial is already imported/used. + */ +const ensureSemanticImport = (root: postcss.Root): void => { + let kaizenImport: postcss.AtRule | undefined + let alreadyImported = false + + root.walkAtRules((atRule) => { + if (atRule.name !== 'import' && atRule.name !== 'use') return + if (atRule.params.includes('design-tokens/sass/semantic-color')) alreadyImported = true + if (!kaizenImport && atRule.params.includes('design-tokens/sass/')) kaizenImport = atRule + }) + + if (alreadyImported) return + + if (kaizenImport) { + const params = kaizenImport.params.replace( + /(design-tokens\/sass\/)[a-z-]+/i, + `$1semantic-color`, + ) + // Clone the existing import's own-line formatting so the addition sits on its + // own line rather than being appended to the source import. + kaizenImport.cloneAfter({ params, raws: { ...kaizenImport.raws, before: '\n' } }) + return + } + + root.prepend(postcss.atRule({ name: 'import', params: `'${SEMANTIC_SASS_MODULE}'` })) +} + +/** + * Walks every declaration in `root`, rewriting hard-coded primitive colours to + * the new semantic tokens for the group the declaration's property targets + * (background / text / foreground / border — see {@link groupForProperty}). + * When `scss` is true, SCSS `$color-*` variables are also rewritten and the + * semantic sass import is injected if needed. + * + * Only confident 1:1 mappings within a group are rewritten (see + * {@link PRIMITIVE_TO_SEMANTIC}). Unmapped primitives *within a recognised + * group*, and alpha/`rgb()`/`rgba()` usages, are left untouched and returned in + * `skipped` for the stage-2 fallback. Colour references in properties with no + * confident group (e.g. `box-shadow`, `outline-color`) are left untouched and + * NOT reported — they are out of scope for this codemod. + */ +export const rewriteSemanticColors = ( + root: postcss.Root, + { scss = false }: { scss?: boolean } = {}, +): SemanticColorResult => { + const converted: ConvertedToken[] = [] + const skipped: SkippedToken[] = [] + let introducedScssSemantic = false + + root.walkDecls((decl) => { + if (!referencesPrimitiveColor(decl.value)) return + + const group = groupForProperty(decl.prop) + if (!group) return // property has no confident semantic group — out of scope + + const line = decl.source?.start?.line ?? 0 + const map = PRIMITIVE_TO_SEMANTIC[group] + + // Alpha / rgb() usages are solid-color-only in the semantic set — flag for a + // design decision (stage 2) instead of guessing, and don't touch the value. + if (hasAlphaColorUsage(decl.value)) { + skipped.push({ line, property: decl.prop, value: decl.value.trim() }) + return + } + + // CSS custom property references: var(--color-X) → var(--). + let value = decl.value.replace(COLOR_VAR_REFERENCE, (match, primitive: string) => { + const semantic = map[primitive] + if (semantic) { + const replacement = `var(--${semantic})` + converted.push({ line, property: decl.prop, from: match, to: replacement }) + return replacement + } + skipped.push({ line, property: decl.prop, value: match }) + return match + }) + + // SCSS variable references: $color-X → $ (SCSS files only). + if (scss) { + value = value.replace(SCSS_COLOR_VAR_REFERENCE, (match, primitive: string) => { + const semantic = map[primitive] + if (semantic) { + const replacement = `$${semantic}` + converted.push({ line, property: decl.prop, from: match, to: replacement }) + introducedScssSemantic = true + return replacement + } + skipped.push({ line, property: decl.prop, value: match }) + return match + }) + } + + decl.value = value + }) + + if (introducedScssSemantic) ensureSemanticImport(root) + + return { converted, skipped } +} diff --git a/packages/components/codemods/migratePrimitivesToSemanticTokens/semanticTokenMap.spec.ts b/packages/components/codemods/migratePrimitivesToSemanticTokens/semanticTokenMap.spec.ts new file mode 100644 index 00000000000..18bffa0415e --- /dev/null +++ b/packages/components/codemods/migratePrimitivesToSemanticTokens/semanticTokenMap.spec.ts @@ -0,0 +1,90 @@ +import { semanticColorTokens } from '../../../design-tokens/src/js/semanticColorTokens' +import { + PRIMITIVE_TO_SEMANTIC, + SEMANTIC_TO_PRIMITIVE, + groupForProperty, + type SemanticGroup, +} from './semanticTokenMap' + +/** Reduce a source token value (`var(--color-gray-600)` | null) to `gray-600` | null. */ +const toPrimitive = (value: string | null): string | null => { + if (value === null) return null + const match = /var\(\s*--color-([a-z0-9-]+)\s*\)/i.exec(value) + return match ? match[1] : null +} + +describe('semanticTokenMap', () => { + describe('SEMANTIC_TO_PRIMITIVE mirror', () => { + // Guards against drift from the design-tokens source of truth. If a token's + // primitive changes (or a token is added/removed/un-nulled), this fails and + // the mirror must be updated. + const groups: SemanticGroup[] = ['background', 'text', 'foreground', 'border'] + + it.each(groups)('matches the non-null %s entries in semanticColorTokens.ts', (group) => { + const expected: Record = {} + for (const [token, value] of Object.entries(semanticColorTokens[group])) { + const primitive = toPrimitive(value) + if (primitive) expected[token] = primitive + } + expect(SEMANTIC_TO_PRIMITIVE[group]).toEqual(expected) + }) + }) + + describe('PRIMITIVE_TO_SEMANTIC (inverted, collisions dropped)', () => { + it('drops primitives that back more than one token in a group', () => { + // gray-200 → bg-secondary AND bg-secondary_hover; white → bg-primary AND bg-overlay. + expect(PRIMITIVE_TO_SEMANTIC.background).not.toHaveProperty('gray-200') + expect(PRIMITIVE_TO_SEMANTIC.background).not.toHaveProperty('white') + // yellow-400 → bg-warning-secondary AND bg-warning-solid. + expect(PRIMITIVE_TO_SEMANTIC.background).not.toHaveProperty('yellow-400') + // gray-550 → text-quaternary AND text-placeholder; white → two on-brand tokens. + expect(PRIMITIVE_TO_SEMANTIC.text).not.toHaveProperty('gray-550') + expect(PRIMITIVE_TO_SEMANTIC.text).not.toHaveProperty('white') + // purple-800 backs four fg-* tokens. + expect(PRIMITIVE_TO_SEMANTIC.foreground).not.toHaveProperty('purple-800') + }) + + it('keeps confident 1:1 mappings per group', () => { + expect(PRIMITIVE_TO_SEMANTIC.background['gray-300']).toBe('bg-tertiary') + expect(PRIMITIVE_TO_SEMANTIC.background['blue-500']).toBe('bg-brand-solid') + expect(PRIMITIVE_TO_SEMANTIC.text['gray-700']).toBe('text-secondary') + expect(PRIMITIVE_TO_SEMANTIC.text['blue-500']).toBe('text-brand-primary') + expect(PRIMITIVE_TO_SEMANTIC.foreground['gray-500']).toBe('fg-tertiary') + expect(PRIMITIVE_TO_SEMANTIC.foreground['blue-500']).toBe('fg-brand-primary') + expect(PRIMITIVE_TO_SEMANTIC.border['gray-500']).toBe('border-secondary') + }) + + it('resolves the same primitive to different tokens across groups by context', () => { + // blue-500 means something different depending on the surface it paints. + expect(PRIMITIVE_TO_SEMANTIC.background['blue-500']).toBe('bg-brand-solid') + expect(PRIMITIVE_TO_SEMANTIC.text['blue-500']).toBe('text-brand-primary') + expect(PRIMITIVE_TO_SEMANTIC.foreground['blue-500']).toBe('fg-brand-primary') + expect(PRIMITIVE_TO_SEMANTIC.border['blue-500']).toBe('border-brand') + }) + }) + + describe('groupForProperty', () => { + it('maps standard properties to their group', () => { + expect(groupForProperty('color')).toBe('text') + expect(groupForProperty('background')).toBe('background') + expect(groupForProperty('background-color')).toBe('background') + expect(groupForProperty('fill')).toBe('foreground') + expect(groupForProperty('stroke')).toBe('foreground') + expect(groupForProperty('border-color')).toBe('border') + expect(groupForProperty('border-inline-start')).toBe('border') + }) + + it('infers a group for custom properties from a keyword', () => { + expect(groupForProperty('--border-color')).toBe('border') + expect(groupForProperty('--background-hover')).toBe('background') + expect(groupForProperty('--text-color')).toBe('text') + expect(groupForProperty('--icon-fill')).toBe('foreground') + }) + + it('returns null for properties with no confident group', () => { + expect(groupForProperty('box-shadow')).toBeNull() + expect(groupForProperty('outline-color')).toBeNull() + expect(groupForProperty('--spacing')).toBeNull() + }) + }) +}) diff --git a/packages/components/codemods/migratePrimitivesToSemanticTokens/semanticTokenMap.ts b/packages/components/codemods/migratePrimitivesToSemanticTokens/semanticTokenMap.ts new file mode 100644 index 00000000000..35c51278dbe --- /dev/null +++ b/packages/components/codemods/migratePrimitivesToSemanticTokens/semanticTokenMap.ts @@ -0,0 +1,246 @@ +/** + * Confident primitive → semantic colour-token mappings for the four semantic + * groups (`background`, `text`, `foreground`, `border`). + * + * Source of truth: `packages/design-tokens/src/js/semanticColorTokens.ts`. That + * file maps a semantic token → the primitive it paints with; here we mirror the + * *mapped* (non-`null`) entries per group and **invert** them (primitive → + * semantic) because the codemod rewrites hard-coded primitives to the new + * semantic token. + * + * The mirror is duplicated here (rather than imported) so the codemod has no + * runtime dependency on `@kaizen/design-tokens` when run in a consumer repo — + * matching the sibling `migrateBorderColorsToSemanticTokens` codemod. + * `semanticTokenMap.spec.ts` asserts this mirror never drifts from the source. + * + * ### Why some primitives are intentionally absent + * A primitive is only a *confident* migration target if it maps to exactly ONE + * semantic token within its group. When a primitive backs several tokens in the + * same group (e.g. `gray-200` → both `bg-secondary` and `bg-secondary_hover`, + * `white` → both `bg-primary` and `bg-overlay`, `purple-800` → four `fg-*` + * tokens) there is no 1:1 mapping, so it is dropped by {@link invertGroup} and + * the codemod skips + reports it for the `/semantic-colours-migration` stage-2 + * fallback rather than guessing. `null` (unmapped) tokens are omitted entirely. + */ + +export type SemanticGroup = 'background' | 'text' | 'foreground' | 'border' + +/** + * semantic token name → primitive colour-token name, mirrored verbatim from the + * non-`null` entries of `semanticColorTokens.ts` (values reduced to the bare + * primitive, e.g. `var(--color-gray-600)` → `gray-600`). + */ +export const SEMANTIC_TO_PRIMITIVE: Record> = { + background: { + 'bg-primary': 'white', + 'bg-secondary': 'gray-200', + 'bg-secondary_hover': 'gray-200', + 'bg-tertiary': 'gray-300', + 'bg-primary-solid': 'purple-800', + 'bg-secondary-solid': 'gray-600', + 'bg-overlay': 'white', + 'bg-brand-primary': 'blue-100', + 'bg-brand-secondary': 'blue-200', + 'bg-brand-solid': 'blue-500', + 'bg-brand-solid_hover': 'blue-700', + 'bg-error-primary': 'red-100', + 'bg-error-secondary': 'red-300', + 'bg-error-solid': 'red-400', + 'bg-success-primary': 'green-100', + 'bg-success-secondary': 'green-300', + 'bg-success-solid': 'green-600', + 'bg-warning-primary': 'yellow-100', + 'bg-warning-secondary': 'yellow-400', + 'bg-warning-solid': 'yellow-400', + }, + text: { + 'text-primary': 'purple-800', + 'text-secondary': 'gray-700', + 'text-tertiary': 'gray-600', + 'text-quaternary': 'gray-550', + 'text-placeholder': 'gray-550', + 'text-secondary_on-brand': 'white', + 'text-quaternary_on-brand': 'white', + 'text-brand-primary': 'blue-500', + 'text-brand-secondary': 'blue-700', + 'text-brand-secondary_hover': 'blue-600', + 'text-error-primary': 'red-600', + 'text-success-primary': 'green-600', + }, + foreground: { + 'fg-primary': 'purple-800', + 'fg-secondary': 'purple-800', + 'fg-secondary_hover': 'purple-800', + 'fg-tertiary': 'gray-500', + 'fg-quaternary': 'purple-800', + 'fg-white': 'white', + 'fg-brand-primary': 'blue-500', + 'fg-error-primary': 'red-500', + 'fg-success-primary': 'green-500', + 'fg-success-secondary': 'green-400', + 'fg-warning-primary': 'yellow-700', + }, + border: { + 'border-primary': 'gray-600', + 'border-secondary': 'gray-500', + 'border-secondary_alt': 'gray-200', + 'border-tertiary': 'gray-300', + 'border-brand': 'blue-500', + 'border-brand_alt': 'blue-300', + 'border-error': 'red-500', + 'border-error_subtle': 'red-300', + }, +} + +/** + * Inverts one group's `semantic → primitive` table into `primitive → semantic`, + * dropping any primitive that backs more than one semantic token in the group + * (no confident 1:1 mapping). Those collisions are handled by stage 2. + */ +const invertGroup = (entries: Record): Record => { + const primitiveCounts = new Map() + for (const primitive of Object.values(entries)) { + primitiveCounts.set(primitive, (primitiveCounts.get(primitive) ?? 0) + 1) + } + + const inverted: Record = {} + for (const [semantic, primitive] of Object.entries(entries)) { + if (primitiveCounts.get(primitive) === 1) inverted[primitive] = semantic + } + return inverted +} + +/** + * Confident `primitive → semantic token name` map per group, derived from + * {@link SEMANTIC_TO_PRIMITIVE}. The semantic token name already carries its + * group prefix (`bg-`, `text-`, `fg-`, `border-`) so it doubles as the CSS var + * name, the SCSS variable, and the Tailwind utility. + */ +export const PRIMITIVE_TO_SEMANTIC: Record> = { + background: invertGroup(SEMANTIC_TO_PRIMITIVE.background), + text: invertGroup(SEMANTIC_TO_PRIMITIVE.text), + foreground: invertGroup(SEMANTIC_TO_PRIMITIVE.foreground), + border: invertGroup(SEMANTIC_TO_PRIMITIVE.border), +} + +/** + * Matches CSS properties that can carry a border *color*: the `border` + * shorthand, per-side/logical shorthands, and the explicit `*-color` longhands. + * Excludes `border-width`/`border-style`/`border-radius`/`border-image`, which + * never hold a color. + */ +const BORDER_COLOR_PROPERTY = + /^border(-(top|right|bottom|left|block|inline)(-(start|end))?)?(-color)?$/ + +/** + * Resolves the semantic group a *custom property* (`--foo`) targets from a + * keyword in its name (e.g. `--icon-fill-color` → foreground). Returns `null` + * when no group can be inferred, so the codemod leaves it for stage 2 rather + * than guessing. + */ +const groupForCustomProperty = (prop: string): SemanticGroup | null => { + if (prop.includes('border')) return 'border' + if (prop.includes('background') || /(^|--|-)bg(-|$)/.test(prop)) return 'background' + if (prop.includes('text')) return 'text' + if ( + prop.includes('icon') || + prop.includes('fill') || + prop.includes('stroke') || + /(^|--|-)fg(-|$)/.test(prop) + ) { + return 'foreground' + } + return null +} + +/** + * Resolves which semantic group a CSS/SCSS declaration property targets: + * - `color` → text + * - `background` / `background-color` → background + * - `fill` / `stroke` → foreground (icons / glyphs) + * - border color properties → border + * - custom properties → inferred from the name (see {@link groupForCustomProperty}) + * + * Returns `null` for properties with no confident group (e.g. `box-shadow`, + * `outline-color`); the codemod then leaves the declaration untouched. + */ +export const groupForProperty = (property: string): SemanticGroup | null => { + const prop = property.trim().toLowerCase() + if (prop.startsWith('--')) return groupForCustomProperty(prop) + if (BORDER_COLOR_PROPERTY.test(prop)) return 'border' + if (prop === 'color') return 'text' + if (prop === 'background' || prop === 'background-color') return 'background' + if (prop === 'fill' || prop === 'stroke') return 'foreground' + return null +} + +/** Matches a `var(--color-)` reference and captures the primitive name. */ +export const COLOR_VAR_REFERENCE = /var\(\s*--color-([a-z0-9-]+)\s*\)/gi + +/** + * Matches a solid `$color-` SCSS variable reference (the form consumers + * get from `@import '@kaizen/design-tokens/sass/color'`) and captures the + * primitive name. Matches shade-less primitives (`$color-white`) as well as + * `$color-gray-600`. Deliberately does NOT match the `-rgb`/`-id` companions + * (`$color-gray-600-rgb`) — the trailing `(?![\w-])` stops before the `-rgb`. + * A leading `\w`/`.`/`-` is disallowed so namespaced (`color.$color-…`) refs are + * left for stage 2 rather than mis-rewritten. + */ +export const SCSS_COLOR_VAR_REFERENCE = /(? + /var\(\s*--color-/i.test(value) || /(? + /\brgba?\(/i.test(value) || /\$color-[a-z0-9-]+-rgb\b/i.test(value) + +/** Tailwind utility roots and the semantic group each targets. */ +export const UTILITY_ROOT_GROUP: Record = { + bg: 'background', + text: 'text', + fill: 'foreground', + stroke: 'foreground', + border: 'border', +} + +/** Primitive colour families used to recognise a Tailwind *color* utility. */ +const COLOR_FAMILIES = 'gray|blue|red|green|yellow|orange|purple|teal|pink|white|black' + +/** Tailwind utility roots, alternation-ready. */ +const UTILITY_ROOTS = Object.keys(UTILITY_ROOT_GROUP).join('|') + +/** + * Matches a named color utility and captures `[root, primitive]`, e.g. + * `bg-gray-600` → `['bg', 'gray-600']`, `fill-white` → `['fill', 'white']`. + */ +export const NAMED_COLOR_UTILITY = new RegExp( + `^(${UTILITY_ROOTS})-((?:${COLOR_FAMILIES})(?:-\\d+)?)$`, + 'i', +) + +/** + * Matches an arbitrary-value color utility and captures `[root, primitive]`, + * e.g. `bg-[var(--color-gray-600)]` → `['bg', 'gray-600']`. + */ +export const ARBITRARY_COLOR_UTILITY = new RegExp( + `^(${UTILITY_ROOTS})-\\[var\\(\\s*--color-([a-z0-9-]+)\\s*\\)\\]$`, + 'i', +) + +/** + * Heuristic for *any* Tailwind color utility (named or arbitrary) so unmapped + * ones can be reported (skipped) rather than silently ignored. Excludes + * structural utilities (`border-2`, `bg-cover`, `text-sm`, `stroke-2`) and + * already-semantic classes (`bg-secondary-solid`). + */ +export const COLOR_UTILITY = new RegExp( + `^(${UTILITY_ROOTS})-(?:${COLOR_FAMILIES})(?:-\\d+)?$|^(${UTILITY_ROOTS})-\\[.+\\]$`, + 'i', +) diff --git a/packages/components/codemods/migratePrimitivesToSemanticTokens/transformCssSemanticColors.spec.ts b/packages/components/codemods/migratePrimitivesToSemanticTokens/transformCssSemanticColors.spec.ts new file mode 100644 index 00000000000..21adb58fa23 --- /dev/null +++ b/packages/components/codemods/migratePrimitivesToSemanticTokens/transformCssSemanticColors.spec.ts @@ -0,0 +1,79 @@ +import { transformCssSemanticColors } from './transformCssSemanticColors' + +describe('transformCssSemanticColors()', () => { + it('rewrites a text color (color → text-*)', () => { + const { code, converted } = transformCssSemanticColors('.a { color: var(--color-gray-700); }') + expect(code).toContain('color: var(--text-secondary);') + expect(converted).toHaveLength(1) + expect(converted[0]).toMatchObject({ to: 'var(--text-secondary)' }) + }) + + it('rewrites a background color (background / background-color → bg-*)', () => { + const { code } = transformCssSemanticColors( + '.a { background: var(--color-blue-500); } .b { background-color: var(--color-gray-300); }', + ) + expect(code).toContain('background: var(--bg-brand-solid);') + expect(code).toContain('background-color: var(--bg-tertiary);') + }) + + it('rewrites a foreground color (fill / stroke → fg-*)', () => { + const { code } = transformCssSemanticColors( + '.a { fill: var(--color-red-500); stroke: var(--color-blue-500); }', + ) + expect(code).toContain('fill: var(--fg-error-primary);') + expect(code).toContain('stroke: var(--fg-brand-primary);') + }) + + it('rewrites a border color (border → border-*)', () => { + const { code } = transformCssSemanticColors('.a { border: 1px solid var(--color-gray-500); }') + expect(code).toContain('border: 1px solid var(--border-secondary);') + }) + + it('maps the same primitive to a different token per property (context)', () => { + // blue-500 → bg-brand-solid on background, text-brand-primary on color. + const { code } = transformCssSemanticColors( + '.a { background: var(--color-blue-500); color: var(--color-blue-500); }', + ) + expect(code).toContain('background: var(--bg-brand-solid);') + expect(code).toContain('color: var(--text-brand-primary);') + }) + + it('rewrites a group-inferred custom property', () => { + const { code } = transformCssSemanticColors('.a { --text-color: var(--color-purple-800); }') + expect(code).toContain('--text-color: var(--text-primary);') + }) + + it('skips and reports colliding primitives (no confident 1:1 mapping)', () => { + // gray-200 backs both bg-secondary and bg-secondary_hover. + const { code, converted, skipped } = transformCssSemanticColors( + '.a { background: var(--color-gray-200); }', + ) + expect(code).toContain('background: var(--color-gray-200);') + expect(converted).toHaveLength(0) + expect(skipped).toHaveLength(1) + expect(skipped[0]).toMatchObject({ property: 'background', value: 'var(--color-gray-200)' }) + }) + + it('skips and reports primitives with no mapping in the group', () => { + const { skipped } = transformCssSemanticColors('.a { color: var(--color-orange-500); }') + expect(skipped).toHaveLength(1) + }) + + it('skips and reports alpha / rgb() usages for a design decision', () => { + const { code, converted, skipped } = transformCssSemanticColors( + '.a { background: rgba(var(--color-blue-500), 0.5); }', + ) + expect(code).toContain('rgba(var(--color-blue-500), 0.5)') + expect(converted).toHaveLength(0) + expect(skipped).toHaveLength(1) + }) + + it('leaves colors in out-of-scope properties untouched and unreported', () => { + const input = + '.a { box-shadow: 0 0 0 1px var(--color-gray-500); outline-color: var(--color-red-500); }' + const { code, converted, skipped } = transformCssSemanticColors(input) + expect(code).toBe(input) + expect(converted).toHaveLength(0) + expect(skipped).toHaveLength(0) + }) +}) diff --git a/packages/components/codemods/migratePrimitivesToSemanticTokens/transformCssSemanticColors.ts b/packages/components/codemods/migratePrimitivesToSemanticTokens/transformCssSemanticColors.ts new file mode 100644 index 00000000000..55aa601cea6 --- /dev/null +++ b/packages/components/codemods/migratePrimitivesToSemanticTokens/transformCssSemanticColors.ts @@ -0,0 +1,31 @@ +import postcss from 'postcss' +import { + rewriteSemanticColors, + type ConvertedToken, + type SemanticColorResult, + type SkippedToken, +} from './semanticColorPostcss' + +export type { ConvertedToken, SkippedToken } + +export type CssTransformResult = SemanticColorResult & { code: string } + +/** + * Rewrites hard-coded primitive colours to the new semantic tokens in plain + * **CSS**, matching CSS custom property references and selecting the semantic + * group from the declaration property: + * `color: var(--color-gray-700)` → `color: var(--text-secondary)` + * `background-color: var(--color-blue-500)` → `background-color: var(--bg-brand-solid)` + * `fill: var(--color-red-500)` → `fill: var(--fg-error-primary)` + * `border-color: var(--color-gray-500)` → `border-color: var(--border-secondary)` + * + * SCSS `$variable` references are handled by {@link transformScssSemanticColors}. + * Only confident 1:1 mappings within a group are rewritten; unmapped and + * alpha/`rgb()` colours are left untouched and returned in `skipped` for the + * stage-2 fallback. + */ +export const transformCssSemanticColors = (source: string): CssTransformResult => { + const root = postcss.parse(source) + const { converted, skipped } = rewriteSemanticColors(root, { scss: false }) + return { code: root.toString(), converted, skipped } +} diff --git a/packages/components/codemods/migratePrimitivesToSemanticTokens/transformScssSemanticColors.spec.ts b/packages/components/codemods/migratePrimitivesToSemanticTokens/transformScssSemanticColors.spec.ts new file mode 100644 index 00000000000..47eda9eb0c1 --- /dev/null +++ b/packages/components/codemods/migratePrimitivesToSemanticTokens/transformScssSemanticColors.spec.ts @@ -0,0 +1,82 @@ +import { transformScssSemanticColors } from './transformScssSemanticColors' + +describe('transformScssSemanticColors()', () => { + it('rewrites SCSS $color-* variables to the semantic variable per group', () => { + const input = `@import "~@kaizen/design-tokens/sass/color"; +.a { + color: $color-gray-700; + background: $color-blue-500; +}` + const { code, converted } = transformScssSemanticColors(input) + expect(code).toContain('color: $text-secondary;') + expect(code).toContain('background: $bg-brand-solid;') + expect(converted).toHaveLength(2) + }) + + it('rewrites CSS custom property references inside SCSS too', () => { + const input = `.a { + &:hover { color: var(--color-gray-700); } +}` + const { code } = transformScssSemanticColors(input) + expect(code).toContain('color: var(--text-secondary);') + }) + + it('rewrites shade-less primitives ($color-white → fg-white in a fill context)', () => { + const { code } = transformScssSemanticColors('.a { fill: $color-white; }') + expect(code).toContain('fill: $fg-white;') + }) + + it('adds the semantic-color sass import (matching existing style) when introducing a semantic var', () => { + const input = `@import "~@kaizen/design-tokens/sass/color"; +.a { + color: $color-gray-700; +}` + const { code } = transformScssSemanticColors(input) + expect(code).toContain('@import "~@kaizen/design-tokens/sass/semantic-color";') + expect(code).toContain('color: $text-secondary;') + }) + + it('prepends a plain semantic-color import when no kaizen sass import exists', () => { + const { code } = transformScssSemanticColors('.a { background: $color-blue-500; }') + expect(code).toContain("@import '@kaizen/design-tokens/sass/semantic-color';") + expect(code).toContain('background: $bg-brand-solid;') + }) + + it('does not duplicate the semantic-color import when already present', () => { + const input = `@import "~@kaizen/design-tokens/sass/color"; +@import "~@kaizen/design-tokens/sass/semantic-color"; +.a { + color: $color-gray-700; +}` + const { code } = transformScssSemanticColors(input) + expect(code.match(/sass\/semantic-color/g)).toHaveLength(1) + }) + + it('skips and reports rgba()/alpha usages for a design decision', () => { + const input = `@import "~@kaizen/design-tokens/sass/color"; +.footer { + background: rgba($color-gray-600-rgb, 0.1); +}` + const { code, converted, skipped } = transformScssSemanticColors(input) + expect(code).toContain('background: rgba($color-gray-600-rgb, 0.1);') + expect(converted).toHaveLength(0) + expect(skipped).toHaveLength(1) + expect(skipped[0]).toMatchObject({ property: 'background' }) + }) + + it('skips and reports colliding / unmapped $color-* variables', () => { + const input = '.a { background: $color-gray-200; color: $color-gray-400; }' + const { code, converted, skipped } = transformScssSemanticColors(input) + expect(code).toContain('background: $color-gray-200;') + expect(code).toContain('color: $color-gray-400;') + expect(converted).toHaveLength(0) + expect(skipped).toHaveLength(2) + }) + + it('leaves out-of-scope $color-* usages untouched', () => { + const input = '.a { box-shadow: 0 0 1px $color-gray-500; }' + const { code, converted } = transformScssSemanticColors(input) + expect(code).toBe(input) + expect(converted).toHaveLength(0) + }) +}) diff --git a/packages/components/codemods/migratePrimitivesToSemanticTokens/transformScssSemanticColors.ts b/packages/components/codemods/migratePrimitivesToSemanticTokens/transformScssSemanticColors.ts new file mode 100644 index 00000000000..a3bc4ad3586 --- /dev/null +++ b/packages/components/codemods/migratePrimitivesToSemanticTokens/transformScssSemanticColors.ts @@ -0,0 +1,32 @@ +import scssSyntax from 'postcss-scss' +import { + rewriteSemanticColors, + type ConvertedToken, + type SemanticColorResult, + type SkippedToken, +} from './semanticColorPostcss' + +export type { ConvertedToken, SkippedToken } + +export type ScssTransformResult = SemanticColorResult & { code: string } + +/** + * Rewrites hard-coded primitive colours to the new semantic tokens in **SCSS**, + * covering both reference styles a `.scss` file can use and selecting the + * semantic group from the declaration property: + * - SCSS variable (Kaizen sass): `color: $color-gray-700` → `$text-secondary` + * - CSS custom property: `background-color: var(--color-blue-500)` → `var(--bg-brand-solid)` + * + * When a semantic `$*` variable is introduced, the + * `@kaizen/design-tokens/sass/semantic-color` import is added (matching the + * file's existing import style) so the output still compiles. Only confident + * 1:1 mappings within a group are rewritten; unmapped colours and + * alpha/`rgb()`/`rgba()` usages such as `rgba($color-gray-600-rgb, 0.1)` are + * left untouched and returned in `skipped` for the stage-2 fallback. Nested + * rules, `@layer`, and other SCSS constructs are preserved by the SCSS parser. + */ +export const transformScssSemanticColors = (source: string): ScssTransformResult => { + const root = scssSyntax.parse(source) + const { converted, skipped } = rewriteSemanticColors(root, { scss: true }) + return { code: root.toString(), converted, skipped } +} diff --git a/packages/components/codemods/migratePrimitivesToSemanticTokens/transformTailwindSemanticClasses.spec.ts b/packages/components/codemods/migratePrimitivesToSemanticTokens/transformTailwindSemanticClasses.spec.ts new file mode 100644 index 00000000000..0ff65a06b8c --- /dev/null +++ b/packages/components/codemods/migratePrimitivesToSemanticTokens/transformTailwindSemanticClasses.spec.ts @@ -0,0 +1,78 @@ +import { transformTailwindSemanticClasses } from './transformTailwindSemanticClasses' + +describe('transformTailwindSemanticClasses()', () => { + it('rewrites bg / text / border color utilities in a className attribute', () => { + const input = `const A = () =>
` + const { code, converted } = transformTailwindSemanticClasses(input) + expect(code).toContain('className="bg-brand-solid text-secondary border-secondary p-2"') + expect(converted).toHaveLength(3) + }) + + it('maps fill / stroke utilities to the fg-* semantic utility', () => { + const input = `const A = () =>
` + const { code } = transformTailwindSemanticClasses(input) + expect(code).toContain('className="fg-error-primary fg-brand-primary"') + }) + + it('maps the same primitive to a different utility per root (context)', () => { + const input = `const A = () =>
` + const { code } = transformTailwindSemanticClasses(input) + expect(code).toContain('bg-brand-solid') + expect(code).toContain('text-brand-primary') + expect(code).toContain('border-brand') + }) + + it('rewrites an arbitrary-value utility', () => { + const input = `const A = () =>
` + const { code } = transformTailwindSemanticClasses(input) + expect(code).toContain('className="bg-brand-solid"') + }) + + it('rewrites classes inside a clsx call, including object keys', () => { + const input = `const A = () =>
` + const { code } = transformTailwindSemanticClasses(input) + expect(code).toContain("'text-secondary'") + expect(code).toContain("'bg-brand-solid'") + }) + + it('preserves a Tailwind utility prefix (goals-)', () => { + const input = `const A = () =>
` + const { code, converted } = transformTailwindSemanticClasses(input, { prefix: 'goals-' }) + expect(code).toContain('className="goals-bg-brand-solid"') + expect(converted[0]).toMatchObject({ to: 'goals-bg-brand-solid' }) + }) + + it('preserves a colon namespace prefix (EP:) and stacked variants', () => { + const input = `const A = () =>
` + const { code } = transformTailwindSemanticClasses(input) + expect(code).toContain('EP:bg-brand-solid') + expect(code).toContain('hover:text-secondary') + }) + + it('skips dynamic class names and reports unmapped / colliding color utilities', () => { + const input = `const A = () =>
` + const { code, converted, skipped } = transformTailwindSemanticClasses(input) + expect(converted).toHaveLength(0) + expect(code).toContain('bg-gray-200') + expect(code).toContain('text-white') + expect(skipped).toEqual([ + { line: 1, token: 'bg-gray-200' }, + { line: 1, token: 'text-white' }, + ]) + }) + + it('leaves structural and already-semantic utilities untouched', () => { + const input = `const A = () =>
` + const { code, converted, skipped } = transformTailwindSemanticClasses(input) + expect(code).toBe(input) + expect(converted).toHaveLength(0) + expect(skipped).toHaveLength(0) + }) + + it('leaves non-className strings untouched', () => { + const input = `const label = "bg-blue-500"` + const { code, converted } = transformTailwindSemanticClasses(input) + expect(code).toBe(input) + expect(converted).toHaveLength(0) + }) +}) diff --git a/packages/components/codemods/migratePrimitivesToSemanticTokens/transformTailwindSemanticClasses.ts b/packages/components/codemods/migratePrimitivesToSemanticTokens/transformTailwindSemanticClasses.ts new file mode 100644 index 00000000000..5741de3e811 --- /dev/null +++ b/packages/components/codemods/migratePrimitivesToSemanticTokens/transformTailwindSemanticClasses.ts @@ -0,0 +1,182 @@ +import ts from 'typescript' +import { + ARBITRARY_COLOR_UTILITY, + COLOR_UTILITY, + NAMED_COLOR_UTILITY, + PRIMITIVE_TO_SEMANTIC, + UTILITY_ROOT_GROUP, +} from './semanticTokenMap' + +export type ConvertedClass = { line: number; from: string; to: string } +export type SkippedClass = { line: number; token: string } + +export type TailwindTransformResult = { + code: string + converted: ConvertedClass[] + skipped: SkippedClass[] +} + +/** JSX attributes whose string value is a class list. */ +const CLASSNAME_ATTRIBUTES = new Set(['className', 'class']) +/** Call expressions whose string arguments are class names. */ +const CLASSNAME_FUNCTIONS = new Set(['clsx', 'classnames', 'classNames', 'cx', 'cn']) + +type TokenResult = + | { status: 'converted'; token: string } + | { status: 'skipped' } + | { status: 'unchanged' } + +/** + * Resolves the semantic utility for a bare Tailwind color utility (variant / + * prefix already stripped), or `null` if it isn't a color utility we recognise. + * The semantic token name already carries its group prefix (`bg-`, `text-`, + * `fg-`, `border-`) so it doubles as the replacement utility — a `fill-`/ + * `stroke-` primitive therefore maps to an `fg-*` utility. + */ +const mapUtility = (utility: string): { mapped: string } | 'skip' | null => { + const named = NAMED_COLOR_UTILITY.exec(utility) + const arbitrary = named ? null : ARBITRARY_COLOR_UTILITY.exec(utility) + const match = named ?? arbitrary + if (match) { + const root = match[1].toLowerCase() + const primitive = match[2].toLowerCase() + const group = UTILITY_ROOT_GROUP[root] + const semantic = group && PRIMITIVE_TO_SEMANTIC[group][primitive] + if (semantic) return { mapped: semantic } + return 'skip' + } + // A color utility we can't confidently map (unmapped family / arbitrary) — + // report for stage 2. + if (COLOR_UTILITY.test(utility)) return 'skip' + return null +} + +/** + * Transforms a single class token, preserving any variant chain (`hover:`, + * `md:`, or a namespace prefix like `EP:`) and any configured utility prefix + * (Tailwind `prefix`, like `goals-`). Only the utility portion is matched + * against the color maps; the prefix/variant is re-attached verbatim. + */ +const transformToken = (token: string, prefix: string): TokenResult => { + // Split off the variant chain: everything up to and including the last colon. + // This handles stacked variants (`hover:md:`) and colon-style prefixes (`EP:`). + const lastColon = token.lastIndexOf(':') + const variant = lastColon >= 0 ? token.slice(0, lastColon + 1) : '' + let utility = lastColon >= 0 ? token.slice(lastColon + 1) : token + + // Strip a configured dash-style utility prefix (e.g. `goals-`) so we can match + // the bare utility, then re-apply it. Colon-style prefixes are already handled + // by the variant split above. + let utilityPrefix = '' + if (prefix && !prefix.endsWith(':') && utility.startsWith(prefix)) { + utilityPrefix = prefix + utility = utility.slice(prefix.length) + } + + const result = mapUtility(utility) + if (result === null) return { status: 'unchanged' } + if (result === 'skip') return { status: 'skipped' } + return { status: 'converted', token: `${variant}${utilityPrefix}${result.mapped}` } +} + +const transformClassValue = ( + text: string, + prefix: string, + line: number, + converted: ConvertedClass[], + skipped: SkippedClass[], +): { value: string; changed: boolean } => { + let changed = false + const value = text + .split(/(\s+)/) // keep whitespace separators so spacing is preserved + .map((part) => { + if (part.trim() === '') return part + const result = transformToken(part, prefix) + if (result.status === 'converted') { + converted.push({ line, from: part, to: result.token }) + changed = true + return result.token + } + if (result.status === 'skipped') skipped.push({ line, token: part }) + return part + }) + .join('') + return { value, changed } +} + +/** + * Rewrites Tailwind color utilities (`bg-*`, `text-*`, `fill-*`, `stroke-*`, + * `border-*`) in `className`/`class` attributes and `clsx`-style calls to the + * new semantic utilities, honouring a consumer Tailwind `prefix` (e.g. `goals-` + * or `EP:`). + * + * Edits are spliced back into the original source by node position, so file + * formatting is otherwise preserved (run prettier afterwards regardless). + * Dynamic/computed class names and unmapped color utilities are left untouched; + * unmapped color utilities are returned in `skipped` for the stage-2 fallback. + */ +export const transformTailwindSemanticClasses = ( + source: string, + { + prefix = '', + scriptKind = ts.ScriptKind.TSX, + }: { prefix?: string; scriptKind?: ts.ScriptKind } = {}, +): TailwindTransformResult => { + const sourceFile = ts.createSourceFile( + 'temp.tsx', + source, + ts.ScriptTarget.Latest, + true, + scriptKind, + ) + + const converted: ConvertedClass[] = [] + const skipped: SkippedClass[] = [] + // Inner-text edits keyed by start position to avoid processing a literal twice + // (e.g. a clsx call that also lives inside a className attribute). + const edits = new Map() + + const collectFromStringLiteral = (node: ts.StringLiteralLike): void => { + const start = node.getStart(sourceFile) + if (edits.has(start)) return + const line = sourceFile.getLineAndCharacterOfPosition(start).line + 1 + const { value, changed } = transformClassValue(node.text, prefix, line, converted, skipped) + if (changed) { + // Replace the inner text only, leaving the surrounding quotes/backticks intact. + edits.set(start, { start: start + 1, end: node.getEnd() - 1, text: value }) + } else { + edits.set(start, { start, end: start, text: '' }) // mark as seen, no-op + } + } + + const collectStrings = (node: ts.Node): void => { + if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) { + collectFromStringLiteral(node) + return + } + ts.forEachChild(node, collectStrings) + } + + const visit = (node: ts.Node): void => { + if (ts.isJsxAttribute(node) && CLASSNAME_ATTRIBUTES.has(node.name.getText(sourceFile))) { + if (node.initializer) collectStrings(node.initializer) + } + if (ts.isCallExpression(node) && CLASSNAME_FUNCTIONS.has(node.expression.getText(sourceFile))) { + node.arguments.forEach(collectStrings) + } + ts.forEachChild(node, visit) + } + visit(sourceFile) + + // Apply edits back-to-front so earlier offsets stay valid. + const realEdits = Array.from(edits.values()) + .filter((edit) => edit.end > edit.start) + .sort((a, b) => b.start - a.start) + + let code = source + for (const edit of realEdits) { + code = code.slice(0, edit.start) + edit.text + code.slice(edit.end) + } + + return { code, converted, skipped } +} diff --git a/packages/components/src/Avatar/Avatar.module.css b/packages/components/src/Avatar/Avatar.module.css index b4a5849c2a9..28646f04352 100644 --- a/packages/components/src/Avatar/Avatar.module.css +++ b/packages/components/src/Avatar/Avatar.module.css @@ -2,7 +2,7 @@ .wrapper { --avatar-x-y: 1.5rem; - background: var(--color-gray-300); + background: var(--bg-tertiary); border-radius: 100%; box-sizing: border-box; overflow: hidden; @@ -44,7 +44,7 @@ } .otherUser { - background: var(--color-gray-300); + background: var(--bg-tertiary); } .company { @@ -86,7 +86,7 @@ .initials { box-sizing: border-box; - color: var(--color-purple-800); + color: var(--text-primary); padding-left: 5px; padding-right: 5px; text-align: center; diff --git a/packages/components/src/AvatarGroup/AvatarGroup.module.css b/packages/components/src/AvatarGroup/AvatarGroup.module.css index 81a7a76d128..b7c41fe42b3 100644 --- a/packages/components/src/AvatarGroup/AvatarGroup.module.css +++ b/packages/components/src/AvatarGroup/AvatarGroup.module.css @@ -9,7 +9,7 @@ .AvatarCounter { direction: ltr; align-items: center; - background: var(--color-gray-300); + background: var(--bg-tertiary); border: 3px solid var(--color-white); border-radius: 100%; box-sizing: border-box; diff --git a/packages/components/src/Badge/Badge.module.css b/packages/components/src/Badge/Badge.module.css index a133f73b62a..61e6afe5322 100644 --- a/packages/components/src/Badge/Badge.module.css +++ b/packages/components/src/Badge/Badge.module.css @@ -1,7 +1,7 @@ @layer kz-components { .badge { border-radius: var(--spacing-12); - color: var(--color-purple-800); + color: var(--text-primary); display: inline-block; position: relative; font-family: var(--typography-paragraph-extra-small-font-family); @@ -12,7 +12,7 @@ padding: 1px var(--spacing-6); min-width: 8px; text-align: center; - background-color: var(--badge-background-color, var(--color-gray-300)); + background-color: var(--badge-background-color, var(--bg-tertiary)); } .reversed { @@ -22,9 +22,9 @@ } .reversed.active { - --badge-background-color: var(--color-green-300); + --badge-background-color: var(--bg-success-secondary); - color: var(--color-purple-800); + color: var(--text-primary); } .reversed.dark { @@ -45,7 +45,7 @@ } .active { - --badge-background-color: var(--color-blue-500); + --badge-background-color: var(--bg-brand-solid); color: var(--color-white); } @@ -53,11 +53,11 @@ .dark { --badge-background-color: rgb(var(--color-purple-700-rgb), 0.1); - color: var(--color-purple-800); + color: var(--text-primary); } .dot { - --badge-background-color: var(--color-green-300); + --badge-background-color: var(--bg-success-secondary); padding: var(--spacing-6); min-width: unset; @@ -92,7 +92,7 @@ } .animationOn .badge.active { - --badge-background-color: var(--color-blue-500); + --badge-background-color: var(--bg-brand-solid); color: var(--color-white); } diff --git a/packages/components/src/BrandMoment/BrandMoment.module.css b/packages/components/src/BrandMoment/BrandMoment.module.css index 1490f5d1894..c4ac3795cae 100644 --- a/packages/components/src/BrandMoment/BrandMoment.module.css +++ b/packages/components/src/BrandMoment/BrandMoment.module.css @@ -10,24 +10,24 @@ /** @deprecated */ .positive { - --brand-moment-background-color: var(--color-green-100); + --brand-moment-background-color: var(--bg-success-primary); } .negative { - --brand-moment-background-color: var(--color-red-100); + --brand-moment-background-color: var(--bg-error-primary); } /** end @deprecated */ .informative { - --brand-moment-background-color: var(--color-blue-100); + --brand-moment-background-color: var(--bg-brand-primary); } .success { - --brand-moment-background-color: var(--color-green-100); + --brand-moment-background-color: var(--bg-success-primary); } .warning { - --brand-moment-background-color: var(--color-red-100); + --brand-moment-background-color: var(--bg-error-primary); } .container { diff --git a/packages/components/src/Button/Button.module.css b/packages/components/src/Button/Button.module.css index 9e915131270..7c668b3d477 100644 --- a/packages/components/src/Button/Button.module.css +++ b/packages/components/src/Button/Button.module.css @@ -14,10 +14,10 @@ --button-padding-y: calc(var(--spacing-8) - var(--button-border-width)); --button-padding-x: calc(var(--spacing-16) - var(--button-border-width)); - background-color: var(--button-bg-color, var(--color-blue-500)); + background-color: var(--button-bg-color, var(--bg-brand-solid)); border: var(--button-border-width) solid; border-radius: var(--border-solid-border-radius); - border-color: var(--button-border-color, var(--color-blue-500)); + border-color: var(--button-border-color, var(--border-brand)); box-sizing: border-box; color: var(--button-text-color, var(--color-white)); display: inline-flex; @@ -39,12 +39,12 @@ } &[data-pressed] { - --button-bg-color: var(--color-blue-700); + --button-bg-color: var(--bg-brand-solid_hover); --button-border-color: var(--color-blue-700); } &[data-pending] { - --button-bg-color: var(--color-blue-700); + --button-bg-color: var(--bg-brand-solid_hover); --button-border-color: var(--color-blue-700); } @@ -99,21 +99,21 @@ .secondary { --button-bg-color: var(--color-white); - --button-border-color: var(--color-gray-500); - --button-text-color: var(--color-purple-800); + --button-border-color: var(--border-secondary); + --button-text-color: var(--text-primary); &[data-hovered] { --button-bg-color: var(--color-gray-200); - --button-border-color: var(--color-gray-600); + --button-border-color: var(--border-primary); } &[data-pressed] { - --button-bg-color: var(--color-gray-300); + --button-bg-color: var(--bg-tertiary); --button-border-color: var(--color-black); } &[data-pending] { - --button-bg-color: var(--color-gray-300); + --button-bg-color: var(--bg-tertiary); --button-border-color: var(--color-black); } @@ -127,26 +127,26 @@ .tertiary { --button-bg-color: transparent; --button-border-color: transparent; - --button-text-color: var(--color-purple-800); + --button-text-color: var(--text-primary); &[data-hovered], &[data-pressed], &[data-pending] { - --button-text-color: var(--color-blue-500); + --button-text-color: var(--text-brand-primary); } &[data-hovered] { - --button-bg-color: var(--color-blue-200); + --button-bg-color: var(--bg-brand-secondary); --button-border-color: var(--color-blue-200); } &[data-pressed] { - --button-bg-color: var(--color-blue-100); + --button-bg-color: var(--bg-brand-primary); --button-border-color: var(--color-blue-100); } &[data-pending] { - --button-bg-color: var(--color-blue-100); + --button-bg-color: var(--bg-brand-primary); --button-border-color: var(--color-blue-100); } @@ -168,16 +168,16 @@ .primaryReversed { --button-bg-color: var(--color-white); --button-border-color: var(--color-white); - --button-text-color: var(--color-purple-800); + --button-text-color: var(--text-primary); &[data-hovered] { --button-bg-color: var(--color-gray-200); - --button-border-color: var(--color-gray-200); + --button-border-color: var(--border-secondary_alt); } &[data-pressed] { - --button-bg-color: var(--color-gray-300); - --button-border-color: var(--color-gray-300); + --button-bg-color: var(--bg-tertiary); + --button-border-color: var(--border-tertiary); } &[data-pending] { diff --git a/packages/components/src/ButtonGroup/ButtonGroup.module.css b/packages/components/src/ButtonGroup/ButtonGroup.module.css index d918d6f3768..bd08c214a2e 100644 --- a/packages/components/src/ButtonGroup/ButtonGroup.module.css +++ b/packages/components/src/ButtonGroup/ButtonGroup.module.css @@ -15,7 +15,7 @@ } .child:not(.firstChild) { - border-inline-start: 1px solid var(--color-blue-300); + border-inline-start: 1px solid var(--border-brand_alt); } .child:not(.firstChild, .lastChild) { diff --git a/packages/components/src/ButtonGroup/ButtonGroup.module.scss b/packages/components/src/ButtonGroup/ButtonGroup.module.scss index c9abf4089d1..7612b8523c8 100644 --- a/packages/components/src/ButtonGroup/ButtonGroup.module.scss +++ b/packages/components/src/ButtonGroup/ButtonGroup.module.scss @@ -1,4 +1,5 @@ @import '~@kaizen/design-tokens/sass/border'; +@import '~@kaizen/design-tokens/sass/semantic-color'; @import '~@kaizen/design-tokens/sass/color'; @layer kz-components { @@ -28,7 +29,7 @@ } &:not(.firstChild) { - border-inline-start: 1px solid $color-blue-300; + border-inline-start: 1px solid $border-brand_alt; } &:not(.firstChild, .lastChild) { diff --git a/packages/components/src/ButtonV1/Button/Button.module.scss b/packages/components/src/ButtonV1/Button/Button.module.scss index 18b10f54c20..58c0a5f8749 100644 --- a/packages/components/src/ButtonV1/Button/Button.module.scss +++ b/packages/components/src/ButtonV1/Button/Button.module.scss @@ -1,4 +1,5 @@ @import '~@kaizen/design-tokens/sass/spacing'; +@import '~@kaizen/design-tokens/sass/semantic-color'; @import '~@kaizen/design-tokens/sass/color'; @import '~@kaizen/design-tokens/sass/border'; @import '~@kaizen/design-tokens/sass/typography'; @@ -18,8 +19,9 @@ line-height: $typography-button-primary-line-height; letter-spacing: $typography-button-primary-letter-spacing; display: inline-flex; - // ^inline-flex is used over (block) flex here to fix an edge case where the parent element is display:grid - // and this element is an , causing the element to be full width. + + /* ^inline-flex is used over (block) flex here to fix an edge case where the parent element is display:grid */ + /* and this element is an , causing the element to be full width. */ align-items: center; box-sizing: border-box; min-height: $button-height; @@ -28,7 +30,7 @@ position: relative; text-align: left; cursor: pointer; - overflow: visible; // Required for the focus ring on IE11 + overflow: visible; /* Required for the focus ring on IE11 */ &[href] { text-decoration: none; @@ -51,7 +53,7 @@ content: ''; position: absolute; background: transparent; - border-color: $color-blue-500; + border-color: $border-brand; border-radius: $border-focus-ring-border-radius; border-width: $border-focus-ring-border-width; border-style: $border-focus-ring-border-style; @@ -62,7 +64,7 @@ &.reversed { &:focus-visible { &::after { - border-color: $color-blue-300; + border-color: $border-brand_alt; } &.disabled, @@ -96,8 +98,8 @@ %variant-default { background-color: $color-white; - border-color: $color-gray-500; - color: $color-purple-800; + border-color: $border-secondary; + color: $text-primary; @include enabled-pseudo-states-variant($color-gray-200, $color-gray-600); @@ -111,16 +113,16 @@ } %variant-primary { - background-color: $color-blue-500; + background-color: $bg-brand-solid; border-color: $border-borderless-border-color; color: $color-white; @include enabled-pseudo-states-variant($color-blue-600, $border-borderless-border-color); &.reversed { - background-color: $color-green-300; + background-color: $bg-success-secondary; border-color: $border-borderless-border-color; - color: $color-purple-800; + color: $text-primary; @include enabled-pseudo-states-variant($color-green-400, $border-borderless-border-color); } @@ -134,9 +136,9 @@ @include enabled-pseudo-states-variant($color-red-600, $border-borderless-border-color); &.reversed { - background-color: $color-red-300; + background-color: $bg-error-secondary; border-color: $border-borderless-border-color; - color: $color-purple-800; + color: $text-primary; @include enabled-pseudo-states-variant($color-red-400, $border-borderless-border-color); } @@ -150,7 +152,7 @@ letter-spacing: $typography-button-secondary-letter-spacing; background-color: transparent; border-color: $border-borderless-border-color; - color: $color-blue-500; + color: $text-brand-primary; @include enabled-pseudo-states-variant($color-blue-100, $border-borderless-border-color); @@ -171,7 +173,7 @@ } %variant-secondary-destructive { - color: $color-red-600; + color: $text-error-primary; @include enabled-pseudo-states-variant($color-red-100, $border-borderless-border-color); } diff --git a/packages/components/src/Calendar/CalendarRange/CalendarRange.module.scss b/packages/components/src/Calendar/CalendarRange/CalendarRange.module.scss index 6d11074b519..5b94badbdce 100644 --- a/packages/components/src/Calendar/CalendarRange/CalendarRange.module.scss +++ b/packages/components/src/Calendar/CalendarRange/CalendarRange.module.scss @@ -1,4 +1,5 @@ @import '~@kaizen/design-tokens/sass/color'; +@import '~@kaizen/design-tokens/sass/semantic-color'; @import '~@kaizen/design-tokens/sass/spacing'; @layer kz-components { @@ -25,7 +26,7 @@ position: absolute; content: ''; inset: calc(-1 * #{$spacing-md}) 0; - border-inline-start: 1px solid $color-gray-300; + border-inline-start: 1px solid $border-tertiary; } } @@ -43,22 +44,22 @@ display: flex; justify-content: space-between; width: 100%; - color: $color-purple-800; + color: $text-primary; top: -0.25rem; } .dayRangeStart, .dayRangeEnd { - background-color: $color-blue-500; + background-color: $bg-brand-solid; color: $color-white; } .dayRangeMiddle { - background-color: $color-blue-100; - color: $color-blue-500; + background-color: $bg-brand-primary; + color: $text-brand-primary; & [class*='_button_'] { - color: $color-blue-500; + color: $text-brand-primary; } } diff --git a/packages/components/src/Calendar/CalendarSingle/CalendarSingle.module.scss b/packages/components/src/Calendar/CalendarSingle/CalendarSingle.module.scss index f9ecddd555f..e57bba774ad 100644 --- a/packages/components/src/Calendar/CalendarSingle/CalendarSingle.module.scss +++ b/packages/components/src/Calendar/CalendarSingle/CalendarSingle.module.scss @@ -1,4 +1,5 @@ @import '~@kaizen/design-tokens/sass/color'; +@import '~@kaizen/design-tokens/sass/semantic-color'; @layer kz-components { .nav { @@ -6,7 +7,7 @@ display: flex; justify-content: space-between; width: 100%; - color: $color-purple-800; + color: $text-primary; top: -0.25rem; } diff --git a/packages/components/src/Calendar/LegacyCalendarRange/LegacyCalendarRange.module.scss b/packages/components/src/Calendar/LegacyCalendarRange/LegacyCalendarRange.module.scss index c3be26611b4..8159feb7d7f 100644 --- a/packages/components/src/Calendar/LegacyCalendarRange/LegacyCalendarRange.module.scss +++ b/packages/components/src/Calendar/LegacyCalendarRange/LegacyCalendarRange.module.scss @@ -1,4 +1,5 @@ @import '~@kaizen/design-tokens/sass/color'; +@import '~@kaizen/design-tokens/sass/semantic-color'; @layer kz-components { .nav { @@ -6,22 +7,22 @@ display: flex; justify-content: space-between; width: 100%; - color: $color-purple-800; + color: $text-primary; top: -0.25rem; } .dayRangeStart, .dayRangeEnd { - background-color: $color-blue-500; + background-color: $bg-brand-solid; color: $color-white; } .dayRangeMiddle { - background-color: $color-blue-100; - color: $color-blue-500; + background-color: $bg-brand-primary; + color: $text-brand-primary; & [class*='_button_'] { - color: $color-blue-500; + color: $text-brand-primary; } } diff --git a/packages/components/src/Calendar/baseCalendarClassNames.module.scss b/packages/components/src/Calendar/baseCalendarClassNames.module.scss index 458373e0523..011708f45fd 100644 --- a/packages/components/src/Calendar/baseCalendarClassNames.module.scss +++ b/packages/components/src/Calendar/baseCalendarClassNames.module.scss @@ -1,4 +1,5 @@ @import '~@kaizen/design-tokens/sass/typography'; +@import '~@kaizen/design-tokens/sass/semantic-color'; @import '~@kaizen/design-tokens/sass/color'; @import '~@kaizen/design-tokens/sass/spacing'; @import '~@kaizen/design-tokens/sass/border'; @@ -35,7 +36,7 @@ &:focus-visible { &::after { - border: $border-focus-ring-border-width $border-focus-ring-border-style $color-blue-500; + border: $border-focus-ring-border-width $border-focus-ring-border-style $border-brand; } } } @@ -77,7 +78,7 @@ font-weight: $typography-heading-5-font-weight; line-height: $typography-heading-4-line-height; letter-spacing: $typography-heading-4-letter-spacing; - color: $color-purple-800; + color: $text-primary; } .navButton { @@ -89,13 +90,13 @@ border-radius: $button-border-radius; &:hover { - background-color: $color-blue-100; - color: $color-blue-500; + background-color: $bg-brand-primary; + color: $text-brand-primary; } &:focus-visible { - background-color: $color-blue-100; - color: $color-blue-500; + background-color: $bg-brand-primary; + color: $text-brand-primary; } } @@ -122,7 +123,7 @@ letter-spacing: $typography-paragraph-body-letter-spacing; font-weight: $typography-paragraph-body-font-weight; line-height: $typography-paragraph-body-line-height; - color: $color-purple-800; + color: $text-primary; } .cell { @@ -147,26 +148,26 @@ $border-borderless-border-color; border-radius: $button-border-radius; padding: calc(#{$spacing-md} / 3) 0.625rem; - color: $color-purple-800; + color: $text-primary; &:hover { - background-color: $color-blue-100; - color: $color-blue-500; + background-color: $bg-brand-primary; + color: $text-brand-primary; } &:focus-visible { - background-color: $color-blue-100; - color: $color-blue-500; + background-color: $bg-brand-primary; + color: $text-brand-primary; } } .dayToday { - color: $color-blue-500; + color: $text-brand-primary; font-weight: bold; } .daySelected { - background-color: $color-blue-500; + background-color: $bg-brand-solid; border-radius: $button-border-radius; color: $color-white; @@ -182,10 +183,10 @@ } .dayDisabled { - // Focus ring styles have been removed as react-day-picker has been updated such that - // disabled days do not receive focus, and keyboard navigation only lands on available days. - // https://github.com/gpbl/react-day-picker/pull/1519 - // https://github.com/gpbl/react-day-picker/issues/1449#issuecomment-1149942033 + /* Focus ring styles have been removed as react-day-picker has been updated such that */ + /* disabled days do not receive focus, and keyboard navigation only lands on available days. */ + /* https://github.com/gpbl/react-day-picker/pull/1519 */ + /* https://github.com/gpbl/react-day-picker/issues/1449#issuecomment-1149942033 */ background: none; pointer-events: none; diff --git a/packages/components/src/Card/Card.module.css b/packages/components/src/Card/Card.module.css index ab67b81fa29..4751db2c239 100644 --- a/packages/components/src/Card/Card.module.css +++ b/packages/components/src/Card/Card.module.css @@ -1,12 +1,12 @@ @layer kz-components { .wrapper { - color: var(--color-purple-800); + color: var(--text-primary); border: var(--border-width-1) solid var(--card-border-color); background-color: var(--card-background-color); border-radius: var(--border-solid-border-radius); box-shadow: var(--card-box-shadow); - --card-border-color: var(--color-gray-500); + --card-border-color: var(--border-secondary); --card-background-color: var(--color-white); --card-box-shadow: var(--shadow-small-box-shadow); } @@ -16,17 +16,17 @@ } .blue { - --card-background-color: var(--color-blue-100); + --card-background-color: var(--bg-brand-primary); --card-border-color: var(--color-blue-400); } .gray { --card-background-color: var(--color-gray-200); - --card-border-color: var(--color-gray-500); + --card-border-color: var(--border-secondary); } .green { - --card-background-color: var(--color-green-100); + --card-background-color: var(--bg-success-primary); --card-border-color: var(--color-green-500); } @@ -41,17 +41,17 @@ } .red { - --card-background-color: var(--color-red-100); - --card-border-color: var(--color-red-500); + --card-background-color: var(--bg-error-primary); + --card-border-color: var(--border-error); } .white { --card-background-color: var(--color-white); - --card-border-color: var(--color-gray-500); + --card-border-color: var(--border-secondary); } .yellow { - --card-background-color: var(--color-yellow-100); + --card-background-color: var(--bg-warning-primary); --card-border-color: var(--color-yellow-700); } } diff --git a/packages/components/src/ClearButton/ClearButton.module.css b/packages/components/src/ClearButton/ClearButton.module.css index 90f5f1f4fcb..f4e641f423f 100644 --- a/packages/components/src/ClearButton/ClearButton.module.css +++ b/packages/components/src/ClearButton/ClearButton.module.css @@ -28,7 +28,7 @@ &:hover, &:focus-visible { - color: var(--color-purple-800); + color: var(--text-primary); } } diff --git a/packages/components/src/Collapsible/Collapsible/Collapsible.module.scss b/packages/components/src/Collapsible/Collapsible/Collapsible.module.scss index 3d1c74be9c6..f934b1559a1 100644 --- a/packages/components/src/Collapsible/Collapsible/Collapsible.module.scss +++ b/packages/components/src/Collapsible/Collapsible/Collapsible.module.scss @@ -9,12 +9,12 @@ $divider-color: rgba($color-gray-600-rgb, 0.2); $container-border-width: var(--border-width-1); - // We need a full border radius on this container element, then have classes - // beneath to toggle nested borders off and on for different use cases. + /* We need a full border radius on this container element, then have classes */ + /* beneath to toggle nested borders off and on for different use cases. */ .container { background-color: $color-white; box-shadow: $shadow-small-box-shadow; - border: $container-border-width var(--border-solid-border-style) var(--color-gray-500); + border: $container-border-width var(--border-solid-border-style) var(--border-secondary); border-radius: $border-borderless-border-radius; } @@ -33,8 +33,8 @@ background-color: $heading-active-color; } - // Round the bottom corners of the header so when the container is open, the - // header background is not rounded on the corners and flush with the content beneath. + /* Round the bottom corners of the header so when the container is open, the */ + /* header background is not rounded on the corners and flush with the content beneath. */ &.open { border-bottom-left-radius: 0; border-bottom-right-radius: 0; @@ -49,9 +49,9 @@ } } - // When a collapsible group is rendered, we need the first group to have a rounded - // on top and the last group to have a rounded bottom edge. Then when the last group - // is open we remove the rounded edge as the content sits beneath and needs to be straight. + /* When a collapsible group is rendered, we need the first group to have a rounded */ + /* on top and the last group to have a rounded bottom edge. Then when the last group */ + /* is open we remove the rounded edge as the content sits beneath and needs to be straight. */ .groupItem { & + .groupItem { border-top: 1px $border-solid-border-style $divider-color; @@ -76,7 +76,7 @@ } .chevronButton:hover { - // hack to get rid of the IconButton hover styling because it clashes with the hover styling on the Collapsible header + /* hack to get rid of the IconButton hover styling because it clashes with the hover styling on the Collapsible header */ background-color: transparent !important; /* stylelint-disable-line declaration-no-important */ } diff --git a/packages/components/src/Collapsible/CollapsibleGroup/CollapsibleGroup.module.scss b/packages/components/src/Collapsible/CollapsibleGroup/CollapsibleGroup.module.scss index ca1af8149e1..b521723ed75 100644 --- a/packages/components/src/Collapsible/CollapsibleGroup/CollapsibleGroup.module.scss +++ b/packages/components/src/Collapsible/CollapsibleGroup/CollapsibleGroup.module.scss @@ -2,12 +2,12 @@ @import '~@kaizen/design-tokens/sass/shadow'; @layer kz-components { - // We need a full border radius on this container element, then have classes - // beneath to toggle nested borders off and on for different use cases. + /* We need a full border radius on this container element, then have classes */ + /* beneath to toggle nested borders off and on for different use cases. */ .container { background-color: white; box-shadow: $shadow-small-box-shadow; - border: var(--border-width-1) var(--border-solid-border-style) var(--color-gray-500); + border: var(--border-width-1) var(--border-solid-border-style) var(--border-secondary); border-radius: $border-borderless-border-radius; } } diff --git a/packages/components/src/Collapsible/ExpertAdviceCollapsible/ExpertAdviceCollapsible.module.scss b/packages/components/src/Collapsible/ExpertAdviceCollapsible/ExpertAdviceCollapsible.module.scss index 31dd8c5df59..9c86cb85d02 100644 --- a/packages/components/src/Collapsible/ExpertAdviceCollapsible/ExpertAdviceCollapsible.module.scss +++ b/packages/components/src/Collapsible/ExpertAdviceCollapsible/ExpertAdviceCollapsible.module.scss @@ -1,4 +1,5 @@ @import '~@kaizen/design-tokens/sass/border'; +@import '~@kaizen/design-tokens/sass/semantic-color'; @import '~@kaizen/design-tokens/sass/color'; @import '~@kaizen/design-tokens/sass/spacing'; @@ -7,14 +8,14 @@ background-color: $color-purple-100; border: var(--border-width-1) var(--border-solid-border-style) var(--color-purple-400); box-shadow: none; - color: $color-purple-800; + color: $text-primary; &:hover { border-color: $color-purple-500; } } - // Override Collapsible header + /* Override Collapsible header */ .expertAdviceContainer > div { &:first-of-type { background-color: $color-purple-100; diff --git a/packages/components/src/DateInput/DateInputWithIconButton/DateInputWithIconButton.module.css b/packages/components/src/DateInput/DateInputWithIconButton/DateInputWithIconButton.module.css index 0707bac1d99..104f23e9817 100644 --- a/packages/components/src/DateInput/DateInputWithIconButton/DateInputWithIconButton.module.css +++ b/packages/components/src/DateInput/DateInputWithIconButton/DateInputWithIconButton.module.css @@ -27,14 +27,14 @@ } &:hover:not([disabled]) { - background-color: var(--color-blue-100); - color: var(--color-blue-500); + background-color: var(--bg-brand-primary); + color: var(--text-brand-primary); } } .calendarActive { - color: var(--color-blue-500); - background-color: var(--color-blue-100); + color: var(--text-brand-primary); + background-color: var(--bg-brand-primary); } .disabled { diff --git a/packages/components/src/DatePicker/DatePicker.module.css b/packages/components/src/DatePicker/DatePicker.module.css index 622eb25ba22..e38d2031162 100644 --- a/packages/components/src/DatePicker/DatePicker.module.css +++ b/packages/components/src/DatePicker/DatePicker.module.css @@ -1,6 +1,6 @@ @layer kz-components { .datePicker { - color: var(--color-purple-800); + color: var(--text-primary); } .isReversed { diff --git a/packages/components/src/DateRangePicker/DateRangePicker.module.scss b/packages/components/src/DateRangePicker/DateRangePicker.module.scss index c5736d3fba7..bfb76aa611f 100644 --- a/packages/components/src/DateRangePicker/DateRangePicker.module.scss +++ b/packages/components/src/DateRangePicker/DateRangePicker.module.scss @@ -1,14 +1,15 @@ @import '~@kaizen/design-tokens/sass/typography'; +@import '~@kaizen/design-tokens/sass/semantic-color'; @import '~@kaizen/design-tokens/sass/color'; @import '~@kaizen/design-tokens/sass/spacing'; @import '~@kaizen/design-tokens/sass/border'; @import '../../styles/utils/button-reset'; @layer kz-components { - // Vars + /* Vars */ $button-height: 48px; $button-base-padding-horizontal: $spacing-sm; - $button-icon-size: 1.25rem; // 20px + $button-icon-size: 1.25rem; /* 20px */ $disabled-opacity: 0.5; $placeholder-opacity: 0.7; @@ -21,7 +22,7 @@ align-items: center; height: $button-height; width: 100%; - color: $color-purple-800; + color: $text-primary; font-family: $typography-paragraph-body-font-family; font-size: $typography-paragraph-body-font-size; font-weight: $typography-paragraph-body-font-weight; @@ -30,7 +31,7 @@ text-align: start; background-color: $color-white; background-clip: padding-box; - border: $border-solid-border-width $border-solid-border-style $color-gray-500; + border: $border-solid-border-width $border-solid-border-style $border-secondary; border-radius: $border-solid-border-radius; margin-top: $spacing-6; padding: 0 $button-base-padding-horizontal; @@ -38,8 +39,8 @@ &:focus-visible:not([disabled]), &:hover:not([disabled]) { background-color: $color-gray-200; - border-color: $color-gray-600; - color: $color-purple-800; + border-color: $border-primary; + color: $text-primary; } &:focus-visible:not([disabled]) { @@ -67,7 +68,7 @@ margin-top: -1px; } - // Icon adornment styles + /* Icon adornment styles */ @mixin vertically-center-icon { position: absolute; height: $button-icon-size; @@ -80,7 +81,7 @@ .startIconAdornment { @include vertically-center-icon; - color: $color-purple-800; + color: $text-primary; opacity: $disabled-opacity; inset-inline: $spacing-sm auto; } diff --git a/packages/components/src/EmptyState/EmptyState.module.css b/packages/components/src/EmptyState/EmptyState.module.css index d165991beb7..3e3f7712726 100644 --- a/packages/components/src/EmptyState/EmptyState.module.css +++ b/packages/components/src/EmptyState/EmptyState.module.css @@ -10,7 +10,7 @@ border: var(--empty-state-border-width) solid var(--empty-state-border-color); border-radius: var(--border-solid-border-radius); background-color: var(--empty-state-background-color); - color: var(--color-purple-800); + color: var(--text-primary); } .straightCorners { @@ -19,17 +19,17 @@ .success { --empty-state-border-color: var(--color-green-500); - --empty-state-background-color: var(--color-green-100); + --empty-state-background-color: var(--bg-success-primary); } .warning { - --empty-state-border-color: var(--color-red-500); - --empty-state-background-color: var(--color-red-100); + --empty-state-border-color: var(--border-error); + --empty-state-background-color: var(--bg-error-primary); } .informative { --empty-state-border-color: var(--color-blue-400); - --empty-state-background-color: var(--color-blue-100); + --empty-state-background-color: var(--bg-brand-primary); } .expert-advice { diff --git a/packages/components/src/FieldGroup/_docs/FieldGroup.stickersheet.stories.tsx b/packages/components/src/FieldGroup/_docs/FieldGroup.stickersheet.stories.tsx index 49d5a977a9e..c64e5d39eed 100644 --- a/packages/components/src/FieldGroup/_docs/FieldGroup.stickersheet.stories.tsx +++ b/packages/components/src/FieldGroup/_docs/FieldGroup.stickersheet.stories.tsx @@ -22,11 +22,11 @@ const FieldGroupTemplate = ({
- + - +
) diff --git a/packages/components/src/FieldGroup/_docs/FieldGroup.stories.tsx b/packages/components/src/FieldGroup/_docs/FieldGroup.stories.tsx index a47100c0b91..f98e6f3dc99 100644 --- a/packages/components/src/FieldGroup/_docs/FieldGroup.stories.tsx +++ b/packages/components/src/FieldGroup/_docs/FieldGroup.stories.tsx @@ -10,7 +10,7 @@ const meta = { children: ( <> - + ), }, @@ -35,11 +35,11 @@ export const Inline: Story = { <> - + - + ), @@ -50,11 +50,11 @@ export const Default: Story = { <> - + - + ), diff --git a/packages/components/src/FieldMessage/FieldMessage.module.css b/packages/components/src/FieldMessage/FieldMessage.module.css index 932ed19ecf6..bd127fdafed 100644 --- a/packages/components/src/FieldMessage/FieldMessage.module.css +++ b/packages/components/src/FieldMessage/FieldMessage.module.css @@ -25,20 +25,20 @@ .caution { border-radius: var(--border-solid-border-radius); padding: var(--validation-message-padding); - color: var(--color-purple-800); + color: var(--text-primary); } .error { - background: var(--color-red-100); + background: var(--bg-error-primary); opacity: 1; &.reversed { - background: var(--color-red-300); + background: var(--bg-error-secondary); } } .caution { - background: var(--color-yellow-100); + background: var(--bg-warning-primary); opacity: 1; &.reversed { @@ -68,7 +68,7 @@ } .reversed & { - color: var(--color-purple-800); + color: var(--text-primary); } } } diff --git a/packages/components/src/Filter/FilterButton/subcomponents/FilterButtonBase/FilterButtonBase.module.scss b/packages/components/src/Filter/FilterButton/subcomponents/FilterButtonBase/FilterButtonBase.module.scss index d7dfe4a2d64..8dd0c45e443 100644 --- a/packages/components/src/Filter/FilterButton/subcomponents/FilterButtonBase/FilterButtonBase.module.scss +++ b/packages/components/src/Filter/FilterButton/subcomponents/FilterButtonBase/FilterButtonBase.module.scss @@ -1,4 +1,5 @@ @import '~@kaizen/design-tokens/sass/border'; +@import '~@kaizen/design-tokens/sass/semantic-color'; @import '~@kaizen/design-tokens/sass/color'; @import '~@kaizen/design-tokens/sass/typography'; @import '~@kaizen/design-tokens/sass/spacing'; @@ -14,17 +15,17 @@ position: relative; display: inline-flex; align-items: center; - min-height: 3rem; // 48px + min-height: 3rem; /* 48px */ padding: $spacing-sm; border-width: 0; border-radius: $border-solid-border-radius; - background-color: $color-blue-100; + background-color: $bg-brand-primary; font-family: $typography-button-secondary-font-family; font-weight: $typography-button-secondary-font-weight; font-size: $typography-button-secondary-font-size; line-height: $typography-button-secondary-line-height; letter-spacing: $typography-button-secondary-letter-spacing; - color: $color-blue-500; + color: $text-brand-primary; text-align: start; &:hover, @@ -35,7 +36,7 @@ &:hover, &:active, &:focus-visible { - background-color: $color-blue-200; + background-color: $bg-brand-secondary; } &:focus { @@ -51,7 +52,7 @@ position: absolute; inset: $focus-ring-offset-outer; border-radius: $border-focus-ring-border-radius; - border: $border-focus-ring-border-width $border-focus-ring-border-style $color-blue-500; + border: $border-focus-ring-border-width $border-focus-ring-border-style $border-brand; } } } diff --git a/packages/components/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.module.css b/packages/components/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.module.css index 4fffecaf878..fb9315abb52 100644 --- a/packages/components/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.module.css +++ b/packages/components/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.module.css @@ -5,7 +5,7 @@ z-index: 1000; box-sizing: border-box; background: var(--color-white); - color: var(--color-purple-800); + color: var(--text-primary); border-radius: var(--border-solid-border-radius); box-shadow: var(--shadow-large-box-shadow); padding: var(--spacing-12) 0; diff --git a/packages/components/src/Filter/FilterMultiSelect/subcomponents/MultiSelectOption/MultiSelectOption.module.scss b/packages/components/src/Filter/FilterMultiSelect/subcomponents/MultiSelectOption/MultiSelectOption.module.scss index ec094935dfd..5b3d779516a 100644 --- a/packages/components/src/Filter/FilterMultiSelect/subcomponents/MultiSelectOption/MultiSelectOption.module.scss +++ b/packages/components/src/Filter/FilterMultiSelect/subcomponents/MultiSelectOption/MultiSelectOption.module.scss @@ -1,4 +1,5 @@ @import '~@kaizen/design-tokens/sass/border'; +@import '~@kaizen/design-tokens/sass/semantic-color'; @import '~@kaizen/design-tokens/sass/color'; @import '~@kaizen/design-tokens/sass/typography'; @import '~@kaizen/design-tokens/sass/spacing'; @@ -11,7 +12,7 @@ height: calc(#{$iconAndBadgeHeight} - #{$border-solid-border-width} * 2); width: calc(#{$iconAndBadgeHeight} - #{$border-solid-border-width} * 2); border: $border-solid-border-width $border-solid-border-style; - border-color: $color-gray-500; + border-color: $border-secondary; border-radius: $border-solid-border-radius; display: inline-flex; justify-content: center; @@ -39,21 +40,21 @@ &:hover, &:active, &:focus { - background-color: $color-blue-100; + background-color: $bg-brand-primary; .badge { background-color: $color-white; } .icon { - border-color: $color-gray-600; + border-color: $border-primary; background-color: $color-gray-200; } } &.isFocused, &:focus-visible { - background-color: $color-blue-100; + background-color: $bg-brand-primary; &::after { $focus-ring-offset: calc((#{$border-focus-ring-border-width} * 2) + 1px); @@ -61,15 +62,15 @@ content: ''; position: absolute; background: transparent; - border: $border-focus-ring-border-width $border-focus-ring-border-style $color-blue-500; + border: $border-focus-ring-border-width $border-focus-ring-border-style $border-brand; border-radius: $border-focus-ring-border-radius; inset: calc(-1 * #{$focus-ring-offset}); - z-index: 1; // show border when sibling option is hovered + z-index: 1; /* show border when sibling option is hovered */ } } &:focus { - outline: none; // cancel browser style + outline: none; /* cancel browser style */ } } @@ -79,17 +80,17 @@ &:hover { .icon { - border-color: $color-gray-500; + border-color: $border-secondary; } .badge { - background-color: $color-gray-300; + background-color: $bg-tertiary; } } } .badgeContainer { - // Must be the same height as .icon, so they align vertically + /* Must be the same height as .icon, so they align vertically */ height: $iconAndBadgeHeight; display: flex; align-items: center; @@ -107,7 +108,7 @@ &:active, &:focus { .icon { - background-color: $color-gray-600; + background-color: $bg-secondary-solid; } } } diff --git a/packages/components/src/Filter/FilterMultiSelect/subcomponents/SelectionControlButton/SelectionControlButton.module.scss b/packages/components/src/Filter/FilterMultiSelect/subcomponents/SelectionControlButton/SelectionControlButton.module.scss index e6bf397dbb8..c5c31b4b9a2 100644 --- a/packages/components/src/Filter/FilterMultiSelect/subcomponents/SelectionControlButton/SelectionControlButton.module.scss +++ b/packages/components/src/Filter/FilterMultiSelect/subcomponents/SelectionControlButton/SelectionControlButton.module.scss @@ -1,4 +1,5 @@ @import '~@kaizen/design-tokens/sass/border'; +@import '~@kaizen/design-tokens/sass/semantic-color'; @import '~@kaizen/design-tokens/sass/color'; @import '~@kaizen/design-tokens/sass/typography'; @import '~@kaizen/design-tokens/sass/spacing'; @@ -25,7 +26,7 @@ font-size: $typography-button-secondary-font-size; line-height: $typography-button-secondary-line-height; letter-spacing: $typography-button-secondary-letter-spacing; - color: $color-blue-500; + color: $text-brand-primary; &:focus { outline: none; @@ -35,7 +36,7 @@ &:active, &:focus { &:not(.isDisabled) { - background-color: $color-blue-100; + background-color: $bg-brand-primary; } } @@ -48,11 +49,11 @@ border-radius: $border-focus-ring-border-radius; border-width: $border-focus-ring-border-width; border-style: $border-focus-ring-border-style; - border-color: $color-blue-500; + border-color: $border-brand; inset: calc(-1 * #{$focus-ring-offset}); } - // TODO: copied from Calendar button since the design is not settled + /* TODO: copied from Calendar button since the design is not settled */ &.isDisabled { pointer-events: none; color: rgba($color-purple-800-rgb, 0.3); diff --git a/packages/components/src/GuidanceBlock/GuidanceBlock.module.css b/packages/components/src/GuidanceBlock/GuidanceBlock.module.css index c46cb82e3c2..b38c05b9ca3 100644 --- a/packages/components/src/GuidanceBlock/GuidanceBlock.module.css +++ b/packages/components/src/GuidanceBlock/GuidanceBlock.module.css @@ -18,7 +18,7 @@ box-shadow: var(--shadow-small-box-shadow); position: relative; top: -1px; - color: var(--color-purple-800); + color: var(--text-primary); @media (width >= 1024px) { min-height: calc(12rem - calc(var(--banner-padding) * 2)); @@ -132,29 +132,29 @@ } .default { - border-color: var(--color-gray-500); + border-color: var(--border-secondary); background: var(--color-white); } .positive { border-color: var(--color-green-500); - background: var(--color-green-100); + background: var(--bg-success-primary); } .negative, .assertive { - border-color: var(--color-red-500); - background: var(--color-red-100); + border-color: var(--border-error); + background: var(--bg-error-primary); } .informative { border-color: var(--color-blue-400); - background: var(--color-blue-100); + background: var(--bg-brand-primary); } .cautionary { border-color: var(--color-yellow-700); - background: var(--color-yellow-100); + background: var(--bg-warning-primary); } .prominent { diff --git a/packages/components/src/Heading/Heading.module.css b/packages/components/src/Heading/Heading.module.css index 1a866b95c32..45b311d9fcb 100644 --- a/packages/components/src/Heading/Heading.module.css +++ b/packages/components/src/Heading/Heading.module.css @@ -70,12 +70,12 @@ } .dark { - color: var(--color-purple-800); + color: var(--text-primary); opacity: 1; } .dark-reduced-opacity { - color: var(--color-purple-800); + color: var(--text-primary); opacity: 0.7; } @@ -91,7 +91,7 @@ .positive { &.small { - color: var(--color-green-600); + color: var(--text-success-primary); } &.large { @@ -101,7 +101,7 @@ .negative { &.small { - color: var(--color-red-600); + color: var(--text-error-primary); } &.large { diff --git a/packages/components/src/Icon/_docs/Icon.docs.module.css b/packages/components/src/Icon/_docs/Icon.docs.module.css index 98e2aaf857d..2cdf17423c9 100644 --- a/packages/components/src/Icon/_docs/Icon.docs.module.css +++ b/packages/components/src/Icon/_docs/Icon.docs.module.css @@ -3,7 +3,7 @@ position: relative; background: none; padding: 0; - color: var(--color-blue-500); + color: var(--text-brand-primary); &:hover { color: var(--color-blue-400); diff --git a/packages/components/src/Icon/_docs/Icon.docs.stories.tsx b/packages/components/src/Icon/_docs/Icon.docs.stories.tsx index f6279bd0862..89c76b8cc47 100644 --- a/packages/components/src/Icon/_docs/Icon.docs.stories.tsx +++ b/packages/components/src/Icon/_docs/Icon.docs.stories.tsx @@ -101,7 +101,7 @@ export const Color: Story = {
- +
), } @@ -297,7 +297,7 @@ export const AlignmentDont: Story = { export const ContrastDo: Story = { render: () => ( -
+
), @@ -305,7 +305,7 @@ export const ContrastDo: Story = { export const ContrastDont: Story = { render: () => ( -
+
), @@ -346,7 +346,7 @@ export const DistinguishDont: Story = {
Selected - +
), diff --git a/packages/components/src/Icon/_docs/Icon.stickersheet.stories.tsx b/packages/components/src/Icon/_docs/Icon.stickersheet.stories.tsx index 5d5f94682fa..9cb4c1fd752 100644 --- a/packages/components/src/Icon/_docs/Icon.stickersheet.stories.tsx +++ b/packages/components/src/Icon/_docs/Icon.stickersheet.stories.tsx @@ -40,7 +40,7 @@ const StickerSheetTemplate: StickerSheetStory = { - + ))} diff --git a/packages/components/src/IconV1/_docs/Icon.docs.stories.tsx b/packages/components/src/IconV1/_docs/Icon.docs.stories.tsx index 9a65f571e4a..045be516dac 100644 --- a/packages/components/src/IconV1/_docs/Icon.docs.stories.tsx +++ b/packages/components/src/IconV1/_docs/Icon.docs.stories.tsx @@ -38,7 +38,7 @@ export const Playground: Story = { export const ApplyColour: Story = { render: (args) => ( -
+
), diff --git a/packages/components/src/Illustration/subcomponents/Base/Base.module.scss b/packages/components/src/Illustration/subcomponents/Base/Base.module.scss index 5693317ae38..5a79be035bd 100644 --- a/packages/components/src/Illustration/subcomponents/Base/Base.module.scss +++ b/packages/components/src/Illustration/subcomponents/Base/Base.module.scss @@ -1,4 +1,5 @@ @import '~@kaizen/design-tokens/sass/color'; +@import '~@kaizen/design-tokens/sass/semantic-color'; @import '~@kaizen/design-tokens/sass/animation'; @layer kz-components { @@ -12,7 +13,7 @@ position: relative; } - // nested to get more specificity, beat out the generic button styles + /* nested to get more specificity, beat out the generic button styles */ .figure .pausePlayButton { opacity: 0%; position: absolute; @@ -28,7 +29,7 @@ } svg { - color: $color-purple-800; + color: $text-primary; opacity: 70%; } @@ -109,10 +110,10 @@ } } - // If the .visually-hidden class is applied to natively focusable elements - // (such as a, button, input, etc) they must become visible when they receive - // keyboard focus. Otherwise, a sighted keyboard user would have to try and - // figure out where their visible focus indicator had gone to. + /* If the .visually-hidden class is applied to natively focusable elements */ + /* (such as a, button, input, etc) they must become visible when they receive */ + /* keyboard focus. Otherwise, a sighted keyboard user would have to try and */ + /* figure out where their visible focus indicator had gone to. */ .visuallyHidden:not(:focus, :active) { clip-path: rect(0 0 0 0); position: absolute; diff --git a/packages/components/src/Input/Input/Input.module.scss b/packages/components/src/Input/Input/Input.module.scss index d54f58e7221..3906b42f167 100644 --- a/packages/components/src/Input/Input/Input.module.scss +++ b/packages/components/src/Input/Input/Input.module.scss @@ -1,4 +1,5 @@ @import '~@kaizen/design-tokens/sass/border'; +@import '~@kaizen/design-tokens/sass/semantic-color'; @import '~@kaizen/design-tokens/sass/color'; @import '~@kaizen/design-tokens/sass/spacing'; @import '../../../styles/utils/form-variables'; @@ -7,7 +8,7 @@ @layer kz-components { $input-height: 48px; $input-base-padding-horizontal: $spacing-sm; - $input-icon-size: 1.25rem; // 20px + $input-icon-size: 1.25rem; /* 20px */ $input-disabled-background: $color-gray-300; $input-disabled-opacity: 0.3; $input-disabled-border-alpha: 50%; @@ -59,9 +60,9 @@ } /* stylelint-disable no-descending-specificity */ - /////////////////////////////////////////////////// - // ICON ADORNMENT STYLES - /////////////////////////////////////////////////// + /* ///////////////////////////////////////////////// */ + /* ICON ADORNMENT STYLES */ + /* ///////////////////////////////////////////////// */ @mixin vertically-center-icon { position: absolute; @@ -113,12 +114,12 @@ } } - // Default theme + /* Default theme */ /* stylelint-disable-next-line no-duplicate-selectors */ .withStartIconAdornment { /* stylelint-disable-next-line no-duplicate-selectors */ .startIconAdornment { - color: $color-purple-800; + color: $text-primary; opacity: 50%; } @@ -139,7 +140,7 @@ } } - // Reversed + /* Reversed */ .withStartIconAdornment.withReversed { .startIconAdornment { color: $color-white; @@ -180,14 +181,14 @@ } } - /////////////////////////////////////////////////// - // THEMES - /////////////////////////////////////////////////// + /* ///////////////////////////////////////////////// */ + /* THEMES */ + /* ///////////////////////////////////////////////// */ - // Default + /* Default */ .input.default { background-color: $color-white; - color: $color-purple-800; + color: $text-primary; display: flex; align-items: center; @@ -196,7 +197,7 @@ } &:focus + .focusRing { - border-color: $color-blue-500; + border-color: $border-brand; } &.disabled { @@ -206,19 +207,19 @@ } &:not(.error, .caution) { - border-color: $color-gray-500; + border-color: $border-secondary; &:disabled { border-color: rgba($color-gray-500-rgb, $input-disabled-opacity); } @include form-input-focus-state { - border-color: $color-gray-600; + border-color: $border-primary; } } &.error { - border-color: $color-red-500; + border-color: $border-error; &.disabled { border-color: rgba($color-red-500-rgb, $input-disabled-opacity); @@ -234,7 +235,7 @@ } } - // Reversed (Dark Backgrounds) + /* Reversed (Dark Backgrounds) */ .input.reversed { background: transparent; color: $color-white; @@ -265,7 +266,7 @@ } &.error { - border-color: $color-red-300; + border-color: $border-error_subtle; &.disabled { border-color: rgba($color-red-300-rgb, $input-disabled-opacity); diff --git a/packages/components/src/Input/InputSearch/InputSearch.module.scss b/packages/components/src/Input/InputSearch/InputSearch.module.scss index 5b6619d307e..edf19dae661 100644 --- a/packages/components/src/Input/InputSearch/InputSearch.module.scss +++ b/packages/components/src/Input/InputSearch/InputSearch.module.scss @@ -1,4 +1,5 @@ @import '~@kaizen/design-tokens/sass/border'; +@import '~@kaizen/design-tokens/sass/semantic-color'; @import '~@kaizen/design-tokens/sass/color'; @import '~@kaizen/design-tokens/sass/spacing'; @import '../../../styles/utils/forms'; @@ -8,7 +9,7 @@ $input-padding-before-start-icon: 1rem; $input-padding-between-icon-input: $spacing-sm; $input-padding-after-end-icon: $spacing-sm; - $input-icon-size: 1.25rem; // 20px + $input-icon-size: 1.25rem; /* 20px */ $input-padding-with-icon: calc( #{$input-padding-before-start-icon} + #{$input-icon-size} + #{$input-padding-between-icon-input} ); @@ -20,7 +21,7 @@ $input-placeholder-opacity--reversed: 0.8; $start-icon-opacity--reversed: $input-placeholder-opacity--reversed; - // [type="search"] is required to override performance-ui global materialize CSS >:| + /* [type="search"] is required to override performance-ui global materialize CSS >:| */ $classname--input: '.input[type="search"]'; .wrapper { @@ -100,32 +101,32 @@ } /* stylelint-disable no-descending-specificity */ - /////////////////////////////////////////////////// - // THEMES - /////////////////////////////////////////////////// + /* ///////////////////////////////////////////////// */ + /* THEMES */ + /* ///////////////////////////////////////////////// */ - // Default + /* Default */ .default { #{$classname--input} { - border-color: $color-gray-500; + border-color: $border-secondary; background-color: $color-white; - color: $color-purple-800; + color: $text-primary; @include form-input-placeholder { - color: $color-purple-800; + color: $text-primary; opacity: $input-placeholder-opacity--default; } } .startIconAdornment { - color: $color-purple-800; + color: $text-primary; opacity: $start-icon-opacity--default; } &:hover, &:focus-within { #{$classname--input} { - border-color: $color-gray-600; + border-color: $border-primary; background-color: $color-gray-200; @include form-input-placeholder { @@ -139,21 +140,21 @@ } } - // Secondary + /* Secondary */ .secondary { #{$classname--input} { border-color: transparent; background-color: $color-gray-200; - color: $color-purple-800; + color: $text-primary; @include form-input-placeholder { - color: $color-purple-800; + color: $text-primary; opacity: $input-placeholder-opacity--default; } } .startIconAdornment { - color: $color-purple-800; + color: $text-primary; opacity: $start-icon-opacity--default; } @@ -161,7 +162,7 @@ &:focus-within { #{$classname--input} { border-color: transparent; - background-color: $color-gray-300; + background-color: $bg-tertiary; @include form-input-placeholder { opacity: 100%; @@ -174,7 +175,7 @@ } } - // Reversed + /* Reversed */ .reversed { #{$classname--input} { background: rgba($color-white-rgb, 0.1); diff --git a/packages/components/src/Label/Label.module.css b/packages/components/src/Label/Label.module.css index ac9ea643dd8..93cfc5810c7 100644 --- a/packages/components/src/Label/Label.module.css +++ b/packages/components/src/Label/Label.module.css @@ -1,7 +1,7 @@ @layer kz-components { :root { --label-start-margin: var(--spacing-xs); - --dt-color-form-text-color: var(--color-purple-800); + --dt-color-form-text-color: var(--text-primary); } .label { diff --git a/packages/components/src/LikertScale/LikertScale.module.scss b/packages/components/src/LikertScale/LikertScale.module.scss index 4454bb5741f..07971df0505 100644 --- a/packages/components/src/LikertScale/LikertScale.module.scss +++ b/packages/components/src/LikertScale/LikertScale.module.scss @@ -1,4 +1,5 @@ @import '~@kaizen/design-tokens/sass/border'; +@import '~@kaizen/design-tokens/sass/semantic-color'; @import '~@kaizen/design-tokens/sass/color'; @import '~@kaizen/design-tokens/sass/spacing'; @@ -124,7 +125,7 @@ padding: 0; overflow-x: visible; display: inline-block; - width: 18.5%; // 5 columns + width: 18.5%; /* 5 columns */ position: relative; @include fill($block-height); @@ -137,7 +138,7 @@ margin-inline-end: 0; } - // Hack to bridge the gaps between items so mouse always hovers on something + /* Hack to bridge the gaps between items so mouse always hovers on something */ &::before, &::after { content: ''; @@ -245,7 +246,7 @@ background 0.1s, border-color 0.1s; height: $block-height; - border: $border-solid-border-width $border-solid-border-style $color-gray-500; + border: $border-solid-border-width $border-solid-border-style $border-secondary; &:hover { transition: diff --git a/packages/components/src/Link/Link.module.css b/packages/components/src/Link/Link.module.css index 9953dee9692..4d84cbbbdd0 100644 --- a/packages/components/src/Link/Link.module.css +++ b/packages/components/src/Link/Link.module.css @@ -1,6 +1,6 @@ @layer kz-components { .link { - color: var(--link-text-color, var(--color-blue-500)); + color: var(--link-text-color, var(--text-brand-primary)); font-family: var(--typography-paragraph-body-font-family); font-size: var(--link-font-size, inherit); line-height: var(--link-line-height, inherit); @@ -18,7 +18,7 @@ } .isUnderlined { - border-bottom: var(--spacing-1) solid var(--link-text-color, var(--color-blue-500)); + border-bottom: var(--spacing-1) solid var(--link-text-color, var(--border-brand)); } .link .icon > * { @@ -35,19 +35,19 @@ } .primary[data-hovered] { - --link-text-color: var(--color-blue-600); + --link-text-color: var(--text-brand-secondary_hover); } .primary[data-pressed] { - --link-text-color: var(--color-blue-700); + --link-text-color: var(--text-brand-secondary); } .secondary { - --link-text-color: var(--color-purple-800); + --link-text-color: var(--text-primary); } .secondary[data-hovered] { - --link-text-color: var(--color-gray-600); + --link-text-color: var(--text-tertiary); } .secondary[data-pressed] { diff --git a/packages/components/src/Loading/LoadingParagraph/LoadingParagraph.module.scss b/packages/components/src/Loading/LoadingParagraph/LoadingParagraph.module.scss index cb9e3fc3002..c6851a13da5 100644 --- a/packages/components/src/Loading/LoadingParagraph/LoadingParagraph.module.scss +++ b/packages/components/src/Loading/LoadingParagraph/LoadingParagraph.module.scss @@ -1,4 +1,5 @@ @import '~@kaizen/design-tokens/sass/color'; +@import '~@kaizen/design-tokens/sass/semantic-color'; @import '~@kaizen/design-tokens/sass/spacing'; @import '../mixins'; @@ -6,7 +7,7 @@ .loadingParagraph { @extend %loadingBase; - background-color: $color-gray-300; + background-color: $bg-tertiary; height: calc(#{$spacing-md} * 0.625); margin-bottom: calc(#{$spacing-md} * 0.5); } @@ -45,7 +46,7 @@ margin-bottom: 0; } - // We double the class selector to increase specificity above rules like `h1.default-style` + /* We double the class selector to increase specificity above rules like `h1.default-style` */ .inheritBaseline.inheritBaseline { position: static; } diff --git a/packages/components/src/Menu/Menu.module.css b/packages/components/src/Menu/Menu.module.css index 9053a445224..8e1db934a1d 100644 --- a/packages/components/src/Menu/Menu.module.css +++ b/packages/components/src/Menu/Menu.module.css @@ -1,6 +1,6 @@ @layer kz-components { .menu { - color: var(--color-purple-800); + color: var(--text-primary); width: 248px; flex: 1 1 auto; min-height: 0; diff --git a/packages/components/src/Menu/MenuItem.module.css b/packages/components/src/Menu/MenuItem.module.css index 729cba9e6c5..acc1fede0fa 100644 --- a/packages/components/src/Menu/MenuItem.module.css +++ b/packages/components/src/Menu/MenuItem.module.css @@ -6,7 +6,7 @@ letter-spacing: var(--typography-paragraph-body-letter-spacing); font-weight: var(--typography-paragraph-body-font-weight); line-height: var(--typography-paragraph-body-line-height); - color: var(--color-purple-800); + color: var(--text-primary); padding: var(--spacing-6) var(--spacing-8); border: var(--border-focus-ring-border-width) var(--border-focus-ring-border-style) transparent; border-radius: 4px; @@ -33,8 +33,8 @@ .item[data-hovered], .item[data-focus-visible] { - background-color: var(--color-blue-100); - color: var(--color-blue-500); + background-color: var(--bg-brand-primary); + color: var(--text-brand-primary); } .item[data-focus-visible] { diff --git a/packages/components/src/MenuV1/subcomponents/MenuDropdown/MenuDropdown.module.scss b/packages/components/src/MenuV1/subcomponents/MenuDropdown/MenuDropdown.module.scss index de3d03e9c0f..64e78e594f9 100644 --- a/packages/components/src/MenuV1/subcomponents/MenuDropdown/MenuDropdown.module.scss +++ b/packages/components/src/MenuV1/subcomponents/MenuDropdown/MenuDropdown.module.scss @@ -1,4 +1,5 @@ @import '~@kaizen/design-tokens/sass/border'; +@import '~@kaizen/design-tokens/sass/semantic-color'; @import '~@kaizen/design-tokens/sass/color'; @import '~@kaizen/design-tokens/sass/shadow'; @import '~@kaizen/design-tokens/sass/spacing'; @@ -11,7 +12,7 @@ z-index: $dropdown-index; box-sizing: border-box; background: $color-white; - color: $color-purple-800; + color: $text-primary; border-radius: $border-solid-border-radius; box-shadow: $shadow-large-box-shadow; max-height: 22rem; diff --git a/packages/components/src/MenuV1/subcomponents/MenuHeading/MenuHeading.module.scss b/packages/components/src/MenuV1/subcomponents/MenuHeading/MenuHeading.module.scss index b413fefd2f5..98724696f24 100644 --- a/packages/components/src/MenuV1/subcomponents/MenuHeading/MenuHeading.module.scss +++ b/packages/components/src/MenuV1/subcomponents/MenuHeading/MenuHeading.module.scss @@ -1,4 +1,5 @@ @import '~@kaizen/design-tokens/sass/color'; +@import '~@kaizen/design-tokens/sass/semantic-color'; @import '~@kaizen/design-tokens/sass/spacing'; @layer kz-components { @@ -6,7 +7,7 @@ .heading { padding: 10px $spacing-sm 5px; - color: $color-purple-800; + color: $text-primary; margin: 0 $gutter; display: block; } diff --git a/packages/components/src/MenuV1/subcomponents/MenuItem/MenuItem.module.scss b/packages/components/src/MenuV1/subcomponents/MenuItem/MenuItem.module.scss index 137624b1c4a..32600d9dbd2 100644 --- a/packages/components/src/MenuV1/subcomponents/MenuItem/MenuItem.module.scss +++ b/packages/components/src/MenuV1/subcomponents/MenuItem/MenuItem.module.scss @@ -1,4 +1,5 @@ @import '~@kaizen/design-tokens/sass/color'; +@import '~@kaizen/design-tokens/sass/semantic-color'; @import '~@kaizen/design-tokens/sass/spacing'; @import '~@kaizen/design-tokens/sass/typography'; @@ -26,7 +27,7 @@ line-height: $typography-paragraph-body-line-height; letter-spacing: $typography-paragraph-body-letter-spacing; text-decoration: none; - color: $color-purple-800; + color: $text-primary; &:hover { text-decoration: none; @@ -34,19 +35,19 @@ &:not(.menuItem--disabled):hover, &:focus { - background: $color-blue-100; - color: $color-blue-500; + background: $bg-brand-primary; + color: $text-brand-primary; .menuItem__Icon { - color: $color-blue-500; + color: $text-brand-primary; } &.menuItem--destructive { - background: $color-red-100; - color: $color-red-600; + background: $bg-error-primary; + color: $text-error-primary; .menuItem__Icon { - color: $color-red-600; + color: $text-error-primary; } } } @@ -56,7 +57,7 @@ } &:focus-visible { - border-color: $color-blue-500; + border-color: $border-brand; } } @@ -69,15 +70,15 @@ } .menuItem--active { - color: $color-blue-500; + color: $text-brand-primary; font-weight: $typography-paragraph-bold-font-weight; } .menuItem--destructive { - color: $color-red-600; + color: $text-error-primary; .menuItem__Icon { - color: $color-red-600; + color: $text-error-primary; } } diff --git a/packages/components/src/Modal/ConfirmationModal/ConfirmationModal.module.scss b/packages/components/src/Modal/ConfirmationModal/ConfirmationModal.module.scss index a7d40654bdb..caecaa390de 100644 --- a/packages/components/src/Modal/ConfirmationModal/ConfirmationModal.module.scss +++ b/packages/components/src/Modal/ConfirmationModal/ConfirmationModal.module.scss @@ -1,4 +1,5 @@ @import '~@kaizen/design-tokens/sass/border'; +@import '~@kaizen/design-tokens/sass/semantic-color'; @import '~@kaizen/design-tokens/sass/color'; @import '~@kaizen/design-tokens/sass/spacing'; @import '~@kaizen/design-tokens/sass/layout'; @@ -33,7 +34,7 @@ } } - // override Murmur global styles :( + /* override Murmur global styles :( */ h1 { color: var(--color-purple-800); } @@ -89,7 +90,7 @@ width: 155px; height: 155px; margin: 0 auto; - color: $color-purple-800; + color: $text-primary; @media (max-width: $layout-breakpoints-medium) { width: 86px; @@ -106,32 +107,32 @@ } } - // -------------------------------- - // variant styles - // -------------------------------- + /* -------------------------------- */ + /* variant styles */ + /* -------------------------------- */ .success { - background-color: var(--color-green-100); + background-color: var(--bg-success-primary); - --spot-icon-color: var(--color-green-500); + --spot-icon-color: var(--fg-success-primary); } .informative { - background-color: var(--color-blue-100); + background-color: var(--bg-brand-primary); - --spot-icon-color: var(--color-blue-500); + --spot-icon-color: var(--fg-brand-primary); } .warning { - background-color: var(--color-red-100); + background-color: var(--bg-error-primary); - --spot-icon-color: var(--color-red-500); + --spot-icon-color: var(--fg-error-primary); } .cautionary { - background-color: var(--color-yellow-100); + background-color: var(--bg-warning-primary); - --spot-icon-color: var(--color-yellow-700); + --spot-icon-color: var(--fg-warning-primary); } - // -------------------------------- + /* -------------------------------- */ } diff --git a/packages/components/src/Modal/ConfirmationModal/_docs/ConfirmationModal.stories.tsx b/packages/components/src/Modal/ConfirmationModal/_docs/ConfirmationModal.stories.tsx index a8dde5d8586..43c6fdf602c 100644 --- a/packages/components/src/Modal/ConfirmationModal/_docs/ConfirmationModal.stories.tsx +++ b/packages/components/src/Modal/ConfirmationModal/_docs/ConfirmationModal.stories.tsx @@ -46,7 +46,7 @@ const ConfirmationModalTemplate: Story = { return ( <> - - - @@ -99,7 +99,7 @@ export const OnAfterEnter: Story = { return ( <> - -
+
>): JSX.Element