@acorex/styles 22.1.0-next.3 → 22.1.0-next.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acorex/styles",
3
- "version": "22.1.0-next.3",
3
+ "version": "22.1.0-next.4",
4
4
  "main": "index.css",
5
5
  "author": "Ali Safari",
6
6
  "license": "ISC",
@@ -1,81 +0,0 @@
1
- /**
2
- * Contract checks for the optional Depth appearance layer.
3
- * Run: node packages/styles/themes/appearance.contract.mjs
4
- */
5
- import { readFileSync } from 'node:fs';
6
- import { dirname, join } from 'node:path';
7
- import { fileURLToPath } from 'node:url';
8
-
9
- const __dirname = dirname(fileURLToPath(import.meta.url));
10
- const root = join(__dirname, '../../..');
11
-
12
- const defaultCss = readFileSync(join(__dirname, 'default.css'), 'utf8');
13
- const appearanceCss = readFileSync(join(__dirname, 'appearance.css'), 'utf8');
14
-
15
- let failed = 0;
16
-
17
- function assert(condition, message) {
18
- if (!condition) {
19
- failed += 1;
20
- console.error(`FAIL: ${message}`);
21
- } else {
22
- console.log(`PASS: ${message}`);
23
- }
24
- }
25
-
26
- assert(defaultCss.includes('--ax-sys-elevation-control-rest:'), 'Flat elevation tokens exist');
27
- assert(defaultCss.includes('--ax-sys-effect-control-rest:'), 'Combined effect tokens exist');
28
- assert(!defaultCss.includes('--ax-sys-press-offset'), 'Press offset token removed from Flat defaults');
29
- assert(defaultCss.includes('@utility ax-effect-control'), 'ax-effect-control utility exists');
30
- assert(defaultCss.includes('@utility ax-effect-input'), 'ax-effect-input utility exists');
31
- assert(defaultCss.includes('@utility ax-focus-ring'), 'Focus ring utility preserved');
32
- assert(/@utility ax-effect-control-active \{[\s\S]*?transform: none/.test(defaultCss), 'Active utility keeps transform none');
33
- assert(
34
- /@utility ax-effect-transition \{[\s\S]*?transition-property: box-shadow, background-image, border-color/.test(
35
- defaultCss,
36
- ),
37
- 'Button effect transition excludes transform',
38
- );
39
- assert(/@utility ax-solid \{[\s\S]*?ax-effect-control/.test(defaultCss), 'Solid uses control effect');
40
- assert(/@utility ax-outline \{[\s\S]*?ax-effect-control-subtle/.test(defaultCss), 'Outline uses subtle effect');
41
- assert(/@utility ax-link \{[\s\S]*?ax-effect-control/.test(defaultCss) === false, 'Link has no control elevation');
42
- assert(defaultCss.includes('box-shadow: var(--ax-sys-effect-input)'), 'Editors use inset effect');
43
- assert(!defaultCss.includes('translateY(var(--ax-sys-press-offset))'), 'No press-offset translateY remains');
44
-
45
- assert(appearanceCss.includes('.ax-appearance-depth'), 'Depth activator class exists');
46
- assert(appearanceCss.includes('0 1px 1px rgb(15 23 42 / 0.08)'), 'Solid rest uses tight contact shadow');
47
- assert(appearanceCss.includes('0 1px 2px rgb(15 23 42 / 0.1)'), 'Solid hover stays close to control');
48
- assert(appearanceCss.includes('0 1px 1px rgb(15 23 42 / 0.05)'), 'Subtle rest is weaker than Solid');
49
- assert(appearanceCss.includes('--ax-sys-elevation-control-subtle-rest:'), 'Subtle control elevation exists');
50
- assert(appearanceCss.includes('--ax-sys-gradient-control-subtle: none'), 'Outline/Twotone keep no gradient');
51
- assert(appearanceCss.includes('rgb(255 255 255 / 0.06)'), 'Solid gradient is nearly imperceptible');
52
- assert(!appearanceCss.includes('--ax-sys-press-offset'), 'Press offset token removed from Depth');
53
- assert(!appearanceCss.includes('translateY'), 'Depth sheet has no translateY');
54
- assert(
55
- appearanceCss.includes('--ax-sys-effect-control-active: var(--ax-sys-inset-pressed)'),
56
- 'Active uses inset pressed',
57
- );
58
- assert(appearanceCss.includes('.ax-dark.ax-appearance-depth'), 'Dark depth selector exists');
59
- assert(appearanceCss.includes('.ax-dark.ax-appearance-depth .ax-light'), 'Nested light depth reset exists');
60
- assert(!/(^|[^a-z-])\.dark(\s|,|\{)/.test(appearanceCss), 'No non-ax .dark selector');
61
- assert(appearanceCss.includes('@media (forced-colors: active)'), 'Forced colors support');
62
-
63
- const entries = [
64
- join(root, 'apps/showcase/src/styles.css'),
65
- join(root, 'apps/documentation/src/styles.css'),
66
- join(root, 'packages/components/.storybook/styles.css'),
67
- ];
68
-
69
- for (const entry of entries) {
70
- const css = readFileSync(entry, 'utf8');
71
- const defaultIndex = css.indexOf('themes/default.css');
72
- const appearanceIndex = css.indexOf('themes/appearance.css');
73
- assert(defaultIndex >= 0 && appearanceIndex > defaultIndex, `Import order OK: ${entry}`);
74
- }
75
-
76
- if (failed > 0) {
77
- console.error(`\n${failed} assertion(s) failed`);
78
- process.exit(1);
79
- }
80
-
81
- console.log('\nAll appearance contract checks passed.');
@@ -1,910 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { existsSync, readFileSync } from 'node:fs';
3
- import { dirname, join } from 'node:path';
4
- import test from 'node:test';
5
- import { fileURLToPath } from 'node:url';
6
-
7
- const __dirname = dirname(fileURLToPath(import.meta.url));
8
- const workspaceRoot = join(__dirname, '../../..');
9
- const themesDir = join(workspaceRoot, 'packages/styles/themes');
10
- const componentsDir = join(workspaceRoot, 'packages/styles/components');
11
-
12
- function read(path) {
13
- return readFileSync(path, 'utf8');
14
- }
15
-
16
- function extractUtility(css, name) {
17
- const marker = `@utility ${name} {`;
18
- const start = css.indexOf(marker);
19
- if (start < 0) {
20
- return '';
21
- }
22
-
23
- let depth = 0;
24
- const openBrace = css.indexOf('{', start);
25
- for (let i = openBrace; i < css.length; i += 1) {
26
- if (css[i] === '{') {
27
- depth += 1;
28
- } else if (css[i] === '}') {
29
- depth -= 1;
30
- if (depth === 0) {
31
- return css.slice(start, i + 1);
32
- }
33
- }
34
- }
35
-
36
- return '';
37
- }
38
-
39
- const SEMANTIC_TOKENS = [
40
- '--ax-sys-effect-control-rest',
41
- '--ax-sys-effect-field-rest',
42
- '--ax-sys-effect-floating',
43
- '--ax-sys-effect-prominent-floating',
44
- '--ax-sys-effect-overlay',
45
- '--ax-sys-effect-modal',
46
- '--ax-sys-backdrop-filter-surface',
47
- '--ax-sys-surface-fill-opacity',
48
- '--ax-sys-surface-overlay-control-rest',
49
- '--ax-sys-surface-overlay-field-rest',
50
- '--ax-sys-surface-overlay-overlay',
51
- '--ax-sys-surface-overlay-modal',
52
- '--ax-sys-bevel-control-rest',
53
- '--ax-sys-bevel-control-hover',
54
- '--ax-sys-bevel-control-active',
55
- '--ax-sys-bevel-control-disabled',
56
- '--ax-sys-effect-control-neutral-rest',
57
- '--ax-sys-effect-control-neutral-hover',
58
- '--ax-sys-effect-control-neutral-active',
59
- '--ax-sys-effect-control-neutral-disabled',
60
- '--ax-sys-bevel-control-neutral-rest',
61
- '--ax-sys-bevel-control-neutral-hover',
62
- '--ax-sys-bevel-control-neutral-active',
63
- '--ax-sys-bevel-control-neutral-disabled',
64
- '--ax-sys-inner-border-control-neutral-rest',
65
- '--ax-sys-inner-border-control-neutral-hover',
66
- '--ax-sys-inner-border-control-neutral-active',
67
- '--ax-sys-gradient-control-neutral-rest',
68
- '--ax-sys-gradient-control-neutral-hover',
69
- '--ax-sys-gradient-control-neutral-active',
70
- ];
71
-
72
- const COMPONENT_FILES = {
73
- button: join(workspaceRoot, 'packages/components/button/src/lib/button.component.css'),
74
- editor: join(componentsDir, '_editor-container.css'),
75
- dialog: join(workspaceRoot, 'packages/components/dialog/src/lib/dialog.component.css'),
76
- dropdown: join(componentsDir, '_drop-down.css'),
77
- popup: join(workspaceRoot, 'packages/components/popup/src/lib/popup.component.css'),
78
- toast: join(workspaceRoot, 'packages/components/toast/src/lib/toast.component.css'),
79
- notification: join(workspaceRoot, 'packages/components/notification/src/lib/notification.component.css'),
80
- menu: join(workspaceRoot, 'packages/components/menu/src/lib/menu.component.css'),
81
- tooltip: join(workspaceRoot, 'packages/components/tooltip/src/lib/tooltip.component.css'),
82
- drawer: join(workspaceRoot, 'packages/components/drawer/src/lib/drawer-item/drawer.component.css'),
83
- actionsheet: join(componentsDir, '_actionsheet.css'),
84
- };
85
-
86
- const appearancesDir = join(themesDir, 'appearances');
87
-
88
- const APPEARANCE_FILES = {
89
- aggregate: join(themesDir, 'appearance.css'),
90
- index: join(appearancesDir, 'index.css'),
91
- flat: join(appearancesDir, 'flat.css'),
92
- depth: join(appearancesDir, 'depth.css'),
93
- translucent: join(appearancesDir, 'translucent.css'),
94
- };
95
-
96
- const FORBIDDEN_IN_APPEARANCE = [
97
- 'ax-button',
98
- '.ax-editor-container',
99
- '.ax-dialog',
100
- '.ax-overlay-pane',
101
- 'ax-toast',
102
- '.ax-popup',
103
- '@apply',
104
- ];
105
-
106
- test('appearance.css is backward-compatible aggregate entry', () => {
107
- const css = read(APPEARANCE_FILES.aggregate);
108
- assert.ok(css.includes("@import './appearances/index.css'"));
109
- for (const selector of FORBIDDEN_IN_APPEARANCE) {
110
- assert.equal(css.includes(selector), false, `appearance.css must not contain ${selector}`);
111
- }
112
- });
113
-
114
- test('appearances/index.css imports all appearance files', () => {
115
- const css = read(APPEARANCE_FILES.index);
116
- assert.ok(css.includes("@import './flat.css'"));
117
- assert.ok(css.includes("@import './depth.css'"));
118
- assert.ok(css.includes("@import './translucent.css'"));
119
- assert.equal(css.includes("@import './tonal.css'"), false);
120
- });
121
-
122
- for (const [name, file] of Object.entries({
123
- flat: APPEARANCE_FILES.flat,
124
- depth: APPEARANCE_FILES.depth,
125
- translucent: APPEARANCE_FILES.translucent,
126
- })) {
127
- test(`${name}.css is self-contained and token-only`, () => {
128
- const css = read(file);
129
- assert.ok(css.includes(`.ax-appearance-${name}`));
130
- for (const selector of FORBIDDEN_IN_APPEARANCE) {
131
- assert.equal(css.includes(selector), false, `${name}.css must not contain ${selector}`);
132
- }
133
- assert.doesNotMatch(css, /(^|[^a-z-])\.dark(\s|,|\{)/);
134
- });
135
- }
136
-
137
- test('depth.css defines tactile beveled control tokens and lightly recessed fields', () => {
138
- const css = read(APPEARANCE_FILES.depth);
139
- assert.ok(css.includes('--ax-sys-effect-control-rest: var(--ax-ref-shadow-100)'));
140
- assert.ok(css.includes('--ax-sys-effect-control-hover: var(--ax-sys-effect-control-rest)'));
141
- assert.ok(css.includes('--ax-sys-effect-control-active: var(--ax-ref-shadow-0)'));
142
- assert.ok(css.includes('--ax-sys-effect-field-rest: var(--ax-ref-inset-low)'));
143
- assert.ok(css.includes('inset 0 1px 2px rgb(15 23 42 / 0.1)'));
144
- assert.ok(css.includes('--ax-sys-bevel-control-rest:'));
145
- assert.ok(css.includes('inset 0 -2px 2px rgb(15 23 42 / 0.1)'));
146
- assert.doesNotMatch(css, /inset 0 -4px 5px rgb\(15 23 42/);
147
- assert.ok(css.includes('rgb(15 23 42 / 0.075) 100%'));
148
- assert.ok(css.includes('--ax-sys-inner-border-control: inset 0 0 0 1px rgb(15 23 42 / 0.14)'));
149
- assert.ok(css.includes('--ax-sys-gradient-control-rest: var(--ax-ref-gradient-control)'));
150
- assert.ok(css.includes('--ax-sys-effect-control-neutral-rest:'));
151
- assert.ok(css.includes('--ax-sys-gradient-control-neutral-rest:'));
152
- assert.ok(css.includes('--ax-sys-transform-control-active: translateY(1px)'));
153
- assert.ok(css.includes('--ax-sys-surface-overlay-control-rest: transparent'));
154
- });
155
-
156
- test('tonal appearance stylesheet is removed', () => {
157
- assert.equal(read(APPEARANCE_FILES.index).includes('tonal.css'), false);
158
- assert.equal(read(join(themesDir, 'appearance.css')).includes('ax-appearance-tonal'), false);
159
- });
160
-
161
- test('default.css defines flat semantic tokens with neutral bevel defaults', () => {
162
- const css = read(join(themesDir, 'default.css'));
163
- for (const token of SEMANTIC_TOKENS) {
164
- assert.ok(css.includes(`${token}:`), `missing ${token}`);
165
- }
166
- assert.ok(css.includes('--ax-sys-effect-control-rest: 0 0 #0000'));
167
- assert.ok(css.includes('--ax-sys-bevel-control-rest: 0 0 #0000'));
168
- assert.ok(css.includes('--ax-sys-effect-control-neutral-rest: var(--ax-sys-effect-control-rest)'));
169
- assert.ok(css.includes('--ax-sys-bevel-control-neutral-rest: var(--ax-sys-bevel-control-rest)'));
170
- assert.ok(css.includes('--ax-sys-inner-border-control-neutral-rest: var(--ax-sys-inner-border-control)'));
171
- assert.ok(css.includes('--ax-sys-gradient-control-neutral-rest: var(--ax-sys-gradient-control-rest)'));
172
- assert.ok(css.includes('--ax-sys-effect-control-neutral-active: var(--ax-sys-effect-control-active)'));
173
- assert.ok(css.includes('--ax-sys-gradient-control-neutral-active: var(--ax-sys-gradient-control-active)'));
174
- assert.ok(css.includes('0 4px 6px -1px rgb(0 0 0 / 0.1)'));
175
- assert.ok(css.includes('0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)'));
176
- assert.ok(css.includes('0 10px 15px -3px rgb(0 0 0 / 0.1)'));
177
- assert.ok(css.includes('.ax-editor-container.ax-solid'));
178
- // Host shadow = field effect only; contrast border lives on ::after
179
- assert.ok(css.includes('box-shadow: var(--ax-sys-effect-field-rest)'));
180
- assert.ok(css.includes('box-shadow: var(--ax-sys-effect-field-hover)'));
181
- assert.equal(
182
- css.includes(
183
- 'box-shadow: var(--ax-sys-contrast-inner-border-field), var(--ax-sys-effect-field-rest)',
184
- ),
185
- false,
186
- );
187
- // Utilities must not wipe editor overlays owned by _editor-container.css
188
- const editorUtility = css.slice(css.indexOf('.ax-editor-container.ax-solid,'));
189
- assert.equal(editorUtility.includes('background-image: none'), false);
190
- });
191
-
192
- test('default.css look utilities are token-driven without base elevation', () => {
193
- const css = read(join(themesDir, 'default.css'));
194
- assert.doesNotMatch(extractUtility(css, 'ax-solid'), /ax-effect-control/);
195
- assert.doesNotMatch(extractUtility(css, 'ax-outline'), /ax-effect-control/);
196
- assert.doesNotMatch(extractUtility(css, 'ax-twotone'), /ax-effect-control/);
197
- // Interactive looks change color only — box-shadow owned by components (Contrast-safe)
198
- assert.doesNotMatch(extractUtility(css, 'ax-solid-interactive'), /ax-effect-control-hover/);
199
- assert.doesNotMatch(extractUtility(css, 'ax-outline-interactive'), /ax-effect-control-subtle-hover/);
200
- });
201
-
202
- test('effect utilities compose contrast control border', () => {
203
- const css = read(join(themesDir, 'default.css'));
204
- assert.match(
205
- extractUtility(css, 'ax-effect-control-hover'),
206
- /--ax-sys-contrast-inner-border-control/,
207
- );
208
- assert.match(
209
- extractUtility(css, 'ax-effect-control-active'),
210
- /--ax-sys-contrast-inner-border-control/,
211
- );
212
- assert.match(
213
- extractUtility(css, 'ax-effect-blank-hover'),
214
- /--ax-sys-contrast-inner-border-control/,
215
- );
216
- });
217
-
218
- test('translucent keeps controls and fields flat', () => {
219
- const css = read(APPEARANCE_FILES.translucent);
220
- assert.match(
221
- css,
222
- /\.ax-appearance-translucent \{[\s\S]*?--ax-sys-effect-control-rest: 0 0 #0000[\s\S]*?--ax-sys-effect-field-rest: 0 0 #0000/,
223
- );
224
- assert.ok(css.includes('--ax-sys-surface-overlay-control-rest: transparent'));
225
- assert.ok(css.includes('--ax-sys-surface-overlay-field-rest: transparent'));
226
- assert.ok(css.includes('--ax-sys-surface-fill-opacity:'));
227
- assert.ok(css.includes('@supports not (backdrop-filter: blur(1px))'));
228
- });
229
-
230
- test('flat.css resets bevel and surface overlays to neutral', () => {
231
- const css = read(APPEARANCE_FILES.flat);
232
- assert.ok(css.includes('--ax-sys-bevel-control-rest: 0 0 #0000'));
233
- assert.ok(css.includes('--ax-sys-surface-overlay-control-rest: transparent'));
234
- assert.ok(css.includes('--ax-sys-surface-overlay-field-rest: transparent'));
235
- });
236
-
237
- test('component tokens use rest state naming for rendered surfaces', () => {
238
- const dropdown = read(COMPONENT_FILES.dropdown);
239
- const toast = read(COMPONENT_FILES.toast);
240
- assert.ok(dropdown.includes('--ax-comp-dropdown-shadow-rest'));
241
- assert.equal(dropdown.includes('--ax-comp-dropdown-shadow-open'), false);
242
- assert.ok(toast.includes('--ax-comp-toast-shadow-rest'));
243
- assert.equal(toast.includes('--ax-comp-toast-shadow-open'), false);
244
- });
245
-
246
- test('component tokens are consumed by CSS properties', () => {
247
- const checks = [
248
- [COMPONENT_FILES.button, '--ax-comp-button-shadow-rest', 'box-shadow:'],
249
- [COMPONENT_FILES.button, '--ax-comp-button-surface-overlay-rest', 'background-image:'],
250
- [COMPONENT_FILES.editor, '--ax-comp-editor-shadow-rest', 'box-shadow:'],
251
- [COMPONENT_FILES.editor, '--ax-comp-editor-surface-overlay-rest', 'background-image:'],
252
- [COMPONENT_FILES.dialog, '--ax-comp-dialog-shadow-rest', 'box-shadow:'],
253
- [COMPONENT_FILES.dialog, '--ax-comp-dialog-surface-overlay-rest', 'background-image:'],
254
- [COMPONENT_FILES.dropdown, '--ax-comp-dropdown-shadow-rest', 'box-shadow:'],
255
- [COMPONENT_FILES.dropdown, '--ax-comp-dropdown-surface-overlay-rest', 'background-image:'],
256
- [COMPONENT_FILES.popup, '--ax-comp-popup-shadow-rest', 'box-shadow:'],
257
- [COMPONENT_FILES.popup, '--ax-comp-popup-surface-overlay-rest', 'background-image:'],
258
- [COMPONENT_FILES.toast, '--ax-comp-toast-shadow-rest', 'box-shadow:'],
259
- [COMPONENT_FILES.toast, '--ax-comp-toast-surface-overlay-rest', 'background-image:'],
260
- [COMPONENT_FILES.notification, '--ax-comp-notification-shadow-rest', 'box-shadow:'],
261
- [COMPONENT_FILES.notification, '--ax-comp-notification-surface-overlay-rest', 'background-image:'],
262
- [COMPONENT_FILES.menu, '--ax-comp-menu-shadow-rest', 'box-shadow:'],
263
- [COMPONENT_FILES.menu, '--ax-comp-menu-surface-overlay-rest', 'background-image:'],
264
- [COMPONENT_FILES.tooltip, '--ax-comp-tooltip-shadow-rest', 'box-shadow:'],
265
- [COMPONENT_FILES.drawer, '--ax-comp-drawer-shadow-rest', 'box-shadow:'],
266
- [COMPONENT_FILES.drawer, '--ax-comp-drawer-surface-overlay-rest', 'background-image:'],
267
- [COMPONENT_FILES.actionsheet, '--ax-comp-action-sheet-shadow-rest', 'box-shadow:'],
268
- [COMPONENT_FILES.actionsheet, '--ax-comp-action-sheet-surface-overlay-rest', 'background-image:'],
269
- ];
270
-
271
- for (const [file, token, property] of checks) {
272
- const css = read(file);
273
- assert.ok(css.includes(token), `${file} missing ${token}`);
274
- assert.ok(css.includes(property), `${file} does not consume tokens via ${property}`);
275
- }
276
- });
277
-
278
- test('migrated components avoid raw duplicated shadow literals', () => {
279
- const rawShadow = /0 \d+px \d+px -?\d+px rgb\(/;
280
- for (const [name, file] of Object.entries(COMPONENT_FILES)) {
281
- const css = read(file);
282
- assert.equal(rawShadow.test(css), false, `${name} contains raw shadow literal`);
283
- }
284
- });
285
-
286
- test('button maps neutral depth tokens for default solid only', () => {
287
- const css = read(COMPONENT_FILES.button);
288
- const defaultBlock = css.match(/&\.ax-solid-interactive \{[\s\S]*?&\.ax-default \{([^}]+)\}/)?.[1] ?? '';
289
- assert.ok(defaultBlock.includes('border-color: rgba(var(--ax-sys-color-border-surface))'));
290
- assert.ok(defaultBlock.includes('--ax-comp-button-highlight-rest: 0 0 #0000'));
291
- assert.match(
292
- css,
293
- /&\.ax-solid-interactive \{[\s\S]*?&\.ax-default \{[\s\S]*?--ax-comp-button-shadow-rest:\s*var\(--ax-sys-effect-control-neutral-rest\)/,
294
- );
295
- assert.match(
296
- css,
297
- /&\.ax-default \{[\s\S]*?--ax-comp-button-shadow-hover:\s*var\(--ax-sys-effect-control-neutral-hover\)/,
298
- );
299
- assert.match(
300
- css,
301
- /&\.ax-default \{[\s\S]*?--ax-comp-button-shadow-active:\s*var\(--ax-sys-effect-control-neutral-active\)/,
302
- );
303
- assert.match(
304
- css,
305
- /&\.ax-default \{[\s\S]*?--ax-comp-button-bevel-rest:\s*var\(--ax-sys-bevel-control-neutral-rest\)/,
306
- );
307
- assert.match(
308
- css,
309
- /&\.ax-default \{[\s\S]*?--ax-comp-button-bevel-hover:\s*var\(--ax-sys-bevel-control-neutral-hover\)/,
310
- );
311
- assert.match(
312
- css,
313
- /&\.ax-default \{[\s\S]*?--ax-comp-button-bevel-active:\s*var\(--ax-sys-bevel-control-neutral-active\)/,
314
- );
315
- assert.match(
316
- css,
317
- /&\.ax-default \{[\s\S]*?--ax-comp-button-inner-border-rest:\s*var\(--ax-sys-inner-border-control-neutral-rest\)/,
318
- );
319
- assert.match(
320
- css,
321
- /&\.ax-default \{[\s\S]*?--ax-comp-button-inner-border-hover:\s*var\(--ax-sys-inner-border-control-neutral-hover\)/,
322
- );
323
- assert.match(
324
- css,
325
- /&\.ax-default \{[\s\S]*?--ax-comp-button-inner-border-active:\s*var\(--ax-sys-inner-border-control-neutral-active\)/,
326
- );
327
- assert.match(
328
- css,
329
- /&\.ax-default \{[\s\S]*?--ax-comp-button-background-image-rest:\s*var\(--ax-sys-gradient-control-neutral-rest\)/,
330
- );
331
- assert.match(
332
- css,
333
- /&\.ax-default \{[\s\S]*?--ax-comp-button-background-image-hover:\s*var\(--ax-sys-gradient-control-neutral-hover\)/,
334
- );
335
- assert.match(
336
- css,
337
- /&\.ax-default \{[\s\S]*?--ax-comp-button-background-image-active:\s*var\(--ax-sys-gradient-control-neutral-active\)/,
338
- );
339
- assert.doesNotMatch(css, /&\.ax-outline-interactive \{[\s\S]*?&\.ax-default \{[\s\S]*?neutral/);
340
- assert.doesNotMatch(css, /&\.ax-link-interactive \{[\s\S]*?&\.ax-default/);
341
- assert.doesNotMatch(css, /0 1px 1px rgb\(15 23 42 \/ 0\.1\)/);
342
- });
343
-
344
- test('default solid uses same hover and active guards as colored solid', () => {
345
- const css = read(COMPONENT_FILES.button);
346
- const solidBlock = css.match(/&\.ax-solid-interactive \{([\s\S]*?)\n \}/)?.[1] ?? '';
347
- assert.ok(solidBlock.includes('&:not(.ax-state-disabled, .ax-state-loading):hover'));
348
- assert.ok(solidBlock.includes('&:not(.ax-state-disabled, .ax-state-loading):active'));
349
- assert.equal(
350
- (solidBlock.match(/&:not\(\.ax-state-disabled, \.ax-state-loading\):hover/g) ?? []).length,
351
- 1,
352
- );
353
- });
354
-
355
- test('depth overrides complete neutral state contract', () => {
356
- const css = read(APPEARANCE_FILES.depth);
357
- for (const token of [
358
- '--ax-sys-effect-control-neutral-rest',
359
- '--ax-sys-effect-control-neutral-hover',
360
- '--ax-sys-effect-control-neutral-active',
361
- '--ax-sys-effect-control-neutral-disabled',
362
- '--ax-sys-bevel-control-neutral-rest',
363
- '--ax-sys-bevel-control-neutral-hover',
364
- '--ax-sys-bevel-control-neutral-active',
365
- '--ax-sys-inner-border-control-neutral-rest',
366
- '--ax-sys-inner-border-control-neutral-hover',
367
- '--ax-sys-gradient-control-neutral-rest',
368
- '--ax-sys-gradient-control-neutral-hover',
369
- '--ax-sys-gradient-control-neutral-active',
370
- ]) {
371
- assert.ok(css.includes(`${token}:`), `depth.css missing ${token}`);
372
- }
373
- assert.doesNotMatch(css, /--ax-sys-inner-border-control-neutral:/);
374
- });
375
-
376
- test('depth neutral default solid matches pale reference (border + soft shadow)', () => {
377
- const css = read(APPEARANCE_FILES.depth);
378
- assert.ok(css.includes('0 1px 2px rgb(15 23 42 / 0.08), 0 2px 4px rgb(15 23 42 / 0.05)'));
379
- assert.ok(css.includes('--ax-sys-bevel-control-neutral-rest: 0 0 #0000'));
380
- assert.ok(css.includes('--ax-sys-inner-border-control-neutral-rest: 0 0 #0000'));
381
- assert.ok(css.includes('--ax-sys-gradient-control-neutral-rest: var(--ax-ref-gradient-control-neutral)'));
382
- assert.ok(css.includes('--ax-sys-effect-control-neutral-hover: var(--ax-sys-effect-control-neutral-rest)'));
383
- assert.ok(css.includes('--ax-sys-bevel-control-hover: var(--ax-sys-bevel-control-rest)'));
384
- });
385
-
386
- test('outline default retains border utility', () => {
387
- const css = read(join(themesDir, 'default.css'));
388
- const outlineUtility = extractUtility(css, 'ax-outline');
389
- assert.ok(outlineUtility.includes('border'));
390
- const buttonCss = read(COMPONENT_FILES.button);
391
- assert.doesNotMatch(buttonCss, /\.ax-outline-interactive[\s\S]*?border-color:\s*transparent/);
392
- });
393
-
394
- test('button defines and consumes bevel component tokens in solid stack', () => {
395
- const css = read(COMPONENT_FILES.button);
396
- assert.ok(css.includes('--ax-comp-button-bevel-rest: var(--ax-sys-bevel-control-rest)'));
397
- assert.ok(css.includes('--ax-comp-button-bevel-hover: var(--ax-sys-bevel-control-hover)'));
398
- assert.ok(css.includes('--ax-comp-button-bevel-active: var(--ax-sys-bevel-control-active)'));
399
- assert.ok(css.includes('--ax-comp-button-bevel-disabled: var(--ax-sys-bevel-control-disabled)'));
400
- assert.ok(css.includes('--ax-comp-button-inner-border-rest: var(--ax-sys-inner-border-control)'));
401
- assert.match(css, /\.ax-solid-interactive \{[\s\S]*?var\(--ax-comp-button-bevel-rest\)/);
402
- assert.match(css, /\.ax-solid-interactive \{[\s\S]*?var\(--ax-comp-button-inner-border-rest\)/);
403
- assert.doesNotMatch(css, /inset 0 2px 1px rgb\(255 255 255/);
404
- });
405
-
406
- test('outline and twotone do not receive full rest bevel stack', () => {
407
- const css = read(COMPONENT_FILES.button);
408
- assert.doesNotMatch(css, /\.ax-outline-interactive \{[\s\S]*?--ax-comp-button-bevel-rest/);
409
- assert.doesNotMatch(css, /\.ax-twotone-interactive \{[\s\S]*?--ax-comp-button-bevel-rest/);
410
- assert.match(css, /\.ax-twotone-interactive \{[\s\S]*?--ax-comp-button-shadow-subtle-rest/);
411
- });
412
-
413
- test('disabled and loading suppress decorative bevel on solid', () => {
414
- const css = read(COMPONENT_FILES.button);
415
- const disabledBlock = css.match(/&\.ax-state-disabled,\s*&\.ax-state-loading \{([^}]+)\}/)?.[1] ?? '';
416
- assert.ok(
417
- disabledBlock.includes(
418
- 'box-shadow: var(--ax-comp-button-contrast-inner-border-rest), var(--ax-comp-button-inner-border-rest)',
419
- ),
420
- );
421
- assert.equal(disabledBlock.includes('bevel'), false);
422
- assert.ok(css.includes('--ax-comp-button-bevel-disabled: var(--ax-sys-bevel-control-disabled)'));
423
- });
424
-
425
- test('fab consumes control-level bevel and gradient tokens', () => {
426
- const fabPath = join(workspaceRoot, 'packages/components/fab/src/lib/fab.component.css');
427
- const css = read(fabPath);
428
- assert.ok(css.includes('--ax-comp-fab-bevel-rest: var(--ax-sys-bevel-control-rest)'));
429
- assert.ok(css.includes('--ax-comp-fab-shadow-rest: var(--ax-sys-effect-control-rest)'));
430
- assert.ok(css.includes('var(--ax-comp-fab-bevel-rest)'));
431
- assert.equal(css.includes('--ax-sys-effect-floating'), false);
432
- assert.equal(css.includes('--ax-sys-effect-modal'), false);
433
- });
434
-
435
- test('link button remains explicitly shadowless', () => {
436
- const css = read(COMPONENT_FILES.button);
437
- assert.match(css, /\.ax-link-interactive \{[\s\S]*?box-shadow: none/);
438
- assert.match(css, /\.ax-link-interactive \{[\s\S]*?background-image: none/);
439
- assert.match(css, /\.ax-link-interactive \{[\s\S]*?transform: none/);
440
- });
441
-
442
- test('editor uses field inset tokens only', () => {
443
- const css = read(COMPONENT_FILES.editor);
444
- assert.ok(css.includes('--ax-comp-editor-shadow-rest: var(--ax-sys-effect-field-rest)'));
445
- assert.doesNotMatch(css, /--ax-sys-effect-overlay|--ax-sys-effect-modal|--ax-sys-effect-floating/);
446
- });
447
-
448
- test('overlay service propagates visual context', () => {
449
- const css = read(join(workspaceRoot, 'packages/cdk/overlay/src/lib/overlay.service.ts'));
450
- assert.ok(css.includes('resolveVisualContext'));
451
- assert.ok(css.includes('applyVisualContext'));
452
- assert.ok(css.includes('contextElement'));
453
- });
454
-
455
- const ANCHORLESS_OVERLAY_SERVICES = [
456
- join(workspaceRoot, 'packages/components/toast/src/lib/toast.service.ts'),
457
- join(workspaceRoot, 'packages/components/notification/src/lib/notification.service.ts'),
458
- join(workspaceRoot, 'packages/components/dialog/src/lib/dialog.service.ts'),
459
- join(workspaceRoot, 'packages/components/popup/src/lib/popup.service.ts'),
460
- join(workspaceRoot, 'packages/components/action-sheet/src/lib/action-sheet.service.ts'),
461
- ];
462
-
463
- for (const servicePath of ANCHORLESS_OVERLAY_SERVICES) {
464
- test(`anchorless overlay service forwards contextElement — ${servicePath.split('/').slice(-2).join('/')}`, () => {
465
- const source = read(servicePath);
466
- assert.match(source, /contextElement:\s*config\.contextElement/);
467
- });
468
- }
469
-
470
- test('package exports appearance stylesheets', () => {
471
- const pkg = JSON.parse(read(join(workspaceRoot, 'packages/styles/package.json')));
472
- assert.equal(pkg.exports['./themes/appearance.css'], './themes/appearance.css');
473
- assert.equal(pkg.exports['./themes/appearances/depth.css'], './themes/appearances/depth.css');
474
- assert.equal(pkg.exports['./themes/appearances/index.css'], './themes/appearances/index.css');
475
- assert.equal(pkg.exports['./themes/appearances/flat.css'], './themes/appearances/flat.css');
476
- });
477
-
478
- test('modified surface files avoid stale .dark selectors', () => {
479
- const staleDark = /(^|[^a-z-])\.dark(\s|,|\{)/;
480
- for (const file of [COMPONENT_FILES.dropdown, COMPONENT_FILES.menu]) {
481
- const css = read(file);
482
- assert.equal(staleDark.test(css), false, `${file} uses stale .dark selector`);
483
- }
484
- });
485
-
486
- test('prominent-floating semantic token exists across appearances', () => {
487
- const defaultCss = read(join(themesDir, 'default.css'));
488
- assert.ok(defaultCss.includes('--ax-sys-effect-prominent-floating:'));
489
- assert.ok(read(APPEARANCE_FILES.depth).includes('--ax-sys-effect-prominent-floating:'));
490
- assert.ok(read(APPEARANCE_FILES.translucent).includes('--ax-sys-effect-prominent-floating:'));
491
- });
492
-
493
- test('dark and nested light mappings include prominent-floating', () => {
494
- const depthCss = read(APPEARANCE_FILES.depth);
495
- assert.match(depthCss, /\.ax-dark\.ax-appearance-depth[\s\S]*?--ax-ref-shadow-300:/);
496
- assert.match(depthCss, /\.ax-dark \.ax-light\.ax-appearance-depth[\s\S]*?--ax-ref-shadow-300:/);
497
- assert.ok(depthCss.includes('--ax-sys-effect-prominent-floating: var(--ax-ref-shadow-300)'));
498
- });
499
-
500
- test('notification consumes prominent-floating, not modal', () => {
501
- const css = read(COMPONENT_FILES.notification);
502
- assert.ok(css.includes('--ax-comp-notification-shadow-rest: var(--ax-sys-effect-prominent-floating)'));
503
- assert.equal(css.includes('--ax-sys-effect-modal'), false);
504
- assert.equal(css.includes('--ax-comp-notification-shadow-open'), false);
505
- });
506
-
507
- const CONTEXT_OPTION_FILES = [
508
- ['dialog', join(workspaceRoot, 'packages/components/dialog/src/lib/dialog.class.ts')],
509
- ['toast', join(workspaceRoot, 'packages/components/toast/src/lib/toast.class.ts')],
510
- ['notification', join(workspaceRoot, 'packages/components/notification/src/lib/notification.class.ts')],
511
- ['popup', join(workspaceRoot, 'packages/components/popup/src/lib/popup.interface.ts')],
512
- ['action-sheet', join(workspaceRoot, 'packages/components/action-sheet/src/lib/action-sheet.class.ts')],
513
- ];
514
-
515
- for (const [name, file] of CONTEXT_OPTION_FILES) {
516
- test(`${name} options expose contextElement`, () => {
517
- const source = read(file);
518
- assert.match(source, /contextElement\?: HTMLElement \| ElementRef<HTMLElement>/);
519
- assert.match(source, /outside the originating DOM subtree/);
520
- });
521
- }
522
-
523
- test('context menu uses shared resolver utilities', () => {
524
- const source = read(join(workspaceRoot, 'packages/components/menu/src/lib/context-menu.component.ts'));
525
- assert.match(source, /resolveVisualContext/);
526
- assert.match(source, /applyVisualContext/);
527
- assert.doesNotMatch(source, /classList\.contains\('ax-dark'\)/);
528
- });
529
-
530
- test('popover does not manually inject ax-dark', () => {
531
- const source = read(join(workspaceRoot, 'packages/components/popover/src/lib/popover.component.ts'));
532
- assert.doesNotMatch(source, /classList\.add\(['"]ax-dark['"]\)/);
533
- assert.doesNotMatch(source, /isDark\(\)/);
534
- });
535
-
536
- test('depth fields use lightly recessed inset tokens', () => {
537
- const depthCss = read(APPEARANCE_FILES.depth);
538
- const flatCss = read(APPEARANCE_FILES.flat);
539
- const editorCss = read(COMPONENT_FILES.editor);
540
- assert.ok(depthCss.includes('inset 0 1px 2px rgb(15 23 42 / 0.1)'));
541
- assert.ok(depthCss.includes('inset 0 1px 3px rgb(15 23 42 / 0.13)'));
542
- assert.ok(flatCss.includes('--ax-sys-effect-field-rest: 0 0 #0000'));
543
- assert.ok(editorCss.includes('--ax-comp-editor-shadow-rest: var(--ax-sys-effect-field-rest)'));
544
- assert.doesNotMatch(editorCss, /--ax-sys-effect-floating|--ax-sys-effect-modal|--ax-sys-effect-overlay/);
545
- });
546
-
547
- test('floating surfaces expose backdrop-filter consumption', () => {
548
- for (const file of [COMPONENT_FILES.dropdown, COMPONENT_FILES.toast, COMPONENT_FILES.notification, COMPONENT_FILES.dialog]) {
549
- const css = read(file);
550
- assert.ok(css.includes('backdrop-filter:'), `${file} must consume backdrop-filter`);
551
- assert.ok(css.includes('-webkit-backdrop-filter:'), `${file} must consume -webkit-backdrop-filter`);
552
- }
553
- });
554
-
555
- const contrastDir = join(themesDir, 'contrast');
556
- const CONTRAST_FILES = {
557
- index: join(contrastDir, 'index.css'),
558
- enhanced: join(contrastDir, 'enhanced.css'),
559
- high: join(contrastDir, 'high.css'),
560
- };
561
-
562
- const FORBIDDEN_IN_CONTRAST = ['ax-button', '.ax-editor-container', '@apply', 'ax-appearance-high-contrast', 'ax-appearance-enhanced'];
563
-
564
- test('contrast/enhanced.css exists and defines ax-contrast-enhanced', () => {
565
- assert.ok(existsSync(CONTRAST_FILES.enhanced), 'enhanced.css must exist');
566
- const css = read(CONTRAST_FILES.enhanced);
567
- assert.ok(css.includes('.ax-contrast-enhanced'));
568
- assert.equal(css.includes('.ax-appearance-enhanced'), false, 'Enhanced must not be an Appearance');
569
- assert.equal(css.includes('ax-appearance-enhanced'), false);
570
- for (const selector of FORBIDDEN_IN_CONTRAST) {
571
- assert.equal(css.includes(selector), false, `enhanced.css must not contain ${selector}`);
572
- }
573
- assert.doesNotMatch(css, /(^|[^a-z-])\.dark(\s|,|\{)/);
574
- });
575
-
576
- test('contrast/high.css exists and defines ax-contrast-high', () => {
577
- const css = read(CONTRAST_FILES.high);
578
- assert.ok(css.includes('.ax-contrast-high'));
579
- assert.equal(css.includes('.ax-appearance-high-contrast'), false, 'High Contrast must not be an Appearance');
580
- for (const selector of FORBIDDEN_IN_CONTRAST) {
581
- assert.equal(css.includes(selector), false, `high.css must not contain ${selector}`);
582
- }
583
- assert.doesNotMatch(css, /(^|[^a-z-])\.dark(\s|,|\{)/);
584
- });
585
-
586
- test('contrast/index.css aggregates enhanced and high contrast profiles', () => {
587
- const css = read(CONTRAST_FILES.index);
588
- assert.ok(css.includes("@import './enhanced.css'"));
589
- assert.ok(css.includes("@import './high.css'"));
590
- });
591
-
592
- test('default.css defines contrast token defaults', () => {
593
- const css = read(join(themesDir, 'default.css'));
594
- assert.ok(css.includes('--ax-sys-contrast-inner-border-control: 0 0 #0000'));
595
- assert.ok(css.includes('--ax-sys-contrast-inner-border-invalid:'));
596
- assert.ok(css.includes('rgba(var(--ax-sys-color-danger-600))'));
597
- assert.ok(css.includes('--ax-sys-contrast-surface-overlay-control: transparent'));
598
- assert.ok(css.includes('--ax-sys-contrast-disabled-opacity: 1'));
599
- });
600
-
601
- test('default.css resets inherited color on localized ax-dark / nested ax-light', () => {
602
- const css = read(join(themesDir, 'default.css'));
603
- assert.ok(css.includes('color: rgba(var(--ax-sys-color-on-darkest-surface))'));
604
- assert.ok(css.includes('.ax-dark .ax-light'));
605
- assert.ok(css.includes('color: rgba(var(--ax-sys-color-on-lightest-surface))'));
606
- });
607
-
608
- test('enhanced contrast defines control, field, and surface boundary tokens', () => {
609
- const css = read(CONTRAST_FILES.enhanced);
610
- assert.ok(css.includes('.ax-contrast-enhanced {'));
611
- assert.ok(css.includes('--ax-sys-contrast-inner-border-control:'));
612
- assert.ok(css.includes('--ax-sys-contrast-border-control:'));
613
- assert.ok(css.includes('--ax-sys-contrast-inner-border-field:'));
614
- assert.ok(css.includes('--ax-sys-contrast-border-field:'));
615
- assert.ok(css.includes('--ax-sys-contrast-inner-border-surface:'));
616
- assert.ok(css.includes('--ax-sys-contrast-border-surface:'));
617
- assert.ok(css.includes('--ax-sys-contrast-surface-overlay-field:'));
618
- assert.ok(css.includes('--ax-sys-contrast-disabled-opacity:'));
619
- assert.ok(css.includes('--ax-sys-contrast-inner-border-invalid:'));
620
- assert.ok(css.includes('--ax-sys-contrast-border-invalid:'));
621
- assert.ok(css.includes('--ax-sys-contrast-focus-ring-invalid:'));
622
- });
623
-
624
- test('enhanced contrast light and dark values exist with nested light restoration', () => {
625
- const css = read(CONTRAST_FILES.enhanced);
626
- assert.ok(css.includes('inset 0 0 0 1px rgb(15 23 42 / 0.20)'));
627
- assert.ok(css.includes('.ax-dark.ax-contrast-enhanced'));
628
- assert.ok(css.includes('inset 0 0 0 1px rgb(255 255 255 / 0.22)'));
629
- assert.ok(css.includes('.ax-dark .ax-light.ax-contrast-enhanced'));
630
- });
631
-
632
- test('enhanced contrast is weaker than high where reasonably testable', () => {
633
- const enhanced = read(CONTRAST_FILES.enhanced);
634
- const high = read(CONTRAST_FILES.high);
635
- assert.ok(enhanced.includes('rgb(15 23 42 / 0.34)'));
636
- assert.ok(high.includes('rgb(15 23 42 / 0.78)'));
637
- assert.ok(enhanced.includes('--ax-sys-contrast-disabled-opacity: 0.72'));
638
- assert.ok(high.includes('--ax-sys-contrast-disabled-opacity: 0.62'));
639
- });
640
-
641
- test('enhanced contrast does not override appearance-owned tokens', () => {
642
- const css = read(CONTRAST_FILES.enhanced);
643
- assert.equal(css.includes('--ax-sys-gradient-control-rest'), false);
644
- assert.equal(css.includes('--ax-sys-gradient-control-hover'), false);
645
- assert.equal(css.includes('--ax-sys-gradient-control-active'), false);
646
- assert.equal(css.includes('--ax-sys-bevel-control-rest'), false);
647
- assert.equal(css.includes('--ax-sys-bevel-control-hover'), false);
648
- assert.equal(css.includes('--ax-sys-bevel-control-active'), false);
649
- assert.equal(css.includes('--ax-sys-highlight-control'), false);
650
- assert.equal(css.includes('--ax-sys-highlight-control-subtle'), false);
651
- assert.equal(css.includes('--ax-sys-backdrop-filter-surface'), false);
652
- assert.equal(css.includes('--ax-sys-surface-fill-opacity'), false);
653
- assert.equal(css.includes('--ax-sys-filter-surface'), false);
654
- });
655
-
656
- test('enhanced control overlay is transparent; field overlay remains non-transparent', () => {
657
- const css = read(CONTRAST_FILES.enhanced);
658
- assert.match(
659
- css,
660
- /\.ax-contrast-enhanced \{[\s\S]*?--ax-sys-contrast-surface-overlay-control:\s*transparent;/,
661
- );
662
- assert.match(
663
- css,
664
- /\.ax-contrast-enhanced \{[\s\S]*?--ax-sys-contrast-surface-overlay-field:\s*rgb\(15 23 42 \/ 0\.028\);/,
665
- );
666
- assert.match(
667
- css,
668
- /\.ax-dark\.ax-contrast-enhanced[\s\S]*?--ax-sys-contrast-surface-overlay-control:\s*transparent;/,
669
- );
670
- assert.match(
671
- css,
672
- /\.ax-dark\.ax-contrast-enhanced[\s\S]*?--ax-sys-contrast-surface-overlay-field:\s*rgb\(255 255 255 \/ 0\.055\);/,
673
- );
674
- assert.match(
675
- css,
676
- /\.ax-dark \.ax-light\.ax-contrast-enhanced[\s\S]*?--ax-sys-contrast-surface-overlay-control:\s*transparent;/,
677
- );
678
- });
679
-
680
- test('enhanced field boundary is stronger than control boundary', () => {
681
- const css = read(CONTRAST_FILES.enhanced);
682
- assert.ok(css.includes('--ax-sys-contrast-inner-border-control: inset 0 0 0 1px rgb(15 23 42 / 0.20)'));
683
- assert.ok(css.includes('--ax-sys-contrast-inner-border-field: inset 0 0 0 1px rgb(15 23 42 / 0.34)'));
684
- assert.ok(css.includes('--ax-sys-contrast-border-control: rgb(15 23 42 / 0.28)'));
685
- assert.ok(css.includes('--ax-sys-contrast-border-field: rgb(15 23 42 / 0.40)'));
686
- });
687
-
688
- test('high contrast light and dark values exist', () => {
689
- const css = read(CONTRAST_FILES.high);
690
- assert.ok(css.includes('.ax-contrast-high {'));
691
- assert.ok(css.includes('inset 0 0 0 1px rgb(15 23 42 / 0.75)'));
692
- assert.ok(css.includes('.ax-dark.ax-contrast-high'));
693
- assert.ok(css.includes('inset 0 0 0 1px rgb(255 255 255 / 0.78)'));
694
- assert.ok(css.includes('.ax-dark .ax-light.ax-contrast-high'));
695
- });
696
-
697
- test('high control overlay is transparent; field overlay remains strong', () => {
698
- const css = read(CONTRAST_FILES.high);
699
- assert.match(
700
- css,
701
- /\.ax-contrast-high \{[\s\S]*?--ax-sys-contrast-surface-overlay-control:\s*transparent;/,
702
- );
703
- assert.match(
704
- css,
705
- /\.ax-contrast-high \{[\s\S]*?--ax-sys-contrast-surface-overlay-field:\s*rgb\(15 23 42 \/ 0\.045\);/,
706
- );
707
- assert.match(
708
- css,
709
- /\.ax-dark\.ax-contrast-high[\s\S]*?--ax-sys-contrast-surface-overlay-control:\s*transparent;/,
710
- );
711
- assert.ok(css.includes('--ax-sys-contrast-focus-ring:'));
712
- assert.ok(css.includes('--ax-sys-contrast-focus-ring-offset:'));
713
- });
714
-
715
- test('high does not override appearance-owned tokens in normal mode', () => {
716
- const css = read(CONTRAST_FILES.high);
717
- const normalMode = css.split('@media (forced-colors: active)')[0];
718
- assert.equal(normalMode.includes('--ax-sys-gradient-control-rest'), false);
719
- assert.equal(normalMode.includes('--ax-sys-gradient-control-hover'), false);
720
- assert.equal(normalMode.includes('--ax-sys-gradient-control-active'), false);
721
- assert.equal(normalMode.includes('--ax-sys-bevel-control-rest'), false);
722
- assert.equal(normalMode.includes('--ax-sys-bevel-control-hover'), false);
723
- assert.equal(normalMode.includes('--ax-sys-bevel-control-active'), false);
724
- assert.equal(normalMode.includes('--ax-sys-highlight-control'), false);
725
- assert.equal(normalMode.includes('--ax-sys-highlight-control-subtle'), false);
726
- assert.equal(normalMode.includes('--ax-sys-backdrop-filter-surface'), false);
727
- assert.equal(normalMode.includes('--ax-sys-surface-fill-opacity'), false);
728
- assert.equal(normalMode.includes('--ax-sys-filter-surface'), false);
729
- assert.equal(normalMode.includes('--ax-sys-effect-control-rest'), false);
730
- assert.equal(normalMode.includes('.ax-appearance-depth'), false);
731
- assert.equal(normalMode.includes('.ax-appearance-translucent'), false);
732
- });
733
-
734
- test('high field boundary is stronger than enhanced field boundary', () => {
735
- const enhanced = read(CONTRAST_FILES.enhanced);
736
- const high = read(CONTRAST_FILES.high);
737
- assert.ok(enhanced.includes('--ax-sys-contrast-inner-border-field: inset 0 0 0 1px rgb(15 23 42 / 0.34)'));
738
- assert.ok(high.includes('--ax-sys-contrast-inner-border-field: inset 0 0 0 1px rgb(15 23 42 / 0.78)'));
739
- assert.ok(enhanced.includes('--ax-sys-contrast-border-field: rgb(15 23 42 / 0.40)'));
740
- assert.ok(high.includes('--ax-sys-contrast-border-field: rgb(15 23 42 / 0.78)'));
741
- });
742
-
743
- test('forced-colors may neutralize decorative appearance tokens', () => {
744
- const high = read(CONTRAST_FILES.high);
745
- const appearances = read(APPEARANCE_FILES.index);
746
- assert.ok(high.includes('@media (forced-colors: active)'));
747
- assert.ok(high.includes('forced-color-adjust: auto'));
748
- assert.match(
749
- high,
750
- /@media \(forced-colors: active\)[\s\S]*?--ax-sys-gradient-control-rest:\s*none;[\s\S]*?--ax-sys-backdrop-filter-surface:\s*none;[\s\S]*?--ax-sys-surface-fill-opacity:\s*1;/,
751
- );
752
- assert.ok(appearances.includes('@media (forced-colors: active)'));
753
- assert.ok(appearances.includes('--ax-sys-backdrop-filter-surface: none'));
754
- assert.ok(appearances.includes('--ax-sys-surface-fill-opacity: 1'));
755
- });
756
-
757
- test('no contrast file contains @apply, component selectors, or .dark', () => {
758
- for (const file of [CONTRAST_FILES.enhanced, CONTRAST_FILES.high, CONTRAST_FILES.index]) {
759
- const css = read(file);
760
- assert.equal(css.includes('@apply'), false, `${file} must not contain @apply`);
761
- assert.equal(css.includes('ax-button'), false, `${file} must not contain component selectors`);
762
- assert.doesNotMatch(css, /(^|[^a-z-])\.dark(\s|,|\{)/);
763
- }
764
- });
765
-
766
- test('button background-image stack still includes contrast overlay via tokens', () => {
767
- const css = read(COMPONENT_FILES.button);
768
- assert.ok(css.includes('var(--ax-comp-button-contrast-surface-overlay-rest)'));
769
- assert.ok(css.includes('var(--ax-sys-contrast-surface-overlay-control)'));
770
- assert.equal(css.includes('rgb(15 23 42'), false, 'Button must not hardcode contrast colors');
771
- assert.equal(css.includes('rgb(255 255 255'), false, 'Button must not hardcode contrast colors');
772
- assert.doesNotMatch(css, /(^|[^a-z-])\.dark(\s|,|\{)/);
773
- });
774
-
775
- test('depth and translucent dark and nested light mappings exist', () => {
776
- const depthCss = read(APPEARANCE_FILES.depth);
777
- const translucentCss = read(APPEARANCE_FILES.translucent);
778
- assert.match(depthCss, /\.ax-dark\.ax-appearance-depth[\s\S]*?--ax-sys-bevel-control-rest:/);
779
- assert.match(depthCss, /\.ax-dark \.ax-light\.ax-appearance-depth[\s\S]*?--ax-sys-bevel-control-rest:/);
780
- assert.match(translucentCss, /\.ax-dark\.ax-appearance-translucent/);
781
- });
782
-
783
- test('button consumes contrast inner-border and surface overlay tokens', () => {
784
- const css = read(COMPONENT_FILES.button);
785
- assert.ok(css.includes('--ax-comp-button-contrast-inner-border-rest'));
786
- assert.ok(css.includes('--ax-comp-button-contrast-surface-overlay-rest'));
787
- assert.ok(css.includes('var(--ax-comp-button-contrast-inner-border-rest)'));
788
- });
789
-
790
- test('editor invalid state uses danger contrast ring, not field border', () => {
791
- const css = read(COMPONENT_FILES.editor);
792
- assert.ok(css.includes('--ax-comp-editor-contrast-inner-border-invalid: var(--ax-sys-contrast-inner-border-invalid)'));
793
- assert.ok(css.includes('.ax-state-error'));
794
- assert.equal(css.includes('outline-danger-light'), false);
795
- assert.ok(css.includes('--ax-sys-contrast-focus-ring-invalid'));
796
- });
797
-
798
- test('editor consumes contrast inner-border tokens via ::after ring', () => {
799
- const css = read(COMPONENT_FILES.editor);
800
- assert.ok(css.includes('--ax-comp-editor-contrast-inner-border-rest'));
801
- assert.ok(css.includes('--ax-comp-editor-contrast-inner-border-focus'));
802
- assert.ok(css.includes('--ax-comp-editor-contrast-ring'));
803
- assert.ok(css.includes('var(--ax-comp-editor-contrast-ring)'));
804
- assert.ok(css.includes('&::after'));
805
- });
806
-
807
- test('floating surfaces opt out of contrast and appearance tokens', () => {
808
- const css = read(join(componentsDir, '_floating-surface.css'));
809
- assert.ok(css.includes(':is(.ax-popup, ax-toast, .ax-overlay-pane, .ax-dialog)'));
810
- assert.ok(css.includes('--ax-sys-contrast-inner-border-surface: 0 0 #0000'));
811
- assert.ok(css.includes('--ax-sys-inner-border-surface: 0 0 #0000'));
812
- });
813
-
814
- test('popup, toast, and dialog overlays use theme-only visual context', () => {
815
- const popup = read(join(workspaceRoot, 'packages/components/popup/src/lib/popup.service.ts'));
816
- const toast = read(join(workspaceRoot, 'packages/components/toast/src/lib/toast.service.ts'));
817
- const dialog = read(join(workspaceRoot, 'packages/components/dialog/src/lib/dialog.service.ts'));
818
- assert.ok(popup.includes("visualContextScope: 'theme'"));
819
- assert.ok(toast.includes("visualContextScope: 'theme'"));
820
- assert.ok(dialog.includes("visualContextScope: 'theme'"));
821
- });
822
-
823
- test('dropdown popovers use theme-only visual context', () => {
824
- const panel = read(join(workspaceRoot, 'packages/components/dropdown/src/lib/dropdown-panel.component.html'));
825
- const box = read(join(workspaceRoot, 'packages/components/dropdown/src/lib/dropdown-box.component.html'));
826
- assert.ok(panel.includes('visualContextScope="theme"'));
827
- assert.ok(box.includes('visualContextScope="theme"'));
828
- });
829
-
830
- test('modal surface owners consume contrast boundary tokens', () => {
831
- const checks = [
832
- [COMPONENT_FILES.notification, '--ax-comp-notification-contrast-inner-border-rest'],
833
- [COMPONENT_FILES.menu, '--ax-comp-menu-contrast-inner-border-rest'],
834
- [COMPONENT_FILES.drawer, '--ax-comp-drawer-contrast-inner-border-rest'],
835
- [COMPONENT_FILES.actionsheet, '--ax-comp-action-sheet-contrast-inner-border-rest'],
836
- ];
837
- for (const [file, token] of checks) {
838
- const css = read(file);
839
- assert.ok(css.includes(token), `${file} missing ${token}`);
840
- assert.ok(css.includes(`var(${token})`), `${file} does not consume ${token}`);
841
- }
842
- });
843
-
844
- test('floating shell owners still declare contrast tokens but inherit neutralized sys values', () => {
845
- for (const file of [COMPONENT_FILES.dropdown, COMPONENT_FILES.popup, COMPONENT_FILES.toast, COMPONENT_FILES.dialog]) {
846
- const css = read(file);
847
- assert.ok(css.includes('--ax-comp-'));
848
- assert.ok(css.includes('contrast-inner-border-rest'));
849
- assert.ok(css.includes('var(--ax-sys-contrast-inner-border-surface)'));
850
- }
851
- });
852
-
853
- test('focus ring utilities remain in default.css', () => {
854
- const css = read(join(themesDir, 'default.css'));
855
- assert.ok(css.includes('@utility ax-focus-ring'));
856
- assert.ok(css.includes('@utility ax-focus-ring-editor'));
857
- });
858
-
859
- test('package exports contrast stylesheets including enhanced', () => {
860
- const pkg = JSON.parse(read(join(workspaceRoot, 'packages/styles/package.json')));
861
- assert.equal(pkg.exports['./themes/contrast'], './themes/contrast/index.css');
862
- assert.equal(pkg.exports['./themes/contrast/index.css'], './themes/contrast/index.css');
863
- assert.equal(pkg.exports['./themes/contrast/enhanced.css'], './themes/contrast/enhanced.css');
864
- assert.equal(pkg.exports['./themes/contrast/high.css'], './themes/contrast/high.css');
865
- assert.equal(pkg.exports['./themes/appearance.css'], './themes/appearance.css');
866
- assert.equal(pkg.exports['./themes/appearances/depth.css'], './themes/appearances/depth.css');
867
- });
868
-
869
- test('showcase and documentation import contrast aggregate', () => {
870
- const showcase = read(join(workspaceRoot, 'apps/showcase/src/styles.css'));
871
- const documentation = read(join(workspaceRoot, 'apps/documentation/src/styles.css'));
872
- const storybook = read(join(workspaceRoot, 'packages/components/.storybook/styles.css'));
873
- assert.ok(showcase.includes('themes/contrast/index.css'));
874
- assert.equal(showcase.includes('themes/contrast/high.css'), false, 'must not duplicate high with aggregate');
875
- assert.ok(documentation.includes('themes/contrast/index.css'));
876
- assert.equal(documentation.includes('themes/contrast/high.css'), false);
877
- assert.ok(storybook.includes('themes/contrast/index.css'));
878
- });
879
-
880
- test('overlay resolver propagates and clears contrast classes including enhanced', () => {
881
- const source = read(join(workspaceRoot, 'packages/cdk/overlay/src/lib/overlay-visual-context.ts'));
882
- assert.ok(source.includes('CONTRAST_CLASSES'));
883
- assert.ok(source.includes('ax-contrast-enhanced'));
884
- assert.ok(source.includes('ax-contrast-high'));
885
- assert.ok(source.includes('context.contrast'));
886
- });
887
-
888
- test('contrast state module validates enhanced and persisted values', () => {
889
- const source = read(join(workspaceRoot, 'libs/shared/services/src/lib/contrast-document.ts'));
890
- assert.ok(source.includes('acorex-showcase-contrast'));
891
- assert.ok(source.includes('isValidContrast'));
892
- assert.ok(source.includes("'enhanced'"));
893
- assert.ok(source.includes("contrast === 'enhanced'"));
894
- assert.ok(source.includes("contrast === 'high'"));
895
- assert.ok(source.includes('ax-contrast-enhanced'));
896
- });
897
-
898
- test('header contrast options include enhanced', () => {
899
- const source = read(join(workspaceRoot, 'libs/shared/components/src/lib/layout-header/layout-header.component.ts'));
900
- assert.ok(source.includes("name: 'enhanced'"));
901
- assert.ok(source.includes("text: 'Enhanced'"));
902
- });
903
-
904
- test('pre-paint initialization validates enhanced', () => {
905
- const source = read(join(workspaceRoot, 'apps/showcase/src/index.html'));
906
- assert.ok(source.includes('enhanced: 1'));
907
- assert.ok(source.includes("contrast === 'enhanced'"));
908
- assert.ok(source.includes('ax-contrast-enhanced'));
909
- assert.equal(source.includes('ax-contrast-standard'), false);
910
- });