Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 66 additions & 10 deletions packages/devextreme-scss/build/tokens/build-tokens.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import url from 'node:url';
import { createRequire } from 'node:module';
import { readdir, readFile, rm } from 'node:fs/promises';
import StyleDictionary from 'style-dictionary';
import { fileHeader, formattedVariables } from 'style-dictionary/utils';
import { registerTransforms } from './transforms.mjs';
import {
buildAvailableNames,
Expand Down Expand Up @@ -175,6 +176,10 @@ const buildPath = `${path.resolve(dirname, '../../scss/_design-system')}/`;
const THEME_NAME = 'fluent';
const THEME_FOLDER = 'fluent-next';

// Kept in step with the @includes in widgets/fluent-next/_design-system.scss.
const MODE_ROLES_MIXIN = 'roles';
const MODE_ALIASES_MIXIN = 'aliases';

const themePath = path.resolve(dirname, `../../scss/widgets/${THEME_FOLDER}`);

const FLUENT_PALETTES = [
Expand Down Expand Up @@ -231,6 +236,46 @@ const getModeFiles = (mode) => [
// properties. Absent from the bridge, `ds.$button-color-bg-rest` is now a Sass error.
const getBridgeFiles = () => getModeFiles('light');

/*
* Every bundle needs the mode-dependent declarations more than once: under the mode it was built
* for, under the opposite one, and under the relative "inverted" scope. A `:root` block cannot be
* re-scoped on load — `meta.load-css` emits it verbatim and `@use` paths take no interpolation — so
* these layers ship as mixins the theme places under the selectors it wants.
*
* Two files use it. The roles carry the mode's own values, one file per mode. The aliases carry the
* layers whose TEXT is mode-independent but whose values read a role (`box-shadow.md` is geometry
* over `color.shadow-key`): a custom property resolves where it is declared, so leaving them on
* `:root` would freeze them at the bundle's mode no matter what class sits below. Same text in
* every scope, resolved anew in each.
*
* Otherwise identical to Style Dictionary's own `css/variables` (lib/common/formats.js) minus the
* selector nesting; keep the two in step.
*/
// `prefix` belongs to the declaration lines, not to the header comment — upstream drops it before
// building the header (getFormattingCloneWithoutPrefix), and so must we.
const headerFormatting = ({ prefix, ...formatting } = {}) => formatting;

StyleDictionary.registerFormat({
name: 'dx/mode-scoped-mixin',
format: async ({ dictionary, file, options }) => {
const {
outputReferences, outputReferenceFallbacks, usesDtcg, formatting, sort, mixin,
} = options;
const header = await fileHeader({ file, formatting: headerFormatting(formatting), options });
const variables = formattedVariables({
format: 'css',
dictionary,
outputReferences,
outputReferenceFallbacks,
formatting: { ...formatting, indentation: ' ' },
usesDtcg,
sort,
});

return `${header}@mixin ${mixin}() {\n${variables}\n}\n`;
},
});

StyleDictionary.registerFormat({
name: 'scssToCss',
format: ({ dictionary }) => dictionary.allTokens
Expand Down Expand Up @@ -315,8 +360,6 @@ const createModeConfig = (mode) => createConfig(mode, getModeFiles(mode), [
const filePath = normalizeFilePath(token);

return filePath.includes(`base/colors/utility/${THEME_NAME}.json`)
|| filePath.includes(`global/${THEME_NAME}.json`)
|| filePath.includes(`figma-utils/box-shadow/semantic/${THEME_NAME}.json`)
|| filePath.includes(`figma-utils/icon/set/${THEME_NAME}.json`);
},
options: FILE_OPTIONS,
Expand All @@ -330,22 +373,35 @@ const createModeConfig = (mode) => createConfig(mode, getModeFiles(mode), [
filter: (token) => normalizeFilePath(token).includes(`semantic/typography/${THEME_NAME}`),
options: FILE_OPTIONS,
},
{
destination: `${THEME_NAME}/semantic/box-shadow.scss`,
format: 'css/variables',
filter: (token) => normalizeFilePath(token).includes(`semantic/box-shadow/${THEME_NAME}.json`),
options: FILE_OPTIONS,
},
{
destination: `${THEME_NAME}/semantic/colors/${mode}.scss`,
format: 'css/variables',
format: 'dx/mode-scoped-mixin',
filter: (token) => {
const filePath = normalizeFilePath(token);

return filePath.includes(`semantic/colors/${THEME_NAME}/${mode}.json`)
|| filePath.includes(`icons/${THEME_NAME}/${mode}.json`);
},
options: FILE_OPTIONS,
options: { ...FILE_OPTIONS, mixin: MODE_ROLES_MIXIN },
},
/*
* The three layers that read a colour role without being one: the box-shadow composites and
* their Figma layer parts (geometry over `color.shadow-*`) and the global aliases (focus rings
* over `color.border-focus*`). Written once, included in every mode scope — see the
* dx/mode-scoped-mixin comment for why they cannot stay on `:root`. Both mode configs emit this
* file; the sources are mode-independent, so the two writes are byte-identical.
*/
{
destination: `${THEME_NAME}/mode-aliases.scss`,
format: 'dx/mode-scoped-mixin',
filter: (token) => {
const filePath = normalizeFilePath(token);

return filePath.includes(`semantic/box-shadow/${THEME_NAME}.json`)
|| filePath.includes(`global/${THEME_NAME}.json`)
|| filePath.includes(`figma-utils/box-shadow/semantic/${THEME_NAME}.json`);
},
options: { ...FILE_OPTIONS, mixin: MODE_ALIASES_MIXIN },
},
]);

Expand Down
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ $theme-marker-mode: null !default;
* --dx-color-shadow carries alpha (the DS ships no solid-black token; the shadow roles are
* rgba over black) — unlike the legacy solid #000 of the other themes.
*/
:root {
:root,
.dx-theme-mode-light,
.dx-theme-mode-dark,
.dx-theme-mode-inverted {
--dx-component-color-bg: #{ds.$color-bg};
--dx-color-main-bg: #{ds.$color-bg-canvas};
--dx-color-primary: #{ds.$color-content-primary};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
@use "sass:meta";
@use "colors";
@use "../../_design-system/fluent/semantic/colors/light" as light-roles;
@use "../../_design-system/fluent/semantic/colors/dark" as dark-roles;
@use "../../_design-system/fluent/mode-aliases" as mode-aliases;

$accent: colors.$color;

Expand All @@ -11,10 +14,83 @@ $accent: colors.$color;
* to every stylesheet. Component size tokens are absent for the same reason plus one more:
* fluent-next maps sizes onto the base scales (spacing, font-size, border-radius, border-width),
* so no widget would read the layout names either.
*
* What is loaded here is what does NOT depend on the colour mode. The rest goes through
* `mode-values` below, because a custom property resolves where it is DECLARED: an alias onto a
* mode-dependent role, left on `:root`, freezes at the bundle's mode and ignores every mode class
* under it. That is why `mode-aliases` exists rather than a plain `:root` box-shadow layer.
*/
@include meta.load-css("../../_design-system/base");
@include meta.load-css("../../_design-system/fluent/base");
@include meta.load-css("../../_design-system/fluent/accents/#{$accent}");
@include meta.load-css("../../_design-system/fluent/semantic/typography");
@include meta.load-css("../../_design-system/fluent/semantic/box-shadow");
@include meta.load-css("../../_design-system/fluent/semantic/colors/#{colors.$mode}");

/*
* Everything a colour mode decides, in one place so the three scopes below cannot drift apart:
* the roles for that mode, the aliases that read them, and `--dx-theme-mode` naming the outcome.
*
* The marker is what the JS reads. `dx-theme-mode-inverted` means "the opposite of my
* surroundings", so no amount of class-reading tells you which mode an element ended up in - only
* the cascade knows. Overlays are reparented to the viewport and have to be given the mode their
* owner resolved to, so `core/utils/swatch_container.ts` asks the browser for this property
* instead of walking up the ancestor classes.
*/
@mixin mode-values($mode) {
--dx-theme-mode: #{$mode};

@if $mode == "light" {
@include light-roles.roles();
} @else {
@include dark-roles.roles();
}

@include mode-aliases.aliases();
}

/*
* Both modes ship in every bundle and a class picks between them: `dx-theme-mode-light` / `-dark`
* name a mode outright, `dx-theme-mode-inverted` asks for the opposite of its surroundings.
* Everything downstream reads these values through custom properties, so any element carrying one
* of the classes repaints itself and its subtree.
*
* Selector weight is one class throughout, `:root` included, so an override still wins by coming
* after the theme - the rule that held before the classes existed. The third block is what makes
* "inverted" relative: without it an island would keep inverting the bundle rather than the page
* whenever the page names its mode by class. `:where()` keeps that block at the same one-class
* weight as the rest.
*
* Two limits of that third block, both inherent to descendant selectors - CSS cannot ask for the
* NEAREST matching ancestor:
*
* - "inverted" flips the bundle's mode unless it sits anywhere inside a scope naming the
* opposite mode, at any distance. `dark > light > inverted` therefore resolves against the
* dark, not against the light next to it. Name the mode outright when that matters.
* - it is not recursive: an inverted island inside an inverted island stays inverted rather than
* flipping back.
*
* `--dx-theme-mode` keeps the JS honest about both: whatever these rules resolve to is what the
* overlay container is given.
*/
@mixin mode-scopes($own, $other) {
:root,
.dx-theme-mode-#{$own} {
@include mode-values($own);
}

.dx-theme-mode-#{$other},
.dx-theme-mode-inverted {
@include mode-values($other);
}

:where(.dx-theme-mode-#{$other}) .dx-theme-mode-inverted {
@include mode-values($own);
}
}

@if colors.$mode == "light" {
@include mode-scopes("light", "dark");
} @else if colors.$mode == "dark" {
@include mode-scopes("dark", "light");
} @else {
@error "fluent-next: unknown colour mode #{meta.inspect(colors.$mode)}; expected \"light\" or \"dark\".";
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,15 @@
@use "validation/public" as validationPublic;
@use "widget/public" as widgetPublic;

:root {
/*
* The system tier is declared on the document root and on every element that names a theme mode.
* A custom property resolves where it is declared, so a `:root`-only alias onto a role would freeze
* at the bundle's mode and ignore a mode class further down (see _design-system.scss).
*/
:root,
.dx-theme-mode-light,
.dx-theme-mode-dark,
.dx-theme-mode-inverted {
@include commonPublic.publish();
@include typographyPublic.publish();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@

.dx-icon {
font-size: $diagram-toolbar-icon-size;
color: $diagram-content;
color: var(--dx-global-content);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,19 @@ $grid-column-chooser-message-content: ds.$color-content-subtle !default;
$grid-column-separator-bg: ds.$color-bg-primary !default;
$grid-filter-row-bg: ds.$color-bg-canvas !default;
$grid-header-filter-icon-active: ds.$color-content-primary !default;
$grid-text-stub-bg: null !default;
$grid-text-stub-bg: data-uri("images/widgets/fluent-next/grid/text-stub.png") !default;
$grid-filter-panel-content: ds.$color-content-primary !default;
$grid-draggable-column-content: ds.$color-content-subtle !default;

@if $mode == "light" {
$grid-text-stub-bg: data-uri("images/widgets/fluent-next/color-schemes/light/grid/text-stub.png") !default;
}

@if $mode == "dark" {
$grid-text-stub-bg: data-uri("images/widgets/fluent-next/color-schemes/dark/grid/text-stub.png") !default;
}

:root {
/*
* Declared on the document root and on every element that names a theme mode. A custom property
* resolves where it is DECLARED, so a `:root`-only alias onto a mode-dependent role would freeze
* at the bundle's mode and ignore a mode class further down (see _design-system.scss).
*/
:root,
.dx-theme-mode-light,
.dx-theme-mode-dark,
.dx-theme-mode-inverted {
--dx-datagrid-row-alternation-bg: #{$grid-row-alternation-bg};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,2 @@
@use "../colors" as *;

// Theme-layer values used by this widget's style files (phase 0.5 layer normalization)
$icon-pulldown-bg: null !default;

@if $mode == "light" {
$icon-pulldown-bg: data-uri("images/widgets/fluent-next/color-schemes/light/pulldown.png") !default;
}

@if $mode == "dark" {
$icon-pulldown-bg: data-uri("images/widgets/fluent-next/color-schemes/dark/pulldown.png") !default;
}
$icon-pulldown-bg: data-uri("images/widgets/fluent-next/pulldown.png") !default;
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,15 @@ $text-editor-content-disabled: ds.$color-content-disabled !default;

$text-editor-label-content-focused: ds.$color-content-primary;

:root {
/*
* Declared on the document root and on every element that names a theme mode. A custom property
* resolves where it is DECLARED, so a `:root`-only alias onto a mode-dependent role would freeze
* at the bundle's mode and ignore a mode class further down (see _design-system.scss).
*/
:root,
.dx-theme-mode-light,
.dx-theme-mode-dark,
.dx-theme-mode-inverted {
--dx-texteditor-color-text: #{$text-editor-content};
--dx-texteditor-color-label: #{$text-editor-placeholder};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,9 @@
"--dx-toolbar-height"
],
"publicSurfaceUndeclared": [],
"publicSurfaceDifferences": [],
"publicSurfaceDifferences": [
"--dx-theme-mode: only in fluent-next"
],
"publicTierManualDeclarations": [
"fluent-next/_colors.scss: --dx-color-border",
"fluent-next/_colors.scss: --dx-color-danger",
Expand All @@ -527,6 +529,7 @@
"fluent-next/_colors.scss: --dx-color-text",
"fluent-next/_colors.scss: --dx-color-warning",
"fluent-next/_colors.scss: --dx-component-color-bg",
"fluent-next/_design-system.scss: --dx-theme-mode",
"fluent-next/_sizes.scss: --dx-border-radius",
"fluent-next/_sizes.scss: --dx-border-width",
"fluent-next/_sizes.scss: --dx-component-height",
Expand Down
Loading
Loading