|
| 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 | +}; |
0 commit comments