flawed-avatar 0.2.1

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 (152) hide show
  1. package/assets/animations/idle/Breathing Idle.fbx +0 -0
  2. package/assets/animations/idle/look away gesture.fbx +0 -0
  3. package/assets/animations/idle/weight shift.fbx +0 -0
  4. package/assets/animations/speaking/Agreeing.fbx +0 -0
  5. package/assets/animations/speaking/Talking (1).fbx +0 -0
  6. package/assets/animations/speaking/Talking (2).fbx +0 -0
  7. package/assets/animations/speaking/Talking (3).fbx +0 -0
  8. package/assets/animations/speaking/Talking.fbx +0 -0
  9. package/assets/animations/speaking/head nod yes.fbx +0 -0
  10. package/assets/animations/thinking/Thinking.fbx +0 -0
  11. package/assets/animations/thinking/thoughtful head shake.fbx +0 -0
  12. package/assets/animations/working/acknowledging.fbx +0 -0
  13. package/assets/animations/working/lengthy head nod.fbx +0 -0
  14. package/assets/icon.png +0 -0
  15. package/assets/models/CaptainLobster.vrm +0 -0
  16. package/assets/models/default-avatar.vrm +0 -0
  17. package/dist/chat-preload.cjs +87 -0
  18. package/dist/chat-renderer-bundle/chat-index.html +16 -0
  19. package/dist/chat-renderer-bundle/chat-renderer.js +355 -0
  20. package/dist/chat-renderer-bundle/styles/base.css +106 -0
  21. package/dist/chat-renderer-bundle/styles/chat.css +516 -0
  22. package/dist/chat-renderer-bundle/styles/components/button.css +221 -0
  23. package/dist/chat-renderer-bundle/styles/components/indicator.css +216 -0
  24. package/dist/chat-renderer-bundle/styles/components/input.css +139 -0
  25. package/dist/chat-renderer-bundle/styles/components/toast.css +204 -0
  26. package/dist/chat-renderer-bundle/styles/controls.css +279 -0
  27. package/dist/chat-renderer-bundle/styles/settings.css +310 -0
  28. package/dist/chat-renderer-bundle/styles/tokens.css +220 -0
  29. package/dist/chat-renderer-bundle/styles/utilities.css +349 -0
  30. package/dist/main/main/display-utils.d.ts +12 -0
  31. package/dist/main/main/display-utils.js +29 -0
  32. package/dist/main/main/gateway-client.d.ts +13 -0
  33. package/dist/main/main/gateway-client.js +265 -0
  34. package/dist/main/main/main.d.ts +1 -0
  35. package/dist/main/main/main.js +157 -0
  36. package/dist/main/main/persistence/chat-store.d.ts +8 -0
  37. package/dist/main/main/persistence/chat-store.js +110 -0
  38. package/dist/main/main/persistence/file-store.d.ts +17 -0
  39. package/dist/main/main/persistence/file-store.js +183 -0
  40. package/dist/main/main/persistence/index.d.ts +8 -0
  41. package/dist/main/main/persistence/index.js +8 -0
  42. package/dist/main/main/persistence/migrations.d.ts +23 -0
  43. package/dist/main/main/persistence/migrations.js +191 -0
  44. package/dist/main/main/persistence/settings-store.d.ts +32 -0
  45. package/dist/main/main/persistence/settings-store.js +174 -0
  46. package/dist/main/main/persistence/types.d.ts +72 -0
  47. package/dist/main/main/persistence/types.js +69 -0
  48. package/dist/main/main/settings-broadcast.d.ts +3 -0
  49. package/dist/main/main/settings-broadcast.js +9 -0
  50. package/dist/main/main/stdin-listener.d.ts +15 -0
  51. package/dist/main/main/stdin-listener.js +27 -0
  52. package/dist/main/main/tray.d.ts +3 -0
  53. package/dist/main/main/tray.js +59 -0
  54. package/dist/main/main/window-manager.d.ts +23 -0
  55. package/dist/main/main/window-manager.js +232 -0
  56. package/dist/main/main/window.d.ts +3 -0
  57. package/dist/main/main/window.js +528 -0
  58. package/dist/main/shared/config.d.ts +91 -0
  59. package/dist/main/shared/config.js +111 -0
  60. package/dist/main/shared/ipc-channels.d.ts +54 -0
  61. package/dist/main/shared/ipc-channels.js +68 -0
  62. package/dist/main/shared/types.d.ts +6 -0
  63. package/dist/main/shared/types.js +1 -0
  64. package/dist/preload.cjs +256 -0
  65. package/dist/renderer-bundle/index.html +63 -0
  66. package/dist/renderer-bundle/renderer.js +100734 -0
  67. package/dist/renderer-bundle/styles/base.css +106 -0
  68. package/dist/renderer-bundle/styles/chat.css +516 -0
  69. package/dist/renderer-bundle/styles/components/button.css +221 -0
  70. package/dist/renderer-bundle/styles/components/indicator.css +216 -0
  71. package/dist/renderer-bundle/styles/components/input.css +139 -0
  72. package/dist/renderer-bundle/styles/components/toast.css +204 -0
  73. package/dist/renderer-bundle/styles/controls.css +279 -0
  74. package/dist/renderer-bundle/styles/settings.css +310 -0
  75. package/dist/renderer-bundle/styles/tokens.css +220 -0
  76. package/dist/renderer-bundle/styles/utilities.css +349 -0
  77. package/index.ts +32 -0
  78. package/openclaw.plugin.json +22 -0
  79. package/package.json +45 -0
  80. package/src/electron-launcher.ts +63 -0
  81. package/src/main/chat-preload.cjs +87 -0
  82. package/src/main/display-utils.ts +39 -0
  83. package/src/main/gateway-client.ts +312 -0
  84. package/src/main/main.ts +169 -0
  85. package/src/main/persistence/chat-store.ts +143 -0
  86. package/src/main/persistence/file-store.ts +221 -0
  87. package/src/main/persistence/index.ts +69 -0
  88. package/src/main/persistence/migrations.ts +232 -0
  89. package/src/main/persistence/settings-store.ts +219 -0
  90. package/src/main/persistence/types.ts +107 -0
  91. package/src/main/preload.cjs +256 -0
  92. package/src/main/settings-broadcast.ts +13 -0
  93. package/src/main/settings-preload.cjs +153 -0
  94. package/src/main/stdin-listener.ts +34 -0
  95. package/src/main/tray.ts +65 -0
  96. package/src/main/window-manager.ts +298 -0
  97. package/src/main/window.ts +614 -0
  98. package/src/renderer/audio/audio-player.ts +161 -0
  99. package/src/renderer/audio/frequency-analyzer.ts +104 -0
  100. package/src/renderer/audio/index.ts +36 -0
  101. package/src/renderer/audio/kokoro-model-loader.ts +128 -0
  102. package/src/renderer/audio/kokoro-tts-service.ts +370 -0
  103. package/src/renderer/audio/lip-sync-profile.json +1 -0
  104. package/src/renderer/audio/phoneme-mapper.ts +120 -0
  105. package/src/renderer/audio/tts-controller.ts +344 -0
  106. package/src/renderer/audio/tts-service-factory.ts +75 -0
  107. package/src/renderer/audio/tts-service.ts +16 -0
  108. package/src/renderer/audio/types.ts +120 -0
  109. package/src/renderer/audio/web-speech-tts.ts +177 -0
  110. package/src/renderer/audio/wlipsync-analyzer.ts +145 -0
  111. package/src/renderer/avatar/animation-loader.ts +114 -0
  112. package/src/renderer/avatar/animator.ts +322 -0
  113. package/src/renderer/avatar/expressions.ts +165 -0
  114. package/src/renderer/avatar/eye-gaze.ts +255 -0
  115. package/src/renderer/avatar/eye-saccades.ts +133 -0
  116. package/src/renderer/avatar/hover-awareness.ts +125 -0
  117. package/src/renderer/avatar/ibl-enhancer.ts +163 -0
  118. package/src/renderer/avatar/lip-sync.ts +258 -0
  119. package/src/renderer/avatar/mixamo-retarget.ts +169 -0
  120. package/src/renderer/avatar/pixel-transparency.ts +65 -0
  121. package/src/renderer/avatar/scene.ts +70 -0
  122. package/src/renderer/avatar/spring-bones.ts +27 -0
  123. package/src/renderer/avatar/state-machine.ts +117 -0
  124. package/src/renderer/avatar/vrm-loader.ts +71 -0
  125. package/src/renderer/chat-window/chat-index.html +16 -0
  126. package/src/renderer/chat-window/chat-renderer.ts +28 -0
  127. package/src/renderer/index.html +63 -0
  128. package/src/renderer/renderer.ts +329 -0
  129. package/src/renderer/settings-window/settings-controls.ts +223 -0
  130. package/src/renderer/settings-window/settings-index.html +16 -0
  131. package/src/renderer/settings-window/settings-panel.ts +346 -0
  132. package/src/renderer/settings-window/settings-renderer.ts +5 -0
  133. package/src/renderer/styles/base.css +106 -0
  134. package/src/renderer/styles/chat.css +516 -0
  135. package/src/renderer/styles/components/button.css +221 -0
  136. package/src/renderer/styles/components/indicator.css +216 -0
  137. package/src/renderer/styles/components/input.css +139 -0
  138. package/src/renderer/styles/components/toast.css +204 -0
  139. package/src/renderer/styles/controls.css +279 -0
  140. package/src/renderer/styles/settings.css +310 -0
  141. package/src/renderer/styles/tokens.css +220 -0
  142. package/src/renderer/styles/utilities.css +349 -0
  143. package/src/renderer/types/avatar-bridge.d.ts +86 -0
  144. package/src/renderer/types/chat-bridge.d.ts +37 -0
  145. package/src/renderer/types/settings-bridge.d.ts +54 -0
  146. package/src/renderer/ui/chat-bubble.ts +435 -0
  147. package/src/renderer/ui/icons.ts +47 -0
  148. package/src/renderer/ui/typing-indicator.ts +41 -0
  149. package/src/service.ts +163 -0
  150. package/src/shared/config.ts +135 -0
  151. package/src/shared/ipc-channels.ts +81 -0
  152. package/src/shared/types.ts +7 -0
@@ -0,0 +1,223 @@
1
+ // Reusable form control factories for settings panel
2
+
3
+ function debounce(fn: (v: number) => void, ms: number): (v: number) => void {
4
+ let timer: ReturnType<typeof setTimeout> | null = null;
5
+ return (v: number) => {
6
+ if (timer) clearTimeout(timer);
7
+ timer = setTimeout(() => fn(v), ms);
8
+ };
9
+ }
10
+
11
+ export interface SliderHandle {
12
+ el: HTMLElement;
13
+ setValue(v: number): void;
14
+ }
15
+
16
+ export function createSlider(opts: {
17
+ min: number;
18
+ max: number;
19
+ step: number;
20
+ value: number;
21
+ label?: string;
22
+ debounceMs?: number;
23
+ onChange: (v: number) => void;
24
+ }): SliderHandle {
25
+ const el = document.createElement("div");
26
+ el.style.display = "flex";
27
+ el.style.alignItems = "center";
28
+ el.style.gap = "8px";
29
+ el.style.width = "100%";
30
+
31
+ const input = document.createElement("input");
32
+ input.type = "range";
33
+ input.className = "settings__range";
34
+ input.min = String(opts.min);
35
+ input.max = String(opts.max);
36
+ input.step = String(opts.step);
37
+ input.value = String(opts.value);
38
+ input.style.flex = "1";
39
+
40
+ const valueDisplay = document.createElement("span");
41
+ valueDisplay.className = "settings__value";
42
+ valueDisplay.style.minWidth = "36px";
43
+ valueDisplay.textContent = formatSliderValue(opts.value, opts.step);
44
+
45
+ const debouncedChange = opts.debounceMs
46
+ ? debounce(opts.onChange, opts.debounceMs)
47
+ : opts.onChange;
48
+
49
+ input.addEventListener("input", () => {
50
+ const v = parseFloat(input.value);
51
+ valueDisplay.textContent = formatSliderValue(v, opts.step);
52
+ debouncedChange(v);
53
+ });
54
+
55
+ el.appendChild(input);
56
+ el.appendChild(valueDisplay);
57
+
58
+ return {
59
+ el,
60
+ setValue(v: number) {
61
+ input.value = String(v);
62
+ valueDisplay.textContent = formatSliderValue(v, opts.step);
63
+ },
64
+ };
65
+ }
66
+
67
+ function formatSliderValue(v: number, step: number): string {
68
+ const decimals = step < 1 ? Math.max(1, String(step).split(".")[1]?.length ?? 1) : 0;
69
+ return v.toFixed(decimals);
70
+ }
71
+
72
+ export interface ToggleHandle {
73
+ el: HTMLElement;
74
+ setValue(v: boolean): void;
75
+ }
76
+
77
+ export function createToggle(opts: {
78
+ initial: boolean;
79
+ onChange: (v: boolean) => void;
80
+ }): ToggleHandle {
81
+ const el = document.createElement("div");
82
+ el.className = "settings__toggle";
83
+ if (opts.initial) el.classList.add("is-on");
84
+
85
+ let value = opts.initial;
86
+
87
+ el.addEventListener("click", () => {
88
+ value = !value;
89
+ el.classList.toggle("is-on", value);
90
+ opts.onChange(value);
91
+ });
92
+
93
+ return {
94
+ el,
95
+ setValue(v: boolean) {
96
+ value = v;
97
+ el.classList.toggle("is-on", v);
98
+ },
99
+ };
100
+ }
101
+
102
+ export interface SelectHandle {
103
+ el: HTMLSelectElement;
104
+ setValue(v: string): void;
105
+ }
106
+
107
+ export function createSelect(opts: {
108
+ options: { value: string; label: string }[];
109
+ selected: string;
110
+ onChange: (v: string) => void;
111
+ }): SelectHandle {
112
+ const el = document.createElement("select");
113
+ el.className = "settings__select";
114
+
115
+ for (const opt of opts.options) {
116
+ const option = document.createElement("option");
117
+ option.value = opt.value;
118
+ option.textContent = opt.label;
119
+ if (opt.value === opts.selected) option.selected = true;
120
+ el.appendChild(option);
121
+ }
122
+
123
+ el.addEventListener("change", () => {
124
+ opts.onChange(el.value);
125
+ });
126
+
127
+ return {
128
+ el,
129
+ setValue(v: string) {
130
+ el.value = v;
131
+ },
132
+ };
133
+ }
134
+
135
+ export interface RadioGroupHandle {
136
+ el: HTMLElement;
137
+ setValue(v: string): void;
138
+ }
139
+
140
+ export function createRadioGroup(opts: {
141
+ options: { value: string; label: string }[];
142
+ selected: string;
143
+ onChange: (v: string) => void;
144
+ }): RadioGroupHandle {
145
+ const el = document.createElement("div");
146
+ el.className = "settings__radio-group";
147
+ const buttons: Map<string, HTMLButtonElement> = new Map();
148
+
149
+ for (const opt of opts.options) {
150
+ const btn = document.createElement("button");
151
+ btn.type = "button";
152
+ btn.className = "settings__radio";
153
+ btn.textContent = opt.label;
154
+ btn.dataset.value = opt.value;
155
+ if (opt.value === opts.selected) btn.classList.add("is-selected");
156
+ btn.addEventListener("click", () => {
157
+ for (const b of buttons.values()) b.classList.remove("is-selected");
158
+ btn.classList.add("is-selected");
159
+ opts.onChange(opt.value);
160
+ });
161
+ buttons.set(opt.value, btn);
162
+ el.appendChild(btn);
163
+ }
164
+
165
+ return {
166
+ el,
167
+ setValue(v: string) {
168
+ for (const [value, btn] of buttons) {
169
+ btn.classList.toggle("is-selected", value === v);
170
+ }
171
+ },
172
+ };
173
+ }
174
+
175
+ export function createRow(label: string, control: HTMLElement): HTMLElement {
176
+ const row = document.createElement("div");
177
+ row.className = "settings__row";
178
+
179
+ const labelEl = document.createElement("span");
180
+ labelEl.className = "settings__label";
181
+ labelEl.textContent = label;
182
+
183
+ row.appendChild(labelEl);
184
+ row.appendChild(control);
185
+ return row;
186
+ }
187
+
188
+ export function createStackedRow(label: string, control: HTMLElement): HTMLElement {
189
+ const row = document.createElement("div");
190
+ row.className = "settings__row settings__row--stacked";
191
+
192
+ const labelEl = document.createElement("span");
193
+ labelEl.className = "settings__label";
194
+ labelEl.textContent = label;
195
+
196
+ row.appendChild(labelEl);
197
+ row.appendChild(control);
198
+ return row;
199
+ }
200
+
201
+ export function createSection(title: string): HTMLElement {
202
+ const section = document.createElement("div");
203
+ section.className = "settings__section";
204
+
205
+ const titleEl = document.createElement("div");
206
+ titleEl.className = "settings__section-title";
207
+ titleEl.textContent = title;
208
+
209
+ section.appendChild(titleEl);
210
+ return section;
211
+ }
212
+
213
+ export function createButton(label: string, opts?: {
214
+ variant?: "primary" | "secondary" | "ghost";
215
+ onClick?: () => void;
216
+ }): HTMLButtonElement {
217
+ const btn = document.createElement("button");
218
+ btn.type = "button";
219
+ btn.className = `btn btn--${opts?.variant ?? "secondary"}`;
220
+ btn.textContent = label;
221
+ if (opts?.onClick) btn.addEventListener("click", opts.onClick);
222
+ return btn;
223
+ }
@@ -0,0 +1,16 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta http-equiv="Content-Security-Policy"
6
+ content="default-src 'self' file: data: blob:; script-src 'self';
7
+ style-src 'self' 'unsafe-inline'; img-src 'self' file: data: blob:;
8
+ connect-src 'self' file: data: blob:" />
9
+ <title>OpenClaw Settings</title>
10
+ <link rel="stylesheet" href="./styles/settings.css" />
11
+ </head>
12
+ <body>
13
+ <div id="settings-root" class="settings"></div>
14
+ <script type="module" src="./settings-renderer.js"></script>
15
+ </body>
16
+ </html>
@@ -0,0 +1,346 @@
1
+ import {
2
+ createSlider,
3
+ createToggle,
4
+ createSelect,
5
+ createRadioGroup,
6
+ createRow,
7
+ createStackedRow,
8
+ createSection,
9
+ createButton,
10
+ } from "./settings-controls.js";
11
+
12
+ const TABS = ["Avatar", "Camera", "Voice", "Lighting", "Advanced"] as const;
13
+
14
+ export function createSettingsPanel(container: HTMLElement, bridge: SettingsBridge): void {
15
+ // Title bar
16
+ const titlebar = document.createElement("div");
17
+ titlebar.className = "settings__titlebar";
18
+
19
+ const titleText = document.createElement("span");
20
+ titleText.className = "settings__titlebar-text";
21
+ titleText.textContent = "Settings";
22
+
23
+ const closeBtn = document.createElement("button");
24
+ closeBtn.className = "settings__titlebar-close";
25
+ closeBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
26
+ <path d="M1 1L11 11M11 1L1 11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
27
+ </svg>`;
28
+ closeBtn.addEventListener("click", () => bridge.close());
29
+
30
+ titlebar.appendChild(titleText);
31
+ titlebar.appendChild(closeBtn);
32
+ container.appendChild(titlebar);
33
+
34
+ // Tab bar
35
+ const tabBar = document.createElement("div");
36
+ tabBar.className = "settings__tabs";
37
+
38
+ const panels: Map<string, HTMLElement> = new Map();
39
+ const tabButtons: Map<string, HTMLButtonElement> = new Map();
40
+
41
+ for (const tab of TABS) {
42
+ const btn = document.createElement("button");
43
+ btn.type = "button";
44
+ btn.className = "settings__tab";
45
+ btn.textContent = tab;
46
+ if (tab === TABS[0]) btn.classList.add("is-active");
47
+
48
+ btn.addEventListener("click", () => {
49
+ for (const b of tabButtons.values()) b.classList.remove("is-active");
50
+ btn.classList.add("is-active");
51
+ for (const [name, panel] of panels) {
52
+ panel.hidden = name !== tab;
53
+ }
54
+ });
55
+
56
+ tabButtons.set(tab, btn);
57
+ tabBar.appendChild(btn);
58
+ }
59
+ container.appendChild(tabBar);
60
+
61
+ // Create panels
62
+ for (const tab of TABS) {
63
+ const panel = document.createElement("div");
64
+ panel.className = "settings__panel";
65
+ panel.hidden = tab !== TABS[0];
66
+ panels.set(tab, panel);
67
+ container.appendChild(panel);
68
+ }
69
+
70
+ // ── Avatar Tab ──
71
+ const avatarPanel = panels.get("Avatar")!;
72
+ const avatarSection = createSection("Model");
73
+
74
+ const modelPathEl = document.createElement("div");
75
+ modelPathEl.className = "settings__model-path";
76
+ modelPathEl.textContent = "No model selected";
77
+ avatarSection.appendChild(modelPathEl);
78
+
79
+ const changeModelBtn = createButton("Change Model\u2026", {
80
+ variant: "secondary",
81
+ onClick: async () => {
82
+ const path = await bridge.pickVrmFile();
83
+ if (path) {
84
+ modelPathEl.textContent = formatPath(path);
85
+ modelPathEl.title = path;
86
+ }
87
+ },
88
+ });
89
+ avatarSection.appendChild(changeModelBtn);
90
+
91
+ avatarPanel.appendChild(avatarSection);
92
+
93
+ const scaleSection = createSection("Scale");
94
+ const scaleSlider = createSlider({
95
+ min: 0.5, max: 2.0, step: 0.1, value: 1.0,
96
+ debounceMs: 50,
97
+ onChange: (v) => bridge.setScale(v),
98
+ });
99
+ scaleSection.appendChild(createRow("Avatar Scale", scaleSlider.el));
100
+ avatarPanel.appendChild(scaleSection);
101
+
102
+ // ── Camera Tab ──
103
+ const cameraPanel = panels.get("Camera")!;
104
+ const framingSection = createSection("Framing");
105
+ const framingRadio = createRadioGroup({
106
+ options: [
107
+ { value: "0.6", label: "Head" },
108
+ { value: "1.5", label: "Upper Body" },
109
+ { value: "4.0", label: "Full Body" },
110
+ ],
111
+ selected: "1.5",
112
+ onChange: (v) => bridge.setCameraZoom(parseFloat(v)),
113
+ });
114
+ framingSection.appendChild(framingRadio.el);
115
+ cameraPanel.appendChild(framingSection);
116
+
117
+ const zoomSection = createSection("Zoom");
118
+ const zoomSlider = createSlider({
119
+ min: 0.5, max: 6.0, step: 0.2, value: 3.0,
120
+ debounceMs: 50,
121
+ onChange: (v) => bridge.setCameraZoom(v),
122
+ });
123
+ zoomSection.appendChild(createRow("Camera Zoom", zoomSlider.el));
124
+ cameraPanel.appendChild(zoomSection);
125
+
126
+ // ── Voice Tab ──
127
+ const voicePanel = panels.get("Voice")!;
128
+ const ttsSection = createSection("Text-to-Speech");
129
+
130
+ const ttsToggle = createToggle({
131
+ initial: false,
132
+ onChange: (v) => bridge.setTtsEnabled(v),
133
+ });
134
+ ttsSection.appendChild(createRow("Enable TTS", ttsToggle.el));
135
+
136
+ const engineSelect = createSelect({
137
+ options: [
138
+ { value: "web-speech", label: "Web Speech (System)" },
139
+ { value: "kokoro", label: "Kokoro (Local AI)" },
140
+ ],
141
+ selected: "web-speech",
142
+ onChange: (v) => bridge.setTtsEngine(v as "web-speech" | "kokoro"),
143
+ });
144
+ ttsSection.appendChild(createRow("Engine", engineSelect.el));
145
+
146
+ const voiceInput = document.createElement("input");
147
+ voiceInput.type = "text";
148
+ voiceInput.className = "settings__text-input";
149
+ voiceInput.placeholder = "Voice name (optional)";
150
+ let voiceDebounce: ReturnType<typeof setTimeout> | null = null;
151
+ voiceInput.addEventListener("input", () => {
152
+ if (voiceDebounce) clearTimeout(voiceDebounce);
153
+ voiceDebounce = setTimeout(() => bridge.setTtsVoice(voiceInput.value), 300);
154
+ });
155
+ ttsSection.appendChild(createStackedRow("Voice", voiceInput));
156
+
157
+ voicePanel.appendChild(ttsSection);
158
+
159
+ // ── Lighting Tab ──
160
+ const lightingPanel = panels.get("Lighting")!;
161
+ const profileSection = createSection("Profile");
162
+
163
+ const lightingRadio = createRadioGroup({
164
+ options: [
165
+ { value: "studio", label: "Studio" },
166
+ { value: "warm", label: "Warm" },
167
+ { value: "cool", label: "Cool" },
168
+ { value: "neutral", label: "Neutral" },
169
+ { value: "custom", label: "Custom" },
170
+ ],
171
+ selected: "studio",
172
+ onChange: (v) => {
173
+ bridge.setLightingProfile(v);
174
+ customControls.style.display = v === "custom" ? "block" : "none";
175
+ },
176
+ });
177
+ profileSection.appendChild(lightingRadio.el);
178
+ lightingPanel.appendChild(profileSection);
179
+
180
+ // Custom lighting controls (hidden by default)
181
+ const customControls = document.createElement("div");
182
+ customControls.style.display = "none";
183
+
184
+ const customSection = createSection("Custom Settings");
185
+ const intensitySlider = createSlider({
186
+ min: 0, max: 2, step: 0.1, value: 0.3,
187
+ debounceMs: 50,
188
+ onChange: () => sendCustomLighting(),
189
+ });
190
+ customSection.appendChild(createRow("Intensity", intensitySlider.el));
191
+
192
+ const ambientSlider = createSlider({
193
+ min: 0, max: 1, step: 0.05, value: 0.5,
194
+ debounceMs: 50,
195
+ onChange: () => sendCustomLighting(),
196
+ });
197
+ customSection.appendChild(createRow("Ambient", ambientSlider.el));
198
+
199
+ const colorInput = document.createElement("input");
200
+ colorInput.type = "color";
201
+ colorInput.className = "settings__color-input";
202
+ colorInput.value = "#ffffff";
203
+ colorInput.addEventListener("input", () => sendCustomLighting());
204
+ customSection.appendChild(createRow("Color", colorInput));
205
+
206
+ customControls.appendChild(customSection);
207
+ lightingPanel.appendChild(customControls);
208
+
209
+ function sendCustomLighting(): void {
210
+ bridge.setLightingCustom({
211
+ intensity: parseFloat((intensitySlider.el.querySelector("input") as HTMLInputElement).value),
212
+ color: colorInput.value,
213
+ ambient: parseFloat((ambientSlider.el.querySelector("input") as HTMLInputElement).value),
214
+ });
215
+ }
216
+
217
+ // ── Advanced Tab ──
218
+ const advancedPanel = panels.get("Advanced")!;
219
+
220
+ const opacitySection = createSection("Appearance");
221
+ const opacitySlider = createSlider({
222
+ min: 0.3, max: 1.0, step: 0.05, value: 1.0,
223
+ debounceMs: 50,
224
+ onChange: (v) => bridge.setOpacity(v),
225
+ });
226
+ opacitySection.appendChild(createRow("Opacity", opacitySlider.el));
227
+ advancedPanel.appendChild(opacitySection);
228
+
229
+ const timeoutSection = createSection("Chat");
230
+ const timeoutSelect = createSelect({
231
+ options: [
232
+ { value: "5000", label: "5 seconds" },
233
+ { value: "10000", label: "10 seconds" },
234
+ { value: "30000", label: "30 seconds" },
235
+ { value: "0", label: "Never" },
236
+ ],
237
+ selected: "10000",
238
+ onChange: (v) => bridge.setIdleTimeout(parseInt(v, 10)),
239
+ });
240
+ timeoutSection.appendChild(createRow("Auto-hide", timeoutSelect.el));
241
+ advancedPanel.appendChild(timeoutSection);
242
+
243
+ const positionSection = createSection("Position");
244
+ const snapGrid = document.createElement("div");
245
+ snapGrid.style.display = "grid";
246
+ snapGrid.style.gridTemplateColumns = "1fr 1fr";
247
+ snapGrid.style.gap = "6px";
248
+
249
+ const corners = [
250
+ { value: "topLeft", label: "Top Left" },
251
+ { value: "topRight", label: "Top Right" },
252
+ { value: "bottomLeft", label: "Bottom Left" },
253
+ { value: "bottomRight", label: "Bottom Right" },
254
+ ] as const;
255
+
256
+ for (const corner of corners) {
257
+ const btn = createButton(corner.label, {
258
+ variant: "secondary",
259
+ onClick: () => bridge.snapTo(corner.value),
260
+ });
261
+ snapGrid.appendChild(btn);
262
+ }
263
+ positionSection.appendChild(snapGrid);
264
+ advancedPanel.appendChild(positionSection);
265
+
266
+ // Clear chat
267
+ const chatActionsSection = createSection("Actions");
268
+ const clearChatBtn = createButton("Clear Chat History", {
269
+ variant: "secondary",
270
+ onClick: () => bridge.clearChat(),
271
+ });
272
+ chatActionsSection.appendChild(clearChatBtn);
273
+ advancedPanel.appendChild(chatActionsSection);
274
+
275
+ // ── Init: Populate controls from persisted settings ──
276
+ bridge.getSettings().then((settings) => {
277
+ // Avatar tab
278
+ if (settings.vrmModelPath) {
279
+ modelPathEl.textContent = formatPath(settings.vrmModelPath);
280
+ modelPathEl.title = settings.vrmModelPath;
281
+ }
282
+ scaleSlider.setValue(settings.scale);
283
+
284
+ // Camera tab
285
+ zoomSlider.setValue(settings.zoom);
286
+ // Match closest framing preset
287
+ const presets = [0.6, 1.5, 4.0];
288
+ const closest = presets.reduce((a, b) =>
289
+ Math.abs(b - settings.zoom) < Math.abs(a - settings.zoom) ? b : a,
290
+ );
291
+ framingRadio.setValue(String(closest));
292
+
293
+ // Voice tab
294
+ ttsToggle.setValue(settings.ttsEnabled);
295
+ engineSelect.setValue(settings.ttsEngine);
296
+ voiceInput.value = settings.ttsVoice || "";
297
+
298
+ // Lighting tab
299
+ lightingRadio.setValue(settings.lightingProfile);
300
+ customControls.style.display = settings.lightingProfile === "custom" ? "block" : "none";
301
+ if (settings.lightingCustom) {
302
+ intensitySlider.setValue(settings.lightingCustom.intensity);
303
+ ambientSlider.setValue(settings.lightingCustom.ambient);
304
+ colorInput.value = settings.lightingCustom.color;
305
+ }
306
+
307
+ // Advanced tab
308
+ opacitySlider.setValue(settings.opacity);
309
+ timeoutSelect.setValue(String(settings.idleTimeoutMs));
310
+ });
311
+
312
+ // ── Live sync: Update controls from external changes ──
313
+ bridge.onOpacityChanged((v) => opacitySlider.setValue(v));
314
+ bridge.onScaleChanged((v) => scaleSlider.setValue(v));
315
+ bridge.onCameraZoomChanged((v) => {
316
+ zoomSlider.setValue(v);
317
+ const presets = [0.6, 1.5, 4.0];
318
+ const closest = presets.reduce((a, b) =>
319
+ Math.abs(b - v) < Math.abs(a - v) ? b : a,
320
+ );
321
+ framingRadio.setValue(String(closest));
322
+ });
323
+ bridge.onTtsEnabledChanged((v) => ttsToggle.setValue(v));
324
+ bridge.onTtsEngineChanged((v) => engineSelect.setValue(v));
325
+ bridge.onTtsVoiceChanged((v) => { voiceInput.value = v; });
326
+ bridge.onIdleTimeoutChanged((ms) => timeoutSelect.setValue(String(ms)));
327
+ bridge.onLightingProfileChanged((v) => {
328
+ lightingRadio.setValue(v);
329
+ customControls.style.display = v === "custom" ? "block" : "none";
330
+ });
331
+ bridge.onLightingCustomChanged((v) => {
332
+ intensitySlider.setValue(v.intensity);
333
+ ambientSlider.setValue(v.ambient);
334
+ colorInput.value = v.color;
335
+ });
336
+ bridge.onVrmModelChanged((path) => {
337
+ modelPathEl.textContent = formatPath(path);
338
+ modelPathEl.title = path;
339
+ });
340
+ }
341
+
342
+ function formatPath(fullPath: string): string {
343
+ const parts = fullPath.replace(/\\/g, "/").split("/");
344
+ const filename = parts[parts.length - 1] || fullPath;
345
+ return filename;
346
+ }
@@ -0,0 +1,5 @@
1
+ import { createSettingsPanel } from "./settings-panel.js";
2
+
3
+ const bridge = window.settingsBridge;
4
+ const root = document.getElementById("settings-root")!;
5
+ createSettingsPanel(root, bridge);
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Base styles - Reset and foundation
3
+ * Provides consistent cross-browser defaults
4
+ */
5
+
6
+ /* Box-sizing reset */
7
+ *,
8
+ *::before,
9
+ *::after {
10
+ box-sizing: border-box;
11
+ }
12
+
13
+ /* Remove default margins */
14
+ * {
15
+ margin: 0;
16
+ padding: 0;
17
+ }
18
+
19
+ /* Core document styles */
20
+ html {
21
+ -webkit-text-size-adjust: 100%;
22
+ -webkit-font-smoothing: antialiased;
23
+ -moz-osx-font-smoothing: grayscale;
24
+ text-rendering: optimizeLegibility;
25
+ }
26
+
27
+ html,
28
+ body {
29
+ width: 100%;
30
+ height: 100%;
31
+ overflow: hidden;
32
+ background: transparent;
33
+ }
34
+
35
+ body {
36
+ font-family: var(--font-sans);
37
+ font-size: var(--font-size-md);
38
+ line-height: var(--line-height-normal);
39
+ color: var(--text-primary);
40
+ }
41
+
42
+ /* Screen reader only utility */
43
+ .sr-only {
44
+ position: absolute;
45
+ width: 1px;
46
+ height: 1px;
47
+ padding: 0;
48
+ margin: -1px;
49
+ overflow: hidden;
50
+ clip: rect(0, 0, 0, 0);
51
+ white-space: nowrap;
52
+ border: 0;
53
+ }
54
+
55
+ /* Remove default button styles */
56
+ button {
57
+ font: inherit;
58
+ color: inherit;
59
+ background: none;
60
+ border: none;
61
+ cursor: pointer;
62
+ }
63
+
64
+ button:disabled {
65
+ cursor: not-allowed;
66
+ }
67
+
68
+ /* Remove default input styles */
69
+ input,
70
+ textarea {
71
+ font: inherit;
72
+ color: inherit;
73
+ background: none;
74
+ border: none;
75
+ }
76
+
77
+ input:focus,
78
+ textarea:focus,
79
+ button:focus {
80
+ outline: none;
81
+ }
82
+
83
+ /* Scrollbar styling */
84
+ ::-webkit-scrollbar {
85
+ width: 6px;
86
+ height: 6px;
87
+ }
88
+
89
+ ::-webkit-scrollbar-track {
90
+ background: transparent;
91
+ }
92
+
93
+ ::-webkit-scrollbar-thumb {
94
+ background: var(--scrollbar-thumb);
95
+ border-radius: 3px;
96
+ }
97
+
98
+ ::-webkit-scrollbar-thumb:hover {
99
+ background: var(--scrollbar-thumb-hover);
100
+ }
101
+
102
+ /* Firefox scrollbar */
103
+ * {
104
+ scrollbar-width: thin;
105
+ scrollbar-color: var(--scrollbar-thumb) transparent;
106
+ }