@discourser/design-system 0.12.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-GUJTU4IH.js → chunk-F7LHARS4.js} +12 -8
- package/dist/chunk-F7LHARS4.js.map +1 -0
- package/dist/{chunk-5NW4VZJD.js → chunk-M7J7WKJY.js} +192 -243
- package/dist/chunk-M7J7WKJY.js.map +1 -0
- package/dist/{chunk-SQNOAZ2L.cjs → chunk-QC44JPCA.cjs} +192 -243
- package/dist/chunk-QC44JPCA.cjs.map +1 -0
- package/dist/{chunk-X4PNKWSA.cjs → chunk-QP4EJI3G.cjs} +12 -8
- package/dist/chunk-QP4EJI3G.cjs.map +1 -0
- 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/Stepper/Stepper.d.ts +5 -1
- package/dist/components/Stepper/Stepper.d.ts.map +1 -1
- 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/recipes/stepper.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 +14 -8
- package/src/components/Badge.tsx +1 -1
- package/src/components/Stepper/Stepper.tsx +12 -2
- 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/recipes/stepper.ts +15 -1
- 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-5NW4VZJD.js.map +0 -1
- package/dist/chunk-GUJTU4IH.js.map +0 -1
- package/dist/chunk-SQNOAZ2L.cjs.map +0 -1
- package/dist/chunk-X4PNKWSA.cjs.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": {
|
|
@@ -157,7 +157,7 @@
|
|
|
157
157
|
],
|
|
158
158
|
"scripts": {
|
|
159
159
|
"dev": "pnpm docs:generate && storybook dev -p 6006",
|
|
160
|
-
"build": "pnpm build:panda && pnpm build:lib && pnpm build:types",
|
|
160
|
+
"build": "pnpm build:panda && pnpm typecheck && pnpm build:lib && pnpm build:types",
|
|
161
161
|
"build:panda": "panda codegen",
|
|
162
162
|
"build:css": "panda cssgen --outfile dist/styles.css",
|
|
163
163
|
"build:lib": "tsup",
|
|
@@ -177,6 +177,11 @@
|
|
|
177
177
|
"transform:contract-to-dtcg": "tsx scripts/design-language-to-dtcg.ts",
|
|
178
178
|
"docs:generate": "tsx scripts/generate-storybook-docs.ts",
|
|
179
179
|
"typecheck": "tsc --noEmit",
|
|
180
|
+
"verify": "bash scripts/verify.sh",
|
|
181
|
+
"verify:types": "pnpm build:panda && pnpm typecheck",
|
|
182
|
+
"verify:lint": "pnpm lint",
|
|
183
|
+
"verify:test": "pnpm test run",
|
|
184
|
+
"verify:build": "pnpm build",
|
|
180
185
|
"changeset": "changeset",
|
|
181
186
|
"version": "changeset version",
|
|
182
187
|
"ci:version": "pnpm exec changeset version",
|
|
@@ -197,11 +202,11 @@
|
|
|
197
202
|
"@eslint/js": "^9.0.0",
|
|
198
203
|
"@material/material-color-utilities": "^0.3.0",
|
|
199
204
|
"@pandacss/dev": "^1.8.0",
|
|
200
|
-
"@storybook/addon-a11y": "^
|
|
201
|
-
"@storybook/addon-
|
|
202
|
-
"@storybook/addon-
|
|
203
|
-
"@storybook/react": "^
|
|
204
|
-
"@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",
|
|
205
210
|
"@testing-library/jest-dom": "^6.9.1",
|
|
206
211
|
"@testing-library/react": "^16.3.1",
|
|
207
212
|
"@testing-library/user-event": "^14.6.1",
|
|
@@ -217,6 +222,7 @@
|
|
|
217
222
|
"eslint": "^9.0.0",
|
|
218
223
|
"eslint-plugin-react": "^7.35.0",
|
|
219
224
|
"eslint-plugin-react-hooks": "^5.0.0",
|
|
225
|
+
"eslint-plugin-storybook": "10.2.8",
|
|
220
226
|
"husky": "^9.1.7",
|
|
221
227
|
"inquirer": "^13.1.0",
|
|
222
228
|
"jest-axe": "^10.0.0",
|
|
@@ -225,7 +231,7 @@
|
|
|
225
231
|
"prettier": "^3.3.1",
|
|
226
232
|
"react": "^19.0.0",
|
|
227
233
|
"react-dom": "^19.0.0",
|
|
228
|
-
"storybook": "^
|
|
234
|
+
"storybook": "^10.2.8",
|
|
229
235
|
"tsup": "^8.3.0",
|
|
230
236
|
"tsx": "^4.19.0",
|
|
231
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>(
|
|
@@ -23,7 +23,7 @@ export interface StepperRootProps
|
|
|
23
23
|
extends
|
|
24
24
|
Omit<
|
|
25
25
|
React.ComponentPropsWithoutRef<typeof Steps.Root>,
|
|
26
|
-
'count' | 'size' | 'orientation'
|
|
26
|
+
'count' | 'size' | 'orientation' | 'step' | 'defaultStep'
|
|
27
27
|
>,
|
|
28
28
|
Omit<StepperVariantProps, 'colorPalette'> {
|
|
29
29
|
/** Array of step configurations */
|
|
@@ -38,6 +38,10 @@ export interface StepperRootProps
|
|
|
38
38
|
prevLabel?: string;
|
|
39
39
|
/** Next button label */
|
|
40
40
|
nextLabel?: string;
|
|
41
|
+
/** Current step (controlled mode) */
|
|
42
|
+
step?: number;
|
|
43
|
+
/** Default step (uncontrolled mode) */
|
|
44
|
+
defaultStep?: number;
|
|
41
45
|
/** Custom class name */
|
|
42
46
|
className?: string;
|
|
43
47
|
}
|
|
@@ -113,6 +117,8 @@ export const StepperRoot = forwardRef<HTMLDivElement, StepperRootProps>(
|
|
|
113
117
|
nextLabel = 'Next',
|
|
114
118
|
className,
|
|
115
119
|
children,
|
|
120
|
+
step,
|
|
121
|
+
defaultStep,
|
|
116
122
|
...props
|
|
117
123
|
},
|
|
118
124
|
ref,
|
|
@@ -126,6 +132,8 @@ export const StepperRoot = forwardRef<HTMLDivElement, StepperRootProps>(
|
|
|
126
132
|
count={steps.length}
|
|
127
133
|
orientation={stepOrientation as 'horizontal' | 'vertical'}
|
|
128
134
|
className={cx(classes.root, css({ colorPalette }), className)}
|
|
135
|
+
step={step}
|
|
136
|
+
defaultStep={defaultStep}
|
|
129
137
|
{...props}
|
|
130
138
|
>
|
|
131
139
|
<Steps.List className={classes.list}>
|
|
@@ -135,7 +143,9 @@ export const StepperRoot = forwardRef<HTMLDivElement, StepperRootProps>(
|
|
|
135
143
|
<Steps.Indicator className={classes.indicator}>
|
|
136
144
|
{index + 1}
|
|
137
145
|
</Steps.Indicator>
|
|
138
|
-
{step.title &&
|
|
146
|
+
{step.title && (
|
|
147
|
+
<span className={classes.label}>{step.title}</span>
|
|
148
|
+
)}
|
|
139
149
|
</Steps.Trigger>
|
|
140
150
|
{index < steps.length - 1 && (
|
|
141
151
|
<CustomSeparator index={index} className={classes.separator} />
|
|
@@ -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
|
+
});
|