@sero-ai/ui 0.4.2 → 0.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.
- package/.turbo/turbo-build.log +636 -628
- package/.turbo/turbo-test.log +12 -11
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +20 -2
- package/dist/components/ai-elements/conversation-virtual.cjs +115 -0
- package/dist/components/ai-elements/conversation-virtual.d.cts +30 -0
- package/dist/components/ai-elements/conversation-virtual.d.ts +30 -0
- package/dist/components/ai-elements/conversation-virtual.js +115 -0
- package/dist/components/model-selection/available-model-picker.cjs +21 -77
- package/dist/components/model-selection/available-model-picker.js +22 -78
- package/dist/components/model-selection/model-picker-body.cjs +106 -0
- package/dist/components/model-selection/model-picker-body.d.cts +41 -0
- package/dist/components/model-selection/model-picker-body.d.ts +41 -0
- package/dist/components/model-selection/model-picker-body.js +106 -0
- package/dist/components/model-selection/thinking-level-picker.cjs +7 -3
- package/dist/components/model-selection/thinking-level-picker.js +7 -3
- package/dist/components/model-selection/thinking-picker.cjs +1 -1
- package/dist/components/model-selection/thinking-picker.js +1 -1
- package/dist/components/ui/calendar.cjs +1 -1
- package/dist/components/ui/calendar.js +1 -1
- package/dist/components/ui/combobox.cjs +1 -1
- package/dist/components/ui/combobox.js +1 -1
- package/dist/components/ui/form.d.cts +2 -2
- package/dist/components/ui/form.d.ts +2 -2
- package/dist/components/ui/input-group.cjs +1 -1
- package/dist/components/ui/input-group.js +1 -1
- package/dist/components/ui/input-otp.cjs +1 -1
- package/dist/components/ui/input-otp.js +1 -1
- package/dist/components/ui/input.cjs +3 -3
- package/dist/components/ui/input.js +3 -3
- package/dist/components/ui/native-select.cjs +2 -2
- package/dist/components/ui/native-select.js +2 -2
- package/dist/components/ui/select.cjs +1 -1
- package/dist/components/ui/select.js +1 -1
- package/dist/components/ui/slider.cjs +6 -0
- package/dist/components/ui/slider.d.cts +1 -1
- package/dist/components/ui/slider.d.ts +1 -1
- package/dist/components/ui/slider.js +6 -0
- package/dist/components/ui/spinner.d.cts +2 -1
- package/dist/components/ui/spinner.d.ts +2 -1
- package/dist/components/ui/textarea.cjs +1 -1
- package/dist/components/ui/textarea.js +1 -1
- package/dist/index.cjs +0 -55
- package/dist/index.d.cts +2 -66
- package/dist/index.d.ts +2 -66
- package/dist/index.js +0 -55
- package/dist/reference.cjs +56 -83
- package/dist/reference.js +56 -83
- package/dist/styles/ai-elements.css +2 -0
- package/dist/styles/context-editor.css +2 -0
- package/dist/styles/globals.css +32 -0
- package/dist/styles/model-selection.css +2 -0
- package/dist/styles/plugin.css +4 -1
- package/dist/theme/apply-theme.cjs +65 -12
- package/dist/theme/apply-theme.js +55 -2
- package/dist/theme/index.d.cts +1 -1
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/types.cjs +15 -2
- package/dist/theme/types.d.cts +21 -1
- package/dist/theme/types.d.ts +21 -1
- package/dist/theme/types.js +14 -1
- package/package.json +44 -30
- package/scripts/smoke-dist.mjs +106 -4
- package/src/components/ai-elements/conversation-virtual.test.tsx +126 -0
- package/src/components/ai-elements/conversation-virtual.tsx +162 -0
- package/src/components/model-selection/available-model-picker.tsx +26 -98
- package/src/components/model-selection/model-picker-body.test.tsx +152 -0
- package/src/components/model-selection/model-picker-body.tsx +188 -0
- package/src/components/model-selection/thinking-level-picker.tsx +9 -3
- package/src/components/model-selection/thinking-picker.test.tsx +41 -0
- package/src/components/model-selection/thinking-picker.tsx +1 -1
- package/src/components/ui/calendar.test.tsx +43 -0
- package/src/components/ui/calendar.tsx +1 -1
- package/src/components/ui/combobox.tsx +1 -1
- package/src/components/ui/input-group.tsx +1 -1
- package/src/components/ui/input-otp.tsx +1 -1
- package/src/components/ui/input.tsx +3 -3
- package/src/components/ui/native-select.tsx +2 -2
- package/src/components/ui/select.tsx +1 -1
- package/src/components/ui/slider.tsx +6 -0
- package/src/components/ui/spinner.tsx +1 -1
- package/src/components/ui/textarea.tsx +1 -1
- package/src/index.ts +4 -63
- package/src/styles/ai-elements.css +2 -0
- package/src/styles/context-editor.css +2 -0
- package/src/styles/globals.css +32 -0
- package/src/styles/model-selection.css +2 -0
- package/src/styles/plugin.css +4 -1
- package/src/theme/apply-theme.test.ts +126 -0
- package/src/theme/apply-theme.ts +66 -3
- package/src/theme/types.ts +33 -0
package/src/index.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
// @sero-ai/ui — Shared UI components, AI elements, and utilities
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// Import primitives, dashboard components, hooks, and utilities from the root:
|
|
4
4
|
// import { Button, Card, Input, cn, useIsMobile } from "@sero-ai/ui"
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// import {
|
|
9
|
-
// import { cn } from "@sero-ai/ui/lib/utils.js"
|
|
6
|
+
// AI elements have stable public subpaths so their editor and rendering
|
|
7
|
+
// dependencies are loaded only by consumers that use them:
|
|
8
|
+
// import { Message } from "@sero-ai/ui/ai-elements/message"
|
|
10
9
|
|
|
11
10
|
// ── Utilities ──
|
|
12
11
|
export { cn } from "./lib/utils";
|
|
@@ -77,60 +76,6 @@ export * from "./components/ui/toggle-group";
|
|
|
77
76
|
export * from "./components/ui/toggle";
|
|
78
77
|
export * from "./components/ui/tooltip";
|
|
79
78
|
export * from "./components/ui/tree";
|
|
80
|
-
// ── AI Elements ──
|
|
81
|
-
export * from "./components/ai-elements/agent";
|
|
82
|
-
export * from "./components/ai-elements/artifact";
|
|
83
|
-
export * from "./components/ai-elements/attachments";
|
|
84
|
-
export * from "./components/ai-elements/audio-player";
|
|
85
|
-
export * from "./components/ai-elements/canvas";
|
|
86
|
-
export * from "./components/ai-elements/chain-of-thought";
|
|
87
|
-
export * from "./components/ai-elements/checkpoint";
|
|
88
|
-
export * from "./components/ai-elements/code-block";
|
|
89
|
-
export * from "./components/ai-elements/commit";
|
|
90
|
-
export * from "./components/ai-elements/confirmation";
|
|
91
|
-
export * from "./components/ai-elements/connection";
|
|
92
|
-
export * from "./components/ai-elements/context";
|
|
93
|
-
export * from "./components/ai-elements/controls";
|
|
94
|
-
export * from "./components/ai-elements/conversation";
|
|
95
|
-
export * from "./components/ai-elements/edge";
|
|
96
|
-
export * from "./components/ai-elements/environment-variables";
|
|
97
|
-
export * from "./components/ai-elements/file-tree";
|
|
98
|
-
export * from "./components/ai-elements/image";
|
|
99
|
-
export * from "./components/ai-elements/inline-citation";
|
|
100
|
-
export * from "./components/ai-elements/jsx-preview";
|
|
101
|
-
export * from "./components/ai-elements/message";
|
|
102
|
-
export * from "./components/ai-elements/mic-selector";
|
|
103
|
-
export * from "./components/ai-elements/model-selector";
|
|
104
|
-
export * from "./components/ai-elements/node";
|
|
105
|
-
export * from "./components/ai-elements/open-in-chat";
|
|
106
|
-
export * from "./components/ai-elements/package-info";
|
|
107
|
-
export * from "./components/ai-elements/panel";
|
|
108
|
-
export * from "./components/ai-elements/persona";
|
|
109
|
-
export * from "./components/ai-elements/plan";
|
|
110
|
-
export * from "./components/ai-elements/prompt-input-context";
|
|
111
|
-
export * from "./components/ai-elements/prompt-input-elements";
|
|
112
|
-
export * from "./components/ai-elements/prompt-input-textarea";
|
|
113
|
-
export * from "./components/ai-elements/prompt-input";
|
|
114
|
-
export * from "./components/ai-elements/queue";
|
|
115
|
-
export * from "./components/ai-elements/reasoning";
|
|
116
|
-
export * from "./components/ai-elements/sandbox";
|
|
117
|
-
export * from "./components/ai-elements/schema-display";
|
|
118
|
-
export * from "./components/ai-elements/shimmer";
|
|
119
|
-
export * from "./components/ai-elements/snippet";
|
|
120
|
-
export * from "./components/ai-elements/sources";
|
|
121
|
-
export * from "./components/ai-elements/speech-input";
|
|
122
|
-
export * from "./components/ai-elements/stack-trace";
|
|
123
|
-
export * from "./components/ai-elements/suggestion";
|
|
124
|
-
export * from "./components/ai-elements/task";
|
|
125
|
-
export * from "./components/ai-elements/terminal";
|
|
126
|
-
export * from "./components/ai-elements/test-results";
|
|
127
|
-
export * from "./components/ai-elements/tool";
|
|
128
|
-
export * from "./components/ai-elements/toolbar";
|
|
129
|
-
export * from "./components/ai-elements/transcription";
|
|
130
|
-
export * from "./components/ai-elements/voice-selector-accent";
|
|
131
|
-
export * from "./components/ai-elements/voice-selector";
|
|
132
|
-
export * from "./components/ai-elements/web-preview";
|
|
133
|
-
|
|
134
79
|
// ── Dashboard Components ──
|
|
135
80
|
// Compact presentation components for dashboard widgets and full plugin views.
|
|
136
81
|
// The discovery catalogue is plain data at "@sero-ai/ui/dashboard-catalog.json";
|
|
@@ -138,9 +83,5 @@ export * from "./components/ai-elements/web-preview";
|
|
|
138
83
|
export * from "./components/dashboard";
|
|
139
84
|
export type { DashboardComponentCatalogEntry } from "./components/dashboard/catalog";
|
|
140
85
|
|
|
141
|
-
// ── Model Selection Components ──
|
|
142
|
-
export * from "./components/model-selection/available-model-picker";
|
|
143
|
-
export * from "./components/model-selection/model-warning-list";
|
|
144
|
-
export * from "./components/model-selection/thinking-level-picker";
|
|
145
86
|
export { PluginStyleScope, type PluginStyleScopeProps } from './plugin-style-scope';
|
|
146
87
|
export { usePluginPortalContainer } from './plugin-style-scope-context';
|
package/src/styles/globals.css
CHANGED
|
@@ -504,6 +504,38 @@ html {
|
|
|
504
504
|
}
|
|
505
505
|
}
|
|
506
506
|
|
|
507
|
+
/* Native window glass belongs behind the application. Keep the document
|
|
508
|
+
transparent, then restore an opaque base for blocking work surfaces. */
|
|
509
|
+
html.theme-glass body {
|
|
510
|
+
background-color: transparent;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.theme-glass :is(.window-glass-sidebar, [data-slot="sidebar"]) {
|
|
514
|
+
background-color: var(--window-glass-sidebar);
|
|
515
|
+
box-shadow: inset -1px 0 0 var(--border-subtle);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.theme-glass :is(
|
|
519
|
+
[data-slot="dialog-content"],
|
|
520
|
+
[data-slot="alert-dialog-content"],
|
|
521
|
+
[data-slot="sheet-content"],
|
|
522
|
+
[data-slot="drawer-content"],
|
|
523
|
+
[data-slot="popover-content"],
|
|
524
|
+
[data-slot="dropdown-menu-content"],
|
|
525
|
+
[data-slot="context-menu-content"],
|
|
526
|
+
[data-slot="select-content"]
|
|
527
|
+
) {
|
|
528
|
+
--background: var(--window-glass-opaque-base);
|
|
529
|
+
--card: var(--window-glass-opaque-surface);
|
|
530
|
+
--secondary: var(--window-glass-opaque-elevated);
|
|
531
|
+
--sidebar: var(--window-glass-opaque-surface);
|
|
532
|
+
--sidebar-accent: var(--window-glass-opaque-elevated);
|
|
533
|
+
--bg-base: var(--window-glass-opaque-base);
|
|
534
|
+
--bg-surface: var(--window-glass-opaque-surface);
|
|
535
|
+
--bg-elevated: var(--window-glass-opaque-elevated);
|
|
536
|
+
background-color: var(--window-glass-opaque-base);
|
|
537
|
+
}
|
|
538
|
+
|
|
507
539
|
@media (prefers-reduced-motion: reduce) {
|
|
508
540
|
*,
|
|
509
541
|
*::before,
|
package/src/styles/plugin.css
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
@import "shadcn/tailwind.css";
|
|
4
4
|
@import "./plugin-theme.css";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/* Match the lightweight package root. Specialized components opt into their
|
|
7
|
+
own source files through a matching stylesheet under styles/. */
|
|
8
|
+
@source "../components/ui";
|
|
9
|
+
@source "../components/dashboard";
|
|
7
10
|
|
|
8
11
|
@custom-variant dark (&:is(.dark *));
|
|
9
12
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
4
|
+
import { applyThemePreset, resetTheme, validateThemePreset } from './apply-theme';
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_DARK_COLORS,
|
|
7
|
+
DEFAULT_GLASS_EFFECT,
|
|
8
|
+
DEFAULT_LIGHT_COLORS,
|
|
9
|
+
type ThemePreset,
|
|
10
|
+
} from './types';
|
|
11
|
+
|
|
12
|
+
function createPreset(overrides: Partial<ThemePreset> = {}): ThemePreset {
|
|
13
|
+
return {
|
|
14
|
+
id: 'glass-test',
|
|
15
|
+
name: 'Glass test',
|
|
16
|
+
version: 1,
|
|
17
|
+
colors: {
|
|
18
|
+
light: { ...DEFAULT_LIGHT_COLORS },
|
|
19
|
+
dark: { ...DEFAULT_DARK_COLORS },
|
|
20
|
+
},
|
|
21
|
+
...overrides,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('theme glass effect', () => {
|
|
26
|
+
afterEach(() => resetTheme());
|
|
27
|
+
|
|
28
|
+
it('separates translucent layers and retains solid popup colors', () => {
|
|
29
|
+
applyThemePreset(createPreset({
|
|
30
|
+
glass: { enabled: true, opacity: 0.64 },
|
|
31
|
+
}), 'dark');
|
|
32
|
+
|
|
33
|
+
const root = document.documentElement;
|
|
34
|
+
expect(root.classList.contains('theme-glass')).toBe(true);
|
|
35
|
+
expect(root.style.getPropertyValue('--bg-base')).toBe(
|
|
36
|
+
'color-mix(in srgb, #0a0a0b 64%, transparent)',
|
|
37
|
+
);
|
|
38
|
+
expect(root.style.getPropertyValue('--bg-surface')).toBe(
|
|
39
|
+
'color-mix(in srgb, #111113 18%, transparent)',
|
|
40
|
+
);
|
|
41
|
+
expect(root.style.getPropertyValue('--window-glass-opaque-surface')).toBe('#111113');
|
|
42
|
+
expect(root.style.getPropertyValue('--bg-elevated')).toBe(
|
|
43
|
+
'color-mix(in srgb, #fafafa 8%, transparent)',
|
|
44
|
+
);
|
|
45
|
+
expect(root.style.getPropertyValue('--window-glass-opaque-elevated')).toBe('#18181b');
|
|
46
|
+
expect(root.style.getPropertyValue('--window-glass-sidebar')).toBe(
|
|
47
|
+
'color-mix(in srgb, #111113 8%, transparent)',
|
|
48
|
+
);
|
|
49
|
+
expect(root.style.getPropertyValue('--window-glass-opaque-base')).toBe(
|
|
50
|
+
'#0a0a0b',
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('restores opaque surfaces when the next theme disables glass', () => {
|
|
55
|
+
applyThemePreset(createPreset({
|
|
56
|
+
glass: { enabled: true, opacity: 0.64 },
|
|
57
|
+
}), 'dark');
|
|
58
|
+
applyThemePreset(createPreset(), 'dark');
|
|
59
|
+
|
|
60
|
+
const root = document.documentElement;
|
|
61
|
+
expect(root.classList.contains('theme-glass')).toBe(false);
|
|
62
|
+
expect(root.style.getPropertyValue('--bg-base')).toBe('#0a0a0b');
|
|
63
|
+
expect(root.style.getPropertyValue('--bg-surface')).toBe('#111113');
|
|
64
|
+
expect(root.style.getPropertyValue('--bg-elevated')).toBe('#18181b');
|
|
65
|
+
expect(root.style.getPropertyValue('--window-glass-opaque-surface')).toBe('');
|
|
66
|
+
expect(root.style.getPropertyValue('--window-glass-sidebar')).toBe('');
|
|
67
|
+
expect(root.style.getPropertyValue('--window-glass-opaque-base')).toBe('');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('keeps panel boundaries and soft selections at zero window tint in light mode', () => {
|
|
71
|
+
applyThemePreset(createPreset({
|
|
72
|
+
glass: { enabled: true, opacity: 0 },
|
|
73
|
+
}), 'light');
|
|
74
|
+
|
|
75
|
+
const root = document.documentElement;
|
|
76
|
+
expect(root.style.getPropertyValue('--bg-base')).toBe(
|
|
77
|
+
'color-mix(in srgb, #ffffff 0%, transparent)',
|
|
78
|
+
);
|
|
79
|
+
expect(root.style.getPropertyValue('--window-glass-sidebar')).toBe(
|
|
80
|
+
'color-mix(in srgb, #f4f5f7 8%, transparent)',
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it.each([
|
|
85
|
+
[-1, 0],
|
|
86
|
+
[2, 1],
|
|
87
|
+
])('clamps persisted glass opacity %s to %s', (opacity, expected) => {
|
|
88
|
+
const preset = createPreset({ glass: { enabled: true, opacity } });
|
|
89
|
+
|
|
90
|
+
expect(validateThemePreset(preset)?.glass).toEqual({
|
|
91
|
+
...DEFAULT_GLASS_EFFECT,
|
|
92
|
+
enabled: true,
|
|
93
|
+
opacity: expected,
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
it.each(['light', 'dark'] as const)('can clear every local layer in %s mode independently of native material', (mode) => {
|
|
97
|
+
const preset = createPreset({ glass: {
|
|
98
|
+
enabled: true, opacity: 0, blurRadius: 32, windowsMaterial: 'acrylic', sidebarOpacity: 0,
|
|
99
|
+
surfaceOpacity: 0, selectionOpacity: 0, borderOpacity: 0,
|
|
100
|
+
} });
|
|
101
|
+
const savedTheme = JSON.stringify(preset);
|
|
102
|
+
const validated = validateThemePreset(JSON.parse(savedTheme));
|
|
103
|
+
expect(validated?.glass).toEqual(preset.glass);
|
|
104
|
+
if (!validated) throw new Error('Expected valid theme');
|
|
105
|
+
applyThemePreset(validated, mode);
|
|
106
|
+
const style = document.documentElement.style;
|
|
107
|
+
for (const key of ['--bg-base', '--window-glass-sidebar', '--bg-surface', '--bg-elevated', '--border-default']) {
|
|
108
|
+
expect(style.getPropertyValue(key)).toContain(' 0%, transparent)');
|
|
109
|
+
}
|
|
110
|
+
applyThemePreset({ ...validated, glass: { ...preset.glass!, surfaceOpacity: 0.4 } }, mode);
|
|
111
|
+
expect(style.getPropertyValue('--bg-surface')).toContain(' 40%, transparent)');
|
|
112
|
+
expect(style.getPropertyValue('--window-glass-sidebar')).toContain(' 0%, transparent)');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('normalises invalid saved glass controls', () => {
|
|
116
|
+
const preset = createPreset();
|
|
117
|
+
const validated = validateThemePreset({ ...preset, glass: {
|
|
118
|
+
enabled: true, opacity: NaN, sidebarOpacity: -1, surfaceOpacity: 4,
|
|
119
|
+
selectionOpacity: Infinity, borderOpacity: 'bad', blurRadius: Infinity, windowsMaterial: 'invalid',
|
|
120
|
+
} });
|
|
121
|
+
expect(validated?.glass).toEqual({
|
|
122
|
+
...DEFAULT_GLASS_EFFECT, enabled: true, sidebarOpacity: 0, surfaceOpacity: 1,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
});
|
package/src/theme/apply-theme.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_GLASS_EFFECT,
|
|
3
|
+
WINDOWS_GLASS_MATERIALS,
|
|
4
|
+
type ColorTokens,
|
|
5
|
+
type ThemeGlassEffect,
|
|
6
|
+
type ThemePreset,
|
|
4
7
|
} from './types';
|
|
5
8
|
|
|
6
9
|
// ── Token → CSS Variable Mapping ─────────────────────────────
|
|
@@ -86,6 +89,38 @@ function tint(color: string, percentage: number): string {
|
|
|
86
89
|
return `color-mix(in srgb, ${color} ${percentage}%, transparent)`;
|
|
87
90
|
}
|
|
88
91
|
|
|
92
|
+
function applyGlassEffect(
|
|
93
|
+
root: HTMLElement,
|
|
94
|
+
glass: ThemeGlassEffect | undefined,
|
|
95
|
+
colors: ColorTokens,
|
|
96
|
+
): void {
|
|
97
|
+
const enabled = glass?.enabled === true;
|
|
98
|
+
root.classList.toggle('theme-glass', enabled);
|
|
99
|
+
root.style.removeProperty('--window-glass-opaque-base');
|
|
100
|
+
root.style.removeProperty('--window-glass-sidebar');
|
|
101
|
+
root.style.removeProperty('--window-glass-opaque-surface');
|
|
102
|
+
root.style.removeProperty('--window-glass-opaque-elevated');
|
|
103
|
+
if (!enabled) return;
|
|
104
|
+
|
|
105
|
+
const opacity = Math.min(1, Math.max(0, glass.opacity));
|
|
106
|
+
const percentage = Math.round(opacity * 100);
|
|
107
|
+
root.style.setProperty('--bg-base', tint(colors.bgBase, percentage));
|
|
108
|
+
root.style.setProperty(
|
|
109
|
+
'--window-glass-sidebar',
|
|
110
|
+
tint(colors.bgSurface, (glass.sidebarOpacity ?? DEFAULT_GLASS_EFFECT.sidebarOpacity) * 100),
|
|
111
|
+
);
|
|
112
|
+
root.style.setProperty('--window-glass-opaque-base', colors.bgBase);
|
|
113
|
+
root.style.setProperty('--window-glass-opaque-surface', colors.bgSurface);
|
|
114
|
+
root.style.setProperty('--window-glass-opaque-elevated', colors.bgElevated);
|
|
115
|
+
// Thin local layers preserve the wallpaper while separating nested content.
|
|
116
|
+
root.style.setProperty('--bg-surface', tint(colors.bgSurface, (glass.surfaceOpacity ?? DEFAULT_GLASS_EFFECT.surfaceOpacity) * 100));
|
|
117
|
+
root.style.setProperty('--bg-elevated', tint(colors.textPrimary, (glass.selectionOpacity ?? DEFAULT_GLASS_EFFECT.selectionOpacity) * 100));
|
|
118
|
+
root.style.setProperty('--text-muted', colors.textSecondary);
|
|
119
|
+
const border = (glass.borderOpacity ?? DEFAULT_GLASS_EFFECT.borderOpacity) * 100;
|
|
120
|
+
root.style.setProperty('--border-subtle', tint(colors.textPrimary, border * 2 / 3));
|
|
121
|
+
root.style.setProperty('--border-default', tint(colors.textPrimary, border));
|
|
122
|
+
}
|
|
123
|
+
|
|
89
124
|
// ── Apply / Reset ────────────────────────────────────────────
|
|
90
125
|
|
|
91
126
|
/** Apply a theme preset for the given mode. */
|
|
@@ -130,6 +165,8 @@ export function applyThemePreset(
|
|
|
130
165
|
root.style.setProperty('--radius', preset.radius.md);
|
|
131
166
|
}
|
|
132
167
|
|
|
168
|
+
applyGlassEffect(root, preset.glass, colors);
|
|
169
|
+
|
|
133
170
|
root.classList.toggle('dark', mode === 'dark');
|
|
134
171
|
}
|
|
135
172
|
|
|
@@ -142,6 +179,10 @@ const ALL_MANAGED_VARS = [
|
|
|
142
179
|
'--font-size-base',
|
|
143
180
|
'--spacing',
|
|
144
181
|
'--radius',
|
|
182
|
+
'--window-glass-opaque-base',
|
|
183
|
+
'--window-glass-sidebar',
|
|
184
|
+
'--window-glass-opaque-surface',
|
|
185
|
+
'--window-glass-opaque-elevated',
|
|
145
186
|
];
|
|
146
187
|
|
|
147
188
|
/** Remove all inline theme overrides, reverting to CSS defaults. */
|
|
@@ -150,6 +191,7 @@ export function resetTheme(): void {
|
|
|
150
191
|
for (const cssVar of ALL_MANAGED_VARS) {
|
|
151
192
|
root.style.removeProperty(cssVar);
|
|
152
193
|
}
|
|
194
|
+
root.classList.remove('theme-glass');
|
|
153
195
|
}
|
|
154
196
|
|
|
155
197
|
// ── Validation ───────────────────────────────────────────────
|
|
@@ -221,6 +263,26 @@ function sanitiseRadius(raw: unknown): ThemePreset['radius'] | undefined {
|
|
|
221
263
|
return Object.keys(result).length > 0 ? result : undefined;
|
|
222
264
|
}
|
|
223
265
|
|
|
266
|
+
function sanitiseGlass(raw: unknown): ThemeGlassEffect | undefined {
|
|
267
|
+
if (!raw || typeof raw !== 'object') return undefined;
|
|
268
|
+
const value = raw as Record<string, unknown>;
|
|
269
|
+
if (typeof value.enabled !== 'boolean') return undefined;
|
|
270
|
+
|
|
271
|
+
const result = { ...DEFAULT_GLASS_EFFECT, enabled: value.enabled };
|
|
272
|
+
for (const key of ['opacity', 'sidebarOpacity', 'surfaceOpacity', 'selectionOpacity', 'borderOpacity'] as const) {
|
|
273
|
+
const amount = value[key];
|
|
274
|
+
if (typeof amount === 'number' && Number.isFinite(amount)) {
|
|
275
|
+
result[key] = Math.min(1, Math.max(0, amount));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (typeof value.blurRadius === 'number' && Number.isFinite(value.blurRadius)) {
|
|
279
|
+
result.blurRadius = Math.round(Math.min(64, Math.max(0, value.blurRadius)));
|
|
280
|
+
}
|
|
281
|
+
result.windowsMaterial = WINDOWS_GLASS_MATERIALS.find((material) => material === value.windowsMaterial)
|
|
282
|
+
?? DEFAULT_GLASS_EFFECT.windowsMaterial;
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
|
|
224
286
|
/** Validate and normalise an unknown value into a ThemePreset. */
|
|
225
287
|
export function validateThemePreset(data: unknown): ThemePreset | null {
|
|
226
288
|
if (!data || typeof data !== 'object') return null;
|
|
@@ -246,5 +308,6 @@ export function validateThemePreset(data: unknown): ThemePreset | null {
|
|
|
246
308
|
typography: sanitiseTypography(d.typography),
|
|
247
309
|
spacing: sanitiseSpacing(d.spacing),
|
|
248
310
|
radius: sanitiseRadius(d.radius),
|
|
311
|
+
glass: sanitiseGlass(d.glass),
|
|
249
312
|
};
|
|
250
313
|
}
|
package/src/theme/types.ts
CHANGED
|
@@ -105,6 +105,25 @@ export interface RadiusTokens {
|
|
|
105
105
|
lg?: string;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
export const WINDOWS_GLASS_MATERIALS = ['acrylic', 'mica', 'tabbed'] as const;
|
|
109
|
+
export type WindowsGlassMaterial = typeof WINDOWS_GLASS_MATERIALS[number];
|
|
110
|
+
|
|
111
|
+
export interface ThemeGlassEffect {
|
|
112
|
+
/** Show the desktop through Sero's surfaces. */
|
|
113
|
+
enabled: boolean;
|
|
114
|
+
/** Window tint opacity from 0 to 1. */
|
|
115
|
+
opacity: number;
|
|
116
|
+
/** Direct macOS desktop blur radius, from 0 to 64 pixels. */
|
|
117
|
+
blurRadius?: number;
|
|
118
|
+
/** Windows 11 system backdrop. */
|
|
119
|
+
windowsMaterial?: WindowsGlassMaterial;
|
|
120
|
+
/** Independent local layer opacities from 0 to 1. */
|
|
121
|
+
sidebarOpacity?: number;
|
|
122
|
+
surfaceOpacity?: number;
|
|
123
|
+
selectionOpacity?: number;
|
|
124
|
+
borderOpacity?: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
108
127
|
// ── Theme Preset ─────────────────────────────────────────────
|
|
109
128
|
|
|
110
129
|
export interface ThemePreset {
|
|
@@ -141,6 +160,9 @@ export interface ThemePreset {
|
|
|
141
160
|
|
|
142
161
|
/** Border radius overrides. */
|
|
143
162
|
radius?: RadiusTokens;
|
|
163
|
+
|
|
164
|
+
/** Optional translucent desktop window treatment. */
|
|
165
|
+
glass?: ThemeGlassEffect;
|
|
144
166
|
}
|
|
145
167
|
|
|
146
168
|
/** Lightweight metadata for listing presets without loading full data. */
|
|
@@ -218,6 +240,17 @@ export const DEFAULT_RADIUS: Required<RadiusTokens> = {
|
|
|
218
240
|
lg: '12px',
|
|
219
241
|
};
|
|
220
242
|
|
|
243
|
+
export const DEFAULT_GLASS_EFFECT: Required<ThemeGlassEffect> = {
|
|
244
|
+
enabled: false,
|
|
245
|
+
opacity: 0.78,
|
|
246
|
+
blurRadius: 24,
|
|
247
|
+
windowsMaterial: 'acrylic',
|
|
248
|
+
sidebarOpacity: 0.08,
|
|
249
|
+
surfaceOpacity: 0.18,
|
|
250
|
+
selectionOpacity: 0.08,
|
|
251
|
+
borderOpacity: 0.18,
|
|
252
|
+
};
|
|
253
|
+
|
|
221
254
|
/** Default colour tokens matching globals.css .dark values. */
|
|
222
255
|
export const DEFAULT_DARK_COLORS: ColorTokens = {
|
|
223
256
|
bgBase: '#0a0a0b',
|