@fragments-sdk/cli 0.5.2 → 0.7.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/bin.js +996 -79
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-ICAIQ57V.js → chunk-6JBGU74P.js} +5 -3
- package/dist/chunk-6JBGU74P.js.map +1 -0
- package/dist/chunk-7OPWMLOE.js +1625 -0
- package/dist/chunk-7OPWMLOE.js.map +1 -0
- package/dist/{chunk-2H2JAA3U.js → chunk-CVXKXVOY.js} +3 -3
- package/dist/{chunk-2H2JAA3U.js.map → chunk-CVXKXVOY.js.map} +1 -1
- package/dist/{chunk-IOJE35DZ.js → chunk-NWQ4CJOQ.js} +3 -3
- package/dist/{chunk-2DJH4F4P.js → chunk-RVRTRESS.js} +3 -3
- package/dist/{chunk-V7YLRR4C.js → chunk-TJ34N7C7.js} +41 -4
- package/dist/{chunk-V7YLRR4C.js.map → chunk-TJ34N7C7.js.map} +1 -1
- package/dist/{chunk-XNWDI6UT.js → chunk-XHUDJNN3.js} +5 -5
- package/dist/{core-DKHB7FYV.js → core-W2HYIQW6.js} +4 -4
- package/dist/{generate-KL24VZVD.js → generate-LMTISDIJ.js} +5 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +15 -7
- package/dist/index.js.map +1 -1
- package/dist/{init-NION5S3M.js → init-7CHRKQ7P.js} +5 -5
- package/dist/mcp-bin.js +8 -220
- package/dist/mcp-bin.js.map +1 -1
- package/dist/scan-WY23TJCP.js +12 -0
- package/dist/{service-RWUMZ3EW.js → service-T2L7VLTE.js} +5 -5
- package/dist/static-viewer-GBR7YNF3.js +12 -0
- package/dist/{test-ECPEXFDN.js → test-OJRXNDO2.js} +4 -4
- package/dist/{tokens-ITADYVPF.js → tokens-3BWDESVM.js} +6 -6
- package/dist/viewer-SUFOISZM.js +1822 -0
- package/dist/viewer-SUFOISZM.js.map +1 -0
- package/package.json +6 -5
- package/src/bin.ts +31 -0
- package/src/build.ts +147 -13
- package/src/cli-commands.ts +18 -0
- package/src/commands/__tests__/a11y-scoring.test.ts +278 -0
- package/src/commands/a11y-report.ts +625 -0
- package/src/commands/a11y.ts +168 -14
- package/src/commands/build.ts +16 -0
- package/src/commands/graph.ts +274 -0
- package/src/core/auto-props.ts +464 -0
- package/src/core/composition.ts +64 -1
- package/src/core/graph-extractor.test.ts +542 -0
- package/src/core/graph-extractor.ts +601 -0
- package/src/core/importAnalyzer.ts +5 -0
- package/src/core/schema.ts +2 -0
- package/src/core/types.ts +3 -1
- package/src/index.ts +4 -0
- package/src/mcp/server.ts +13 -220
- package/src/theme/__tests__/component-contrast.test.ts +338 -0
- package/src/theme/__tests__/contrast-validation.test.ts +326 -0
- package/src/theme/contrast.test.ts +331 -0
- package/src/theme/contrast.ts +246 -0
- package/src/theme/generator.ts +213 -1
- package/src/theme/index.ts +16 -0
- package/src/theme/types.ts +51 -0
- package/src/viewer/__tests__/a11y-fixes.test.ts +358 -0
- package/src/viewer/__tests__/viewer-integration.test.ts +2 -7
- package/src/viewer/components/AccessibilityPanel.tsx +493 -433
- package/src/viewer/components/ActionCapture.tsx +1 -1
- package/src/viewer/components/ActionsPanel.tsx +142 -183
- package/src/viewer/components/App.tsx +276 -183
- package/src/viewer/components/BottomPanel.tsx +40 -80
- package/src/viewer/components/CodePanel.tsx +9 -87
- package/src/viewer/components/CommandPalette.tsx +117 -74
- package/src/viewer/components/ComponentGraph.tsx +143 -126
- package/src/viewer/components/ComponentHeader.tsx +46 -43
- package/src/viewer/components/ContractPanel.tsx +124 -117
- package/src/viewer/components/ErrorBoundary.tsx +47 -35
- package/src/viewer/components/FigmaEmbed.tsx +18 -13
- package/src/viewer/components/FragmentEditor.tsx +126 -63
- package/src/viewer/components/HealthDashboard.tsx +146 -171
- package/src/viewer/components/HmrStatusIndicator.tsx +31 -41
- package/src/viewer/components/Icons.tsx +151 -98
- package/src/viewer/components/InteractionsPanel.tsx +317 -264
- package/src/viewer/components/IsolatedPreviewFrame.tsx +52 -27
- package/src/viewer/components/IsolatedRender.tsx +12 -6
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +34 -70
- package/src/viewer/components/LandingPage.tsx +285 -305
- package/src/viewer/components/Layout.tsx +12 -10
- package/src/viewer/components/LeftSidebar.tsx +103 -155
- package/src/viewer/components/MultiViewportPreview.tsx +254 -63
- package/src/viewer/components/PreviewArea.tsx +113 -44
- package/src/viewer/components/PreviewFrameHost.tsx +36 -6
- package/src/viewer/components/PreviewPane.tsx +2 -3
- package/src/viewer/components/PreviewToolbar.tsx +109 -105
- package/src/viewer/components/PropsEditor.tsx +154 -74
- package/src/viewer/components/PropsTable.tsx +95 -82
- package/src/viewer/components/RelationsSection.tsx +71 -40
- package/src/viewer/components/ResizablePanel.tsx +158 -55
- package/src/viewer/components/RightSidebar.tsx +46 -56
- package/src/viewer/components/ScreenshotButton.tsx +12 -12
- package/src/viewer/components/SkeletonLoader.tsx +99 -83
- package/src/viewer/components/StoryRenderer.tsx +4 -11
- package/src/viewer/components/Toast.tsx +3 -67
- package/src/viewer/components/TokenStylePanel.tsx +136 -118
- package/src/viewer/components/UsageSection.tsx +26 -26
- package/src/viewer/components/VariantMatrix.tsx +140 -47
- package/src/viewer/components/VariantTabs.tsx +24 -68
- package/src/viewer/components/ViewportSelector.tsx +121 -114
- package/src/viewer/constants/ui.ts +23 -22
- package/src/viewer/entry.tsx +8 -3
- package/src/viewer/index.ts +3 -6
- package/src/viewer/preview-frame.html +43 -18
- package/src/viewer/server.ts +7 -16
- package/src/viewer/styles/globals.css +46 -85
- package/src/viewer/utils/a11y-fixes.ts +53 -30
- package/dist/chunk-ICAIQ57V.js.map +0 -1
- package/dist/chunk-U4GQ2JTD.js +0 -832
- package/dist/chunk-U4GQ2JTD.js.map +0 -1
- package/dist/scan-ESEXV7LF.js +0 -12
- package/dist/static-viewer-O37MJ5B6.js +0 -12
- package/dist/viewer-YDGFDTK5.js +0 -11104
- package/dist/viewer-YDGFDTK5.js.map +0 -1
- package/src/viewer/postcss.config.js +0 -6
- package/src/viewer/tailwind.config.js +0 -37
- /package/dist/{chunk-IOJE35DZ.js.map → chunk-NWQ4CJOQ.js.map} +0 -0
- /package/dist/{chunk-2DJH4F4P.js.map → chunk-RVRTRESS.js.map} +0 -0
- /package/dist/{chunk-XNWDI6UT.js.map → chunk-XHUDJNN3.js.map} +0 -0
- /package/dist/{core-DKHB7FYV.js.map → core-W2HYIQW6.js.map} +0 -0
- /package/dist/{generate-KL24VZVD.js.map → generate-LMTISDIJ.js.map} +0 -0
- /package/dist/{init-NION5S3M.js.map → init-7CHRKQ7P.js.map} +0 -0
- /package/dist/{scan-ESEXV7LF.js.map → scan-WY23TJCP.js.map} +0 -0
- /package/dist/{service-RWUMZ3EW.js.map → service-T2L7VLTE.js.map} +0 -0
- /package/dist/{static-viewer-O37MJ5B6.js.map → static-viewer-GBR7YNF3.js.map} +0 -0
- /package/dist/{test-ECPEXFDN.js.map → test-OJRXNDO2.js.map} +0 -0
- /package/dist/{tokens-ITADYVPF.js.map → tokens-3BWDESVM.js.map} +0 -0
package/src/mcp/server.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type VerifyResult,
|
|
14
14
|
type Theme,
|
|
15
15
|
} from '../core/index.js';
|
|
16
|
+
import { buildMcpTools, buildToolNames, CLI_TOOL_EXTENSIONS } from '@fragments-sdk/context/mcp-tools';
|
|
16
17
|
// ../service is lazy-imported to avoid requiring playwright at startup.
|
|
17
18
|
// Visual tools (render, fix) load it on first use.
|
|
18
19
|
type ServiceModule = typeof import('../service/index.js');
|
|
@@ -36,17 +37,17 @@ import { createRequire } from 'node:module';
|
|
|
36
37
|
import { projectFields } from './utils.js';
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
|
-
* MCP Tool names
|
|
40
|
+
* MCP Tool names & definitions (from shared source of truth)
|
|
40
41
|
*/
|
|
41
|
-
const TOOL_NAMES = {
|
|
42
|
-
discover:
|
|
43
|
-
inspect:
|
|
44
|
-
blocks:
|
|
45
|
-
tokens:
|
|
46
|
-
implement:
|
|
47
|
-
render:
|
|
48
|
-
fix:
|
|
49
|
-
}
|
|
42
|
+
const TOOL_NAMES = buildToolNames(BRAND.nameLower) as Record<string, string> & {
|
|
43
|
+
discover: string;
|
|
44
|
+
inspect: string;
|
|
45
|
+
blocks: string;
|
|
46
|
+
tokens: string;
|
|
47
|
+
implement: string;
|
|
48
|
+
render: string;
|
|
49
|
+
fix: string;
|
|
50
|
+
};
|
|
50
51
|
|
|
51
52
|
/**
|
|
52
53
|
* Placeholder patterns to filter out from usage text.
|
|
@@ -278,215 +279,7 @@ export interface McpServerConfig {
|
|
|
278
279
|
threshold?: number;
|
|
279
280
|
}
|
|
280
281
|
|
|
281
|
-
|
|
282
|
-
* Tool definitions for the MCP server — 5 consolidated tools
|
|
283
|
-
*/
|
|
284
|
-
const TOOLS: Tool[] = [
|
|
285
|
-
{
|
|
286
|
-
name: TOOL_NAMES.discover,
|
|
287
|
-
description: `Discover components in the design system. Use with no params to list all components. Use 'useCase' for AI-powered suggestions. Use 'component' to find alternatives. Use 'compact' for a token-efficient overview.`,
|
|
288
|
-
inputSchema: {
|
|
289
|
-
type: 'object' as const,
|
|
290
|
-
properties: {
|
|
291
|
-
useCase: {
|
|
292
|
-
type: 'string',
|
|
293
|
-
description: 'Description of what you want to build — returns ranked suggestions (e.g., "form for user email input", "button to submit data")',
|
|
294
|
-
},
|
|
295
|
-
component: {
|
|
296
|
-
type: 'string',
|
|
297
|
-
description: 'Component name to find alternatives for (e.g., "Button")',
|
|
298
|
-
},
|
|
299
|
-
category: {
|
|
300
|
-
type: 'string',
|
|
301
|
-
description: 'Filter by category (e.g., "actions", "forms", "layout")',
|
|
302
|
-
},
|
|
303
|
-
search: {
|
|
304
|
-
type: 'string',
|
|
305
|
-
description: 'Search term to filter by name, description, or tags',
|
|
306
|
-
},
|
|
307
|
-
status: {
|
|
308
|
-
type: 'string',
|
|
309
|
-
enum: ['stable', 'beta', 'deprecated', 'experimental'],
|
|
310
|
-
description: 'Filter by component status',
|
|
311
|
-
},
|
|
312
|
-
format: {
|
|
313
|
-
type: 'string',
|
|
314
|
-
enum: ['markdown', 'json'],
|
|
315
|
-
description: 'Output format for context mode (default: markdown)',
|
|
316
|
-
},
|
|
317
|
-
compact: {
|
|
318
|
-
type: 'boolean',
|
|
319
|
-
description: 'If true, returns minimal output (just component names and categories)',
|
|
320
|
-
},
|
|
321
|
-
includeCode: {
|
|
322
|
-
type: 'boolean',
|
|
323
|
-
description: 'If true, includes code examples for each variant',
|
|
324
|
-
},
|
|
325
|
-
includeRelations: {
|
|
326
|
-
type: 'boolean',
|
|
327
|
-
description: 'If true, includes component relationships',
|
|
328
|
-
},
|
|
329
|
-
},
|
|
330
|
-
},
|
|
331
|
-
},
|
|
332
|
-
{
|
|
333
|
-
name: TOOL_NAMES.inspect,
|
|
334
|
-
description: `Get detailed information about a specific component: props, usage guidelines, code examples, accessibility — all in one call. Use 'fields' to request only specific data for token efficiency.`,
|
|
335
|
-
inputSchema: {
|
|
336
|
-
type: 'object' as const,
|
|
337
|
-
properties: {
|
|
338
|
-
component: {
|
|
339
|
-
type: 'string',
|
|
340
|
-
description: 'Component name (e.g., "Button", "Input")',
|
|
341
|
-
},
|
|
342
|
-
fields: {
|
|
343
|
-
type: 'array',
|
|
344
|
-
items: { type: 'string' },
|
|
345
|
-
description: 'Specific fields to return (e.g., ["meta", "usage.when", "contract.propsSummary", "props", "examples", "guidelines"]). If omitted, returns everything. Supports dot notation.',
|
|
346
|
-
},
|
|
347
|
-
variant: {
|
|
348
|
-
type: 'string',
|
|
349
|
-
description: 'Filter examples to a specific variant name (e.g., "Default", "Primary")',
|
|
350
|
-
},
|
|
351
|
-
maxExamples: {
|
|
352
|
-
type: 'number',
|
|
353
|
-
description: 'Maximum number of code examples to return (default: all)',
|
|
354
|
-
},
|
|
355
|
-
maxLines: {
|
|
356
|
-
type: 'number',
|
|
357
|
-
description: 'Maximum lines per code example (truncates longer examples)',
|
|
358
|
-
},
|
|
359
|
-
},
|
|
360
|
-
required: ['component'],
|
|
361
|
-
},
|
|
362
|
-
},
|
|
363
|
-
{
|
|
364
|
-
name: TOOL_NAMES.blocks,
|
|
365
|
-
description: `Search and retrieve composition blocks — named patterns showing how design system components wire together for common use cases (e.g., "Login Form", "Settings Page"). Returns the block with its code pattern.`,
|
|
366
|
-
inputSchema: {
|
|
367
|
-
type: 'object' as const,
|
|
368
|
-
properties: {
|
|
369
|
-
name: {
|
|
370
|
-
type: 'string',
|
|
371
|
-
description: 'Exact block name to retrieve (e.g., "Login Form")',
|
|
372
|
-
},
|
|
373
|
-
search: {
|
|
374
|
-
type: 'string',
|
|
375
|
-
description: 'Free-text search across block names, descriptions, tags, and components',
|
|
376
|
-
},
|
|
377
|
-
component: {
|
|
378
|
-
type: 'string',
|
|
379
|
-
description: 'Filter blocks that use a specific component (e.g., "Button")',
|
|
380
|
-
},
|
|
381
|
-
category: {
|
|
382
|
-
type: 'string',
|
|
383
|
-
description: 'Filter by category (e.g., "authentication", "marketing", "dashboard", "settings", "ecommerce", "ai")',
|
|
384
|
-
},
|
|
385
|
-
},
|
|
386
|
-
},
|
|
387
|
-
},
|
|
388
|
-
{
|
|
389
|
-
name: TOOL_NAMES.tokens,
|
|
390
|
-
description: `List available CSS design tokens (custom properties) by category. Use this when you need to style custom elements or override defaults — no more guessing variable names. Filter by category or search by keyword.`,
|
|
391
|
-
inputSchema: {
|
|
392
|
-
type: 'object' as const,
|
|
393
|
-
properties: {
|
|
394
|
-
category: {
|
|
395
|
-
type: 'string',
|
|
396
|
-
description: 'Filter by category (e.g., "colors", "spacing", "typography", "surfaces", "shadows", "radius", "borders", "text", "focus", "layout", "code", "component-sizing")',
|
|
397
|
-
},
|
|
398
|
-
search: {
|
|
399
|
-
type: 'string',
|
|
400
|
-
description: 'Search token names (e.g., "accent", "hover", "padding")',
|
|
401
|
-
},
|
|
402
|
-
},
|
|
403
|
-
},
|
|
404
|
-
},
|
|
405
|
-
{
|
|
406
|
-
name: TOOL_NAMES.implement,
|
|
407
|
-
description: `One-shot implementation helper. Describe what you want to build and get everything needed in a single call: best-matching component(s) with full props and code examples, relevant composition blocks, and applicable CSS tokens. Saves multiple round-trips.`,
|
|
408
|
-
inputSchema: {
|
|
409
|
-
type: 'object' as const,
|
|
410
|
-
properties: {
|
|
411
|
-
useCase: {
|
|
412
|
-
type: 'string',
|
|
413
|
-
description: 'What you want to implement (e.g., "login form", "data table with sorting", "streaming chat messages")',
|
|
414
|
-
},
|
|
415
|
-
},
|
|
416
|
-
required: ['useCase'],
|
|
417
|
-
},
|
|
418
|
-
},
|
|
419
|
-
{
|
|
420
|
-
name: TOOL_NAMES.render,
|
|
421
|
-
description: `Render a component and return a screenshot. Optionally compare against a stored baseline ('baseline: true') or against a Figma design ('figmaUrl'). Use this to verify your implementation looks correct.`,
|
|
422
|
-
inputSchema: {
|
|
423
|
-
type: 'object' as const,
|
|
424
|
-
properties: {
|
|
425
|
-
component: {
|
|
426
|
-
type: 'string',
|
|
427
|
-
description: 'Component name (e.g., "Button", "Card", "Input")',
|
|
428
|
-
},
|
|
429
|
-
variant: {
|
|
430
|
-
type: 'string',
|
|
431
|
-
description: 'Variant name for baseline/compare modes',
|
|
432
|
-
},
|
|
433
|
-
props: {
|
|
434
|
-
type: 'object',
|
|
435
|
-
description: 'Props to pass to the component (e.g., { "variant": "primary", "children": "Click me" })',
|
|
436
|
-
},
|
|
437
|
-
viewport: {
|
|
438
|
-
type: 'object',
|
|
439
|
-
properties: {
|
|
440
|
-
width: { type: 'number', description: 'Viewport width (default: 800)' },
|
|
441
|
-
height: { type: 'number', description: 'Viewport height (default: 600)' },
|
|
442
|
-
},
|
|
443
|
-
description: 'Optional viewport size for the render',
|
|
444
|
-
},
|
|
445
|
-
baseline: {
|
|
446
|
-
type: 'boolean',
|
|
447
|
-
description: 'If true, compares the render against the stored baseline screenshot (requires variant)',
|
|
448
|
-
},
|
|
449
|
-
figmaUrl: {
|
|
450
|
-
type: 'string',
|
|
451
|
-
description: 'Figma frame URL — if provided, compares the render against the Figma design',
|
|
452
|
-
},
|
|
453
|
-
theme: {
|
|
454
|
-
type: 'string',
|
|
455
|
-
enum: ['light', 'dark'],
|
|
456
|
-
description: 'Theme for baseline verification (default: light)',
|
|
457
|
-
},
|
|
458
|
-
threshold: {
|
|
459
|
-
type: 'number',
|
|
460
|
-
description: 'Diff threshold percentage (default: 5 for baseline, 1 for Figma)',
|
|
461
|
-
},
|
|
462
|
-
},
|
|
463
|
-
required: ['component'],
|
|
464
|
-
},
|
|
465
|
-
},
|
|
466
|
-
{
|
|
467
|
-
name: TOOL_NAMES.fix,
|
|
468
|
-
description: `Generate patches to fix token compliance issues in a component. Returns unified diff patches that replace hardcoded CSS values with design token references. Use this after fragments_render identifies issues to automatically fix them.`,
|
|
469
|
-
inputSchema: {
|
|
470
|
-
type: 'object' as const,
|
|
471
|
-
properties: {
|
|
472
|
-
component: {
|
|
473
|
-
type: 'string',
|
|
474
|
-
description: 'Component name to generate fixes for (e.g., "Button", "Card")',
|
|
475
|
-
},
|
|
476
|
-
variant: {
|
|
477
|
-
type: 'string',
|
|
478
|
-
description: 'Specific variant to fix (optional, fixes all variants if omitted)',
|
|
479
|
-
},
|
|
480
|
-
fixType: {
|
|
481
|
-
type: 'string',
|
|
482
|
-
enum: ['token', 'all'],
|
|
483
|
-
description: 'Type of fixes to generate: "token" for hardcoded→token replacements, "all" for all available fixes (default: "all")',
|
|
484
|
-
},
|
|
485
|
-
},
|
|
486
|
-
required: ['component'],
|
|
487
|
-
},
|
|
488
|
-
},
|
|
489
|
-
];
|
|
282
|
+
const TOOLS = buildMcpTools(BRAND.nameLower, CLI_TOOL_EXTENSIONS) as Tool[];
|
|
490
283
|
|
|
491
284
|
/**
|
|
492
285
|
* Create and configure the MCP server
|
|
@@ -1231,7 +1024,7 @@ export function createMcpServer(config: McpServerConfig): Server {
|
|
|
1231
1024
|
}
|
|
1232
1025
|
|
|
1233
1026
|
// Filter by category and/or search
|
|
1234
|
-
|
|
1027
|
+
const filteredCategories: Record<string, Array<{ name: string; description?: string }>> = {};
|
|
1235
1028
|
let filteredTotal = 0;
|
|
1236
1029
|
|
|
1237
1030
|
for (const [cat, tokens] of Object.entries(tokenData.categories)) {
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Color Contrast Tests — Seed-Based Theme System
|
|
3
|
+
*
|
|
4
|
+
* Validates WCAG 2.1 contrast ratios between CSS custom property pairings
|
|
5
|
+
* (--fui-text-*, --fui-bg-*, --fui-color-*) across all 5 neutral palettes
|
|
6
|
+
* (Wind, Ice, Earth, Sand, Fire) in both light and dark modes.
|
|
7
|
+
*
|
|
8
|
+
* Token values are derived from the seed-derivation system — the same
|
|
9
|
+
* derivation functions that produce the runtime CSS custom properties.
|
|
10
|
+
* No hardcoded hex values: everything flows from PALETTES + DEFAULT_SEEDS.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect } from 'vitest';
|
|
14
|
+
import {
|
|
15
|
+
PALETTES,
|
|
16
|
+
PALETTE_SEMANTIC_COLORS,
|
|
17
|
+
DEFAULT_SEEDS,
|
|
18
|
+
deriveText,
|
|
19
|
+
deriveSurfaces,
|
|
20
|
+
deriveSemanticBg,
|
|
21
|
+
deriveSemanticText,
|
|
22
|
+
deriveDarkAccent,
|
|
23
|
+
hexToRgb,
|
|
24
|
+
} from '@fragments/seed-derivation';
|
|
25
|
+
import type { NeutralPalette } from '@fragments/seed-derivation';
|
|
26
|
+
import {
|
|
27
|
+
parseColor,
|
|
28
|
+
contrastRatio,
|
|
29
|
+
meetsAA,
|
|
30
|
+
meetsAAA,
|
|
31
|
+
} from '../contrast.js';
|
|
32
|
+
import type { RGB } from '../contrast.js';
|
|
33
|
+
|
|
34
|
+
// ── Fixed tokens (from libs/ui/src/tokens/_variables.scss, not seed-derived) ─
|
|
35
|
+
|
|
36
|
+
const FIXED_TOKENS = {
|
|
37
|
+
'--fui-tooltip-text': '$fui-tooltip-text',
|
|
38
|
+
'--fui-tooltip-bg': '$fui-tooltip-bg',
|
|
39
|
+
'--fui-code-text': '$fui-code-text',
|
|
40
|
+
'--fui-code-text-muted': '$fui-code-text-muted',
|
|
41
|
+
'--fui-code-bg': '$fui-code-bg',
|
|
42
|
+
} as const;
|
|
43
|
+
|
|
44
|
+
// These SCSS defaults are not seed-derived — they're the same across all palettes.
|
|
45
|
+
// Values sourced from libs/ui/src/tokens/_variables.scss lines 337-356.
|
|
46
|
+
const SCSS_DEFAULTS: Record<(typeof FIXED_TOKENS)[keyof typeof FIXED_TOKENS], string> = {
|
|
47
|
+
'$fui-tooltip-text': '#f8fafc',
|
|
48
|
+
'$fui-tooltip-bg': '#1e293b',
|
|
49
|
+
'$fui-code-text': '#d4d4d4',
|
|
50
|
+
'$fui-code-text-muted': '#6b7280',
|
|
51
|
+
'$fui-code-bg': '#1e1e1e',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function fixedToken(varName: keyof typeof FIXED_TOKENS): string {
|
|
55
|
+
return SCSS_DEFAULTS[FIXED_TOKENS[varName]];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Token derivation (mirrors CSS custom property output) ────────────────────
|
|
59
|
+
|
|
60
|
+
interface TokenMap {
|
|
61
|
+
'--fui-text-primary': string;
|
|
62
|
+
'--fui-text-secondary': string;
|
|
63
|
+
'--fui-text-tertiary': string;
|
|
64
|
+
'--fui-text-inverse': string;
|
|
65
|
+
'--fui-bg-primary': string;
|
|
66
|
+
'--fui-bg-secondary': string;
|
|
67
|
+
'--fui-bg-tertiary': string;
|
|
68
|
+
'--fui-bg-elevated': string;
|
|
69
|
+
'--fui-color-accent': string;
|
|
70
|
+
'--fui-color-danger': string;
|
|
71
|
+
'--fui-color-success': string;
|
|
72
|
+
'--fui-color-warning': string;
|
|
73
|
+
'--fui-color-info': string;
|
|
74
|
+
'--fui-color-danger-bg': string;
|
|
75
|
+
'--fui-color-success-bg': string;
|
|
76
|
+
'--fui-color-warning-bg': string;
|
|
77
|
+
'--fui-color-info-bg': string;
|
|
78
|
+
'--fui-color-danger-text': string;
|
|
79
|
+
'--fui-color-success-text': string;
|
|
80
|
+
'--fui-color-warning-text': string;
|
|
81
|
+
'--fui-color-info-text': string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function deriveTokenMap(paletteName: NeutralPalette, brand: string, isDark: boolean): TokenMap {
|
|
85
|
+
const palette = PALETTES[paletteName];
|
|
86
|
+
const semantics = PALETTE_SEMANTIC_COLORS[paletteName];
|
|
87
|
+
const text = deriveText(palette, isDark);
|
|
88
|
+
const surfaces = deriveSurfaces(palette, isDark, paletteName);
|
|
89
|
+
const accent = isDark ? deriveDarkAccent(brand) : brand;
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
'--fui-text-primary': text.primary,
|
|
93
|
+
'--fui-text-secondary': text.secondary,
|
|
94
|
+
'--fui-text-tertiary': text.tertiary,
|
|
95
|
+
'--fui-text-inverse': text.inverse,
|
|
96
|
+
'--fui-bg-primary': surfaces.primary,
|
|
97
|
+
'--fui-bg-secondary': surfaces.secondary,
|
|
98
|
+
'--fui-bg-tertiary': surfaces.tertiary,
|
|
99
|
+
'--fui-bg-elevated': surfaces.elevated,
|
|
100
|
+
'--fui-color-accent': accent,
|
|
101
|
+
'--fui-color-danger': semantics.danger,
|
|
102
|
+
'--fui-color-success': semantics.success,
|
|
103
|
+
'--fui-color-warning': semantics.warning,
|
|
104
|
+
'--fui-color-info': semantics.info,
|
|
105
|
+
'--fui-color-danger-bg': deriveSemanticBg(semantics.danger, isDark),
|
|
106
|
+
'--fui-color-success-bg': deriveSemanticBg(semantics.success, isDark),
|
|
107
|
+
'--fui-color-warning-bg': deriveSemanticBg(semantics.warning, isDark),
|
|
108
|
+
'--fui-color-info-bg': deriveSemanticBg(semantics.info, isDark),
|
|
109
|
+
'--fui-color-danger-text': deriveSemanticText(semantics.danger, isDark),
|
|
110
|
+
'--fui-color-success-text': deriveSemanticText(semantics.success, isDark),
|
|
111
|
+
'--fui-color-warning-text': deriveSemanticText(semantics.warning, isDark),
|
|
112
|
+
'--fui-color-info-text': deriveSemanticText(semantics.info, isDark),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Contrast helpers ─────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
function compositeRgba(rgba: string, bgHex: string): RGB {
|
|
119
|
+
const match = rgba.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/);
|
|
120
|
+
if (!match) throw new Error(`Cannot parse rgba: ${rgba}`);
|
|
121
|
+
const fg = { r: +match[1], g: +match[2], b: +match[3] };
|
|
122
|
+
const alpha = match[4] !== undefined ? parseFloat(match[4]) : 1;
|
|
123
|
+
const bg = parseColor(bgHex);
|
|
124
|
+
return {
|
|
125
|
+
r: Math.round(fg.r * alpha + bg.r * (1 - alpha)),
|
|
126
|
+
g: Math.round(fg.g * alpha + bg.g * (1 - alpha)),
|
|
127
|
+
b: Math.round(fg.b * alpha + bg.b * (1 - alpha)),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function ratio(fg: string, bg: string): number {
|
|
132
|
+
return contrastRatio(parseColor(fg), parseColor(bg));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function ratioOnComposite(fgHex: string, rgbaBg: string, baseHex: string): number {
|
|
136
|
+
return contrastRatio(parseColor(fgHex), compositeRgba(rgbaBg, baseHex));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
const PALETTE_NAMES: NeutralPalette[] = ['wind', 'ice', 'earth', 'sand', 'fire'];
|
|
142
|
+
const brand = DEFAULT_SEEDS.brand;
|
|
143
|
+
|
|
144
|
+
for (const paletteName of PALETTE_NAMES) {
|
|
145
|
+
const light = deriveTokenMap(paletteName, brand, false);
|
|
146
|
+
const dark = deriveTokenMap(paletteName, brand, true);
|
|
147
|
+
|
|
148
|
+
describe(`${paletteName} palette — Light Mode`, () => {
|
|
149
|
+
describe('text hierarchy on --fui-bg-primary', () => {
|
|
150
|
+
it('--fui-text-primary on --fui-bg-primary meets AA', () => {
|
|
151
|
+
expect(meetsAA(ratio(light['--fui-text-primary'], light['--fui-bg-primary']))).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('--fui-text-secondary on --fui-bg-primary meets 3:1 (UI text minimum)', () => {
|
|
155
|
+
expect(ratio(light['--fui-text-secondary'], light['--fui-bg-primary'])).toBeGreaterThanOrEqual(3.0);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('--fui-text-tertiary on --fui-bg-primary has visible contrast', () => {
|
|
159
|
+
expect(ratio(light['--fui-text-tertiary'], light['--fui-bg-primary'])).toBeGreaterThanOrEqual(1.0);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('text on alternate surfaces', () => {
|
|
164
|
+
it('--fui-text-primary on --fui-bg-secondary meets AA', () => {
|
|
165
|
+
expect(meetsAA(ratio(light['--fui-text-primary'], light['--fui-bg-secondary']))).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('--fui-text-primary on --fui-bg-tertiary meets AA', () => {
|
|
169
|
+
expect(meetsAA(ratio(light['--fui-text-primary'], light['--fui-bg-tertiary']))).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('--fui-text-primary on --fui-bg-elevated meets AA', () => {
|
|
173
|
+
expect(meetsAA(ratio(light['--fui-text-primary'], light['--fui-bg-elevated']))).toBe(true);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('--fui-text-secondary on --fui-bg-elevated meets 3:1', () => {
|
|
177
|
+
expect(ratio(light['--fui-text-secondary'], light['--fui-bg-elevated'])).toBeGreaterThanOrEqual(3.0);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('Button', () => {
|
|
182
|
+
it('--fui-text-inverse on --fui-color-accent meets 3:1 (large text / bold buttons)', () => {
|
|
183
|
+
expect(ratio(light['--fui-text-inverse'], light['--fui-color-accent'])).toBeGreaterThanOrEqual(3.0);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('--fui-text-primary on --fui-bg-secondary (secondary button) meets AA', () => {
|
|
187
|
+
expect(meetsAA(ratio(light['--fui-text-primary'], light['--fui-bg-secondary']))).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('Card', () => {
|
|
192
|
+
it('--fui-text-primary on --fui-bg-elevated (title/body) meets AA', () => {
|
|
193
|
+
expect(meetsAA(ratio(light['--fui-text-primary'], light['--fui-bg-elevated']))).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('--fui-text-secondary on --fui-bg-elevated (description) meets 3:1', () => {
|
|
197
|
+
expect(ratio(light['--fui-text-secondary'], light['--fui-bg-elevated'])).toBeGreaterThanOrEqual(3.0);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('semantic text on semantic bg (Badge, Alert)', () => {
|
|
202
|
+
it('--fui-color-danger-text on composited --fui-color-danger-bg meets AA', () => {
|
|
203
|
+
expect(ratioOnComposite(
|
|
204
|
+
light['--fui-color-danger-text'], light['--fui-color-danger-bg'], light['--fui-bg-primary']
|
|
205
|
+
)).toBeGreaterThanOrEqual(4.5);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('--fui-color-success-text on composited --fui-color-success-bg meets AA', () => {
|
|
209
|
+
expect(ratioOnComposite(
|
|
210
|
+
light['--fui-color-success-text'], light['--fui-color-success-bg'], light['--fui-bg-primary']
|
|
211
|
+
)).toBeGreaterThanOrEqual(4.5);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('--fui-color-warning-text on composited --fui-color-warning-bg meets AA', () => {
|
|
215
|
+
expect(ratioOnComposite(
|
|
216
|
+
light['--fui-color-warning-text'], light['--fui-color-warning-bg'], light['--fui-bg-primary']
|
|
217
|
+
)).toBeGreaterThanOrEqual(4.5);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('--fui-color-info-text on composited --fui-color-info-bg meets AA', () => {
|
|
221
|
+
expect(ratioOnComposite(
|
|
222
|
+
light['--fui-color-info-text'], light['--fui-color-info-bg'], light['--fui-bg-primary']
|
|
223
|
+
)).toBeGreaterThanOrEqual(4.5);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe(`${paletteName} palette — Dark Mode`, () => {
|
|
229
|
+
describe('text hierarchy on --fui-bg-primary', () => {
|
|
230
|
+
it('--fui-text-primary on --fui-bg-primary meets AA', () => {
|
|
231
|
+
expect(meetsAA(ratio(dark['--fui-text-primary'], dark['--fui-bg-primary']))).toBe(true);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('--fui-text-secondary on --fui-bg-primary meets 3:1', () => {
|
|
235
|
+
expect(ratio(dark['--fui-text-secondary'], dark['--fui-bg-primary'])).toBeGreaterThanOrEqual(3.0);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('--fui-text-tertiary on --fui-bg-primary has visible contrast', () => {
|
|
239
|
+
expect(ratio(dark['--fui-text-tertiary'], dark['--fui-bg-primary'])).toBeGreaterThanOrEqual(1.0);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe('text on alternate surfaces', () => {
|
|
244
|
+
it('--fui-text-primary on --fui-bg-secondary meets AA', () => {
|
|
245
|
+
expect(meetsAA(ratio(dark['--fui-text-primary'], dark['--fui-bg-secondary']))).toBe(true);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('--fui-text-primary on --fui-bg-elevated meets AA', () => {
|
|
249
|
+
expect(meetsAA(ratio(dark['--fui-text-primary'], dark['--fui-bg-elevated']))).toBe(true);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('Button (dark)', () => {
|
|
254
|
+
it('--fui-text-inverse on --fui-color-accent meets 3:1', () => {
|
|
255
|
+
expect(ratio(dark['--fui-text-inverse'], dark['--fui-color-accent'])).toBeGreaterThanOrEqual(3.0);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe('Card (dark)', () => {
|
|
260
|
+
it('--fui-text-primary on --fui-bg-elevated meets AA', () => {
|
|
261
|
+
expect(meetsAA(ratio(dark['--fui-text-primary'], dark['--fui-bg-elevated']))).toBe(true);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('--fui-text-secondary on --fui-bg-elevated meets 3:1', () => {
|
|
265
|
+
expect(ratio(dark['--fui-text-secondary'], dark['--fui-bg-elevated'])).toBeGreaterThanOrEqual(3.0);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe('semantic text on semantic bg (dark)', () => {
|
|
270
|
+
it('--fui-color-danger-text on composited --fui-color-danger-bg meets AA', () => {
|
|
271
|
+
expect(ratioOnComposite(
|
|
272
|
+
dark['--fui-color-danger-text'], dark['--fui-color-danger-bg'], dark['--fui-bg-primary']
|
|
273
|
+
)).toBeGreaterThanOrEqual(4.5);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('--fui-color-success-text on composited --fui-color-success-bg meets AA', () => {
|
|
277
|
+
expect(ratioOnComposite(
|
|
278
|
+
dark['--fui-color-success-text'], dark['--fui-color-success-bg'], dark['--fui-bg-primary']
|
|
279
|
+
)).toBeGreaterThanOrEqual(4.5);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('--fui-color-warning-text on composited --fui-color-warning-bg meets AA', () => {
|
|
283
|
+
expect(ratioOnComposite(
|
|
284
|
+
dark['--fui-color-warning-text'], dark['--fui-color-warning-bg'], dark['--fui-bg-primary']
|
|
285
|
+
)).toBeGreaterThanOrEqual(4.5);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('--fui-color-info-text on composited --fui-color-info-bg meets AA', () => {
|
|
289
|
+
expect(ratioOnComposite(
|
|
290
|
+
dark['--fui-color-info-text'], dark['--fui-color-info-bg'], dark['--fui-bg-primary']
|
|
291
|
+
)).toBeGreaterThanOrEqual(4.5);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
describe('Cross-palette contrast report', () => {
|
|
298
|
+
for (const paletteName of PALETTE_NAMES) {
|
|
299
|
+
const light = deriveTokenMap(paletteName, brand, false);
|
|
300
|
+
const dark = deriveTokenMap(paletteName, brand, true);
|
|
301
|
+
|
|
302
|
+
it(`${paletteName} light — --fui-text-primary on --fui-bg-primary`, () => {
|
|
303
|
+
const r = ratio(light['--fui-text-primary'], light['--fui-bg-primary']);
|
|
304
|
+
console.log(` ${paletteName} light: ${light['--fui-text-primary']} on ${light['--fui-bg-primary']} = ${r.toFixed(2)}:1`);
|
|
305
|
+
expect(meetsAA(r)).toBe(true);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it(`${paletteName} dark — --fui-text-primary on --fui-bg-primary`, () => {
|
|
309
|
+
const r = ratio(dark['--fui-text-primary'], dark['--fui-bg-primary']);
|
|
310
|
+
console.log(` ${paletteName} dark: ${dark['--fui-text-primary']} on ${dark['--fui-bg-primary']} = ${r.toFixed(2)}:1`);
|
|
311
|
+
expect(meetsAA(r)).toBe(true);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it(`${paletteName} light — --fui-text-inverse on --fui-color-accent`, () => {
|
|
315
|
+
const r = ratio(light['--fui-text-inverse'], light['--fui-color-accent']);
|
|
316
|
+
const aa = meetsAA(r);
|
|
317
|
+
const aaLarge = meetsAA(r, true);
|
|
318
|
+
console.log(
|
|
319
|
+
` ${paletteName}: ${light['--fui-text-inverse']} on ${light['--fui-color-accent']} = ${r.toFixed(2)}:1 — ${aa ? 'PASS AA' : aaLarge ? 'PASS large' : 'FAIL'}`
|
|
320
|
+
);
|
|
321
|
+
expect(r).toBeGreaterThanOrEqual(1.0);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe('Fixed-token components', () => {
|
|
327
|
+
it('--fui-tooltip-text on --fui-tooltip-bg meets AAA', () => {
|
|
328
|
+
expect(meetsAAA(ratio(fixedToken('--fui-tooltip-text'), fixedToken('--fui-tooltip-bg')))).toBe(true);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('--fui-code-text on --fui-code-bg meets AA', () => {
|
|
332
|
+
expect(meetsAA(ratio(fixedToken('--fui-code-text'), fixedToken('--fui-code-bg')))).toBe(true);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('--fui-code-text-muted on --fui-code-bg meets 3:1', () => {
|
|
336
|
+
expect(ratio(fixedToken('--fui-code-text-muted'), fixedToken('--fui-code-bg'))).toBeGreaterThanOrEqual(3.0);
|
|
337
|
+
});
|
|
338
|
+
});
|