Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/library-agnostic-tokens.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'helixir': minor
'@helixir/core': minor
---

feat: make suggest_fix token suggestions library-agnostic with optional tokenPrefix parameter
55 changes: 36 additions & 19 deletions packages/core/src/handlers/suggest-fix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,26 +36,41 @@ export interface SuggestFixInput {
memberName?: string;
suggestedName?: string;
eventName?: string;
/** Optional token prefix from the component's library (e.g. '--hx-', '--fast-'). When provided, suggested tokens use this prefix instead of generic placeholders. */
tokenPrefix?: string;
}

// ─── Token heuristics ────────────────────────────────────────────────────────

const TOKEN_SUGGESTIONS: Record<string, string> = {
'background-color': '--sl-color-neutral-0',
background: '--sl-color-neutral-0',
color: '--sl-color-neutral-900',
'border-color': '--sl-color-neutral-300',
'box-shadow': '--sl-shadow-medium',
'font-family': '--sl-font-sans',
'font-size': '--sl-font-size-medium',
'border-radius': '--sl-border-radius-medium',
padding: '--sl-spacing-medium',
margin: '--sl-spacing-medium',
gap: '--sl-spacing-small',
/** Maps CSS properties to semantic token suffixes (library-agnostic). */
const TOKEN_SUFFIXES: Record<string, string> = {
'background-color': 'color-neutral-0',
background: 'color-neutral-0',
color: 'color-neutral-900',
'border-color': 'color-neutral-300',
'box-shadow': 'shadow-medium',
'font-family': 'font-sans',
'font-size': 'font-size-medium',
'border-radius': 'border-radius-medium',
padding: 'spacing-medium',
margin: 'spacing-medium',
gap: 'spacing-small',
};

function suggestTokenForProperty(property: string): string {
return TOKEN_SUGGESTIONS[property] ?? '--your-design-token';
/**
* Suggests a token name for a CSS property. When a tokenPrefix is provided,
* generates a library-specific token (e.g. `--hx-color-neutral-0`).
* Without a prefix, returns a generic placeholder.
*/
function suggestTokenForProperty(property: string, tokenPrefix?: string): string {
const suffix = TOKEN_SUFFIXES[property];
if (!suffix) {
return tokenPrefix ? `${tokenPrefix}design-token` : '--your-design-token';
}
if (tokenPrefix) {
return `${tokenPrefix}${suffix}`;
}
return `--your-${suffix}`;
}

// ─── Shadow DOM fixes ────────────────────────────────────────────────────────
Expand Down Expand Up @@ -193,7 +208,7 @@ function fixShadowDom(input: SuggestFixInput): FixSuggestion {
// ─── Token fallback fixes ────────────────────────────────────────────────────

function fixTokenFallback(input: SuggestFixInput): FixSuggestion {
const { original, property } = input;
const { original, property, tokenPrefix } = input;

if (input.issue === 'missing-fallback') {
// Extract the var() call and add a sensible fallback
Expand Down Expand Up @@ -223,7 +238,7 @@ function fixTokenFallback(input: SuggestFixInput): FixSuggestion {
}

if (input.issue === 'hardcoded-color') {
const token = suggestTokenForProperty(property ?? 'color');
const token = suggestTokenForProperty(property ?? 'color', tokenPrefix);
// Extract the property and value
const propMatch = original.match(/([a-z-]+)\s*:\s*([^;]+)/i);
if (propMatch) {
Expand All @@ -250,10 +265,10 @@ function fixTokenFallback(input: SuggestFixInput): FixSuggestion {
// ─── Theme compatibility fixes ───────────────────────────────────────────────

function fixThemeCompat(input: SuggestFixInput): FixSuggestion {
const { original, property } = input;
const { original, property, tokenPrefix } = input;

if (input.issue === 'hardcoded-color') {
const token = suggestTokenForProperty(property ?? 'background');
const token = suggestTokenForProperty(property ?? 'background', tokenPrefix);
const propMatch = original.match(/([a-z-]+)\s*:\s*([^;]+)/i);
if (propMatch) {
const [, prop, value] = propMatch;
Expand All @@ -269,9 +284,11 @@ function fixThemeCompat(input: SuggestFixInput): FixSuggestion {
}

if (input.issue === 'contrast-pair') {
const bgToken = suggestTokenForProperty('background-color', tokenPrefix);
const fgToken = suggestTokenForProperty('color', tokenPrefix);
return {
original,
suggestion: `background: var(--sl-color-neutral-0); color: var(--sl-color-neutral-900);`,
suggestion: `background: var(${bgToken}); color: var(${fgToken});`,
explanation: `Light-on-light or dark-on-dark color pairs create contrast issues. Use semantic token pairs (surface + on-surface) that maintain readable contrast across themes.`,
severity: 'warning',
};
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/tools/styling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ const SuggestFixArgsSchema = z.object({
memberName: z.string().optional(),
suggestedName: z.string().optional(),
eventName: z.string().optional(),
tokenPrefix: z.string().optional(),
});

const ValidateComponentCodeArgsSchema = z.object({
Expand Down Expand Up @@ -622,6 +623,11 @@ export const STYLING_TOOL_DEFINITIONS = [
type: 'string',
description: 'Optional event name for event usage fixes.',
},
tokenPrefix: {
type: 'string',
description:
'Optional token prefix from the component library (e.g. "--hx-", "--fast-", "--md-"). When provided, suggested replacement tokens use this prefix. Get this from diagnose_styling.',
},
},
required: ['type', 'issue', 'original'],
additionalProperties: false,
Expand Down
64 changes: 64 additions & 0 deletions tests/handlers/suggest-fix.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,70 @@ describe('suggestFix β€” layout', () => {
});
});

// ─── Library-agnostic token suggestions ─────────────────────────────────────

describe('suggestFix β€” library-agnostic tokens', () => {
it('uses provided tokenPrefix instead of hardcoded --sl- tokens', () => {
const result = suggestFix({
type: 'token-fallback',
issue: 'hardcoded-color',
original: 'background-color: #3b82f6;',
property: 'background-color',
tokenPrefix: '--hx-',
});
expect(result.suggestion).toContain('--hx-');
expect(result.suggestion).not.toContain('--sl-');
});

it('uses generic placeholder when no tokenPrefix provided', () => {
const result = suggestFix({
type: 'token-fallback',
issue: 'hardcoded-color',
original: 'color: #333;',
property: 'color',
});
// Should NOT contain library-specific prefix
expect(result.suggestion).not.toContain('--sl-');
expect(result.suggestion).toContain('var(');
});

it('uses tokenPrefix in theme-compat hardcoded-color fix', () => {
const result = suggestFix({
type: 'theme-compat',
issue: 'hardcoded-color',
original: 'background: white;',
property: 'background',
tokenPrefix: '--fast-',
});
expect(result.suggestion).toContain('--fast-');
expect(result.suggestion).not.toContain('--sl-');
});

it('uses tokenPrefix in theme-compat contrast-pair fix', () => {
const result = suggestFix({
type: 'theme-compat',
issue: 'contrast-pair',
original: 'background: #f0f0f0; color: #e0e0e0;',
tokenPrefix: '--md-',
});
expect(result.suggestion).toContain('--md-');
expect(result.suggestion).not.toContain('--sl-');
});

it('generates property-appropriate token names from prefix', () => {
const result = suggestFix({
type: 'token-fallback',
issue: 'hardcoded-color',
original: 'border-radius: 8px;',
property: 'border-radius',
tokenPrefix: '--hx-',
});
expect(result.suggestion).toContain('--hx-');
// Should generate a radius-related token name
expect(result.suggestion).toMatch(/--hx-.*radius/i);
});
});

// ─── Result structure ───────────────────────────────────────────────────────

describe('suggestFix β€” result structure', () => {
Expand Down
Loading