@discourser/design-system 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-BQAXPMOR.js → chunk-F7LHARS4.js} +2 -2
- package/dist/{chunk-BQAXPMOR.js.map → chunk-F7LHARS4.js.map} +1 -1
- package/dist/{chunk-ZNAYN5UV.js → chunk-M7J7WKJY.js} +178 -242
- package/dist/chunk-M7J7WKJY.js.map +1 -0
- package/dist/{chunk-XSX6TKJZ.cjs → chunk-QC44JPCA.cjs} +178 -242
- package/dist/chunk-QC44JPCA.cjs.map +1 -0
- package/dist/{chunk-MIBEMJNS.cjs → chunk-QP4EJI3G.cjs} +2 -2
- package/dist/{chunk-MIBEMJNS.cjs.map → chunk-QP4EJI3G.cjs.map} +1 -1
- package/dist/components/Badge.d.ts +1 -1
- package/dist/components/Badge.d.ts.map +1 -1
- package/dist/components/Checkbox.d.ts +2 -2
- package/dist/components/index.cjs +33 -33
- package/dist/components/index.js +1 -1
- package/dist/index.cjs +37 -37
- package/dist/index.js +2 -2
- package/dist/preset/colors/create-palette-bridge.d.ts +45 -0
- package/dist/preset/colors/create-palette-bridge.d.ts.map +1 -0
- package/dist/preset/colors/index.d.ts +13 -926
- package/dist/preset/colors/index.d.ts.map +1 -1
- package/dist/preset/colors/m3-error.d.ts +4 -306
- package/dist/preset/colors/m3-error.d.ts.map +1 -1
- package/dist/preset/colors/m3-primary.d.ts +1 -306
- package/dist/preset/colors/m3-primary.d.ts.map +1 -1
- package/dist/preset/colors/m3-secondary.d.ts +7 -0
- package/dist/preset/colors/m3-secondary.d.ts.map +1 -0
- package/dist/preset/colors/m3-tertiary.d.ts +7 -0
- package/dist/preset/colors/m3-tertiary.d.ts.map +1 -0
- package/dist/preset/index.cjs +2 -2
- package/dist/preset/index.d.ts.map +1 -1
- package/dist/preset/index.js +1 -1
- package/dist/preset/recipes/radio-group.d.ts.map +1 -1
- package/dist/preset/semantic-tokens.d.ts +4 -4
- package/dist/preset/shadows.d.ts +12 -26
- package/dist/preset/shadows.d.ts.map +1 -1
- package/package.json +8 -7
- package/src/components/Badge.tsx +1 -1
- package/src/languages/__tests__/transform.test.ts +100 -0
- package/src/preset/__tests__/shadows.test.ts +83 -0
- package/src/preset/__tests__/token-resolution.test.ts +247 -0
- package/src/preset/colors/create-palette-bridge.ts +156 -0
- package/src/preset/colors/index.ts +4 -0
- package/src/preset/colors/m3-error.ts +12 -77
- package/src/preset/colors/m3-primary.ts +5 -90
- package/src/preset/colors/m3-secondary.ts +13 -0
- package/src/preset/colors/m3-tertiary.ts +13 -0
- package/src/preset/index.ts +1 -6
- package/src/preset/recipes/__tests__/recipe-shadows.test.ts +238 -0
- package/src/preset/recipes/button.ts +3 -3
- package/src/preset/recipes/radio-group.ts +12 -11
- package/src/preset/semantic-tokens.ts +6 -6
- package/src/preset/shadows.ts +12 -26
- package/src/stories/foundations/Colors.mdx +1 -1
- package/src/stories/foundations/Elevation.mdx +37 -8
- package/src/stories/foundations/Spacing.mdx +1 -1
- package/src/stories/foundations/Typography.mdx +1 -1
- package/dist/chunk-XSX6TKJZ.cjs.map +0 -1
- package/dist/chunk-ZNAYN5UV.js.map +0 -1
|
@@ -99,7 +99,7 @@ export declare const m3SemanticTokens: {
|
|
|
99
99
|
};
|
|
100
100
|
};
|
|
101
101
|
};
|
|
102
|
-
|
|
102
|
+
m3Secondary: {
|
|
103
103
|
DEFAULT: {
|
|
104
104
|
value: {
|
|
105
105
|
base: string;
|
|
@@ -113,7 +113,7 @@ export declare const m3SemanticTokens: {
|
|
|
113
113
|
};
|
|
114
114
|
};
|
|
115
115
|
};
|
|
116
|
-
|
|
116
|
+
onM3Secondary: {
|
|
117
117
|
DEFAULT: {
|
|
118
118
|
value: {
|
|
119
119
|
base: string;
|
|
@@ -127,7 +127,7 @@ export declare const m3SemanticTokens: {
|
|
|
127
127
|
};
|
|
128
128
|
};
|
|
129
129
|
};
|
|
130
|
-
|
|
130
|
+
m3Tertiary: {
|
|
131
131
|
DEFAULT: {
|
|
132
132
|
value: {
|
|
133
133
|
base: string;
|
|
@@ -141,7 +141,7 @@ export declare const m3SemanticTokens: {
|
|
|
141
141
|
};
|
|
142
142
|
};
|
|
143
143
|
};
|
|
144
|
-
|
|
144
|
+
onM3Tertiary: {
|
|
145
145
|
DEFAULT: {
|
|
146
146
|
value: {
|
|
147
147
|
base: string;
|
package/dist/preset/shadows.d.ts
CHANGED
|
@@ -1,43 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shadow semantic tokens
|
|
3
|
-
*
|
|
2
|
+
* Shadow semantic tokens - chained to M3 base elevation tokens
|
|
3
|
+
*
|
|
4
|
+
* Three-layer architecture:
|
|
5
|
+
* 1. Base tokens (level0-5): M3 elevation values defined in transform.ts
|
|
6
|
+
* 2. Semantic tokens (xs-2xl): Reference base tokens for consistency
|
|
7
|
+
* 3. Components: Use semantic tokens for flexibility
|
|
4
8
|
*/
|
|
5
9
|
export declare const shadows: {
|
|
6
10
|
xs: {
|
|
7
|
-
value:
|
|
8
|
-
base: string;
|
|
9
|
-
_dark: string;
|
|
10
|
-
};
|
|
11
|
+
value: string;
|
|
11
12
|
};
|
|
12
13
|
sm: {
|
|
13
|
-
value:
|
|
14
|
-
base: string;
|
|
15
|
-
_dark: string;
|
|
16
|
-
};
|
|
14
|
+
value: string;
|
|
17
15
|
};
|
|
18
16
|
md: {
|
|
19
|
-
value:
|
|
20
|
-
base: string;
|
|
21
|
-
_dark: string;
|
|
22
|
-
};
|
|
17
|
+
value: string;
|
|
23
18
|
};
|
|
24
19
|
lg: {
|
|
25
|
-
value:
|
|
26
|
-
base: string;
|
|
27
|
-
_dark: string;
|
|
28
|
-
};
|
|
20
|
+
value: string;
|
|
29
21
|
};
|
|
30
22
|
xl: {
|
|
31
|
-
value:
|
|
32
|
-
base: string;
|
|
33
|
-
_dark: string;
|
|
34
|
-
};
|
|
23
|
+
value: string;
|
|
35
24
|
};
|
|
36
25
|
'2xl': {
|
|
37
|
-
value:
|
|
38
|
-
base: string;
|
|
39
|
-
_dark: string;
|
|
40
|
-
};
|
|
26
|
+
value: string;
|
|
41
27
|
};
|
|
42
28
|
inset: {
|
|
43
29
|
value: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shadows.d.ts","sourceRoot":"","sources":["../../src/preset/shadows.ts"],"names":[],"mappings":"AAEA
|
|
1
|
+
{"version":3,"file":"shadows.d.ts","sourceRoot":"","sources":["../../src/preset/shadows.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;CAyBlB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@discourser/design-system",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Aesthetic-agnostic design system with Panda CSS and Ark UI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -202,11 +202,11 @@
|
|
|
202
202
|
"@eslint/js": "^9.0.0",
|
|
203
203
|
"@material/material-color-utilities": "^0.3.0",
|
|
204
204
|
"@pandacss/dev": "^1.8.0",
|
|
205
|
-
"@storybook/addon-a11y": "^
|
|
206
|
-
"@storybook/addon-
|
|
207
|
-
"@storybook/addon-
|
|
208
|
-
"@storybook/react": "^
|
|
209
|
-
"@storybook/react-vite": "^
|
|
205
|
+
"@storybook/addon-a11y": "^10.2.8",
|
|
206
|
+
"@storybook/addon-docs": "^10.2.8",
|
|
207
|
+
"@storybook/addon-mcp": "^0.2.2",
|
|
208
|
+
"@storybook/react": "^10.2.8",
|
|
209
|
+
"@storybook/react-vite": "^10.2.8",
|
|
210
210
|
"@testing-library/jest-dom": "^6.9.1",
|
|
211
211
|
"@testing-library/react": "^16.3.1",
|
|
212
212
|
"@testing-library/user-event": "^14.6.1",
|
|
@@ -222,6 +222,7 @@
|
|
|
222
222
|
"eslint": "^9.0.0",
|
|
223
223
|
"eslint-plugin-react": "^7.35.0",
|
|
224
224
|
"eslint-plugin-react-hooks": "^5.0.0",
|
|
225
|
+
"eslint-plugin-storybook": "10.2.8",
|
|
225
226
|
"husky": "^9.1.7",
|
|
226
227
|
"inquirer": "^13.1.0",
|
|
227
228
|
"jest-axe": "^10.0.0",
|
|
@@ -230,7 +231,7 @@
|
|
|
230
231
|
"prettier": "^3.3.1",
|
|
231
232
|
"react": "^19.0.0",
|
|
232
233
|
"react-dom": "^19.0.0",
|
|
233
|
-
"storybook": "^
|
|
234
|
+
"storybook": "^10.2.8",
|
|
234
235
|
"tsup": "^8.3.0",
|
|
235
236
|
"tsx": "^4.19.0",
|
|
236
237
|
"typescript": "^5.7.0",
|
package/src/components/Badge.tsx
CHANGED
|
@@ -13,7 +13,7 @@ export interface BadgeProps extends Omit<BaseBadgeProps, 'colorPalette'> {
|
|
|
13
13
|
* The color palette to use for the badge.
|
|
14
14
|
* @default "primary"
|
|
15
15
|
*/
|
|
16
|
-
colorPalette?: 'primary' | 'neutral' | 'error' | 'gray' | 'red' | undefined;
|
|
16
|
+
colorPalette?: 'primary' | 'secondary' | 'tertiary' | 'neutral' | 'error' | 'gray' | 'red' | undefined;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const Badge = forwardRef<HTMLDivElement, BadgeProps>(
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { transformToPandaTheme } from '../transform';
|
|
3
|
+
import { material3Language } from '../material3.language';
|
|
4
|
+
|
|
5
|
+
describe('Design Language Transform', () => {
|
|
6
|
+
const pandaTheme = transformToPandaTheme(material3Language);
|
|
7
|
+
|
|
8
|
+
describe('Shadow base tokens', () => {
|
|
9
|
+
it('should transform elevation.levels to shadow tokens', () => {
|
|
10
|
+
expect(pandaTheme.tokens.shadows).toBeDefined();
|
|
11
|
+
expect(pandaTheme.tokens.shadows).toHaveProperty('level0');
|
|
12
|
+
expect(pandaTheme.tokens.shadows).toHaveProperty('level1');
|
|
13
|
+
expect(pandaTheme.tokens.shadows).toHaveProperty('level2');
|
|
14
|
+
expect(pandaTheme.tokens.shadows).toHaveProperty('level3');
|
|
15
|
+
expect(pandaTheme.tokens.shadows).toHaveProperty('level4');
|
|
16
|
+
expect(pandaTheme.tokens.shadows).toHaveProperty('level5');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should preserve M3 shadow values exactly', () => {
|
|
20
|
+
const expected = material3Language.elevation.levels;
|
|
21
|
+
|
|
22
|
+
expect(pandaTheme.tokens.shadows.level0.value).toBe(expected.level0);
|
|
23
|
+
expect(pandaTheme.tokens.shadows.level1.value).toBe(expected.level1);
|
|
24
|
+
expect(pandaTheme.tokens.shadows.level2.value).toBe(expected.level2);
|
|
25
|
+
expect(pandaTheme.tokens.shadows.level3.value).toBe(expected.level3);
|
|
26
|
+
expect(pandaTheme.tokens.shadows.level4.value).toBe(expected.level4);
|
|
27
|
+
expect(pandaTheme.tokens.shadows.level5.value).toBe(expected.level5);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('level0 should be none (no shadow)', () => {
|
|
31
|
+
expect(pandaTheme.tokens.shadows.level0.value).toBe('none');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('level1-5 should contain rgba shadow values', () => {
|
|
35
|
+
expect(pandaTheme.tokens.shadows.level1.value).toContain('rgba');
|
|
36
|
+
expect(pandaTheme.tokens.shadows.level2.value).toContain('rgba');
|
|
37
|
+
expect(pandaTheme.tokens.shadows.level3.value).toContain('rgba');
|
|
38
|
+
expect(pandaTheme.tokens.shadows.level4.value).toContain('rgba');
|
|
39
|
+
expect(pandaTheme.tokens.shadows.level5.value).toContain('rgba');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('shadow values should follow M3 pattern (two shadows)', () => {
|
|
43
|
+
// M3 shadows typically have two parts: ambient and directional
|
|
44
|
+
const shadowLevels = [1, 2, 3, 4, 5];
|
|
45
|
+
|
|
46
|
+
shadowLevels.forEach(level => {
|
|
47
|
+
const shadowValue = pandaTheme.tokens.shadows[`level${level}`].value;
|
|
48
|
+
const shadowParts = shadowValue.split(',').length;
|
|
49
|
+
|
|
50
|
+
// Should have 2 parts (ambient + directional)
|
|
51
|
+
expect(shadowParts).toBeGreaterThanOrEqual(2);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('Elevation progression', () => {
|
|
57
|
+
it('should have increasing shadow complexity from level1 to level5', () => {
|
|
58
|
+
// Extract shadow strings and check they increase in complexity
|
|
59
|
+
// This is a heuristic test - higher levels should have larger offsets/blur
|
|
60
|
+
|
|
61
|
+
const levels = [1, 2, 3, 4, 5].map(
|
|
62
|
+
level => pandaTheme.tokens.shadows[`level${level}`].value
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Check that each level is different
|
|
66
|
+
const uniqueLevels = new Set(levels);
|
|
67
|
+
expect(uniqueLevels.size).toBe(5);
|
|
68
|
+
|
|
69
|
+
// Check that level1 is simpler than level5 (by string length as proxy)
|
|
70
|
+
expect(levels[0].length).toBeLessThan(levels[4].length);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('Transform function integrity', () => {
|
|
75
|
+
it('should not modify the original language object', () => {
|
|
76
|
+
const original = JSON.stringify(material3Language);
|
|
77
|
+
transformToPandaTheme(material3Language);
|
|
78
|
+
const after = JSON.stringify(material3Language);
|
|
79
|
+
|
|
80
|
+
expect(after).toBe(original);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should return theme with expected structure', () => {
|
|
84
|
+
expect(pandaTheme).toHaveProperty('tokens');
|
|
85
|
+
expect(pandaTheme).toHaveProperty('semanticTokens');
|
|
86
|
+
expect(pandaTheme).toHaveProperty('textStyles');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should transform all token categories', () => {
|
|
90
|
+
expect(pandaTheme.tokens).toHaveProperty('colors');
|
|
91
|
+
expect(pandaTheme.tokens).toHaveProperty('fonts');
|
|
92
|
+
expect(pandaTheme.tokens).toHaveProperty('fontSizes');
|
|
93
|
+
expect(pandaTheme.tokens).toHaveProperty('spacing');
|
|
94
|
+
expect(pandaTheme.tokens).toHaveProperty('radii');
|
|
95
|
+
expect(pandaTheme.tokens).toHaveProperty('shadows');
|
|
96
|
+
expect(pandaTheme.tokens).toHaveProperty('durations');
|
|
97
|
+
expect(pandaTheme.tokens).toHaveProperty('easings');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { shadows } from '../shadows';
|
|
3
|
+
|
|
4
|
+
describe('Shadow Token Architecture', () => {
|
|
5
|
+
describe('Semantic tokens chain to M3 base tokens', () => {
|
|
6
|
+
it('xs should reference shadows.level1', () => {
|
|
7
|
+
expect(shadows.xs.value).toBe('{shadows.level1}');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('sm should reference shadows.level2', () => {
|
|
11
|
+
expect(shadows.sm.value).toBe('{shadows.level2}');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('md should reference shadows.level3', () => {
|
|
15
|
+
expect(shadows.md.value).toBe('{shadows.level3}');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('lg should reference shadows.level4', () => {
|
|
19
|
+
expect(shadows.lg.value).toBe('{shadows.level4}');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('xl should reference shadows.level5', () => {
|
|
23
|
+
expect(shadows.xl.value).toBe('{shadows.level5}');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('2xl should reference shadows.level5', () => {
|
|
27
|
+
expect(shadows['2xl'].value).toBe('{shadows.level5}');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('Utility tokens remain independent', () => {
|
|
32
|
+
it('inset should have base and _dark values (not reference base tokens)', () => {
|
|
33
|
+
expect(shadows.inset.value).toHaveProperty('base');
|
|
34
|
+
expect(shadows.inset.value).toHaveProperty('_dark');
|
|
35
|
+
expect(typeof shadows.inset.value.base).toBe('string');
|
|
36
|
+
expect(shadows.inset.value.base).toContain('inset');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('inset should not reference a level token', () => {
|
|
40
|
+
expect(shadows.inset.value.base).not.toMatch(/\{shadows\.level\d\}/);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('Token coverage', () => {
|
|
45
|
+
it('should have all required semantic shadow tokens', () => {
|
|
46
|
+
const requiredTokens = ['xs', 'sm', 'md', 'lg', 'xl', '2xl', 'inset'];
|
|
47
|
+
const definedTokens = Object.keys(shadows);
|
|
48
|
+
|
|
49
|
+
requiredTokens.forEach(token => {
|
|
50
|
+
expect(definedTokens).toContain(token);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should not have unexpected tokens', () => {
|
|
55
|
+
const allowedTokens = ['xs', 'sm', 'md', 'lg', 'xl', '2xl', 'inset', 'focus-underline', 'inset-border'];
|
|
56
|
+
const definedTokens = Object.keys(shadows);
|
|
57
|
+
|
|
58
|
+
definedTokens.forEach(token => {
|
|
59
|
+
expect(allowedTokens).toContain(token);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('No hardcoded Park UI values', () => {
|
|
65
|
+
it('semantic elevation tokens should not contain hardcoded color references', () => {
|
|
66
|
+
const elevationTokens = ['xs', 'sm', 'md', 'lg', 'xl', '2xl'];
|
|
67
|
+
|
|
68
|
+
elevationTokens.forEach(token => {
|
|
69
|
+
const value = shadows[token as keyof typeof shadows].value;
|
|
70
|
+
|
|
71
|
+
// Should be a string reference, not an object with base/_dark
|
|
72
|
+
expect(typeof value).toBe('string');
|
|
73
|
+
|
|
74
|
+
// Should reference a level token
|
|
75
|
+
expect(value).toMatch(/^\{shadows\.level\d\}$/);
|
|
76
|
+
|
|
77
|
+
// Should NOT contain color references
|
|
78
|
+
expect(value).not.toContain('{colors.');
|
|
79
|
+
expect(value).not.toContain('rgba');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { discourserPandaPreset as preset } from '../index';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Integration tests for token resolution
|
|
6
|
+
* These tests verify that the full Panda config resolves tokens correctly
|
|
7
|
+
*/
|
|
8
|
+
describe('Token Resolution Integration', () => {
|
|
9
|
+
|
|
10
|
+
describe('Shadow token resolution', () => {
|
|
11
|
+
it('preset should include semantic shadow tokens', () => {
|
|
12
|
+
expect(preset.theme?.extend?.semanticTokens?.shadows).toBeDefined();
|
|
13
|
+
|
|
14
|
+
const shadows = preset.theme?.extend?.semanticTokens?.shadows;
|
|
15
|
+
expect(shadows).toHaveProperty('xs');
|
|
16
|
+
expect(shadows).toHaveProperty('sm');
|
|
17
|
+
expect(shadows).toHaveProperty('md');
|
|
18
|
+
expect(shadows).toHaveProperty('lg');
|
|
19
|
+
expect(shadows).toHaveProperty('xl');
|
|
20
|
+
expect(shadows).toHaveProperty('2xl');
|
|
21
|
+
expect(shadows).toHaveProperty('inset');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('preset should include base shadow tokens from M3', () => {
|
|
25
|
+
expect(preset.theme?.extend?.tokens?.shadows).toBeDefined();
|
|
26
|
+
|
|
27
|
+
const shadows = preset.theme?.extend?.tokens?.shadows;
|
|
28
|
+
expect(shadows).toHaveProperty('level0');
|
|
29
|
+
expect(shadows).toHaveProperty('level1');
|
|
30
|
+
expect(shadows).toHaveProperty('level2');
|
|
31
|
+
expect(shadows).toHaveProperty('level3');
|
|
32
|
+
expect(shadows).toHaveProperty('level4');
|
|
33
|
+
expect(shadows).toHaveProperty('level5');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('semantic tokens should reference base tokens using token syntax', () => {
|
|
37
|
+
const semanticShadows = preset.theme?.extend?.semanticTokens?.shadows;
|
|
38
|
+
|
|
39
|
+
expect(semanticShadows?.xs?.value).toBe('{shadows.level1}');
|
|
40
|
+
expect(semanticShadows?.sm?.value).toBe('{shadows.level2}');
|
|
41
|
+
expect(semanticShadows?.md?.value).toBe('{shadows.level3}');
|
|
42
|
+
expect(semanticShadows?.lg?.value).toBe('{shadows.level4}');
|
|
43
|
+
expect(semanticShadows?.xl?.value).toBe('{shadows.level5}');
|
|
44
|
+
expect(semanticShadows?.['2xl']?.value).toBe('{shadows.level5}');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('Recipe shadow usage', () => {
|
|
49
|
+
it('button recipe should be included in preset', () => {
|
|
50
|
+
const recipes = preset.theme?.extend?.recipes;
|
|
51
|
+
expect(recipes).toHaveProperty('button');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('button recipe should use semantic shadow tokens', () => {
|
|
55
|
+
const button = preset.theme?.extend?.recipes?.button;
|
|
56
|
+
// Panda CSS conditional styles (_hover, _active) are not in SystemStyleObject type
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
58
|
+
const elevated = button?.variants?.variant?.elevated as any;
|
|
59
|
+
|
|
60
|
+
expect(elevated?.boxShadow).toBe('sm');
|
|
61
|
+
expect(elevated?._hover?.boxShadow).toBe('md');
|
|
62
|
+
expect(elevated?._active?.boxShadow).toBe('xs');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('Preset structure', () => {
|
|
67
|
+
it('should have required theme extensions', () => {
|
|
68
|
+
expect(preset.theme?.extend).toBeDefined();
|
|
69
|
+
expect(preset.theme?.extend?.tokens).toBeDefined();
|
|
70
|
+
expect(preset.theme?.extend?.semanticTokens).toBeDefined();
|
|
71
|
+
expect(preset.theme?.extend?.recipes).toBeDefined();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should include color palette transformations', () => {
|
|
75
|
+
const colors = preset.theme?.extend?.tokens?.colors;
|
|
76
|
+
|
|
77
|
+
// M3 uses tonal palettes
|
|
78
|
+
expect(colors).toHaveProperty('primary');
|
|
79
|
+
expect(colors).toHaveProperty('secondary');
|
|
80
|
+
expect(colors).toHaveProperty('tertiary');
|
|
81
|
+
expect(colors).toHaveProperty('neutral');
|
|
82
|
+
expect(colors).toHaveProperty('error');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('Token reference integrity', () => {
|
|
87
|
+
it('semantic tokens should only reference existing base tokens', () => {
|
|
88
|
+
const baseShadows = preset.theme?.extend?.tokens?.shadows;
|
|
89
|
+
const semanticShadows = preset.theme?.extend?.semanticTokens?.shadows;
|
|
90
|
+
|
|
91
|
+
const baseTokenNames = Object.keys(baseShadows || {});
|
|
92
|
+
|
|
93
|
+
Object.entries(semanticShadows || {}).forEach(([_name, config]) => {
|
|
94
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
95
|
+
const value = (config as any)?.value;
|
|
96
|
+
|
|
97
|
+
// If it's a token reference, verify the referenced token exists
|
|
98
|
+
if (typeof value === 'string' && value.match(/^\{shadows\./)) {
|
|
99
|
+
const referencedToken = value.replace(/^\{shadows\./, '').replace(/\}$/, '');
|
|
100
|
+
|
|
101
|
+
if (referencedToken.startsWith('level')) {
|
|
102
|
+
expect(baseTokenNames).toContain(referencedToken);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should not have circular token references', () => {
|
|
109
|
+
const semanticShadows = preset.theme?.extend?.semanticTokens?.shadows;
|
|
110
|
+
|
|
111
|
+
// Semantic tokens should reference base tokens, not each other
|
|
112
|
+
Object.entries(semanticShadows || {}).forEach(([_name, config]) => {
|
|
113
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
114
|
+
const value = (config as any)?.value;
|
|
115
|
+
|
|
116
|
+
if (typeof value === 'string' && value.match(/^\{shadows\./)) {
|
|
117
|
+
const referencedToken = value.replace(/^\{shadows\./, '').replace(/\}$/, '');
|
|
118
|
+
|
|
119
|
+
// Should reference level tokens, not other semantic tokens
|
|
120
|
+
expect(referencedToken).toMatch(/^level\d$/);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('inset token should remain independent (not chained to level tokens)', () => {
|
|
126
|
+
const semanticShadows = preset.theme?.extend?.semanticTokens?.shadows;
|
|
127
|
+
const insetToken = semanticShadows?.inset;
|
|
128
|
+
|
|
129
|
+
// STORY-002 acceptance criterion: inset must NOT reference base tokens
|
|
130
|
+
expect(insetToken?.value).toBeDefined();
|
|
131
|
+
|
|
132
|
+
// Should be an object with base/_dark, not a string reference
|
|
133
|
+
expect(typeof insetToken?.value).toBe('object');
|
|
134
|
+
expect(insetToken?.value).toHaveProperty('base');
|
|
135
|
+
expect(insetToken?.value).toHaveProperty('_dark');
|
|
136
|
+
|
|
137
|
+
// Should NOT contain token references
|
|
138
|
+
const insetString = JSON.stringify(insetToken);
|
|
139
|
+
expect(insetString).not.toMatch(/\{shadows\.level\d\}/);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('utility tokens (focus-underline, inset-border) should remain independent', () => {
|
|
143
|
+
const semanticShadows = preset.theme?.extend?.semanticTokens?.shadows;
|
|
144
|
+
|
|
145
|
+
const focusUnderline = semanticShadows?.['focus-underline'];
|
|
146
|
+
const insetBorder = semanticShadows?.['inset-border'];
|
|
147
|
+
|
|
148
|
+
// TODO: STORY-003 will add these utility tokens
|
|
149
|
+
// For now, they should NOT be defined (current behavior)
|
|
150
|
+
expect(focusUnderline).toBeUndefined();
|
|
151
|
+
expect(insetBorder).toBeUndefined();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('Design system consistency', () => {
|
|
156
|
+
it('all shadow-using recipes should use semantic or custom values', () => {
|
|
157
|
+
const recipes = preset.theme?.extend?.recipes || {};
|
|
158
|
+
const slotRecipes = preset.theme?.extend?.slotRecipes || {};
|
|
159
|
+
|
|
160
|
+
// Name mapping for recipes registered under different names
|
|
161
|
+
const nameMapping: Record<string, string> = {
|
|
162
|
+
'switch': 'switchComponent',
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const recipesWithShadows = [
|
|
166
|
+
'button',
|
|
167
|
+
'card',
|
|
168
|
+
'dialog',
|
|
169
|
+
'drawer',
|
|
170
|
+
'popover',
|
|
171
|
+
'select',
|
|
172
|
+
'slider',
|
|
173
|
+
'switch',
|
|
174
|
+
'toast',
|
|
175
|
+
'tooltip',
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
recipesWithShadows.forEach(recipeName => {
|
|
179
|
+
// Try to find in recipes first, then slotRecipes, accounting for name mapping
|
|
180
|
+
const mappedName = nameMapping[recipeName] || recipeName;
|
|
181
|
+
const recipe = recipes[recipeName as keyof typeof recipes] ||
|
|
182
|
+
slotRecipes[mappedName as keyof typeof slotRecipes];
|
|
183
|
+
|
|
184
|
+
if (!recipe) {
|
|
185
|
+
throw new Error(`Recipe ${recipeName} (mapped to ${mappedName}) not found in recipes or slotRecipes`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const recipeString = JSON.stringify(recipe);
|
|
189
|
+
|
|
190
|
+
// Should NOT use base level tokens directly
|
|
191
|
+
expect(recipeString).not.toMatch(/['"]level[0-5]['"]/);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('ALL recipes in preset should avoid direct base token usage (system-wide check)', () => {
|
|
196
|
+
const recipes = preset.theme?.extend?.recipes || {};
|
|
197
|
+
const slotRecipes = preset.theme?.extend?.slotRecipes || {};
|
|
198
|
+
|
|
199
|
+
// Check EVERY recipe in the preset (both simple recipes and slot recipes)
|
|
200
|
+
const allRecipes = { ...recipes, ...slotRecipes };
|
|
201
|
+
|
|
202
|
+
Object.entries(allRecipes).forEach(([recipeName, recipe]) => {
|
|
203
|
+
if (!recipe) return;
|
|
204
|
+
|
|
205
|
+
const recipeString = JSON.stringify(recipe);
|
|
206
|
+
|
|
207
|
+
// No recipe should use level0-5 directly
|
|
208
|
+
const hasBaseTokens = recipeString.match(/['"]level[0-5]['"]/);
|
|
209
|
+
|
|
210
|
+
if (hasBaseTokens) {
|
|
211
|
+
// Fail with clear message showing which recipe broke the rule
|
|
212
|
+
throw new Error(`Recipe ${recipeName} contains base token reference: ${hasBaseTokens[0]}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
expect(recipeString).not.toMatch(/boxShadow['"]?\s*:\s*['"]level\d/);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('utility shadow tokens should be used in appropriate recipes', () => {
|
|
220
|
+
const recipes = preset.theme?.extend?.recipes || {};
|
|
221
|
+
|
|
222
|
+
// TODO: STORY-003 will migrate to utility tokens (focus-underline, inset-border)
|
|
223
|
+
// For now, recipes use inline values (current behavior)
|
|
224
|
+
|
|
225
|
+
// Input currently uses inline '0 1px 0 0 var(--shadow-color)'
|
|
226
|
+
const input = recipes.input;
|
|
227
|
+
if (input) {
|
|
228
|
+
const inputString = JSON.stringify(input);
|
|
229
|
+
expect(inputString).toContain('0 1px 0 0 var(--shadow-color)');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Textarea currently uses inline '0 1px 0 0 var(--shadow-color)'
|
|
233
|
+
const textarea = recipes.textarea;
|
|
234
|
+
if (textarea) {
|
|
235
|
+
const textareaString = JSON.stringify(textarea);
|
|
236
|
+
expect(textareaString).toContain('0 1px 0 0 var(--shadow-color)');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// RadioGroup currently uses inline 'inset 0 0 0 1px var(--shadow-color)'
|
|
240
|
+
const radioGroup = recipes.radioGroup || recipes['radio-group'];
|
|
241
|
+
if (radioGroup) {
|
|
242
|
+
const radioString = JSON.stringify(radioGroup);
|
|
243
|
+
expect(radioString).toContain('inset 0 0 0 1px var(--shadow-color)');
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|