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
7 changes: 7 additions & 0 deletions .github/renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,13 @@
"matchPackageNames": [
"*"
]
},
{
"matchPackageNames": [
"@devexpress/design-tokens-internal"
],
"automerge": false,
"minimumReleaseAge": null
}
],
"lockFileMaintenance": {
Expand Down
27 changes: 27 additions & 0 deletions packages/devextreme-scss/.stylelintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
"color-function-notation": "legacy",
"declaration-block-no-redundant-longhand-properties": null,
"declaration-no-important": true,
"declaration-property-value-disallowed-list": [
{ "/.*/": ["/var\\(\\s*--dxds-/"] },
{ "message": "Read a design token through the ds bridge (ds.$name), not var(--dxds-…): the bridge fails the build on an unknown name, a raw custom property compiles and degrades silently. The public --dx-* properties are unaffected." }
],
"font-family-name-quotes": "always-unless-keyword",
"@stylistic/indentation": [2, { "ignore": ["inside-parens"] }],
"keyframes-name-pattern": "dx-[a-z0-9-]+",
Expand Down Expand Up @@ -69,6 +73,29 @@
"rules": {
"scss/dollar-variable-pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$"
}
},
{
"comment": "A file that emits rules consumes the widget's own variables, so the token a value comes from is stated once, next to the other variables of that widget. Declaration files are exempted by the override below. `@use … with ()` arguments are at-rule parameters and stay invisible to stylelint; fluent-next-naming.test.ts covers that form.",
"files": ["scss/widgets/fluent-next/**/*.scss"],
"rules": {
"declaration-property-value-disallowed-list": [
{ "/.*/": ["/var\\(\\s*--dxds-/", "/\\bds\\.\\$/"] },
{ "message": "Resolve the design token into a variable in _colors.scss or _sizes.scss, then use that variable here" }
]
}
},
{
"files": [
"scss/widgets/fluent-next/**/_colors.scss",
"scss/widgets/fluent-next/**/_sizes.scss",
"scss/widgets/fluent-next/**/_variables.scss"
],
"rules": {
"declaration-property-value-disallowed-list": [
{ "/.*/": ["/var\\(\\s*--dxds-/"] },
{ "message": "Read a design token through the ds bridge (ds.$name), not var(--dxds-…): the bridge fails the build on an unknown name, a raw custom property compiles and degrades silently" }
]
}
}
]
}
99 changes: 79 additions & 20 deletions packages/devextreme-scss/build/tokens/build-tokens.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import { createRequire } from 'node:module';
import { readdir, readFile, rm } from 'node:fs/promises';
import StyleDictionary from 'style-dictionary';
import { registerTransforms } from './transforms.mjs';
import {
buildAvailableNames,
collectCustomPropertyReferences,
collectTokenReferences,
} from './consumed-tokens.ts';

// Suppress ONE known noisy sd-transforms warning about unresolvable
// {font-weight…} references inside math expressions. Scoped to console.warn
Expand Down Expand Up @@ -123,6 +128,9 @@ const tokensDir = path.dirname(require.resolve('@devexpress/design-tokens-intern
const buildPath = `${path.resolve(dirname, '../../scss/_design-system')}/`;

const THEME_NAME = 'fluent';
const THEME_FOLDER = 'fluent-next';

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

const FLUENT_PALETTES = [
'blue',
Expand Down Expand Up @@ -173,10 +181,13 @@ const getModeFiles = (mode) => [
`semantic/colors/${THEME_NAME}/${mode}`,
];

const getComponentThemeFiles = () => [
...getModeFiles('light'),
`components/core/theme/${THEME_NAME}`,
];
/*
* Source files behind the SCSS bridge. The component tier is deliberately absent: its 601 tokens
* are aliases onto the semantic roles, the theme reads the roles directly, and emitting the tier
* put 601 unreferenced custom properties into every theme stylesheet. Leaving it out of the bridge
* also turns `ds.$button-color-bg-rest` into a Sass error rather than a dangling var().
*/
const getBridgeFiles = () => getModeFiles('light');

StyleDictionary.registerFormat({
name: 'scssToCss',
Expand Down Expand Up @@ -291,21 +302,10 @@ const createModeConfig = (mode) => createConfig(mode, getModeFiles(mode), [
},
]);

const createComponentThemeConfig = () => createConfig('components-theme', getComponentThemeFiles(), [
{
destination: `${THEME_NAME}/components/theme.scss`,
format: 'css/variables',
filter: (token) => normalizeFilePath(token).includes(`components/core/theme/${THEME_NAME}.json`),
options: FILE_OPTIONS,
},
]);

// All token names for the SCSS bridge file: the common + light-mode + component
// *theme* (color) set. Component *size* tokens are intentionally excluded — fluent-next
// maps sizes onto the base scales (spacing/font-size/border-radius/…), so no widget
// references the component `*-layout-*` tokens and they are not emitted (see
// widgets/fluent-next/_design-system.scss).
const createDsConfig = () => createConfig('ds', getComponentThemeFiles(), [
// Component *size* tokens are excluded for the same reason as the component theme: fluent-next
// maps sizes onto the base scales (spacing/font-size/border-radius/…), so no widget would read the
// `*-layout-*` names (see widgets/fluent-next/_design-system.scss).
const createDsConfig = () => createConfig('ds', getBridgeFiles(), [
{
destination: 'variables/_ds.scss',
format: 'scssToCss',
Expand All @@ -315,7 +315,6 @@ const createDsConfig = () => createConfig('ds', getComponentThemeFiles(), [
const configs = [
...FLUENT_PALETTES.map(createPaletteConfig),
...FLUENT_MODES.map(createModeConfig),
createComponentThemeConfig(),
createDsConfig(),
];

Expand Down Expand Up @@ -359,6 +358,64 @@ async function validateReferences() {
return files.length;
}

async function collectThemeStyleSheets() {
const entries = await readdir(themePath, { withFileTypes: true, recursive: true });

return entries
.filter((entry) => entry.isFile() && entry.name.endsWith('.scss'))
.map((entry) => path.join(entry.parentPath, entry.name));
}

/*
* Every `ds.$…` a widget reads must still exist in the token package. validateReferences() above
* only checks the generated output against itself, so a release that deletes a token surfaces much
* later, as a Sass "Undefined variable" on the first bundle that touches it — one name per rebuild,
* with nothing pointing at the bump as the cause.
*
* The check reads the package's flat index instead of the generated bridge: the two carry the same
* names, but the index also carries the version for the message and needs no generated output.
* Reusing getBridgeFiles() is what keeps the scope from drifting away from the generator.
*/
async function validateConsumedTokens() {
const { version, tokens } = JSON.parse(
await readFile(path.join(tokensDir, 'tokens.flat.json'), 'utf-8'),
);
const availableNames = buildAvailableNames(
Object.keys(tokens),
new Set(getBridgeFiles()),
);

const referenced = new Map();

for (const file of await collectThemeStyleSheets()) {
const content = await readFile(file, 'utf-8');
const found = [
...collectTokenReferences(content).map((name) => [name, `ds.$${name}`]),
...collectCustomPropertyReferences(content).map((name) => [name, `var(--dxds-${name})`]),
];

for (const [name, reference] of found) {
if (!referenced.has(name)) {
referenced.set(name, { file, reference });
}
}
}

const missing = [...referenced].filter(([name]) => !availableNames.has(name));

if (missing.length > 0) {
const details = missing
.map(([, { file, reference }]) => ` ${reference} (first used in ${path.relative(themePath, file)})`)
.join('\n');

throw new Error(
`Tokens used by ${THEME_FOLDER} but absent from @devexpress/design-tokens-internal ${version}:\n${details}`,
);
}

return referenced.size;
}

async function build() {
await rm(buildPath, { recursive: true, force: true });

Expand All @@ -372,8 +429,10 @@ async function build() {
}

const fileCount = await validateReferences();
const consumedCount = await validateConsumedTokens();

console.log(`Design tokens generated: ${fileCount} files in ${buildPath}`);
console.log(`Design tokens consumed by ${THEME_FOLDER}: ${consumedCount} verified against the package`);
}

await build();
52 changes: 52 additions & 0 deletions packages/devextreme-scss/build/tokens/consumed-tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Pure half of the consumed-token check driven by build-tokens.mjs: everything here is a plain
* transformation, so tests/consumed-tokens.test.ts can exercise it without running a build.
*/

/*
* Commented-out declarations still spell out token names (stepper/_colors.scss parks a few), so
* comments are stripped before scanning — a dead reference must not fail the build.
*
* Line comments go first, so a `/* … *\/` nested in one disappears with it. The cost is that a `//`
* inside a string or a url() swallows the rest of its line: a reference sharing that line would go
* uncounted. That under-reports rather than failing wrongly, no theme stylesheet does it today, and
* fluent-next-naming.test.ts strips comments the same way.
*/
export const stripScssComments = (content: string): string => content
.replace(/\/\/[^\n\r]*/g, '')
.split(/\/\*|\*\//)
.filter((_, index) => index % 2 === 0)
.join('');

/*
* The charset is wider than the kebab-case the generator emits, so a malformed name is captured
* whole and fails the check. Matching only [a-z0-9-] would truncate `ds.$spacing-40_typo` to the
* valid `spacing-40` and report the stylesheet as verified.
*/
export const collectTokenReferences = (content: string): string[] => [
...stripScssComments(content).matchAll(/\bds\.\$([\w-]+)/g),
].map(([, name]) => name);

/*
* Nothing forces a stylesheet through the bridge — `var(--dxds-…)` written by hand compiles to
* whatever the browser resolves, so a dropped token would degrade silently. No theme stylesheet
* does it today; collecting the form keeps it that way. devextreme-vnext documents the same escape
* hatch as an open gap (VNEXT_DESIGN_TOKENS.md, "Known gaps").
*/
export const collectCustomPropertyReferences = (content: string): string[] => [
...stripScssComments(content).matchAll(/var\(\s*--dxds-([\w-]+)/g),
].map(([, name]) => name);

/*
* tokens.flat.json spans every design system, and 128 of the names fluent-next uses also exist
* under material — so the lookup is narrowed to the source files the bridge is generated from.
*/
export const buildAvailableNames = (
flatTokenKeys: Iterable<string>,
consumedSourceFiles: ReadonlySet<string>,
): Set<string> => new Set(
[...flatTokenKeys]
.map((key) => key.split(':'))
.filter(([sourceFile]) => consumedSourceFiles.has(sourceFile))
.map(([, tokenPath]) => tokenPath.replace(/\//g, '-')),
);
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@
$accent: colors.$color;

/*
* Tier order mirrors the design-tokens package: base scales and palettes, then semantic roles,
* then components. The component size tokens (fluent components sizes) are intentionally NOT
* emitted: fluent-next maps sizes onto the base scales (spacing, font-size, border-radius,
* border-width), so the component layout custom properties would never be referenced by any
* widget. Only the component color theme is consumed.
* Tier order mirrors the design-tokens package: base scales and palettes, then semantic roles.
*
* The component tier is not emitted at all. Its tokens are aliases onto the semantic roles, the
* theme reads those roles directly, and emitting the tier only added unreferenced custom properties
* 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.
*/
@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}");
@include meta.load-css("../../_design-system/fluent/components/theme");
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ $palette-border: ds.$color-border-neutral-default-rest !default;

// Non-color theme-level values (opacity/font family) — kept referencing the theme layer.
$global-font-family: ds.$font-family-sans-serif !default;
$invalid-badge-bg-rest: ds.$color-content-danger-compound-rest !default;
$invalid-badge-content-rest: ds.$color-content-neutral-default-static-dark-rest !default;
$valid-badge-content-rest: ds.$color-surface-success-default-rest !default;
$palette-text: ds.$color-content-neutral-default-rest !default;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
@use "sizes" as *;
@use "../sizes" as *;
@use "../../base/mixins" as *;
@use "../../../_design-system/variables/ds" as ds;
@use "../validation/sizes" as validationSizes;
@use "../../base/validation" as baseValidation with (
$validation-summary-margin-top: ds.$spacing-200,
$validation-message-content-padding: ds.$spacing-100,
$validation-summary-margin-top: validationSizes.$validation-summary-margin-block-start,
$validation-message-content-padding: validationSizes.$validation-message-padding,
);
@use "../list/sizes" as listSizes;

$invalid-badge-bg-rest: ds.$color-content-danger-compound-rest !default;

@mixin dx-base-typography() {
@include dx-base-typography-mixin(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
@use "../colors" as *;
@use "../../../_design-system/variables/ds" as ds;
@use "sass:color";
@use "colors" as *;
@use "sizes" as *;
Expand Down Expand Up @@ -32,7 +31,7 @@
$datagrid-focused-border-color: gridBaseColors.$grid-border-focused,
$header-filter-color: gridBaseColors.$grid-header-filter-icon-rest,
$header-filter-color-empty: gridBaseColors.$grid-header-filter-empty-icon-rest,
$base-focus-color: ds.$color-content-neutral-default-inverted-rest,
$base-focus-color: gridBaseColors.$grid-content-focused,
$datagrid-text-stub-background-image-path: gridBaseColors.$grid-text-stub-bg-rest,
$datagrid-group-row-border: $data-grid-group-row-border,
$datagrid-sticky-column-border: $data-grid-sticky-column-border,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,5 @@ $gantt-ti-bg-rest: ds.$color-surface-primary-alpha-hovered !default;
* variables (NAMING.md, O7).
*/
$gantt-successor-background-color: ds.$color-surface-neutral-default-static-light-rest;

$gantt-selection-bg-rest: ds.$color-surface-primary-deep-rest !default;
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
@use "../../base/gantt/mixins" as *;
@use "../gridBase/colors" as gridBaseColors;
@use "../form/sizes" as formSizes;
@use "../../../_design-system/variables/ds" as ds;

// adduse
@use "../splitterBar";
Expand Down Expand Up @@ -254,7 +253,7 @@
}

.dx-gantt-sel {
background-color: ds.$color-surface-primary-deep-rest;
background-color: $gantt-selection-bg-rest;
}

.dx-gantt-conn-v {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,5 @@ $grid-ai-chat-message-border-rest: ds.$color-border-neutral-default-rest !defaul
$grid-ai-chat-message-error-content-rest: ds.$color-content-danger-default-rest !default;
$grid-icon-rest: ds.$color-content-neutral-subdued-rest !default;
$grid-ai-chat-message-success-content-rest: ds.$color-content-success-default-rest !default;

$grid-content-focused: ds.$color-content-neutral-default-inverted-rest !default;
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
@use "sass:color";
@use "../colors" as *;
@use "../sizes" as *;
@use "../../../_design-system/variables/ds" as ds;
@use "sizes" as *;
@use "../../base/map" with (
$map-marker-tooltip-margin: ds.$spacing-100,
$map-marker-tooltip-margin: $map-marker-tooltip-margin,
);

// adduse
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@use "../../../_design-system/variables/ds" as ds;

// adduse

$map-marker-tooltip-margin: ds.$spacing-100 !default;
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
@use "../colors" as *;
@use "../../../_design-system/variables/ds" as ds;
@use "sass:math";
@use "sass:color";
@use "colors" as *;
Expand Down Expand Up @@ -31,7 +30,7 @@
$datagrid-row-error-color: gridBaseColors.$grid-row-error-content-rest,
$header-filter-color: gridBaseColors.$grid-header-filter-icon-rest,
$header-filter-color-empty: gridBaseColors.$grid-header-filter-empty-icon-rest,
$base-focus-color: ds.$color-content-neutral-default-inverted-rest,
$base-focus-color: gridBaseColors.$grid-content-focused,
);
@use 'layout/cell';
@include grid-base(treelist);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ $validation-message-font-size: ds.$font-size-caption-md !default;
$validation-message-padding-inline: ds.$spacing-60 !default;
$validation-message-line-height: ds.$line-height-120 !default; // dx-no-semantic-role: 120 is off the line-height-role scale
}

$validation-summary-margin-block-start: ds.$spacing-200 !default;
$validation-message-padding: ds.$spacing-100 !default;
Loading
Loading