@db-ux/core-postcss-plugin 0.0.0 → 4.5.4-postcss-a42fe67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1 @@
1
+ # @db-ux/core-postcss-plugin
package/README.md ADDED
@@ -0,0 +1,299 @@
1
+ # @db-ux/core-postcss-plugin
2
+
3
+ PostCSS plugins for the DB UX Design System.
4
+
5
+ ## Plugins
6
+
7
+ | Plugin | Description |
8
+ | ------------- | ------------------------------------------------------------------------------------------------------------ |
9
+ | `dbUxFlatten` | Flatten CSS custom properties by resolving `var()`, `@property`, `calc()`, `color-mix()`, and `light-dark()` |
10
+
11
+ ## Install
12
+
13
+ ```shell
14
+ npm install @db-ux/core-postcss-plugin --save-dev
15
+ ```
16
+
17
+ ## Imports
18
+
19
+ ```ts
20
+ // Named import (recommended)
21
+ import { dbUxFlatten } from "@db-ux/core-postcss-plugin";
22
+
23
+ // Default import (backward compatible — exports dbUxFlatten)
24
+ import dbUxFlatten from "@db-ux/core-postcss-plugin";
25
+ ```
26
+
27
+ ## CSS Setup
28
+
29
+ Use `@layer` to ensure the theme overrides base component styles. The plugin detects `@import ... layer()` rules to assign the correct layer to each file:
30
+
31
+ ```css
32
+ /* styles.css */
33
+ @layer db-ux, db-theme;
34
+ @import "@db-ux/db-theme/build/styles/rollup.css" layer(db-theme);
35
+ @import "@db-ux/core-components/build/styles/rollup.css" layer(db-ux);
36
+ ```
37
+
38
+ ## Framework Integration
39
+
40
+ ### Vite (React, Vue, Svelte, etc.)
41
+
42
+ Configure the plugin directly in `vite.config.ts`:
43
+
44
+ ```ts
45
+ // vite.config.ts
46
+ import { defineConfig } from "vite";
47
+ import { dbUxFlatten } from "@db-ux/core-postcss-plugin";
48
+
49
+ export default defineConfig({
50
+ css: {
51
+ transformer: "postcss", // required for Vite 8+ (default: 'lightningcss')
52
+ postcss: {
53
+ plugins: [dbUxFlatten()]
54
+ }
55
+ }
56
+ });
57
+ ```
58
+
59
+ > **Note**: Vite 8+ uses `lightningcss` as the default CSS transformer, which does not run PostCSS plugins. Set `css.transformer: 'postcss'` to enable PostCSS processing. Vite 7 and earlier use PostCSS by default and do not need this option.
60
+
61
+ Works in both dev and build mode.
62
+
63
+ ### Angular
64
+
65
+ Create a `postcss.config.json` in your project root:
66
+
67
+ ```json
68
+ {
69
+ "plugins": {
70
+ "@db-ux/core-postcss-plugin": {}
71
+ }
72
+ }
73
+ ```
74
+
75
+ Angular CLI (`@angular/build:application`) only supports JSON-based PostCSS configs and loads plugins by name via `require()`. Works in both `ng build` and `ng serve`.
76
+
77
+ ### Next.js
78
+
79
+ Create a `postcss.config.mjs` in your project root:
80
+
81
+ ```js
82
+ // postcss.config.mjs
83
+ import { dbUxFlatten } from "@db-ux/core-postcss-plugin";
84
+
85
+ export default {
86
+ plugins: [dbUxFlatten()]
87
+ };
88
+ ```
89
+
90
+ Works with both webpack and turbopack.
91
+
92
+ > **Note**: For CommonJS (`postcss.config.js`):
93
+ >
94
+ > ```js
95
+ > const { dbUxFlatten } = require("@db-ux/core-postcss-plugin");
96
+ > module.exports = { plugins: [dbUxFlatten()] };
97
+ > ```
98
+
99
+ ### Webpack (standalone)
100
+
101
+ ```js
102
+ // webpack.config.js
103
+ const { dbUxFlatten } = require("@db-ux/core-postcss-plugin");
104
+
105
+ module.exports = {
106
+ module: {
107
+ rules: [
108
+ {
109
+ test: /\.css$/,
110
+ use: [
111
+ "style-loader",
112
+ "css-loader",
113
+ {
114
+ loader: "postcss-loader",
115
+ options: {
116
+ postcssOptions: {
117
+ plugins: [dbUxFlatten()]
118
+ }
119
+ }
120
+ }
121
+ ]
122
+ }
123
+ ]
124
+ }
125
+ };
126
+ ```
127
+
128
+ ---
129
+
130
+ ## `dbUxFlatten`
131
+
132
+ Flattens DB UX Design System CSS custom properties by resolving `var()`, `@property`, `calc()`, `color-mix()`, and `light-dark()`.
133
+
134
+ ### What it does
135
+
136
+ 1. **Collects** all `@property` declarations and their `initial-value` as a resolution cache
137
+ 2. **Detects `@layer` priority** from `@layer` order declarations and `@import ... layer()` rules — theme values override base values regardless of processing order
138
+ 3. **Detects dynamic variables** that must stay as `var()` references:
139
+ - Variables re-declared in non-`:root`/`:host` selectors (e.g. `[data-density=functional]`)
140
+ - Variables re-declared inside `@media` queries
141
+ - Variables matching `dynamicPrefixes` (default: `--db-adaptive-*`)
142
+ 4. **Resolves** all static `var()` references recursively — including nested `var()` with fallbacks like `var(--a, var(--b))`
143
+ 5. **Evaluates** `calc()` expressions when all values are static
144
+ 6. **Evaluates** `color-mix(in srgb, ...)` when all colors are resolved
145
+ 7. **Collapses** `light-dark(x, y)` to `x` when both arguments are identical after resolution
146
+ 8. **Removes** `@property` rules and unused intermediate declarations
147
+
148
+ ### Options
149
+
150
+ | Option | Type | Default | Description |
151
+ | ------------------ | ---------- | -------------------- | ----------------------------------------------------------------------- |
152
+ | `removeAtProperty` | `boolean` | `true` | Remove `@property` rules after resolving |
153
+ | `removeResolved` | `boolean` | `true` | Remove declarations from `@property` that are no longer referenced |
154
+ | `dynamicPrefixes` | `string[]` | `['--db-adaptive-']` | Variable prefixes that are always treated as dynamic and never resolved |
155
+
156
+ ```ts
157
+ dbUxFlatten({
158
+ removeAtProperty: true,
159
+ removeResolved: true,
160
+ dynamicPrefixes: ["--db-adaptive-", "--my-custom-dynamic-"]
161
+ });
162
+ ```
163
+
164
+ ### How it works
165
+
166
+ #### Static vs. dynamic variables
167
+
168
+ - **Static**: Only declared in `:root`/`:host` and `@property` — safe to inline everywhere
169
+ - **Dynamic**: Re-declared in class selectors, data attributes, `@media` queries, or matching `dynamicPrefixes` — must stay as `var()` references
170
+
171
+ ```css
172
+ /* Static — will be resolved */
173
+ @property --db-neutral-0 {
174
+ syntax: "<color>";
175
+ initial-value: #0d0e10;
176
+ inherits: true;
177
+ }
178
+
179
+ /* Dynamic — stays as var() */
180
+ :root {
181
+ --db-spacing-fixed-sm: 0.75rem;
182
+ }
183
+ [data-density="functional"] {
184
+ --db-spacing-fixed-sm: 0.5rem; /* re-declared → dynamic */
185
+ }
186
+ ```
187
+
188
+ #### `@layer` and `@import layer()` support
189
+
190
+ The plugin detects layer priority from two sources:
191
+
192
+ 1. **`@layer` order declarations**: `@layer db-ux, db-theme;` — later in the list = higher priority
193
+ 2. **`@import ... layer()` rules**: `@import "file.css" layer(db-theme);` — maps each imported file to its layer
194
+
195
+ This works even when the bundler processes each imported file independently (e.g. Angular), because the plugin sees the `@import` rules in the entry CSS file first and builds a file-to-layer mapping before the imported files are processed.
196
+
197
+ ```css
198
+ @layer db-ux, db-theme;
199
+ @import "@db-ux/db-theme/build/styles/rollup.css" layer(db-theme);
200
+ @import "@db-ux/core-components/build/styles/rollup.css" layer(db-ux);
201
+ ```
202
+
203
+ Unlayered CSS always wins over all layers, matching the CSS spec.
204
+
205
+ #### Nested `var()` with fallbacks
206
+
207
+ ```css
208
+ /* Input */
209
+ font-family: var(--db-icon-font-family, var(--db-icon-default-font-family));
210
+
211
+ /* Output (--db-icon-font-family unknown, --db-icon-default-font-family resolved) */
212
+ font-family: var(--db-icon-font-family, "db-default", icon-font-fallback);
213
+ ```
214
+
215
+ #### `light-dark()` collapsing
216
+
217
+ When both arguments resolve to the same value, the function is collapsed:
218
+
219
+ ```css
220
+ /* Input */
221
+ --db-color: light-dark(
222
+ var(--db-neutral-origin-light-default),
223
+ var(--db-neutral-origin-dark-default)
224
+ );
225
+
226
+ /* Output (both resolve to #232529) */
227
+ --db-color: #232529;
228
+
229
+ /* Output (different values — kept) */
230
+ --db-color: light-dark(#232529, #f0f0f0);
231
+ ```
232
+
233
+ ### Example
234
+
235
+ **Input:**
236
+
237
+ ```css
238
+ @layer db-ux, db-theme;
239
+
240
+ @layer db-theme {
241
+ @property --db-brand-origin-light-default {
242
+ syntax: "<color>";
243
+ initial-value: #ec0016;
244
+ inherits: true;
245
+ }
246
+ @property --db-brand-origin-dark-default {
247
+ syntax: "<color>";
248
+ initial-value: #ec0016;
249
+ inherits: true;
250
+ }
251
+ }
252
+
253
+ @layer db-ux {
254
+ :root {
255
+ --db-brand-origin-default: light-dark(
256
+ var(--db-brand-origin-light-default),
257
+ var(--db-brand-origin-dark-default)
258
+ );
259
+ }
260
+ .button {
261
+ color: var(--db-adaptive-on-bg-basic-emphasis-100-default);
262
+ border-radius: var(--db-border-radius-xs);
263
+ }
264
+ }
265
+ ```
266
+
267
+ **Output:**
268
+
269
+ ```css
270
+ :root {
271
+ --db-brand-origin-default: #ec0016;
272
+ }
273
+ .button {
274
+ color: var(--db-adaptive-on-bg-basic-emphasis-100-default);
275
+ border-radius: 0.25rem;
276
+ }
277
+ ```
278
+
279
+ - `--db-brand-origin-default`: both light/dark resolved to `#ec0016` (theme value) → `light-dark()` collapsed
280
+ - `--db-adaptive-*`: kept as `var()` (dynamic prefix)
281
+ - `--db-border-radius-xs`: resolved from `@property` (static)
282
+
283
+ ---
284
+
285
+ ## Package Structure
286
+
287
+ ```text
288
+ src/
289
+ ├── index.ts # barrel export
290
+ └── plugins/
291
+ └── flatten/
292
+ ├── index.ts # dbUxFlatten plugin
293
+ ├── data.ts # types & constants
294
+ └── helpers/
295
+ ├── css-parser.ts # generic CSS string parsing
296
+ ├── resolve.ts # var(), calc(), color-mix() resolution
297
+ ├── collect.ts # PostCSS AST collection (layers, vars, imports)
298
+ └── transform.ts # transformRoot + collapseLightDark
299
+ ```
@@ -0,0 +1,3 @@
1
+ export { dbUxFlatten } from './plugins/flatten/index.js';
2
+ export type { FlattenOptions } from './plugins/flatten/index.js';
3
+ export { dbUxFlatten as default } from './plugins/flatten/index.js';
package/build/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { dbUxFlatten } from './plugins/flatten/index.js';
2
+ // Default export for backward compatibility:
3
+ // import dbUxFlatten from '@db-ux/postcss-plugin';
4
+ export { dbUxFlatten as default } from './plugins/flatten/index.js';
@@ -0,0 +1,17 @@
1
+ export type FlattenOptions = {
2
+ /** Remove @property rules after resolving (default: true) */
3
+ removeAtProperty?: boolean;
4
+ /** Remove declarations whose variables came from @property and are no longer referenced (default: true) */
5
+ removeResolved?: boolean;
6
+ /**
7
+ * Variable prefixes that are always treated as dynamic (never resolved).
8
+ * Default: ['--db-adaptive-']
9
+ */
10
+ dynamicPrefixes?: string[];
11
+ };
12
+ export type VarEntry = {
13
+ value: string;
14
+ layer: string | null;
15
+ file: string;
16
+ };
17
+ export declare const DEFAULT_DYNAMIC_PREFIXES: string[];
@@ -0,0 +1 @@
1
+ export const DEFAULT_DYNAMIC_PREFIXES = ['--db-adaptive-'];
@@ -0,0 +1,71 @@
1
+ import type { ChildNode, Root } from 'postcss';
2
+ import type { VarEntry } from '../data.js';
3
+ /**
4
+ * Check whether a CSS selector targets only `:root` and/or `:host`.
5
+ * @param selector - The CSS selector string to check
6
+ * @returns True if the selector only contains `:root` and/or `:host`
7
+ */
8
+ export declare const isRootSelector: (selector: string) => boolean;
9
+ /**
10
+ * Walk up the PostCSS AST from a node to find the enclosing `@layer` name.
11
+ * @param node - The PostCSS child node to start from
12
+ * @returns The layer name, or null if not inside any `@layer`
13
+ */
14
+ export declare const getLayerName: (node: ChildNode) => string | null;
15
+ /**
16
+ * Parse `@layer` order declarations (e.g. `@layer db-ux, db-theme;`) from a root.
17
+ * Later names in the list get higher priority indices.
18
+ * @param root - The PostCSS root to scan
19
+ * @returns A map of layer name to priority index
20
+ */
21
+ export declare const collectLayerOrder: (root: Root) => Map<string, number>;
22
+ /**
23
+ * Parse `@import` rules to extract file-specifier to layer name mappings.
24
+ * @param root - The PostCSS root to scan
25
+ * @returns A map of import specifier to layer name
26
+ */
27
+ export declare const collectImportLayers: (root: Root) => Map<string, string>;
28
+ /**
29
+ * Get the numeric priority for a layer. Unlayered CSS (null) gets the highest
30
+ * priority (`MAX_SAFE_INTEGER`), matching the CSS spec.
31
+ * @param layer - The layer name, or null for unlayered CSS
32
+ * @param layerOrder - The layer priority map
33
+ * @returns The numeric priority (higher = wins)
34
+ */
35
+ export declare const getLayerPriority: (layer: string | null, layerOrder: Map<string, number>) => number;
36
+ /**
37
+ * Pick the best (highest-priority) value from multiple `VarEntry` entries
38
+ * for the same variable, based on `@layer` priority.
39
+ * @param entries - All collected entries for a single variable
40
+ * @param layerOrder - The layer priority map
41
+ * @returns The value string from the highest-priority entry
42
+ */
43
+ export declare const pickBestVar: (entries: VarEntry[], layerOrder: Map<string, number>) => string;
44
+ /**
45
+ * Look up the `@layer` name for a file by matching its path against
46
+ * known `@import ... layer()` specifiers.
47
+ * @param filePath - The absolute file path to look up
48
+ * @param importLayerMap - Map of import specifiers to layer names
49
+ * @returns The layer name, or null if not associated with any layer
50
+ */
51
+ export declare const getFileLayer: (filePath: string, importLayerMap: Map<string, string>) => string | null;
52
+ /**
53
+ * Collect all CSS custom property declarations and `@property` initial-values
54
+ * from a PostCSS root, assigning each a layer and detecting dynamic variables.
55
+ *
56
+ * A variable is marked as dynamic if:
57
+ * - Its name matches one of the `prefixes`
58
+ * - It is declared inside a non-`:root`/`:host` selector
59
+ * - It is declared inside `@media` within `:root`/`:host`
60
+ *
61
+ * Duplicate entries (same prop + file + layer) are skipped.
62
+ *
63
+ * @param root - The PostCSS root to scan
64
+ * @param varMap - Shared map to accumulate variable entries into
65
+ * @param propertyNames - Shared set to track `@property` variable names
66
+ * @param dynamicVars - Shared set to track dynamic variable names
67
+ * @param prefixes - Variable prefixes that are always treated as dynamic
68
+ * @param forceLayer - If set, overrides the detected layer for all entries
69
+ * @param file - The source file path for deduplication
70
+ */
71
+ export declare const collectVarsWithLayer: (root: Root, varMap: Map<string, VarEntry[]>, propertyNames: Set<string>, dynamicVars: Set<string>, prefixes: string[], forceLayer: string | null, file: string) => void;
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Check whether a CSS selector targets only `:root` and/or `:host`.
3
+ * @param selector - The CSS selector string to check
4
+ * @returns True if the selector only contains `:root` and/or `:host`
5
+ */
6
+ export const isRootSelector = (selector) => {
7
+ const trimmed = selector.trim();
8
+ return /^(:root|:host)(\s*,\s*(:root|:host))*$/.test(trimmed);
9
+ };
10
+ /**
11
+ * Walk up the PostCSS AST from a node to find the enclosing `@layer` name.
12
+ * @param node - The PostCSS child node to start from
13
+ * @returns The layer name, or null if not inside any `@layer`
14
+ */
15
+ export const getLayerName = (node) => {
16
+ let current = node.parent;
17
+ while (current) {
18
+ if (current.type === 'atrule' &&
19
+ current.name === 'layer' &&
20
+ current.params) {
21
+ return current.params.trim();
22
+ }
23
+ current = current.parent;
24
+ }
25
+ return null;
26
+ };
27
+ /**
28
+ * Parse `@layer` order declarations (e.g. `@layer db-ux, db-theme;`) from a root.
29
+ * Later names in the list get higher priority indices.
30
+ * @param root - The PostCSS root to scan
31
+ * @returns A map of layer name to priority index
32
+ */
33
+ export const collectLayerOrder = (root) => {
34
+ const order = new Map();
35
+ root.walkAtRules('layer', (atRule) => {
36
+ if (atRule.nodes && atRule.nodes.length > 0)
37
+ return;
38
+ const names = atRule.params
39
+ .split(',')
40
+ .map((n) => n.trim())
41
+ .filter(Boolean);
42
+ for (let i = 0; i < names.length; i++) {
43
+ order.set(names[i], i);
44
+ }
45
+ });
46
+ return order;
47
+ };
48
+ /**
49
+ * Parse `@import` rules to extract file-specifier to layer name mappings.
50
+ * @param root - The PostCSS root to scan
51
+ * @returns A map of import specifier to layer name
52
+ */
53
+ export const collectImportLayers = (root) => {
54
+ const importLayers = new Map();
55
+ root.walkAtRules('import', (atRule) => {
56
+ const params = atRule.params;
57
+ const layerMatch = params.match(/layer\(([^)]+)\)/);
58
+ if (!layerMatch)
59
+ return;
60
+ const layerName = layerMatch[1].trim();
61
+ const fileMatch = params.match(/(?:url\(\s*)?["']([^"']+)["'](?:\s*\))?/);
62
+ if (!fileMatch)
63
+ return;
64
+ importLayers.set(fileMatch[1], layerName);
65
+ });
66
+ return importLayers;
67
+ };
68
+ /**
69
+ * Get the numeric priority for a layer. Unlayered CSS (null) gets the highest
70
+ * priority (`MAX_SAFE_INTEGER`), matching the CSS spec.
71
+ * @param layer - The layer name, or null for unlayered CSS
72
+ * @param layerOrder - The layer priority map
73
+ * @returns The numeric priority (higher = wins)
74
+ */
75
+ export const getLayerPriority = (layer, layerOrder) => {
76
+ if (layer === null)
77
+ return Number.MAX_SAFE_INTEGER;
78
+ return layerOrder.get(layer) ?? -1;
79
+ };
80
+ /**
81
+ * Pick the best (highest-priority) value from multiple `VarEntry` entries
82
+ * for the same variable, based on `@layer` priority.
83
+ * @param entries - All collected entries for a single variable
84
+ * @param layerOrder - The layer priority map
85
+ * @returns The value string from the highest-priority entry
86
+ */
87
+ export const pickBestVar = (entries, layerOrder) => {
88
+ let best = entries[0];
89
+ for (let i = 1; i < entries.length; i++) {
90
+ if (getLayerPriority(entries[i].layer, layerOrder) >=
91
+ getLayerPriority(best.layer, layerOrder)) {
92
+ best = entries[i];
93
+ }
94
+ }
95
+ return best.value;
96
+ };
97
+ /**
98
+ * Look up the `@layer` name for a file by matching its path against
99
+ * known `@import ... layer()` specifiers.
100
+ * @param filePath - The absolute file path to look up
101
+ * @param importLayerMap - Map of import specifiers to layer names
102
+ * @returns The layer name, or null if not associated with any layer
103
+ */
104
+ export const getFileLayer = (filePath, importLayerMap) => {
105
+ for (const [specifier, layer] of importLayerMap) {
106
+ if (filePath.replace(/\\/g, '/').endsWith(specifier.replace(/\\/g, '/'))) {
107
+ return layer;
108
+ }
109
+ }
110
+ return null;
111
+ };
112
+ /**
113
+ * Collect all CSS custom property declarations and `@property` initial-values
114
+ * from a PostCSS root, assigning each a layer and detecting dynamic variables.
115
+ *
116
+ * A variable is marked as dynamic if:
117
+ * - Its name matches one of the `prefixes`
118
+ * - It is declared inside a non-`:root`/`:host` selector
119
+ * - It is declared inside `@media` within `:root`/`:host`
120
+ *
121
+ * Duplicate entries (same prop + file + layer) are skipped.
122
+ *
123
+ * @param root - The PostCSS root to scan
124
+ * @param varMap - Shared map to accumulate variable entries into
125
+ * @param propertyNames - Shared set to track `@property` variable names
126
+ * @param dynamicVars - Shared set to track dynamic variable names
127
+ * @param prefixes - Variable prefixes that are always treated as dynamic
128
+ * @param forceLayer - If set, overrides the detected layer for all entries
129
+ * @param file - The source file path for deduplication
130
+ */
131
+ export const collectVarsWithLayer = (root, varMap, propertyNames, dynamicVars, prefixes, forceLayer, file) => {
132
+ const addVar = (prop, value, layer) => {
133
+ const entries = varMap.get(prop);
134
+ if (entries) {
135
+ if (entries.some((e) => e.file === file && e.layer === layer))
136
+ return;
137
+ entries.push({ value, layer, file });
138
+ }
139
+ else {
140
+ varMap.set(prop, [{ value, layer, file }]);
141
+ }
142
+ };
143
+ root.walkAtRules('property', (atRule) => {
144
+ const propName = atRule.params.trim();
145
+ propertyNames.add(propName);
146
+ const layer = forceLayer ?? getLayerName(atRule);
147
+ atRule.walkDecls('initial-value', (decl) => {
148
+ addVar(propName, decl.value, layer);
149
+ });
150
+ });
151
+ root.walkDecls(/^--/, (decl) => {
152
+ const parent = decl.parent;
153
+ if (prefixes.some((p) => decl.prop.startsWith(p))) {
154
+ dynamicVars.add(decl.prop);
155
+ }
156
+ if (parent && parent.type === 'rule') {
157
+ const rule = parent;
158
+ if (!isRootSelector(rule.selector)) {
159
+ dynamicVars.add(decl.prop);
160
+ }
161
+ }
162
+ if (parent && parent.type === 'rule') {
163
+ const rule = parent;
164
+ if (isRootSelector(rule.selector) &&
165
+ rule.parent &&
166
+ rule.parent.type === 'atrule') {
167
+ const atRule = rule.parent;
168
+ if (atRule.name === 'media') {
169
+ dynamicVars.add(decl.prop);
170
+ }
171
+ }
172
+ }
173
+ addVar(decl.prop, decl.value, forceLayer ?? getLayerName(decl));
174
+ });
175
+ };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Find the matching closing paren for a CSS function call starting at a given position.
3
+ * Assumes the opening `(` has already been consumed (depth starts at 1).
4
+ * @param value - The full CSS value string
5
+ * @param openIndex - The index right after the opening `(`
6
+ * @returns The index right after the matching `)`, or -1 if unbalanced
7
+ */
8
+ export declare const findMatchingParenthesis: (value: string, openIndex: number) => number;
9
+ /**
10
+ * Find the index of the first top-level comma in a string,
11
+ * skipping commas inside nested parentheses.
12
+ * @param value - The string to search
13
+ * @returns The index of the first top-level comma, or -1 if none found
14
+ */
15
+ export declare const findTopLevelComma: (value: string) => number;
16
+ /**
17
+ * Find the next occurrence of a CSS function by name and extract its span and inner content.
18
+ * Uses paren counting to handle nested parentheses.
19
+ * @param value - The CSS value string to search
20
+ * @param funcName - The function name (e.g. "var", "calc", "light-dark")
21
+ * @param fromIndex - The index to start searching from
22
+ * @returns An object with `start`, `end`, and `inner` content, or null if not found
23
+ */
24
+ export declare const findCssFunction: (value: string, funcName: string, fromIndex?: number) => {
25
+ start: number;
26
+ end: number;
27
+ inner: string;
28
+ } | null;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Find the matching closing paren for a CSS function call starting at a given position.
3
+ * Assumes the opening `(` has already been consumed (depth starts at 1).
4
+ * @param value - The full CSS value string
5
+ * @param openIndex - The index right after the opening `(`
6
+ * @returns The index right after the matching `)`, or -1 if unbalanced
7
+ */
8
+ export const findMatchingParenthesis = (value, openIndex) => {
9
+ let depth = 1;
10
+ let i = openIndex;
11
+ while (i < value.length && depth > 0) {
12
+ if (value[i] === '(')
13
+ depth++;
14
+ if (value[i] === ')')
15
+ depth--;
16
+ i++;
17
+ }
18
+ return depth === 0 ? i : -1;
19
+ };
20
+ /**
21
+ * Find the index of the first top-level comma in a string,
22
+ * skipping commas inside nested parentheses.
23
+ * @param value - The string to search
24
+ * @returns The index of the first top-level comma, or -1 if none found
25
+ */
26
+ export const findTopLevelComma = (value) => {
27
+ let depth = 0;
28
+ for (let i = 0; i < value.length; i++) {
29
+ if (value[i] === '(')
30
+ depth++;
31
+ else if (value[i] === ')')
32
+ depth--;
33
+ else if (value[i] === ',' && depth === 0)
34
+ return i;
35
+ }
36
+ return -1;
37
+ };
38
+ /**
39
+ * Find the next occurrence of a CSS function by name and extract its span and inner content.
40
+ * Uses paren counting to handle nested parentheses.
41
+ * @param value - The CSS value string to search
42
+ * @param funcName - The function name (e.g. "var", "calc", "light-dark")
43
+ * @param fromIndex - The index to start searching from
44
+ * @returns An object with `start`, `end`, and `inner` content, or null if not found
45
+ */
46
+ export const findCssFunction = (value, funcName, fromIndex = 0) => {
47
+ const prefix = `${funcName}(`;
48
+ const idx = value.indexOf(prefix, fromIndex);
49
+ if (idx === -1)
50
+ return null;
51
+ const end = findMatchingParenthesis(value, idx + prefix.length);
52
+ if (end === -1)
53
+ return null;
54
+ return {
55
+ start: idx,
56
+ end,
57
+ inner: value.slice(idx + prefix.length, end - 1)
58
+ };
59
+ };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Recursively resolve `var()` references in a CSS value string.
3
+ * Uses a `seen` set per resolution chain to prevent circular references.
4
+ * If a var is known, it replaces the entire `var(...)` with the resolved value.
5
+ * If unknown but has a fallback, it resolves vars inside the fallback.
6
+ * @param value - The CSS value string containing `var()` references
7
+ * @param varMap - Map of variable names to their resolved values
8
+ * @param seen - Set of variable names already visited in the current chain
9
+ * @returns The value with all resolvable `var()` references inlined
10
+ */
11
+ export declare const resolveVars: (value: string, varMap: Map<string, string>, seen?: Set<string>) => string;
12
+ /**
13
+ * Collect all CSS variable names referenced in a value, including
14
+ * names inside nested `var()` fallbacks.
15
+ * @param value - The CSS value string to scan
16
+ * @param refs - Set to add discovered variable names to
17
+ */
18
+ export declare const collectVarReferences: (value: string, refs: Set<string>) => void;
19
+ /**
20
+ * Resolve all fully-static `calc()` expressions in a CSS value.
21
+ * Skips expressions that still contain unresolved `var()` or nested CSS functions.
22
+ * @param value - The CSS value string potentially containing `calc()` expressions
23
+ * @returns The value with all evaluable `calc()` expressions replaced
24
+ */
25
+ export declare const resolveCalc: (value: string) => string;
26
+ /**
27
+ * Resolve all fully-static `color-mix()` expressions in a CSS value.
28
+ * Skips expressions that still contain unresolved `var()` references.
29
+ * @param value - The CSS value string potentially containing `color-mix()` expressions
30
+ * @returns The value with all evaluable `color-mix()` expressions replaced
31
+ */
32
+ export declare const resolveColorMix: (value: string) => string;
@@ -0,0 +1,333 @@
1
+ import { findCssFunction, findTopLevelComma } from './css-parser.js';
2
+ // ── var() ───────────────────────────────────────────────────────────────
3
+ /**
4
+ * Find the next `var(` occurrence and parse it into name and optional fallback.
5
+ * @param value - The CSS value string to search
6
+ * @param fromIndex - The index to start searching from
7
+ * @returns The parsed var reference, or null if not found
8
+ */
9
+ const findNextVar = (value, fromIndex) => {
10
+ const found = findCssFunction(value, 'var', fromIndex);
11
+ if (!found)
12
+ return null;
13
+ const commaIdx = findTopLevelComma(found.inner);
14
+ if (commaIdx === -1) {
15
+ return {
16
+ start: found.start,
17
+ end: found.end,
18
+ name: found.inner.trim(),
19
+ fallback: null
20
+ };
21
+ }
22
+ return {
23
+ start: found.start,
24
+ end: found.end,
25
+ name: found.inner.slice(0, commaIdx).trim(),
26
+ fallback: found.inner.slice(commaIdx + 1).trim()
27
+ };
28
+ };
29
+ /**
30
+ * Recursively resolve `var()` references in a CSS value string.
31
+ * Uses a `seen` set per resolution chain to prevent circular references.
32
+ * If a var is known, it replaces the entire `var(...)` with the resolved value.
33
+ * If unknown but has a fallback, it resolves vars inside the fallback.
34
+ * @param value - The CSS value string containing `var()` references
35
+ * @param varMap - Map of variable names to their resolved values
36
+ * @param seen - Set of variable names already visited in the current chain
37
+ * @returns The value with all resolvable `var()` references inlined
38
+ */
39
+ export const resolveVars = (value, varMap, seen = new Set()) => {
40
+ let result = value;
41
+ let searchFrom = 0;
42
+ while (searchFrom < result.length) {
43
+ const found = findNextVar(result, searchFrom);
44
+ if (!found)
45
+ break;
46
+ const { start, end, name, fallback } = found;
47
+ if (seen.has(name)) {
48
+ searchFrom = end;
49
+ continue;
50
+ }
51
+ const resolved = varMap.get(name);
52
+ if (resolved !== undefined) {
53
+ const childSeen = new Set(seen);
54
+ childSeen.add(name);
55
+ const resolvedValue = resolveVars(resolved, varMap, childSeen);
56
+ result = result.slice(0, start) + resolvedValue + result.slice(end);
57
+ searchFrom = start + resolvedValue.length;
58
+ }
59
+ else if (fallback !== null) {
60
+ const resolvedFallback = resolveVars(fallback, varMap, seen);
61
+ const replacement = `var(${name}, ${resolvedFallback})`;
62
+ result = result.slice(0, start) + replacement + result.slice(end);
63
+ searchFrom = start + replacement.length;
64
+ }
65
+ else {
66
+ searchFrom = end;
67
+ }
68
+ }
69
+ return result;
70
+ };
71
+ /**
72
+ * Collect all CSS variable names referenced in a value, including
73
+ * names inside nested `var()` fallbacks.
74
+ * @param value - The CSS value string to scan
75
+ * @param refs - Set to add discovered variable names to
76
+ */
77
+ export const collectVarReferences = (value, refs) => {
78
+ let searchFrom = 0;
79
+ while (searchFrom < value.length) {
80
+ const found = findNextVar(value, searchFrom);
81
+ if (!found)
82
+ break;
83
+ refs.add(found.name);
84
+ if (found.fallback) {
85
+ collectVarReferences(found.fallback, refs);
86
+ }
87
+ searchFrom = found.end;
88
+ }
89
+ };
90
+ // ── Generic CSS function resolver ───────────────────────────────────────
91
+ /**
92
+ * Find and evaluate all occurrences of a CSS function in a value string.
93
+ * Skips occurrences that still contain unresolved `var()` references.
94
+ * @param value - The CSS value string to process
95
+ * @param funcName - The CSS function name to match (e.g. "calc", "color-mix")
96
+ * @param evaluate - Callback that receives the inner content and returns the
97
+ * evaluated result, or null to skip
98
+ * @param skipNestedFunctions - If true, skip when inner content contains other CSS functions
99
+ * @returns The value with all evaluable occurrences replaced
100
+ */
101
+ const resolveCssFunction = (value, funcName, evaluate, skipNestedFunctions = false) => {
102
+ let result = value;
103
+ let searchFrom = 0;
104
+ while (searchFrom < result.length) {
105
+ const found = findCssFunction(result, funcName, searchFrom);
106
+ if (!found)
107
+ break;
108
+ if (found.inner.includes('var(')) {
109
+ searchFrom = found.end;
110
+ continue;
111
+ }
112
+ if (skipNestedFunctions && /[a-z-]+\(/i.test(found.inner)) {
113
+ searchFrom = found.end;
114
+ continue;
115
+ }
116
+ const evaluated = evaluate(found.inner);
117
+ if (evaluated !== null) {
118
+ result =
119
+ result.slice(0, found.start) +
120
+ evaluated +
121
+ result.slice(found.end);
122
+ searchFrom = found.start + evaluated.length;
123
+ }
124
+ else {
125
+ searchFrom = found.end;
126
+ }
127
+ }
128
+ return result;
129
+ };
130
+ // ── calc() ──────────────────────────────────────────────────────────────
131
+ /**
132
+ * Parse a CSS unit value like "0.75rem" into its numeric value and unit.
133
+ * @param str - The string to parse (e.g. "0.75rem", "100%", "2")
134
+ * @returns An object with `value` and `unit`, or null if not parseable
135
+ */
136
+ const parseUnit = (str) => {
137
+ const match = str.trim().match(/^(-?[\d.]+)\s*(%|[a-z]*)$/i);
138
+ if (!match)
139
+ return null;
140
+ return { value: Number.parseFloat(match[1]), unit: match[2] || '' };
141
+ };
142
+ /**
143
+ * Evaluate a simple `calc()` expression with static values.
144
+ * Supports `+`, `-`, `*`, `/` with a single unit type (e.g. `calc(2 * 0.75rem)`).
145
+ * @param expr - The inner content of a `calc()` expression
146
+ * @returns The evaluated result as a string (e.g. "1.5rem"), or null if not evaluable
147
+ */
148
+ const evaluateCalc = (expr) => {
149
+ const tokens = expr.trim().split(/\s+/).filter(Boolean);
150
+ if (tokens.length === 0)
151
+ return null;
152
+ let result = 0;
153
+ let resultUnit = '';
154
+ let operator = '+';
155
+ for (const token of tokens) {
156
+ if (['+', '-', '*', '/'].includes(token)) {
157
+ operator = token;
158
+ continue;
159
+ }
160
+ const parsed = parseUnit(token);
161
+ if (!parsed)
162
+ return null;
163
+ if (parsed.unit && !resultUnit) {
164
+ resultUnit = parsed.unit;
165
+ }
166
+ else if (parsed.unit && parsed.unit !== resultUnit && resultUnit) {
167
+ return null;
168
+ }
169
+ switch (operator) {
170
+ case '+':
171
+ result += parsed.value;
172
+ break;
173
+ case '-':
174
+ result -= parsed.value;
175
+ break;
176
+ case '*':
177
+ result *= parsed.value;
178
+ break;
179
+ case '/':
180
+ if (parsed.value === 0)
181
+ return null;
182
+ result /= parsed.value;
183
+ break;
184
+ }
185
+ }
186
+ const rounded = Math.abs(result) < 1e-10 ? 0 : Math.round(result * 1e6) / 1e6;
187
+ return `${rounded}${resultUnit}`;
188
+ };
189
+ /**
190
+ * Resolve all fully-static `calc()` expressions in a CSS value.
191
+ * Skips expressions that still contain unresolved `var()` or nested CSS functions.
192
+ * @param value - The CSS value string potentially containing `calc()` expressions
193
+ * @returns The value with all evaluable `calc()` expressions replaced
194
+ */
195
+ export const resolveCalc = (value) => resolveCssFunction(value, 'calc', evaluateCalc, true);
196
+ // ── color-mix() ─────────────────────────────────────────────────────────
197
+ /**
198
+ * Parse a hex color string to an RGBA tuple.
199
+ * Supports `#rgb`, `#rrggbb`, `#rgba`, and `#rrggbbaa`.
200
+ * @param hex - The hex color string
201
+ * @returns An [r, g, b, a] tuple (0-255 for rgb, 0-1 for alpha), or null if not parseable
202
+ */
203
+ const parseHexColor = (hex) => {
204
+ const h = hex.replace('#', '');
205
+ let r, g, b;
206
+ let a = 1;
207
+ if (h.length === 3) {
208
+ r = Number.parseInt(h[0] + h[0], 16);
209
+ g = Number.parseInt(h[1] + h[1], 16);
210
+ b = Number.parseInt(h[2] + h[2], 16);
211
+ }
212
+ else if (h.length === 4) {
213
+ r = Number.parseInt(h[0] + h[0], 16);
214
+ g = Number.parseInt(h[1] + h[1], 16);
215
+ b = Number.parseInt(h[2] + h[2], 16);
216
+ a = Number.parseInt(h[3] + h[3], 16) / 255;
217
+ }
218
+ else if (h.length === 6) {
219
+ r = Number.parseInt(h.slice(0, 2), 16);
220
+ g = Number.parseInt(h.slice(2, 4), 16);
221
+ b = Number.parseInt(h.slice(4, 6), 16);
222
+ }
223
+ else if (h.length === 8) {
224
+ r = Number.parseInt(h.slice(0, 2), 16);
225
+ g = Number.parseInt(h.slice(2, 4), 16);
226
+ b = Number.parseInt(h.slice(4, 6), 16);
227
+ a = Number.parseInt(h.slice(6, 8), 16) / 255;
228
+ }
229
+ else {
230
+ return null;
231
+ }
232
+ if (Number.isNaN(r) ||
233
+ Number.isNaN(g) ||
234
+ Number.isNaN(b) ||
235
+ Number.isNaN(a))
236
+ return null;
237
+ return [r, g, b, a];
238
+ };
239
+ /**
240
+ * Convert a number (0-255) to a two-digit hex string.
241
+ * @param n - The number to convert
242
+ * @returns A zero-padded hex string (e.g. "0a", "ff")
243
+ */
244
+ const toHex = (n) => Math.round(Math.max(0, Math.min(255, n)))
245
+ .toString(16)
246
+ .padStart(2, '0');
247
+ /**
248
+ * Evaluate a `color-mix(in srgb, ...)` expression with two color arguments.
249
+ * Supports hex colors and `transparent`. Handles percentage-based mixing
250
+ * with premultiplied alpha blending.
251
+ * @param args - The inner arguments of `color-mix()`
252
+ * @returns The mixed color as a hex string, "transparent", or null if not evaluable
253
+ */
254
+ const evaluateColorMix = (args) => {
255
+ const srgbMatch = args.match(/^in\s+srgb\s*,\s*([\s\S]+?)\s*,\s*([\s\S]+?)\s*$/);
256
+ if (!srgbMatch)
257
+ return null;
258
+ const parseColorArg = (arg) => {
259
+ const parts = arg.trim().split(/\s+/);
260
+ if (parts.length === 1)
261
+ return { color: parts[0], percentage: null };
262
+ if (parts.length === 2) {
263
+ const pctMatch = parts[1].match(/^([\d.]+)%$/);
264
+ if (!pctMatch)
265
+ return null;
266
+ return {
267
+ color: parts[0],
268
+ percentage: Number.parseFloat(pctMatch[1])
269
+ };
270
+ }
271
+ return null;
272
+ };
273
+ const arg1 = parseColorArg(srgbMatch[1]);
274
+ const arg2 = parseColorArg(srgbMatch[2]);
275
+ if (!arg1 || !arg2)
276
+ return null;
277
+ let p1 = arg1.percentage;
278
+ let p2 = arg2.percentage;
279
+ if (p1 === null && p2 === null) {
280
+ p1 = 50;
281
+ p2 = 50;
282
+ }
283
+ else if (p1 !== null && p2 === null) {
284
+ p2 = 100 - p1;
285
+ }
286
+ else if (p1 === null && p2 !== null) {
287
+ p1 = 100 - p2;
288
+ }
289
+ const pct1 = p1 / 100;
290
+ const pct2 = p2 / 100;
291
+ const isTransparent = (c) => c === 'transparent';
292
+ let rgb1, alpha1 = 1;
293
+ if (isTransparent(arg1.color)) {
294
+ rgb1 = [0, 0, 0];
295
+ alpha1 = 0;
296
+ }
297
+ else {
298
+ const parsed = parseHexColor(arg1.color);
299
+ if (!parsed)
300
+ return null;
301
+ rgb1 = [parsed[0], parsed[1], parsed[2]];
302
+ alpha1 = parsed[3];
303
+ }
304
+ let rgb2, alpha2 = 1;
305
+ if (isTransparent(arg2.color)) {
306
+ rgb2 = [0, 0, 0];
307
+ alpha2 = 0;
308
+ }
309
+ else {
310
+ const parsed = parseHexColor(arg2.color);
311
+ if (!parsed)
312
+ return null;
313
+ rgb2 = [parsed[0], parsed[1], parsed[2]];
314
+ alpha2 = parsed[3];
315
+ }
316
+ const mixedAlpha = alpha1 * pct1 + alpha2 * pct2;
317
+ if (mixedAlpha === 0)
318
+ return 'transparent';
319
+ const mix = (c1, c2) => (c1 * alpha1 * pct1 + c2 * alpha2 * pct2) / mixedAlpha;
320
+ const r = mix(rgb1[0], rgb2[0]);
321
+ const g = mix(rgb1[1], rgb2[1]);
322
+ const b = mix(rgb1[2], rgb2[2]);
323
+ if (mixedAlpha >= 0.995)
324
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
325
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}${toHex(mixedAlpha * 255)}`;
326
+ };
327
+ /**
328
+ * Resolve all fully-static `color-mix()` expressions in a CSS value.
329
+ * Skips expressions that still contain unresolved `var()` references.
330
+ * @param value - The CSS value string potentially containing `color-mix()` expressions
331
+ * @returns The value with all evaluable `color-mix()` expressions replaced
332
+ */
333
+ export const resolveColorMix = (value) => resolveCssFunction(value, 'color-mix', evaluateColorMix);
@@ -0,0 +1,22 @@
1
+ import type { Root } from 'postcss';
2
+ /**
3
+ * Collapse `light-dark(x, y)` to just `x` when both arguments are identical
4
+ * after trimming whitespace. Handles nested parentheses correctly.
5
+ * @param value - The CSS value string potentially containing `light-dark()` calls
6
+ * @returns The value with identical-argument `light-dark()` calls collapsed
7
+ */
8
+ export declare const collapseLightDark: (value: string) => string;
9
+ /**
10
+ * Transform all declarations in a PostCSS root by resolving static `var()`,
11
+ * evaluating `calc()` and `color-mix()`, collapsing identical `light-dark()`,
12
+ * and optionally removing `@property` rules and unused declarations.
13
+ *
14
+ * @param root - The PostCSS root to transform
15
+ * @param staticVarMap - Map of static variable names to their resolved values
16
+ * @param referencedVars - Set to track which variables are still referenced after resolution
17
+ * @param propertyNames - Set of variable names that came from `@property`
18
+ * @param dynamicVars - Set of dynamic variable names (never removed)
19
+ * @param removeAtProperty - Whether to remove `@property` rules
20
+ * @param removeResolved - Whether to remove unused `@property`-sourced declarations
21
+ */
22
+ export declare const transformRoot: (root: Root, staticVarMap: Map<string, string>, referencedVars: Set<string>, propertyNames: Set<string>, dynamicVars: Set<string>, removeAtProperty: boolean, removeResolved: boolean) => void;
@@ -0,0 +1,85 @@
1
+ import { findCssFunction, findTopLevelComma } from './css-parser.js';
2
+ import { collectVarReferences, resolveCalc, resolveColorMix, resolveVars } from './resolve.js';
3
+ /**
4
+ * Collapse `light-dark(x, y)` to just `x` when both arguments are identical
5
+ * after trimming whitespace. Handles nested parentheses correctly.
6
+ * @param value - The CSS value string potentially containing `light-dark()` calls
7
+ * @returns The value with identical-argument `light-dark()` calls collapsed
8
+ */
9
+ export const collapseLightDark = (value) => {
10
+ let result = value;
11
+ let searchFrom = 0;
12
+ while (searchFrom < result.length) {
13
+ const found = findCssFunction(result, 'light-dark', searchFrom);
14
+ if (!found)
15
+ break;
16
+ const commaIdx = findTopLevelComma(found.inner);
17
+ if (commaIdx === -1) {
18
+ searchFrom = found.end;
19
+ continue;
20
+ }
21
+ const light = found.inner.slice(0, commaIdx).trim();
22
+ const dark = found.inner.slice(commaIdx + 1).trim();
23
+ if (light === dark) {
24
+ result =
25
+ result.slice(0, found.start) + light + result.slice(found.end);
26
+ searchFrom = found.start + light.length;
27
+ }
28
+ else {
29
+ searchFrom = found.end;
30
+ }
31
+ }
32
+ return result;
33
+ };
34
+ /**
35
+ * Transform all declarations in a PostCSS root by resolving static `var()`,
36
+ * evaluating `calc()` and `color-mix()`, collapsing identical `light-dark()`,
37
+ * and optionally removing `@property` rules and unused declarations.
38
+ *
39
+ * @param root - The PostCSS root to transform
40
+ * @param staticVarMap - Map of static variable names to their resolved values
41
+ * @param referencedVars - Set to track which variables are still referenced after resolution
42
+ * @param propertyNames - Set of variable names that came from `@property`
43
+ * @param dynamicVars - Set of dynamic variable names (never removed)
44
+ * @param removeAtProperty - Whether to remove `@property` rules
45
+ * @param removeResolved - Whether to remove unused `@property`-sourced declarations
46
+ */
47
+ export const transformRoot = (root, staticVarMap, referencedVars, propertyNames, dynamicVars, removeAtProperty, removeResolved) => {
48
+ root.walkDecls((decl) => {
49
+ const hasVar = decl.value.includes('var(');
50
+ const hasCalc = decl.value.includes('calc(');
51
+ const hasColorMix = decl.value.includes('color-mix(');
52
+ const hasLightDark = decl.value.includes('light-dark(');
53
+ if (!hasVar && !hasCalc && !hasColorMix && !hasLightDark)
54
+ return;
55
+ let resolved = decl.value;
56
+ if (hasVar) {
57
+ resolved = resolveVars(resolved, staticVarMap);
58
+ }
59
+ if (resolved.includes('calc(')) {
60
+ resolved = resolveCalc(resolved);
61
+ }
62
+ if (resolved.includes('color-mix(')) {
63
+ resolved = resolveColorMix(resolved);
64
+ }
65
+ if (resolved.includes('light-dark(')) {
66
+ resolved = collapseLightDark(resolved);
67
+ }
68
+ collectVarReferences(resolved, referencedVars);
69
+ decl.value = resolved;
70
+ });
71
+ if (removeAtProperty) {
72
+ root.walkAtRules('property', (atRule) => {
73
+ atRule.remove();
74
+ });
75
+ }
76
+ if (removeResolved) {
77
+ root.walkDecls(/^--/, (decl) => {
78
+ if (propertyNames.has(decl.prop) &&
79
+ !referencedVars.has(decl.prop) &&
80
+ !dynamicVars.has(decl.prop)) {
81
+ decl.remove();
82
+ }
83
+ });
84
+ }
85
+ };
@@ -0,0 +1,16 @@
1
+ import type { PluginCreator } from 'postcss';
2
+ import type { FlattenOptions } from './data.js';
3
+ /**
4
+ * PostCSS plugin that flattens DB UX Design System CSS custom properties
5
+ * by resolving `var()`, `@property`, `calc()`, `color-mix()`, and `light-dark()`.
6
+ *
7
+ * Detects dynamic variables (re-declared in non-`:root` selectors, `@media`,
8
+ * or matching `dynamicPrefixes`) and leaves them as `var()` references.
9
+ * Respects `@layer` priority via `@layer` order declarations and `@import ... layer()` rules.
10
+ *
11
+ * @param opts - Plugin options
12
+ * @returns A PostCSS plugin instance
13
+ */
14
+ declare const dbUxFlatten: PluginCreator<FlattenOptions>;
15
+ export { dbUxFlatten };
16
+ export type { FlattenOptions };
@@ -0,0 +1,52 @@
1
+ import { DEFAULT_DYNAMIC_PREFIXES } from './data.js';
2
+ import { collectImportLayers, collectLayerOrder, collectVarsWithLayer, getFileLayer, pickBestVar } from './helpers/collect.js';
3
+ import { transformRoot } from './helpers/transform.js';
4
+ /**
5
+ * PostCSS plugin that flattens DB UX Design System CSS custom properties
6
+ * by resolving `var()`, `@property`, `calc()`, `color-mix()`, and `light-dark()`.
7
+ *
8
+ * Detects dynamic variables (re-declared in non-`:root` selectors, `@media`,
9
+ * or matching `dynamicPrefixes`) and leaves them as `var()` references.
10
+ * Respects `@layer` priority via `@layer` order declarations and `@import ... layer()` rules.
11
+ *
12
+ * @param opts - Plugin options
13
+ * @returns A PostCSS plugin instance
14
+ */
15
+ const dbUxFlatten = (opts = {}) => {
16
+ const removeAtProperty = opts.removeAtProperty ?? true;
17
+ const removeResolved = opts.removeResolved ?? true;
18
+ const dynamicPrefixes = opts.dynamicPrefixes ?? DEFAULT_DYNAMIC_PREFIXES;
19
+ const varMap = new Map();
20
+ const propertyNames = new Set();
21
+ const dynamicVars = new Set();
22
+ const referencedVars = new Set();
23
+ const layerOrder = new Map();
24
+ const importLayerMap = new Map();
25
+ return {
26
+ postcssPlugin: 'postcss-flatten-db-variables',
27
+ Once(root) {
28
+ const filePath = root.source?.input?.file ?? '';
29
+ const fileLayerOrder = collectLayerOrder(root);
30
+ for (const [name, priority] of fileLayerOrder) {
31
+ layerOrder.set(name, priority);
32
+ }
33
+ const fileImportLayers = collectImportLayers(root);
34
+ for (const [specifier, layer] of fileImportLayers) {
35
+ importLayerMap.set(specifier, layer);
36
+ }
37
+ const fileLayer = getFileLayer(filePath, importLayerMap);
38
+ collectVarsWithLayer(root, varMap, propertyNames, dynamicVars, dynamicPrefixes, fileLayer, filePath);
39
+ },
40
+ OnceExit(root) {
41
+ const staticVarMap = new Map();
42
+ for (const [key, entries] of varMap) {
43
+ if (!dynamicVars.has(key)) {
44
+ staticVarMap.set(key, pickBestVar(entries, layerOrder));
45
+ }
46
+ }
47
+ transformRoot(root, staticVarMap, referencedVars, propertyNames, dynamicVars, removeAtProperty, removeResolved);
48
+ }
49
+ };
50
+ };
51
+ dbUxFlatten.postcss = true;
52
+ export { dbUxFlatten };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@db-ux/core-postcss-plugin",
3
- "version": "0.0.0",
3
+ "version": "4.5.4-postcss-a42fe67",
4
4
  "type": "module",
5
5
  "description": "PostCSS plugins for DB UX Design System",
6
6
  "repository": {
@@ -8,6 +8,41 @@
8
8
  "url": "git+https://github.com/db-ux-design-system/core-web.git"
9
9
  },
10
10
  "license": "Apache-2.0",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./build/index.d.ts",
14
+ "default": "./build/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "CHANGELOG.md",
19
+ "build"
20
+ ],
21
+ "keywords": [
22
+ "postcss",
23
+ "postcss-plugin",
24
+ "db-ux",
25
+ "css-variables",
26
+ "light-dark"
27
+ ],
28
+ "scripts": {
29
+ "build": "tsc",
30
+ "copy-output": "npm-run-all copy:*",
31
+ "copy:changelog": "cpr CHANGELOG.md ../../build-outputs/postcss-plugin/CHANGELOG.md --overwrite",
32
+ "copy:outputs": "cpr build ../../build-outputs/postcss-plugin/build -o",
33
+ "copy:package.json": "cpr package.json ../../build-outputs/postcss-plugin/package.json -o",
34
+ "copy:readme": "cpr README.md ../../build-outputs/postcss-plugin/README.md -o",
35
+ "test": "vitest run --config vitest.config.ts",
36
+ "test:update": "vitest run --config vitest.config.ts --update"
37
+ },
38
+ "peerDependencies": {
39
+ "postcss": "^8.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "postcss": "8.5.8",
43
+ "typescript": "5.9.3",
44
+ "vitest": "3.2.4"
45
+ },
11
46
  "publishConfig": {
12
47
  "registry": "https://registry.npmjs.org/",
13
48
  "access": "public"