@yahoo/uds 3.174.0 → 3.175.0

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.
@@ -970,7 +970,7 @@ function generateConfigStyles(config, schema, theme, previewOptions, emit, gener
970
970
  * @param schema - Production schema with defaults and variables
971
971
  * @param theme - Tailwind theme function to resolve design tokens
972
972
  * @param previewOptions - Options for generating preview mode class modifiers
973
- * @returns CSS styles object with grouped selectors for default aliases
973
+ * @returns CSS styles object with separate twin rules for default aliases
974
974
  */
975
975
  const generateStylesInner = (config, schema, theme, previewOptions, generateStylesContext) => {
976
976
  const styles = generateConfigStyles(config, schema, theme, previewOptions, previewOptions.emit ?? "exhaustive", generateStylesContext);
@@ -1035,7 +1035,8 @@ const generateStylesInner = (config, schema, theme, previewOptions, generateStyl
1035
1035
  finalStyles[selector] = declarations;
1036
1036
  continue;
1037
1037
  }
1038
- finalStyles[`${selector}, ${defaultsSelector}`] = declarations;
1038
+ finalStyles[selector] = declarations;
1039
+ finalStyles[defaultsSelector] = declarations;
1039
1040
  }
1040
1041
  return finalStyles;
1041
1042
  };
@@ -969,7 +969,7 @@ function generateConfigStyles(config, schema, theme, previewOptions, emit, gener
969
969
  * @param schema - Production schema with defaults and variables
970
970
  * @param theme - Tailwind theme function to resolve design tokens
971
971
  * @param previewOptions - Options for generating preview mode class modifiers
972
- * @returns CSS styles object with grouped selectors for default aliases
972
+ * @returns CSS styles object with separate twin rules for default aliases
973
973
  */
974
974
  const generateStylesInner = (config, schema, theme, previewOptions, generateStylesContext) => {
975
975
  const styles = generateConfigStyles(config, schema, theme, previewOptions, previewOptions.emit ?? "exhaustive", generateStylesContext);
@@ -1034,7 +1034,8 @@ const generateStylesInner = (config, schema, theme, previewOptions, generateStyl
1034
1034
  finalStyles[selector] = declarations;
1035
1035
  continue;
1036
1036
  }
1037
- finalStyles[`${selector}, ${defaultsSelector}`] = declarations;
1037
+ finalStyles[selector] = declarations;
1038
+ finalStyles[defaultsSelector] = declarations;
1038
1039
  }
1039
1040
  return finalStyles;
1040
1041
  };
@@ -27,6 +27,7 @@ const showHelp = () => {
27
27
  require_print.print(` ${require_colors.cyan("--verbose, -v")} List every scanned file used for purging`);
28
28
  require_print.print(` ${require_colors.cyan("--workers <n>")} Number of parallel workers (default: auto = CPU count - 1)`);
29
29
  require_print.print(` ${require_colors.cyan("--emit <mode>")} Emission mode: exhaustive (default) | selective`);
30
+ require_print.print(` ${require_colors.cyan("--utilities <mode>")} Utility ownership: emit (default) | app-sheet (omit app utilities; requires an onlyTheme app sheet with the same config)`);
30
31
  require_print.print(` ${require_colors.cyan("--include-dev-files")} Also scan test/spec/story files (excluded by default)`);
31
32
  require_print.print(` ${require_colors.cyan("--include-stories")} Also scan story files, keeping test/spec excluded`);
32
33
  require_print.print(` ${require_colors.cyan("--force")} Overwrite existing uds.theme.ts (for init)`);
@@ -26,6 +26,11 @@ const parseEmitOption = (value) => {
26
26
  if (value === "exhaustive" || value === "selective") return value;
27
27
  throw new Error(`Invalid --emit "${String(value)}". Expected 'exhaustive' or 'selective'.`);
28
28
  };
29
+ const parseUtilitiesOption = (value) => {
30
+ if (value === void 0 || value === null) return;
31
+ if (value === "emit" || value === "app-sheet") return value;
32
+ throw new Error(`Invalid --utilities "${String(value)}". Expected 'emit' or 'app-sheet'.`);
33
+ };
29
34
  const getCssRunOptions = (workspaceDir, options) => {
30
35
  return {
31
36
  workspaceDir,
@@ -35,6 +40,7 @@ const getCssRunOptions = (workspaceDir, options) => {
35
40
  entryOption: entryOption(options.entry),
36
41
  configOption: stringOption(options.config),
37
42
  emit: parseEmitOption(options.emit),
43
+ utilities: parseUtilitiesOption(options.utilities),
38
44
  includeDevFiles: isTruthyFlag(options["include-dev-files"]) || isTruthyFlag(options.includeDevFiles) ? true : void 0,
39
45
  includeStories: isTruthyFlag(options["include-stories"]) || isTruthyFlag(options.includeStories) ? true : void 0,
40
46
  watch: isTruthyFlag(options.watch) || isTruthyFlag(options.w),
@@ -26,6 +26,11 @@ const parseEmitOption = (value) => {
26
26
  if (value === "exhaustive" || value === "selective") return value;
27
27
  throw new Error(`Invalid --emit "${String(value)}". Expected 'exhaustive' or 'selective'.`);
28
28
  };
29
+ const parseUtilitiesOption = (value) => {
30
+ if (value === void 0 || value === null) return;
31
+ if (value === "emit" || value === "app-sheet") return value;
32
+ throw new Error(`Invalid --utilities "${String(value)}". Expected 'emit' or 'app-sheet'.`);
33
+ };
29
34
  const getCssRunOptions = (workspaceDir, options) => {
30
35
  return {
31
36
  workspaceDir,
@@ -35,6 +40,7 @@ const getCssRunOptions = (workspaceDir, options) => {
35
40
  entryOption: entryOption(options.entry),
36
41
  configOption: stringOption(options.config),
37
42
  emit: parseEmitOption(options.emit),
43
+ utilities: parseUtilitiesOption(options.utilities),
38
44
  includeDevFiles: isTruthyFlag(options["include-dev-files"]) || isTruthyFlag(options.includeDevFiles) ? true : void 0,
39
45
  includeStories: isTruthyFlag(options["include-stories"]) || isTruthyFlag(options.includeStories) ? true : void 0,
40
46
  watch: isTruthyFlag(options.watch) || isTruthyFlag(options.w),
@@ -27,6 +27,7 @@ const showHelp = () => {
27
27
  print(` ${cyan("--verbose, -v")} List every scanned file used for purging`);
28
28
  print(` ${cyan("--workers <n>")} Number of parallel workers (default: auto = CPU count - 1)`);
29
29
  print(` ${cyan("--emit <mode>")} Emission mode: exhaustive (default) | selective`);
30
+ print(` ${cyan("--utilities <mode>")} Utility ownership: emit (default) | app-sheet (omit app utilities; requires an onlyTheme app sheet with the same config)`);
30
31
  print(` ${cyan("--include-dev-files")} Also scan test/spec/story files (excluded by default)`);
31
32
  print(` ${cyan("--include-stories")} Also scan story files, keeping test/spec excluded`);
32
33
  print(` ${cyan("--force")} Overwrite existing uds.theme.ts (for init)`);
@@ -9,6 +9,7 @@ const require_nodeUtils = require("./nodeUtils.cjs");
9
9
  const require_spinner = require("../packages/cli/dist/lib/spinner.cjs");
10
10
  const require_index = require("../packages/config/dist/index.cjs");
11
11
  const require_safelist = require("../purger/optimized/utils/safelist.cjs");
12
+ const require_filterUnusedVariantRules = require("../postcss/filterUnusedVariantRules.cjs");
12
13
  const require_generate_helpers = require("./generate.helpers.cjs");
13
14
  let node_fs = require("node:fs");
14
15
  node_fs = require_runtime.__toESM(node_fs, 1);
@@ -48,12 +49,14 @@ const generateCSS = async (safelist, config, options) => {
48
49
  config,
49
50
  enablePreflight: cssFlags.enablePreflight,
50
51
  enableFontFaceDeclarations: cssFlags.enableFontFaceDeclarations,
51
- emit: options?.cssOptions?.emit
52
+ emit: options?.cssOptions?.emit,
53
+ utilities: options?.cssOptions?.utilities
52
54
  }),
53
55
  shouldPruneVars: cssFlags.shouldPruneVars,
54
56
  safeVarPrefixes: options?.safeVarPrefixes ?? [],
55
57
  scopeClass,
56
- otherBoundaryScopes: options?.otherBoundaryScopes ?? []
58
+ otherBoundaryScopes: options?.otherBoundaryScopes ?? [],
59
+ unusedVariantClasses: options?.variantClassMaps ? require_filterUnusedVariantRules.buildUnusedVariantClassSet(safelist, options.variantClassMaps) : void 0
57
60
  });
58
61
  const result = await require_perf.measureAsync("gen:postcss", () => (0, postcss.default)(plugins).process(sourceCSS, { from: void 0 }));
59
62
  let css = await require_perf.measureAsync("gen:scoped-color-fix", () => require_generate_helpers.applyScopedColorModeFix(result.css, scopeClass));
@@ -106,6 +109,8 @@ const generateSimpleModeCSS = async (options) => {
106
109
  ]), tokensConfig, {
107
110
  scope: options.scope,
108
111
  contentDir: entryDirs,
112
+ variantClassMaps: [options.variants, options.autoVariants],
113
+ cssOptions: { utilities: options.utilities },
109
114
  safeVarPrefixes: [...require_utils.getMotionVarPrefixes(options.componentData, [...allComponents]), ...require_utils.getConfigurableCssVariables()]
110
115
  });
111
116
  const outputPath = node_path.default.isAbsolute(options.outFile) ? options.outFile : node_path.default.join(options.workspaceDir, options.outFile);
@@ -1,6 +1,6 @@
1
1
 
2
2
  import { EmitMode } from "../packages/automated-config/dist/utils/index.cjs";
3
- import { TailwindSafelistConfig } from "./generate.helpers.cjs";
3
+ import { TailwindSafelistConfig, UtilitiesMode } from "./generate.helpers.cjs";
4
4
  import { UDSCSSOptimizationOptions } from "./theme.cjs";
5
5
 
6
6
  //#region ../css/dist/css/generate.d.mts
@@ -27,6 +27,26 @@ interface UDSCSSOptions {
27
27
  * (`scoped` / `excludeBoundaries`) + the isolation fence for coexistence safety.
28
28
  */
29
29
  emit?: EmitMode;
30
+ /**
31
+ * Who owns utility (non-component) CSS in this bundle.
32
+ *
33
+ * - `'emit'` (default): compile utilities from the entry content scan into
34
+ * this file using THIS theme's uds.config tokens — prefixed with `scope`
35
+ * when set, so this theme's values win inside the scope even if the app
36
+ * sheet's theme differs. Required when this file is the app's only
37
+ * stylesheet, or when the root Tailwind config diverges from uds.config.
38
+ *
39
+ * - `'app-sheet'`: omit them — the app's root Tailwind sheet owns all
40
+ * utilities, including inside `scope`. Only valid in the two-sheet
41
+ * architecture where the root config uses
42
+ * `tailwindPlugin({ onlyTheme: true })` with the SAME uds.config, so
43
+ * utility values are identical and only specificity changes. Explicit
44
+ * `css.safelist` entries are still emitted.
45
+ *
46
+ * Ignored (forced to `'emit'`, with a warning) for `scoped:` package
47
+ * bundles — their sources aren't part of any app sheet's content scan.
48
+ */
49
+ utilities?: UtilitiesMode;
30
50
  /**
31
51
  * Additional selectors whose subtrees this theme's bundles must not match.
32
52
  * Every emitted bundle (main and each `scoped:` entry) gets a
@@ -1,6 +1,6 @@
1
1
 
2
2
  import { EmitMode } from "../packages/automated-config/dist/utils/index.js";
3
- import { TailwindSafelistConfig } from "./generate.helpers.js";
3
+ import { TailwindSafelistConfig, UtilitiesMode } from "./generate.helpers.js";
4
4
  import { UDSCSSOptimizationOptions } from "./theme.js";
5
5
 
6
6
  //#region ../css/dist/css/generate.d.mts
@@ -27,6 +27,26 @@ interface UDSCSSOptions {
27
27
  * (`scoped` / `excludeBoundaries`) + the isolation fence for coexistence safety.
28
28
  */
29
29
  emit?: EmitMode;
30
+ /**
31
+ * Who owns utility (non-component) CSS in this bundle.
32
+ *
33
+ * - `'emit'` (default): compile utilities from the entry content scan into
34
+ * this file using THIS theme's uds.config tokens — prefixed with `scope`
35
+ * when set, so this theme's values win inside the scope even if the app
36
+ * sheet's theme differs. Required when this file is the app's only
37
+ * stylesheet, or when the root Tailwind config diverges from uds.config.
38
+ *
39
+ * - `'app-sheet'`: omit them — the app's root Tailwind sheet owns all
40
+ * utilities, including inside `scope`. Only valid in the two-sheet
41
+ * architecture where the root config uses
42
+ * `tailwindPlugin({ onlyTheme: true })` with the SAME uds.config, so
43
+ * utility values are identical and only specificity changes. Explicit
44
+ * `css.safelist` entries are still emitted.
45
+ *
46
+ * Ignored (forced to `'emit'`, with a warning) for `scoped:` package
47
+ * bundles — their sources aren't part of any app sheet's content scan.
48
+ */
49
+ utilities?: UtilitiesMode;
30
50
  /**
31
51
  * Additional selectors whose subtrees this theme's bundles must not match.
32
52
  * Every emitted bundle (main and each `scoped:` entry) gets a
@@ -8,6 +8,7 @@ const require_entryPoints = require("../../../tailwind-internal/dist/utils/entry
8
8
  const require_pruneVars = require("../postcss/pruneVars.cjs");
9
9
  const require_safelist$1 = require("../purger/optimized/utils/safelist.cjs");
10
10
  const require_runner_helpers = require("./runner.helpers.cjs");
11
+ const require_filterUnusedVariantRules = require("../postcss/filterUnusedVariantRules.cjs");
11
12
  const require_optimizeCSS = require("../utils/optimizeCSS.cjs");
12
13
  const require_postcss = require("./postcss.cjs");
13
14
  let node_fs = require("node:fs");
@@ -46,6 +47,10 @@ const getRawContentEntries = (rawContents) => rawContents.map((rawContent) => ({
46
47
  raw: rawContent,
47
48
  extension: "html"
48
49
  }));
50
+ const getTailwindContentSources = (options) => {
51
+ const sources = [...options.utilities === "app-sheet" ? [] : getContentGlobs(options.contentDir), ...getRawContentEntries(options.rawContents)];
52
+ return sources.length > 0 ? sources : getRawContentEntries([""]);
53
+ };
49
54
  const getTailwindEntrypointCandidates = () => [new URL("../../../tailwind-internal/dist/vendor/tailwindcss/lib/index.js", require("url").pathToFileURL(__filename).href), new URL("../vendor/tailwindcss/lib/index.js", require("url").pathToFileURL(__filename).href)];
50
55
  const loadTailwindcss = async () => {
51
56
  for (const candidate of getTailwindEntrypointCandidates()) {
@@ -69,11 +74,11 @@ const getInternalIconModeCss = (scopeClass) => {
69
74
  };
70
75
  const createTailwindPlugin = async (options) => {
71
76
  return (await loadTailwindcss())({
72
- content: [
73
- ...getContentGlobs(options.contentDir),
74
- ...getRawContentEntries(getInternalSafelistRawContent(options.safelist)),
75
- ...getRawContentEntries(options.rawContents ?? [])
76
- ],
77
+ content: getTailwindContentSources({
78
+ contentDir: options.contentDir,
79
+ utilities: options.utilities,
80
+ rawContents: [...getInternalSafelistRawContent(options.safelist), ...options.rawContents ?? []]
81
+ }),
77
82
  safelist: options.safelist,
78
83
  corePlugins: { preflight: options.enablePreflight },
79
84
  plugins: [require_tailwindPlugin.tailwindPlugin({
@@ -86,6 +91,7 @@ const createTailwindPlugin = async (options) => {
86
91
  };
87
92
  const buildPostcssPlugins = (options) => {
88
93
  const plugins = [options.tailwindPlugin, (0, autoprefixer.default)()];
94
+ if (options.unusedVariantClasses && options.unusedVariantClasses.size > 0) plugins.splice(1, 0, require_filterUnusedVariantRules.filterUnusedVariantRulesPlugin(options.unusedVariantClasses));
89
95
  if (options.shouldPruneVars) plugins.push(...require_pruneVars.getPruneVarPlugins(options.safeVarPrefixes));
90
96
  if (options.scopeClass) plugins.push((0, postcss_scope.default)(options.scopeClass));
91
97
  const otherBoundaryScopes = options.otherBoundaryScopes ?? [];
@@ -5,5 +5,11 @@ type TailwindSafelistConfig = string | {
5
5
  pattern: RegExp;
6
6
  variants?: string[];
7
7
  };
8
+ /**
9
+ * Who owns utility (non-component) CSS in a generated bundle. `'emit'`
10
+ * compiles utilities from the entry content scan into the bundle;
11
+ * `'app-sheet'` omits them because the app's own Tailwind sheet supplies them.
12
+ */
13
+ type UtilitiesMode = 'emit' | 'app-sheet';
8
14
  //#endregion
9
- export { TailwindSafelistConfig };
15
+ export { TailwindSafelistConfig, UtilitiesMode };
@@ -7,5 +7,11 @@ type TailwindSafelistConfig = string | {
7
7
  pattern: RegExp;
8
8
  variants?: string[];
9
9
  };
10
+ /**
11
+ * Who owns utility (non-component) CSS in a generated bundle. `'emit'`
12
+ * compiles utilities from the entry content scan into the bundle;
13
+ * `'app-sheet'` omits them because the app's own Tailwind sheet supplies them.
14
+ */
15
+ type UtilitiesMode = 'emit' | 'app-sheet';
10
16
  //#endregion
11
- export { TailwindSafelistConfig };
17
+ export { TailwindSafelistConfig, UtilitiesMode };
@@ -7,6 +7,7 @@ import { hasAllowedEntryFileExtension } from "../../../tailwind-internal/dist/ut
7
7
  import { getPruneVarPlugins } from "../postcss/pruneVars.js";
8
8
  import { getInternalSafelistClasses } from "../purger/optimized/utils/safelist.js";
9
9
  import { formatCssDuration } from "./runner.helpers.js";
10
+ import { filterUnusedVariantRulesPlugin } from "../postcss/filterUnusedVariantRules.js";
10
11
  import { optimizeCSS } from "../utils/optimizeCSS.js";
11
12
  import { appendIsolationExclusionPlugin, fixScopedColorModeSelectorsPlugin, fixScopedSelfOrParentSelectorsPlugin } from "./postcss.js";
12
13
  import fs from "node:fs";
@@ -40,6 +41,10 @@ const getRawContentEntries = (rawContents) => rawContents.map((rawContent) => ({
40
41
  raw: rawContent,
41
42
  extension: "html"
42
43
  }));
44
+ const getTailwindContentSources = (options) => {
45
+ const sources = [...options.utilities === "app-sheet" ? [] : getContentGlobs(options.contentDir), ...getRawContentEntries(options.rawContents)];
46
+ return sources.length > 0 ? sources : getRawContentEntries([""]);
47
+ };
43
48
  const getTailwindEntrypointCandidates = () => [new URL("../../../tailwind-internal/dist/vendor/tailwindcss/lib/index.js", import.meta.url), new URL("../vendor/tailwindcss/lib/index.js", import.meta.url)];
44
49
  const loadTailwindcss = async () => {
45
50
  for (const candidate of getTailwindEntrypointCandidates()) {
@@ -63,11 +68,11 @@ const getInternalIconModeCss = (scopeClass) => {
63
68
  };
64
69
  const createTailwindPlugin = async (options) => {
65
70
  return (await loadTailwindcss())({
66
- content: [
67
- ...getContentGlobs(options.contentDir),
68
- ...getRawContentEntries(getInternalSafelistRawContent(options.safelist)),
69
- ...getRawContentEntries(options.rawContents ?? [])
70
- ],
71
+ content: getTailwindContentSources({
72
+ contentDir: options.contentDir,
73
+ utilities: options.utilities,
74
+ rawContents: [...getInternalSafelistRawContent(options.safelist), ...options.rawContents ?? []]
75
+ }),
71
76
  safelist: options.safelist,
72
77
  corePlugins: { preflight: options.enablePreflight },
73
78
  plugins: [tailwindPlugin({
@@ -80,6 +85,7 @@ const createTailwindPlugin = async (options) => {
80
85
  };
81
86
  const buildPostcssPlugins = (options) => {
82
87
  const plugins = [options.tailwindPlugin, autoprefixer()];
88
+ if (options.unusedVariantClasses && options.unusedVariantClasses.size > 0) plugins.splice(1, 0, filterUnusedVariantRulesPlugin(options.unusedVariantClasses));
83
89
  if (options.shouldPruneVars) plugins.push(...getPruneVarPlugins(options.safeVarPrefixes));
84
90
  if (options.scopeClass) plugins.push(postcssScope(options.scopeClass));
85
91
  const otherBoundaryScopes = options.otherBoundaryScopes ?? [];
@@ -8,6 +8,7 @@ import { loadConfigFile, scanDirectoriesForSafelist } from "./nodeUtils.js";
8
8
  import { spinStop } from "../packages/cli/dist/lib/spinner.js";
9
9
  import { defaultTokensConfig } from "../packages/config/dist/index.js";
10
10
  import { deduplicateSafelist, getInternalSafelistClasses, getThemeAndScaleClasses } from "../purger/optimized/utils/safelist.js";
11
+ import { buildUnusedVariantClassSet } from "../postcss/filterUnusedVariantRules.js";
11
12
  import { applyScopedColorModeFix, buildPostcssPlugins, createTailwindPlugin, getCssFeatureFlags, getInternalIconModeCss, optimizeGeneratedCss, printSimpleModeStats } from "./generate.helpers.js";
12
13
  import fs from "node:fs";
13
14
  import path from "node:path";
@@ -44,12 +45,14 @@ const generateCSS = async (safelist, config, options) => {
44
45
  config,
45
46
  enablePreflight: cssFlags.enablePreflight,
46
47
  enableFontFaceDeclarations: cssFlags.enableFontFaceDeclarations,
47
- emit: options?.cssOptions?.emit
48
+ emit: options?.cssOptions?.emit,
49
+ utilities: options?.cssOptions?.utilities
48
50
  }),
49
51
  shouldPruneVars: cssFlags.shouldPruneVars,
50
52
  safeVarPrefixes: options?.safeVarPrefixes ?? [],
51
53
  scopeClass,
52
- otherBoundaryScopes: options?.otherBoundaryScopes ?? []
54
+ otherBoundaryScopes: options?.otherBoundaryScopes ?? [],
55
+ unusedVariantClasses: options?.variantClassMaps ? buildUnusedVariantClassSet(safelist, options.variantClassMaps) : void 0
53
56
  });
54
57
  const result = await measureAsync("gen:postcss", () => postcss(plugins).process(sourceCSS, { from: void 0 }));
55
58
  let css = await measureAsync("gen:scoped-color-fix", () => applyScopedColorModeFix(result.css, scopeClass));
@@ -102,6 +105,8 @@ const generateSimpleModeCSS = async (options) => {
102
105
  ]), tokensConfig, {
103
106
  scope: options.scope,
104
107
  contentDir: entryDirs,
108
+ variantClassMaps: [options.variants, options.autoVariants],
109
+ cssOptions: { utilities: options.utilities },
105
110
  safeVarPrefixes: [...getMotionVarPrefixes(options.componentData, [...allComponents]), ...getConfigurableCssVariables()]
106
111
  });
107
112
  const outputPath = path.isAbsolute(options.outFile) ? options.outFile : path.join(options.workspaceDir, options.outFile);
@@ -151,6 +151,7 @@ const runSimpleMode = async (options, context) => {
151
151
  autoVariants: context.autoVariants,
152
152
  componentData: context.componentData,
153
153
  scope: options.scope,
154
+ utilities: options.utilities,
154
155
  configPath,
155
156
  isWatch: options.watch,
156
157
  silent: options.silent,
@@ -348,6 +349,8 @@ const runThemeMode = async (options, context) => {
348
349
  ]);
349
350
  const allMotionComponents = [...inheritedComponents];
350
351
  const emit = options.emit ?? themeConfig.css?.emit ?? "exhaustive";
352
+ const utilities = options.utilities ?? themeConfig.css?.utilities ?? "emit";
353
+ if (utilities === "app-sheet" && scopedPackageTargets.length > 0) genLog.warn(`css.utilities 'app-sheet' ignored for scoped bundles (${scopedPackageTargets.map((target) => target.packageName).join(", ")}): package sources aren't in any app sheet's content scan; utilities still emitted (scoped).`);
351
354
  const allBoundarySelectors = [...emit === "selective" ? scopedPackageTargets.map((target) => target.scopeClass) : [], ...themeConfig.css?.excludeBoundaries ?? []];
352
355
  require_perf.captureMemory("before-gen");
353
356
  const mainCssResult = await require_perf.measureAsync("gen", () => require_generate.generateCSS([...themeConfig.css?.safelist ?? [], ...mainSafelist], appConfig, {
@@ -355,9 +358,11 @@ const runThemeMode = async (options, context) => {
355
358
  contentDir: entryDirs,
356
359
  cssOptions: {
357
360
  ...themeConfig.css,
358
- emit
361
+ emit,
362
+ utilities
359
363
  },
360
364
  otherBoundaryScopes: allBoundarySelectors,
365
+ variantClassMaps: [context.variants, context.autoVariants],
361
366
  safeVarPrefixes: [
362
367
  ...require_utils.getMotionVarPrefixes(context.componentData, allMotionComponents),
363
368
  ...require_utils.getConfigurableCssVariables(),
@@ -390,9 +395,11 @@ const runThemeMode = async (options, context) => {
390
395
  contentDir: scopedPackageTarget.entryDirs,
391
396
  cssOptions: {
392
397
  ...themeConfig.css,
393
- emit
398
+ emit,
399
+ utilities: "emit"
394
400
  },
395
401
  otherBoundaryScopes: allBoundarySelectors.filter((selector) => selector !== scopedPackageTarget.scopeClass),
402
+ variantClassMaps: [context.variants, context.autoVariants],
396
403
  referenceCss: themeConfig.css?.optimization?.deduplicateScopedCss === false ? void 0 : mainCssResult.css,
397
404
  safeVarPrefixes: [
398
405
  ...require_utils.getMotionVarPrefixes(context.componentData, [...packageScanResult.components]),
@@ -149,6 +149,7 @@ const runSimpleMode = async (options, context) => {
149
149
  autoVariants: context.autoVariants,
150
150
  componentData: context.componentData,
151
151
  scope: options.scope,
152
+ utilities: options.utilities,
152
153
  configPath,
153
154
  isWatch: options.watch,
154
155
  silent: options.silent,
@@ -346,6 +347,8 @@ const runThemeMode = async (options, context) => {
346
347
  ]);
347
348
  const allMotionComponents = [...inheritedComponents];
348
349
  const emit = options.emit ?? themeConfig.css?.emit ?? "exhaustive";
350
+ const utilities = options.utilities ?? themeConfig.css?.utilities ?? "emit";
351
+ if (utilities === "app-sheet" && scopedPackageTargets.length > 0) genLog.warn(`css.utilities 'app-sheet' ignored for scoped bundles (${scopedPackageTargets.map((target) => target.packageName).join(", ")}): package sources aren't in any app sheet's content scan; utilities still emitted (scoped).`);
349
352
  const allBoundarySelectors = [...emit === "selective" ? scopedPackageTargets.map((target) => target.scopeClass) : [], ...themeConfig.css?.excludeBoundaries ?? []];
350
353
  captureMemory("before-gen");
351
354
  const mainCssResult = await measureAsync("gen", () => generateCSS([...themeConfig.css?.safelist ?? [], ...mainSafelist], appConfig, {
@@ -353,9 +356,11 @@ const runThemeMode = async (options, context) => {
353
356
  contentDir: entryDirs,
354
357
  cssOptions: {
355
358
  ...themeConfig.css,
356
- emit
359
+ emit,
360
+ utilities
357
361
  },
358
362
  otherBoundaryScopes: allBoundarySelectors,
363
+ variantClassMaps: [context.variants, context.autoVariants],
359
364
  safeVarPrefixes: [
360
365
  ...getMotionVarPrefixes(context.componentData, allMotionComponents),
361
366
  ...getConfigurableCssVariables(),
@@ -388,9 +393,11 @@ const runThemeMode = async (options, context) => {
388
393
  contentDir: scopedPackageTarget.entryDirs,
389
394
  cssOptions: {
390
395
  ...themeConfig.css,
391
- emit
396
+ emit,
397
+ utilities: "emit"
392
398
  },
393
399
  otherBoundaryScopes: allBoundarySelectors.filter((selector) => selector !== scopedPackageTarget.scopeClass),
400
+ variantClassMaps: [context.variants, context.autoVariants],
394
401
  referenceCss: themeConfig.css?.optimization?.deduplicateScopedCss === false ? void 0 : mainCssResult.css,
395
402
  safeVarPrefixes: [
396
403
  ...getMotionVarPrefixes(context.componentData, [...packageScanResult.components]),
@@ -0,0 +1,71 @@
1
+ /*! © 2026 Yahoo, Inc. UDS v0.0.0-development */
2
+ //#region ../css/dist/postcss/filterUnusedVariantRules.mjs
3
+ /*! © 2026 Yahoo, Inc. UDS CSS v0.0.0-development */
4
+ const isPatternEntry = (entry) => typeof entry === "object" && entry !== null && entry.pattern instanceof RegExp;
5
+ /**
6
+ * Computes the variant classes that are enumerated by the variant class maps
7
+ * but absent from the safelist, for every axis the scan DID find at least one
8
+ * class of.
9
+ *
10
+ * Rationale: Tailwind v3 emits an `addComponents` rule when ANY class in its
11
+ * selector matches a candidate, so variant-agnostic classes shared across
12
+ * selectors (componentState classes like `…-value-checked-root`, default
13
+ * aliases) ship rules for variants the app never uses. Restricting to axes
14
+ * with at least one safelisted member keeps the filter conservative: an axis
15
+ * the scanner never matched contributes nothing, so components that only
16
+ * appear through another bundle or through raw `content` strings are left
17
+ * untouched.
18
+ */
19
+ const buildUnusedVariantClassSet = (safelist, variantClassMaps) => {
20
+ const safelistedClasses = new Set(safelist.filter((entry) => typeof entry === "string"));
21
+ const patterns = safelist.filter(isPatternEntry).map((entry) => entry.pattern);
22
+ const isSafelisted = (className) => safelistedClasses.has(className) || patterns.some((pattern) => pattern.test(className));
23
+ const axes = /* @__PURE__ */ new Map();
24
+ for (const map of variantClassMaps) for (const [axisKey, values] of Object.entries(map)) {
25
+ const axisClasses = axes.get(axisKey) ?? /* @__PURE__ */ new Set();
26
+ for (const className of Object.values(values)) if (typeof className === "string" && !/\s/.test(className)) axisClasses.add(className);
27
+ axes.set(axisKey, axisClasses);
28
+ }
29
+ const unused = /* @__PURE__ */ new Set();
30
+ for (const axisClasses of axes.values()) {
31
+ let axisIsScanned = false;
32
+ for (const className of axisClasses) if (isSafelisted(className)) {
33
+ axisIsScanned = true;
34
+ break;
35
+ }
36
+ if (!axisIsScanned) continue;
37
+ for (const className of axisClasses) if (!isSafelisted(className)) unused.add(className);
38
+ }
39
+ return unused;
40
+ };
41
+ const CLASS_TOKEN_RE = /\.((?:[a-zA-Z0-9_-]|\\.)+)/g;
42
+ const selectorReferencesUnusedClass = (selector, unused) => {
43
+ for (const match of selector.matchAll(CLASS_TOKEN_RE)) if (unused.has(match[1])) return true;
44
+ return false;
45
+ };
46
+ /**
47
+ * PostCSS plugin dropping selector alternatives (and whole rules) that
48
+ * reference a variant class in `unused`. Runs on the Tailwind-generated
49
+ * bundle, after which emptied `@media`/`@supports` blocks are removed. Placed
50
+ * before var pruning so variables only referenced by dropped rules get pruned
51
+ * too.
52
+ */
53
+ const filterUnusedVariantRulesPlugin = (unused) => ({
54
+ postcssPlugin: "uds-filter-unused-variant-rules",
55
+ Once(root) {
56
+ if (unused.size === 0) return;
57
+ root.walkRules((rule) => {
58
+ if (!rule.selector.includes(".uds-")) return;
59
+ const kept = rule.selectors.filter((selector) => !selectorReferencesUnusedClass(selector, unused));
60
+ if (kept.length === rule.selectors.length) return;
61
+ if (kept.length === 0) rule.remove();
62
+ else rule.selector = kept.join(", ");
63
+ });
64
+ root.walkAtRules((atRule) => {
65
+ if ((atRule.name === "media" || atRule.name === "supports") && (atRule.nodes?.length ?? 0) === 0) atRule.remove();
66
+ });
67
+ }
68
+ });
69
+ //#endregion
70
+ exports.buildUnusedVariantClassSet = buildUnusedVariantClassSet;
71
+ exports.filterUnusedVariantRulesPlugin = filterUnusedVariantRulesPlugin;
@@ -0,0 +1,70 @@
1
+ /*! © 2026 Yahoo, Inc. UDS v0.0.0-development */
2
+ //#region ../css/dist/postcss/filterUnusedVariantRules.mjs
3
+ /*! © 2026 Yahoo, Inc. UDS CSS v0.0.0-development */
4
+ const isPatternEntry = (entry) => typeof entry === "object" && entry !== null && entry.pattern instanceof RegExp;
5
+ /**
6
+ * Computes the variant classes that are enumerated by the variant class maps
7
+ * but absent from the safelist, for every axis the scan DID find at least one
8
+ * class of.
9
+ *
10
+ * Rationale: Tailwind v3 emits an `addComponents` rule when ANY class in its
11
+ * selector matches a candidate, so variant-agnostic classes shared across
12
+ * selectors (componentState classes like `…-value-checked-root`, default
13
+ * aliases) ship rules for variants the app never uses. Restricting to axes
14
+ * with at least one safelisted member keeps the filter conservative: an axis
15
+ * the scanner never matched contributes nothing, so components that only
16
+ * appear through another bundle or through raw `content` strings are left
17
+ * untouched.
18
+ */
19
+ const buildUnusedVariantClassSet = (safelist, variantClassMaps) => {
20
+ const safelistedClasses = new Set(safelist.filter((entry) => typeof entry === "string"));
21
+ const patterns = safelist.filter(isPatternEntry).map((entry) => entry.pattern);
22
+ const isSafelisted = (className) => safelistedClasses.has(className) || patterns.some((pattern) => pattern.test(className));
23
+ const axes = /* @__PURE__ */ new Map();
24
+ for (const map of variantClassMaps) for (const [axisKey, values] of Object.entries(map)) {
25
+ const axisClasses = axes.get(axisKey) ?? /* @__PURE__ */ new Set();
26
+ for (const className of Object.values(values)) if (typeof className === "string" && !/\s/.test(className)) axisClasses.add(className);
27
+ axes.set(axisKey, axisClasses);
28
+ }
29
+ const unused = /* @__PURE__ */ new Set();
30
+ for (const axisClasses of axes.values()) {
31
+ let axisIsScanned = false;
32
+ for (const className of axisClasses) if (isSafelisted(className)) {
33
+ axisIsScanned = true;
34
+ break;
35
+ }
36
+ if (!axisIsScanned) continue;
37
+ for (const className of axisClasses) if (!isSafelisted(className)) unused.add(className);
38
+ }
39
+ return unused;
40
+ };
41
+ const CLASS_TOKEN_RE = /\.((?:[a-zA-Z0-9_-]|\\.)+)/g;
42
+ const selectorReferencesUnusedClass = (selector, unused) => {
43
+ for (const match of selector.matchAll(CLASS_TOKEN_RE)) if (unused.has(match[1])) return true;
44
+ return false;
45
+ };
46
+ /**
47
+ * PostCSS plugin dropping selector alternatives (and whole rules) that
48
+ * reference a variant class in `unused`. Runs on the Tailwind-generated
49
+ * bundle, after which emptied `@media`/`@supports` blocks are removed. Placed
50
+ * before var pruning so variables only referenced by dropped rules get pruned
51
+ * too.
52
+ */
53
+ const filterUnusedVariantRulesPlugin = (unused) => ({
54
+ postcssPlugin: "uds-filter-unused-variant-rules",
55
+ Once(root) {
56
+ if (unused.size === 0) return;
57
+ root.walkRules((rule) => {
58
+ if (!rule.selector.includes(".uds-")) return;
59
+ const kept = rule.selectors.filter((selector) => !selectorReferencesUnusedClass(selector, unused));
60
+ if (kept.length === rule.selectors.length) return;
61
+ if (kept.length === 0) rule.remove();
62
+ else rule.selector = kept.join(", ");
63
+ });
64
+ root.walkAtRules((atRule) => {
65
+ if ((atRule.name === "media" || atRule.name === "supports") && (atRule.nodes?.length ?? 0) === 0) atRule.remove();
66
+ });
67
+ }
68
+ });
69
+ //#endregion
70
+ export { buildUnusedVariantClassSet, filterUnusedVariantRulesPlugin };
@@ -956,7 +956,7 @@ function generateConfigStyles(config, schema, theme, previewOptions, emit, gener
956
956
  * @param schema - Production schema with defaults and variables
957
957
  * @param theme - Tailwind theme function to resolve design tokens
958
958
  * @param previewOptions - Options for generating preview mode class modifiers
959
- * @returns CSS styles object with grouped selectors for default aliases
959
+ * @returns CSS styles object with separate twin rules for default aliases
960
960
  */
961
961
  const generateStylesInner = (config, schema, theme, previewOptions, generateStylesContext) => {
962
962
  const styles = generateConfigStyles(config, schema, theme, previewOptions, previewOptions.emit ?? "exhaustive", generateStylesContext);
@@ -1021,7 +1021,8 @@ const generateStylesInner = (config, schema, theme, previewOptions, generateStyl
1021
1021
  finalStyles[selector] = declarations;
1022
1022
  continue;
1023
1023
  }
1024
- finalStyles[`${selector}, ${defaultsSelector}`] = declarations;
1024
+ finalStyles[selector] = declarations;
1025
+ finalStyles[defaultsSelector] = declarations;
1025
1026
  }
1026
1027
  return finalStyles;
1027
1028
  };
@@ -955,7 +955,7 @@ function generateConfigStyles(config, schema, theme, previewOptions, emit, gener
955
955
  * @param schema - Production schema with defaults and variables
956
956
  * @param theme - Tailwind theme function to resolve design tokens
957
957
  * @param previewOptions - Options for generating preview mode class modifiers
958
- * @returns CSS styles object with grouped selectors for default aliases
958
+ * @returns CSS styles object with separate twin rules for default aliases
959
959
  */
960
960
  const generateStylesInner = (config, schema, theme, previewOptions, generateStylesContext) => {
961
961
  const styles = generateConfigStyles(config, schema, theme, previewOptions, previewOptions.emit ?? "exhaustive", generateStylesContext);
@@ -1020,7 +1020,8 @@ const generateStylesInner = (config, schema, theme, previewOptions, generateStyl
1020
1020
  finalStyles[selector] = declarations;
1021
1021
  continue;
1022
1022
  }
1023
- finalStyles[`${selector}, ${defaultsSelector}`] = declarations;
1023
+ finalStyles[selector] = declarations;
1024
+ finalStyles[defaultsSelector] = declarations;
1024
1025
  }
1025
1026
  return finalStyles;
1026
1027
  };