Skip to content

Commit 100d582

Browse files
authored
✨ Add theme.json Tailwind shade label configuration (#2)
* 💄 Improve default `theme.json` color labels * 🎨 Improve code styling
1 parent 89e479e commit 100d582

File tree

3 files changed

+176
-32
lines changed

3 files changed

+176
-32
lines changed

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ export default defineConfig({
6666

6767
// Name of the editor iframe element (default: 'editor-canvas')
6868
iframeName: 'editor-canvas',
69-
}
70-
})
69+
},
70+
}),
7171
],
7272
});
7373
```
@@ -86,6 +86,12 @@ import { wordpressThemeJson } from '@roots/vite-plugin';
8686
export default defineConfig({
8787
plugins: [
8888
wordpressThemeJson({
89+
// Optional: Configure shade labels
90+
shadeLabels: {
91+
100: 'Lightest',
92+
900: 'Darkest',
93+
},
94+
8995
// Optional: Disable specific transformations
9096
disableTailwindColors: false,
9197
disableTailwindFonts: false,

src/index.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ interface ThemeJsonPluginOptions {
1515
*/
1616
tailwindConfig?: Record<string, unknown>;
1717

18+
/**
19+
* Labels for color shades to use in the WordPress editor.
20+
* Keys should be shade numbers (e.g. 50, 100, 500) and values are the human-readable labels.
21+
* For example: { 50: 'Lightest', 100: 'Lighter', 500: 'Default' }
22+
* When provided, color names will be formatted as '{label} {color}' instead of '{color} ({shade})'.
23+
*/
24+
shadeLabels?: Record<string, string>;
25+
1826
/**
1927
* Whether to disable generating color palette entries in theme.json.
2028
* When true, no color variables will be processed from the @theme block.
@@ -252,7 +260,7 @@ export function wordpressPlugin(
252260
editorPattern: /editor/,
253261
cssPattern: 'editor.css',
254262
iframeName: 'editor-canvas',
255-
...config.hmr
263+
...config.hmr,
256264
};
257265

258266
// HMR code to inject
@@ -428,10 +436,10 @@ if (import.meta.hot) {
428436
if (
429437
hmrConfig.enabled &&
430438
!transformedCode.includes('vite:beforeUpdate') &&
431-
(
432-
(typeof hmrConfig.editorPattern === 'string' && id.includes(hmrConfig.editorPattern)) ||
433-
(hmrConfig.editorPattern instanceof RegExp && hmrConfig.editorPattern.test(id))
434-
)
439+
((typeof hmrConfig.editorPattern === 'string' &&
440+
id.includes(hmrConfig.editorPattern)) ||
441+
(hmrConfig.editorPattern instanceof RegExp &&
442+
hmrConfig.editorPattern.test(id)))
435443
) {
436444
transformedCode = `${transformedCode}\n${hmrCode}`;
437445
}
@@ -672,14 +680,20 @@ export function wordpressThemeJson(config: ThemeJsonConfig = {}): VitePlugin {
672680
.filter(([name]) => !name.endsWith('-*'))
673681
.map(([name, value]) => {
674682
const [colorName, shade] = name.split('-');
683+
const capitalizedColor =
684+
colorName.charAt(0).toUpperCase() +
685+
colorName.slice(1);
675686
const displayName =
676687
shade && !Number.isNaN(Number(shade))
677-
? `${colorName}-${shade}`
678-
: name;
688+
? config.shadeLabels &&
689+
shade in config.shadeLabels
690+
? `${config.shadeLabels[shade]} ${capitalizedColor}`
691+
: `${capitalizedColor} (${shade})`
692+
: capitalizedColor;
679693

680694
return {
681695
name: displayName,
682-
slug: displayName.toLowerCase(),
696+
slug: name.toLowerCase(),
683697
color: value,
684698
};
685699
})

tests/index.test.ts

Lines changed: 146 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ describe('wordpressPlugin', () => {
184184
const result = (plugin.options as any)({
185185
input: 'src/index.ts',
186186
}) as InputOptions;
187+
187188
const external = result.external as (id: string) => boolean;
188189

189190
expect(external('@wordpress/element')).toBe(true);
@@ -195,6 +196,7 @@ describe('wordpressPlugin', () => {
195196
const result = (plugin.options as any)({
196197
input: 'src/index.ts',
197198
}) as InputOptions;
199+
198200
const external = result.external as (id: string) => boolean;
199201

200202
expect(external('react')).toBe(false);
@@ -266,6 +268,7 @@ describe('wordpressThemeJson', () => {
266268
vi.mocked(fs.readFileSync).mockReturnValue(
267269
JSON.stringify(mockBaseThemeJson)
268270
);
271+
269272
vi.mocked(path.resolve).mockImplementation((...paths: string[]) =>
270273
paths.join('/')
271274
);
@@ -289,36 +292,16 @@ describe('wordpressThemeJson', () => {
289292
}
290293
`;
291294

292-
// Transform to capture CSS content
293295
(plugin.transform as any)(cssContent, 'app.css');
294-
295-
// Mock emitFile to capture theme.json
296296
const emitFile = vi.fn();
297297
(plugin.generateBundle as any).call({ emitFile });
298298

299299
expect(emitFile).toHaveBeenCalledWith(
300300
expect.objectContaining({
301301
fileName: 'assets/theme.json',
302-
source: expect.stringContaining('"name": "primary"'),
302+
source: expect.stringContaining('"name": "Primary"'),
303303
})
304304
);
305-
306-
const themeJson = JSON.parse(emitFile.mock.calls[0][0].source);
307-
expect(themeJson.settings.color.palette).toContainEqual({
308-
name: 'primary',
309-
slug: 'primary',
310-
color: '#000000',
311-
});
312-
expect(themeJson.settings.typography.fontFamilies).toContainEqual({
313-
name: 'inter',
314-
slug: 'inter',
315-
fontFamily: 'Inter',
316-
});
317-
expect(themeJson.settings.typography.fontSizes).toContainEqual({
318-
name: 'lg',
319-
slug: 'lg',
320-
size: '1.125rem',
321-
});
322305
});
323306

324307
it('should handle numeric color shades', () => {
@@ -329,6 +312,10 @@ describe('wordpressThemeJson', () => {
329312
const cssContent = `
330313
@theme {
331314
--color-red-500: #ef4444;
315+
--color-blue-100: #e0f2fe;
316+
--color-primary: #000000;
317+
--color-white: #ffffff;
318+
--color-black: #000000;
332319
}
333320
`;
334321

@@ -337,11 +324,36 @@ describe('wordpressThemeJson', () => {
337324
(plugin.generateBundle as any).call({ emitFile });
338325

339326
const themeJson = JSON.parse(emitFile.mock.calls[0][0].source);
327+
340328
expect(themeJson.settings.color.palette).toContainEqual({
341-
name: 'red-500',
329+
name: 'Red (500)',
342330
slug: 'red-500',
343331
color: '#ef4444',
344332
});
333+
334+
expect(themeJson.settings.color.palette).toContainEqual({
335+
name: 'Blue (100)',
336+
slug: 'blue-100',
337+
color: '#e0f2fe',
338+
});
339+
340+
expect(themeJson.settings.color.palette).toContainEqual({
341+
name: 'Primary',
342+
slug: 'primary',
343+
color: '#000000',
344+
});
345+
346+
expect(themeJson.settings.color.palette).toContainEqual({
347+
name: 'White',
348+
slug: 'white',
349+
color: '#ffffff',
350+
});
351+
352+
expect(themeJson.settings.color.palette).toContainEqual({
353+
name: 'Black',
354+
slug: 'black',
355+
color: '#000000',
356+
});
345357
});
346358

347359
it('should respect disable flags', () => {
@@ -365,6 +377,7 @@ describe('wordpressThemeJson', () => {
365377
(plugin.generateBundle as any).call({ emitFile });
366378

367379
const themeJson = JSON.parse(emitFile.mock.calls[0][0].source);
380+
368381
expect(themeJson.settings.color?.palette).toBeUndefined();
369382
expect(themeJson.settings.typography.fontFamilies).toBeUndefined();
370383
expect(themeJson.settings.typography.fontSizes).toBeUndefined();
@@ -435,6 +448,117 @@ describe('wordpressThemeJson', () => {
435448
).rejects.toThrow('Unclosed @theme { block - missing closing brace');
436449
});
437450

451+
it('should handle shade labels', () => {
452+
const plugin = wordpressThemeJson({
453+
tailwindConfig: mockTailwindConfig,
454+
shadeLabels: {
455+
'50': 'Lightest',
456+
'100': 'Lighter',
457+
'500': 'Default',
458+
'900': 'Darkest',
459+
},
460+
});
461+
462+
const cssContent = `
463+
@theme {
464+
--color-blue-50: #f0f9ff;
465+
--color-blue-100: #e0f2fe;
466+
--color-blue-500: #3b82f6;
467+
--color-blue-900: #1e3a8a;
468+
--color-primary: #000000;
469+
}
470+
`;
471+
472+
(plugin.transform as any)(cssContent, 'app.css');
473+
const emitFile = vi.fn();
474+
(plugin.generateBundle as any).call({ emitFile });
475+
476+
const themeJson = JSON.parse(emitFile.mock.calls[0][0].source);
477+
478+
expect(themeJson.settings.color.palette).toContainEqual({
479+
name: 'Lightest Blue',
480+
slug: 'blue-50',
481+
color: '#f0f9ff',
482+
});
483+
484+
expect(themeJson.settings.color.palette).toContainEqual({
485+
name: 'Lighter Blue',
486+
slug: 'blue-100',
487+
color: '#e0f2fe',
488+
});
489+
490+
expect(themeJson.settings.color.palette).toContainEqual({
491+
name: 'Default Blue',
492+
slug: 'blue-500',
493+
color: '#3b82f6',
494+
});
495+
496+
expect(themeJson.settings.color.palette).toContainEqual({
497+
name: 'Darkest Blue',
498+
slug: 'blue-900',
499+
color: '#1e3a8a',
500+
});
501+
502+
expect(themeJson.settings.color.palette).toContainEqual({
503+
name: 'Primary',
504+
slug: 'primary',
505+
color: '#000000',
506+
});
507+
});
508+
509+
it('should format shades without labels as Color (shade)', () => {
510+
const plugin = wordpressThemeJson({
511+
tailwindConfig: mockTailwindConfig,
512+
// No shade labels configured
513+
});
514+
515+
const cssContent = `
516+
@theme {
517+
--color-blue-50: #f0f9ff;
518+
--color-blue-100: #e0f2fe;
519+
--color-red-500: #ef4444;
520+
--color-gray-900: #111827;
521+
--color-primary: #000000;
522+
}
523+
`;
524+
525+
(plugin.transform as any)(cssContent, 'app.css');
526+
const emitFile = vi.fn();
527+
(plugin.generateBundle as any).call({ emitFile });
528+
529+
const themeJson = JSON.parse(emitFile.mock.calls[0][0].source);
530+
531+
expect(themeJson.settings.color.palette).toContainEqual({
532+
name: 'Blue (50)',
533+
slug: 'blue-50',
534+
color: '#f0f9ff',
535+
});
536+
537+
expect(themeJson.settings.color.palette).toContainEqual({
538+
name: 'Blue (100)',
539+
slug: 'blue-100',
540+
color: '#e0f2fe',
541+
});
542+
543+
expect(themeJson.settings.color.palette).toContainEqual({
544+
name: 'Red (500)',
545+
slug: 'red-500',
546+
color: '#ef4444',
547+
});
548+
549+
expect(themeJson.settings.color.palette).toContainEqual({
550+
name: 'Gray (900)',
551+
slug: 'gray-900',
552+
color: '#111827',
553+
});
554+
555+
expect(themeJson.settings.color.palette).toContainEqual({
556+
name: 'Primary',
557+
slug: 'primary',
558+
color: '#000000',
559+
});
560+
});
561+
438562
it('should preserve existing theme.json settings', () => {
439563
const existingThemeJson = {
440564
settings: {

0 commit comments

Comments
 (0)