@portel/photon-core 2.8.0 → 2.8.2

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.
Files changed (66) hide show
  1. package/dist/base.js +1 -1
  2. package/dist/base.js.map +1 -1
  3. package/dist/channels/registry.js +7 -7
  4. package/dist/channels/registry.js.map +1 -1
  5. package/dist/collections/Collection.js +1 -1
  6. package/dist/collections/Collection.js.map +1 -1
  7. package/dist/collections/ReactiveArray.d.ts +7 -2
  8. package/dist/collections/ReactiveArray.d.ts.map +1 -1
  9. package/dist/collections/ReactiveArray.js +21 -0
  10. package/dist/collections/ReactiveArray.js.map +1 -1
  11. package/dist/decorators.js +1 -1
  12. package/dist/decorators.js.map +1 -1
  13. package/dist/design-system/index.js +1 -1
  14. package/dist/design-system/index.js.map +1 -1
  15. package/dist/design-system/oklch.d.ts +119 -0
  16. package/dist/design-system/oklch.d.ts.map +1 -0
  17. package/dist/design-system/oklch.js +354 -0
  18. package/dist/design-system/oklch.js.map +1 -0
  19. package/dist/design-system/tokens.d.ts +5 -4
  20. package/dist/design-system/tokens.d.ts.map +1 -1
  21. package/dist/design-system/tokens.js +16 -14
  22. package/dist/design-system/tokens.js.map +1 -1
  23. package/dist/generator.d.ts +0 -48
  24. package/dist/generator.d.ts.map +1 -1
  25. package/dist/generator.js +0 -26
  26. package/dist/generator.js.map +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +1 -3
  30. package/dist/index.js.map +1 -1
  31. package/dist/mcp-apps.d.ts.map +1 -1
  32. package/dist/mcp-apps.js +1 -2
  33. package/dist/mcp-apps.js.map +1 -1
  34. package/dist/memory.d.ts +4 -1
  35. package/dist/memory.d.ts.map +1 -1
  36. package/dist/memory.js +49 -25
  37. package/dist/memory.js.map +1 -1
  38. package/dist/rendering/layout-selector.js +2 -2
  39. package/dist/rendering/layout-selector.js.map +1 -1
  40. package/dist/schema-extractor.d.ts.map +1 -1
  41. package/dist/schema-extractor.js +13 -1
  42. package/dist/schema-extractor.js.map +1 -1
  43. package/dist/stateful.d.ts +1 -2
  44. package/dist/stateful.d.ts.map +1 -1
  45. package/dist/stateful.js +30 -13
  46. package/dist/stateful.js.map +1 -1
  47. package/dist/types.d.ts +7 -3
  48. package/dist/types.d.ts.map +1 -1
  49. package/dist/types.js.map +1 -1
  50. package/package.json +1 -1
  51. package/src/base.ts +1 -1
  52. package/src/channels/registry.ts +7 -7
  53. package/src/collections/Collection.ts +4 -4
  54. package/src/collections/ReactiveArray.ts +24 -2
  55. package/src/decorators.ts +1 -1
  56. package/src/design-system/index.ts +1 -1
  57. package/src/design-system/oklch.ts +493 -0
  58. package/src/design-system/tokens.ts +17 -14
  59. package/src/generator.ts +0 -69
  60. package/src/index.ts +0 -14
  61. package/src/mcp-apps.ts +2 -3
  62. package/src/memory.ts +43 -24
  63. package/src/rendering/layout-selector.ts +5 -5
  64. package/src/schema-extractor.ts +16 -1
  65. package/src/stateful.ts +28 -13
  66. package/src/types.ts +9 -3
@@ -28,8 +28,8 @@
28
28
  export type Emitter = (event: string, data: unknown) => void;
29
29
 
30
30
  export class ReactiveArray<T> extends Array<T> {
31
- private _propertyName: string = '';
32
- private _emitter: Emitter = () => {};
31
+ protected _propertyName: string = '';
32
+ protected _emitter: Emitter = () => {};
33
33
 
34
34
  /**
35
35
  * Create a new ReactiveArray bound to a property name and emitter function.
@@ -50,11 +50,28 @@ export class ReactiveArray<T> extends Array<T> {
50
50
  return arr;
51
51
  }
52
52
 
53
+ /**
54
+ * Auto-stamp plain objects with `_addedAt` timestamp on add operations.
55
+ * Only stamps if no recognized timestamp field already exists.
56
+ */
57
+ private _stampAdded(items: T[]): void {
58
+ const now = Date.now();
59
+ for (const item of items) {
60
+ if (item && typeof item === 'object' && !Array.isArray(item)) {
61
+ const rec = item as Record<string, unknown>;
62
+ if (rec._addedAt === undefined && rec.createdAt === undefined && rec.created_at === undefined) {
63
+ rec._addedAt = now;
64
+ }
65
+ }
66
+ }
67
+ }
68
+
53
69
  /**
54
70
  * Add one or more elements to the end of the array.
55
71
  * Emits `{prop}:added` for each item.
56
72
  */
57
73
  push(...items: T[]): number {
74
+ this._stampAdded(items);
58
75
  const result = super.push(...items);
59
76
  items.forEach((item) => this._emitter(`${this._propertyName}:added`, item));
60
77
  return result;
@@ -89,6 +106,7 @@ export class ReactiveArray<T> extends Array<T> {
89
106
  * Emits `{prop}:added` for each item.
90
107
  */
91
108
  unshift(...items: T[]): number {
109
+ this._stampAdded(items);
92
110
  const result = super.unshift(...items);
93
111
  items.forEach((item) => this._emitter(`${this._propertyName}:added`, item));
94
112
  return result;
@@ -99,6 +117,7 @@ export class ReactiveArray<T> extends Array<T> {
99
117
  * Emits `{prop}:removed` for deleted items and `{prop}:added` for inserted items.
100
118
  */
101
119
  splice(start: number, deleteCount?: number, ...items: T[]): T[] {
120
+ this._stampAdded(items);
102
121
  const removed = super.splice(start, deleteCount ?? 0, ...items);
103
122
  removed.forEach((item) =>
104
123
  this._emitter(`${this._propertyName}:removed`, item)
@@ -112,6 +131,9 @@ export class ReactiveArray<T> extends Array<T> {
112
131
  * Emits `{prop}:updated` with `{ index, value, previous }`.
113
132
  */
114
133
  set(index: number, value: T): void {
134
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
135
+ (value as Record<string, unknown>)._updatedAt = Date.now();
136
+ }
115
137
  const previous = this[index];
116
138
  this[index] = value;
117
139
  this._emitter(`${this._propertyName}:updated`, { index, value, previous });
package/src/decorators.ts CHANGED
@@ -70,7 +70,7 @@ export async function withLock<T>(
70
70
  const lockManager = getLockManager();
71
71
 
72
72
  if (!lockManager) {
73
- // No lock manager, run without lock
73
+ console.warn(`[photon] withLock('${lockName}'): no lock manager configured, running without lock`);
74
74
  return fn();
75
75
  }
76
76
 
@@ -9,7 +9,7 @@
9
9
  * - Apple HIG: https://developer.apple.com/design/human-interface-guidelines/layout
10
10
  */
11
11
 
12
- export * from './tokens.js';
12
+ export * from './tokens.js'; // includes oklch.js re-exports
13
13
  export * from './transaction-ui.js';
14
14
 
15
15
  import { generateTokensCSS } from './tokens.js';
@@ -0,0 +1,493 @@
1
+ /**
2
+ * OKLCH Color Engine
3
+ *
4
+ * Pure-math OKLCH → sRGB conversion for perceptually uniform theme generation.
5
+ * No external dependencies.
6
+ *
7
+ * References:
8
+ * - Björn Ottosson's Oklab: https://bottosson.github.io/posts/oklab/
9
+ * - CSS Color Level 4: https://www.w3.org/TR/css-color-4/#ok-lab
10
+ */
11
+
12
+ // =============================================================================
13
+ // TYPES
14
+ // =============================================================================
15
+
16
+ export interface ThemeConfig {
17
+ /** Color hue: 0-360 */
18
+ hue: number;
19
+ /** Color saturation/vibrancy: 0-0.4 */
20
+ chroma: number;
21
+ /** Base lightness: 0-1 */
22
+ lightness: number;
23
+ /** Dark or light mode */
24
+ theme: 'dark' | 'light';
25
+ }
26
+
27
+ export interface ThemePreset {
28
+ name: string;
29
+ config: Omit<ThemeConfig, 'theme'>;
30
+ }
31
+
32
+ /** All generated theme colors as hex strings */
33
+ export interface GeneratedThemeColors {
34
+ // Surfaces
35
+ surface: string;
36
+ surfaceContainer: string;
37
+ surfaceContainerHigh: string;
38
+ surfaceContainerHighest: string;
39
+ surfaceBright: string;
40
+
41
+ // Text
42
+ onSurface: string;
43
+ onSurfaceVariant: string;
44
+ onSurfaceMuted: string;
45
+
46
+ // Primary
47
+ primary: string;
48
+ primaryContainer: string;
49
+ onPrimary: string;
50
+ onPrimaryContainer: string;
51
+
52
+ // Status - derived from hue offsets
53
+ success: string;
54
+ successContainer: string;
55
+ onSuccess: string;
56
+ onSuccessContainer: string;
57
+
58
+ warning: string;
59
+ warningContainer: string;
60
+ onWarning: string;
61
+ onWarningContainer: string;
62
+
63
+ error: string;
64
+ errorContainer: string;
65
+ onError: string;
66
+ onErrorContainer: string;
67
+
68
+ // Outline
69
+ outline: string;
70
+ outlineVariant: string;
71
+
72
+ // Scrim
73
+ scrim: string;
74
+ }
75
+
76
+ /** Beam-specific CSS variables generated from OKLCH */
77
+ export interface BeamThemeColors {
78
+ bgApp: string;
79
+ bgGlass: string;
80
+ bgGlassStrong: string;
81
+ bgPanel: string;
82
+ tPrimary: string;
83
+ tMuted: string;
84
+ borderGlass: string;
85
+ accentPrimary: string;
86
+ accentSecondary: string;
87
+ glowPrimary: string;
88
+ // Status
89
+ colorError: string;
90
+ colorErrorGlow: string;
91
+ colorErrorBg: string;
92
+ colorSuccess: string;
93
+ colorSuccessBg: string;
94
+ colorWarning: string;
95
+ colorWarningBg: string;
96
+ colorWarningGlow: string;
97
+ colorInfo: string;
98
+ // CLI preview
99
+ cliBg: string;
100
+ cliBorder: string;
101
+ cliText: string;
102
+ cliMuted: string;
103
+ cliHoverBg: string;
104
+ // Syntax highlighting
105
+ syntaxComment: string;
106
+ syntaxKeyword: string;
107
+ syntaxString: string;
108
+ syntaxNumber: string;
109
+ syntaxFunction: string;
110
+ syntaxOperator: string;
111
+ syntaxVariable: string;
112
+ syntaxPunctuation: string;
113
+ }
114
+
115
+ // =============================================================================
116
+ // OKLCH → sRGB CONVERSION
117
+ // =============================================================================
118
+
119
+ /**
120
+ * Convert OKLCH to linear sRGB.
121
+ * L: 0-1 (lightness), C: 0-0.4+ (chroma), H: 0-360 (hue in degrees)
122
+ */
123
+ export function oklchToSRGB(L: number, C: number, H: number): [number, number, number] {
124
+ // Convert hue to radians
125
+ const hRad = (H * Math.PI) / 180;
126
+
127
+ // OKLCH → OKLab
128
+ const a = C * Math.cos(hRad);
129
+ const b = C * Math.sin(hRad);
130
+
131
+ // OKLab → linear LMS (via inverse of the matrix)
132
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
133
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
134
+ const s_ = L - 0.0894841775 * a - 1.2914855480 * b;
135
+
136
+ // Cube the values (inverse of cube root)
137
+ const l = l_ * l_ * l_;
138
+ const m = m_ * m_ * m_;
139
+ const s = s_ * s_ * s_;
140
+
141
+ // Linear LMS → linear sRGB
142
+ const r = +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
143
+ const g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
144
+ const bv = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s;
145
+
146
+ // Apply sRGB gamma (linear → sRGB)
147
+ return [gammaEncode(r), gammaEncode(g), gammaEncode(bv)];
148
+ }
149
+
150
+ /** sRGB gamma encoding (linear → sRGB) */
151
+ function gammaEncode(c: number): number {
152
+ if (c <= 0.0031308) {
153
+ return 12.92 * c;
154
+ }
155
+ return 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
156
+ }
157
+
158
+ /** Clamp a value to [0, 1] */
159
+ function clamp01(v: number): number {
160
+ return Math.max(0, Math.min(1, v));
161
+ }
162
+
163
+ /**
164
+ * Convert sRGB [0-1] to hex color string.
165
+ */
166
+ export function srgbToHex(r: number, g: number, b: number): string {
167
+ const toHex = (c: number) => {
168
+ const v = Math.round(clamp01(c) * 255);
169
+ return v.toString(16).padStart(2, '0');
170
+ };
171
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
172
+ }
173
+
174
+ /**
175
+ * Convert OKLCH directly to hex color string.
176
+ * Gamut-clamps out-of-range values.
177
+ */
178
+ export function oklchToHex(L: number, C: number, H: number): string {
179
+ const [r, g, b] = oklchToSRGB(L, C, H);
180
+ return srgbToHex(r, g, b);
181
+ }
182
+
183
+ /**
184
+ * Convert OKLCH to rgba() string with alpha.
185
+ */
186
+ function oklchToRgba(L: number, C: number, H: number, alpha: number): string {
187
+ const [r, g, b] = oklchToSRGB(L, C, H);
188
+ return `rgba(${Math.round(clamp01(r) * 255)}, ${Math.round(clamp01(g) * 255)}, ${Math.round(clamp01(b) * 255)}, ${alpha})`;
189
+ }
190
+
191
+ // =============================================================================
192
+ // THEME PRESETS
193
+ // =============================================================================
194
+
195
+ export const themePresets: ThemePreset[] = [
196
+ { name: 'Default Violet', config: { hue: 260, chroma: 0.15, lightness: 0.65 } },
197
+ { name: 'Ocean Blue', config: { hue: 220, chroma: 0.12, lightness: 0.60 } },
198
+ { name: 'Emerald', config: { hue: 155, chroma: 0.14, lightness: 0.60 } },
199
+ { name: 'Amber', config: { hue: 75, chroma: 0.14, lightness: 0.70 } },
200
+ { name: 'Rose', config: { hue: 350, chroma: 0.14, lightness: 0.62 } },
201
+ { name: 'Monochrome', config: { hue: 0, chroma: 0.0, lightness: 0.65 } },
202
+ ];
203
+
204
+ // =============================================================================
205
+ // THEME COLOR GENERATION
206
+ // =============================================================================
207
+
208
+ /**
209
+ * Generate Material Design 3-compatible theme colors from OKLCH parameters.
210
+ */
211
+ export function generateThemeColors(config: ThemeConfig): GeneratedThemeColors {
212
+ const { hue: H, chroma: C, lightness: L, theme } = config;
213
+ const isDark = theme === 'dark';
214
+
215
+ // --- Surface colors ---
216
+ // Dark: very low lightness, minimal chroma for subtle tinting
217
+ // Light: very high lightness, minimal chroma
218
+ const surface = isDark
219
+ ? oklchToHex(0.15, C * 0.03, H)
220
+ : oklchToHex(0.97, C * 0.02, H);
221
+ const surfaceContainer = isDark
222
+ ? oklchToHex(0.19, C * 0.04, H)
223
+ : oklchToHex(0.94, C * 0.025, H);
224
+ const surfaceContainerHigh = isDark
225
+ ? oklchToHex(0.23, C * 0.05, H)
226
+ : oklchToHex(0.91, C * 0.03, H);
227
+ const surfaceContainerHighest = isDark
228
+ ? oklchToHex(0.27, C * 0.06, H)
229
+ : oklchToHex(0.88, C * 0.035, H);
230
+ const surfaceBright = isDark
231
+ ? oklchToHex(0.95, C * 0.01, H)
232
+ : oklchToHex(1.0, 0, H);
233
+
234
+ // --- Text colors ---
235
+ const onSurface = isDark
236
+ ? oklchToHex(0.93, C * 0.02, H)
237
+ : oklchToHex(0.22, C * 0.02, H);
238
+ const onSurfaceVariant = isDark
239
+ ? oklchToHex(0.75, C * 0.04, H)
240
+ : oklchToHex(0.38, C * 0.04, H);
241
+ const onSurfaceMuted = isDark
242
+ ? oklchToHex(0.55, C * 0.05, H)
243
+ : oklchToHex(0.50, C * 0.05, H);
244
+
245
+ // --- Primary accent ---
246
+ const primary = isDark
247
+ ? oklchToHex(L, C, H)
248
+ : oklchToHex(L * 0.7, C, H);
249
+ const primaryContainer = isDark
250
+ ? oklchToHex(0.30, C * 0.5, H)
251
+ : oklchToHex(0.90, C * 0.3, H);
252
+ const onPrimary = isDark
253
+ ? oklchToHex(0.15, C * 0.1, H)
254
+ : oklchToHex(0.98, C * 0.01, H);
255
+ const onPrimaryContainer = isDark
256
+ ? oklchToHex(0.90, C * 0.2, H)
257
+ : oklchToHex(0.20, C * 0.15, H);
258
+
259
+ // --- Status colors (hue offsets from base) ---
260
+ // Success: green tones
261
+ const successH = (H + 145) % 360;
262
+ const success = isDark
263
+ ? oklchToHex(0.70, 0.15, successH)
264
+ : oklchToHex(0.45, 0.15, successH);
265
+ const successContainer = isDark
266
+ ? oklchToHex(0.25, 0.06, successH)
267
+ : oklchToHex(0.92, 0.06, successH);
268
+ const onSuccess = isDark
269
+ ? oklchToHex(0.15, 0.05, successH)
270
+ : oklchToHex(0.98, 0.01, successH);
271
+ const onSuccessContainer = isDark
272
+ ? oklchToHex(0.90, 0.08, successH)
273
+ : oklchToHex(0.20, 0.08, successH);
274
+
275
+ // Warning: amber/yellow tones
276
+ const warningH = (H + 100) % 360;
277
+ const warning = isDark
278
+ ? oklchToHex(0.75, 0.15, warningH)
279
+ : oklchToHex(0.50, 0.15, warningH);
280
+ const warningContainer = isDark
281
+ ? oklchToHex(0.25, 0.06, warningH)
282
+ : oklchToHex(0.92, 0.06, warningH);
283
+ const onWarning = isDark
284
+ ? oklchToHex(0.15, 0.05, warningH)
285
+ : oklchToHex(0.98, 0.01, warningH);
286
+ const onWarningContainer = isDark
287
+ ? oklchToHex(0.90, 0.08, warningH)
288
+ : oklchToHex(0.20, 0.08, warningH);
289
+
290
+ // Error: red tones
291
+ const errorH = (H + 210) % 360;
292
+ const error = isDark
293
+ ? oklchToHex(0.65, 0.18, errorH)
294
+ : oklchToHex(0.48, 0.18, errorH);
295
+ const errorContainer = isDark
296
+ ? oklchToHex(0.25, 0.07, errorH)
297
+ : oklchToHex(0.92, 0.07, errorH);
298
+ const onError = isDark
299
+ ? oklchToHex(0.15, 0.05, errorH)
300
+ : oklchToHex(0.98, 0.01, errorH);
301
+ const onErrorContainer = isDark
302
+ ? oklchToHex(0.90, 0.08, errorH)
303
+ : oklchToHex(0.20, 0.08, errorH);
304
+
305
+ // --- Outline ---
306
+ const outline = isDark
307
+ ? oklchToHex(0.40, C * 0.06, H)
308
+ : oklchToHex(0.65, C * 0.06, H);
309
+ const outlineVariant = isDark
310
+ ? oklchToHex(0.28, C * 0.04, H)
311
+ : oklchToHex(0.82, C * 0.04, H);
312
+
313
+ // --- Scrim ---
314
+ const scrim = isDark ? 'rgba(0, 0, 0, 0.5)' : 'rgba(0, 0, 0, 0.2)';
315
+
316
+ return {
317
+ surface, surfaceContainer, surfaceContainerHigh, surfaceContainerHighest, surfaceBright,
318
+ onSurface, onSurfaceVariant, onSurfaceMuted,
319
+ primary, primaryContainer, onPrimary, onPrimaryContainer,
320
+ success, successContainer, onSuccess, onSuccessContainer,
321
+ warning, warningContainer, onWarning, onWarningContainer,
322
+ error, errorContainer, onError, onErrorContainer,
323
+ outline, outlineVariant,
324
+ scrim,
325
+ };
326
+ }
327
+
328
+ /**
329
+ * Generate Beam-specific CSS variable values from OKLCH parameters.
330
+ * These map to the --bg-app, --accent-primary, etc. variables used in beam-app.ts.
331
+ */
332
+ export function generateBeamThemeColors(config: ThemeConfig): BeamThemeColors {
333
+ const { hue: H, chroma: C, lightness: L, theme } = config;
334
+ const isDark = theme === 'dark';
335
+
336
+ // Secondary accent: shifted hue, reduced chroma
337
+ const secondaryH = (H + 70) % 360;
338
+
339
+ // === Surfaces ===
340
+ const bgApp = isDark
341
+ ? oklchToHex(0.15, C * 0.04, H)
342
+ : oklchToHex(0.92, C * 0.03, H);
343
+ const bgGlass = isDark
344
+ ? oklchToRgba(0.19, C * 0.04, H, 0.6)
345
+ : oklchToRgba(0.98, C * 0.02, H, 0.75);
346
+ const bgGlassStrong = isDark
347
+ ? oklchToRgba(0.19, C * 0.04, H, 0.85)
348
+ : oklchToRgba(0.98, C * 0.02, H, 0.9);
349
+ const bgPanel = isDark
350
+ ? oklchToHex(0.18, C * 0.05, H)
351
+ : oklchToHex(0.96, C * 0.02, H);
352
+
353
+ // === Text ===
354
+ const tPrimary = isDark
355
+ ? oklchToHex(0.93, C * 0.02, H)
356
+ : oklchToHex(0.22, C * 0.03, H);
357
+ const tMuted = isDark
358
+ ? oklchToHex(0.58, C * 0.04, H)
359
+ : oklchToHex(0.48, C * 0.04, H);
360
+
361
+ // === Borders ===
362
+ const borderGlass = isDark
363
+ ? oklchToRgba(0.75, C * 0.03, H, 0.1)
364
+ : oklchToRgba(0.40, C * 0.05, H, 0.12);
365
+
366
+ // === Accents ===
367
+ const accentPrimary = isDark
368
+ ? oklchToHex(L, C, H)
369
+ : oklchToHex(L * 0.72, C * 0.9, H);
370
+ const accentSecondary = isDark
371
+ ? oklchToHex(L * 0.9, C * 0.8, secondaryH)
372
+ : oklchToHex(L * 0.6, C * 0.7, secondaryH);
373
+ const glowPrimary = isDark
374
+ ? oklchToRgba(L, C, H, 0.3)
375
+ : oklchToRgba(L * 0.72, C * 0.9, H, 0.15);
376
+
377
+ // === Status Colors ===
378
+ const successH = (H + 145) % 360;
379
+ const warningH = (H + 100) % 360;
380
+ const errorH = (H + 210) % 360;
381
+
382
+ const colorError = isDark
383
+ ? oklchToHex(0.68, 0.18, errorH)
384
+ : oklchToHex(0.50, 0.16, errorH);
385
+ const colorErrorGlow = oklchToRgba(0.68, 0.18, errorH, isDark ? 0.3 : 0.2);
386
+ const colorErrorBg = oklchToRgba(0.68, 0.18, errorH, isDark ? 0.1 : 0.08);
387
+ const colorSuccess = isDark
388
+ ? oklchToHex(0.72, 0.16, successH)
389
+ : oklchToHex(0.48, 0.14, successH);
390
+ const colorSuccessBg = oklchToRgba(0.55, 0.12, successH, isDark ? 0.2 : 0.12);
391
+ const colorWarning = isDark
392
+ ? oklchToHex(0.78, 0.15, warningH)
393
+ : oklchToHex(0.55, 0.14, warningH);
394
+ const colorWarningBg = oklchToRgba(0.78, 0.15, warningH, isDark ? 0.15 : 0.12);
395
+ const colorWarningGlow = oklchToRgba(0.78, 0.15, warningH, isDark ? 0.3 : 0.2);
396
+
397
+ // === CLI Preview ===
398
+ const cliBg = isDark
399
+ ? oklchToHex(0.10, C * 0.03, H)
400
+ : oklchToHex(0.96, C * 0.02, H);
401
+ const cliBorder = isDark
402
+ ? oklchToHex(0.28, C * 0.05, H)
403
+ : borderGlass;
404
+ const cliText = isDark
405
+ ? oklchToHex(0.72, 0.14, successH)
406
+ : oklchToHex(0.42, 0.12, successH);
407
+ const cliMuted = isDark
408
+ ? oklchToHex(0.40, C * 0.04, H)
409
+ : tMuted;
410
+ const cliHoverBg = isDark
411
+ ? oklchToHex(0.15, C * 0.04, H)
412
+ : oklchToHex(0.92, C * 0.03, H);
413
+
414
+ // === Syntax Highlighting (fixed offsets from base hue) ===
415
+ const syntaxComment = isDark
416
+ ? oklchToHex(0.50, C * 0.04, H)
417
+ : oklchToHex(0.50, C * 0.04, H);
418
+ const syntaxKeyword = isDark
419
+ ? oklchToHex(0.72, 0.14, (H + 30) % 360)
420
+ : oklchToHex(0.45, 0.14, (H + 30) % 360);
421
+ const syntaxString = isDark
422
+ ? oklchToHex(0.75, 0.13, successH)
423
+ : oklchToHex(0.45, 0.13, successH);
424
+ const syntaxNumber = isDark
425
+ ? oklchToHex(0.72, 0.14, (H + 180) % 360)
426
+ : oklchToHex(0.48, 0.14, (H + 180) % 360);
427
+ const syntaxFunction = isDark
428
+ ? oklchToHex(0.75, 0.12, (H + 60) % 360)
429
+ : oklchToHex(0.45, 0.12, (H + 60) % 360);
430
+ const syntaxOperator = isDark
431
+ ? oklchToHex(0.70, 0.10, (H + 15) % 360)
432
+ : oklchToHex(0.45, 0.10, (H + 15) % 360);
433
+ const syntaxVariable = isDark
434
+ ? oklchToHex(0.80, 0.10, (H - 20 + 360) % 360)
435
+ : oklchToHex(0.40, 0.10, (H - 20 + 360) % 360);
436
+ const syntaxPunctuation = isDark
437
+ ? oklchToHex(0.65, C * 0.04, H)
438
+ : oklchToHex(0.40, C * 0.04, H);
439
+
440
+ return {
441
+ bgApp, bgGlass, bgGlassStrong, bgPanel,
442
+ tPrimary, tMuted, borderGlass,
443
+ accentPrimary, accentSecondary, glowPrimary,
444
+ colorError, colorErrorGlow, colorErrorBg,
445
+ colorSuccess, colorSuccessBg,
446
+ colorWarning, colorWarningBg, colorWarningGlow,
447
+ colorInfo: accentSecondary,
448
+ cliBg, cliBorder, cliText, cliMuted, cliHoverBg,
449
+ syntaxComment, syntaxKeyword, syntaxString, syntaxNumber,
450
+ syntaxFunction, syntaxOperator, syntaxVariable, syntaxPunctuation,
451
+ };
452
+ }
453
+
454
+ /**
455
+ * Convert BeamThemeColors to a CSS custom properties object
456
+ * suitable for applying to :host or document root.
457
+ */
458
+ export function beamThemeToCSS(colors: BeamThemeColors): Record<string, string> {
459
+ return {
460
+ '--bg-app': colors.bgApp,
461
+ '--bg-glass': colors.bgGlass,
462
+ '--bg-glass-strong': colors.bgGlassStrong,
463
+ '--bg-panel': colors.bgPanel,
464
+ '--t-primary': colors.tPrimary,
465
+ '--t-muted': colors.tMuted,
466
+ '--border-glass': colors.borderGlass,
467
+ '--accent-primary': colors.accentPrimary,
468
+ '--accent-secondary': colors.accentSecondary,
469
+ '--glow-primary': colors.glowPrimary,
470
+ '--color-error': colors.colorError,
471
+ '--color-error-glow': colors.colorErrorGlow,
472
+ '--color-error-bg': colors.colorErrorBg,
473
+ '--color-success': colors.colorSuccess,
474
+ '--color-success-bg': colors.colorSuccessBg,
475
+ '--color-warning': colors.colorWarning,
476
+ '--color-warning-bg': colors.colorWarningBg,
477
+ '--color-warning-glow': colors.colorWarningGlow,
478
+ '--color-info': colors.colorInfo,
479
+ '--cli-bg': colors.cliBg,
480
+ '--cli-border': colors.cliBorder,
481
+ '--cli-text': colors.cliText,
482
+ '--cli-muted': colors.cliMuted,
483
+ '--cli-hover-bg': colors.cliHoverBg,
484
+ '--syntax-comment': colors.syntaxComment,
485
+ '--syntax-keyword': colors.syntaxKeyword,
486
+ '--syntax-string': colors.syntaxString,
487
+ '--syntax-number': colors.syntaxNumber,
488
+ '--syntax-function': colors.syntaxFunction,
489
+ '--syntax-operator': colors.syntaxOperator,
490
+ '--syntax-variable': colors.syntaxVariable,
491
+ '--syntax-punctuation': colors.syntaxPunctuation,
492
+ };
493
+ }
@@ -8,6 +8,9 @@
8
8
  * This is the foundation for all Photon UI - both BEAM interface and auto-generated UIs.
9
9
  */
10
10
 
11
+ // Re-export OKLCH engine (browser-safe, no Node.js deps)
12
+ export * from './oklch.js';
13
+
11
14
  // =============================================================================
12
15
  // SPACING - 8pt Grid System (Apple HIG)
13
16
  // =============================================================================
@@ -122,14 +125,14 @@ const colorPalette = {
122
125
  100: '#ffffff',
123
126
  },
124
127
 
125
- // Light-theme neutrals (cool blue-gray undertone, higher contrast)
128
+ // Light-theme neutrals (warm cream/beige undertone)
126
129
  neutralLight: {
127
- 85: '#BCC5CF', // heavy borders / dividers
128
- 88: '#C4CDD5', // surface-container-highest
129
- 91: '#D5DBE1', // surface-container-high
130
- 94: '#E5E9EE', // bg-app — visible structure
131
- 96: '#EBEEF2', // surface-container
132
- 98: '#F6F7F9', // surface — slightly off-white panels
130
+ 85: '#C8C0B8', // heavy borders / dividers
131
+ 88: '#D0C9C1', // surface-container-highest
132
+ 91: '#DDD7CF', // surface-container-high
133
+ 94: '#EAE4DD', // bg-app — warm cream structure
134
+ 96: '#F0EBE5', // surface-container
135
+ 98: '#F8F5F1', // surface — warm off-white panels
133
136
  },
134
137
 
135
138
  // Primary (blue - trust, action)
@@ -244,10 +247,10 @@ export const colorsLight = {
244
247
  surfaceContainerHighest: colorPalette.neutralLight[88], // #C4CDD5
245
248
  surfaceBright: colorPalette.neutral[100], // #FFFFFF — overlays/modals
246
249
 
247
- // Text on surfaces (deep charcoal, high contrast)
248
- onSurface: '#1A2332', // 12.3:1 on #E5E9EE — WCAG AAA
249
- onSurfaceVariant: '#3D4C5C', // darker variant for readability
250
- onSurfaceMuted: '#556270', // 5.1:1 on #E5E9EE — WCAG AA
250
+ // Text on surfaces (warm charcoal, high contrast)
251
+ onSurface: '#2C2420', // warm brown-charcoal — WCAG AAA
252
+ onSurfaceVariant: '#4A3F38', // warm dark brown for readability
253
+ onSurfaceMuted: '#6B5E54', // warm muted brown — WCAG AA
251
254
 
252
255
  // Primary (darker for light theme)
253
256
  primary: colorPalette.primary[40],
@@ -273,9 +276,9 @@ export const colorsLight = {
273
276
  onError: colorPalette.neutral[100], // white text
274
277
  onErrorContainer: colorPalette.error[10],
275
278
 
276
- // Outline (visible but not harsh)
277
- outline: '#8B95A3', // stronger than before for visibility
278
- outlineVariant: '#D1D5DB', // visible border, not washed out
279
+ // Outline (warm, visible but not harsh)
280
+ outline: '#9B9088', // warm gray-brown for visibility
281
+ outlineVariant: '#D5CFC8', // warm border, not washed out
279
282
 
280
283
  // Scrim (overlay — lighter than dark theme's 0.5)
281
284
  scrim: 'rgba(0, 0, 0, 0.2)' as const,