@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.
@@ -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 { createContext, mergeProps } from '@ark-ui/react/utils';
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").FormEventHandler<SVGSVGElement> | undefined;
181
- onChangeCapture?: import("react").FormEventHandler<SVGSVGElement> | undefined;
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").FormEventHandler<SVGSVGElement> | undefined;
184
- onInput?: import("react").FormEventHandler<SVGSVGElement> | undefined;
185
- onInputCapture?: import("react").FormEventHandler<SVGSVGElement> | undefined;
186
- onReset?: import("react").FormEventHandler<SVGSVGElement> | undefined;
187
- onResetCapture?: import("react").FormEventHandler<SVGSVGElement> | undefined;
188
- onSubmit?: import("react").FormEventHandler<SVGSVGElement> | undefined;
189
- onSubmitCapture?: import("react").FormEventHandler<SVGSVGElement> | undefined;
190
- onInvalid?: import("react").FormEventHandler<SVGSVGElement> | undefined;
191
- onInvalidCapture?: import("react").FormEventHandler<SVGSVGElement> | undefined;
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.14.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.30.0",
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.0.0",
202
+ "@eslint/js": "^9.39.2",
203
203
  "@material/material-color-utilities": "^0.3.0",
204
- "@pandacss/dev": "^1.8.0",
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.1",
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.0.0",
215
- "@types/react": "^19.0.0",
216
- "@types/react-dom": "^19.0.0",
217
- "@typescript-eslint/eslint-plugin": "^8.0.0",
218
- "@typescript-eslint/parser": "^8.0.0",
219
- "@vitejs/plugin-react": "^4.3.0",
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.0",
222
- "eslint": "^9.0.0",
223
- "eslint-plugin-react": "^7.35.0",
224
- "eslint-plugin-react-hooks": "^5.0.0",
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.1.0",
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.3.1",
232
- "react": "^19.0.0",
233
- "react-dom": "^19.0.0",
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.3.0",
236
- "tsx": "^4.19.0",
237
- "typescript": "^5.7.0",
238
- "vite": "^6.0.0",
239
- "vitest": "^2.1.0"
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
+ });