@astryxdesign/theme-neutral 0.7.5-canary.fccd771 → 0.7.5

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.
@@ -24,13 +24,6 @@ function contrastRatio(foreground: string, background: string): number {
24
24
  return (lighter + 0.05) / (darker + 0.05);
25
25
  }
26
26
 
27
- const statusFill = {
28
- accent: 'var(--astryx-theme-neutral-color-status-fill-accent)',
29
- success: 'var(--astryx-theme-neutral-color-status-fill-success)',
30
- warning: 'var(--astryx-theme-neutral-color-status-fill-warning)',
31
- error: 'var(--astryx-theme-neutral-color-status-fill-error)',
32
- } as const;
33
-
34
27
  const lightDark = (light: string, dark: string) =>
35
28
  `light-dark(${light}, ${dark})`;
36
29
 
@@ -81,17 +74,50 @@ describe('neutral theme palette mappings', () => {
81
74
  neutralPalettes.neutral.light[10],
82
75
  neutralPalettes.neutral.dark[95],
83
76
  ),
84
- '--color-success': lightDark(
77
+ '--color-info': lightDark('#0074e2', '#6d9cfe'),
78
+ '--color-info-muted': lightDark(
79
+ neutralPalettes.blue.light[85],
80
+ `${neutralPalettes.blue.dark[75]}3D`,
81
+ ),
82
+ '--color-on-info': lightDark(
83
+ neutralPalettes.neutral.light[100],
84
+ neutralPalettes.neutral.dark[5],
85
+ ),
86
+ // Strong status fills: Neutral's released Badge fill colours.
87
+ '--color-success': lightDark('#198100', '#64af4c'),
88
+ '--color-warning': '#ffce2f',
89
+ '--color-error': lightDark('#c9303a', '#ff705d'),
90
+ '--color-text-info': lightDark(
91
+ neutralPalettes.blue.light[45],
92
+ neutralPalettes.blue.dark[80],
93
+ ),
94
+ '--color-text-success': lightDark(
95
+ neutralPalettes.green.light[45],
96
+ neutralPalettes.green.light[75],
97
+ ),
98
+ '--color-text-warning': lightDark(
99
+ neutralPalettes.yellow.light[45],
100
+ neutralPalettes.yellow.light[80],
101
+ ),
102
+ '--color-text-error': lightDark(
103
+ neutralPalettes.red.light[45],
104
+ neutralPalettes.red.dark[80],
105
+ ),
106
+ '--color-on-info-muted': lightDark(
107
+ neutralPalettes.blue.light[25],
108
+ neutralPalettes.blue.dark[80],
109
+ ),
110
+ '--color-on-success-muted': lightDark(
85
111
  neutralPalettes.green.light[25],
86
- neutralPalettes.green.light[80],
112
+ neutralPalettes.green.light[75],
87
113
  ),
88
- '--color-warning': lightDark(
114
+ '--color-on-warning-muted': lightDark(
89
115
  neutralPalettes.yellow.light[25],
90
- neutralPalettes.yellow.light[85],
116
+ neutralPalettes.yellow.light[80],
91
117
  ),
92
- '--color-error': lightDark(
118
+ '--color-on-error-muted': lightDark(
93
119
  neutralPalettes.red.light[25],
94
- neutralPalettes.red.dark[85],
120
+ neutralPalettes.red.dark[80],
95
121
  ),
96
122
  '--color-background-red': lightDark(
97
123
  neutralPalettes.red.light[85],
@@ -291,106 +317,114 @@ describe('neutral syntax contrast', () => {
291
317
  });
292
318
  });
293
319
 
294
- describe('neutral theme-local status mappings', () => {
295
- it('preserves the independently approved component status fills', () => {
296
- expect(neutralTheme.localTokens).toMatchObject({
297
- '--astryx-theme-neutral-color-status-fill-accent':
298
- 'light-dark(#0074e2, #6d9cfe)',
299
- '--astryx-theme-neutral-color-status-fill-success':
300
- 'light-dark(#198100, #64af4c)',
301
- '--astryx-theme-neutral-color-status-fill-warning': '#ffce2f',
302
- '--astryx-theme-neutral-color-status-fill-error':
303
- 'light-dark(#c9303a, #ff705d)',
320
+ describe('neutral semantic status roles', () => {
321
+ it('keeps public plain-surface and on-muted roles on reviewed palette stops', () => {
322
+ expect(neutralTheme.tokens).toMatchObject({
323
+ '--color-text-info': lightDark(
324
+ neutralPalettes.blue.light[45],
325
+ neutralPalettes.blue.dark[80],
326
+ ),
327
+ '--color-text-success': lightDark(
328
+ neutralPalettes.green.light[45],
329
+ neutralPalettes.green.light[75],
330
+ ),
331
+ '--color-text-warning': lightDark(
332
+ neutralPalettes.yellow.light[45],
333
+ neutralPalettes.yellow.light[80],
334
+ ),
335
+ '--color-text-error': lightDark(
336
+ neutralPalettes.red.light[45],
337
+ neutralPalettes.red.dark[80],
338
+ ),
339
+ '--color-on-info-muted': lightDark(
340
+ neutralPalettes.blue.light[25],
341
+ neutralPalettes.blue.dark[80],
342
+ ),
343
+ '--color-on-success-muted': lightDark(
344
+ neutralPalettes.green.light[25],
345
+ neutralPalettes.green.light[75],
346
+ ),
347
+ '--color-on-warning-muted': lightDark(
348
+ neutralPalettes.yellow.light[25],
349
+ neutralPalettes.yellow.light[80],
350
+ ),
351
+ '--color-on-error-muted': lightDark(
352
+ neutralPalettes.red.light[25],
353
+ neutralPalettes.red.dark[80],
354
+ ),
304
355
  });
305
- expect(neutralTheme.tokens).not.toHaveProperty(
306
- '--astryx-theme-neutral-color-status-fill-accent',
356
+ expect(neutralTheme.localTokens).not.toHaveProperty(
357
+ '--astryx-theme-neutral-color-status-emphasized-info',
307
358
  );
308
359
  });
309
360
 
310
- it('shares each status fill across the components with the same role', () => {
311
- expect(neutralTheme.components?.badge?.['variant:info']).toMatchObject({
312
- backgroundColor: statusFill.accent,
361
+ it('keeps only the released accent local token, unused by Neutral itself', () => {
362
+ expect(neutralTheme.localTokens).toEqual({
363
+ '--astryx-theme-neutral-color-status-fill-accent': lightDark(
364
+ '#0074e2',
365
+ '#6d9cfe',
366
+ ),
313
367
  });
314
- expect(
315
- neutralTheme.components?.['status-dot']?.['variant:success'],
316
- ).toEqual({
317
- backgroundColor: statusFill.success,
368
+ const css = JSON.stringify({
369
+ tokens: neutralTheme.tokens,
370
+ components: neutralTheme.components,
318
371
  });
372
+ expect(css).not.toContain('--astryx-theme-neutral-');
373
+ });
374
+
375
+ it('uses public semantic roles on plain and pale surfaces while preserving fill roles', () => {
376
+ expect(neutralTheme.tokens).toMatchObject({
377
+ '--color-info': lightDark('#0074e2', '#6d9cfe'),
378
+ // Strong status fills: Neutral's released Badge fill colours.
379
+ '--color-success': lightDark('#198100', '#64af4c'),
380
+ '--color-warning': '#ffce2f',
381
+ '--color-error': lightDark('#c9303a', '#ff705d'),
382
+ });
383
+ expect(neutralTheme.components).not.toHaveProperty('icon');
384
+ expect(neutralTheme.components).not.toHaveProperty('text-input');
385
+ expect(neutralTheme.components).not.toHaveProperty('text-area-counter');
386
+ expect(neutralTheme.components?.badge).not.toHaveProperty('variant:info');
387
+ // Status and accent variants read the portable roles, not overrides.
388
+ expect(neutralTheme.components).not.toHaveProperty('status-dot');
389
+ expect(neutralTheme.components).not.toHaveProperty('step-indicator');
390
+ expect(neutralTheme.components).not.toHaveProperty('avatar-status-dot');
391
+ expect(neutralTheme.components?.['progress-bar']).not.toHaveProperty(
392
+ 'variant:accent',
393
+ );
319
394
  expect(
320
- neutralTheme.components?.['avatar-status-dot']?.['variant:error'],
321
- ).toEqual({backgroundColor: statusFill.error});
322
- expect(
323
- neutralTheme.components?.['step-indicator']?.['status:warning'],
324
- ).toEqual({'--color-warning': statusFill.warning});
325
- expect(
326
- neutralTheme.components?.['progress-bar']?.['variant:accent'],
327
- ).toEqual({
328
- '--color-accent': statusFill.accent,
395
+ neutralTheme.components?.button?.['variant:destructive'],
396
+ ).toMatchObject({
397
+ backgroundColor: 'var(--color-error-muted)',
398
+ color: 'var(--color-on-error-muted)',
329
399
  });
330
400
  });
331
401
 
402
+ it('keeps plain-surface status roles at their required contrast on the page background in light mode', () => {
403
+ const surface = neutralPalettes.neutral.light[95];
404
+ for (const foreground of [
405
+ neutralPalettes.blue.light[45],
406
+ neutralPalettes.green.light[45],
407
+ neutralPalettes.yellow.light[45],
408
+ neutralPalettes.red.light[45],
409
+ ]) {
410
+ expect(contrastRatio(foreground, surface)).toBeGreaterThanOrEqual(4.5);
411
+ }
412
+ });
413
+
332
414
  it('does not adopt the unapproved table row-status target', () => {
333
415
  expect(neutralTheme.components).not.toHaveProperty('table-row-status');
334
416
  });
335
417
  });
336
418
 
337
- describe('neutral Banner tint mappings', () => {
338
- it('uses a dedicated muted info surface without changing categorical blue', () => {
339
- const mutedAccent = 'var(--astryx-theme-neutral-color-status-muted-accent)';
340
- expect(neutralTheme.localTokens).toMatchObject({
341
- '--astryx-theme-neutral-color-status-muted-accent': lightDark(
342
- neutralPalettes.blue.light[85],
343
- `${neutralPalettes.blue.dark[75]}3D`,
344
- ),
345
- });
346
- expect(neutralTheme.components?.banner?.['status:info']).toMatchObject({
347
- '--color-accent-muted': mutedAccent,
348
- });
419
+ describe('neutral Banner and destructive Button', () => {
420
+ it('paint with the portable roles and no Neutral-only overrides', () => {
421
+ expect(neutralTheme.components).not.toHaveProperty('banner');
349
422
  expect(neutralTheme.tokens['--color-background-blue']).toBe(
350
423
  lightDark(neutralPalettes.blue.light[85], neutralPalettes.blue.dark[25]),
351
424
  );
352
- });
353
-
354
- it('keeps the error background on the semantic muted token', () => {
355
- expect(
356
- neutralTheme.components?.banner?.['status:error'],
357
- ).not.toHaveProperty('--color-error-muted');
358
- });
359
-
360
- it('uses light overlays in light mode and dark overlays in dark mode', () => {
361
- expect(neutralTheme.localTokens).toMatchObject({
362
- '--astryx-theme-neutral-color-on-tint-neutral':
363
- 'light-dark(#fafafa4D, #0a0a0a4D)',
364
- '--astryx-theme-neutral-color-on-tint-overlay-hover':
365
- 'light-dark(#fafafa1A, #0a0a0a1A)',
366
- '--astryx-theme-neutral-color-on-tint-overlay-pressed':
367
- 'light-dark(#fafafa33, #0a0a0a33)',
368
- });
369
- expect(neutralTheme.components?.banner?.base).toMatchObject({
370
- '--color-neutral': 'var(--astryx-theme-neutral-color-on-tint-neutral)',
371
- '--color-overlay-hover':
372
- 'var(--astryx-theme-neutral-color-on-tint-overlay-hover)',
373
- '--color-overlay-pressed':
374
- 'var(--astryx-theme-neutral-color-on-tint-overlay-pressed)',
375
- });
376
- });
377
- });
378
-
379
- describe('neutral destructive Button interaction treatment', () => {
380
- it('uses component-scoped red palette overlays', () => {
381
- expect(neutralTheme.localTokens).toMatchObject({
382
- '--astryx-theme-neutral-color-destructive-overlay-hover':
383
- 'light-dark(#ff7f770D, #ee736c0D)',
384
- '--astryx-theme-neutral-color-destructive-overlay-pressed':
385
- 'light-dark(#ff7f771A, #ee736c1A)',
386
- });
387
- expect(
388
- neutralTheme.components?.button?.['variant:destructive'],
389
- ).toMatchObject({
390
- '--color-overlay-hover':
391
- 'var(--astryx-theme-neutral-color-destructive-overlay-hover)',
392
- '--color-overlay-pressed':
393
- 'var(--astryx-theme-neutral-color-destructive-overlay-pressed)',
425
+ expect(neutralTheme.components?.button?.['variant:destructive']).toEqual({
426
+ backgroundColor: 'var(--color-error-muted)',
427
+ color: 'var(--color-on-error-muted)',
394
428
  });
395
429
  });
396
430
  });
@@ -33,40 +33,12 @@ const neutralSyntax = defineSyntaxTheme({
33
33
  });
34
34
 
35
35
  const neutralLocalTokens: Record<string, TokenValue> = {
36
+ // Deprecated: kept with its released name, meaning, and value for Neutral
37
+ // extensions (theme:neutral DEC-1); Neutral itself paints status through the
38
+ // portable --color-* roles and no longer reads it.
36
39
  '--astryx-theme-neutral-color-status-fill-accent': ['#0074e2', '#6d9cfe'],
37
- '--astryx-theme-neutral-color-status-fill-success': ['#198100', '#64af4c'],
38
- '--astryx-theme-neutral-color-status-fill-warning': '#ffce2f',
39
- '--astryx-theme-neutral-color-status-fill-error': ['#c9303a', '#ff705d'],
40
- '--astryx-theme-neutral-color-status-muted-accent': [
41
- blue.light[85],
42
- withAlpha(blue.dark[75], '3D'),
43
- ],
44
- '--astryx-theme-neutral-color-on-tint-neutral': ['#fafafa4D', '#0a0a0a4D'],
45
- '--astryx-theme-neutral-color-on-tint-overlay-hover': [
46
- '#fafafa1A',
47
- '#0a0a0a1A',
48
- ],
49
- '--astryx-theme-neutral-color-on-tint-overlay-pressed': [
50
- '#fafafa33',
51
- '#0a0a0a33',
52
- ],
53
- '--astryx-theme-neutral-color-destructive-overlay-hover': [
54
- withAlpha(red.light[70], '0D'),
55
- withAlpha(red.dark[65], '0D'),
56
- ],
57
- '--astryx-theme-neutral-color-destructive-overlay-pressed': [
58
- withAlpha(red.light[70], '1A'),
59
- withAlpha(red.dark[65], '1A'),
60
- ],
61
40
  };
62
41
 
63
- const statusFill = {
64
- accent: 'var(--astryx-theme-neutral-color-status-fill-accent)',
65
- success: 'var(--astryx-theme-neutral-color-status-fill-success)',
66
- warning: 'var(--astryx-theme-neutral-color-status-fill-warning)',
67
- error: 'var(--astryx-theme-neutral-color-status-fill-error)',
68
- } as const;
69
-
70
42
  export const neutralTheme = defineTheme({
71
43
  name: 'neutral',
72
44
  localTokens: neutralLocalTokens,
@@ -154,15 +126,31 @@ export const neutralTheme = defineTheme({
154
126
  '--color-on-error': [neutral.light[100], neutral.dark[5]],
155
127
  '--color-on-warning': neutral.light[5],
156
128
 
129
+ '--color-info': ['#0074e2', '#6d9cfe'],
130
+ '--color-info-muted': [blue.light[85], withAlpha(blue.dark[75], '3D')],
131
+ '--color-on-info': [neutral.light[100], neutral.dark[5]],
132
+
133
+ '--color-text-info': [blue.light[45], blue.dark[80]],
134
+ '--color-text-success': [green.light[45], green.light[75]],
135
+ '--color-text-warning': [yellow.light[45], yellow.light[80]],
136
+ '--color-text-error': [red.light[45], red.dark[80]],
137
+
138
+ '--color-on-info-muted': [blue.light[25], blue.dark[80]],
139
+ '--color-on-success-muted': [green.light[25], green.light[75]],
140
+ '--color-on-warning-muted': [yellow.light[25], yellow.light[80]],
141
+ '--color-on-error-muted': [red.light[25], red.dark[80]],
142
+
157
143
  // Icon
158
144
  '--color-icon-accent': [neutral.light[10], neutral.dark[95]],
159
145
  '--color-icon-primary': [neutral.light[0], neutral.dark[100]],
160
146
  '--color-icon-secondary': [neutral.light[45], neutral.dark[65]],
161
147
  '--color-icon-disabled': [neutral.light[60], neutral.dark[35]],
162
148
 
163
- '--color-success': [green.light[25], green.light[80]],
164
- '--color-error': [red.light[25], red.dark[85]],
165
- '--color-warning': [yellow.light[25], yellow.light[85]],
149
+ // Strong status fills (architecture:theme-tokens INV11): Neutral's
150
+ // reviewed status-fill colours, with --color-on-<status> above.
151
+ '--color-success': ['#198100', '#64af4c'],
152
+ '--color-warning': '#ffce2f',
153
+ '--color-error': ['#c9303a', '#ff705d'],
166
154
  '--color-success-muted': [green.dark[85], withAlpha(green.light[75], '3D')],
167
155
  '--color-error-muted': [red.light[85], withAlpha(red.dark[75], '3D')],
168
156
  '--color-warning-muted': [
@@ -182,7 +170,7 @@ export const neutralTheme = defineTheme({
182
170
  withAlpha(neutral.light[0], '1A'),
183
171
  withAlpha(neutral.dark[0], '4D'),
184
172
  ],
185
- '--color-tint-hover': ['black', 'white'],
173
+ '--color-tint-hover': [neutral.light[0], neutral.dark[100]],
186
174
 
187
175
  '--color-background-red': [red.light[85], red.dark[25]],
188
176
  '--color-border-red': [red.light[80], red.light[65]],
@@ -288,35 +276,15 @@ export const neutralTheme = defineTheme({
288
276
  button: {
289
277
  'variant:destructive': {
290
278
  backgroundColor: 'var(--color-error-muted)',
291
- color: 'var(--color-error)',
292
- '--color-overlay-hover':
293
- 'var(--astryx-theme-neutral-color-destructive-overlay-hover)',
294
- '--color-overlay-pressed':
295
- 'var(--astryx-theme-neutral-color-destructive-overlay-pressed)',
279
+ color: 'var(--color-on-error-muted)',
296
280
  },
297
281
  },
298
282
 
299
283
  badge: {
300
- 'variant:info': {
301
- backgroundColor: statusFill.accent,
302
- color: 'var(--color-on-accent)',
303
- },
304
284
  'variant:neutral': {
305
285
  backgroundColor: 'var(--color-background-gray)',
306
286
  color: 'var(--color-text-gray)',
307
287
  },
308
- 'variant:success': {
309
- backgroundColor: statusFill.success,
310
- color: 'var(--color-on-success)',
311
- },
312
- 'variant:warning': {
313
- backgroundColor: statusFill.warning,
314
- color: 'var(--color-on-warning)',
315
- },
316
- 'variant:error': {
317
- backgroundColor: statusFill.error,
318
- color: 'var(--color-on-error)',
319
- },
320
288
 
321
289
  'variant:red': {
322
290
  backgroundColor: 'var(--color-background-red)',
@@ -360,18 +328,6 @@ export const neutralTheme = defineTheme({
360
328
  },
361
329
  },
362
330
 
363
- 'status-dot': {
364
- 'variant:success': {backgroundColor: statusFill.success},
365
- 'variant:warning': {backgroundColor: statusFill.warning},
366
- 'variant:error': {backgroundColor: statusFill.error},
367
- 'variant:accent': {backgroundColor: statusFill.accent},
368
- },
369
-
370
- 'avatar-status-dot': {
371
- 'variant:success': {backgroundColor: statusFill.success},
372
- 'variant:error': {backgroundColor: statusFill.error},
373
- },
374
-
375
331
  // Give the Neutral segmented control a roomier inset without changing its
376
332
  // outside height. The selected item stays flat against the tinted track.
377
333
  'segmented-control': {
@@ -394,45 +350,6 @@ export const neutralTheme = defineTheme({
394
350
  },
395
351
  },
396
352
 
397
- banner: {
398
- base: {
399
- '--color-neutral': 'var(--astryx-theme-neutral-color-on-tint-neutral)',
400
- '--color-overlay-hover':
401
- 'var(--astryx-theme-neutral-color-on-tint-overlay-hover)',
402
- '--color-overlay-pressed':
403
- 'var(--astryx-theme-neutral-color-on-tint-overlay-pressed)',
404
- },
405
- 'status:info': {
406
- '--color-accent-muted':
407
- 'var(--astryx-theme-neutral-color-status-muted-accent)',
408
- '--color-text-primary': 'var(--color-text-blue)',
409
- '--color-text-secondary': 'var(--color-text-blue)',
410
- '--color-accent': 'var(--color-text-blue)',
411
- },
412
- 'status:success': {
413
- '--color-text-primary': 'var(--color-text-green)',
414
- '--color-text-secondary': 'var(--color-text-green)',
415
- '--color-success': 'var(--color-text-green)',
416
- },
417
- 'status:warning': {
418
- '--color-text-primary': 'var(--color-text-yellow)',
419
- '--color-text-secondary': 'var(--color-text-yellow)',
420
- '--color-warning': 'var(--color-text-yellow)',
421
- },
422
- 'status:error': {
423
- '--color-text-primary': 'var(--color-text-red)',
424
- '--color-text-secondary': 'var(--color-text-red)',
425
- '--color-error': 'var(--color-text-red)',
426
- },
427
- },
428
-
429
- 'step-indicator': {
430
- 'status:accent': {'--color-accent': statusFill.accent},
431
- 'status:success': {'--color-success': statusFill.success},
432
- 'status:warning': {'--color-warning': statusFill.warning},
433
- 'status:error': {'--color-error': statusFill.error},
434
- },
435
-
436
353
  switch: {
437
354
  base: {
438
355
  '--color-background-gray': 'var(--color-border-emphasized)',
@@ -443,18 +360,6 @@ export const neutralTheme = defineTheme({
443
360
  base: {
444
361
  '--color-background-muted': 'var(--color-border-emphasized)',
445
362
  },
446
- 'variant:accent': {
447
- '--color-accent': statusFill.accent,
448
- },
449
- 'variant:success': {
450
- '--color-success': statusFill.success,
451
- },
452
- 'variant:warning': {
453
- '--color-warning': statusFill.warning,
454
- },
455
- 'variant:error': {
456
- '--color-error': statusFill.error,
457
- },
458
363
  },
459
364
 
460
365
  card: {