@discourser/design-system 0.14.0 → 0.15.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-F7LHARS4.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { __export } from './chunk-WR5Z5QMQ.js';
|
|
2
2
|
import { ark as ark$1 } from '@ark-ui/react/factory';
|
|
3
|
-
import {
|
|
3
|
+
import { mergeProps, createContext } from '@ark-ui/react/utils';
|
|
4
4
|
import { forwardRef, useMemo, createContext as createContext$1, useContext } from 'react';
|
|
5
5
|
import { styled, createStyleContext, Stack } from 'styled-system/jsx';
|
|
6
6
|
import { group, absoluteCenter, spinner, button, input, inputAddon, inputGroup, textarea, heading, badge, toast, card, dialog, switchComponent, accordion, drawer, tabs, checkbox, radioGroup, select, slider, avatar, progress, skeleton, popover, tooltip, stepper } from 'styled-system/recipes';
|
|
@@ -177,18 +177,18 @@ export declare const Indicator: import("react").ForwardRefExoticComponent<Omit<{
|
|
|
177
177
|
onFocusCapture?: import("react").FocusEventHandler<SVGSVGElement> | undefined;
|
|
178
178
|
onBlur?: import("react").FocusEventHandler<SVGSVGElement> | undefined;
|
|
179
179
|
onBlurCapture?: import("react").FocusEventHandler<SVGSVGElement> | undefined;
|
|
180
|
-
onChange?: import("react").
|
|
181
|
-
onChangeCapture?: import("react").
|
|
180
|
+
onChange?: import("react").ChangeEventHandler<SVGSVGElement, Element> | undefined;
|
|
181
|
+
onChangeCapture?: import("react").ChangeEventHandler<SVGSVGElement, Element> | undefined;
|
|
182
182
|
onBeforeInput?: import("react").InputEventHandler<SVGSVGElement> | undefined;
|
|
183
|
-
onBeforeInputCapture?: import("react").
|
|
184
|
-
onInput?: import("react").
|
|
185
|
-
onInputCapture?: import("react").
|
|
186
|
-
onReset?: import("react").
|
|
187
|
-
onResetCapture?: import("react").
|
|
188
|
-
onSubmit?: import("react").
|
|
189
|
-
onSubmitCapture?: import("react").
|
|
190
|
-
onInvalid?: import("react").
|
|
191
|
-
onInvalidCapture?: import("react").
|
|
183
|
+
onBeforeInputCapture?: import("react").InputEventHandler<SVGSVGElement> | undefined;
|
|
184
|
+
onInput?: import("react").InputEventHandler<SVGSVGElement> | undefined;
|
|
185
|
+
onInputCapture?: import("react").InputEventHandler<SVGSVGElement> | undefined;
|
|
186
|
+
onReset?: import("react").ReactEventHandler<SVGSVGElement> | undefined;
|
|
187
|
+
onResetCapture?: import("react").ReactEventHandler<SVGSVGElement> | undefined;
|
|
188
|
+
onSubmit?: import("react").SubmitEventHandler<SVGSVGElement> | undefined;
|
|
189
|
+
onSubmitCapture?: import("react").SubmitEventHandler<SVGSVGElement> | undefined;
|
|
190
|
+
onInvalid?: import("react").ReactEventHandler<SVGSVGElement> | undefined;
|
|
191
|
+
onInvalidCapture?: import("react").ReactEventHandler<SVGSVGElement> | undefined;
|
|
192
192
|
onLoad?: import("react").ReactEventHandler<SVGSVGElement> | undefined;
|
|
193
193
|
onLoadCapture?: import("react").ReactEventHandler<SVGSVGElement> | undefined;
|
|
194
194
|
onError?: import("react").ReactEventHandler<SVGSVGElement> | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@discourser/design-system",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Aesthetic-agnostic design system with Panda CSS and Ark UI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -193,50 +193,50 @@
|
|
|
193
193
|
"react-dom": ">=19.0.0"
|
|
194
194
|
},
|
|
195
195
|
"dependencies": {
|
|
196
|
-
"@ark-ui/react": "^5.
|
|
196
|
+
"@ark-ui/react": "^5.31.0",
|
|
197
197
|
"clsx": "^2.1.1"
|
|
198
198
|
},
|
|
199
199
|
"devDependencies": {
|
|
200
200
|
"@changesets/changelog-github": "^0.5.2",
|
|
201
201
|
"@changesets/cli": "^2.29.8",
|
|
202
|
-
"@eslint/js": "^9.
|
|
202
|
+
"@eslint/js": "^9.39.2",
|
|
203
203
|
"@material/material-color-utilities": "^0.3.0",
|
|
204
|
-
"@pandacss/dev": "^1.8.
|
|
204
|
+
"@pandacss/dev": "^1.8.1",
|
|
205
205
|
"@storybook/addon-a11y": "^10.2.8",
|
|
206
206
|
"@storybook/addon-docs": "^10.2.8",
|
|
207
207
|
"@storybook/addon-mcp": "^0.2.2",
|
|
208
208
|
"@storybook/react": "^10.2.8",
|
|
209
209
|
"@storybook/react-vite": "^10.2.8",
|
|
210
210
|
"@testing-library/jest-dom": "^6.9.1",
|
|
211
|
-
"@testing-library/react": "^16.3.
|
|
211
|
+
"@testing-library/react": "^16.3.2",
|
|
212
212
|
"@testing-library/user-event": "^14.6.1",
|
|
213
213
|
"@types/inquirer": "^9.0.9",
|
|
214
|
-
"@types/node": "^22.
|
|
215
|
-
"@types/react": "^19.
|
|
216
|
-
"@types/react-dom": "^19.
|
|
217
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
218
|
-
"@typescript-eslint/parser": "^8.
|
|
219
|
-
"@vitejs/plugin-react": "^4.
|
|
214
|
+
"@types/node": "^22.19.11",
|
|
215
|
+
"@types/react": "^19.2.14",
|
|
216
|
+
"@types/react-dom": "^19.2.3",
|
|
217
|
+
"@typescript-eslint/eslint-plugin": "^8.55.0",
|
|
218
|
+
"@typescript-eslint/parser": "^8.55.0",
|
|
219
|
+
"@vitejs/plugin-react": "^4.7.0",
|
|
220
220
|
"@vitest/ui": "^2.1.9",
|
|
221
|
-
"axe-core": "^4.11.
|
|
222
|
-
"eslint": "^9.
|
|
223
|
-
"eslint-plugin-react": "^7.
|
|
224
|
-
"eslint-plugin-react-hooks": "^5.
|
|
221
|
+
"axe-core": "^4.11.1",
|
|
222
|
+
"eslint": "^9.39.2",
|
|
223
|
+
"eslint-plugin-react": "^7.37.5",
|
|
224
|
+
"eslint-plugin-react-hooks": "^5.2.0",
|
|
225
225
|
"eslint-plugin-storybook": "10.2.8",
|
|
226
226
|
"husky": "^9.1.7",
|
|
227
|
-
"inquirer": "^13.
|
|
227
|
+
"inquirer": "^13.2.2",
|
|
228
228
|
"jest-axe": "^10.0.0",
|
|
229
229
|
"jsdom": "^27.4.0",
|
|
230
230
|
"lint-staged": "^16.2.7",
|
|
231
|
-
"prettier": "^3.
|
|
232
|
-
"react": "^19.
|
|
233
|
-
"react-dom": "^19.
|
|
231
|
+
"prettier": "^3.8.1",
|
|
232
|
+
"react": "^19.2.4",
|
|
233
|
+
"react-dom": "^19.2.4",
|
|
234
234
|
"storybook": "^10.2.8",
|
|
235
|
-
"tsup": "^8.
|
|
236
|
-
"tsx": "^4.
|
|
237
|
-
"typescript": "^5.
|
|
238
|
-
"vite": "^6.
|
|
239
|
-
"vitest": "^2.1.
|
|
235
|
+
"tsup": "^8.5.1",
|
|
236
|
+
"tsx": "^4.21.0",
|
|
237
|
+
"typescript": "^5.9.3",
|
|
238
|
+
"vite": "^6.4.1",
|
|
239
|
+
"vitest": "^2.1.9"
|
|
240
240
|
},
|
|
241
241
|
"keywords": [
|
|
242
242
|
"design-system",
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/* eslint-disable no-undef */
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Component Completeness Verification for Translation Documentation
|
|
8
|
+
*
|
|
9
|
+
* This test suite verifies that all exported components from src/components/index.ts
|
|
10
|
+
* are documented in the Figma Translation suite (05-Components.mdx).
|
|
11
|
+
*
|
|
12
|
+
* Prevents missing component documentation when new components are added.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const COMPONENTS_EXPORT_PATH = join(process.cwd(), 'src/components/index.ts');
|
|
16
|
+
const COMPONENTS_MDX_PATH = join(
|
|
17
|
+
process.cwd(),
|
|
18
|
+
'stories/documentation/figma-translation/05-Components.mdx',
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Extract exported component names from src/components/index.ts
|
|
23
|
+
*/
|
|
24
|
+
function extractComponentExports(): string[] {
|
|
25
|
+
const content = readFileSync(COMPONENTS_EXPORT_PATH, 'utf-8');
|
|
26
|
+
const components: string[] = [];
|
|
27
|
+
|
|
28
|
+
// Match: export { ComponentName } from './ComponentName'
|
|
29
|
+
const exportPattern =
|
|
30
|
+
/export\s+\{\s*([A-Z][a-zA-Z0-9]+)(?:,\s*type\s+[A-Z][a-zA-Z0-9]+Props)?\s*\}\s+from\s+['"].\/([A-Z][a-zA-Z0-9]+)['"]/g;
|
|
31
|
+
let match;
|
|
32
|
+
|
|
33
|
+
while ((match = exportPattern.exec(content)) !== null) {
|
|
34
|
+
const componentName = match[1];
|
|
35
|
+
components.push(componentName);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Match: export * as ComponentName from './ComponentName'
|
|
39
|
+
const namespacePattern =
|
|
40
|
+
/export\s+\*\s+as\s+([A-Z][a-zA-Z0-9]+)\s+from\s+['"].\/([A-Z][a-zA-Z0-9]+)['"]/g;
|
|
41
|
+
|
|
42
|
+
while ((match = namespacePattern.exec(content)) !== null) {
|
|
43
|
+
const componentName = match[1];
|
|
44
|
+
components.push(componentName);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Match standalone exports like: export { toaster, Toaster } from './Toast'
|
|
48
|
+
const standalonePattern =
|
|
49
|
+
/export\s+\{\s*([a-zA-Z][a-zA-Z0-9]+)(?:,\s*([a-zA-Z][a-zA-Z0-9]+))?\s*\}\s+from\s+['"].\/[A-Z][a-zA-Z0-9]+['"]/g;
|
|
50
|
+
|
|
51
|
+
while ((match = standalonePattern.exec(content)) !== null) {
|
|
52
|
+
// Add first export (might be lowercase like 'toaster')
|
|
53
|
+
if (match[1] && match[1][0] === match[1][0].toUpperCase()) {
|
|
54
|
+
components.push(match[1]);
|
|
55
|
+
}
|
|
56
|
+
// Add second export if exists
|
|
57
|
+
if (match[2] && match[2][0] === match[2][0].toUpperCase()) {
|
|
58
|
+
components.push(match[2]);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return [...new Set(components)].sort();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if a component is documented in 05-Components.mdx
|
|
67
|
+
*/
|
|
68
|
+
function isComponentDocumented(
|
|
69
|
+
componentName: string,
|
|
70
|
+
mdxContent: string,
|
|
71
|
+
): boolean {
|
|
72
|
+
// Check for heading: ### ComponentName
|
|
73
|
+
const headingPattern = new RegExp(`^###\\s+${componentName}(?:\\s|$)`, 'gm');
|
|
74
|
+
|
|
75
|
+
if (headingPattern.test(mdxContent)) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Special cases: some components may be documented under different names
|
|
80
|
+
// or as part of compound components
|
|
81
|
+
|
|
82
|
+
// InputGroup, InputAddon documented as part of Input section
|
|
83
|
+
if (['InputGroup', 'InputAddon'].includes(componentName)) {
|
|
84
|
+
return /^###\s+Input(?:Group|Addon)/gm.test(mdxContent);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ButtonGroup documented as part of Button or Group section
|
|
88
|
+
if (componentName === 'ButtonGroup') {
|
|
89
|
+
return /^###\s+(?:Button|Group)/gm.test(mdxContent);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Toaster is documented with Toast
|
|
93
|
+
if (componentName === 'Toaster') {
|
|
94
|
+
return /^###\s+Toast/gm.test(mdxContent);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Icon, CloseButton, AbsoluteCenter, Group are utility components
|
|
98
|
+
// They may be in a "Utility" section
|
|
99
|
+
if (
|
|
100
|
+
['Icon', 'CloseButton', 'AbsoluteCenter', 'Group'].includes(componentName)
|
|
101
|
+
) {
|
|
102
|
+
const utilitySection = /^##\s+\d+\.\s+Utility/gm.test(mdxContent);
|
|
103
|
+
const hasHeading = new RegExp(`###\\s+${componentName}`, 'gm').test(
|
|
104
|
+
mdxContent,
|
|
105
|
+
);
|
|
106
|
+
return utilitySection && hasHeading;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Components that are not UI components (type exports, helpers, etc.)
|
|
114
|
+
* These are allowed to not be in the translation docs
|
|
115
|
+
*/
|
|
116
|
+
const UTILITY_EXPORTS_ALLOWLIST = [
|
|
117
|
+
// Type exports (if any appear in the export list)
|
|
118
|
+
'ButtonProps',
|
|
119
|
+
'CardProps',
|
|
120
|
+
|
|
121
|
+
// Internal utilities that aren't visual components
|
|
122
|
+
'toaster', // (lowercase - programmatic API, not a component)
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Normalize component name for comparison
|
|
127
|
+
* Handles cases where config uses different naming than export
|
|
128
|
+
*/
|
|
129
|
+
function normalizeComponentName(name: string): string {
|
|
130
|
+
// switchComponent (in config) vs Switch (in export)
|
|
131
|
+
if (name === 'switchComponent') return 'Switch';
|
|
132
|
+
if (name === 'Switch') return 'Switch';
|
|
133
|
+
|
|
134
|
+
return name;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
describe('Translation Documentation - Component Completeness', () => {
|
|
138
|
+
it('should document all exported components', () => {
|
|
139
|
+
const components = extractComponentExports();
|
|
140
|
+
const mdxContent = readFileSync(COMPONENTS_MDX_PATH, 'utf-8');
|
|
141
|
+
|
|
142
|
+
const undocumentedComponents: string[] = [];
|
|
143
|
+
|
|
144
|
+
components.forEach((componentName) => {
|
|
145
|
+
// Skip utility exports
|
|
146
|
+
if (UTILITY_EXPORTS_ALLOWLIST.includes(componentName)) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Skip lowercase exports (like toaster) - they're programmatic APIs
|
|
151
|
+
if (componentName[0] === componentName[0].toLowerCase()) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const normalized = normalizeComponentName(componentName);
|
|
156
|
+
|
|
157
|
+
if (!isComponentDocumented(normalized, mdxContent)) {
|
|
158
|
+
undocumentedComponents.push(componentName);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (undocumentedComponents.length > 0) {
|
|
163
|
+
const errorMessage =
|
|
164
|
+
`\n\nThe following components are exported from src/components/index.ts but NOT documented in 05-Components.mdx:\n\n` +
|
|
165
|
+
undocumentedComponents.map((name) => ` - ${name}`).join('\n') +
|
|
166
|
+
`\n\nAdd a "### ${undocumentedComponents[0]}" heading section to 05-Components.mdx with:\n` +
|
|
167
|
+
` - Maps from: [Figma/Shadcn equivalent]\n` +
|
|
168
|
+
` - Import: import { ${undocumentedComponents[0]} } from '@discourser/design-system'\n` +
|
|
169
|
+
` - Type: Simple (recipe) or Compound (slot recipe)\n` +
|
|
170
|
+
` - Props table with variants, sizes, etc.\n\n` +
|
|
171
|
+
`See 07-ExtensionGuide.mdx for the full template.\n`;
|
|
172
|
+
|
|
173
|
+
throw new Error(errorMessage);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
expect(undocumentedComponents).toHaveLength(0);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should extract at least 15 component exports', () => {
|
|
180
|
+
// Sanity check: ensure we're actually finding component exports
|
|
181
|
+
const components = extractComponentExports();
|
|
182
|
+
|
|
183
|
+
expect(components.length).toBeGreaterThanOrEqual(15);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should find component headings in 05-Components.mdx', () => {
|
|
187
|
+
// Sanity check: ensure the MDX file has component sections
|
|
188
|
+
const mdxContent = readFileSync(COMPONENTS_MDX_PATH, 'utf-8');
|
|
189
|
+
|
|
190
|
+
// Count ### headings (component sections)
|
|
191
|
+
const headingPattern = /^###\s+[A-Z][a-zA-Z0-9]+/gm;
|
|
192
|
+
const headings = mdxContent.match(headingPattern) || [];
|
|
193
|
+
|
|
194
|
+
// Should have at least 20 component sections
|
|
195
|
+
expect(headings.length).toBeGreaterThanOrEqual(20);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('05-Components.mdx should exist', () => {
|
|
199
|
+
expect(() => {
|
|
200
|
+
readFileSync(COMPONENTS_MDX_PATH, 'utf-8');
|
|
201
|
+
}).not.toThrow();
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('Translation Documentation - Cross-Reference Integrity', () => {
|
|
206
|
+
const TRANSLATION_DOCS_PATH = join(
|
|
207
|
+
process.cwd(),
|
|
208
|
+
'stories/documentation/figma-translation',
|
|
209
|
+
);
|
|
210
|
+
const GUIDELINES_PATH = join(
|
|
211
|
+
process.cwd(),
|
|
212
|
+
'stories/documentation/guidelines',
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const TRANSLATION_FILES = [
|
|
216
|
+
'00-FigmaTranslation.mdx',
|
|
217
|
+
'01-Colors.mdx',
|
|
218
|
+
'02-Typography.mdx',
|
|
219
|
+
'03-Spacing.mdx',
|
|
220
|
+
'04-Shadows-Radii.mdx',
|
|
221
|
+
'05-Components.mdx',
|
|
222
|
+
'06-Layout.mdx',
|
|
223
|
+
'07-ExtensionGuide.mdx',
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Extract cross-reference links from MDX content
|
|
228
|
+
* Matches: [Link Text](/docs/documentation-guidelines-99-component-name--docs)
|
|
229
|
+
*/
|
|
230
|
+
function extractCrossReferences(
|
|
231
|
+
content: string,
|
|
232
|
+
): Array<{ link: string; text: string; line: number }> {
|
|
233
|
+
const references: Array<{ link: string; text: string; line: number }> = [];
|
|
234
|
+
const lines = content.split('\n');
|
|
235
|
+
|
|
236
|
+
const linkPattern =
|
|
237
|
+
/\[([^\]]+)\]\(\/docs\/(documentation-(?:guidelines|figma-translation)-[^)]+)\)/g;
|
|
238
|
+
|
|
239
|
+
lines.forEach((line, lineIndex) => {
|
|
240
|
+
let match;
|
|
241
|
+
while ((match = linkPattern.exec(line)) !== null) {
|
|
242
|
+
references.push({
|
|
243
|
+
text: match[1],
|
|
244
|
+
link: match[2],
|
|
245
|
+
line: lineIndex + 1,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
return references;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
it('should have valid cross-references to Guidelines documentation', () => {
|
|
254
|
+
const brokenLinks: Array<{
|
|
255
|
+
file: string;
|
|
256
|
+
link: string;
|
|
257
|
+
text: string;
|
|
258
|
+
line: number;
|
|
259
|
+
}> = [];
|
|
260
|
+
|
|
261
|
+
TRANSLATION_FILES.forEach((filename) => {
|
|
262
|
+
const filePath = join(TRANSLATION_DOCS_PATH, filename);
|
|
263
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
264
|
+
const references = extractCrossReferences(content);
|
|
265
|
+
|
|
266
|
+
references.forEach((ref) => {
|
|
267
|
+
// Check if it's a guidelines reference
|
|
268
|
+
if (ref.link.includes('documentation-guidelines')) {
|
|
269
|
+
// Extract the guideline file reference
|
|
270
|
+
// Example: documentation-guidelines-99-button--docs -> should check for button.mdx or 99-Button.mdx
|
|
271
|
+
|
|
272
|
+
const guidelineFilePattern = ref.link
|
|
273
|
+
.replace(/^documentation-guidelines-(?:99-)?/, '')
|
|
274
|
+
.replace(/--docs$/, '');
|
|
275
|
+
|
|
276
|
+
// Try common patterns
|
|
277
|
+
const possiblePaths = [
|
|
278
|
+
join(GUIDELINES_PATH, `${guidelineFilePattern}.mdx`),
|
|
279
|
+
join(GUIDELINES_PATH, `99-${guidelineFilePattern}.mdx`),
|
|
280
|
+
join(
|
|
281
|
+
GUIDELINES_PATH,
|
|
282
|
+
`99-${guidelineFilePattern.charAt(0).toUpperCase() + guidelineFilePattern.slice(1)}.mdx`,
|
|
283
|
+
),
|
|
284
|
+
join(GUIDELINES_PATH, 'components', `${guidelineFilePattern}.mdx`),
|
|
285
|
+
join(
|
|
286
|
+
GUIDELINES_PATH,
|
|
287
|
+
'design-tokens',
|
|
288
|
+
`${guidelineFilePattern}.mdx`,
|
|
289
|
+
),
|
|
290
|
+
join(
|
|
291
|
+
GUIDELINES_PATH,
|
|
292
|
+
`${guidelineFilePattern.replace(/-/g, '/')}.mdx`,
|
|
293
|
+
),
|
|
294
|
+
join(GUIDELINES_PATH, `overview-patterns.mdx`), // Special case
|
|
295
|
+
];
|
|
296
|
+
|
|
297
|
+
const exists = possiblePaths.some((path) => {
|
|
298
|
+
try {
|
|
299
|
+
readFileSync(path, 'utf-8');
|
|
300
|
+
return true;
|
|
301
|
+
} catch {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
if (!exists) {
|
|
307
|
+
brokenLinks.push({
|
|
308
|
+
file: filename,
|
|
309
|
+
link: ref.link,
|
|
310
|
+
text: ref.text,
|
|
311
|
+
line: ref.line,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
if (brokenLinks.length > 0) {
|
|
319
|
+
const errorMessage = brokenLinks
|
|
320
|
+
.map(
|
|
321
|
+
(ref) =>
|
|
322
|
+
` ${ref.file}:${ref.line}\n Link: [${ref.text}](/docs/${ref.link})\n Target file not found in stories/documentation/guidelines/`,
|
|
323
|
+
)
|
|
324
|
+
.join('\n\n');
|
|
325
|
+
|
|
326
|
+
console.warn(
|
|
327
|
+
`\n\nWarning: Found ${brokenLinks.length} potentially broken cross-reference link(s):\n\n${errorMessage}\n\n` +
|
|
328
|
+
`This may be a false positive if the Guidelines file exists but wasn't detected.\n` +
|
|
329
|
+
`Verify manually if needed.\n`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// This is a warning, not a hard failure, since file structure may vary
|
|
334
|
+
// Just log the count
|
|
335
|
+
expect(brokenLinks.length).toBeLessThanOrEqual(10);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should extract cross-references from translation files', () => {
|
|
339
|
+
// Sanity check: ensure we're finding cross-references
|
|
340
|
+
const allReferences = TRANSLATION_FILES.flatMap((filename) => {
|
|
341
|
+
const filePath = join(TRANSLATION_DOCS_PATH, filename);
|
|
342
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
343
|
+
return extractCrossReferences(content);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Should find at least 10 cross-reference links
|
|
347
|
+
expect(allReferences.length).toBeGreaterThanOrEqual(10);
|
|
348
|
+
});
|
|
349
|
+
});
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/* eslint-disable no-undef */
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { discourserPandaPreset as preset } from '../index';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Token Accuracy Verification for Translation Documentation
|
|
9
|
+
*
|
|
10
|
+
* This test suite verifies that all tokens referenced in the Figma Translation
|
|
11
|
+
* documentation (files 01-04) actually exist in the design system configuration.
|
|
12
|
+
*
|
|
13
|
+
* Prevents documentation drift where docs reference non-existent tokens.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const TRANSLATION_DOCS_PATH = join(
|
|
17
|
+
process.cwd(),
|
|
18
|
+
'stories/documentation/figma-translation',
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const TRANSLATION_FILES = [
|
|
22
|
+
'01-Colors.mdx',
|
|
23
|
+
'02-Typography.mdx',
|
|
24
|
+
'03-Spacing.mdx',
|
|
25
|
+
'04-Shadows-Radii.mdx',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build a comprehensive list of valid tokens from the Panda preset
|
|
30
|
+
*/
|
|
31
|
+
function buildKnownTokenList(): Set<string> {
|
|
32
|
+
const tokens = new Set<string>();
|
|
33
|
+
|
|
34
|
+
// Semantic tokens from preset
|
|
35
|
+
const semanticColors = preset.theme?.extend?.semanticTokens?.colors || {};
|
|
36
|
+
const semanticShadows = preset.theme?.extend?.semanticTokens?.shadows || {};
|
|
37
|
+
|
|
38
|
+
// Add semantic color tokens (with nested structure)
|
|
39
|
+
Object.keys(semanticColors).forEach((key) => {
|
|
40
|
+
tokens.add(key);
|
|
41
|
+
// Handle nested tokens like fg.default, fg.muted, surface.container, etc.
|
|
42
|
+
const value = semanticColors[key];
|
|
43
|
+
if (typeof value === 'object' && value !== null && 'value' in value) {
|
|
44
|
+
tokens.add(key);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Add semantic shadow tokens
|
|
49
|
+
Object.keys(semanticShadows).forEach((key) => {
|
|
50
|
+
tokens.add(key);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Add semantic radii tokens
|
|
54
|
+
const semanticRadii = preset.theme?.extend?.semanticTokens?.radii || {};
|
|
55
|
+
Object.keys(semanticRadii).forEach((key) => {
|
|
56
|
+
tokens.add(key);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Add textStyle tokens
|
|
60
|
+
const textStyles = preset.theme?.extend?.textStyles || {};
|
|
61
|
+
Object.keys(textStyles).forEach((key) => {
|
|
62
|
+
tokens.add(key);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Add common Radix color scale patterns (primary.1-12, secondary.1-12, etc.)
|
|
66
|
+
const colorPalettes = [
|
|
67
|
+
'primary',
|
|
68
|
+
'secondary',
|
|
69
|
+
'tertiary',
|
|
70
|
+
'error',
|
|
71
|
+
'neutral',
|
|
72
|
+
'gray',
|
|
73
|
+
'red',
|
|
74
|
+
];
|
|
75
|
+
colorPalettes.forEach((palette) => {
|
|
76
|
+
for (let i = 1; i <= 12; i++) {
|
|
77
|
+
tokens.add(`${palette}.${i}`);
|
|
78
|
+
tokens.add(`${palette}.a${i}`); // Alpha variants
|
|
79
|
+
}
|
|
80
|
+
tokens.add(`${palette}.contrast`); // Contrast token (if it exists)
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Add M3 explicit tokens
|
|
84
|
+
const m3Tokens = [
|
|
85
|
+
'm3Primary',
|
|
86
|
+
'm3Secondary',
|
|
87
|
+
'm3Tertiary',
|
|
88
|
+
'm3Primary.container',
|
|
89
|
+
'm3Secondary.container',
|
|
90
|
+
'm3Tertiary.container',
|
|
91
|
+
'onM3Primary',
|
|
92
|
+
'onM3Secondary',
|
|
93
|
+
'onM3Tertiary',
|
|
94
|
+
'onM3Primary.container',
|
|
95
|
+
'onM3Secondary.container',
|
|
96
|
+
'onM3Tertiary.container',
|
|
97
|
+
];
|
|
98
|
+
m3Tokens.forEach((token) => tokens.add(token));
|
|
99
|
+
|
|
100
|
+
// Add surface tokens
|
|
101
|
+
const surfaceTokens = [
|
|
102
|
+
'surface',
|
|
103
|
+
'surface.dim',
|
|
104
|
+
'surface.bright',
|
|
105
|
+
'surface.container',
|
|
106
|
+
'surface.container.low',
|
|
107
|
+
'surface.container.lowest',
|
|
108
|
+
'surface.container.high',
|
|
109
|
+
'surface.container.highest',
|
|
110
|
+
'onSurface',
|
|
111
|
+
'onSurface.variant',
|
|
112
|
+
'outline',
|
|
113
|
+
'outline.variant',
|
|
114
|
+
];
|
|
115
|
+
surfaceTokens.forEach((token) => tokens.add(token));
|
|
116
|
+
|
|
117
|
+
// Add common semantic tokens
|
|
118
|
+
const commonTokens = [
|
|
119
|
+
'canvas',
|
|
120
|
+
'border',
|
|
121
|
+
'fg.default',
|
|
122
|
+
'fg.muted',
|
|
123
|
+
'fg.subtle',
|
|
124
|
+
];
|
|
125
|
+
commonTokens.forEach((token) => tokens.add(token));
|
|
126
|
+
|
|
127
|
+
// Add spacing tokens (numeric values are always valid in Panda)
|
|
128
|
+
// We'll handle these separately in validation
|
|
129
|
+
|
|
130
|
+
// Add font families
|
|
131
|
+
const fontFamilies = ['body', 'display', 'mono'];
|
|
132
|
+
fontFamilies.forEach((token) => tokens.add(token));
|
|
133
|
+
|
|
134
|
+
// Add font weights
|
|
135
|
+
const fontWeights = ['light', 'normal', 'medium', 'semibold', 'bold'];
|
|
136
|
+
fontWeights.forEach((token) => tokens.add(token));
|
|
137
|
+
|
|
138
|
+
// Add common border radius values
|
|
139
|
+
const radiiTokens = [
|
|
140
|
+
'l1',
|
|
141
|
+
'l2',
|
|
142
|
+
'l3',
|
|
143
|
+
'xs',
|
|
144
|
+
'sm',
|
|
145
|
+
'md',
|
|
146
|
+
'lg',
|
|
147
|
+
'xl',
|
|
148
|
+
'2xl',
|
|
149
|
+
'3xl',
|
|
150
|
+
'full',
|
|
151
|
+
'none',
|
|
152
|
+
];
|
|
153
|
+
radiiTokens.forEach((token) => tokens.add(token));
|
|
154
|
+
|
|
155
|
+
return tokens;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Extract token references from MDX content
|
|
160
|
+
* Matches patterns like: bg="token", color="token", shadow="token", etc.
|
|
161
|
+
*/
|
|
162
|
+
function extractTokenReferences(
|
|
163
|
+
content: string,
|
|
164
|
+
_filename: string,
|
|
165
|
+
): Array<{ token: string; line: number; context: string }> {
|
|
166
|
+
const references: Array<{ token: string; line: number; context: string }> =
|
|
167
|
+
[];
|
|
168
|
+
const lines = content.split('\n');
|
|
169
|
+
|
|
170
|
+
// Regex patterns for different token types
|
|
171
|
+
const patterns = [
|
|
172
|
+
// CSS props: bg="...", color="...", borderColor="...", etc.
|
|
173
|
+
/(?:bg|color|borderColor|shadow|rounded|textStyle|fontFamily|fontWeight|p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr|gap|rowGap|columnGap|maxW|minH|h|w|display|flexDir|alignItems|justifyContent|gridTemplateColumns|borderRadius|borderTopRadius|borderBottomRadius|borderLeftRadius|borderRightRadius|borderTopLeftRadius|borderTopRightRadius|borderBottomLeftRadius|borderBottomRightRadius)="([^"]+)"/g,
|
|
174
|
+
|
|
175
|
+
// Semantic tokens in prose: `canvas`, `fg.default`, etc. (backtick wrapped)
|
|
176
|
+
/`((?:fg|bg|surface|outline|canvas|border|onSurface|onM3Primary|onM3Secondary|onM3Tertiary|m3Primary|m3Secondary|m3Tertiary)(?:\.[a-zA-Z0-9]+)*)`/g,
|
|
177
|
+
|
|
178
|
+
// Color palette references: primary.9, error.11, etc. (backtick wrapped)
|
|
179
|
+
/`((?:primary|secondary|tertiary|error|neutral|gray|red)\.(?:\d+|a\d+|contrast))`/g,
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
lines.forEach((line, lineIndex) => {
|
|
183
|
+
patterns.forEach((pattern) => {
|
|
184
|
+
let match;
|
|
185
|
+
while ((match = pattern.exec(line)) !== null) {
|
|
186
|
+
const token = match[1];
|
|
187
|
+
|
|
188
|
+
// Skip empty matches, URLs, and obvious non-tokens
|
|
189
|
+
if (
|
|
190
|
+
!token ||
|
|
191
|
+
token.includes('http') ||
|
|
192
|
+
token.includes('//') ||
|
|
193
|
+
token.includes('.com') ||
|
|
194
|
+
token.includes('.mdx')
|
|
195
|
+
) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
references.push({
|
|
200
|
+
token,
|
|
201
|
+
line: lineIndex + 1,
|
|
202
|
+
context: line.trim().substring(0, 80), // First 80 chars for context
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return references;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Check if a token is valid
|
|
213
|
+
*/
|
|
214
|
+
function isValidToken(token: string, knownTokens: Set<string>): boolean {
|
|
215
|
+
// Direct match
|
|
216
|
+
if (knownTokens.has(token)) {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Numeric spacing values are always valid (4, 6, 8, 16, etc.)
|
|
221
|
+
if (/^\d+(?:\.\d+)?$/.test(token)) {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Percentage values
|
|
226
|
+
if (/^\d+%$/.test(token)) {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Viewport units
|
|
231
|
+
if (/^(?:100vh|100vw|dvh|full|auto)$/.test(token)) {
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// CSS keywords
|
|
236
|
+
const cssKeywords = [
|
|
237
|
+
'flex',
|
|
238
|
+
'grid',
|
|
239
|
+
'block',
|
|
240
|
+
'inline-block',
|
|
241
|
+
'none',
|
|
242
|
+
'inline',
|
|
243
|
+
'inline-flex',
|
|
244
|
+
'inline-grid',
|
|
245
|
+
'column',
|
|
246
|
+
'row',
|
|
247
|
+
'wrap',
|
|
248
|
+
'nowrap',
|
|
249
|
+
'flex-start',
|
|
250
|
+
'flex-end',
|
|
251
|
+
'center',
|
|
252
|
+
'space-between',
|
|
253
|
+
'space-around',
|
|
254
|
+
'space-evenly',
|
|
255
|
+
'stretch',
|
|
256
|
+
'baseline',
|
|
257
|
+
'transparent',
|
|
258
|
+
'white',
|
|
259
|
+
'black',
|
|
260
|
+
'inherit',
|
|
261
|
+
'currentColor',
|
|
262
|
+
'auto',
|
|
263
|
+
'hidden',
|
|
264
|
+
'visible',
|
|
265
|
+
'scroll',
|
|
266
|
+
'unset',
|
|
267
|
+
'initial',
|
|
268
|
+
];
|
|
269
|
+
if (cssKeywords.includes(token)) {
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Breakpoint tokens (base, sm, md, lg, xl, 2xl)
|
|
274
|
+
if (/^(?:base|sm|md|lg|xl|2xl)$/.test(token)) {
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Grid template patterns: "1fr", "repeat(2, 1fr)", "200px 1fr"
|
|
279
|
+
if (/^(?:\d+fr|repeat\(|auto|minmax\(|\d+px)/.test(token)) {
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Specific max-width tokens
|
|
284
|
+
if (
|
|
285
|
+
/^breakpoint-/.test(token) ||
|
|
286
|
+
/^(?:xs|sm|md|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|full)$/.test(token)
|
|
287
|
+
) {
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
describe('Translation Documentation - Token Accuracy', () => {
|
|
295
|
+
const knownTokens = buildKnownTokenList();
|
|
296
|
+
|
|
297
|
+
TRANSLATION_FILES.forEach((filename) => {
|
|
298
|
+
describe(`${filename}`, () => {
|
|
299
|
+
it('should only reference valid tokens', () => {
|
|
300
|
+
const filePath = join(TRANSLATION_DOCS_PATH, filename);
|
|
301
|
+
let content: string;
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
content = readFileSync(filePath, 'utf-8');
|
|
305
|
+
} catch (error) {
|
|
306
|
+
throw new Error(`Failed to read ${filename}: ${error}`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const references = extractTokenReferences(content, filename);
|
|
310
|
+
const invalidReferences: typeof references = [];
|
|
311
|
+
|
|
312
|
+
references.forEach((ref) => {
|
|
313
|
+
if (!isValidToken(ref.token, knownTokens)) {
|
|
314
|
+
invalidReferences.push(ref);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
if (invalidReferences.length > 0) {
|
|
319
|
+
const errorMessage = invalidReferences
|
|
320
|
+
.map(
|
|
321
|
+
(ref) =>
|
|
322
|
+
` Line ${ref.line}: Token "${ref.token}" does not exist\n Context: ${ref.context}`,
|
|
323
|
+
)
|
|
324
|
+
.join('\n\n');
|
|
325
|
+
|
|
326
|
+
throw new Error(
|
|
327
|
+
`\n\nFound ${invalidReferences.length} invalid token(s) in ${filename}:\n\n${errorMessage}\n\n` +
|
|
328
|
+
`Tokens must exist in panda.config.ts, preset files, or be valid CSS values.`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// If we get here, all tokens are valid
|
|
333
|
+
expect(invalidReferences).toHaveLength(0);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should have extracted tokens from all files', () => {
|
|
339
|
+
// Sanity check: ensure we're actually finding tokens
|
|
340
|
+
const allReferences = TRANSLATION_FILES.flatMap((filename) => {
|
|
341
|
+
const filePath = join(TRANSLATION_DOCS_PATH, filename);
|
|
342
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
343
|
+
return extractTokenReferences(content, filename);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// We should find at least 50 token references across all files
|
|
347
|
+
expect(allReferences.length).toBeGreaterThan(50);
|
|
348
|
+
});
|
|
349
|
+
});
|