Skip to content

Commit 4259afb

Browse files
committed
feat(elevation): implement z-index foundation and update theme presets
- Add basic-elevation plugin for global stacking tokens - Define --z-index-* scale (hide to toast) - Update all theme presets with standard elevation values - Add Elevation Spec preview module with visual stacking gallery - Register foundation in compiler, UI, and preview registries
1 parent cb77ddd commit 4259afb

6 files changed

Lines changed: 183 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. We will log
1515
- **Theme Presets (Personas)**: Introduced a library of "One-Click Personas" (Midnight Pro, Cyberpunk, Corporate Clean, Minimalist) for rapid theme experimentation.
1616
- **Dev Handoff: Copy to Clipboard**: Added one-click copy functionality to the Style Guide, allowing users to instantly copy design tokens as CSS variables.
1717
- **Accessibility Master Audit**: Implemented a dedicated audit preview module that scans for WCAG contrast violations across all semantic color pairs and provides one-click "Nudge" fixes.
18+
- **Z-Index & Elevation Foundation**: Introduced a global stacking token system (`--z-index-*`) with a dedicated preview spec and interactive controls. All theme presets have been updated with standard elevation foundations.
1819

1920
### Changed
2021

src/app/preview-registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { alertPreviewModule } from '../plugins/basic-alert';
44
import { buttonsPreviewModule } from '../plugins/basic-buttons';
55
import { cardPreviewModule } from '../plugins/basic-card';
66
import { colorsPreviewModule } from '../plugins/basic-colors';
7+
import { elevationPreviewModule } from '../plugins/basic-elevation';
78
import { inputsPreviewModule } from '../plugins/basic-inputs';
89
import { layoutPreviewModule } from '../plugins/basic-layout';
910
import { modalPreviewModule } from '../plugins/basic-modal';
@@ -39,6 +40,7 @@ export const previewModules = [
3940
radiusPreviewModule,
4041
spacingPreviewModule,
4142
shadowPreviewModule,
43+
elevationPreviewModule,
4244
alertPreviewModule,
4345
tablePreviewModule,
4446
typographyPreviewModule,

src/app/ui.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { alertControlModule } from '../plugins/basic-alert';
33
import { buttonsControlModule } from '../plugins/basic-buttons';
44
import { cardControlModule } from '../plugins/basic-card';
55
import { colorsControlModule } from '../plugins/basic-colors';
6+
import { elevationControlModule } from '../plugins/basic-elevation';
67
import { inputsControlModule } from '../plugins/basic-inputs';
78
import { layoutControlModule } from '../plugins/basic-layout';
89
import { modalControlModule } from '../plugins/basic-modal';
@@ -34,6 +35,7 @@ export const controlsRegistry: ControlsRegistry = {
3435
spacing: spacingControlModule,
3536
radius: radiusControlModule,
3637
shadow: shadowControlModule,
38+
elevation: elevationControlModule,
3739
buttons: buttonsControlModule,
3840
inputs: inputsControlModule,
3941
card: cardControlModule,

src/compiler/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { alertCompilerEntry } from '../plugins/basic-alert';
22
import { buttonsCompilerEntry } from '../plugins/basic-buttons';
33
import { cardCompilerEntry } from '../plugins/basic-card';
44
import { colorsCompilerEntry } from '../plugins/basic-colors/index';
5+
import { elevationCompilerEntry } from '../plugins/basic-elevation';
56
import { inputsCompilerEntry } from '../plugins/basic-inputs';
67
import { layoutCompilerEntry } from '../plugins/basic-layout';
78
import { modalCompilerEntry } from '../plugins/basic-modal';
@@ -33,6 +34,7 @@ export const compilerRegistry: EmitterEntry[] = [
3334
spacingCompilerEntry,
3435
radiusCompilerEntry,
3536
shadowCompilerEntry,
37+
elevationCompilerEntry,
3638
buttonsCompilerEntry,
3739
inputsCompilerEntry,
3840
cardCompilerEntry,
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import type { ControlModule } from '../../app/registry';
2+
import type { ThemeConfig } from '../../compiler/types';
3+
4+
declare module '../../compiler/types' {
5+
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
6+
interface ThemeModules {
7+
elevation: Record<string, number>;
8+
}
9+
}
10+
11+
export const elevationCompilerEntry = {
12+
id: 'elevation' as const,
13+
title: 'Elevation',
14+
isEnabled: (config: ThemeConfig) => Boolean(config.elevation),
15+
emitTokens: (config: ThemeConfig) => {
16+
if (!config.elevation) return '';
17+
const lines = Object.keys(config.elevation)
18+
.sort((a, b) => config.elevation[a] - config.elevation[b])
19+
.map((key) => ` --z-index-${key}: ${config.elevation[key]};`);
20+
return lines.join('\n');
21+
},
22+
emitUtilities: (config: ThemeConfig) => {
23+
if (!config.elevation) return '';
24+
return Object.keys(config.elevation)
25+
.map((key) => `.z-${key} { z-index: var(--z-index-${key}); }`)
26+
.join('\n');
27+
},
28+
emitComponents: () => '',
29+
};
30+
31+
export const elevationControlModule: ControlModule = {
32+
id: 'elevation',
33+
title: 'Z-Index & Elevation',
34+
mount: (container, api) => {
35+
const renderElevationInput = (key: string, label: string) => `
36+
<div class="control-group">
37+
<label for="z-${key}">${label}</label>
38+
<div style="display: flex; gap: 8px; align-items: center;">
39+
<input id="z-${key}" name="z-${key}" type="number" step="1" style="flex: 1;" />
40+
<span style="font-size: 10px; opacity: 0.5; font-family: monospace;">z-${key}</span>
41+
</div>
42+
</div>
43+
`;
44+
45+
container.innerHTML = `
46+
<div class="elevation-editor" style="display: grid; gap: 12px;">
47+
<p class="controls-placeholder">Define global stacking order. Higher values overlap lower values.</p>
48+
${renderElevationInput('base', 'Base (Default)')}
49+
${renderElevationInput('raised', 'Raised')}
50+
${renderElevationInput('dropdown', 'Dropdown')}
51+
${renderElevationInput('sticky', 'Sticky')}
52+
${renderElevationInput('fixed', 'Fixed')}
53+
${renderElevationInput('modal', 'Modal')}
54+
${renderElevationInput('popover', 'Popover')}
55+
${renderElevationInput('tooltip', 'Tooltip')}
56+
${renderElevationInput('toast', 'Toast Notifications')}
57+
</div>
58+
`;
59+
60+
const keys = ['base', 'raised', 'dropdown', 'sticky', 'fixed', 'modal', 'popover', 'tooltip', 'toast'];
61+
62+
const sync = () => {
63+
const cfg = api.getConfig();
64+
if (!cfg.elevation) return;
65+
66+
keys.forEach((key) => {
67+
const input = container.querySelector<HTMLInputElement>(`#z-${key}`);
68+
if (input) input.value = String(cfg.elevation?.[key] ?? 0);
69+
});
70+
};
71+
72+
const onChange = () => {
73+
const nextElevation: Record<string, number> = {
74+
hide: -1,
75+
auto: 0, // Placeholder-ish
76+
max: 2147483647
77+
};
78+
79+
keys.forEach((key) => {
80+
const input = container.querySelector<HTMLInputElement>(`#z-${key}`);
81+
if (input) nextElevation[key] = Number(input.value);
82+
});
83+
84+
api.updateConfig((cfg) => ({
85+
...cfg,
86+
elevation: { ...cfg.elevation, ...nextElevation },
87+
}));
88+
};
89+
90+
container.addEventListener('input', onChange);
91+
const unsubscribe = api.subscribe(sync);
92+
sync();
93+
94+
return () => {
95+
unsubscribe();
96+
container.removeEventListener('input', onChange);
97+
};
98+
}
99+
};
100+
101+
export const elevationPreviewModule = {
102+
id: 'elevation',
103+
title: 'Elevation Spec',
104+
render: (config: ThemeConfig) => {
105+
const elev = config.elevation ?? elevationDefaults.elevation;
106+
107+
// Create a visual stacking order list
108+
const sortedLevels = Object.entries(elev)
109+
.sort(([, a], [, b]) => a - b);
110+
111+
return `
112+
<div style="color: var(--on-background);">
113+
<h3>Stacking Order</h3>
114+
<p style="font-size: 12px; opacity: 0.7; margin-bottom: 24px;">Global z-index tokens for preventing layer conflicts.</p>
115+
116+
<div style="display: grid; gap: 8px; margin-bottom: 40px;">
117+
${sortedLevels.map(([key, val]) => `
118+
<div style="display: flex; justify-content: space-between; align-items: center; padding: 12px; background: var(--surface-card); border-radius: 8px; border: 1px solid rgba(128,128,128,0.1);">
119+
<div>
120+
<span style="font-weight: 700; color: var(--color-primary-500);">--z-index-${key}</span>
121+
<div style="font-size: 10px; opacity: 0.6;">.z-${key}</div>
122+
</div>
123+
<code style="font-weight: 800; background: rgba(0,0,0,0.2); padding: 4px 8px; border-radius: 4px;">${val}</code>
124+
</div>
125+
`).join('')}
126+
</div>
127+
128+
<h3>Visual Stacking Preview</h3>
129+
<div style="position: relative; height: 300px; display: flex; align-items: center; justify-content: center; background: #000; border-radius: 12px; overflow: hidden;">
130+
<div class="z-base" style="position: absolute; width: 200px; height: 120px; background: #1e293b; border: 2px solid #334155; border-radius: 8px; display: flex; align-items: flex-start; padding: 8px; color: #fff; font-size: 10px; transform: translate(-40px, -40px);">BASE (0)</div>
131+
<div class="z-raised" style="position: absolute; width: 200px; height: 120px; background: #334155; border: 2px solid #475569; border-radius: 8px; display: flex; align-items: flex-start; padding: 8px; color: #fff; font-size: 10px; transform: translate(-20px, -20px); box-shadow: var(--shadow-sm);">RAISED (10)</div>
132+
<div class="z-dropdown" style="position: absolute; width: 200px; height: 120px; background: #475569; border: 2px solid #64748b; border-radius: 8px; display: flex; align-items: flex-start; padding: 8px; color: #fff; font-size: 10px; transform: translate(0, 0); box-shadow: var(--shadow-md);">DROPDOWN (1000)</div>
133+
<div class="z-modal" style="position: absolute; width: 200px; height: 120px; background: var(--color-primary-500); border: 2px solid var(--color-primary-600); border-radius: 8px; display: flex; align-items: flex-start; padding: 8px; color: var(--on-primary); font-size: 10px; transform: translate(20px, 20px); box-shadow: var(--shadow-lg);">MODAL (1300)</div>
134+
<div class="z-tooltip" style="position: absolute; width: 80px; height: 30px; background: #fff; border-radius: 4px; display: flex; align-items: center; justify-content: center; color: #000; font-size: 9px; font-weight: 900; transform: translate(60px, 40px);">TOOLTIP (1500)</div>
135+
</div>
136+
</div>
137+
`;
138+
},
139+
};
140+
141+
export const elevationDefaults = {
142+
elevation: {
143+
hide: -1,
144+
auto: 0,
145+
base: 0,
146+
raised: 10,
147+
dropdown: 1000,
148+
sticky: 1100,
149+
fixed: 1200,
150+
modal: 1300,
151+
popover: 1400,
152+
tooltip: 1500,
153+
toast: 1600,
154+
max: 2147483647
155+
}
156+
};

src/plugins/basic-presets/presets.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ export const THEME_PRESETS: Record<string, PartialThemeConfig> = {
2121
motion: {
2222
durations: { fast: 150, base: 300, slow: 500 },
2323
easing: { in: 'ease-in', out: 'ease-out', inOut: 'ease-in-out' }
24+
},
25+
elevation: {
26+
hide: -1, auto: 0, base: 0, raised: 10, dropdown: 1000, sticky: 1100,
27+
fixed: 1200, modal: 1300, popover: 1400, tooltip: 1500, toast: 1600, max: 2147483647
2428
}
2529
},
2630
midnight: {
@@ -46,6 +50,10 @@ export const THEME_PRESETS: Record<string, PartialThemeConfig> = {
4650
out: 'cubic-bezier(0, 0, 0.2, 1)',
4751
inOut: 'cubic-bezier(0.4, 0, 0.2, 1)',
4852
}
53+
},
54+
elevation: {
55+
hide: -1, auto: 0, base: 0, raised: 10, dropdown: 1000, sticky: 1100,
56+
fixed: 1200, modal: 1300, popover: 1400, tooltip: 1500, toast: 1600, max: 2147483647
4957
}
5058
},
5159
cyberpunk: {
@@ -71,6 +79,10 @@ export const THEME_PRESETS: Record<string, PartialThemeConfig> = {
7179
out: 'steps(4, start)',
7280
inOut: 'cubic-bezier(1, 0, 0, 1)',
7381
}
82+
},
83+
elevation: {
84+
hide: -1, auto: 0, base: 0, raised: 10, dropdown: 1000, sticky: 1100,
85+
fixed: 1200, modal: 1300, popover: 1400, tooltip: 1500, toast: 1600, max: 2147483647
7486
}
7587
},
7688
corporate: {
@@ -89,6 +101,10 @@ export const THEME_PRESETS: Record<string, PartialThemeConfig> = {
89101
paletteMode: 'analogous',
90102
},
91103
radius: { sm: '2px', md: '4px', lg: '8px' },
104+
elevation: {
105+
hide: -1, auto: 0, base: 0, raised: 10, dropdown: 1000, sticky: 1100,
106+
fixed: 1200, modal: 1300, popover: 1400, tooltip: 1500, toast: 1600, max: 2147483647
107+
}
92108
},
93109
minimalist: {
94110
name: 'Minimalist',
@@ -106,5 +122,9 @@ export const THEME_PRESETS: Record<string, PartialThemeConfig> = {
106122
paletteMode: 'manual',
107123
},
108124
radius: { sm: '8px', md: '16px', lg: '32px' },
125+
elevation: {
126+
hide: -1, auto: 0, base: 0, raised: 10, dropdown: 1000, sticky: 1100,
127+
fixed: 1200, modal: 1300, popover: 1400, tooltip: 1500, toast: 1600, max: 2147483647
128+
}
109129
}
110130
};

0 commit comments

Comments
 (0)