@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.45 → 2.0.0-next.49
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/.storybook/ComponentPreview.tsx +26 -0
- package/.storybook/PreviewTemplate.tsx +99 -0
- package/.storybook/action-handler.ts +81 -0
- package/.storybook/channels.ts +44 -0
- package/.storybook/main.ts +313 -0
- package/.storybook/manager.ts +71 -0
- package/.storybook/openbridgeTheme.ts +197 -0
- package/.storybook/preview-head.html +15 -0
- package/.storybook/preview.tsx +129 -0
- package/.storybook/vitest.setup.ts +23 -0
- package/bundle/openbridge-webcomponents.bundle.js +14869 -14889
- package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
- package/custom-elements.json +81 -82
- package/dist/components/badge/badge.css.js +15 -15
- package/dist/components/icon-button/icon-button.d.ts +4 -0
- package/dist/components/icon-button/icon-button.d.ts.map +1 -1
- package/dist/components/icon-button/icon-button.js.map +1 -1
- package/dist/navigation-instruments/compass/compass.d.ts +3 -3
- package/dist/navigation-instruments/compass/compass.d.ts.map +1 -1
- package/dist/navigation-instruments/compass/compass.js +3 -3
- package/dist/navigation-instruments/compass/compass.js.map +1 -1
- package/dist/navigation-instruments/compass-indicator/compass-indicator.d.ts +12 -0
- package/dist/navigation-instruments/compass-indicator/compass-indicator.d.ts.map +1 -1
- package/dist/navigation-instruments/compass-indicator/compass-indicator.js +9 -1
- package/dist/navigation-instruments/compass-indicator/compass-indicator.js.map +1 -1
- package/dist/navigation-instruments/velocity-projection-plot/velocity-projection-plot.d.ts +4 -4
- package/dist/navigation-instruments/velocity-projection-plot/velocity-projection-plot.d.ts.map +1 -1
- package/dist/navigation-instruments/velocity-projection-plot/velocity-projection-plot.js +15 -14
- package/dist/navigation-instruments/velocity-projection-plot/velocity-projection-plot.js.map +1 -1
- package/dist/navigation-instruments/watch/environment.d.ts +39 -1
- package/dist/navigation-instruments/watch/environment.d.ts.map +1 -1
- package/dist/navigation-instruments/watch/environment.js +71 -88
- package/dist/navigation-instruments/watch/environment.js.map +1 -1
- package/dist/navigation-instruments/watch/watch.d.ts +1 -1
- package/dist/navigation-instruments/watch/watch.d.ts.map +1 -1
- package/dist/navigation-instruments/watch/watch.js +4 -4
- package/dist/navigation-instruments/watch/watch.js.map +1 -1
- package/dist/navigation-instruments/wind/wind.d.ts +1 -1
- package/dist/navigation-instruments/wind/wind.d.ts.map +1 -1
- package/dist/navigation-instruments/wind/wind.js +3 -3
- package/dist/navigation-instruments/wind/wind.js.map +1 -1
- package/dist/navigation-instruments/wind-indicator/wind-indicator.d.ts +63 -9
- package/dist/navigation-instruments/wind-indicator/wind-indicator.d.ts.map +1 -1
- package/dist/navigation-instruments/wind-indicator/wind-indicator.js +57 -1995
- package/dist/navigation-instruments/wind-indicator/wind-indicator.js.map +1 -1
- package/dist/navigation-instruments/wind-propulsion/wind-propulsion.d.ts +1 -1
- package/dist/navigation-instruments/wind-propulsion/wind-propulsion.d.ts.map +1 -1
- package/dist/navigation-instruments/wind-propulsion/wind-propulsion.js +4 -4
- package/dist/navigation-instruments/wind-propulsion/wind-propulsion.js.map +1 -1
- package/eslint.config.mjs +589 -0
- package/fix-imports.mjs +185 -0
- package/fix-js-extensions.mjs +44 -0
- package/lit-localize.json +15 -0
- package/new-component.ts +148 -0
- package/package.json +56 -3
- package/postcss.config.mjs +195 -0
- package/script/check-css-mixins.ts +191 -0
- package/script/check-css-variables.ts +280 -0
- package/script/convert-icons.ts +202 -0
- package/script/convert-vessel-svg-to-ts.ts +110 -0
- package/script/docgen/README.md +95 -0
- package/script/docgen/docs-gen.ts +225 -0
- package/script/docgen/prompt-system.txt +187 -0
- package/script/download-alert-icons.ts +193 -0
- package/script/download-icons.ts +314 -0
- package/script/figmavariables.json +139 -0
- package/script/generate-bundle-entry.ts +77 -0
- package/script/prepare-full-bundle.ts +105 -0
- package/script/sort-custom-element-manifest.ts +67 -0
- package/src/components/badge/badge.css +15 -15
- package/src/components/badge/badge.stories.ts +2 -0
- package/src/components/icon-button/icon-button.ts +4 -0
- package/src/navigation-instruments/compass/compass.stories.ts +5 -2
- package/src/navigation-instruments/compass/compass.ts +4 -4
- package/src/navigation-instruments/compass-indicator/compass-indicator.stories.ts +34 -1
- package/src/navigation-instruments/compass-indicator/compass-indicator.ts +19 -1
- package/src/navigation-instruments/velocity-projection-plot/velocity-projection-plot.stories.ts +13 -13
- package/src/navigation-instruments/velocity-projection-plot/velocity-projection-plot.ts +15 -11
- package/src/navigation-instruments/watch/environment.ts +120 -87
- package/src/navigation-instruments/watch/watch.stories.ts +2 -2
- package/src/navigation-instruments/watch/watch.ts +3 -3
- package/src/navigation-instruments/wind/wind.stories.ts +4 -3
- package/src/navigation-instruments/wind/wind.ts +2 -2
- package/src/navigation-instruments/wind-indicator/wind-indicator.stories.ts +22 -12
- package/src/navigation-instruments/wind-indicator/wind-indicator.ts +91 -46
- package/src/navigation-instruments/wind-propulsion/wind-propulsion.stories.ts +3 -3
- package/src/navigation-instruments/wind-propulsion/wind-propulsion.ts +3 -3
- package/tsconfig.json +38 -0
- package/vite.config.ts +107 -0
- package/vitest.browser.config.ts +14 -0
- package/vitest.config.ts +51 -0
- package/xliff/es-419.xlf +111 -0
- package/xliff/fi-FI.xlf +139 -0
- package/dist/NotoSans.ttf +0 -0
- package/dist/oicl.svg +0 -4
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module CSSMixinAudit
|
|
3
|
+
* @description
|
|
4
|
+
* This script audits CSS mixins within the project's source directory. It performs a comprehensive
|
|
5
|
+
* scan of all CSS files to match mixin definitions (@define-mixin) against their usages (@mixin).
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Scans all CSS files under the src directory (using glob src/\*\* / *.css).
|
|
9
|
+
* - Identifies unique mixin definitions and their locations.
|
|
10
|
+
* - Identifies unique mixin usages and their locations.
|
|
11
|
+
* - Detects duplicate definitions (warning).
|
|
12
|
+
* - Detects usages of undefined mixins (error).
|
|
13
|
+
* - Detects unused mixin definitions (warning).
|
|
14
|
+
* - Exits with code 1 if any undefined mixin usages are found, suitable for CI pipelines.
|
|
15
|
+
*
|
|
16
|
+
* Usage Examples:
|
|
17
|
+
* ```bash
|
|
18
|
+
* # Run via npm script
|
|
19
|
+
* npm run lint:mixins
|
|
20
|
+
*
|
|
21
|
+
* # Run directly using tsx
|
|
22
|
+
* tsx script/check-css-mixins.ts
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* Expected Output:
|
|
26
|
+
* ```
|
|
27
|
+
* CSS mixin audit report
|
|
28
|
+
* Scanned 73 CSS files under src/
|
|
29
|
+
* Found 73 unique mixin definitions and 41 unique mixin usages
|
|
30
|
+
*
|
|
31
|
+
* Defined mixins
|
|
32
|
+
* - font-label (1) => src/mixins/fonts.css:1
|
|
33
|
+
* ...
|
|
34
|
+
*
|
|
35
|
+
* ✅ CSS mixin audit passed: no undefined mixin usages found.
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* Note: This module is intended to be run as a standalone script. It does not export any public utilities.
|
|
39
|
+
* It utilizes internal functions to process file content using fs, path, and globby.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import fs from 'fs';
|
|
43
|
+
import path from 'path';
|
|
44
|
+
import {globby} from 'globby';
|
|
45
|
+
|
|
46
|
+
interface MixinDefinitionLocation {
|
|
47
|
+
file: string;
|
|
48
|
+
line: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface MixinUsageLocation {
|
|
52
|
+
file: string;
|
|
53
|
+
line: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const jsDefinedMixins = new Set(['style']);
|
|
57
|
+
|
|
58
|
+
function getLineNumber(text: string, index: number): number {
|
|
59
|
+
return text.slice(0, index).split('\n').length;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function formatLocation(location: {file: string; line: number}): string {
|
|
63
|
+
return `${path.relative(process.cwd(), location.file)}:${location.line}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function printSection(title: string, lines: string[]): void {
|
|
67
|
+
console.log(`\n${title}`);
|
|
68
|
+
if (lines.length === 0) {
|
|
69
|
+
console.log(' - none');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const line of lines) {
|
|
74
|
+
console.log(` - ${line}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function run(): Promise<void> {
|
|
79
|
+
const cssFiles = await globby('src/**/*.css', {
|
|
80
|
+
cwd: process.cwd(),
|
|
81
|
+
absolute: true,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const definitions = new Map<string, MixinDefinitionLocation[]>();
|
|
85
|
+
const usages = new Map<string, MixinUsageLocation[]>();
|
|
86
|
+
|
|
87
|
+
for (const file of cssFiles) {
|
|
88
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
89
|
+
|
|
90
|
+
const defineRegex = /@define-mixin\s+([A-Za-z0-9_-]+)/g;
|
|
91
|
+
for (const match of content.matchAll(defineRegex)) {
|
|
92
|
+
const name = match[1];
|
|
93
|
+
const index = match.index;
|
|
94
|
+
if (index == null) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const location = {file, line: getLineNumber(content, index)};
|
|
98
|
+
definitions.set(name, [...(definitions.get(name) ?? []), location]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const usageRegex = /@mixin\s+([A-Za-z0-9_-]+)/g;
|
|
102
|
+
for (const match of content.matchAll(usageRegex)) {
|
|
103
|
+
const name = match[1];
|
|
104
|
+
const index = match.index;
|
|
105
|
+
if (index == null) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const location = {file, line: getLineNumber(content, index)};
|
|
109
|
+
usages.set(name, [...(usages.get(name) ?? []), location]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const definedNames = [...definitions.keys()].sort();
|
|
114
|
+
const usedNames = [...usages.keys()].sort();
|
|
115
|
+
|
|
116
|
+
const duplicateDefinitions = definedNames.filter(
|
|
117
|
+
(name) => (definitions.get(name)?.length ?? 0) > 1
|
|
118
|
+
);
|
|
119
|
+
const undefinedUsages = usedNames.filter(
|
|
120
|
+
(name) => !definitions.has(name) && !jsDefinedMixins.has(name)
|
|
121
|
+
);
|
|
122
|
+
const unusedDefinitions = definedNames.filter((name) => !usages.has(name));
|
|
123
|
+
|
|
124
|
+
const definitionLines = definedNames.map((name) => {
|
|
125
|
+
const entries = definitions.get(name) ?? [];
|
|
126
|
+
return `${name} (${entries.length}) => ${entries
|
|
127
|
+
.map((entry) => formatLocation(entry))
|
|
128
|
+
.join(', ')}`;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const usageLines = usedNames.map((name) => {
|
|
132
|
+
const entries = usages.get(name) ?? [];
|
|
133
|
+
return `${name} (${entries.length}) => ${entries
|
|
134
|
+
.map((entry) => formatLocation(entry))
|
|
135
|
+
.join(', ')}`;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const duplicateDefinitionLines = duplicateDefinitions.map((name) => {
|
|
139
|
+
const entries = definitions.get(name) ?? [];
|
|
140
|
+
return `${name} (${entries.length}) => ${entries
|
|
141
|
+
.map((entry) => formatLocation(entry))
|
|
142
|
+
.join(', ')}`;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const undefinedUsageLines = undefinedUsages.map((name) => {
|
|
146
|
+
const entries = usages.get(name) ?? [];
|
|
147
|
+
return `${name} (${entries.length}) => ${entries
|
|
148
|
+
.map((entry) => formatLocation(entry))
|
|
149
|
+
.join(', ')}`;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const unusedDefinitionLines = unusedDefinitions.map((name) => {
|
|
153
|
+
const entries = definitions.get(name) ?? [];
|
|
154
|
+
return `${name} (${entries.length}) => ${entries
|
|
155
|
+
.map((entry) => formatLocation(entry))
|
|
156
|
+
.join(', ')}`;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
console.log('CSS mixin audit report');
|
|
160
|
+
console.log(`Scanned ${cssFiles.length} CSS files under src/`);
|
|
161
|
+
console.log(
|
|
162
|
+
`Found ${definedNames.length} unique mixin definitions and ${usedNames.length} unique mixin usages`
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
printSection('Defined mixins', definitionLines);
|
|
166
|
+
printSection('Used mixins', usageLines);
|
|
167
|
+
printSection(
|
|
168
|
+
'Duplicate mixin definitions (warning)',
|
|
169
|
+
duplicateDefinitionLines
|
|
170
|
+
);
|
|
171
|
+
printSection('Used but undefined mixins (error)', undefinedUsageLines);
|
|
172
|
+
printSection('Defined but unused mixins (warning)', unusedDefinitionLines);
|
|
173
|
+
|
|
174
|
+
if (undefinedUsages.length > 0) {
|
|
175
|
+
console.error(
|
|
176
|
+
`\n❌ CSS mixin audit failed: ${undefinedUsages.length} undefined mixin name(s) found.`
|
|
177
|
+
);
|
|
178
|
+
process.exitCode = 1;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log('\n✅ CSS mixin audit passed: no undefined mixin usages found.');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
run().catch((error: unknown) => {
|
|
186
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
187
|
+
console.error(
|
|
188
|
+
`❌ CSS mixin audit failed with an unexpected error: ${message}`
|
|
189
|
+
);
|
|
190
|
+
process.exitCode = 1;
|
|
191
|
+
});
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module CSSVariableAudit
|
|
3
|
+
* @description
|
|
4
|
+
* This script audits CSS variables within the project's source directory. It scans all CSS files
|
|
5
|
+
* to identify variable definitions (both standard `--var: value;` and `@property` rules)
|
|
6
|
+
* and their usages via the `var()` function.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Scans all CSS files under the src directory (using glob src/\*\* / *.css).
|
|
10
|
+
* - Identifies unique variable definitions and their locations.
|
|
11
|
+
* - Identifies unique variable usages and their locations (including fallback detection).
|
|
12
|
+
* - Detects duplicate variable definitions (warning).
|
|
13
|
+
* - Detects usages of undefined variables (error), excluding a predefined set of allowed undefined variables.
|
|
14
|
+
* - Skips `src/palettes/variables.css` for usage checks as it is considered a definition-only file.
|
|
15
|
+
* - Exits with code 1 if any undefined variable usages (without fallbacks) are found.
|
|
16
|
+
*
|
|
17
|
+
* Usage Examples:
|
|
18
|
+
* ```bash
|
|
19
|
+
* # Run via npm script
|
|
20
|
+
* npm run lint:variables
|
|
21
|
+
*
|
|
22
|
+
* # Run directly using tsx
|
|
23
|
+
* tsx script/check-css-variables.ts
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* Expected Output:
|
|
27
|
+
* ```
|
|
28
|
+
* CSS variable audit report
|
|
29
|
+
* Scanned 195 CSS files under src/
|
|
30
|
+
* Found 2755 unique variable definitions and 1103 unique variable usages
|
|
31
|
+
*
|
|
32
|
+
* Defined variables
|
|
33
|
+
* - --ob-color-primary (1) => src/palettes/variables.css:10
|
|
34
|
+
* ...
|
|
35
|
+
*
|
|
36
|
+
* ✅ CSS variable audit passed: no undefined variable usages found.
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* Note: This module is intended to be run as a standalone script. It does not export any public utilities.
|
|
40
|
+
* It utilizes internal functions to process file content using fs, path, and globby.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import fs from 'fs';
|
|
44
|
+
import path from 'path';
|
|
45
|
+
import {globby} from 'globby';
|
|
46
|
+
|
|
47
|
+
const allowedUndefinedVariables = new Set([
|
|
48
|
+
'--alert-components-badge-label-spacing',
|
|
49
|
+
'--app-components-alert-components-general-notification-item-icon-size-large',
|
|
50
|
+
'--app-components-badge-border-radius',
|
|
51
|
+
'--app-components-badge-icon-size',
|
|
52
|
+
'--app-components-badge-icon-size-flat',
|
|
53
|
+
'--app-components-badge-icon-size-large',
|
|
54
|
+
'--app-components-badge-label-spacing',
|
|
55
|
+
'--app-components-badge-min-size-large',
|
|
56
|
+
'--app-components-badge-padding',
|
|
57
|
+
'--app-components-global-alert-item-counter-spacing',
|
|
58
|
+
'--color-container-background-color',
|
|
59
|
+
'--course',
|
|
60
|
+
'--critical-enabled-background-color',
|
|
61
|
+
'--critical-enabled-border-color',
|
|
62
|
+
'--critical-focused-background-color',
|
|
63
|
+
'--critical-focused-border-color',
|
|
64
|
+
'--critical-hover-background-color',
|
|
65
|
+
'--critical-hover-border-color',
|
|
66
|
+
'--critical-pressed-background-color',
|
|
67
|
+
'--critical-pressed-border-color',
|
|
68
|
+
'--custom-width',
|
|
69
|
+
'--enabled-background-color',
|
|
70
|
+
'--global-size-spacing-list-item-menu-padding-horizonal-list-item',
|
|
71
|
+
'--grid-columns',
|
|
72
|
+
'--grid-columns-rest',
|
|
73
|
+
'--heading',
|
|
74
|
+
'--image-size',
|
|
75
|
+
'--menu-navigation-components-date-item-padding-vertical',
|
|
76
|
+
'--menu-navigation-components-tab-item-divider-height',
|
|
77
|
+
'--obc-instrument-field-source-width',
|
|
78
|
+
'--obc-instrument-field-tag-width',
|
|
79
|
+
'--on-container-active-color',
|
|
80
|
+
'--percent',
|
|
81
|
+
'--scale',
|
|
82
|
+
'--spinner-progress-deg',
|
|
83
|
+
'--ui-components-icon-toggle-button-horizontal-item-touch-target-size',
|
|
84
|
+
'--ui-components-keyboard-nummeric-component-height-min',
|
|
85
|
+
'--ui-components-keyboard-nummeric-touch-target-size',
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
interface VariableDefinitionLocation {
|
|
89
|
+
file: string;
|
|
90
|
+
line: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface VariableUsageLocation {
|
|
94
|
+
file: string;
|
|
95
|
+
line: number;
|
|
96
|
+
hasFallback: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getLineNumber(text: string, index: number): number {
|
|
100
|
+
return text.slice(0, index).split('\n').length;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function formatLocation(location: {file: string; line: number}): string {
|
|
104
|
+
return `${path.relative(process.cwd(), location.file)}:${location.line}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function printSection(title: string, lines: string[]): void {
|
|
108
|
+
console.log(`\n${title}`);
|
|
109
|
+
if (lines.length === 0) {
|
|
110
|
+
console.log(' - none');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const line of lines) {
|
|
115
|
+
console.log(` - ${line}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function run(): Promise<void> {
|
|
120
|
+
const cssFiles = await globby('src/**/*.css', {
|
|
121
|
+
cwd: process.cwd(),
|
|
122
|
+
absolute: true,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const definitions = new Map<string, VariableDefinitionLocation[]>();
|
|
126
|
+
const usages = new Map<string, VariableUsageLocation[]>();
|
|
127
|
+
|
|
128
|
+
for (const file of cssFiles) {
|
|
129
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
130
|
+
const isPaletteFile = file.endsWith(
|
|
131
|
+
`${path.sep}src${path.sep}palettes${path.sep}variables.css`
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const definitionRegex = /(^|[;{\s])(\-\-[A-Za-z0-9_-]+)\s*:/gm;
|
|
135
|
+
for (const match of content.matchAll(definitionRegex)) {
|
|
136
|
+
const name = match[2];
|
|
137
|
+
const index = match.index;
|
|
138
|
+
if (index == null) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const location = {file, line: getLineNumber(content, index)};
|
|
142
|
+
definitions.set(name, [...(definitions.get(name) ?? []), location]);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const propertyDefinitionRegex = /@property\s+(\-\-[A-Za-z0-9_-]+)/g;
|
|
146
|
+
for (const match of content.matchAll(propertyDefinitionRegex)) {
|
|
147
|
+
const name = match[1];
|
|
148
|
+
const index = match.index;
|
|
149
|
+
if (index == null) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const location = {file, line: getLineNumber(content, index)};
|
|
153
|
+
definitions.set(name, [...(definitions.get(name) ?? []), location]);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (isPaletteFile) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let start = 0;
|
|
161
|
+
while ((start = content.indexOf('var(', start)) !== -1) {
|
|
162
|
+
let openParens = 1;
|
|
163
|
+
let end = start + 4;
|
|
164
|
+
let commaIndex = -1;
|
|
165
|
+
while (openParens > 0 && end < content.length) {
|
|
166
|
+
if (content[end] === '(') {
|
|
167
|
+
openParens++;
|
|
168
|
+
} else if (content[end] === ')') {
|
|
169
|
+
openParens--;
|
|
170
|
+
} else if (content[end] === ',' && openParens === 1) {
|
|
171
|
+
if (commaIndex === -1) {
|
|
172
|
+
commaIndex = end;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
end++;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (openParens === 0) {
|
|
179
|
+
const firstArg =
|
|
180
|
+
commaIndex !== -1
|
|
181
|
+
? content.slice(start + 4, commaIndex)
|
|
182
|
+
: content.slice(start + 4, end - 1);
|
|
183
|
+
|
|
184
|
+
const nameMatch = firstArg.trim().match(/^--[A-Za-z0-9_-]+/);
|
|
185
|
+
if (nameMatch) {
|
|
186
|
+
const name = nameMatch[0];
|
|
187
|
+
const hasFallback = commaIndex !== -1;
|
|
188
|
+
const location = {
|
|
189
|
+
file,
|
|
190
|
+
line: getLineNumber(content, start),
|
|
191
|
+
hasFallback,
|
|
192
|
+
};
|
|
193
|
+
usages.set(name, [...(usages.get(name) ?? []), location]);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
start += 4; // Move past 'var(' to find next occurrence (including nested ones)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const definedNames = [...definitions.keys()].sort();
|
|
201
|
+
const usedNames = [...usages.keys()].sort();
|
|
202
|
+
|
|
203
|
+
const duplicateDefinitions = definedNames.filter(
|
|
204
|
+
(name) => (definitions.get(name)?.length ?? 0) > 1
|
|
205
|
+
);
|
|
206
|
+
const undefinedUsages = usedNames.filter((name) => {
|
|
207
|
+
if (allowedUndefinedVariables.has(name)) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (definitions.has(name)) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const entries = usages.get(name) ?? [];
|
|
216
|
+
return entries.some((entry) => !entry.hasFallback);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const definitionLines = definedNames.map((name) => {
|
|
220
|
+
const entries = definitions.get(name) ?? [];
|
|
221
|
+
return `${name} (${entries.length}) => ${entries
|
|
222
|
+
.map((entry) => formatLocation(entry))
|
|
223
|
+
.join(', ')}`;
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const usageLines = usedNames.map((name) => {
|
|
227
|
+
const entries = usages.get(name) ?? [];
|
|
228
|
+
return `${name} (${entries.length}) => ${entries
|
|
229
|
+
.map((entry) => formatLocation(entry))
|
|
230
|
+
.join(', ')}`;
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const duplicateDefinitionLines = duplicateDefinitions.map((name) => {
|
|
234
|
+
const entries = definitions.get(name) ?? [];
|
|
235
|
+
return `${name} (${entries.length}) => ${entries
|
|
236
|
+
.map((entry) => formatLocation(entry))
|
|
237
|
+
.join(', ')}`;
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const undefinedUsageLines = undefinedUsages.map((name) => {
|
|
241
|
+
const entries = usages.get(name) ?? [];
|
|
242
|
+
return `${name} (${entries.length}) => ${entries
|
|
243
|
+
.map((entry) => formatLocation(entry))
|
|
244
|
+
.join(', ')}`;
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
console.log('CSS variable audit report');
|
|
248
|
+
console.log(`Scanned ${cssFiles.length} CSS files under src/`);
|
|
249
|
+
console.log(
|
|
250
|
+
`Found ${definedNames.length} unique variable definitions and ${usedNames.length} unique variable usages`
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
printSection('Defined variables', definitionLines);
|
|
254
|
+
printSection('Used variables', usageLines);
|
|
255
|
+
printSection(
|
|
256
|
+
'Duplicate variable definitions (warning)',
|
|
257
|
+
duplicateDefinitionLines
|
|
258
|
+
);
|
|
259
|
+
printSection('Used but undefined variables (error)', undefinedUsageLines);
|
|
260
|
+
|
|
261
|
+
if (undefinedUsages.length > 0) {
|
|
262
|
+
console.error(
|
|
263
|
+
`\n❌ CSS variable audit failed: ${undefinedUsages.length} undefined variable name(s) found.`
|
|
264
|
+
);
|
|
265
|
+
process.exitCode = 1;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
console.log(
|
|
270
|
+
'\n✅ CSS variable audit passed: no undefined variable usages found.'
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
run().catch((error: unknown) => {
|
|
275
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
276
|
+
console.error(
|
|
277
|
+
`❌ CSS variable audit failed with an unexpected error: ${message}`
|
|
278
|
+
);
|
|
279
|
+
process.exitCode = 1;
|
|
280
|
+
});
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type {Node, RGBA, Style} from '@figma/rest-api-spec';
|
|
2
|
+
import * as dotenv from 'dotenv';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
|
6
|
+
const figmaVariablesPath = path.join(__dirname, 'figmavariables.json');
|
|
7
|
+
const figmaVariables = JSON.parse(fs.readFileSync(figmaVariablesPath, 'utf8'));
|
|
8
|
+
|
|
9
|
+
dotenv.config();
|
|
10
|
+
|
|
11
|
+
export interface IconRef {
|
|
12
|
+
name: string;
|
|
13
|
+
id: string;
|
|
14
|
+
javascriptName: string;
|
|
15
|
+
styles: {[colorCode: string]: {cssClass: string}};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getSingleColorIcon(imageData: string, icon: IconRef): string {
|
|
19
|
+
// replace fill color with currentColor
|
|
20
|
+
const fillRegex = /fill="[^"]+"/g;
|
|
21
|
+
const replace = 'fill="currentColor"';
|
|
22
|
+
let imageDataNew = imageData.replace(fillRegex, replace);
|
|
23
|
+
|
|
24
|
+
// remove fillOpacity
|
|
25
|
+
const fillOpacityRegex = /fill-opacity="[^"]+"/g;
|
|
26
|
+
imageDataNew = imageDataNew.replace(fillOpacityRegex, '');
|
|
27
|
+
|
|
28
|
+
return imageDataNew;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getCssColorIcon(imageData: string, icon: IconRef): string {
|
|
32
|
+
// replace fill color with currentColor
|
|
33
|
+
const fillRegex = /fill="([^"]+)"/g;
|
|
34
|
+
|
|
35
|
+
const replace = (match: string, color: string) => {
|
|
36
|
+
const cssClass = icon.styles[color];
|
|
37
|
+
if (cssClass === undefined) {
|
|
38
|
+
if (color === 'black') return 'fill="currentColor"';
|
|
39
|
+
if (color === 'none') return 'fill="none"';
|
|
40
|
+
if (color === 'white') return 'fill="none"';
|
|
41
|
+
if (color === '#C90000')
|
|
42
|
+
return 'fill="var(--alarm-enabled-background-color)"';
|
|
43
|
+
console.warn(
|
|
44
|
+
'No css class for color',
|
|
45
|
+
color,
|
|
46
|
+
icon.name,
|
|
47
|
+
Object.keys(icon.styles)
|
|
48
|
+
);
|
|
49
|
+
return 'fill="currentColor"';
|
|
50
|
+
}
|
|
51
|
+
return `style="fill: var(--${cssClass.cssClass})"`;
|
|
52
|
+
};
|
|
53
|
+
imageData = imageData.replace(fillRegex, replace);
|
|
54
|
+
|
|
55
|
+
// remove fillOpacity
|
|
56
|
+
const fillOpacityRegex = /fill-opacity="[^"]+"/g;
|
|
57
|
+
imageData = imageData.replace(fillOpacityRegex, '');
|
|
58
|
+
|
|
59
|
+
// replace stroke color with currentColor
|
|
60
|
+
const strokeRegex = /stroke="([^"]+)"/g;
|
|
61
|
+
const replaceStroke = (match: string, color: string) => {
|
|
62
|
+
const cssClass = icon.styles[color];
|
|
63
|
+
if (cssClass === undefined) {
|
|
64
|
+
if (color === 'black') return 'stroke="currentColor"';
|
|
65
|
+
if (color === 'none') return 'stroke="none"';
|
|
66
|
+
if (color === 'white') return 'stroke="none"';
|
|
67
|
+
console.warn(
|
|
68
|
+
'No css class for color',
|
|
69
|
+
color,
|
|
70
|
+
icon.name,
|
|
71
|
+
Object.keys(icon.styles)
|
|
72
|
+
);
|
|
73
|
+
return 'stroke="currentColor"';
|
|
74
|
+
}
|
|
75
|
+
return `style="stroke: var(--${cssClass.cssClass})"`;
|
|
76
|
+
};
|
|
77
|
+
imageData = imageData.replace(strokeRegex, replaceStroke);
|
|
78
|
+
|
|
79
|
+
// remove strokeOpacity
|
|
80
|
+
const strokeOpacityRegex = /stroke-opacity="[^"]+"/g;
|
|
81
|
+
imageData = imageData.replace(strokeOpacityRegex, '');
|
|
82
|
+
|
|
83
|
+
return imageData;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function kebabToUpperCamelCase(kebabCase: string): string {
|
|
87
|
+
const words = kebabCase.replace(/ /g, '').split('-');
|
|
88
|
+
const upperCamelCase = words
|
|
89
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
90
|
+
.join('');
|
|
91
|
+
return upperCamelCase;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function getStylesForNode(
|
|
95
|
+
node: Node,
|
|
96
|
+
styles: {[styleId: string]: Style}
|
|
97
|
+
): {[colorCode: string]: {cssClass: string}} {
|
|
98
|
+
let out = {};
|
|
99
|
+
|
|
100
|
+
if ('children' in node) {
|
|
101
|
+
for (const child of node.children) {
|
|
102
|
+
out = {...out, ...getStylesForNode(child, styles)};
|
|
103
|
+
if ('fills' in child) {
|
|
104
|
+
let fils: string | undefined;
|
|
105
|
+
child.fills.forEach((fill) => {
|
|
106
|
+
if (fill.type === 'SOLID') {
|
|
107
|
+
if (fils !== undefined) {
|
|
108
|
+
console.warn(
|
|
109
|
+
'Multiple fills',
|
|
110
|
+
fils,
|
|
111
|
+
rgbaToHexOrColorName(fill.color!)
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
fils = rgbaToHexOrColorName(fill.color!);
|
|
115
|
+
if ('boundVariables' in fill) {
|
|
116
|
+
const variableId = fill.boundVariables.color.id;
|
|
117
|
+
out[fils] = {cssClass: figmaVariables[variableId]};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
if (fils !== undefined && child.styles && 'fill' in child.styles) {
|
|
122
|
+
const styleId = child.styles.fill as string;
|
|
123
|
+
const figmaStyle = styles[styleId];
|
|
124
|
+
const cssClass = styleToCssClass(figmaStyle);
|
|
125
|
+
out[fils] = {cssClass: cssClass};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if ('strokes' in child) {
|
|
129
|
+
let strokes: string;
|
|
130
|
+
child.strokes.forEach((stroke) => {
|
|
131
|
+
if (stroke.type === 'SOLID') {
|
|
132
|
+
if (strokes !== undefined) {
|
|
133
|
+
console.warn(
|
|
134
|
+
'Multiple strokes',
|
|
135
|
+
strokes,
|
|
136
|
+
rgbaToHexOrColorName(stroke.color!)
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
strokes = rgbaToHexOrColorName(stroke.color!);
|
|
140
|
+
if ('boundVariables' in stroke) {
|
|
141
|
+
const variableId = stroke.boundVariables.color.id;
|
|
142
|
+
out[strokes] = {cssClass: figmaVariables[variableId]};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
if (strokes !== undefined && child.styles?.stroke) {
|
|
147
|
+
const styleId = child.styles.stroke;
|
|
148
|
+
const figmaStyle = styles[styleId];
|
|
149
|
+
const cssClass = styleToCssClass(figmaStyle);
|
|
150
|
+
out[strokes] = {cssClass: cssClass};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if ('fillOverrideTable' in child) {
|
|
154
|
+
for (const fill of Object.values(child.fillOverrideTable)) {
|
|
155
|
+
if (fill === null) continue;
|
|
156
|
+
if ('fills' in fill) {
|
|
157
|
+
for (const f of fill.fills) {
|
|
158
|
+
if (f.type === 'SOLID') {
|
|
159
|
+
const color = rgbaToHexOrColorName(f.color!);
|
|
160
|
+
if ('boundVariables' in f) {
|
|
161
|
+
const variableId = f.boundVariables.color.id;
|
|
162
|
+
out[color] = {cssClass: figmaVariables[variableId]};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (!('inheritFillStyleId' in fill)) continue;
|
|
168
|
+
const styleId = fill.inheritFillStyleId;
|
|
169
|
+
const figmaStyle = styles[styleId];
|
|
170
|
+
const color = rgbaToHexOrColorName(fill.fills[0].color!);
|
|
171
|
+
const cssClass = styleToCssClass(figmaStyle);
|
|
172
|
+
out[color] = {cssClass: cssClass};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function decimalToHex(d: number): string {
|
|
182
|
+
const v = Math.round(d * 255).toString(16);
|
|
183
|
+
return v.length === 1 ? `0${v}` : v;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function rgbaToHexOrColorName(rgba: RGBA): string {
|
|
187
|
+
const isBlack = rgba.r === 0 && rgba.g === 0 && rgba.b === 0 && rgba.a === 1;
|
|
188
|
+
const isWhite = rgba.r === 1 && rgba.g === 1 && rgba.b === 1 && rgba.a === 1;
|
|
189
|
+
if (isBlack) {
|
|
190
|
+
return 'black';
|
|
191
|
+
} else if (isWhite) {
|
|
192
|
+
return 'white';
|
|
193
|
+
} else {
|
|
194
|
+
return `#${decimalToHex(rgba.r)}${decimalToHex(rgba.g)}${decimalToHex(
|
|
195
|
+
rgba.b
|
|
196
|
+
)}`.toUpperCase();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function styleToCssClass(style: Style): string {
|
|
201
|
+
return style.name.replace(/[\/ ]/g, '-').toLocaleLowerCase();
|
|
202
|
+
}
|