@optilogic/core 1.5.0 → 1.6.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.
@@ -8,116 +8,130 @@
8
8
  import type { Theme } from "./types";
9
9
 
10
10
  /**
11
- * Optilogic Legacy Theme - Default light theme
11
+ * Optilogic Light Theme - Default light theme
12
12
  *
13
- * Classic Optilogic branding with deep blue (#0C0A5A) primary.
13
+ * The current Optilogic light theme, synced from platform-leapfrog. Indigo
14
+ * (#5766F2) primary on a soft lavender-white surface.
14
15
  */
15
- export const OPTILOGIC_LEGACY_THEME: Theme = {
16
- id: "optilogic-legacy",
17
- name: "Optilogic Legacy",
18
- description: "Classic Optilogic color scheme with blue accents",
16
+ export const OPTILOGIC_LIGHT_THEME: Theme = {
17
+ id: "optilogic-light",
18
+ name: "Optilogic Light",
19
+ description: "The default Optilogic light theme",
19
20
  author: "Optilogic",
20
- background: "#FFFFFF",
21
- foreground: "#0C0A5A",
22
- card: "#F9F9F9",
23
- cardForeground: "#0C0A5A",
24
- popover: "#FFFFFF",
25
- popoverForeground: "#0C0A5A",
26
- primary: "#0C0A5A",
27
- primaryForeground: "#FFFFFF",
28
- accent: "#CFD4FB",
21
+ background: "#F3F4FF",
22
+ foreground: "#000000",
23
+ card: "#EDEEFF",
24
+ cardForeground: "#000000",
25
+ popover: "#EDEEFF",
26
+ popoverForeground: "#000000",
27
+ primary: "#5766F2",
28
+ primaryForeground: "#000000",
29
+ primaryHover: "#4555E0",
30
+ accent: "#929BEF",
29
31
  accentForeground: "#0C0A5A",
30
- secondary: "#5766F2",
31
- secondaryForeground: "#FFFFFF",
32
- muted: "#E6EAF0",
33
- mutedForeground: "#737272",
34
- destructive: "#DB2828",
32
+ secondary: "#CFD4FB",
33
+ secondaryForeground: "#000000",
34
+ muted: "#E8ECFF",
35
+ mutedForeground: "#2E2D5E",
36
+ destructive: "#DC2626",
35
37
  destructiveForeground: "#FFFFFF",
36
- success: "#44BD7E",
37
- successForeground: "#FFFFFF",
38
- warning: "#F5CF47",
39
- warningForeground: "#0C0A5A",
40
- chip: "#B8C5F9",
41
- chipForeground: "#0C0A5A",
42
- border: "#D0D0D0",
43
- input: "#D0D0D0",
38
+ success: "#929BEF",
39
+ successForeground: "#0C0A5A",
40
+ warning: "#B45309",
41
+ warningForeground: "#FFFFFF",
42
+ chip: "#5766F2",
43
+ chipForeground: "#FFFFFF",
44
+ border: "#C8CEFF",
45
+ input: "#C8CEFF",
44
46
  ring: "#5766F2",
45
- popoverBorder: "#D0D0D0",
46
- divider: "#D0D0D0",
47
- toggleTrack: "#D0D0D0",
47
+ popoverBorder: "#C8CEFF",
48
+ divider: "#B8BEE0",
49
+ toggleTrack: "#929BEF",
48
50
  toggleTrackForeground: "#FFFFFF",
49
51
  inputHover: "#5766F2",
50
- chart1: "#78D237",
51
- chart2: "#F5CF47",
52
- chart3: "#5766F2",
53
- chart4: "#44BD7E",
54
- chart5: "#929BEF",
55
- chart6: "#DB2828",
56
- chart7: "#E07B39",
57
- chart8: "#2BBBAD",
58
- chart9: "#A5673F",
59
- chart10: "#00B5AD",
60
- chart11: "#6435C9",
61
- chart12: "#E03997",
52
+ chart1: "#5766F2",
53
+ chart2: "#929BEF",
54
+ chart3: "#CFD4FB",
55
+ chart4: "#0C0A5A",
56
+ chart5: "#B45309",
57
+ chart6: "#DC2626",
58
+ chart7: "#A855F7",
59
+ chart8: "#EC4899",
60
+ chart9: "#55548C",
61
+ chart10: "#0C0A5A",
62
+ chart11: "#C05621",
63
+ chart12: "#06B6D4",
62
64
  radius: "0.5rem",
65
+ brandGradientFrom: "#1A7B4A",
66
+ brandGradientTo: "#72DA60",
63
67
  };
64
68
 
65
69
  /**
66
- * Optilogic Dark Theme - Branded dark mode
70
+ * Optilogic Dark Theme - Default dark mode
67
71
  *
68
- * Uses Optilogic marketing site colors for a branded dark experience.
69
- * Primary palette: deep teal background with signature neon green accent.
72
+ * The current Optilogic dark theme, synced from platform-leapfrog. Indigo
73
+ * primary on a deep navy surface.
70
74
  */
71
75
  export const OPTILOGIC_DARK_THEME: Theme = {
72
76
  id: "optilogic-dark",
73
77
  name: "Optilogic Dark",
74
- description: "Optilogic branded dark theme with signature green",
78
+ description: "The default Optilogic dark theme",
75
79
  author: "Optilogic",
76
- background: "#042926",
77
- foreground: "#E0E7E6",
78
- card: "#0A3F34",
79
- cardForeground: "#E0E7E6",
80
- popover: "#0A3F34",
81
- popoverForeground: "#E0E7E6",
82
- primary: "#23EF6A",
83
- primaryForeground: "#042926",
84
- accent: "#23EF6A",
85
- accentForeground: "#042926",
86
- secondary: "#0D4D3F",
87
- secondaryForeground: "#E0E7E6",
88
- muted: "#0D4D3F",
89
- mutedForeground: "#9CB5B0",
80
+ background: "#141826",
81
+ foreground: "#E8EAF0",
82
+ card: "#2D3252",
83
+ cardForeground: "#E8EAF0",
84
+ popover: "#2D3252",
85
+ popoverForeground: "#E8EAF0",
86
+ primary: "#5766F2",
87
+ primaryForeground: "#FFFFFF",
88
+ primaryHover: "#7B8FF7",
89
+ accent: "#5766F2",
90
+ accentForeground: "#FFFFFF",
91
+ accentHover: "#7B8FF7",
92
+ secondary: "#2D3252",
93
+ secondaryForeground: "#E8EAF0",
94
+ muted: "#2D3252",
95
+ mutedForeground: "#C5C9D4",
90
96
  destructive: "#EF4444",
91
97
  destructiveForeground: "#FFFFFF",
92
- success: "#23EF6A",
93
- successForeground: "#042926",
94
- warning: "#FBBF24",
95
- warningForeground: "#042926",
96
- chip: "#0D4D3F",
97
- chipForeground: "#A7D5CA",
98
- border: "#1A5C4C",
99
- input: "#1A5C4C",
100
- ring: "#23EF6A",
101
- popoverBorder: "#237A64",
102
- divider: "#1A5C4C",
103
- toggleTrack: "#1A5C4C",
104
- toggleTrackForeground: "#E0E7E6",
105
- inputHover: "#23EF6A",
106
- chart1: "#23EF6A",
107
- chart2: "#3B82F6",
108
- chart3: "#FBBF24",
109
- chart4: "#F97316",
110
- chart5: "#A78BFA",
98
+ success: "#22C55E",
99
+ successForeground: "#141826",
100
+ warning: "#F59E0B",
101
+ warningForeground: "#141826",
102
+ chip: "#5766F2",
103
+ chipForeground: "#FFFFFF",
104
+ border: "#3A3F5C",
105
+ input: "#2D3252",
106
+ ring: "#7B8FF7",
107
+ popoverBorder: "#3A3F5C",
108
+ divider: "#363B5C",
109
+ toggleTrack: "#4A5068",
110
+ toggleTrackForeground: "#141826",
111
+ inputHover: "#7B8FF7",
112
+ chart1: "#7B8FF7",
113
+ chart2: "#5766F2",
114
+ chart3: "#22D3EE",
115
+ chart4: "#22C55E",
116
+ chart5: "#F59E0B",
111
117
  chart6: "#EF4444",
112
- chart7: "#06B6D4",
118
+ chart7: "#A855F7",
113
119
  chart8: "#EC4899",
114
- chart9: "#84CC16",
120
+ chart9: "#C5C9D4",
115
121
  chart10: "#14B8A6",
116
- chart11: "#F59E0B",
117
- chart12: "#8B5CF6",
122
+ chart11: "#FB923C",
123
+ chart12: "#60A5FA",
118
124
  radius: "0.5rem",
125
+ brandGradientFrom: "#1A7B4A",
126
+ brandGradientTo: "#50FFA7",
119
127
  };
120
128
 
129
+ /**
130
+ * @deprecated Use OPTILOGIC_LIGHT_THEME. Retained so existing imports resolve;
131
+ * now points at the current Optilogic Light theme.
132
+ */
133
+ export const OPTILOGIC_LEGACY_THEME = OPTILOGIC_LIGHT_THEME;
134
+
121
135
  /**
122
136
  * Modern Light Theme - Clean, readable light mode
123
137
  *
@@ -289,14 +303,64 @@ export const DARK_ELEGANT_THEME: Theme = {
289
303
  };
290
304
 
291
305
  /**
292
- * All available preset themes
306
+ * Green Theme - Natural, earthy greens
307
+ *
308
+ * Synced from platform-leapfrog. Soft green primary on deep forest surfaces.
309
+ */
310
+ export const GREEN_THEME: Theme = {
311
+ id: "green-theme",
312
+ name: "Green Theme",
313
+ description: "A green theme with natural, earthy tones",
314
+ author: "Optilogic",
315
+ background: "#1a2820",
316
+ foreground: "#E6F5EC",
317
+ card: "#2d4038",
318
+ cardForeground: "#E6F5EC",
319
+ popover: "#243630",
320
+ popoverForeground: "#E6F5EC",
321
+ primary: "#6FCF97",
322
+ primaryForeground: "#1a2820",
323
+ accent: "#6FCF97",
324
+ accentForeground: "#1a2820",
325
+ secondary: "#1f3329",
326
+ secondaryForeground: "#E6F5EC",
327
+ muted: "#354840",
328
+ mutedForeground: "#9DB8A8",
329
+ destructive: "#EB5757",
330
+ destructiveForeground: "#E6F5EC",
331
+ success: "#6FCF97",
332
+ successForeground: "#1a2820",
333
+ warning: "#F2C94C",
334
+ warningForeground: "#1a2820",
335
+ chip: "#3a5045",
336
+ chipForeground: "#C5E3D1",
337
+ border: "#243630",
338
+ input: "#243630",
339
+ ring: "#6FCF97",
340
+ chart1: "#6FCF97",
341
+ chart2: "#8FE3B0",
342
+ chart3: "#9DB8A8",
343
+ chart4: "#2d4038",
344
+ chart5: "#354840",
345
+ chart6: "#EB5757",
346
+ chart7: "#F2C94C",
347
+ chart8: "#56CCF2",
348
+ chart9: "#BB6BD9",
349
+ chart10: "#4ECDC4",
350
+ chart11: "#A8D8B9",
351
+ chart12: "#E07B39",
352
+ radius: "0.5rem",
353
+ };
354
+
355
+ /**
356
+ * All available preset themes (shown in the theme picker).
293
357
  */
294
358
  export const PRESET_THEMES: Theme[] = [
295
- OPTILOGIC_LEGACY_THEME,
359
+ OPTILOGIC_LIGHT_THEME,
296
360
  OPTILOGIC_DARK_THEME,
297
361
  MODERN_LIGHT_THEME,
298
362
  MODERN_DARK_THEME,
299
- DARK_ELEGANT_THEME,
363
+ GREEN_THEME,
300
364
  ];
301
365
 
302
366
  /**
@@ -305,22 +369,33 @@ export const PRESET_THEMES: Theme[] = [
305
369
  export const ALL_THEMES: Theme[] = PRESET_THEMES;
306
370
 
307
371
  /**
308
- * Get a preset theme by ID
372
+ * Retired preset IDs mapped to their current replacement, so themes saved
373
+ * under an old ID (e.g. in localStorage) resolve to a shipping preset.
374
+ */
375
+ export const LEGACY_THEME_ID_MAP: Record<string, string> = {
376
+ "optilogic-legacy": OPTILOGIC_LIGHT_THEME.id,
377
+ "dark-elegant": OPTILOGIC_DARK_THEME.id,
378
+ };
379
+
380
+ /**
381
+ * Get a preset theme by ID, transparently resolving retired IDs.
309
382
  */
310
383
  export function getPresetTheme(id: string): Theme | undefined {
311
- return ALL_THEMES.find((theme) => theme.id === id);
384
+ const resolvedId = LEGACY_THEME_ID_MAP[id] ?? id;
385
+ return ALL_THEMES.find((theme) => theme.id === resolvedId);
312
386
  }
313
387
 
314
388
  /**
315
389
  * Get the default theme (fallback)
316
390
  */
317
391
  export function getDefaultTheme(): Theme {
318
- return OPTILOGIC_LEGACY_THEME;
392
+ return OPTILOGIC_LIGHT_THEME;
319
393
  }
320
394
 
321
395
  /**
322
396
  * Check if a theme is a preset (not user-created)
323
397
  */
324
398
  export function isPresetTheme(themeId: string): boolean {
325
- return ALL_THEMES.some((theme) => theme.id === themeId);
399
+ const resolvedId = LEGACY_THEME_ID_MAP[themeId] ?? themeId;
400
+ return ALL_THEMES.some((theme) => theme.id === resolvedId);
326
401
  }
@@ -54,6 +54,14 @@ export interface Theme {
54
54
  input: string; // --input
55
55
  ring: string; // --ring (focus ring)
56
56
 
57
+ /** Hover states (optional, with fallbacks) */
58
+ primaryHover?: string; // --primary-hover (primary control hover; fallback: primary)
59
+ accentHover?: string; // --accent-hover (accent surface hover; fallback: derived from card+accent)
60
+
61
+ /** Brand identity gradient (optional, with fallbacks) */
62
+ brandGradientFrom?: string; // --brand-from (fallback: primary)
63
+ brandGradientTo?: string; // --brand-to (fallback: chart-2)
64
+
57
65
  /** Elevated surface border (optional, with fallback) */
58
66
  popoverBorder?: string; // --popover-border (floating surface border; fallback: border)
59
67
 
@@ -110,6 +110,28 @@ export function themeToHsl(theme: Theme): ThemeHSL {
110
110
  };
111
111
  }
112
112
 
113
+ /**
114
+ * Blend two hex colors by `amount` (0..1). Used to derive --accent-hover and
115
+ * --accent-active when a theme doesn't specify them explicitly, so theme
116
+ * switches always refresh those vars from the current card+accent pair.
117
+ */
118
+ function blendHex(base: string, blend: string, amount: number): string {
119
+ const a = base.replace(/^#/, "");
120
+ const b = blend.replace(/^#/, "");
121
+ const ar = parseInt(a.substring(0, 2), 16);
122
+ const ag = parseInt(a.substring(2, 4), 16);
123
+ const ab = parseInt(a.substring(4, 6), 16);
124
+ const br = parseInt(b.substring(0, 2), 16);
125
+ const bg = parseInt(b.substring(2, 4), 16);
126
+ const bb = parseInt(b.substring(4, 6), 16);
127
+ const r = Math.round(ar + (br - ar) * amount);
128
+ const g = Math.round(ag + (bg - ag) * amount);
129
+ const bl = Math.round(ab + (bb - ab) * amount);
130
+ const hex = (v: number) =>
131
+ Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0");
132
+ return "#" + hex(r) + hex(g) + hex(bl);
133
+ }
134
+
113
135
  /**
114
136
  * Derive an input hover border color from the theme's HSL values.
115
137
  * Blends toward the foreground for a subtle but visible hover effect.
@@ -219,6 +241,37 @@ export function applyTheme(theme: Theme, targetElement?: HTMLElement): void {
219
241
  `${hslTheme.hover ?? deriveHoverChannels(theme, hslTheme)} / 0.18`
220
242
  );
221
243
 
244
+ // Hover-state tokens (optional, with fallbacks).
245
+ // --primary-hover: explicit value, else falls back to --primary via styles.css.
246
+ if (theme.primaryHover) {
247
+ element.style.setProperty("--primary-hover", hexToHsl(theme.primaryHover));
248
+ } else {
249
+ element.style.removeProperty("--primary-hover");
250
+ }
251
+ // --accent-hover / --accent-active: always re-derived from the current
252
+ // card+accent (explicit accentHover wins) so theme switches refresh them.
253
+ if (theme.card && theme.accent) {
254
+ const accentHover = theme.accentHover
255
+ ? theme.accentHover
256
+ : blendHex(theme.card, theme.accent, 0.08);
257
+ const accentActive = blendHex(theme.card, theme.accent, 0.15);
258
+ element.style.setProperty("--accent-hover", hexToHsl(accentHover));
259
+ element.style.setProperty("--accent-active", hexToHsl(accentActive));
260
+ }
261
+
262
+ // Brand identity gradient (optional). When unset, removeProperty lets the
263
+ // styles.css :root fallback (primary → chart-2) take over.
264
+ if (theme.brandGradientFrom) {
265
+ element.style.setProperty("--brand-from", hexToHsl(theme.brandGradientFrom));
266
+ } else {
267
+ element.style.removeProperty("--brand-from");
268
+ }
269
+ if (theme.brandGradientTo) {
270
+ element.style.setProperty("--brand-to", hexToHsl(theme.brandGradientTo));
271
+ } else {
272
+ element.style.removeProperty("--brand-to");
273
+ }
274
+
222
275
  element.style.setProperty("--chart-1", hslTheme.chart1);
223
276
  element.style.setProperty("--chart-2", hslTheme.chart2);
224
277
  element.style.setProperty("--chart-3", hslTheme.chart3);
@@ -362,6 +415,10 @@ export function areThemesEqual(theme1: Theme, theme2: Theme): boolean {
362
415
  "toggleTrackForeground",
363
416
  "inputHover",
364
417
  "hover",
418
+ "primaryHover",
419
+ "accentHover",
420
+ "brandGradientFrom",
421
+ "brandGradientTo",
365
422
  "chart1",
366
423
  "chart2",
367
424
  "chart3",