lumina-slides 8.9.4 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LUMINA_LLM_EXAMPLES.json +234 -0
- package/README.md +18 -18
- package/dist/lumina-slides.js +13207 -12659
- package/dist/lumina-slides.umd.cjs +215 -215
- package/dist/style.css +1 -1
- package/package.json +5 -4
- package/src/App.vue +16 -0
- package/src/animation/index.ts +11 -0
- package/src/animation/registry.ts +126 -0
- package/src/animation/stagger.ts +95 -0
- package/src/animation/types.ts +53 -0
- package/src/components/LandingPage.vue +229 -0
- package/src/components/LuminaDeck.vue +224 -0
- package/src/components/LuminaSpeakerNotes.vue +701 -0
- package/src/components/base/BaseSlide.vue +122 -0
- package/src/components/base/LuminaElement.vue +67 -0
- package/src/components/base/VideoPlayer.vue +204 -0
- package/src/components/layouts/LayoutAuto.vue +71 -0
- package/src/components/layouts/LayoutChart.vue +287 -0
- package/src/components/layouts/LayoutCustom.vue +92 -0
- package/src/components/layouts/LayoutDiagram.vue +253 -0
- package/src/components/layouts/LayoutFeatures.vue +121 -0
- package/src/components/layouts/LayoutFlex.vue +172 -0
- package/src/components/layouts/LayoutFree.vue +62 -0
- package/src/components/layouts/LayoutHalf.vue +127 -0
- package/src/components/layouts/LayoutStatement.vue +74 -0
- package/src/components/layouts/LayoutSteps.vue +106 -0
- package/src/components/layouts/LayoutTimeline.vue +104 -0
- package/src/components/layouts/LayoutVideo.vue +41 -0
- package/src/components/parts/FlexBullets.vue +45 -0
- package/src/components/parts/FlexButton.vue +132 -0
- package/src/components/parts/FlexImage.vue +54 -0
- package/src/components/parts/FlexOrdered.vue +44 -0
- package/src/components/parts/FlexSpacer.vue +13 -0
- package/src/components/parts/FlexStepper.vue +59 -0
- package/src/components/parts/FlexText.vue +29 -0
- package/src/components/parts/FlexTimeline.vue +67 -0
- package/src/components/parts/FlexTitle.vue +39 -0
- package/src/components/parts/LuminaBackground.vue +100 -0
- package/src/components/site/LivePreview.vue +101 -0
- package/src/components/site/SiteApi.vue +301 -0
- package/src/components/site/SiteDashboard.vue +604 -0
- package/src/components/site/SiteDocs.vue +3267 -0
- package/src/components/site/SiteExamples.vue +65 -0
- package/src/components/site/SiteFooter.vue +6 -0
- package/src/components/site/SiteHome.vue +362 -0
- package/src/components/site/SiteNavBar.vue +122 -0
- package/src/components/site/SitePlayground.vue +389 -0
- package/src/components/site/SitePromptBuilder.vue +266 -0
- package/src/components/site/SiteUserMenu.vue +90 -0
- package/src/components/studio/ActionEditor.vue +108 -0
- package/src/components/studio/ArrayEditor.vue +124 -0
- package/src/components/studio/CollapsibleSection.vue +33 -0
- package/src/components/studio/ColorField.vue +22 -0
- package/src/components/studio/EditorCanvas.vue +326 -0
- package/src/components/studio/EditorLayoutFeatures.vue +18 -0
- package/src/components/studio/EditorLayoutFixed.vue +46 -0
- package/src/components/studio/EditorLayoutFlex.vue +133 -0
- package/src/components/studio/EditorLayoutHalf.vue +18 -0
- package/src/components/studio/EditorLayoutStatement.vue +18 -0
- package/src/components/studio/EditorLayoutSteps.vue +18 -0
- package/src/components/studio/EditorLayoutTimeline.vue +18 -0
- package/src/components/studio/EditorNode.vue +89 -0
- package/src/components/studio/FieldEditor.vue +133 -0
- package/src/components/studio/IconPicker.vue +109 -0
- package/src/components/studio/LayerItem.vue +117 -0
- package/src/components/studio/LuminaStudio.vue +30 -0
- package/src/components/studio/SaveSuccessModal.vue +138 -0
- package/src/components/studio/SlideNavigator.vue +373 -0
- package/src/components/studio/SliderField.vue +44 -0
- package/src/components/studio/StudioInspector.vue +595 -0
- package/src/components/studio/StudioJsonEditor.vue +191 -0
- package/src/components/studio/StudioLayers.vue +145 -0
- package/src/components/studio/StudioSettings.vue +514 -0
- package/src/components/studio/StudioSidebar.vue +29 -0
- package/src/components/studio/StudioToolbar.vue +222 -0
- package/src/components/studio/fieldLabels.ts +224 -0
- package/src/components/studio/inspectors/DiagramEdgeEditor.vue +77 -0
- package/src/components/studio/inspectors/DiagramNodeEditor.vue +117 -0
- package/src/components/studio/nodes/StudioDiagramNode.vue +138 -0
- package/src/composables/useAuth.ts +87 -0
- package/src/composables/useEditor.ts +224 -0
- package/src/composables/useElementState.ts +81 -0
- package/src/composables/useFlexLayout.ts +122 -0
- package/src/composables/useKeyboard.ts +45 -0
- package/src/composables/useLumina.ts +32 -0
- package/src/composables/useStudio.ts +87 -0
- package/src/composables/useSwipeNav.ts +53 -0
- package/src/composables/useTransition.ts +373 -0
- package/src/core/Lumina.ts +819 -0
- package/src/core/animationConfig.ts +251 -0
- package/src/core/compression.ts +34 -0
- package/src/core/elementController.ts +170 -0
- package/src/core/elementId.ts +27 -0
- package/src/core/elementResolver.ts +207 -0
- package/src/core/events.ts +53 -0
- package/src/core/fonts.ts +100 -0
- package/src/core/presets.ts +231 -0
- package/src/core/prompts.ts +272 -0
- package/src/core/schema.ts +478 -0
- package/src/core/speaker-channel.ts +250 -0
- package/src/core/store.ts +461 -0
- package/src/core/theme.ts +666 -0
- package/src/core/types.ts +1611 -0
- package/src/directives/vStudio.ts +45 -0
- package/src/index.ts +175 -0
- package/src/main.ts +17 -0
- package/src/router/index.ts +92 -0
- package/src/style/main.css +462 -0
- package/src/utils/deep.ts +127 -0
- package/src/utils/firebase.ts +184 -0
- package/src/utils/streaming.ts +134 -0
- package/src/views/DashboardView.vue +32 -0
- package/src/views/DeckView.vue +289 -0
- package/src/views/HomeView.vue +17 -0
- package/src/views/SiteLayout.vue +21 -0
- package/src/views/StudioView.vue +61 -0
- package/src/vite-env.d.ts +6 -0
- package/IMPLEMENTATION.md +0 -418
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
import type { ThemeConfig } from './types';
|
|
2
|
+
import { THEME_PRESETS } from './presets';
|
|
3
|
+
import { loadGoogleFont } from './fonts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Default Theme Values
|
|
7
|
+
|
|
8
|
+
* Complete set of default design tokens for the Lumina Engine.
|
|
9
|
+
*/
|
|
10
|
+
export const DEFAULT_THEME: Required<ThemeConfig> = {
|
|
11
|
+
colors: {
|
|
12
|
+
// Core
|
|
13
|
+
primary: '#3b82f6',
|
|
14
|
+
secondary: '#8b5cf6',
|
|
15
|
+
accent: '#06b6d4',
|
|
16
|
+
background: '#030303',
|
|
17
|
+
surface: '#0a0a0a',
|
|
18
|
+
text: '#ffffff',
|
|
19
|
+
textSecondary: '#e5e7eb',
|
|
20
|
+
muted: '#9ca3af',
|
|
21
|
+
|
|
22
|
+
// Semantic
|
|
23
|
+
success: '#10b981',
|
|
24
|
+
warning: '#f59e0b',
|
|
25
|
+
danger: '#ef4444',
|
|
26
|
+
info: '#3b82f6',
|
|
27
|
+
|
|
28
|
+
// UI Elements
|
|
29
|
+
border: 'rgba(255, 255, 255, 0.1)',
|
|
30
|
+
borderSubtle: 'rgba(255, 255, 255, 0.05)',
|
|
31
|
+
shadow: '#000000',
|
|
32
|
+
overlay: 'rgba(0, 0, 0, 0.5)',
|
|
33
|
+
highlight: 'rgba(59, 130, 246, 0.2)',
|
|
34
|
+
|
|
35
|
+
// Interactive
|
|
36
|
+
buttonPrimary: '#3b82f6',
|
|
37
|
+
buttonPrimaryText: '#ffffff',
|
|
38
|
+
buttonSecondary: 'rgba(255, 255, 255, 0.1)',
|
|
39
|
+
buttonSecondaryText: '#ffffff',
|
|
40
|
+
link: '#3b82f6',
|
|
41
|
+
linkHover: '#60a5fa',
|
|
42
|
+
|
|
43
|
+
// Gradients
|
|
44
|
+
gradientFrom: '#3b82f6',
|
|
45
|
+
gradientVia: undefined,
|
|
46
|
+
gradientTo: '#8b5cf6',
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
typography: {
|
|
50
|
+
fontFamily: {
|
|
51
|
+
heading: 'Inter, system-ui, sans-serif',
|
|
52
|
+
body: 'Inter, system-ui, sans-serif',
|
|
53
|
+
mono: 'ui-monospace, SFMono-Regular, monospace',
|
|
54
|
+
},
|
|
55
|
+
fontSize: {
|
|
56
|
+
xs: '0.75rem',
|
|
57
|
+
sm: '0.875rem',
|
|
58
|
+
base: '1rem',
|
|
59
|
+
lg: '1.125rem',
|
|
60
|
+
xl: '1.25rem',
|
|
61
|
+
'2xl': '1.5rem',
|
|
62
|
+
'3xl': '1.875rem',
|
|
63
|
+
'4xl': '2.25rem',
|
|
64
|
+
'5xl': '3rem',
|
|
65
|
+
'6xl': '3.75rem',
|
|
66
|
+
'7xl': '4.5rem',
|
|
67
|
+
},
|
|
68
|
+
fontWeight: {
|
|
69
|
+
light: 300,
|
|
70
|
+
normal: 400,
|
|
71
|
+
medium: 500,
|
|
72
|
+
semibold: 600,
|
|
73
|
+
bold: 800,
|
|
74
|
+
extrabold: 900,
|
|
75
|
+
},
|
|
76
|
+
lineHeight: {
|
|
77
|
+
tight: '1.1',
|
|
78
|
+
snug: '1.25',
|
|
79
|
+
normal: '1.5',
|
|
80
|
+
relaxed: '1.625',
|
|
81
|
+
loose: '2',
|
|
82
|
+
},
|
|
83
|
+
letterSpacing: {
|
|
84
|
+
tighter: '-0.05em',
|
|
85
|
+
tight: '-0.025em',
|
|
86
|
+
normal: '0',
|
|
87
|
+
wide: '0.025em',
|
|
88
|
+
wider: '0.05em',
|
|
89
|
+
widest: '0.1em',
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
spacing: {
|
|
94
|
+
none: '0',
|
|
95
|
+
xs: '0.25rem',
|
|
96
|
+
sm: '0.5rem',
|
|
97
|
+
md: '1rem',
|
|
98
|
+
lg: '1.5rem',
|
|
99
|
+
xl: '2rem',
|
|
100
|
+
'2xl': '3rem',
|
|
101
|
+
'3xl': '4rem',
|
|
102
|
+
'4xl': '6rem',
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
borderRadius: {
|
|
106
|
+
none: '0',
|
|
107
|
+
sm: '0.25rem',
|
|
108
|
+
md: '0.5rem',
|
|
109
|
+
lg: '0.75rem',
|
|
110
|
+
xl: '1rem',
|
|
111
|
+
'2xl': '1.5rem',
|
|
112
|
+
'3xl': '2rem',
|
|
113
|
+
full: '9999px',
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
effects: {
|
|
117
|
+
// Gradients
|
|
118
|
+
useGradients: true,
|
|
119
|
+
gradientDirection: 'to-b',
|
|
120
|
+
gradientFrom: undefined, // Falls back to colors.gradientFrom
|
|
121
|
+
gradientVia: undefined,
|
|
122
|
+
gradientTo: undefined, // Falls back to colors.gradientTo
|
|
123
|
+
|
|
124
|
+
// Shadows
|
|
125
|
+
useShadows: true,
|
|
126
|
+
shadowIntensity: 'md',
|
|
127
|
+
shadowColor: 'rgba(0, 0, 0, 0.4)',
|
|
128
|
+
|
|
129
|
+
// Glass
|
|
130
|
+
useGlass: true,
|
|
131
|
+
glassOpacity: 0.12,
|
|
132
|
+
glassBlur: '20px',
|
|
133
|
+
glassBorderOpacity: 0.20,
|
|
134
|
+
|
|
135
|
+
// Orb
|
|
136
|
+
useOrb: true,
|
|
137
|
+
orbOpacity: 0.20,
|
|
138
|
+
orbBlur: '120px',
|
|
139
|
+
orbSize: '80vw',
|
|
140
|
+
|
|
141
|
+
// Animations
|
|
142
|
+
animationsEnabled: true,
|
|
143
|
+
transitionDuration: '0.3s',
|
|
144
|
+
transitionEasing: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
|
145
|
+
hoverScale: 1.05,
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
components: {
|
|
149
|
+
// Buttons
|
|
150
|
+
buttonRadius: '9999px',
|
|
151
|
+
buttonPadding: '0.75rem 1.5rem',
|
|
152
|
+
buttonFontWeight: 700,
|
|
153
|
+
buttonTextTransform: 'none',
|
|
154
|
+
|
|
155
|
+
// Cards
|
|
156
|
+
cardRadius: '1.5rem',
|
|
157
|
+
cardPadding: '2rem',
|
|
158
|
+
cardBorderWidth: '1px',
|
|
159
|
+
cardBackground: undefined, // Uses surface color
|
|
160
|
+
|
|
161
|
+
// Timeline
|
|
162
|
+
timelineNodeSize: '1rem',
|
|
163
|
+
timelineLineWidth: '2px',
|
|
164
|
+
timelineNodeColor: undefined, // Uses primary
|
|
165
|
+
timelineLineColor: undefined, // Uses border
|
|
166
|
+
|
|
167
|
+
// Steps
|
|
168
|
+
stepBadgeSize: '2.5rem',
|
|
169
|
+
stepFontSize: '1rem',
|
|
170
|
+
|
|
171
|
+
// Progress
|
|
172
|
+
progressHeight: '0.5rem',
|
|
173
|
+
progressRadius: '9999px',
|
|
174
|
+
progressBackground: 'rgba(255, 255, 255, 0.1)',
|
|
175
|
+
progressFill: undefined, // Uses primary
|
|
176
|
+
|
|
177
|
+
// Tags
|
|
178
|
+
tagPadding: '0.25rem 1rem',
|
|
179
|
+
tagRadius: '9999px',
|
|
180
|
+
tagFontSize: '0.75rem',
|
|
181
|
+
|
|
182
|
+
// Inputs
|
|
183
|
+
inputRadius: '0.5rem',
|
|
184
|
+
inputPadding: '0.75rem 1rem',
|
|
185
|
+
inputBorder: undefined, // Uses border color
|
|
186
|
+
inputFocusBorder: undefined, // Uses primary
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
// Legacy (empty object - use typography.fontFamily instead)
|
|
190
|
+
fonts: {},
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const DEFAULT_THEME_ID = 'lumina-theme-styles';
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Deep merge utility for theme configuration.
|
|
197
|
+
* Exported for use when merging meta.themeConfig with direct meta overrides.
|
|
198
|
+
*/
|
|
199
|
+
export function deepMerge<T extends Record<string, any>>(target: T, source: Partial<T>): T {
|
|
200
|
+
const result = { ...target };
|
|
201
|
+
|
|
202
|
+
for (const key in source) {
|
|
203
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
204
|
+
const sourceValue = source[key];
|
|
205
|
+
const targetValue = target[key];
|
|
206
|
+
|
|
207
|
+
if (
|
|
208
|
+
sourceValue !== null &&
|
|
209
|
+
sourceValue !== undefined &&
|
|
210
|
+
typeof sourceValue === 'object' &&
|
|
211
|
+
!Array.isArray(sourceValue) &&
|
|
212
|
+
typeof targetValue === 'object' &&
|
|
213
|
+
!Array.isArray(targetValue)
|
|
214
|
+
) {
|
|
215
|
+
result[key] = deepMerge(targetValue, sourceValue);
|
|
216
|
+
} else if (sourceValue !== undefined) {
|
|
217
|
+
result[key] = sourceValue as any;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* THEME MANAGER
|
|
227
|
+
* Generates and injects CSS Custom Properties based on the configuration.
|
|
228
|
+
*/
|
|
229
|
+
export class ThemeManager {
|
|
230
|
+
/**
|
|
231
|
+
* Injects the requested theme into the document head.
|
|
232
|
+
*
|
|
233
|
+
* @param theme - A preset name ('ocean', 'cyber') or a complete ThemeConfig object.
|
|
234
|
+
* @param overrides - Optional additional overrides to merge on top.
|
|
235
|
+
*/
|
|
236
|
+
static inject(theme: ThemeConfig | string, overrides?: ThemeConfig) {
|
|
237
|
+
let config: ThemeConfig;
|
|
238
|
+
|
|
239
|
+
// 1. Resolve Base Preset or Config
|
|
240
|
+
if (typeof theme === 'string') {
|
|
241
|
+
const preset = THEME_PRESETS[theme] || THEME_PRESETS['default'];
|
|
242
|
+
config = deepMerge(DEFAULT_THEME, preset);
|
|
243
|
+
} else {
|
|
244
|
+
config = deepMerge(DEFAULT_THEME, theme);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 2. Apply Overrides
|
|
248
|
+
if (overrides) {
|
|
249
|
+
config = deepMerge(config, overrides);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 3. Handle legacy fonts -> typography migration
|
|
253
|
+
if (config.fonts && !config.typography?.fontFamily) {
|
|
254
|
+
config.typography = config.typography || {};
|
|
255
|
+
config.typography.fontFamily = {
|
|
256
|
+
heading: config.fonts.heading,
|
|
257
|
+
body: config.fonts.body,
|
|
258
|
+
mono: config.fonts.mono,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
let styleEl = document.getElementById(DEFAULT_THEME_ID);
|
|
263
|
+
|
|
264
|
+
if (!styleEl) {
|
|
265
|
+
styleEl = document.createElement('style');
|
|
266
|
+
styleEl.id = DEFAULT_THEME_ID;
|
|
267
|
+
document.head.appendChild(styleEl);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const vars = this.generateVars(config);
|
|
271
|
+
styleEl.innerHTML = `:root { ${vars} }`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Gets the current resolved theme configuration.
|
|
276
|
+
*/
|
|
277
|
+
static getDefaultTheme(): Required<ThemeConfig> {
|
|
278
|
+
return DEFAULT_THEME;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Converts a ThemeConfig object into a CSS string of custom properties.
|
|
283
|
+
*/
|
|
284
|
+
private static generateVars(theme: ThemeConfig): string {
|
|
285
|
+
const cssVars: string[] = [];
|
|
286
|
+
|
|
287
|
+
// --- Colors ---
|
|
288
|
+
if (theme.colors) {
|
|
289
|
+
for (const [key, value] of Object.entries(theme.colors)) {
|
|
290
|
+
if (value) {
|
|
291
|
+
const cssKey = this.camelToKebab(key);
|
|
292
|
+
cssVars.push(`--lumina-color-${cssKey}: ${value};`);
|
|
293
|
+
|
|
294
|
+
// RGB conversion for opacity usage
|
|
295
|
+
const rgb = this.hexToRgb(value);
|
|
296
|
+
if (rgb) {
|
|
297
|
+
cssVars.push(`--lumina-color-${cssKey}-rgb: ${rgb.r}, ${rgb.g}, ${rgb.b};`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// --- Typography ---
|
|
304
|
+
if (theme.typography) {
|
|
305
|
+
const typo = theme.typography;
|
|
306
|
+
|
|
307
|
+
// Font Families
|
|
308
|
+
if (typo.fontFamily) {
|
|
309
|
+
for (const [key, value] of Object.entries(typo.fontFamily)) {
|
|
310
|
+
if (value) {
|
|
311
|
+
cssVars.push(`--lumina-font-${key}: ${value};`);
|
|
312
|
+
// Auto-load if it's a known Google Font
|
|
313
|
+
loadGoogleFont(value);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Font Sizes
|
|
319
|
+
if (typo.fontSize) {
|
|
320
|
+
for (const [key, value] of Object.entries(typo.fontSize)) {
|
|
321
|
+
if (value) cssVars.push(`--lumina-text-${key}: ${value};`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Font Weights
|
|
326
|
+
if (typo.fontWeight) {
|
|
327
|
+
for (const [key, value] of Object.entries(typo.fontWeight)) {
|
|
328
|
+
if (value !== undefined) cssVars.push(`--lumina-font-weight-${key}: ${value};`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Line Heights
|
|
333
|
+
if (typo.lineHeight) {
|
|
334
|
+
for (const [key, value] of Object.entries(typo.lineHeight)) {
|
|
335
|
+
if (value) cssVars.push(`--lumina-leading-${key}: ${value};`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Letter Spacing
|
|
340
|
+
if (typo.letterSpacing) {
|
|
341
|
+
for (const [key, value] of Object.entries(typo.letterSpacing)) {
|
|
342
|
+
if (value) cssVars.push(`--lumina-tracking-${key}: ${value};`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// --- Spacing ---
|
|
348
|
+
if (theme.spacing) {
|
|
349
|
+
for (const [key, value] of Object.entries(theme.spacing)) {
|
|
350
|
+
if (value) cssVars.push(`--lumina-space-${key}: ${value};`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// --- Border Radius ---
|
|
355
|
+
if (theme.borderRadius) {
|
|
356
|
+
for (const [key, value] of Object.entries(theme.borderRadius)) {
|
|
357
|
+
if (value) cssVars.push(`--lumina-radius-${key}: ${value};`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// --- Effects ---
|
|
362
|
+
if (theme.effects) {
|
|
363
|
+
const fx = theme.effects;
|
|
364
|
+
|
|
365
|
+
// Booleans as 0/1 for CSS calc tricks
|
|
366
|
+
cssVars.push(`--lumina-use-gradients: ${fx.useGradients ? 1 : 0};`);
|
|
367
|
+
cssVars.push(`--lumina-use-shadows: ${fx.useShadows ? 1 : 0};`);
|
|
368
|
+
cssVars.push(`--lumina-use-glass: ${fx.useGlass ? 1 : 0};`);
|
|
369
|
+
cssVars.push(`--lumina-use-orb: ${fx.useOrb ? 1 : 0};`);
|
|
370
|
+
cssVars.push(`--lumina-use-animations: ${fx.animationsEnabled ? 1 : 0};`);
|
|
371
|
+
|
|
372
|
+
// Gradient Logic
|
|
373
|
+
if (fx.useGradients) {
|
|
374
|
+
cssVars.push(`--lumina-gradient-text-fill: transparent;`);
|
|
375
|
+
cssVars.push(`--lumina-gradient-bg-clip: text;`);
|
|
376
|
+
} else {
|
|
377
|
+
cssVars.push(`--lumina-gradient-text-fill: currentColor;`);
|
|
378
|
+
cssVars.push(`--lumina-gradient-bg-clip: border-box;`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Gradient Colors
|
|
382
|
+
if (fx.gradientDirection) cssVars.push(`--lumina-gradient-direction: ${this.gradientDirectionToCSS(fx.gradientDirection)};`);
|
|
383
|
+
if (fx.gradientFrom) cssVars.push(`--lumina-gradient-from: ${fx.gradientFrom};`);
|
|
384
|
+
if (fx.gradientVia) cssVars.push(`--lumina-gradient-via: ${fx.gradientVia};`);
|
|
385
|
+
if (fx.gradientTo) cssVars.push(`--lumina-gradient-to: ${fx.gradientTo};`);
|
|
386
|
+
|
|
387
|
+
// Shadows
|
|
388
|
+
if (fx.shadowIntensity) cssVars.push(`--lumina-shadow-intensity: ${fx.shadowIntensity};`);
|
|
389
|
+
if (fx.shadowColor) cssVars.push(`--lumina-shadow-color: ${fx.shadowColor};`);
|
|
390
|
+
|
|
391
|
+
// Glass
|
|
392
|
+
if (fx.glassOpacity !== undefined) cssVars.push(`--lumina-glass-opacity: ${fx.glassOpacity};`);
|
|
393
|
+
if (fx.glassBlur) cssVars.push(`--lumina-glass-blur: ${fx.glassBlur};`);
|
|
394
|
+
if (fx.glassBorderOpacity !== undefined) cssVars.push(`--lumina-glass-border-opacity: ${fx.glassBorderOpacity};`);
|
|
395
|
+
|
|
396
|
+
// Orb
|
|
397
|
+
if (fx.orbOpacity !== undefined) cssVars.push(`--lumina-orb-opacity: ${fx.orbOpacity};`);
|
|
398
|
+
if (fx.orbBlur) cssVars.push(`--lumina-orb-blur: ${fx.orbBlur};`);
|
|
399
|
+
if (fx.orbSize) cssVars.push(`--lumina-orb-size: ${fx.orbSize};`);
|
|
400
|
+
|
|
401
|
+
// Animations
|
|
402
|
+
if (fx.transitionDuration) cssVars.push(`--lumina-transition-duration: ${fx.transitionDuration};`);
|
|
403
|
+
if (fx.transitionEasing) cssVars.push(`--lumina-transition-easing: ${fx.transitionEasing};`);
|
|
404
|
+
if (fx.hoverScale !== undefined) cssVars.push(`--lumina-hover-scale: ${fx.hoverScale};`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// --- Components ---
|
|
408
|
+
if (theme.components) {
|
|
409
|
+
const comp = theme.components;
|
|
410
|
+
|
|
411
|
+
// Buttons
|
|
412
|
+
if (comp.buttonRadius) cssVars.push(`--lumina-button-radius: ${comp.buttonRadius};`);
|
|
413
|
+
if (comp.buttonPadding) cssVars.push(`--lumina-button-padding: ${comp.buttonPadding};`);
|
|
414
|
+
if (comp.buttonFontWeight !== undefined) cssVars.push(`--lumina-button-font-weight: ${comp.buttonFontWeight};`);
|
|
415
|
+
if (comp.buttonTextTransform) cssVars.push(`--lumina-button-text-transform: ${comp.buttonTextTransform};`);
|
|
416
|
+
|
|
417
|
+
// Cards
|
|
418
|
+
if (comp.cardRadius) cssVars.push(`--lumina-card-radius: ${comp.cardRadius};`);
|
|
419
|
+
if (comp.cardPadding) cssVars.push(`--lumina-card-padding: ${comp.cardPadding};`);
|
|
420
|
+
if (comp.cardBorderWidth) cssVars.push(`--lumina-card-border-width: ${comp.cardBorderWidth};`);
|
|
421
|
+
if (comp.cardBackground) cssVars.push(`--lumina-card-background: ${comp.cardBackground};`);
|
|
422
|
+
|
|
423
|
+
// Timeline
|
|
424
|
+
if (comp.timelineNodeSize) cssVars.push(`--lumina-timeline-node-size: ${comp.timelineNodeSize};`);
|
|
425
|
+
if (comp.timelineLineWidth) cssVars.push(`--lumina-timeline-line-width: ${comp.timelineLineWidth};`);
|
|
426
|
+
if (comp.timelineNodeColor) cssVars.push(`--lumina-timeline-node-color: ${comp.timelineNodeColor};`);
|
|
427
|
+
if (comp.timelineLineColor) cssVars.push(`--lumina-timeline-line-color: ${comp.timelineLineColor};`);
|
|
428
|
+
|
|
429
|
+
// Steps
|
|
430
|
+
if (comp.stepBadgeSize) cssVars.push(`--lumina-step-badge-size: ${comp.stepBadgeSize};`);
|
|
431
|
+
if (comp.stepFontSize) cssVars.push(`--lumina-step-font-size: ${comp.stepFontSize};`);
|
|
432
|
+
|
|
433
|
+
// Progress
|
|
434
|
+
if (comp.progressHeight) cssVars.push(`--lumina-progress-height: ${comp.progressHeight};`);
|
|
435
|
+
if (comp.progressRadius) cssVars.push(`--lumina-progress-radius: ${comp.progressRadius};`);
|
|
436
|
+
if (comp.progressBackground) cssVars.push(`--lumina-progress-background: ${comp.progressBackground};`);
|
|
437
|
+
if (comp.progressFill) cssVars.push(`--lumina-progress-fill: ${comp.progressFill};`);
|
|
438
|
+
|
|
439
|
+
// Tags
|
|
440
|
+
if (comp.tagPadding) cssVars.push(`--lumina-tag-padding: ${comp.tagPadding};`);
|
|
441
|
+
if (comp.tagRadius) cssVars.push(`--lumina-tag-radius: ${comp.tagRadius};`);
|
|
442
|
+
if (comp.tagFontSize) cssVars.push(`--lumina-tag-font-size: ${comp.tagFontSize};`);
|
|
443
|
+
|
|
444
|
+
// Inputs
|
|
445
|
+
if (comp.inputRadius) cssVars.push(`--lumina-input-radius: ${comp.inputRadius};`);
|
|
446
|
+
if (comp.inputPadding) cssVars.push(`--lumina-input-padding: ${comp.inputPadding};`);
|
|
447
|
+
if (comp.inputBorder) cssVars.push(`--lumina-input-border: ${comp.inputBorder};`);
|
|
448
|
+
if (comp.inputFocusBorder) cssVars.push(`--lumina-input-focus-border: ${comp.inputFocusBorder};`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Legacy support: also generate old variable names for backwards compatibility
|
|
452
|
+
if (theme.colors?.primary) cssVars.push(`--lumina-colors-primary: ${theme.colors.primary};`);
|
|
453
|
+
if (theme.colors?.background) cssVars.push(`--lumina-colors-background: ${theme.colors.background};`);
|
|
454
|
+
if (theme.colors?.text) cssVars.push(`--lumina-colors-text: ${theme.colors.text};`);
|
|
455
|
+
if (theme.colors?.muted) cssVars.push(`--lumina-colors-muted: ${theme.colors.muted};`);
|
|
456
|
+
|
|
457
|
+
// --- Auto-Contrast (Intelligent text color calculation) ---
|
|
458
|
+
const autoContrastVars = this.generateAutoContrastVars(theme);
|
|
459
|
+
cssVars.push(...autoContrastVars);
|
|
460
|
+
|
|
461
|
+
return cssVars.join(' ');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Convert camelCase to kebab-case.
|
|
466
|
+
*/
|
|
467
|
+
private static camelToKebab(str: string): string {
|
|
468
|
+
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Convert gradient direction token to CSS value.
|
|
473
|
+
*/
|
|
474
|
+
private static gradientDirectionToCSS(dir: string): string {
|
|
475
|
+
const map: Record<string, string> = {
|
|
476
|
+
'to-t': 'to top',
|
|
477
|
+
'to-b': 'to bottom',
|
|
478
|
+
'to-l': 'to left',
|
|
479
|
+
'to-r': 'to right',
|
|
480
|
+
'to-tl': 'to top left',
|
|
481
|
+
'to-tr': 'to top right',
|
|
482
|
+
'to-bl': 'to bottom left',
|
|
483
|
+
'to-br': 'to bottom right',
|
|
484
|
+
};
|
|
485
|
+
return map[dir] || 'to bottom';
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Helper to parse a Hex color string into RGB components.
|
|
490
|
+
* Supports both #RGB and #RRGGBB formats.
|
|
491
|
+
*/
|
|
492
|
+
private static hexToRgb(hex: string): { r: number; g: number; b: number } | null {
|
|
493
|
+
// Handle shorthand hex (#RGB)
|
|
494
|
+
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
|
|
495
|
+
hex = hex.replace(shorthandRegex, (_, r, g, b) => r + r + g + g + b + b);
|
|
496
|
+
|
|
497
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
498
|
+
return result ? {
|
|
499
|
+
r: parseInt(result[1], 16),
|
|
500
|
+
g: parseInt(result[2], 16),
|
|
501
|
+
b: parseInt(result[3], 16)
|
|
502
|
+
} : null;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Calculate relative luminance of a color (WCAG formula).
|
|
507
|
+
* Returns value between 0 (black) and 1 (white).
|
|
508
|
+
*/
|
|
509
|
+
private static getLuminance(hex: string): number {
|
|
510
|
+
const rgb = this.hexToRgb(hex);
|
|
511
|
+
if (!rgb) return 0;
|
|
512
|
+
|
|
513
|
+
// Convert to sRGB
|
|
514
|
+
const toLinear = (c: number) => {
|
|
515
|
+
const sRGB = c / 255;
|
|
516
|
+
return sRGB <= 0.03928 ? sRGB / 12.92 : Math.pow((sRGB + 0.055) / 1.055, 2.4);
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const R = toLinear(rgb.r);
|
|
520
|
+
const G = toLinear(rgb.g);
|
|
521
|
+
const B = toLinear(rgb.b);
|
|
522
|
+
|
|
523
|
+
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Determine if a background color is "light" or "dark".
|
|
528
|
+
* Returns true if the color is light (should use dark text).
|
|
529
|
+
*/
|
|
530
|
+
private static isLightColor(hex: string): boolean {
|
|
531
|
+
return this.getLuminance(hex) > 0.4;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Get optimal contrasting text color for a given background.
|
|
536
|
+
* Returns dark or light text color based on background luminance.
|
|
537
|
+
*/
|
|
538
|
+
private static getContrastTextColor(bgHex: string, darkText = '#1a1a1a', lightText = '#ffffff'): string {
|
|
539
|
+
return this.isLightColor(bgHex) ? darkText : lightText;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Get optimal muted text color for a given background.
|
|
544
|
+
*/
|
|
545
|
+
private static getContrastMutedColor(bgHex: string): string {
|
|
546
|
+
return this.isLightColor(bgHex) ? '#4b5563' : '#9ca3af';
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Extract hex color from various formats (hex, rgb, rgba, gradient).
|
|
551
|
+
* Returns the first valid hex color found, or null.
|
|
552
|
+
*/
|
|
553
|
+
private static extractHexColor(color: string): string | null {
|
|
554
|
+
if (!color) return null;
|
|
555
|
+
|
|
556
|
+
// Handle direct hex
|
|
557
|
+
if (color.startsWith('#')) {
|
|
558
|
+
return color.split(' ')[0];
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Handle rgb/rgba
|
|
562
|
+
if (color.startsWith('rgb')) {
|
|
563
|
+
const values = color.match(/\d+/g);
|
|
564
|
+
if (values && values.length >= 3) {
|
|
565
|
+
const r = parseInt(values[0]);
|
|
566
|
+
const g = parseInt(values[1]);
|
|
567
|
+
const b = parseInt(values[2]);
|
|
568
|
+
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Try to find hex in string (for gradients)
|
|
573
|
+
const hexMatch = color.match(/#[a-fA-F0-9]{3,6}/);
|
|
574
|
+
if (hexMatch) return hexMatch[0];
|
|
575
|
+
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Generate auto-contrast CSS variables based on background color.
|
|
581
|
+
* These variables automatically adjust text colors for readability.
|
|
582
|
+
* CRITICAL: Also overrides the main color variables when necessary.
|
|
583
|
+
*/
|
|
584
|
+
private static generateAutoContrastVars(theme: ThemeConfig): string[] {
|
|
585
|
+
const cssVars: string[] = [];
|
|
586
|
+
|
|
587
|
+
// Get background color
|
|
588
|
+
const bgColor = theme.colors?.background;
|
|
589
|
+
if (!bgColor) return cssVars;
|
|
590
|
+
|
|
591
|
+
const bgHex = this.extractHexColor(bgColor);
|
|
592
|
+
if (!bgHex) return cssVars;
|
|
593
|
+
|
|
594
|
+
const isLight = this.isLightColor(bgHex);
|
|
595
|
+
|
|
596
|
+
// Auto text colors based on background
|
|
597
|
+
const autoText = isLight ? '#000000' : '#ffffff';
|
|
598
|
+
const autoMuted = isLight ? '#374151' : '#9ca3af'; // Darker mute for light mode (accessibility fix)
|
|
599
|
+
const autoTextSecondary = isLight ? '#374151' : '#d1d5db';
|
|
600
|
+
|
|
601
|
+
cssVars.push(`--lumina-auto-text: ${autoText};`);
|
|
602
|
+
cssVars.push(`--lumina-auto-text-secondary: ${autoTextSecondary};`);
|
|
603
|
+
cssVars.push(`--lumina-auto-muted: ${autoMuted};`);
|
|
604
|
+
cssVars.push(`--lumina-auto-is-light: ${isLight ? '1' : '0'};`);
|
|
605
|
+
|
|
606
|
+
// Check contrast and override when necessary
|
|
607
|
+
const userText = theme.colors?.text;
|
|
608
|
+
const textHex = userText ? this.extractHexColor(userText) : null;
|
|
609
|
+
|
|
610
|
+
if (textHex) {
|
|
611
|
+
const textLum = this.getLuminance(textHex);
|
|
612
|
+
const bgLum = this.getLuminance(bgHex);
|
|
613
|
+
const contrastRatio = (Math.max(textLum, bgLum) + 0.05) / (Math.min(textLum, bgLum) + 0.05);
|
|
614
|
+
|
|
615
|
+
// If contrast is poor, FORCE override the main variables
|
|
616
|
+
if (contrastRatio < 3) {
|
|
617
|
+
cssVars.push(`--lumina-color-text-safe: ${autoText};`);
|
|
618
|
+
cssVars.push(`--lumina-color-muted-safe: ${autoMuted};`);
|
|
619
|
+
// FORCE OVERRIDE the main variables
|
|
620
|
+
cssVars.push(`--lumina-color-text: ${autoText};`);
|
|
621
|
+
cssVars.push(`--lumina-color-muted: ${autoMuted};`);
|
|
622
|
+
cssVars.push(`--lumina-color-text-secondary: ${autoTextSecondary};`);
|
|
623
|
+
} else {
|
|
624
|
+
cssVars.push(`--lumina-color-text-safe: ${userText};`);
|
|
625
|
+
cssVars.push(`--lumina-color-muted-safe: ${theme.colors?.muted || autoMuted};`);
|
|
626
|
+
}
|
|
627
|
+
} else {
|
|
628
|
+
cssVars.push(`--lumina-color-text-safe: ${autoText};`);
|
|
629
|
+
cssVars.push(`--lumina-color-muted-safe: ${autoMuted};`);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// For LIGHT backgrounds, generate special glass panel colors
|
|
633
|
+
if (isLight) {
|
|
634
|
+
// Glass panels on light backgrounds need dark overlays
|
|
635
|
+
cssVars.push(`--lumina-color-text-rgb: 0, 0, 0;`);
|
|
636
|
+
cssVars.push(`--lumina-glass-opacity: 0.06;`);
|
|
637
|
+
cssVars.push(`--lumina-glass-border-opacity: 0.15;`);
|
|
638
|
+
// Surface should be white for readability
|
|
639
|
+
cssVars.push(`--lumina-color-surface: #ffffff;`);
|
|
640
|
+
cssVars.push(`--lumina-surface-text: #1a1a1a;`);
|
|
641
|
+
cssVars.push(`--lumina-surface-muted: #4b5563;`);
|
|
642
|
+
cssVars.push(`--lumina-shadow-color: rgba(0, 0, 0, 0.15);`);
|
|
643
|
+
// Critical Overrides for Light Mode (High Contrast for Accessibility)
|
|
644
|
+
cssVars.push(`--lumina-color-border: rgba(0, 0, 0, 0.12);`);
|
|
645
|
+
cssVars.push(`--lumina-color-border-subtle: rgba(0, 0, 0, 0.08);`);
|
|
646
|
+
// Muted text must be dark enough to be legible on light backgrounds (Global Fix)
|
|
647
|
+
cssVars.push(`--lumina-color-muted: #374151;`); // gray-700 instead of gray-500
|
|
648
|
+
cssVars.push(`--lumina-color-text-secondary: #1f2937;`); // gray-800
|
|
649
|
+
} else {
|
|
650
|
+
// Dark theme surface
|
|
651
|
+
const surfaceColor = theme.colors?.surface;
|
|
652
|
+
if (surfaceColor) {
|
|
653
|
+
const surfaceHex = this.extractHexColor(surfaceColor);
|
|
654
|
+
if (surfaceHex) {
|
|
655
|
+
const surfaceText = this.getContrastTextColor(surfaceHex);
|
|
656
|
+
const surfaceMuted = this.getContrastMutedColor(surfaceHex);
|
|
657
|
+
cssVars.push(`--lumina-surface-text: ${surfaceText};`);
|
|
658
|
+
cssVars.push(`--lumina-surface-muted: ${surfaceMuted};`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return cssVars;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|