@ethlete/core 5.0.0-next.44 → 5.0.0-next.46
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 +22 -0
- package/fesm2022/ethlete-core.mjs +158 -57
- package/fesm2022/ethlete-core.mjs.map +1 -1
- package/generators/devtools-about/generator.js +113 -0
- package/generators/devtools-about/schema.json +25 -0
- package/generators/generators.json +10 -0
- package/generators/migrate-surface-interaction-swatch/interaction-swatch.js +43 -0
- package/generators/migrate-surface-interaction-swatch/migration.js +29 -0
- package/generators/migrate-surface-interaction-swatch/schema.json +26 -0
- package/generators/tailwind-4-surface-theme/generator.js +119 -29
- package/package.json +1 -1
- package/types/ethlete-core.d.ts +91 -9
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { formatFiles, joinPathFragments, logger, readProjectConfiguration, updateProjectConfiguration, visitNotIgnoredFiles, } from '@nx/devkit';
|
|
2
|
+
const SCRIPT_PATH = 'tools/generate-build-info.js';
|
|
3
|
+
const SCRIPT = `const { execSync } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const [, , outputPath, packageJsonPath] = process.argv;
|
|
8
|
+
|
|
9
|
+
const read = (command) => {
|
|
10
|
+
try {
|
|
11
|
+
return execSync(command, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const version = fs.existsSync(packageJsonPath)
|
|
18
|
+
? JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')).version
|
|
19
|
+
: null;
|
|
20
|
+
|
|
21
|
+
const info = {
|
|
22
|
+
version: version ?? 'unknown',
|
|
23
|
+
sha: read('git rev-parse --short HEAD') ?? 'unknown',
|
|
24
|
+
branch: read('git rev-parse --abbrev-ref HEAD') ?? 'unknown',
|
|
25
|
+
builtAt: new Date().toISOString(),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const contents =
|
|
29
|
+
'// Generated by ' +
|
|
30
|
+
${JSON.stringify(SCRIPT_PATH)} +
|
|
31
|
+
' - do not edit.\\n\\nexport const APP_BUILD_INFO = ' +
|
|
32
|
+
JSON.stringify(info, null, 2) +
|
|
33
|
+
' as const;\\n';
|
|
34
|
+
|
|
35
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
36
|
+
fs.writeFileSync(outputPath, contents);
|
|
37
|
+
`;
|
|
38
|
+
const PLACEHOLDER = `// Generated by ${SCRIPT_PATH} - do not edit.
|
|
39
|
+
|
|
40
|
+
export const APP_BUILD_INFO = {
|
|
41
|
+
version: 'unknown',
|
|
42
|
+
sha: 'unknown',
|
|
43
|
+
branch: 'unknown',
|
|
44
|
+
builtAt: '1970-01-01T00:00:00.000Z',
|
|
45
|
+
} as const;
|
|
46
|
+
`;
|
|
47
|
+
const relativeImport = (from, to) => {
|
|
48
|
+
const fromParts = from.split('/').slice(0, -1);
|
|
49
|
+
const toParts = to.replace(/\.ts$/, '').split('/');
|
|
50
|
+
while (fromParts.length && toParts.length && fromParts[0] === toParts[0]) {
|
|
51
|
+
fromParts.shift();
|
|
52
|
+
toParts.shift();
|
|
53
|
+
}
|
|
54
|
+
const up = fromParts.length ? fromParts.map(() => '..') : ['.'];
|
|
55
|
+
return [...up, ...toParts].join('/');
|
|
56
|
+
};
|
|
57
|
+
/** Wires the `provideQueryDevtools({ about })` call up to the generated constant, if it can be found. */
|
|
58
|
+
const wireProvider = (tree, projectRoot, outputPath) => {
|
|
59
|
+
let wired = false;
|
|
60
|
+
visitNotIgnoredFiles(tree, projectRoot, (file) => {
|
|
61
|
+
if (wired || !file.endsWith('.ts'))
|
|
62
|
+
return;
|
|
63
|
+
const contents = tree.read(file, 'utf-8');
|
|
64
|
+
if (!contents?.includes('provideQueryDevtools('))
|
|
65
|
+
return;
|
|
66
|
+
if (!contents.includes('provideQueryDevtools()')) {
|
|
67
|
+
logger.warn(`${file} already passes options to provideQueryDevtools() - add \`about: APP_BUILD_INFO\` to them yourself.`);
|
|
68
|
+
wired = true;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const importPath = relativeImport(file, outputPath);
|
|
72
|
+
tree.write(file, `import { APP_BUILD_INFO } from '${importPath}';\n` +
|
|
73
|
+
contents.replace('provideQueryDevtools()', 'provideQueryDevtools({ about: APP_BUILD_INFO })'));
|
|
74
|
+
wired = true;
|
|
75
|
+
});
|
|
76
|
+
return wired;
|
|
77
|
+
};
|
|
78
|
+
export const devtoolsAboutGenerator = async (tree, options) => {
|
|
79
|
+
const project = readProjectConfiguration(tree, options.project);
|
|
80
|
+
const outputPath = options.outputPath ?? joinPathFragments(project.sourceRoot ?? project.root, 'build-info.ts');
|
|
81
|
+
if (!tree.exists(SCRIPT_PATH))
|
|
82
|
+
tree.write(SCRIPT_PATH, SCRIPT);
|
|
83
|
+
if (!tree.exists(outputPath))
|
|
84
|
+
tree.write(outputPath, PLACEHOLDER);
|
|
85
|
+
const gitignore = tree.read('.gitignore', 'utf-8') ?? '';
|
|
86
|
+
// Regenerated on every build, and its SHA changes with every commit - committing it would put a
|
|
87
|
+
// one-line diff in every commit that says nothing about the change.
|
|
88
|
+
if (!gitignore.includes(outputPath))
|
|
89
|
+
tree.write('.gitignore', `${gitignore.trimEnd()}\n${outputPath}\n`);
|
|
90
|
+
project.targets ??= {};
|
|
91
|
+
project.targets['build-info'] = {
|
|
92
|
+
executor: 'nx:run-commands',
|
|
93
|
+
options: {
|
|
94
|
+
commands: [`node ${SCRIPT_PATH} ${outputPath} ${joinPathFragments(project.root, 'package.json')}`],
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
for (const target of ['build', 'serve']) {
|
|
98
|
+
const config = project.targets[target];
|
|
99
|
+
if (!config)
|
|
100
|
+
continue;
|
|
101
|
+
config.dependsOn ??= [];
|
|
102
|
+
if (!config.dependsOn.includes('build-info'))
|
|
103
|
+
config.dependsOn.push('build-info');
|
|
104
|
+
}
|
|
105
|
+
updateProjectConfiguration(tree, options.project, project);
|
|
106
|
+
if (!wireProvider(tree, project.root, outputPath)) {
|
|
107
|
+
logger.warn(`No provideQueryDevtools() call found in ${project.root} - pass \`about: APP_BUILD_INFO\` to it yourself.`);
|
|
108
|
+
}
|
|
109
|
+
if (!options.skipFormat)
|
|
110
|
+
await formatFiles(tree);
|
|
111
|
+
};
|
|
112
|
+
export default devtoolsAboutGenerator;
|
|
113
|
+
//# sourceMappingURL=generator.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"$id": "devtools-about",
|
|
4
|
+
"title": "Query devtools About generator",
|
|
5
|
+
"description": "Generates the application's build info (version, commit SHA, branch, build time) and wires it into provideQueryDevtools({ about })",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"project": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"description": "The application to wire up",
|
|
11
|
+
"$default": { "$source": "argv", "index": 0 },
|
|
12
|
+
"x-prompt": "Which application should report its build info?"
|
|
13
|
+
},
|
|
14
|
+
"outputPath": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"description": "Where to write the generated build-info file. Defaults to `<sourceRoot>/build-info.ts`."
|
|
17
|
+
},
|
|
18
|
+
"skipFormat": {
|
|
19
|
+
"type": "boolean",
|
|
20
|
+
"description": "Skip formatting files after generation",
|
|
21
|
+
"default": false
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"required": ["project"]
|
|
25
|
+
}
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
"schema": "./migrate-provider-shape/schema.json",
|
|
11
11
|
"description": "Rewrite destructured provider tuples into the tree-shakeable definition shape"
|
|
12
12
|
},
|
|
13
|
+
"migrate-surface-interaction-swatch": {
|
|
14
|
+
"factory": "./migrate-surface-interaction-swatch/migration",
|
|
15
|
+
"schema": "./migrate-surface-interaction-swatch/schema.json",
|
|
16
|
+
"description": "Rewrite surface theme interactionColor maps into the { color, onColor, inkColor } swatch shape"
|
|
17
|
+
},
|
|
13
18
|
"tailwind-4-color-theme": {
|
|
14
19
|
"factory": "./tailwind-4-color-theme/generator",
|
|
15
20
|
"schema": "./tailwind-4-color-theme/schema.json",
|
|
@@ -19,6 +24,11 @@
|
|
|
19
24
|
"factory": "./tailwind-4-surface-theme/generator",
|
|
20
25
|
"schema": "./tailwind-4-surface-theme/schema.json",
|
|
21
26
|
"description": "Generate surface theming for Tailwind CSS v4"
|
|
27
|
+
},
|
|
28
|
+
"devtools-about": {
|
|
29
|
+
"factory": "./devtools-about/generator",
|
|
30
|
+
"schema": "./devtools-about/schema.json",
|
|
31
|
+
"description": "Generate the app's build info and wire it into provideQueryDevtools({ about })"
|
|
22
32
|
}
|
|
23
33
|
}
|
|
24
34
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Project, SyntaxKind } from 'ts-morph';
|
|
2
|
+
const INTERACTION_KEYS = ['default', 'hover', 'focus', 'active', 'disabled'];
|
|
3
|
+
const createProject = () => new Project({
|
|
4
|
+
useInMemoryFileSystem: true,
|
|
5
|
+
compilerOptions: {
|
|
6
|
+
target: 99,
|
|
7
|
+
module: 99,
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
const isFlatInteractionColor = (initializer) => initializer
|
|
11
|
+
.getProperties()
|
|
12
|
+
.some((property) => property.isKind(SyntaxKind.PropertyAssignment) && INTERACTION_KEYS.includes(property.getName()));
|
|
13
|
+
const findFlatInteractionColor = (sourceFile) => {
|
|
14
|
+
for (const property of sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAssignment)) {
|
|
15
|
+
if (property.getName() !== 'interactionColor')
|
|
16
|
+
continue;
|
|
17
|
+
const initializer = property.getInitializer();
|
|
18
|
+
if (initializer?.isKind(SyntaxKind.ObjectLiteralExpression) && isFlatInteractionColor(initializer)) {
|
|
19
|
+
return initializer;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Rewrites `interactionColor: { default, hover, … }` into `interactionColor: { color: { default, … } }`,
|
|
26
|
+
* the swatch shape that also carries the optional `onColor` and `inkColor` maps.
|
|
27
|
+
*/
|
|
28
|
+
export const migrateInteractionSwatchInFile = (filePath, content) => {
|
|
29
|
+
if (!content.includes('interactionColor')) {
|
|
30
|
+
return { changed: false, content };
|
|
31
|
+
}
|
|
32
|
+
const sourceFile = createProject().createSourceFile(filePath, content);
|
|
33
|
+
let changed = false;
|
|
34
|
+
// Re-queried after every rewrite: manipulating the AST forgets the nodes a single up-front
|
|
35
|
+
// collection would have handed us.
|
|
36
|
+
for (let initializer = findFlatInteractionColor(sourceFile); initializer;) {
|
|
37
|
+
initializer.replaceWithText(`{ color: ${initializer.getText()} }`);
|
|
38
|
+
changed = true;
|
|
39
|
+
initializer = findFlatInteractionColor(sourceFile);
|
|
40
|
+
}
|
|
41
|
+
return { changed, content: changed ? sourceFile.getFullText() : content };
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=interaction-swatch.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { formatFiles } from '@nx/devkit';
|
|
2
|
+
import { createMigrationScope } from '../migrate-provider-shape/migration-scope';
|
|
3
|
+
import { migrateInteractionSwatchInFile } from './interaction-swatch';
|
|
4
|
+
export default async function migrateSurfaceInteractionSwatch(tree, schema) {
|
|
5
|
+
console.log('\n🔄 Migrating surface interactionColor to the swatch shape...');
|
|
6
|
+
const scope = createMigrationScope(tree, schema);
|
|
7
|
+
console.log(` Scope: ${scope.describe()}`);
|
|
8
|
+
let filesChanged = 0;
|
|
9
|
+
scope.visit(tree, (filePath) => {
|
|
10
|
+
if (!filePath.endsWith('.ts') || filePath.endsWith('.d.ts'))
|
|
11
|
+
return;
|
|
12
|
+
const before = tree.read(filePath, 'utf-8');
|
|
13
|
+
if (!before)
|
|
14
|
+
return;
|
|
15
|
+
const result = migrateInteractionSwatchInFile(filePath, before);
|
|
16
|
+
if (!result.changed)
|
|
17
|
+
return;
|
|
18
|
+
tree.write(filePath, result.content);
|
|
19
|
+
filesChanged++;
|
|
20
|
+
console.log(` ✓ ${filePath}`);
|
|
21
|
+
});
|
|
22
|
+
console.log(filesChanged === 0
|
|
23
|
+
? ' Nothing to migrate.\n'
|
|
24
|
+
: `\n✅ Migrated ${filesChanged} file(s). Regenerate your surface CSS with \`nx g @ethlete/core:tailwind-4-surface-theme\`.\n`);
|
|
25
|
+
if (!schema.skipFormat) {
|
|
26
|
+
await formatFiles(tree);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=migration.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"$id": "migrate-surface-interaction-swatch",
|
|
4
|
+
"title": "Migrate surface interactionColor to the swatch shape",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"projects": {
|
|
8
|
+
"type": "array",
|
|
9
|
+
"items": { "type": "string" },
|
|
10
|
+
"description": "Nx projects to migrate. Defaults to every project in the workspace.",
|
|
11
|
+
"alias": "project"
|
|
12
|
+
},
|
|
13
|
+
"include": {
|
|
14
|
+
"type": "array",
|
|
15
|
+
"items": { "type": "string" },
|
|
16
|
+
"description": "Path prefixes to migrate, relative to the workspace root (e.g. 'apps/web'). Unioned with `projects`. Defaults to the whole workspace.",
|
|
17
|
+
"alias": "includePath"
|
|
18
|
+
},
|
|
19
|
+
"skipFormat": {
|
|
20
|
+
"type": "boolean",
|
|
21
|
+
"description": "Skip formatting files after migration",
|
|
22
|
+
"default": false
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"required": []
|
|
26
|
+
}
|
|
@@ -158,9 +158,52 @@ const COLOR_PROPS = ['background', 'color', 'colorMuted', 'colorSubtle', 'border
|
|
|
158
158
|
const isColorProp = (name) => COLOR_PROPS.some((p) => p === name);
|
|
159
159
|
const INTERACTION_KEYS = ['default', 'hover', 'focus', 'active', 'disabled'];
|
|
160
160
|
const isInteractionKey = (name) => INTERACTION_KEYS.some((key) => key === name);
|
|
161
|
+
const SWATCH_KEYS = ['color', 'onColor', 'inkColor'];
|
|
162
|
+
const isSwatchKey = (name) => SWATCH_KEYS.some((key) => key === name);
|
|
163
|
+
function parseInteractionColorMap(obj) {
|
|
164
|
+
const map = {};
|
|
165
|
+
for (const prop of obj.getProperties()) {
|
|
166
|
+
if (!prop.isKind(SyntaxKind.PropertyAssignment))
|
|
167
|
+
continue;
|
|
168
|
+
const name = prop.getName();
|
|
169
|
+
const initializer = prop.getInitializer();
|
|
170
|
+
if (initializer?.isKind(SyntaxKind.StringLiteral) && isInteractionKey(name)) {
|
|
171
|
+
map[name] = initializer.getLiteralValue();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return map;
|
|
175
|
+
}
|
|
176
|
+
function parseInteractionColor(obj, themeName) {
|
|
177
|
+
const swatch = {};
|
|
178
|
+
for (const prop of obj.getProperties()) {
|
|
179
|
+
if (!prop.isKind(SyntaxKind.PropertyAssignment))
|
|
180
|
+
continue;
|
|
181
|
+
const name = prop.getName();
|
|
182
|
+
const initializer = prop.getInitializer();
|
|
183
|
+
if (isInteractionKey(name)) {
|
|
184
|
+
throw new Error(`Surface theme "${themeName}" uses the old flat \`interactionColor\` shape. It now takes a swatch: ` +
|
|
185
|
+
`{ color: { default, hover, focus, active, disabled }, onColor?, inkColor? }. ` +
|
|
186
|
+
`Run \`nx g @ethlete/core:migrate-surface-interaction-swatch\` to convert your themes.`);
|
|
187
|
+
}
|
|
188
|
+
if (!initializer?.isKind(SyntaxKind.ObjectLiteralExpression) || !isSwatchKey(name))
|
|
189
|
+
continue;
|
|
190
|
+
const map = parseInteractionColorMap(initializer);
|
|
191
|
+
if (name === 'color') {
|
|
192
|
+
if (map.default && map.hover && map.focus && map.active && map.disabled) {
|
|
193
|
+
swatch.color = map;
|
|
194
|
+
}
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (map.default) {
|
|
198
|
+
swatch[name] = map;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return swatch.color ? swatch : undefined;
|
|
202
|
+
}
|
|
161
203
|
function parseSurfaceThemeObject(obj) {
|
|
162
204
|
const properties = obj.getProperties();
|
|
163
205
|
const theme = {};
|
|
206
|
+
let interactionColorObj = null;
|
|
164
207
|
for (const prop of properties) {
|
|
165
208
|
if (!prop.isKind(SyntaxKind.PropertyAssignment)) {
|
|
166
209
|
continue;
|
|
@@ -177,24 +220,7 @@ function parseSurfaceThemeObject(obj) {
|
|
|
177
220
|
continue;
|
|
178
221
|
}
|
|
179
222
|
if (propName === 'interactionColor' && initializer.isKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
180
|
-
|
|
181
|
-
const interactionColor = {};
|
|
182
|
-
for (const iProp of interactionProps) {
|
|
183
|
-
if (!iProp.isKind(SyntaxKind.PropertyAssignment))
|
|
184
|
-
continue;
|
|
185
|
-
const iPropName = iProp.getName();
|
|
186
|
-
const iInit = iProp.getInitializer();
|
|
187
|
-
if (iInit?.isKind(SyntaxKind.StringLiteral) && isInteractionKey(iPropName)) {
|
|
188
|
-
interactionColor[iPropName] = iInit.getLiteralValue();
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
if (interactionColor.default &&
|
|
192
|
-
interactionColor.hover &&
|
|
193
|
-
interactionColor.focus &&
|
|
194
|
-
interactionColor.active &&
|
|
195
|
-
interactionColor.disabled) {
|
|
196
|
-
theme.interactionColor = interactionColor;
|
|
197
|
-
}
|
|
223
|
+
interactionColorObj = initializer;
|
|
198
224
|
continue;
|
|
199
225
|
}
|
|
200
226
|
if (propName === 'elevation') {
|
|
@@ -227,6 +253,9 @@ function parseSurfaceThemeObject(obj) {
|
|
|
227
253
|
!theme.border) {
|
|
228
254
|
throw new Error('SurfaceTheme must have name, type, elevation, background, color, colorMuted, colorSubtle, and border properties');
|
|
229
255
|
}
|
|
256
|
+
if (interactionColorObj) {
|
|
257
|
+
theme.interactionColor = parseInteractionColor(interactionColorObj, theme.name);
|
|
258
|
+
}
|
|
230
259
|
return theme;
|
|
231
260
|
}
|
|
232
261
|
// Makes the named theme the sole default of its surface type, overriding any isDefault
|
|
@@ -322,11 +351,14 @@ function generateSurfaceThemeCss(themes, utilityPrefix, runtimePrefix, schema) {
|
|
|
322
351
|
tailwindVars.push(` --color-${utilityPrefix}-surface-${name}-subtle: rgb(${theme.colorSubtle});`);
|
|
323
352
|
tailwindVars.push(` --color-${utilityPrefix}-surface-${name}-border: rgb(${theme.border});`);
|
|
324
353
|
if (theme.interactionColor) {
|
|
325
|
-
|
|
326
|
-
tailwindVars
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
354
|
+
const { color, onColor, inkColor } = theme.interactionColor;
|
|
355
|
+
pushStaticInteractionColors(tailwindVars, `--color-${utilityPrefix}-surface-${name}-interaction`, color);
|
|
356
|
+
if (onColor) {
|
|
357
|
+
pushStaticInteractionColors(tailwindVars, `--color-${utilityPrefix}-surface-${name}-on-interaction`, onColor);
|
|
358
|
+
}
|
|
359
|
+
if (inkColor) {
|
|
360
|
+
pushStaticInteractionColors(tailwindVars, `--color-${utilityPrefix}-surface-${name}-interaction-ink`, inkColor);
|
|
361
|
+
}
|
|
330
362
|
}
|
|
331
363
|
tailwindVars.push('');
|
|
332
364
|
}
|
|
@@ -420,11 +452,39 @@ ${tailwindVars.join('\n')}
|
|
|
420
452
|
${themeVars.join('\n')}
|
|
421
453
|
}
|
|
422
454
|
|
|
423
|
-
${aliasBlock}
|
|
455
|
+
${aliasBlock}
|
|
456
|
+
${buildSurfaceColorScope(runtimePrefix)}`;
|
|
457
|
+
}
|
|
458
|
+
// The reserved `surface` color scope. It bridges the two theming systems - color variables out of
|
|
459
|
+
// surface variables - so `[etProvideColor]="'surface'"` renders a component in the neutral of
|
|
460
|
+
// whatever surface it sits on. It lives here rather than in the SDK's own CSS because a stylesheet
|
|
461
|
+
// shipped with `ProvideColorDirective` would land in every bundle that themes anything, and only
|
|
462
|
+
// apps that use the scope should pay for it.
|
|
463
|
+
function buildSurfaceColorScope(runtimePrefix) {
|
|
464
|
+
const tint = `var(--${runtimePrefix}-surface-interaction, var(--${runtimePrefix}-surface-color))`;
|
|
465
|
+
const on = `var(--${runtimePrefix}-surface-on-interaction, var(--${runtimePrefix}-surface-background))`;
|
|
466
|
+
const ink = `var(--${runtimePrefix}-surface-interaction-ink, var(--${runtimePrefix}-surface-color))`;
|
|
467
|
+
const state = (name, source, fallback) => INTERACTION_STATE_SUFFIXES.map((suffix) => ` --${runtimePrefix}-color-${name}${suffix}: ${suffix ? `var(--${runtimePrefix}-surface-${source}${suffix}, ${fallback})` : fallback};`).join('\n');
|
|
468
|
+
return `/* The reserved \`surface\` color: resolves the color tokens from the ambient surface's neutral swatch. */
|
|
469
|
+
@layer base {
|
|
470
|
+
.${runtimePrefix}-color--surface {
|
|
471
|
+
${state('primary', 'interaction', tint)}
|
|
472
|
+
|
|
473
|
+
${state('on-primary', 'on-interaction', on)}
|
|
474
|
+
|
|
475
|
+
${state('primary-ink', 'interaction-ink', ink)}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
`;
|
|
424
479
|
}
|
|
425
480
|
// Dynamic surface color declarations (unindented). Emitted in `@theme` (so Tailwind generates
|
|
426
481
|
// the utilities) and re-declared on every surface selector (so the utilities resolve per scope).
|
|
427
482
|
function buildDynamicSurfaceColors(utilityPrefix, runtimePrefix) {
|
|
483
|
+
// `on-interaction` and `interaction-ink` are optional per theme, so each falls back to the
|
|
484
|
+
// surface's own background/color - the same defaults the `.<runtimePrefix>-color--surface`
|
|
485
|
+
// bridge applies.
|
|
486
|
+
const onFallback = `var(--${runtimePrefix}-surface-background)`;
|
|
487
|
+
const inkFallback = `var(--${runtimePrefix}-surface-color)`;
|
|
428
488
|
return [
|
|
429
489
|
`--color-${utilityPrefix}-surface-bg: rgb(var(--${runtimePrefix}-surface-background));`,
|
|
430
490
|
`--color-${utilityPrefix}-surface: rgb(var(--${runtimePrefix}-surface-color));`,
|
|
@@ -436,8 +496,35 @@ function buildDynamicSurfaceColors(utilityPrefix, runtimePrefix) {
|
|
|
436
496
|
`--color-${utilityPrefix}-surface-interaction-focus: rgb(var(--${runtimePrefix}-surface-interaction-focus));`,
|
|
437
497
|
`--color-${utilityPrefix}-surface-interaction-active: rgb(var(--${runtimePrefix}-surface-interaction-active));`,
|
|
438
498
|
`--color-${utilityPrefix}-surface-interaction-disabled: rgb(var(--${runtimePrefix}-surface-interaction-disabled));`,
|
|
499
|
+
...INTERACTION_STATE_SUFFIXES.map((suffix) => `--color-${utilityPrefix}-surface-on-interaction${suffix}: rgb(var(--${runtimePrefix}-surface-on-interaction${suffix}, ${onFallback}));`),
|
|
500
|
+
...INTERACTION_STATE_SUFFIXES.map((suffix) => `--color-${utilityPrefix}-surface-interaction-ink${suffix}: rgb(var(--${runtimePrefix}-surface-interaction-ink${suffix}, ${inkFallback}));`),
|
|
439
501
|
];
|
|
440
502
|
}
|
|
503
|
+
const INTERACTION_STATE_SUFFIXES = ['', '-hover', '-focus', '-active', '-disabled'];
|
|
504
|
+
// Fills the optional states from the ones that are set, matching how the color theme generator
|
|
505
|
+
// resolves a swatch: hover falls back to default, focus to hover, active and disabled to default.
|
|
506
|
+
function resolveInteractionStates(map) {
|
|
507
|
+
const hover = map.hover || map.default;
|
|
508
|
+
return {
|
|
509
|
+
'': map.default,
|
|
510
|
+
'-hover': hover,
|
|
511
|
+
'-focus': map.focus || hover,
|
|
512
|
+
'-active': map.active || map.default,
|
|
513
|
+
'-disabled': map.disabled || map.default,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function pushStaticInteractionColors(vars, name, map) {
|
|
517
|
+
const states = resolveInteractionStates(map);
|
|
518
|
+
for (const suffix of INTERACTION_STATE_SUFFIXES) {
|
|
519
|
+
vars.push(` ${name}${suffix}: rgb(${states[suffix]});`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
function pushRuntimeInteractionVars(vars, name, map, indent) {
|
|
523
|
+
const states = resolveInteractionStates(map);
|
|
524
|
+
for (const suffix of INTERACTION_STATE_SUFFIXES) {
|
|
525
|
+
vars.push(`${indent}${name}${suffix}: ${states[suffix]};`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
441
528
|
function pushSurfaceVars(vars, runtimePrefix, theme, indent) {
|
|
442
529
|
vars.push(`${indent}--${runtimePrefix}-surface-background: ${theme.background};`);
|
|
443
530
|
vars.push(`${indent}--${runtimePrefix}-surface-color: ${theme.color};`);
|
|
@@ -447,11 +534,14 @@ function pushSurfaceVars(vars, runtimePrefix, theme, indent) {
|
|
|
447
534
|
vars.push(`${indent}--${runtimePrefix}-surface-type: ${theme.type};`);
|
|
448
535
|
vars.push(`${indent}--${runtimePrefix}-surface-elevation: ${theme.elevation};`);
|
|
449
536
|
if (theme.interactionColor) {
|
|
450
|
-
|
|
451
|
-
vars
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
537
|
+
const { color, onColor, inkColor } = theme.interactionColor;
|
|
538
|
+
pushRuntimeInteractionVars(vars, `--${runtimePrefix}-surface-interaction`, color, indent);
|
|
539
|
+
if (onColor) {
|
|
540
|
+
pushRuntimeInteractionVars(vars, `--${runtimePrefix}-surface-on-interaction`, onColor, indent);
|
|
541
|
+
}
|
|
542
|
+
if (inkColor) {
|
|
543
|
+
pushRuntimeInteractionVars(vars, `--${runtimePrefix}-surface-interaction-ink`, inkColor, indent);
|
|
544
|
+
}
|
|
455
545
|
}
|
|
456
546
|
}
|
|
457
547
|
function findMainStylesFile(tree) {
|