Skip to content

Commit 24829f0

Browse files
committed
feat(presets): add persona library (Midnight, Cyberpunk, Corporate, Minimalist)
1 parent 414ae03 commit 24829f0

7 files changed

Lines changed: 189 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file. We will log
1212
- **Responsive Layout Tokens**: Introduced a new `basic-layout` plugin to manage global responsive breakpoints, container widths, and gutter sizes.
1313
- **Motion & Animation System**: Implemented a dedicated `basic-motion` plugin with global duration and easing tokens, integrated across all components for smooth, consistent transitions.
1414
- **Custom HTML Sandbox**: Added a new sandbox plugin allowing users to test their current theme against custom markup with real-time feedback.
15+
- **Theme Presets (Personas)**: Introduced a library of "One-Click Personas" (Midnight Pro, Cyberpunk, Corporate Clean, Minimalist) for rapid theme experimentation.
1516

1617
### Changed
1718

src/app/preview.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,16 +73,15 @@ const createIframe = (): HTMLIFrameElement => {
7373
<style>${css}</style>
7474
</head>
7575
<body>
76-
${modules
77-
.map(
78-
(mod: PreviewModule, index: number) => `
76+
${modules.length === 1
77+
? `<div class="preview-pane">${modules[0].render(config)}</div>`
78+
: modules.map((mod: PreviewModule, index: number) => `
7979
<details class="preview-accordion"${index === 0 ? ' open' : ''}>
8080
<summary>${mod.title}</summary>
8181
<div class="preview-pane">${mod.render(config)}</div>
8282
</details>
83-
`
84-
)
85-
.join('\n')}
83+
`).join('\n')
84+
}
8685
</body>
8786
</html>
8887
`;

src/app/registry.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export type ControlApi = {
1212
getConfig: () => ThemeConfig;
1313
updateConfig: (mutator: (current: ThemeConfig) => ThemeConfig) => void;
1414
subscribe: (listener: (config: ThemeConfig) => void) => () => void;
15+
setActivePreview: (ids: string[]) => void;
1516
};
1617

1718
export type ControlsRegistry = Record<string, ControlModule>;

src/app/ui.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { inputsControlModule } from '../plugins/basic-inputs';
66
import { layoutControlModule } from '../plugins/basic-layout';
77
import { modalControlModule } from '../plugins/basic-modal';
88
import { motionControlModule } from '../plugins/basic-motion';
9+
import { presetsControlModule } from '../plugins/basic-presets';
910
import { radiusControlModule } from '../plugins/basic-radius';
1011
import { sandboxControlModule } from '../plugins/basic-sandbox';
1112
import { shadowControlModule } from '../plugins/basic-shadow';
@@ -21,6 +22,7 @@ import type { ControlsRegistry } from './registry';
2122
// here we will place all controls we will build
2223
export const controlsRegistry: ControlsRegistry = {
2324
name: themeNameControlModule,
25+
presets: presetsControlModule,
2426
sandbox: sandboxControlModule,
2527
layout: layoutControlModule,
2628
styleguide: styleguideControlModule,

src/main.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,12 @@ if (!controlsHost) {
8080
const preview = createPreview();
8181
preview.mount(previewHost);
8282

83-
const controlApi = { getConfig, updateConfig, subscribe };
83+
const controlApi = {
84+
getConfig,
85+
updateConfig,
86+
subscribe,
87+
setActivePreview: (ids: string[]) => preview.setActive(ids),
88+
};
8489
const controlCleanups: (() => void)[] = [];
8590
const openIds = new Set<string>();
8691
const accordions: { details: HTMLDetailsElement; id: string }[] = [];

src/plugins/basic-presets/index.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type { ControlModule } from '../../app/registry';
2+
import { buildThemeConfig, type PartialThemeConfig } from '../../compiler/types';
3+
4+
import { THEME_PRESETS } from './presets';
5+
6+
const renderPresetCard = (id: string, preset: PartialThemeConfig, activeName: string) => {
7+
const isActive = activeName === preset.name;
8+
const primary = preset.colors?.primary?.[500] ?? '#ccc';
9+
const neutral = preset.colors?.neutral?.[50] ?? '#fff';
10+
const neutral900 = preset.colors?.neutral?.[900] ?? '#000';
11+
12+
return `
13+
<div class="preset-card ${isActive ? 'active' : ''}"
14+
data-id="${id}"
15+
style="cursor: pointer; padding: 12px; border-radius: 12px; border: 2px solid ${isActive ? 'var(--color-primary-500)' : 'rgba(128,128,128,0.1)'}; background: var(--surface-card); transition: all 0.2s ease; position: relative; overflow: hidden;">
16+
<div style="display: flex; gap: 6px; margin-bottom: 8px;">
17+
<div style="width: 24px; height: 24px; border-radius: 50%; background: ${primary}; border: 1px solid rgba(0,0,0,0.1);"></div>
18+
<div style="width: 24px; height: 24px; border-radius: 50%; background: ${neutral}; border: 1px solid rgba(0,0,0,0.1);"></div>
19+
<div style="width: 24px; height: 24px; border-radius: 50%; background: ${neutral900}; border: 1px solid rgba(0,0,0,0.1);"></div>
20+
</div>
21+
<div style="font-size: 11px; font-weight: 800; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${preset.name}</div>
22+
${isActive ? '<div style="position: absolute; top: 6px; right: 6px; width: 6px; height: 6px; border-radius: 50%; background: var(--color-primary-500);"></div>' : ''}
23+
</div>
24+
`;
25+
};
26+
27+
export const presetsControlModule: ControlModule = {
28+
id: 'presets',
29+
title: 'Theme Presets',
30+
mount: (container, api) => {
31+
const render = () => {
32+
const cfg = api.getConfig();
33+
const activeName = cfg.name;
34+
35+
container.innerHTML = `
36+
<div class="control-group">
37+
<label style="margin-bottom: 12px; display: block; opacity: 0.8;">Choose a Visual Persona</label>
38+
<div class="presets-grid" style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
39+
${Object.entries(THEME_PRESETS).map(([id, preset]) => renderPresetCard(id, preset, activeName)).join('')}
40+
</div>
41+
</div>
42+
<p class="controls-hint" style="font-size: 11px; opacity: 0.6; margin-top: 12px;">
43+
Applying a preset will overwrite your current color and radius settings.
44+
</p>
45+
`;
46+
47+
container.querySelectorAll<HTMLElement>('.preset-card').forEach(card => {
48+
card.addEventListener('click', () => {
49+
const id = card.dataset.id;
50+
if (id && THEME_PRESETS[id]) {
51+
const fragment = THEME_PRESETS[id];
52+
api.updateConfig(current => buildThemeConfig(fragment, [current]));
53+
api.setActivePreview(['styleguide']);
54+
}
55+
});
56+
});
57+
};
58+
59+
const unsubscribe = api.subscribe(render);
60+
render();
61+
62+
return unsubscribe;
63+
}
64+
};
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import type { PartialThemeConfig } from '../../compiler/types';
2+
import { generateScale } from '../../utils/colors';
3+
4+
export const THEME_PRESETS: Record<string, PartialThemeConfig> = {
5+
aurora: {
6+
name: 'Aurora',
7+
colors: {
8+
primary: generateScale('#3b82f6'),
9+
secondary: generateScale('#10b981'),
10+
tertiary: generateScale('#8b5cf6'),
11+
neutral: generateScale('#f8fafc'),
12+
tuning: {
13+
tintStrength: 60,
14+
darkDepth: 25,
15+
lightDepth: 92,
16+
hueOffset: 0,
17+
},
18+
paletteMode: 'analogous',
19+
},
20+
radius: { sm: '4px', md: '8px', lg: '16px' },
21+
motion: {
22+
durations: { fast: 150, base: 300, slow: 500 },
23+
easing: { in: 'ease-in', out: 'ease-out', inOut: 'ease-in-out' }
24+
}
25+
},
26+
midnight: {
27+
name: 'Midnight Pro',
28+
colors: {
29+
primary: generateScale('#7c4dff'),
30+
secondary: generateScale('#448aff'),
31+
tertiary: generateScale('#b388ff'),
32+
neutral: generateScale('#0a0b10'),
33+
tuning: {
34+
tintStrength: 40,
35+
darkDepth: 12,
36+
lightDepth: 95,
37+
hueOffset: 0,
38+
},
39+
paletteMode: 'analogous',
40+
},
41+
radius: { sm: '6px', md: '12px', lg: '24px' },
42+
motion: {
43+
durations: { fast: 120, base: 250, slow: 450 },
44+
easing: {
45+
in: 'cubic-bezier(0.4, 0, 1, 1)',
46+
out: 'cubic-bezier(0, 0, 0.2, 1)',
47+
inOut: 'cubic-bezier(0.4, 0, 0.2, 1)',
48+
}
49+
}
50+
},
51+
cyberpunk: {
52+
name: 'Cyberpunk',
53+
colors: {
54+
primary: generateScale('#ff00cc'),
55+
secondary: generateScale('#00fbff'),
56+
tertiary: generateScale('#ffff00'),
57+
neutral: generateScale('#050505'),
58+
tuning: {
59+
tintStrength: 80,
60+
darkDepth: 5,
61+
lightDepth: 90,
62+
hueOffset: 0,
63+
},
64+
paletteMode: 'complementary',
65+
},
66+
radius: { sm: '0px', md: '0px', lg: '0px' },
67+
motion: {
68+
durations: { fast: 80, base: 150, slow: 300 },
69+
easing: {
70+
in: 'steps(4, end)',
71+
out: 'steps(4, start)',
72+
inOut: 'cubic-bezier(1, 0, 0, 1)',
73+
}
74+
}
75+
},
76+
corporate: {
77+
name: 'Corporate Clean',
78+
colors: {
79+
primary: generateScale('#0052cc'),
80+
secondary: generateScale('#0747a6'),
81+
tertiary: generateScale('#2684ff'),
82+
neutral: generateScale('#f4f5f7'),
83+
tuning: {
84+
tintStrength: 10,
85+
darkDepth: 25,
86+
lightDepth: 92,
87+
hueOffset: 0,
88+
},
89+
paletteMode: 'analogous',
90+
},
91+
radius: { sm: '2px', md: '4px', lg: '8px' },
92+
},
93+
minimalist: {
94+
name: 'Minimalist',
95+
colors: {
96+
primary: generateScale('#171717'),
97+
secondary: generateScale('#404040'),
98+
tertiary: generateScale('#737373'),
99+
neutral: generateScale('#ffffff'),
100+
tuning: {
101+
tintStrength: 0,
102+
darkDepth: 20,
103+
lightDepth: 100,
104+
hueOffset: 0,
105+
},
106+
paletteMode: 'manual',
107+
},
108+
radius: { sm: '8px', md: '16px', lg: '32px' },
109+
}
110+
};

0 commit comments

Comments
 (0)