diff --git a/packages/design-system/README.md b/packages/design-system/README.md index 7157f73..b857822 100644 --- a/packages/design-system/README.md +++ b/packages/design-system/README.md @@ -76,6 +76,40 @@ Token sheets ship with the package (`./tokens` export): - **Dark mode**: `[data-theme="dark"]` on `` (falls back to `prefers-color-scheme`). - **Runtime theming per tenant**: `TenantThemeService` in `@fireflyframework/core` overrides the same tokens at runtime — cascade: defaults → tenant → dark → tenant-dark. +### Overriding a component token by context + +A component's **base** tokens (e.g. `--ff-select-min-width`, `--ff-panel-bg`) are *consumed* with a fallback and never *declared* by the component itself: + +```scss +// ff-select.component.scss +.ff-select { + min-width: var(--ff-select-min-width, 180px); +} +``` + +This is deliberate: a custom property specified on an element always wins over one inherited from an ancestor. If the component declared `--ff-select-min-width: 180px;` on `.ff-select` itself, that declaration would always be the specified value for every `ff-select`, and no surrounding container could ever narrow or widen it — the ancestor's value would be inherited but immediately shadowed. Skipping the local declaration and only ever reading the token through `var(--token, )` leaves the property open for any ancestor to set, while the fallback keeps the original look when nobody does. + +To retint or resize a component from a specific context, set its token on a container that wraps it: + +```scss +// A pagination footer that needs its page-size select to shrink below the +// component's own 180px floor, without touching ff-select itself. +.ff-list__page-size { + --ff-select-min-width: 0; + width: 6rem; +} +``` + +```html +
+ +
+``` + +The same technique works for any other base token (`--ff-panel-bg`, `--ff-button-radius`, `--ff-input-border`, …): declare the token on a wrapping element, not on the component's own class. + +**Variant tokens are the exception.** A variant modifier picks the final value for that variant on the element itself (`.ff-panel--warning { --ff-panel-accent: var(--ff-color-warning-500); }`), so an ancestor cannot override it — the element's own declaration wins. Retint a variant through the palette instead (`--ff-color-warning-500` on `:root` or a theme scope), which is what the variant resolves from. + ## Living catalog The monorepo's `playground` app is the catalog: every component with its real variants, a foundations page rendering the token scales, and a theming page with dark toggle + token inspector. diff --git a/packages/design-system/src/lib/overridable-tokens.spec.ts b/packages/design-system/src/lib/overridable-tokens.spec.ts new file mode 100644 index 0000000..e09c9d9 --- /dev/null +++ b/packages/design-system/src/lib/overridable-tokens.spec.ts @@ -0,0 +1,193 @@ +import 'zone.js'; +import 'zone.js/testing'; +import { readFileSync, readdirSync, statSync } from 'node:fs'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { Component } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + BrowserTestingModule, + platformBrowserTesting, +} from '@angular/platform-browser/testing'; +import { FfSelectComponent } from './primitives/ff-select/ff-select.component'; + +TestBed.initTestEnvironment(BrowserTestingModule, platformBrowserTesting(), { + teardown: { destroyAfterEach: true }, +}); + +/** + * Walks a directory tree and returns the absolute paths of every + * `*.component.scss` file found underneath it. + */ +function collectComponentStylesheets(rootDir: string): string[] { + const result: string[] = []; + for (const entry of readdirSync(rootDir)) { + const fullPath = join(rootDir, entry); + const stats = statSync(fullPath); + if (stats.isDirectory()) { + result.push(...collectComponentStylesheets(fullPath)); + } else if (entry.endsWith('.component.scss')) { + result.push(fullPath); + } + } + return result; +} + +/** + * Splits a SCSS source string into the list of "own" declaration bodies of + * every brace-delimited rule, i.e. the text that sits directly inside each + * `{ ... }` excluding the text that belongs to further-nested rules. SCSS + * interpolations (`#{...}`) are blanked out first so their braces never + * confuse the same brace-depth bookkeeping used for real rule blocks. + */ +function ownRuleBodies(scss: string): string[] { + const withoutInterpolation = scss.replace(/#\{[^}]*\}/g, '__I__'); + const bodies: string[] = []; + const stack: string[] = ['']; + + for (const char of withoutInterpolation) { + if (char === '{') { + stack.push(''); + } else if (char === '}') { + const own = stack.pop(); + if (own !== undefined) { + bodies.push(own); + } + } else { + stack[stack.length - 1] += char; + } + } + + return bodies; +} + +/** Strips `//` line comments so prose mentioning a token name cannot be mistaken for code. */ +function withoutLineComments(text: string): string { + return text + .split('\n') + .map((line) => line.replace(/\/\/.*$/, '')) + .join('\n'); +} + +/** + * Custom properties declared as a statement (`--ff-foo: ...;`) directly inside + * a rule body. Consumption (`var(--ff-foo, ...)`) is never followed by a + * colon — only `,`/`)` — so matching `--ff-foo:` unambiguously finds + * declarations regardless of what precedes them (start of block, a `;`, or a + * comment line). + */ +function declaredTokens(ruleBody: string): Set { + const matches = withoutLineComments(ruleBody).matchAll(/(--ff-[\w-]+)\s*:/g); + return new Set(Array.from(matches, (m) => m[1])); +} + +/** Custom properties read via `var(--ff-foo, ...)` directly inside a rule body. */ +function consumedTokens(ruleBody: string): Set { + const matches = withoutLineComments(ruleBody).matchAll(/var\(\s*(--ff-[\w-]+)/g); + return new Set(Array.from(matches, (m) => m[1])); +} + +const libDir = join(dirname(fileURLToPath(import.meta.url))); +const stylesheets = collectComponentStylesheets(libDir); + +describe('design-system component tokens are overridable by an ancestor', () => { + it('finds component stylesheets to check (sanity guard against a broken glob)', () => { + expect(stylesheets.length).toBeGreaterThan(20); + }); + + it.each(stylesheets)( + 'never declares and consumes the same --ff-* token in the same rule (%s)', + (path) => { + // A custom property specified on an element always wins over one + // inherited from an ancestor. The moment a rule both declares + // `--ff-x` and reads `var(--ff-x)`, that rule's own declaration is + // guaranteed to be the specified value for `--ff-x` on every element + // it matches, so an ancestor supplying `--ff-x` can never take + // effect. Declaring a token in one rule (e.g. a `--variant` modifier + // picking its final color) and consuming it in a *different* rule is + // fine — this only forbids the self-shadowing pattern. + const scss = readFileSync(path, 'utf8'); + const offendingRules = ownRuleBodies(scss) + .map((body) => { + const declared = declaredTokens(body); + const consumed = consumedTokens(body); + const overlap = Array.from(declared).filter((token) => consumed.has(token)); + return { body, overlap }; + }) + .filter(({ overlap }) => overlap.length > 0); + + expect(offendingRules).toEqual([]); + }, + ); +}); + +/** + * Behavioural half of the contract above. `getComputedStyle` in the jsdom + * environment used by this Vitest suite does not implement CSS Custom + * Properties at all: it neither inherits `--foo` declarations down to + * descendants nor substitutes `var(--foo, fallback)` in longhand + * properties (verified against this exact scenario before writing this + * spec — the computed value comes back as the literal, unresolved + * `"var(--foo, fallback)"` string, and an inherited custom property reads + * back as `""` on the child). Asserting on `getComputedStyle` here would + * therefore pass or fail for reasons unrelated to the actual cascade, i.e. + * it would not test anything. A real assertion on the final resolved value + * needs a real browser engine (a Playwright/Storybook interaction test), + * which is a larger addition than this fix and is left as a follow-up. + * + * What *is* reliable in jsdom, and is exercised here, is the DOM wiring: + * that the ancestor really is an ancestor of the component's native + * element, and that the ancestor really carries the overriding + * declaration. Combined with the static proof above (the component only + * ever reads this token through `var(--ff-select-min-width, ...)` and + * never re-declares it), the CSS cascade — which is a browser guarantee, + * not application behaviour — is what makes the override effective. + */ +describe('an ancestor can widen/narrow a component through its token', () => { + @Component({ + standalone: true, + imports: [FfSelectComponent], + template: ` +
+ +
+ `, + }) + class NarrowingHostComponent {} + + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [NarrowingHostComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(NarrowingHostComponent); + fixture.detectChanges(); + }); + + it('renders the select as a descendant of the ancestor that declares the override', () => { + const host = fixture.nativeElement as HTMLElement; + const container = host.querySelector('.narrow-container') as HTMLElement; + const select = host.querySelector('ff-select') as HTMLElement; + + expect(container).toBeTruthy(); + expect(select).toBeTruthy(); + expect(container.contains(select)).toBe(true); + }); + + it('exposes the overriding token on the ancestor, ready to be inherited', () => { + const host = fixture.nativeElement as HTMLElement; + const container = host.querySelector('.narrow-container') as HTMLElement; + + expect(container.style.getPropertyValue('--ff-select-min-width').trim()).toBe('64px'); + }); + + it('confirms the select never re-declares --ff-select-min-width, so the ancestor value is free to cascade', () => { + const scssPath = join(libDir, 'primitives', 'ff-select', 'ff-select.component.scss'); + const scss = readFileSync(scssPath, 'utf8'); + + expect(scss).toMatch(/min-width:\s*var\(--ff-select-min-width,\s*180px\)/); + expect(scss).not.toMatch(/^\s*--ff-select-min-width\s*:/m); + }); +}); diff --git a/packages/design-system/src/lib/primitives/ff-avatar/ff-avatar.component.scss b/packages/design-system/src/lib/primitives/ff-avatar/ff-avatar.component.scss index 0f17d29..aae2c35 100644 --- a/packages/design-system/src/lib/primitives/ff-avatar/ff-avatar.component.scss +++ b/packages/design-system/src/lib/primitives/ff-avatar/ff-avatar.component.scss @@ -1,14 +1,13 @@ .ff-avatar { - // Component tokens - --ff-avatar-bg: var(--ff-color-primary-100); - --ff-avatar-color: var(--ff-color-primary-700); - + // Component tokens are consumed with a fallback, never declared here: a + // declaration on this same selector would always beat a value inherited + // from an ancestor, so a container could never retint an avatar. display: inline-flex; align-items: center; justify-content: center; border-radius: var(--ff-radius-full); - background-color: var(--ff-avatar-bg); - color: var(--ff-avatar-color); + background-color: var(--ff-avatar-bg, var(--ff-color-primary-100)); + color: var(--ff-avatar-color, var(--ff-color-primary-700)); overflow: hidden; flex-shrink: 0; diff --git a/packages/design-system/src/lib/primitives/ff-banner/ff-banner.component.scss b/packages/design-system/src/lib/primitives/ff-banner/ff-banner.component.scss index e95100f..508cf6a 100644 --- a/packages/design-system/src/lib/primitives/ff-banner/ff-banner.component.scss +++ b/packages/design-system/src/lib/primitives/ff-banner/ff-banner.component.scss @@ -1,19 +1,23 @@ .ff-banner { - --ff-banner-bg: var(--ff-color-info-50); - --ff-banner-border: var(--ff-color-info-200); - --ff-banner-text: var(--ff-color-neutral-900); - --ff-banner-icon-color: var(--ff-color-info-600); - --ff-banner-action-color: var(--ff-color-info-700); - + // Component tokens are consumed with a fallback, never declared here: a + // declaration on this same selector would always beat a value inherited + // from an ancestor. The host always renders a `ff-banner--` variant + // class alongside this one, so `--ff-banner-bg`/`-border`/`-icon-color`/ + // `-action-color` are provided per variant below (the variant is the + // semantic decision, so those declarations are the final value on + // purpose and are not meant to be retinted from outside); the info + // values here are only the literal fallback used before any variant + // declaration reaches the element. `--ff-banner-text` has no per-variant + // override, so it is a plain reserved-default fallback. display: flex; align-items: center; gap: var(--ff-spacing-sm); padding: var(--ff-spacing-sm) var(--ff-spacing-md); - background-color: var(--ff-banner-bg); - border: 1px solid var(--ff-banner-border); + background-color: var(--ff-banner-bg, var(--ff-color-info-50)); + border: 1px solid var(--ff-banner-border, var(--ff-color-info-200)); font-family: var(--ff-font-family); font-size: var(--ff-font-size-sm); - color: var(--ff-banner-text); + color: var(--ff-banner-text, var(--ff-color-neutral-900)); width: 100%; // Variant overrides @@ -47,7 +51,7 @@ &__icon { flex-shrink: 0; - color: var(--ff-banner-icon-color); + color: var(--ff-banner-icon-color, var(--ff-color-info-600)); font-size: var(--ff-font-size-md); } @@ -66,7 +70,7 @@ &__action { border: none; background: none; - color: var(--ff-banner-action-color); + color: var(--ff-banner-action-color, var(--ff-color-info-700)); font-family: var(--ff-font-family); font-size: var(--ff-font-size-sm); font-weight: var(--ff-font-weight-semibold); diff --git a/packages/design-system/src/lib/primitives/ff-bottom-sheet/ff-bottom-sheet.component.scss b/packages/design-system/src/lib/primitives/ff-bottom-sheet/ff-bottom-sheet.component.scss index dc88839..616dbdb 100644 --- a/packages/design-system/src/lib/primitives/ff-bottom-sheet/ff-bottom-sheet.component.scss +++ b/packages/design-system/src/lib/primitives/ff-bottom-sheet/ff-bottom-sheet.component.scss @@ -1,10 +1,7 @@ .ff-bottom-sheet { - --ff-bottom-sheet-bg: var(--ff-color-surface); - --ff-bottom-sheet-border: var(--ff-color-neutral-200); - --ff-bottom-sheet-title-color: var(--ff-color-neutral-900); - --ff-bottom-sheet-body-color: var(--ff-color-neutral-700); - --ff-bottom-sheet-backdrop: rgba(0, 0, 0, 0.4); - + // Component tokens are consumed with a fallback, never declared here: a + // declaration on this same selector would always beat a value inherited + // from an ancestor, so a container could never restyle a bottom sheet. display: contents; &__backdrop { @@ -14,7 +11,7 @@ display: flex; align-items: flex-end; justify-content: center; - background-color: var(--ff-bottom-sheet-backdrop); + background-color: var(--ff-bottom-sheet-backdrop, rgba(0, 0, 0, 0.4)); } &__panel { @@ -23,7 +20,7 @@ width: 100%; max-width: 640px; max-height: 85vh; - background-color: var(--ff-bottom-sheet-bg); + background-color: var(--ff-bottom-sheet-bg, var(--ff-color-surface)); border-radius: var(--ff-radius-lg) var(--ff-radius-lg) 0 0; box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.15); font-family: var(--ff-font-family); @@ -51,14 +48,14 @@ align-items: center; justify-content: space-between; padding: var(--ff-spacing-md) var(--ff-spacing-lg); - border-bottom: 1px solid var(--ff-bottom-sheet-border); + border-bottom: 1px solid var(--ff-bottom-sheet-border, var(--ff-color-neutral-200)); } &__title { margin: 0; font-size: var(--ff-font-size-lg); font-weight: var(--ff-font-weight-semibold); - color: var(--ff-bottom-sheet-title-color); + color: var(--ff-bottom-sheet-title-color, var(--ff-color-neutral-900)); } &__close { @@ -90,7 +87,7 @@ &__body { padding: var(--ff-spacing-lg); font-size: var(--ff-font-size-md); - color: var(--ff-bottom-sheet-body-color); + color: var(--ff-bottom-sheet-body-color, var(--ff-color-neutral-700)); flex: 1; overflow-y: auto; } @@ -101,7 +98,7 @@ &__footer { padding: var(--ff-spacing-md) var(--ff-spacing-lg); - border-top: 1px solid var(--ff-bottom-sheet-border); + border-top: 1px solid var(--ff-bottom-sheet-border, var(--ff-color-neutral-200)); display: flex; justify-content: flex-end; gap: var(--ff-spacing-sm); diff --git a/packages/design-system/src/lib/primitives/ff-button/ff-button.component.scss b/packages/design-system/src/lib/primitives/ff-button/ff-button.component.scss index 7a5d563..f9309af 100644 --- a/packages/design-system/src/lib/primitives/ff-button/ff-button.component.scss +++ b/packages/design-system/src/lib/primitives/ff-button/ff-button.component.scss @@ -1,11 +1,8 @@ .ff-button { - // Shared component tokens - --ff-btn-radius: var(--ff-radius-md); - --ff-btn-focus-ring: var(--ff-color-border-focus); - --ff-btn-icon-size: 1.2em; - --ff-btn-on-solid-color: var(--ff-color-on-primary); - --ff-btn-ghost-hover-bg: var(--ff-color-neutral-100); - + // Shared component tokens are consumed with a fallback, never declared + // here: a declaration on this same selector would always beat a value + // inherited from an ancestor, so a container could never restyle a + // button. The color-axis hooks below already follow this pattern. display: inline-block; &__native { @@ -14,7 +11,7 @@ justify-content: center; gap: var(--ff-spacing-sm); border: 1px solid transparent; - border-radius: var(--ff-btn-radius); + border-radius: var(--ff-btn-radius, var(--ff-radius-md)); font-family: var(--ff-font-family); font-weight: var(--ff-font-weight-medium); cursor: pointer; @@ -55,7 +52,7 @@ // Style axis: solid &--solid .ff-button__native { background-color: var(--ff-btn-color-bg); - color: var(--ff-btn-on-solid-color); + color: var(--ff-btn-on-solid-color, var(--ff-color-on-primary)); border-color: var(--ff-btn-color-bg); &:hover:not(:disabled) { @@ -68,7 +65,7 @@ } &:focus-visible { - outline: 2px solid var(--ff-btn-focus-ring); + outline: 2px solid var(--ff-btn-focus-ring, var(--ff-color-border-focus)); outline-offset: 2px; } } @@ -84,7 +81,7 @@ } &:focus-visible { - outline: 2px solid var(--ff-btn-focus-ring); + outline: 2px solid var(--ff-btn-focus-ring, var(--ff-color-border-focus)); outline-offset: 2px; } } @@ -96,11 +93,11 @@ border-color: transparent; &:hover:not(:disabled) { - background-color: var(--ff-btn-ghost-hover-bg); + background-color: var(--ff-btn-ghost-hover-bg, var(--ff-color-neutral-100)); } &:focus-visible { - outline: 2px solid var(--ff-btn-focus-ring); + outline: 2px solid var(--ff-btn-focus-ring, var(--ff-color-border-focus)); outline-offset: 2px; } } @@ -119,8 +116,8 @@ // Spinner (matches icon size) &__spinner { display: inline-block; - width: var(--ff-btn-icon-size); - height: var(--ff-btn-icon-size); + width: var(--ff-btn-icon-size, 1.2em); + height: var(--ff-btn-icon-size, 1.2em); border: 2px solid currentColor; border-right-color: transparent; border-radius: var(--ff-radius-full); @@ -133,7 +130,7 @@ display: inline-flex; align-items: center; flex-shrink: 0; - font-size: var(--ff-btn-icon-size); + font-size: var(--ff-btn-icon-size, 1.2em); color: var(--ff-btn-icon-color, inherit); line-height: 1; } diff --git a/packages/design-system/src/lib/primitives/ff-card/ff-card.component.scss b/packages/design-system/src/lib/primitives/ff-card/ff-card.component.scss index 1b98d64..c80b1ec 100644 --- a/packages/design-system/src/lib/primitives/ff-card/ff-card.component.scss +++ b/packages/design-system/src/lib/primitives/ff-card/ff-card.component.scss @@ -1,14 +1,11 @@ .ff-card { - // Component tokens - --ff-card-bg: var(--ff-color-surface); - --ff-card-border: var(--ff-color-neutral-200); - --ff-card-radius: var(--ff-radius-md); - --ff-card-header-color: var(--ff-color-neutral-900); - + // Component tokens are consumed with a fallback, never declared here: a + // declaration on this same selector would always beat a value inherited + // from an ancestor, so a container could never restyle a card. display: block; - background-color: var(--ff-card-bg); - border: 1px solid var(--ff-card-border); - border-radius: var(--ff-card-radius); + background-color: var(--ff-card-bg, var(--ff-color-surface)); + border: 1px solid var(--ff-card-border, var(--ff-color-neutral-200)); + border-radius: var(--ff-card-radius, var(--ff-radius-md)); overflow: hidden; // Shadow variants @@ -31,16 +28,16 @@ // Header zone [ff-card-header] { padding: var(--ff-spacing-md) var(--ff-spacing-lg); - border-bottom: 1px solid var(--ff-card-border); + border-bottom: 1px solid var(--ff-card-border, var(--ff-color-neutral-200)); font-family: var(--ff-font-family); font-size: var(--ff-font-size-lg); font-weight: var(--ff-font-weight-semibold); - color: var(--ff-card-header-color); + color: var(--ff-card-header-color, var(--ff-color-neutral-900)); } // Footer zone [ff-card-footer] { padding: var(--ff-spacing-md) var(--ff-spacing-lg); - border-top: 1px solid var(--ff-card-border); + border-top: 1px solid var(--ff-card-border, var(--ff-color-neutral-200)); } } diff --git a/packages/design-system/src/lib/primitives/ff-checkbox/ff-checkbox.component.scss b/packages/design-system/src/lib/primitives/ff-checkbox/ff-checkbox.component.scss index 7440af2..508f967 100644 --- a/packages/design-system/src/lib/primitives/ff-checkbox/ff-checkbox.component.scss +++ b/packages/design-system/src/lib/primitives/ff-checkbox/ff-checkbox.component.scss @@ -1,13 +1,7 @@ .ff-checkbox { - // Component tokens - --ff-checkbox-border: var(--ff-color-neutral-400); - --ff-checkbox-bg: var(--ff-color-surface); - --ff-checkbox-checked-bg: var(--ff-color-primary-500); - --ff-checkbox-checked-border: var(--ff-color-primary-500); - --ff-checkbox-check-color: var(--ff-color-on-primary); - --ff-checkbox-label-color: var(--ff-color-neutral-900); - --ff-checkbox-focus-ring: var(--ff-color-border-focus); - + // Component tokens are consumed with a fallback, never declared here: a + // declaration on this same selector would always beat a value inherited + // from an ancestor, so a container could never restyle a checkbox. display: inline-flex; &__wrapper { @@ -31,9 +25,9 @@ justify-content: center; width: 18px; height: 18px; - border: 2px solid var(--ff-checkbox-border); + border: 2px solid var(--ff-checkbox-border, var(--ff-color-neutral-400)); border-radius: var(--ff-radius-sm); - background-color: var(--ff-checkbox-bg); + background-color: var(--ff-checkbox-bg, var(--ff-color-surface)); transition: background-color 0.15s, border-color 0.15s; flex-shrink: 0; @@ -46,20 +40,20 @@ &__label { font-family: var(--ff-font-family); font-size: var(--ff-font-size-md); - color: var(--ff-checkbox-label-color); + color: var(--ff-checkbox-label-color, var(--ff-color-neutral-900)); user-select: none; } // Checked state &--checked .ff-checkbox__box { - background-color: var(--ff-checkbox-checked-bg); - border-color: var(--ff-checkbox-checked-border); + background-color: var(--ff-checkbox-checked-bg, var(--ff-color-primary-500)); + border-color: var(--ff-checkbox-checked-border, var(--ff-color-primary-500)); &::after { display: block; width: 5px; height: 9px; - border: solid var(--ff-checkbox-check-color); + border: solid var(--ff-checkbox-check-color, var(--ff-color-on-primary)); border-width: 0 2px 2px 0; transform: rotate(45deg) translate(-1px, -1px); } @@ -67,14 +61,14 @@ // Indeterminate state &--indeterminate .ff-checkbox__box { - background-color: var(--ff-checkbox-checked-bg); - border-color: var(--ff-checkbox-checked-border); + background-color: var(--ff-checkbox-checked-bg, var(--ff-color-primary-500)); + border-color: var(--ff-checkbox-checked-border, var(--ff-color-primary-500)); &::after { display: block; width: 8px; height: 2px; - background-color: var(--ff-checkbox-check-color); + background-color: var(--ff-checkbox-check-color, var(--ff-color-on-primary)); border: none; transform: none; } @@ -82,7 +76,7 @@ // Focus ring &__native:focus-visible ~ .ff-checkbox__box { - outline: 2px solid var(--ff-checkbox-focus-ring); + outline: 2px solid var(--ff-checkbox-focus-ring, var(--ff-color-border-focus)); outline-offset: 2px; } diff --git a/packages/design-system/src/lib/primitives/ff-chip/ff-chip.component.scss b/packages/design-system/src/lib/primitives/ff-chip/ff-chip.component.scss index 156b915..7c03fd3 100644 --- a/packages/design-system/src/lib/primitives/ff-chip/ff-chip.component.scss +++ b/packages/design-system/src/lib/primitives/ff-chip/ff-chip.component.scss @@ -1,13 +1,7 @@ .ff-chip { - // Component tokens - --ff-chip-bg: var(--ff-color-surface); - --ff-chip-border: var(--ff-color-border); - --ff-chip-color: var(--ff-color-neutral-700); - --ff-chip-selected-bg: var(--ff-color-primary-100); - --ff-chip-selected-border: var(--ff-color-primary-500); - --ff-chip-selected-color: var(--ff-color-primary-700); - --ff-chip-focus-ring: var(--ff-color-border-focus); - + // Component tokens are consumed with a fallback, never declared here: a + // declaration on this same selector would always beat a value inherited + // from an ancestor, so a container could never restyle a chip. display: inline-flex; align-items: center; border-radius: var(--ff-radius-full); @@ -17,17 +11,17 @@ &__body { display: inline-flex; align-items: center; - border: 1px solid var(--ff-chip-border); + border: 1px solid var(--ff-chip-border, var(--ff-color-border)); border-radius: var(--ff-radius-full); - background: var(--ff-chip-bg); - color: var(--ff-chip-color); + background: var(--ff-chip-bg, var(--ff-color-surface)); + color: var(--ff-chip-color, var(--ff-color-neutral-700)); font-family: inherit; cursor: pointer; line-height: 1; white-space: nowrap; &:focus-visible { - outline: 2px solid var(--ff-chip-focus-ring); + outline: 2px solid var(--ff-chip-focus-ring, var(--ff-color-border-focus)); outline-offset: 1px; } } @@ -52,7 +46,7 @@ } &:focus-visible { - outline: 2px solid var(--ff-chip-focus-ring); + outline: 2px solid var(--ff-chip-focus-ring, var(--ff-color-border-focus)); outline-offset: 1px; } @@ -76,8 +70,8 @@ // --- Removable: border on host, children transparent --- &--removable { - border: 1px solid var(--ff-chip-border); - background: var(--ff-chip-bg); + border: 1px solid var(--ff-chip-border, var(--ff-color-border)); + background: var(--ff-chip-bg, var(--ff-color-surface)); } &--removable .ff-chip__body { @@ -104,9 +98,9 @@ // --- Filter variant: selected state --- &--filter.ff-chip--selected .ff-chip__body { - background-color: var(--ff-chip-selected-bg); - border-color: var(--ff-chip-selected-border); - color: var(--ff-chip-selected-color); + background-color: var(--ff-chip-selected-bg, var(--ff-color-primary-100)); + border-color: var(--ff-chip-selected-border, var(--ff-color-primary-500)); + color: var(--ff-chip-selected-color, var(--ff-color-primary-700)); } // --- Disabled --- diff --git a/packages/design-system/src/lib/primitives/ff-dialog/ff-dialog.component.scss b/packages/design-system/src/lib/primitives/ff-dialog/ff-dialog.component.scss index b5ad8af..c17e740 100644 --- a/packages/design-system/src/lib/primitives/ff-dialog/ff-dialog.component.scss +++ b/packages/design-system/src/lib/primitives/ff-dialog/ff-dialog.component.scss @@ -1,12 +1,7 @@ .ff-dialog { - // Component tokens - --ff-dialog-bg: var(--ff-color-surface); - --ff-dialog-radius: var(--ff-radius-lg); - --ff-dialog-border: var(--ff-color-neutral-200); - --ff-dialog-title-color: var(--ff-color-neutral-900); - --ff-dialog-body-color: var(--ff-color-neutral-700); - --ff-dialog-backdrop: rgba(0, 0, 0, 0.4); - + // Component tokens are consumed with a fallback, never declared here: a + // declaration on this same selector would always beat a value inherited + // from an ancestor, so a container could never restyle a dialog. display: contents; &__backdrop { @@ -16,7 +11,7 @@ display: flex; align-items: center; justify-content: center; - background-color: var(--ff-dialog-backdrop); + background-color: var(--ff-dialog-backdrop, rgba(0, 0, 0, 0.4)); } &__panel { @@ -26,8 +21,8 @@ max-width: 560px; width: 100%; max-height: 85vh; - background-color: var(--ff-dialog-bg); - border-radius: var(--ff-dialog-radius); + background-color: var(--ff-dialog-bg, var(--ff-color-surface)); + border-radius: var(--ff-dialog-radius, var(--ff-radius-lg)); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); font-family: var(--ff-font-family); } @@ -37,7 +32,7 @@ align-items: center; justify-content: space-between; padding: var(--ff-spacing-lg); - border-bottom: 1px solid var(--ff-dialog-border); + border-bottom: 1px solid var(--ff-dialog-border, var(--ff-color-neutral-200)); } &__close { @@ -70,20 +65,20 @@ margin: 0; font-size: var(--ff-font-size-lg); font-weight: var(--ff-font-weight-semibold); - color: var(--ff-dialog-title-color); + color: var(--ff-dialog-title-color, var(--ff-color-neutral-900)); } &__body { padding: var(--ff-spacing-lg); font-size: var(--ff-font-size-md); - color: var(--ff-dialog-body-color); + color: var(--ff-dialog-body-color, var(--ff-color-neutral-700)); flex: 1; overflow-y: auto; } &__footer { padding: var(--ff-spacing-md) var(--ff-spacing-lg); - border-top: 1px solid var(--ff-dialog-border); + border-top: 1px solid var(--ff-dialog-border, var(--ff-color-neutral-200)); display: flex; justify-content: flex-end; gap: var(--ff-spacing-sm); diff --git a/packages/design-system/src/lib/primitives/ff-divider/ff-divider.component.scss b/packages/design-system/src/lib/primitives/ff-divider/ff-divider.component.scss index 01d8061..0b68e39 100644 --- a/packages/design-system/src/lib/primitives/ff-divider/ff-divider.component.scss +++ b/packages/design-system/src/lib/primitives/ff-divider/ff-divider.component.scss @@ -1,7 +1,7 @@ .ff-divider { - // Component tokens - --ff-divider-color: var(--ff-color-border); - + // Component tokens are consumed with a fallback, never declared here: a + // declaration on this same selector would always beat a value inherited + // from an ancestor, so a container could never recolor a divider. display: block; flex-shrink: 0; border: 0; @@ -10,7 +10,7 @@ &--horizontal { width: 100%; border-top-style: solid; - border-top-color: var(--ff-divider-color); + border-top-color: var(--ff-divider-color, var(--ff-color-border)); } &--horizontal.ff-divider--thin { @@ -26,7 +26,7 @@ display: inline-block; align-self: stretch; border-left-style: solid; - border-left-color: var(--ff-divider-color); + border-left-color: var(--ff-divider-color, var(--ff-color-border)); } &--vertical.ff-divider--thin { diff --git a/packages/design-system/src/lib/primitives/ff-empty-state/ff-empty-state.component.scss b/packages/design-system/src/lib/primitives/ff-empty-state/ff-empty-state.component.scss index 8359554..b2f30ee 100644 --- a/packages/design-system/src/lib/primitives/ff-empty-state/ff-empty-state.component.scss +++ b/packages/design-system/src/lib/primitives/ff-empty-state/ff-empty-state.component.scss @@ -1,14 +1,13 @@ .ff-empty-state { - // Component tokens (overridable by consumers) - --ff-empty-state-gap: var(--ff-spacing-sm); - --ff-empty-state-padding: var(--ff-spacing-xl); - + // Component tokens are consumed with a fallback, never declared here: a + // declaration on this same selector would always beat a value inherited + // from an ancestor, so a container could never resize an empty state. display: flex; flex-direction: column; align-items: center; justify-content: center; - gap: var(--ff-empty-state-gap); - padding: var(--ff-empty-state-padding); + gap: var(--ff-empty-state-gap, var(--ff-spacing-sm)); + padding: var(--ff-empty-state-padding, var(--ff-spacing-xl)); text-align: center; font-family: var(--ff-font-family); diff --git a/packages/design-system/src/lib/primitives/ff-icon-button/ff-icon-button.component.scss b/packages/design-system/src/lib/primitives/ff-icon-button/ff-icon-button.component.scss index 2ffe5e9..ecb990f 100644 --- a/packages/design-system/src/lib/primitives/ff-icon-button/ff-icon-button.component.scss +++ b/packages/design-system/src/lib/primitives/ff-icon-button/ff-icon-button.component.scss @@ -1,12 +1,7 @@ .ff-icon-button { - // Component tokens - --ff-icon-btn-color: var(--ff-color-neutral-700); - --ff-icon-btn-hover-color: var(--ff-color-primary-500); - --ff-icon-btn-hover-bg: var(--ff-color-neutral-100); - --ff-icon-btn-active-bg: var(--ff-color-neutral-200); - --ff-icon-btn-radius: var(--ff-radius-md); - --ff-icon-btn-focus-ring: var(--ff-color-border-focus); - + // Component tokens are consumed with a fallback, never declared here: a + // declaration on this same selector would always beat a value inherited + // from an ancestor, so a container could never restyle an icon button. display: inline-block; &__native { @@ -14,24 +9,24 @@ align-items: center; justify-content: center; border: 1px solid transparent; - border-radius: var(--ff-icon-btn-radius); + border-radius: var(--ff-icon-btn-radius, var(--ff-radius-md)); background-color: transparent; - color: var(--ff-icon-btn-color); + color: var(--ff-icon-btn-color, var(--ff-color-neutral-700)); cursor: pointer; transition: background-color 0.2s, color 0.2s, box-shadow 0.2s; padding: 0; &:hover:not(:disabled) { - background-color: var(--ff-icon-btn-hover-bg); - color: var(--ff-icon-btn-hover-color); + background-color: var(--ff-icon-btn-hover-bg, var(--ff-color-neutral-100)); + color: var(--ff-icon-btn-hover-color, var(--ff-color-primary-500)); } &:active:not(:disabled) { - background-color: var(--ff-icon-btn-active-bg); + background-color: var(--ff-icon-btn-active-bg, var(--ff-color-neutral-200)); } &:focus-visible { - outline: 2px solid var(--ff-icon-btn-focus-ring); + outline: 2px solid var(--ff-icon-btn-focus-ring, var(--ff-color-border-focus)); outline-offset: 2px; } } diff --git a/packages/design-system/src/lib/primitives/ff-input/ff-input.component.scss b/packages/design-system/src/lib/primitives/ff-input/ff-input.component.scss index e6d660e..82621e6 100644 --- a/packages/design-system/src/lib/primitives/ff-input/ff-input.component.scss +++ b/packages/design-system/src/lib/primitives/ff-input/ff-input.component.scss @@ -1,19 +1,7 @@ .ff-input { - // Component tokens - --ff-input-bg: var(--ff-color-surface); - --ff-input-border: var(--ff-color-border); - --ff-input-color: var(--ff-color-neutral-900); - --ff-input-placeholder: var(--ff-color-neutral-400); - --ff-input-focus-border: var(--ff-color-primary-500); - --ff-input-focus-ring: var(--ff-color-border-focus); - --ff-input-label-color: var(--ff-color-neutral-700); - --ff-input-hint-color: var(--ff-color-neutral-500); - --ff-input-error-color: var(--ff-color-error-600); - --ff-input-error-border: var(--ff-color-error-500); - --ff-input-error-ring: var(--ff-color-error-200); - --ff-input-affix-color: var(--ff-color-neutral-500); - --ff-input-radius: var(--ff-radius-md); - + // Component tokens are consumed with a fallback, never declared here: a + // declaration on this same selector would always beat a value inherited + // from an ancestor, so a container could never restyle an input. display: flex; flex-direction: column; gap: var(--ff-spacing-xs); @@ -22,7 +10,7 @@ font-family: var(--ff-font-family); font-size: var(--ff-font-size-sm); font-weight: var(--ff-font-weight-medium); - color: var(--ff-input-label-color); + color: var(--ff-input-label-color, var(--ff-color-neutral-700)); } // Field wrapper: carries the box (border, background, focus ring) so the @@ -30,14 +18,14 @@ &__field { display: flex; align-items: center; - background-color: var(--ff-input-bg); - border: 1px solid var(--ff-input-border); - border-radius: var(--ff-input-radius); + background-color: var(--ff-input-bg, var(--ff-color-surface)); + border: 1px solid var(--ff-input-border, var(--ff-color-border)); + border-radius: var(--ff-input-radius, var(--ff-radius-md)); transition: border-color 0.2s, box-shadow 0.2s; &:focus-within { - border-color: var(--ff-input-focus-border); - box-shadow: 0 0 0 2px var(--ff-input-focus-ring); + border-color: var(--ff-input-focus-border, var(--ff-color-primary-500)); + box-shadow: 0 0 0 2px var(--ff-input-focus-ring, var(--ff-color-border-focus)); } } @@ -50,7 +38,7 @@ display: inline-flex; align-items: center; flex-shrink: 0; - color: var(--ff-input-affix-color); + color: var(--ff-input-affix-color, var(--ff-color-neutral-500)); &:empty { display: none; @@ -74,14 +62,14 @@ font-family: var(--ff-font-family); font-size: var(--ff-font-size-md); line-height: var(--ff-line-height-normal); - color: var(--ff-input-color); + color: var(--ff-input-color, var(--ff-color-neutral-900)); background-color: transparent; border: none; - border-radius: var(--ff-input-radius); + border-radius: var(--ff-input-radius, var(--ff-radius-md)); outline: none; &::placeholder { - color: var(--ff-input-placeholder); + color: var(--ff-input-placeholder, var(--ff-color-neutral-400)); } &:disabled { @@ -93,13 +81,13 @@ &__hint { font-family: var(--ff-font-family); font-size: var(--ff-font-size-xs); - color: var(--ff-input-hint-color); + color: var(--ff-input-hint-color, var(--ff-color-neutral-500)); } &__error { font-family: var(--ff-font-family); font-size: var(--ff-font-size-xs); - color: var(--ff-input-error-color); + color: var(--ff-input-error-color, var(--ff-color-error-600)); } // Textarea @@ -119,22 +107,22 @@ padding: 0 var(--ff-spacing-xs); font-size: var(--ff-font-size-xs); line-height: 1; - background-color: var(--ff-input-bg); + background-color: var(--ff-input-bg, var(--ff-color-surface)); z-index: 1; } } // Error state &--error .ff-input__field { - border-color: var(--ff-input-error-border); + border-color: var(--ff-input-error-border, var(--ff-color-error-500)); &:focus-within { - box-shadow: 0 0 0 2px var(--ff-input-error-ring); + box-shadow: 0 0 0 2px var(--ff-input-error-ring, var(--ff-color-error-200)); } } &--error .ff-input__label { - color: var(--ff-input-error-color); + color: var(--ff-input-error-color, var(--ff-color-error-600)); } // Disabled state diff --git a/packages/design-system/src/lib/primitives/ff-link/ff-link.component.scss b/packages/design-system/src/lib/primitives/ff-link/ff-link.component.scss index 6dfd747..1378da0 100644 --- a/packages/design-system/src/lib/primitives/ff-link/ff-link.component.scss +++ b/packages/design-system/src/lib/primitives/ff-link/ff-link.component.scss @@ -1,13 +1,11 @@ .ff-link { - // Component tokens - --ff-link-color: var(--ff-color-info-600); - --ff-link-hover-color: var(--ff-color-info-700); - --ff-link-focus-ring: var(--ff-color-border-focus); - + // Component tokens are consumed with a fallback, never declared here: a + // declaration on this same selector would always beat a value inherited + // from an ancestor, so a container could never restyle a link. display: inline; &__anchor { - color: var(--ff-link-color); + color: var(--ff-link-color, var(--ff-color-info-600)); font-family: var(--ff-font-family); font-size: inherit; text-decoration: none; @@ -15,11 +13,11 @@ transition: color 0.15s; &:hover { - color: var(--ff-link-hover-color); + color: var(--ff-link-hover-color, var(--ff-color-info-700)); } &:focus-visible { - outline: 2px solid var(--ff-link-focus-ring); + outline: 2px solid var(--ff-link-focus-ring, var(--ff-color-border-focus)); outline-offset: 2px; border-radius: var(--ff-radius-sm); } diff --git a/packages/design-system/src/lib/primitives/ff-loader/ff-loader.component.scss b/packages/design-system/src/lib/primitives/ff-loader/ff-loader.component.scss index d22f388..23b6383 100644 --- a/packages/design-system/src/lib/primitives/ff-loader/ff-loader.component.scss +++ b/packages/design-system/src/lib/primitives/ff-loader/ff-loader.component.scss @@ -1,10 +1,7 @@ .ff-loader { - // Component tokens - --ff-loader-track: var(--ff-color-neutral-200); - --ff-loader-indicator: var(--ff-color-primary-500); - --ff-loader-skeleton-from: var(--ff-color-neutral-100); - --ff-loader-skeleton-to: var(--ff-color-neutral-200); - + // Component tokens are consumed with a fallback, never declared here: a + // declaration on this same selector would always beat a value inherited + // from an ancestor, so a container could never retint a loader. display: inline-flex; align-items: center; justify-content: center; @@ -13,8 +10,8 @@ &__spinner { display: inline-block; border-radius: var(--ff-radius-full); - border: 2px solid var(--ff-loader-track); - border-top-color: var(--ff-loader-indicator); + border: 2px solid var(--ff-loader-track, var(--ff-color-neutral-200)); + border-top-color: var(--ff-loader-indicator, var(--ff-color-primary-500)); animation: ff-loader-spin 0.7s linear infinite; } @@ -24,9 +21,9 @@ border-radius: var(--ff-radius-sm); background: linear-gradient( 90deg, - var(--ff-loader-skeleton-from) 25%, - var(--ff-loader-skeleton-to) 50%, - var(--ff-loader-skeleton-from) 75% + var(--ff-loader-skeleton-from, var(--ff-color-neutral-100)) 25%, + var(--ff-loader-skeleton-to, var(--ff-color-neutral-200)) 50%, + var(--ff-loader-skeleton-from, var(--ff-color-neutral-100)) 75% ); background-size: 200% 100%; animation: ff-loader-shimmer 1.5s ease-in-out infinite; diff --git a/packages/design-system/src/lib/primitives/ff-panel/ff-panel.component.scss b/packages/design-system/src/lib/primitives/ff-panel/ff-panel.component.scss index ac66f89..da5eddb 100644 --- a/packages/design-system/src/lib/primitives/ff-panel/ff-panel.component.scss +++ b/packages/design-system/src/lib/primitives/ff-panel/ff-panel.component.scss @@ -1,13 +1,10 @@ .ff-panel { - // Component tokens (overridable by consumers) - --ff-panel-bg: var(--ff-color-surface); - --ff-panel-border: var(--ff-color-border); - --ff-panel-radius: var(--ff-radius-lg); - --ff-panel-padding: var(--ff-spacing-md); - + // Component tokens are consumed with a fallback, never declared here: a + // declaration on this same selector would always beat a value inherited + // from an ancestor, so a container could never retint/resize a panel. display: block; - background-color: var(--ff-panel-bg); - border-radius: var(--ff-panel-radius); + background-color: var(--ff-panel-bg, var(--ff-color-surface)); + border-radius: var(--ff-panel-radius, var(--ff-radius-lg)); font-family: var(--ff-font-family); overflow: hidden; @@ -52,7 +49,7 @@ // ---- Appearances ---- &--card { - border: 1px solid var(--ff-panel-border); + border: 1px solid var(--ff-panel-border, var(--ff-color-border)); &.ff-panel--fill { background-color: var(--ff-panel-tint); @@ -74,7 +71,7 @@ display: flex; align-items: center; gap: var(--ff-spacing-sm); - padding: var(--ff-panel-padding); + padding: var(--ff-panel-padding, var(--ff-spacing-md)); padding-bottom: 0; &:empty { @@ -97,7 +94,7 @@ } &__body { - padding: var(--ff-panel-padding); + padding: var(--ff-panel-padding, var(--ff-spacing-md)); &:empty { display: none; @@ -105,7 +102,7 @@ } &__footer { - padding: var(--ff-panel-padding); + padding: var(--ff-panel-padding, var(--ff-spacing-md)); padding-top: 0; &:empty { diff --git a/packages/design-system/src/lib/primitives/ff-progress/ff-progress.component.scss b/packages/design-system/src/lib/primitives/ff-progress/ff-progress.component.scss index 51f41e0..43809aa 100644 --- a/packages/design-system/src/lib/primitives/ff-progress/ff-progress.component.scss +++ b/packages/design-system/src/lib/primitives/ff-progress/ff-progress.component.scss @@ -1,22 +1,24 @@ .ff-progress { - // Component tokens (overridable by consumers) - --ff-progress-track-bg: var(--ff-color-neutral-200); - --ff-progress-fill-bg: var(--ff-color-primary-600); - + // Component tokens are consumed with a fallback, never declared here: a + // declaration on this same selector would always beat a value inherited + // from an ancestor. `--ff-progress-fill-bg` is also provided per semantic + // variant below (the variant is the final value on purpose); the primary + // value here is only the literal fallback used before any variant + // declaration reaches the element. display: flex; align-items: center; gap: var(--ff-spacing-sm); &__track { flex: 1 1 auto; - background-color: var(--ff-progress-track-bg); + background-color: var(--ff-progress-track-bg, var(--ff-color-neutral-200)); border-radius: var(--ff-radius-full); overflow: hidden; } &__fill { height: 100%; - background-color: var(--ff-progress-fill-bg); + background-color: var(--ff-progress-fill-bg, var(--ff-color-primary-600)); border-radius: var(--ff-radius-full); transition: width 0.2s ease; } diff --git a/packages/design-system/src/lib/primitives/ff-radio/ff-radio.component.scss b/packages/design-system/src/lib/primitives/ff-radio/ff-radio.component.scss index ad2e639..ab075b2 100644 --- a/packages/design-system/src/lib/primitives/ff-radio/ff-radio.component.scss +++ b/packages/design-system/src/lib/primitives/ff-radio/ff-radio.component.scss @@ -1,13 +1,7 @@ .ff-radio { - // Component tokens - --ff-radio-border: var(--ff-color-neutral-400); - --ff-radio-bg: var(--ff-color-surface); - --ff-radio-selected-bg: var(--ff-color-primary-500); - --ff-radio-selected-border: var(--ff-color-primary-500); - --ff-radio-dot-color: var(--ff-color-on-primary); - --ff-radio-label-color: var(--ff-color-neutral-900); - --ff-radio-focus-ring: var(--ff-color-border-focus); - + // Component tokens are consumed with a fallback, never declared here: a + // declaration on this same selector would always beat a value inherited + // from an ancestor, so a container could never restyle a radio group. display: inline-flex; gap: var(--ff-spacing-md); @@ -49,9 +43,9 @@ justify-content: center; width: 18px; height: 18px; - border: 2px solid var(--ff-radio-border); + border: 2px solid var(--ff-radio-border, var(--ff-color-neutral-400)); border-radius: 50%; - background-color: var(--ff-radio-bg); + background-color: var(--ff-radio-bg, var(--ff-color-surface)); transition: border-color 0.15s, background-color 0.15s; flex-shrink: 0; @@ -61,14 +55,14 @@ width: 8px; height: 8px; border-radius: 50%; - background-color: var(--ff-radio-dot-color); + background-color: var(--ff-radio-dot-color, var(--ff-color-on-primary)); } } // Selected state &__item--selected .ff-radio__dot { - background-color: var(--ff-radio-selected-bg); - border-color: var(--ff-radio-selected-border); + background-color: var(--ff-radio-selected-bg, var(--ff-color-primary-500)); + border-color: var(--ff-radio-selected-border, var(--ff-color-primary-500)); &::after { display: block; @@ -79,13 +73,13 @@ &__label { font-family: var(--ff-font-family); font-size: var(--ff-font-size-md); - color: var(--ff-radio-label-color); + color: var(--ff-radio-label-color, var(--ff-color-neutral-900)); user-select: none; } // Focus ring &__native:focus-visible ~ .ff-radio__dot { - outline: 2px solid var(--ff-radio-focus-ring); + outline: 2px solid var(--ff-radio-focus-ring, var(--ff-color-border-focus)); outline-offset: 2px; } diff --git a/packages/design-system/src/lib/primitives/ff-select/ff-select.component.scss b/packages/design-system/src/lib/primitives/ff-select/ff-select.component.scss index 9b44690..319ed00 100644 --- a/packages/design-system/src/lib/primitives/ff-select/ff-select.component.scss +++ b/packages/design-system/src/lib/primitives/ff-select/ff-select.component.scss @@ -1,17 +1,7 @@ .ff-select { - // Component tokens - --ff-select-bg: var(--ff-color-surface); - --ff-select-border: var(--ff-color-neutral-300); - --ff-select-color: var(--ff-color-neutral-900); - --ff-select-placeholder: var(--ff-color-neutral-400); - --ff-select-hover-border: var(--ff-color-primary-500); - --ff-select-focus-ring: var(--ff-color-border-focus); - --ff-select-dropdown-bg: var(--ff-color-surface); - --ff-select-dropdown-border: var(--ff-color-neutral-200); - --ff-select-option-hover-bg: var(--ff-color-neutral-100); - --ff-select-option-selected-color: var(--ff-color-primary-500); - --ff-select-radius: var(--ff-radius-sm); - + // Component tokens are consumed with a fallback, never declared here: a + // declaration on this same selector would always beat a value inherited + // from an ancestor, so a container could never restyle a select. display: inline-block; position: relative; // Left unspecified on purpose: declaring it here would beat any inherited @@ -30,21 +20,21 @@ justify-content: space-between; width: 100%; padding: var(--ff-spacing-sm) var(--ff-spacing-md); - background-color: var(--ff-select-bg); - border: 1px solid var(--ff-select-border); - border-radius: var(--ff-select-radius); + background-color: var(--ff-select-bg, var(--ff-color-surface)); + border: 1px solid var(--ff-select-border, var(--ff-color-neutral-300)); + border-radius: var(--ff-select-radius, var(--ff-radius-sm)); font-family: var(--ff-font-family); font-size: var(--ff-font-size-md); - color: var(--ff-select-color); + color: var(--ff-select-color, var(--ff-color-neutral-900)); cursor: pointer; transition: border-color 0.15s; &:hover:not(:disabled) { - border-color: var(--ff-select-hover-border); + border-color: var(--ff-select-hover-border, var(--ff-color-primary-500)); } &:focus-visible { - outline: 2px solid var(--ff-select-focus-ring); + outline: 2px solid var(--ff-select-focus-ring, var(--ff-color-border-focus)); outline-offset: 2px; } @@ -59,7 +49,7 @@ } &__search::placeholder { - color: var(--ff-select-placeholder); + color: var(--ff-select-placeholder, var(--ff-color-neutral-400)); } // Value / placeholder @@ -71,7 +61,7 @@ white-space: nowrap; &--placeholder { - color: var(--ff-select-placeholder); + color: var(--ff-select-placeholder, var(--ff-color-neutral-400)); } } @@ -91,9 +81,9 @@ // Dropdown panel content (portaled through the CDK overlay; positioning // and width matching are the overlay's own, this only styles the surface). &__dropdown { - background-color: var(--ff-select-dropdown-bg); - border: 1px solid var(--ff-select-dropdown-border); - border-radius: var(--ff-select-radius); + background-color: var(--ff-select-dropdown-bg, var(--ff-color-surface)); + border: 1px solid var(--ff-select-dropdown-border, var(--ff-color-neutral-200)); + border-radius: var(--ff-select-radius, var(--ff-radius-sm)); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); max-height: 240px; overflow-y: auto; @@ -107,20 +97,20 @@ padding: var(--ff-spacing-sm) var(--ff-spacing-md); font-family: var(--ff-font-family); font-size: var(--ff-font-size-md); - color: var(--ff-select-color); + color: var(--ff-select-color, var(--ff-color-neutral-900)); cursor: pointer; transition: background-color 0.1s; &:hover:not(&--disabled) { - background-color: var(--ff-select-option-hover-bg); + background-color: var(--ff-select-option-hover-bg, var(--ff-color-neutral-100)); } &--active { - background-color: var(--ff-select-option-hover-bg); + background-color: var(--ff-select-option-hover-bg, var(--ff-color-neutral-100)); } &--selected { - color: var(--ff-select-option-selected-color); + color: var(--ff-select-option-selected-color, var(--ff-color-primary-500)); font-weight: var(--ff-font-weight-semibold); } diff --git a/packages/design-system/src/lib/primitives/ff-skeleton/ff-skeleton.component.scss b/packages/design-system/src/lib/primitives/ff-skeleton/ff-skeleton.component.scss index 549ce63..4b737f9 100644 --- a/packages/design-system/src/lib/primitives/ff-skeleton/ff-skeleton.component.scss +++ b/packages/design-system/src/lib/primitives/ff-skeleton/ff-skeleton.component.scss @@ -1,17 +1,15 @@ .ff-skeleton { - // Component tokens (overridable by consumers) - --ff-skeleton-bg: var(--ff-color-neutral-200); - --ff-skeleton-highlight: var(--ff-color-neutral-100); - --ff-skeleton-radius: var(--ff-radius-sm); - + // Component tokens are consumed with a fallback, never declared here: a + // declaration on this same selector would always beat a value inherited + // from an ancestor, so a container could never retint a skeleton. display: flex; flex-direction: column; gap: var(--ff-spacing-sm); &__line { display: block; - background-color: var(--ff-skeleton-bg); - border-radius: var(--ff-skeleton-radius); + background-color: var(--ff-skeleton-bg, var(--ff-color-neutral-200)); + border-radius: var(--ff-skeleton-radius, var(--ff-radius-sm)); } &--circle .ff-skeleton__line { @@ -22,9 +20,9 @@ &--animated .ff-skeleton__line { background-image: linear-gradient( 90deg, - var(--ff-skeleton-bg) 25%, - var(--ff-skeleton-highlight) 50%, - var(--ff-skeleton-bg) 75% + var(--ff-skeleton-bg, var(--ff-color-neutral-200)) 25%, + var(--ff-skeleton-highlight, var(--ff-color-neutral-100)) 50%, + var(--ff-skeleton-bg, var(--ff-color-neutral-200)) 75% ); background-size: 200% 100%; animation: ff-skeleton-shimmer 1.5s ease-in-out infinite; diff --git a/packages/design-system/src/lib/primitives/ff-toast/ff-toast.component.scss b/packages/design-system/src/lib/primitives/ff-toast/ff-toast.component.scss index 10cd7b4..09b0041 100644 --- a/packages/design-system/src/lib/primitives/ff-toast/ff-toast.component.scss +++ b/packages/design-system/src/lib/primitives/ff-toast/ff-toast.component.scss @@ -1,21 +1,24 @@ .ff-toast { - --ff-toast-bg: var(--ff-color-surface); - --ff-toast-border: var(--ff-color-neutral-200); - --ff-toast-text: var(--ff-color-neutral-900); - --ff-toast-icon-color: var(--ff-color-neutral-500); - --ff-toast-radius: var(--ff-radius-md); - + // Component tokens are consumed with a fallback, never declared here: a + // declaration on this same selector would always beat a value inherited + // from an ancestor. The host always renders a `ff-toast--` variant + // class alongside this one, so `--ff-toast-border`/`-icon-color` are + // provided per variant below (the variant is the semantic decision, so + // those declarations are the final value on purpose); the neutral values + // here are only the literal fallback used before any variant declaration + // reaches the element. `--ff-toast-bg`/`-text`/`-radius` have no + // per-variant override, so they are plain reserved-default fallbacks. display: inline-flex; align-items: center; gap: var(--ff-spacing-sm); padding: var(--ff-spacing-sm) var(--ff-spacing-md); - background-color: var(--ff-toast-bg); - border: 1px solid var(--ff-toast-border); - border-radius: var(--ff-toast-radius); + background-color: var(--ff-toast-bg, var(--ff-color-surface)); + border: 1px solid var(--ff-toast-border, var(--ff-color-neutral-200)); + border-radius: var(--ff-toast-radius, var(--ff-radius-md)); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); font-family: var(--ff-font-family); font-size: var(--ff-font-size-sm); - color: var(--ff-toast-text); + color: var(--ff-toast-text, var(--ff-color-neutral-900)); min-width: 240px; max-width: 420px; @@ -42,7 +45,7 @@ &__icon { flex-shrink: 0; - color: var(--ff-toast-icon-color); + color: var(--ff-toast-icon-color, var(--ff-color-neutral-500)); font-size: var(--ff-font-size-md); } diff --git a/packages/design-system/src/lib/primitives/ff-tooltip/ff-tooltip.component.scss b/packages/design-system/src/lib/primitives/ff-tooltip/ff-tooltip.component.scss index e926fd0..720623e 100644 --- a/packages/design-system/src/lib/primitives/ff-tooltip/ff-tooltip.component.scss +++ b/packages/design-system/src/lib/primitives/ff-tooltip/ff-tooltip.component.scss @@ -1,9 +1,7 @@ .ff-tooltip { - // Component tokens - --ff-tooltip-bg: var(--ff-color-neutral-900); - --ff-tooltip-color: var(--ff-color-surface); - --ff-tooltip-radius: var(--ff-radius-sm); - + // Component tokens are consumed with a fallback, never declared here: a + // declaration on this same selector would always beat a value inherited + // from an ancestor, so a container could never restyle a tooltip. position: relative; display: inline-block; @@ -11,9 +9,9 @@ position: absolute; z-index: 1000; padding: var(--ff-spacing-xs); - background-color: var(--ff-tooltip-bg); - color: var(--ff-tooltip-color); - border-radius: var(--ff-tooltip-radius); + background-color: var(--ff-tooltip-bg, var(--ff-color-neutral-900)); + color: var(--ff-tooltip-color, var(--ff-color-surface)); + border-radius: var(--ff-tooltip-radius, var(--ff-radius-sm)); font-family: var(--ff-font-family); font-size: var(--ff-font-size-xs); line-height: 1.4;