Skip to content

Commit 0a30bac

Browse files
authored
Merge pull request #159 from bookedsolidtech/feature/library-agnostic-suggest-fix
feat: make suggest_fix token suggestions library-agnostic
2 parents 20b3001 + 99c65ca commit 0a30bac

File tree

4 files changed

+112
-19
lines changed

4 files changed

+112
-19
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'helixir': minor
3+
'@helixir/core': minor
4+
---
5+
6+
feat: make suggest_fix token suggestions library-agnostic with optional tokenPrefix parameter

packages/core/src/handlers/suggest-fix.ts

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -36,26 +36,41 @@ export interface SuggestFixInput {
3636
memberName?: string;
3737
suggestedName?: string;
3838
eventName?: string;
39+
/** Optional token prefix from the component's library (e.g. '--hx-', '--fast-'). When provided, suggested tokens use this prefix instead of generic placeholders. */
40+
tokenPrefix?: string;
3941
}
4042

4143
// ─── Token heuristics ────────────────────────────────────────────────────────
4244

43-
const TOKEN_SUGGESTIONS: Record<string, string> = {
44-
'background-color': '--sl-color-neutral-0',
45-
background: '--sl-color-neutral-0',
46-
color: '--sl-color-neutral-900',
47-
'border-color': '--sl-color-neutral-300',
48-
'box-shadow': '--sl-shadow-medium',
49-
'font-family': '--sl-font-sans',
50-
'font-size': '--sl-font-size-medium',
51-
'border-radius': '--sl-border-radius-medium',
52-
padding: '--sl-spacing-medium',
53-
margin: '--sl-spacing-medium',
54-
gap: '--sl-spacing-small',
45+
/** Maps CSS properties to semantic token suffixes (library-agnostic). */
46+
const TOKEN_SUFFIXES: Record<string, string> = {
47+
'background-color': 'color-neutral-0',
48+
background: 'color-neutral-0',
49+
color: 'color-neutral-900',
50+
'border-color': 'color-neutral-300',
51+
'box-shadow': 'shadow-medium',
52+
'font-family': 'font-sans',
53+
'font-size': 'font-size-medium',
54+
'border-radius': 'border-radius-medium',
55+
padding: 'spacing-medium',
56+
margin: 'spacing-medium',
57+
gap: 'spacing-small',
5558
};
5659

57-
function suggestTokenForProperty(property: string): string {
58-
return TOKEN_SUGGESTIONS[property] ?? '--your-design-token';
60+
/**
61+
* Suggests a token name for a CSS property. When a tokenPrefix is provided,
62+
* generates a library-specific token (e.g. `--hx-color-neutral-0`).
63+
* Without a prefix, returns a generic placeholder.
64+
*/
65+
function suggestTokenForProperty(property: string, tokenPrefix?: string): string {
66+
const suffix = TOKEN_SUFFIXES[property];
67+
if (!suffix) {
68+
return tokenPrefix ? `${tokenPrefix}design-token` : '--your-design-token';
69+
}
70+
if (tokenPrefix) {
71+
return `${tokenPrefix}${suffix}`;
72+
}
73+
return `--your-${suffix}`;
5974
}
6075

6176
// ─── Shadow DOM fixes ────────────────────────────────────────────────────────
@@ -193,7 +208,7 @@ function fixShadowDom(input: SuggestFixInput): FixSuggestion {
193208
// ─── Token fallback fixes ────────────────────────────────────────────────────
194209

195210
function fixTokenFallback(input: SuggestFixInput): FixSuggestion {
196-
const { original, property } = input;
211+
const { original, property, tokenPrefix } = input;
197212

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

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

252267
function fixThemeCompat(input: SuggestFixInput): FixSuggestion {
253-
const { original, property } = input;
268+
const { original, property, tokenPrefix } = input;
254269

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

271286
if (input.issue === 'contrast-pair') {
287+
const bgToken = suggestTokenForProperty('background-color', tokenPrefix);
288+
const fgToken = suggestTokenForProperty('color', tokenPrefix);
272289
return {
273290
original,
274-
suggestion: `background: var(--sl-color-neutral-0); color: var(--sl-color-neutral-900);`,
291+
suggestion: `background: var(${bgToken}); color: var(${fgToken});`,
275292
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.`,
276293
severity: 'warning',
277294
};

packages/core/src/tools/styling.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ const SuggestFixArgsSchema = z.object({
148148
memberName: z.string().optional(),
149149
suggestedName: z.string().optional(),
150150
eventName: z.string().optional(),
151+
tokenPrefix: z.string().optional(),
151152
});
152153

153154
const ValidateComponentCodeArgsSchema = z.object({
@@ -622,6 +623,11 @@ export const STYLING_TOOL_DEFINITIONS = [
622623
type: 'string',
623624
description: 'Optional event name for event usage fixes.',
624625
},
626+
tokenPrefix: {
627+
type: 'string',
628+
description:
629+
'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.',
630+
},
625631
},
626632
required: ['type', 'issue', 'original'],
627633
additionalProperties: false,

tests/handlers/suggest-fix.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,70 @@ describe('suggestFix — layout', () => {
308308
});
309309
});
310310

311+
// ─── Library-agnostic token suggestions ─────────────────────────────────────
312+
313+
describe('suggestFix — library-agnostic tokens', () => {
314+
it('uses provided tokenPrefix instead of hardcoded --sl- tokens', () => {
315+
const result = suggestFix({
316+
type: 'token-fallback',
317+
issue: 'hardcoded-color',
318+
original: 'background-color: #3b82f6;',
319+
property: 'background-color',
320+
tokenPrefix: '--hx-',
321+
});
322+
expect(result.suggestion).toContain('--hx-');
323+
expect(result.suggestion).not.toContain('--sl-');
324+
});
325+
326+
it('uses generic placeholder when no tokenPrefix provided', () => {
327+
const result = suggestFix({
328+
type: 'token-fallback',
329+
issue: 'hardcoded-color',
330+
original: 'color: #333;',
331+
property: 'color',
332+
});
333+
// Should NOT contain library-specific prefix
334+
expect(result.suggestion).not.toContain('--sl-');
335+
expect(result.suggestion).toContain('var(');
336+
});
337+
338+
it('uses tokenPrefix in theme-compat hardcoded-color fix', () => {
339+
const result = suggestFix({
340+
type: 'theme-compat',
341+
issue: 'hardcoded-color',
342+
original: 'background: white;',
343+
property: 'background',
344+
tokenPrefix: '--fast-',
345+
});
346+
expect(result.suggestion).toContain('--fast-');
347+
expect(result.suggestion).not.toContain('--sl-');
348+
});
349+
350+
it('uses tokenPrefix in theme-compat contrast-pair fix', () => {
351+
const result = suggestFix({
352+
type: 'theme-compat',
353+
issue: 'contrast-pair',
354+
original: 'background: #f0f0f0; color: #e0e0e0;',
355+
tokenPrefix: '--md-',
356+
});
357+
expect(result.suggestion).toContain('--md-');
358+
expect(result.suggestion).not.toContain('--sl-');
359+
});
360+
361+
it('generates property-appropriate token names from prefix', () => {
362+
const result = suggestFix({
363+
type: 'token-fallback',
364+
issue: 'hardcoded-color',
365+
original: 'border-radius: 8px;',
366+
property: 'border-radius',
367+
tokenPrefix: '--hx-',
368+
});
369+
expect(result.suggestion).toContain('--hx-');
370+
// Should generate a radius-related token name
371+
expect(result.suggestion).toMatch(/--hx-.*radius/i);
372+
});
373+
});
374+
311375
// ─── Result structure ───────────────────────────────────────────────────────
312376

313377
describe('suggestFix — result structure', () => {

0 commit comments

Comments
 (0)