clawport-ui 0.1.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.
Files changed (132) hide show
  1. package/.env.example +35 -0
  2. package/BRANDING.md +131 -0
  3. package/CLAUDE.md +252 -0
  4. package/README.md +262 -0
  5. package/SETUP.md +337 -0
  6. package/app/agents/[id]/page.tsx +727 -0
  7. package/app/api/agents/route.ts +12 -0
  8. package/app/api/chat/[id]/route.ts +139 -0
  9. package/app/api/cron-runs/route.ts +13 -0
  10. package/app/api/crons/route.ts +12 -0
  11. package/app/api/kanban/chat/[id]/route.ts +119 -0
  12. package/app/api/kanban/chat-history/[ticketId]/route.ts +36 -0
  13. package/app/api/memory/route.ts +12 -0
  14. package/app/api/transcribe/route.ts +37 -0
  15. package/app/api/tts/route.ts +42 -0
  16. package/app/chat/[id]/page.tsx +10 -0
  17. package/app/chat/page.tsx +200 -0
  18. package/app/crons/page.tsx +870 -0
  19. package/app/docs/page.tsx +399 -0
  20. package/app/favicon.ico +0 -0
  21. package/app/globals.css +692 -0
  22. package/app/kanban/page.tsx +327 -0
  23. package/app/layout.tsx +45 -0
  24. package/app/memory/page.tsx +685 -0
  25. package/app/page.tsx +817 -0
  26. package/app/providers.tsx +37 -0
  27. package/app/settings/page.tsx +901 -0
  28. package/app/settings-provider.tsx +209 -0
  29. package/components/AgentAvatar.tsx +54 -0
  30. package/components/AgentNode.tsx +122 -0
  31. package/components/Breadcrumbs.tsx +126 -0
  32. package/components/DynamicFavicon.tsx +62 -0
  33. package/components/ErrorState.tsx +97 -0
  34. package/components/FeedView.tsx +494 -0
  35. package/components/GlobalSearch.tsx +571 -0
  36. package/components/GridView.tsx +532 -0
  37. package/components/ManorMap.tsx +157 -0
  38. package/components/MobileSidebar.tsx +251 -0
  39. package/components/NavLinks.tsx +271 -0
  40. package/components/OnboardingWizard.tsx +1067 -0
  41. package/components/Sidebar.tsx +115 -0
  42. package/components/ThemeToggle.tsx +108 -0
  43. package/components/chat/AgentList.tsx +537 -0
  44. package/components/chat/ConversationView.tsx +1047 -0
  45. package/components/chat/FileAttachment.tsx +140 -0
  46. package/components/chat/MediaPreview.tsx +111 -0
  47. package/components/chat/VoiceMessage.tsx +139 -0
  48. package/components/crons/PipelineGraph.tsx +327 -0
  49. package/components/crons/WeeklySchedule.tsx +630 -0
  50. package/components/docs/AgentsSection.tsx +209 -0
  51. package/components/docs/ApiReferenceSection.tsx +256 -0
  52. package/components/docs/ArchitectureSection.tsx +221 -0
  53. package/components/docs/ComponentsSection.tsx +253 -0
  54. package/components/docs/CronSystemSection.tsx +235 -0
  55. package/components/docs/DocSection.tsx +346 -0
  56. package/components/docs/GettingStartedSection.tsx +169 -0
  57. package/components/docs/ThemingSection.tsx +257 -0
  58. package/components/docs/TroubleshootingSection.tsx +200 -0
  59. package/components/kanban/AgentPicker.tsx +321 -0
  60. package/components/kanban/CreateTicketModal.tsx +333 -0
  61. package/components/kanban/KanbanBoard.tsx +70 -0
  62. package/components/kanban/KanbanColumn.tsx +166 -0
  63. package/components/kanban/TicketCard.tsx +245 -0
  64. package/components/kanban/TicketDetailPanel.tsx +850 -0
  65. package/components/ui/badge.tsx +48 -0
  66. package/components/ui/button.tsx +64 -0
  67. package/components/ui/card.tsx +92 -0
  68. package/components/ui/dialog.tsx +158 -0
  69. package/components/ui/scroll-area.tsx +58 -0
  70. package/components/ui/separator.tsx +28 -0
  71. package/components/ui/skeleton.tsx +27 -0
  72. package/components/ui/tabs.tsx +91 -0
  73. package/components/ui/tooltip.tsx +57 -0
  74. package/components.json +23 -0
  75. package/docs/API.md +648 -0
  76. package/docs/COMPONENTS.md +1059 -0
  77. package/docs/THEMING.md +795 -0
  78. package/lib/agents-registry.ts +35 -0
  79. package/lib/agents.json +282 -0
  80. package/lib/agents.test.ts +367 -0
  81. package/lib/agents.ts +32 -0
  82. package/lib/anthropic.test.ts +422 -0
  83. package/lib/anthropic.ts +220 -0
  84. package/lib/api-error.ts +16 -0
  85. package/lib/audio-recorder.test.ts +72 -0
  86. package/lib/audio-recorder.ts +169 -0
  87. package/lib/conversations.test.ts +331 -0
  88. package/lib/conversations.ts +117 -0
  89. package/lib/cron-pipelines.test.ts +69 -0
  90. package/lib/cron-pipelines.ts +58 -0
  91. package/lib/cron-runs.test.ts +118 -0
  92. package/lib/cron-runs.ts +67 -0
  93. package/lib/cron-utils.test.ts +222 -0
  94. package/lib/cron-utils.ts +160 -0
  95. package/lib/crons.test.ts +502 -0
  96. package/lib/crons.ts +114 -0
  97. package/lib/env.test.ts +44 -0
  98. package/lib/env.ts +14 -0
  99. package/lib/kanban/automation.test.ts +245 -0
  100. package/lib/kanban/automation.ts +143 -0
  101. package/lib/kanban/chat-store.test.ts +149 -0
  102. package/lib/kanban/chat-store.ts +81 -0
  103. package/lib/kanban/store.test.ts +238 -0
  104. package/lib/kanban/store.ts +98 -0
  105. package/lib/kanban/types.ts +50 -0
  106. package/lib/kanban/useAgentWork.ts +78 -0
  107. package/lib/memory.ts +45 -0
  108. package/lib/multimodal.test.ts +219 -0
  109. package/lib/multimodal.ts +68 -0
  110. package/lib/pipeline.integration.test.ts +343 -0
  111. package/lib/sanitize.ts +194 -0
  112. package/lib/settings.test.ts +137 -0
  113. package/lib/settings.ts +94 -0
  114. package/lib/styles.ts +24 -0
  115. package/lib/themes.ts +9 -0
  116. package/lib/transcribe.test.ts +141 -0
  117. package/lib/transcribe.ts +111 -0
  118. package/lib/types.ts +66 -0
  119. package/lib/utils.ts +6 -0
  120. package/lib/validation.test.ts +132 -0
  121. package/lib/validation.ts +80 -0
  122. package/next.config.ts +7 -0
  123. package/package.json +56 -0
  124. package/postcss.config.mjs +7 -0
  125. package/public/file.svg +1 -0
  126. package/public/globe.svg +1 -0
  127. package/public/next.svg +1 -0
  128. package/public/vercel.svg +1 -0
  129. package/public/window.svg +1 -0
  130. package/scripts/setup.mjs +215 -0
  131. package/tsconfig.json +34 -0
  132. package/vitest.config.ts +17 -0
@@ -0,0 +1,795 @@
1
+ # ClawPort -- Theming, Settings & Customization Guide
2
+
3
+ This document covers ClawPort's visual theming system, settings architecture, and step-by-step
4
+ instructions for extending both. Everything is driven by CSS custom properties and two React
5
+ context providers: `ThemeProvider` and `SettingsProvider`.
6
+
7
+ ---
8
+
9
+ ## Table of Contents
10
+
11
+ 1. [Themes](#themes)
12
+ - [Available Themes](#available-themes)
13
+ - [How Themes Work](#how-themes-work)
14
+ - [CSS Custom Property Tokens](#css-custom-property-tokens)
15
+ - [System Theme Detection](#system-theme-detection)
16
+ - [Theme-Specific Overrides](#theme-specific-overrides)
17
+ - [How to Add a New Theme](#how-to-add-a-new-theme)
18
+ 2. [Settings](#settings)
19
+ - [ClawPortSettings Interface](#clawportsettings-interface)
20
+ - [localStorage Persistence](#localstorage-persistence)
21
+ - [SettingsProvider API](#settingsprovider-api)
22
+ - [Accent Color CSS Variables](#accent-color-css-variables)
23
+ - [Agent Override System](#agent-override-system)
24
+ - [operatorName Flow](#operatorname-flow)
25
+ 3. [Customization Guide](#customization-guide)
26
+ - [Change the Default Accent Color](#change-the-default-accent-color)
27
+ - [Add a New Setting Field](#add-a-new-setting-field)
28
+ - [Add a New Theme](#add-a-new-theme-step-by-step)
29
+ - [CSS Custom Property Naming Conventions](#css-custom-property-naming-conventions)
30
+
31
+ ---
32
+
33
+ ## Themes
34
+
35
+ ### Available Themes
36
+
37
+ ClawPort ships with five themes. Each has an ID, a human-readable label, and an emoji used in the
38
+ onboarding wizard and theme picker.
39
+
40
+ | ID | Label | Emoji | Description |
41
+ |----------|----------|-------|--------------------------------------------|
42
+ | `dark` | Dark | `\ud83c\udf11` | Apple Dark Mode. The default theme. |
43
+ | `glass` | Glass | `\ud83e\ude9f` | Frosted glass dark variant with translucent surfaces. |
44
+ | `color` | Color | `\ud83c\udfa8` | Vibrant purple-indigo variant. |
45
+ | `light` | Light | `\u2600\ufe0f` | Apple Light Mode. |
46
+ | `system` | System | `\u2699\ufe0f` | Follows the OS `prefers-color-scheme` setting. |
47
+
48
+ These are defined in `lib/themes.ts`:
49
+
50
+ ```ts
51
+ export type ThemeId = 'dark' | 'glass' | 'color' | 'light' | 'system';
52
+
53
+ export const THEMES: { id: ThemeId; label: string; emoji: string }[] = [
54
+ { id: 'dark', label: 'Dark', emoji: '\ud83c\udf11' },
55
+ { id: 'glass', label: 'Glass', emoji: '\ud83e\ude9f' },
56
+ { id: 'color', label: 'Color', emoji: '\ud83c\udfa8' },
57
+ { id: 'light', label: 'Light', emoji: '\u2600\ufe0f' },
58
+ { id: 'system', label: 'System', emoji: '\u2699\ufe0f' },
59
+ ];
60
+ ```
61
+
62
+ ### How Themes Work
63
+
64
+ The theme system uses three layers:
65
+
66
+ 1. **`data-theme` attribute on `<html>`** -- Each theme defines a CSS rule block scoped to
67
+ `[data-theme="<id>"]`. The `dark` theme also matches `:root` so it works without any
68
+ attribute set.
69
+
70
+ 2. **CSS custom properties** -- Every color, shadow, radius, and material is expressed as a
71
+ CSS variable. Components consume these via inline styles (e.g., `style={{ color: 'var(--text-primary)' }}`)
72
+ or utility classes. No Tailwind color classes are used directly.
73
+
74
+ 3. **ThemeProvider** (`app/providers.tsx`) -- A React context that manages theme state. On mount
75
+ it reads from `localStorage` key `clawport-theme`. When the user picks a theme, it:
76
+ - Updates React state
77
+ - Writes to `localStorage`
78
+ - Removes the existing `data-theme` attribute
79
+ - Sets the new `data-theme` attribute on `<html>`
80
+ - For the `system` theme, evaluates `window.matchMedia('(prefers-color-scheme: dark)')` and
81
+ resolves to either `dark` or `light`
82
+
83
+ ```ts
84
+ // app/providers.tsx (simplified)
85
+ function apply(t: ThemeId) {
86
+ setThemeState(t);
87
+ localStorage.setItem('clawport-theme', t);
88
+ const html = document.documentElement;
89
+ html.removeAttribute('data-theme');
90
+ if (t === 'system') {
91
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
92
+ html.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
93
+ } else {
94
+ html.setAttribute('data-theme', t);
95
+ }
96
+ }
97
+ ```
98
+
99
+ Consumer hook:
100
+
101
+ ```ts
102
+ import { useTheme } from '@/app/providers';
103
+
104
+ const { theme, setTheme } = useTheme();
105
+ // theme is the ThemeId ('dark', 'glass', etc.)
106
+ // setTheme('light') applies immediately
107
+ ```
108
+
109
+ ### CSS Custom Property Tokens
110
+
111
+ All tokens are defined in `app/globals.css`. Every theme block defines the same full set of
112
+ variables, so components can always rely on them being present.
113
+
114
+ #### Backgrounds
115
+
116
+ | Token | Purpose | Dark example |
117
+ |------------------------|--------------------------------------------|---------------------------|
118
+ | `--bg` | Primary page background | `#000000` |
119
+ | `--bg-secondary` | Card / surface background | `rgba(28,28,30,1)` |
120
+ | `--bg-tertiary` | Nested surface / grouped background | `rgba(44,44,46,1)` |
121
+
122
+ #### Materials (Apple translucent surfaces)
123
+
124
+ | Token | Purpose | Dark example |
125
+ |-------------------------|-------------------------------------------|---------------------------|
126
+ | `--material-regular` | Standard material (sidebar, overlays) | `rgba(28,28,30,0.92)` |
127
+ | `--material-thick` | Dense material | `rgba(22,22,24,0.96)` |
128
+ | `--material-thin` | Light tint material | `rgba(255,255,255,0.06)` |
129
+ | `--material-ultra-thin` | Very subtle tint | `rgba(255,255,255,0.04)` |
130
+
131
+ #### Fills
132
+
133
+ | Token | Purpose | Dark example |
134
+ |----------------------|-----------------------------------------------|---------------------------|
135
+ | `--fill-primary` | Primary interactive fill (buttons, controls) | `rgba(120,120,128,0.36)` |
136
+ | `--fill-secondary` | Hover fill | `rgba(120,120,128,0.32)` |
137
+ | `--fill-tertiary` | Subtle fill (input backgrounds) | `rgba(118,118,128,0.24)` |
138
+ | `--fill-quaternary` | Most subtle fill | `rgba(118,118,128,0.18)` |
139
+
140
+ #### Separators & Borders
141
+
142
+ | Token | Purpose | Dark example |
143
+ |----------------------|-----------------------------------|---------------------------|
144
+ | `--separator` | Translucent divider line | `rgba(84,84,88,0.60)` |
145
+ | `--separator-opaque` | Opaque divider (non-blur contexts)| `#38383A` |
146
+
147
+ #### Text
148
+
149
+ | Token | Purpose | Dark example |
150
+ |----------------------|--------------------------------------|----------------------------|
151
+ | `--text-primary` | Headings, body text | `#FFFFFF` |
152
+ | `--text-secondary` | Labels, supporting text | `rgba(235,235,245,0.60)` |
153
+ | `--text-tertiary` | Placeholder, captions | `rgba(235,235,245,0.30)` |
154
+ | `--text-quaternary` | Disabled / lowest-priority text | `rgba(235,235,245,0.18)` |
155
+
156
+ #### Accent & System Colors
157
+
158
+ | Token | Purpose | Dark example |
159
+ |--------------------|----------------------------------------|---------------|
160
+ | `--accent` | Primary brand accent (buttons, active) | `#F5C518` |
161
+ | `--accent-fill` | Accent at 15% opacity (backgrounds) | `rgba(245,197,24,0.15)` |
162
+ | `--system-blue` | Links, focus rings | `#0A84FF` |
163
+ | `--system-green` | Success, active toggles | `#30D158` |
164
+ | `--system-red` | Errors, destructive actions | `#FF453A` |
165
+ | `--system-orange` | Warnings | `#FF9F0A` |
166
+ | `--system-purple` | Tags, highlights | `#BF5AF2` |
167
+
168
+ Note: `--accent` and `--accent-fill` can be overridden at runtime by the SettingsProvider when
169
+ the user picks a custom accent color. See [Accent Color CSS Variables](#accent-color-css-variables).
170
+
171
+ #### Shadows & Effects
172
+
173
+ | Token | Purpose | Dark example (abbreviated) |
174
+ |--------------------|-----------------------------------------|--------------------------------------|
175
+ | `--inset-shine` | Top inner highlight on cards | `inset 0 1px 0 rgba(255,255,255,0.08)` |
176
+ | `--shadow-subtle` | Minimal elevation | `0 1px 2px rgba(0,0,0,0.20)` |
177
+ | `--shadow-ambient` | Hairline border shadow | `0 0 0 0.5px rgba(0,0,0,0.20)` |
178
+ | `--shadow-key` | Primary directional shadow | `0 4px 16px rgba(0,0,0,0.40)` |
179
+ | `--shadow-card` | Full card elevation (ambient + key + shine) | _(composite)_ |
180
+ | `--shadow-overlay` | Modal / overlay elevation | _(composite)_ |
181
+
182
+ #### Code Blocks
183
+
184
+ | Token | Purpose | Dark example |
185
+ |-----------------|--------------------------|---------------------------|
186
+ | `--code-bg` | Code block background | `rgba(255,255,255,0.06)` |
187
+ | `--code-border` | Code block border | `rgba(255,255,255,0.10)` |
188
+ | `--code-text` | Code text color | `#e5e5ea` |
189
+
190
+ #### Sidebar
191
+
192
+ | Token | Purpose | Dark example |
193
+ |----------------------|----------------------------------|------------------------------------|
194
+ | `--sidebar-bg` | Sidebar background color | `rgba(28,28,30,0.92)` |
195
+ | `--sidebar-backdrop` | Sidebar backdrop-filter | `blur(40px) saturate(180%)` |
196
+
197
+ #### Border Radius
198
+
199
+ | Token | Value |
200
+ |----------------|---------|
201
+ | `--radius-sm` | `6px` |
202
+ | `--radius-md` | `12px` |
203
+ | `--radius-lg` | `16px` |
204
+ | `--radius-xl` | `20px` |
205
+ | `--radius-2xl` | `24px` |
206
+
207
+ #### Easing Functions
208
+
209
+ | Token | Value | Use case |
210
+ |-----------------|-------------------------------------|-----------------------------|
211
+ | `--ease-spring` | `cubic-bezier(0.34, 1.56, 0.64, 1)` | Bouncy interactions |
212
+ | `--ease-smooth` | `cubic-bezier(0.4, 0, 0.2, 1)` | General transitions |
213
+ | `--ease-snappy` | `cubic-bezier(0.2, 0, 0, 1)` | Quick state changes |
214
+
215
+ #### Typography Scale (Tailwind `@theme` tokens)
216
+
217
+ These are registered as Tailwind theme extensions so they work with utility classes (e.g.,
218
+ `text-caption1`). They follow the Apple Human Interface Guidelines type ramp.
219
+
220
+ | Token | Value |
221
+ |------------------------|--------|
222
+ | `--text-caption2` | `11px` |
223
+ | `--text-caption1` | `12px` |
224
+ | `--text-footnote` | `13px` |
225
+ | `--text-subheadline` | `15px` |
226
+ | `--text-body` | `17px` |
227
+ | `--text-title3` | `20px` |
228
+ | `--text-title2` | `22px` |
229
+ | `--text-title1` | `28px` |
230
+ | `--text-large-title` | `34px` |
231
+
232
+ #### Font Families
233
+
234
+ | Token | Value |
235
+ |---------------|--------------------------------------------------------------------------|
236
+ | `--font-sans` | `-apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", system-ui, sans-serif` |
237
+ | `--font-mono` | `"SF Mono", Monaco, Menlo, "Courier New", monospace` |
238
+
239
+ #### Leading (Line Height)
240
+
241
+ | Token | Value |
242
+ |--------------------|--------|
243
+ | `--leading-tight` | `1.15` |
244
+ | `--leading-snug` | `1.3` |
245
+ | `--leading-normal` | `1.47` |
246
+ | `--leading-relaxed`| `1.65` |
247
+
248
+ #### Tracking (Letter Spacing)
249
+
250
+ | Token | Value |
251
+ |--------------------|-----------|
252
+ | `--tracking-tight` | `-0.41px` |
253
+ | `--tracking-normal`| `-0.24px` |
254
+ | `--tracking-wide` | `0.07em` |
255
+
256
+ #### Font Weights
257
+
258
+ | Token | Value |
259
+ |--------------------|-------|
260
+ | `--weight-regular` | `400` |
261
+ | `--weight-medium` | `500` |
262
+ | `--weight-semibold`| `600` |
263
+ | `--weight-bold` | `700` |
264
+
265
+ #### Spacing Scale (4px grid)
266
+
267
+ | Token | Value |
268
+ |-------------|--------|
269
+ | `--space-1` | `4px` |
270
+ | `--space-2` | `8px` |
271
+ | `--space-3` | `12px` |
272
+ | `--space-4` | `16px` |
273
+ | `--space-5` | `20px` |
274
+ | `--space-6` | `24px` |
275
+ | `--space-8` | `32px` |
276
+ | `--space-10`| `40px` |
277
+ | `--space-12`| `48px` |
278
+ | `--space-16`| `64px` |
279
+
280
+ #### Animation Tokens (Tailwind `@theme`)
281
+
282
+ | Token | Value |
283
+ |-------------------------|-------------------------------------------|
284
+ | `--animate-fade-in` | `fadeIn 0.2s ease-out` |
285
+ | `--animate-slide-in` | `slideIn 0.2s ease-out` |
286
+ | `--animate-pulse-red` | `pulse-red 1.5s ease-in-out infinite` |
287
+ | `--animate-blink` | `blink-cursor 1s step-end infinite` |
288
+ | `--animate-float-hint` | `float-hint 2s ease-in-out infinite` |
289
+
290
+ ### System Theme Detection
291
+
292
+ The `system` theme has two layers:
293
+
294
+ 1. **CSS-only fallback** -- A `@media (prefers-color-scheme: light)` block defines all custom
295
+ properties for `[data-theme="system"]`. This ensures correct rendering before JavaScript
296
+ hydrates (dark mode inherits from `:root` which is already dark).
297
+
298
+ 2. **JavaScript resolution** -- When the ThemeProvider applies the `system` theme, it evaluates
299
+ `window.matchMedia('(prefers-color-scheme: dark)')` and sets `data-theme` to either `dark`
300
+ or `light`. This means the actual CSS properties used are identical to the corresponding
301
+ explicit theme.
302
+
303
+ Note: There is currently no `matchMedia` listener for live OS theme changes. If the user
304
+ switches their OS theme while ClawPort is open, they need to re-select `system` or reload.
305
+
306
+ ### Theme-Specific Overrides
307
+
308
+ Some themes have extra CSS rules beyond the custom property definitions:
309
+
310
+ **Glass** -- Applies a radial gradient body background and conditionally shows `.glass-orbs`
311
+ decorative elements:
312
+
313
+ ```css
314
+ [data-theme="glass"] body {
315
+ background: radial-gradient(ellipse at 30% 20%, #1a1040 0%, #0d0d18 40%, #050510 100%);
316
+ }
317
+ [data-theme="glass"] .glass-orbs { display: block; }
318
+ ```
319
+
320
+ **Color** -- Uses a linear gradient body background and applies gradient borders to React Flow
321
+ nodes:
322
+
323
+ ```css
324
+ [data-theme="color"] body {
325
+ background: linear-gradient(135deg, #0a0814 0%, #0f0b20 50%, #0a0814 100%);
326
+ }
327
+ [data-theme="color"] .react-flow__node > div {
328
+ background: linear-gradient(#16112a, #16112a) padding-box,
329
+ linear-gradient(135deg, rgba(139,92,246,0.5), rgba(245,197,24,0.3)) border-box !important;
330
+ border: 1px solid transparent !important;
331
+ }
332
+ ```
333
+
334
+ **Light** -- Overrides `.apple-card` to solid white, adjusts message bubble colors, and
335
+ modifies React Flow edge stroke:
336
+
337
+ ```css
338
+ [data-theme="light"] .apple-card {
339
+ background: #ffffff !important;
340
+ border: 1px solid rgba(60,60,67,0.12) !important;
341
+ }
342
+ [data-theme="light"] .msg-user { background: var(--system-blue) !important; color: #ffffff !important; }
343
+ [data-theme="light"] .msg-assistant {
344
+ background: #ffffff !important;
345
+ color: #000000 !important;
346
+ border: 1px solid rgba(60,60,67,0.12) !important;
347
+ }
348
+ ```
349
+
350
+ ### How to Add a New Theme
351
+
352
+ 1. **Add the theme ID to `lib/themes.ts`:**
353
+
354
+ ```ts
355
+ export type ThemeId = 'dark' | 'glass' | 'color' | 'light' | 'system' | 'midnight';
356
+
357
+ export const THEMES = [
358
+ // ...existing themes
359
+ { id: 'midnight', label: 'Midnight', emoji: '\ud83c\udf03' },
360
+ ];
361
+ ```
362
+
363
+ 2. **Add a CSS custom property block in `app/globals.css`:**
364
+
365
+ Add a `[data-theme="midnight"]` rule block that defines **every** token listed above.
366
+ Copy the `dark` theme block as a starting point and adjust values.
367
+
368
+ ```css
369
+ [data-theme="midnight"] {
370
+ --bg: #0a0a1a;
371
+ --bg-secondary: rgba(15,15,35,1);
372
+ /* ...all other tokens */
373
+ }
374
+ ```
375
+
376
+ 3. **Optionally add theme-specific overrides** (body background gradients, component styles)
377
+ as `[data-theme="midnight"] ...` rules at the bottom of `globals.css`.
378
+
379
+ 4. The ThemeProvider, onboarding wizard, and settings page will automatically pick up the new
380
+ theme from the `THEMES` array -- no additional wiring needed.
381
+
382
+ ---
383
+
384
+ ## Settings
385
+
386
+ ### ClawPortSettings Interface
387
+
388
+ Defined in `lib/settings.ts`:
389
+
390
+ ```ts
391
+ export interface AgentOverride {
392
+ emoji?: string
393
+ profileImage?: string // base64 data URL
394
+ }
395
+
396
+ export interface ClawPortSettings {
397
+ accentColor: string | null
398
+ portalName: string | null
399
+ portalSubtitle: string | null
400
+ portalEmoji: string | null
401
+ portalIcon: string | null // base64 data URL for custom icon image
402
+ iconBgHidden: boolean // hide colored background on sidebar logo
403
+ emojiOnly: boolean // show emoji avatars without colored background
404
+ operatorName: string | null
405
+ agentOverrides: Record<string, AgentOverride>
406
+ }
407
+ ```
408
+
409
+ | Field | Type | Default | Description |
410
+ |------------------|-----------------------------------|---------|-------------|
411
+ | `accentColor` | `string \| null` | `null` | Hex color string (e.g., `"#3B82F6"`). When `null`, the theme's built-in `--accent` is used. |
412
+ | `portalName` | `string \| null` | `null` | Custom name displayed in the sidebar header. Falls back to "ClawPort". |
413
+ | `portalSubtitle` | `string \| null` | `null` | Subtitle below the name. Falls back to "Command Centre". |
414
+ | `portalEmoji` | `string \| null` | `null` | Emoji displayed in the sidebar logo. Falls back to the castle emoji. |
415
+ | `portalIcon` | `string \| null` | `null` | Base64 JPEG data URL for a custom sidebar icon image. Overrides the emoji when set. |
416
+ | `iconBgHidden` | `boolean` | `false` | When `true`, removes the colored gradient background behind the sidebar logo emoji. |
417
+ | `emojiOnly` | `boolean` | `false` | When `true`, agent avatars show just the emoji without a colored circle background. |
418
+ | `operatorName` | `string \| null` | `null` | The human operator's name. Displayed in the sidebar and injected into the chat system prompt. |
419
+ | `agentOverrides` | `Record<string, AgentOverride>` | `{}` | Per-agent customizations keyed by agent ID. Each override can set a custom emoji and/or profile image. |
420
+
421
+ ### localStorage Persistence
422
+
423
+ Settings are stored under the key `'clawport-settings'` as a JSON string.
424
+
425
+ ```
426
+ localStorage key: 'clawport-settings'
427
+ Format: JSON-serialized ClawPortSettings object
428
+ ```
429
+
430
+ Theme is stored separately under key `'clawport-theme'` (managed by ThemeProvider).
431
+
432
+ Onboarding completion is tracked under key `'clawport-onboarded'` (value `'1'`).
433
+
434
+ **Load behavior** (`loadSettings()`):
435
+
436
+ - Returns `DEFAULTS` on server (SSR guard: `typeof window === 'undefined'`)
437
+ - Returns `DEFAULTS` if the key is missing or JSON parsing fails
438
+ - Validates each field by type during parse -- unknown/malformed fields fall back to their default
439
+ - `agentOverrides` is checked with `typeof parsed.agentOverrides === 'object'`
440
+
441
+ **Save behavior** (`saveSettings()`):
442
+
443
+ - Silently no-ops on server
444
+ - Silently catches `localStorage` write errors (e.g., quota exceeded)
445
+
446
+ ### SettingsProvider API
447
+
448
+ The `SettingsProvider` (`app/settings-provider.tsx`) wraps the app and exposes all setting
449
+ mutations via React context. Access it with the `useSettings()` hook.
450
+
451
+ ```ts
452
+ import { useSettings } from '@/app/settings-provider';
453
+
454
+ const {
455
+ settings, // ClawPortSettings (read-only snapshot)
456
+ setAccentColor, // (color: string | null) => void
457
+ setPortalName, // (name: string | null) => void
458
+ setPortalSubtitle, // (subtitle: string | null) => void
459
+ setPortalEmoji, // (emoji: string | null) => void
460
+ setPortalIcon, // (icon: string | null) => void
461
+ setIconBgHidden, // (hidden: boolean) => void
462
+ setEmojiOnly, // (emojiOnly: boolean) => void
463
+ setOperatorName, // (name: string | null) => void
464
+ setAgentOverride, // (agentId: string, override: AgentOverride) => void
465
+ clearAgentOverride, // (agentId: string) => void
466
+ getAgentDisplay, // (agent: Agent) => AgentDisplay
467
+ resetAll, // () => void
468
+ } = useSettings();
469
+ ```
470
+
471
+ **Setter details:**
472
+
473
+ | Function | Signature | Behavior |
474
+ |-----------------------|--------------------------------------------------|----------|
475
+ | `setAccentColor` | `(color: string \| null) => void` | Sets the accent color. Pass `null` to revert to the theme default. Triggers a `useEffect` that applies `--accent` and `--accent-fill` as inline styles on `<html>`. |
476
+ | `setPortalName` | `(name: string \| null) => void` | Sets sidebar name. Empty string coerced to `null`. |
477
+ | `setPortalSubtitle` | `(subtitle: string \| null) => void` | Sets sidebar subtitle. Empty string coerced to `null`. |
478
+ | `setPortalEmoji` | `(emoji: string \| null) => void` | Sets sidebar logo emoji. Empty string coerced to `null`. |
479
+ | `setPortalIcon` | `(icon: string \| null) => void` | Sets sidebar icon image (base64 data URL). Pass `null` to remove. |
480
+ | `setIconBgHidden` | `(hidden: boolean) => void` | Toggles the colored background behind the sidebar logo emoji. |
481
+ | `setEmojiOnly` | `(emojiOnly: boolean) => void` | Toggles emoji-only avatar mode across the entire app. |
482
+ | `setOperatorName` | `(name: string \| null) => void` | Sets the operator's name. Empty string coerced to `null`. |
483
+ | `setAgentOverride` | `(agentId: string, override: AgentOverride) => void` | Merges an override into the agent's existing overrides. Does not replace -- it shallow-merges. |
484
+ | `clearAgentOverride` | `(agentId: string) => void` | Removes all overrides for a specific agent, reverting to defaults. |
485
+ | `getAgentDisplay` | `(agent: Agent) => AgentDisplay` | Resolves the effective emoji, profile image, and emojiOnly flag for an agent, considering overrides. |
486
+ | `resetAll` | `() => void` | Resets all settings to `DEFAULTS` and persists immediately. |
487
+
488
+ **Hydration strategy:**
489
+
490
+ The provider initializes with `DEFAULTS` (not from `localStorage`) so that server and client
491
+ render the same HTML. A `useEffect` on mount calls `loadSettings()` to hydrate from
492
+ `localStorage`, causing a single re-render after first paint.
493
+
494
+ ### Accent Color CSS Variables
495
+
496
+ When the user selects a custom accent color, the SettingsProvider applies it as inline styles
497
+ on `document.documentElement`:
498
+
499
+ ```ts
500
+ // app/settings-provider.tsx
501
+ useEffect(() => {
502
+ const el = document.documentElement.style;
503
+ if (settings.accentColor) {
504
+ el.setProperty('--accent', settings.accentColor);
505
+ el.setProperty('--accent-fill', hexToAccentFill(settings.accentColor));
506
+ } else {
507
+ el.removeProperty('--accent');
508
+ el.removeProperty('--accent-fill');
509
+ }
510
+ }, [settings.accentColor]);
511
+ ```
512
+
513
+ The `hexToAccentFill` helper converts a hex color to `rgba(r,g,b,0.15)`:
514
+
515
+ ```ts
516
+ export function hexToAccentFill(hex: string): string {
517
+ const r = parseInt(hex.slice(1, 3), 16);
518
+ const g = parseInt(hex.slice(3, 5), 16);
519
+ const b = parseInt(hex.slice(5, 7), 16);
520
+ return `rgba(${r},${g},${b},0.15)`;
521
+ }
522
+ ```
523
+
524
+ This means:
525
+ - When `accentColor` is `null`, `--accent` and `--accent-fill` come from the active theme's CSS.
526
+ - When `accentColor` is set, inline styles on `<html>` override the theme's values.
527
+ - Every component using `var(--accent)` or `var(--accent-fill)` picks up the change automatically.
528
+
529
+ **Accent color presets** available in both the settings page and onboarding wizard:
530
+
531
+ | Label | Hex |
532
+ |---------|-----------|
533
+ | Gold | `#F5C518` |
534
+ | Blue | `#3B82F6` |
535
+ | Green | `#22C55E` |
536
+ | Red | `#EF4444` |
537
+ | Orange | `#F97316` |
538
+ | Purple | `#A855F7` |
539
+ | Pink | `#EC4899` |
540
+ | Teal | `#14B8A6` |
541
+ | Cyan | `#06B6D4` |
542
+ | Indigo | `#6366F1` |
543
+ | Rose | `#F43F5E` |
544
+ | Lime | `#84CC16` |
545
+
546
+ A native `<input type="color">` picker is also provided for arbitrary colors.
547
+
548
+ ### Agent Override System
549
+
550
+ Each agent can have a per-agent emoji and/or profile image override, stored in
551
+ `settings.agentOverrides` keyed by agent ID.
552
+
553
+ ```ts
554
+ interface AgentOverride {
555
+ emoji?: string // Custom emoji to replace the agent's default
556
+ profileImage?: string // Base64 data URL (JPEG, max 200px dimension)
557
+ }
558
+ ```
559
+
560
+ **How it works:**
561
+
562
+ 1. The Settings page fetches all agents from `/api/agents`.
563
+ 2. Each agent row is expandable. Inside, the user can:
564
+ - Type a custom emoji
565
+ - Upload a profile image (resized to 200px max via Canvas API, saved as JPEG at 0.85 quality)
566
+ 3. Overrides are shallow-merged: setting a new emoji does not clear an existing profile image.
567
+ 4. A gold dot indicator appears on agent rows that have active overrides.
568
+ 5. `clearAgentOverride(agentId)` removes the entire entry, reverting to the agent's defaults.
569
+
570
+ **Resolution via `getAgentDisplay()`:**
571
+
572
+ ```ts
573
+ const getAgentDisplay = (agent: Agent): AgentDisplay => {
574
+ const override = settings.agentOverrides[agent.id];
575
+ return {
576
+ emoji: override?.emoji || agent.emoji, // Fallback to agent default
577
+ profileImage: override?.profileImage, // undefined if no override
578
+ emojiOnly: settings.emojiOnly, // Global setting
579
+ };
580
+ };
581
+ ```
582
+
583
+ Components like `AgentAvatar` call `getAgentDisplay()` to resolve the effective visual for
584
+ each agent.
585
+
586
+ ### operatorName Flow
587
+
588
+ The operator name flows through the system as follows:
589
+
590
+ 1. **Settings** -- User enters their name in the onboarding wizard (step 0) or settings page.
591
+ Stored as `settings.operatorName`.
592
+
593
+ 2. **Onboarding wizard** -- Commits the name on wizard step 0 via `setOperatorName()`. The
594
+ wizard shows a live preview with the user's initials rendered as a badge.
595
+
596
+ 3. **Sidebar** -- Reads `settings.operatorName` to display the operator's initials in the
597
+ sidebar.
598
+
599
+ 4. **Chat POST** -- When a message is sent to `/api/chat/[id]`, the operator name is included
600
+ in the request payload and injected into the system prompt so agents know who they are
601
+ talking to.
602
+
603
+ ---
604
+
605
+ ## Customization Guide
606
+
607
+ ### Change the Default Accent Color
608
+
609
+ The default accent color is defined per-theme in `app/globals.css`. To change it globally:
610
+
611
+ 1. Edit every theme block's `--accent` and `--accent-fill` values:
612
+
613
+ ```css
614
+ :root, [data-theme="dark"] {
615
+ --accent: #3B82F6; /* New default: Blue */
616
+ --accent-fill: rgba(59,130,246,0.15); /* Same color at 15% opacity */
617
+ }
618
+ ```
619
+
620
+ 2. Repeat for `[data-theme="glass"]`, `[data-theme="color"]`, and the `[data-theme="system"]`
621
+ media query block.
622
+
623
+ 3. The `light` theme has a different accent (`#B8860B`) for contrast reasons -- update it with
624
+ a value that works on white backgrounds.
625
+
626
+ Note: This only changes the theme-level default. Users who have set a custom accent color in
627
+ settings will not be affected (their inline style override takes precedence).
628
+
629
+ ### Add a New Setting Field
630
+
631
+ Follow this sequence to add a new boolean setting called `compactMode`:
632
+
633
+ **Step 1: Types** -- Add the field to `ClawPortSettings` in `lib/settings.ts`:
634
+
635
+ ```ts
636
+ export interface ClawPortSettings {
637
+ // ...existing fields
638
+ compactMode: boolean
639
+ }
640
+ ```
641
+
642
+ **Step 2: Defaults** -- Add the default value:
643
+
644
+ ```ts
645
+ export const DEFAULTS: ClawPortSettings = {
646
+ // ...existing defaults
647
+ compactMode: false,
648
+ }
649
+ ```
650
+
651
+ **Step 3: Parser** -- Add type-safe parsing in `loadSettings()`:
652
+
653
+ ```ts
654
+ return {
655
+ // ...existing fields
656
+ compactMode: typeof parsed.compactMode === 'boolean' ? parsed.compactMode : false,
657
+ }
658
+ ```
659
+
660
+ **Step 4: Provider** -- In `app/settings-provider.tsx`:
661
+
662
+ a. Add to the context interface:
663
+
664
+ ```ts
665
+ interface SettingsContextValue {
666
+ // ...existing
667
+ setCompactMode: (compact: boolean) => void
668
+ }
669
+ ```
670
+
671
+ b. Add to the context default:
672
+
673
+ ```ts
674
+ const SettingsContext = createContext<SettingsContextValue>({
675
+ // ...existing
676
+ compactMode: false, // in the settings object
677
+ setCompactMode: () => {},
678
+ })
679
+ ```
680
+
681
+ c. Add the setter callback:
682
+
683
+ ```ts
684
+ const setCompactMode = useCallback(
685
+ (compact: boolean) => {
686
+ update({ ...settings, compactMode: compact })
687
+ },
688
+ [settings, update],
689
+ )
690
+ ```
691
+
692
+ d. Include in the Provider's `value` prop:
693
+
694
+ ```ts
695
+ value={{ ...existing, setCompactMode }}
696
+ ```
697
+
698
+ e. Update `resetAll` to include the new field.
699
+
700
+ **Step 5: UI** -- Add a toggle in `app/settings/page.tsx` (follow the pattern of the existing
701
+ `emojiOnly` toggle -- an iOS-style switch button with `role="switch"` and `aria-checked`).
702
+
703
+ ### Add a New Theme (Step by Step)
704
+
705
+ 1. **Choose an ID** -- Short, lowercase, no spaces. Example: `midnight`.
706
+
707
+ 2. **Update the type union** in `lib/themes.ts`:
708
+
709
+ ```ts
710
+ export type ThemeId = 'dark' | 'glass' | 'color' | 'light' | 'system' | 'midnight';
711
+ ```
712
+
713
+ 3. **Add to the THEMES array** in `lib/themes.ts`:
714
+
715
+ ```ts
716
+ { id: 'midnight', label: 'Midnight', emoji: '\ud83c\udf03' },
717
+ ```
718
+
719
+ 4. **Define all CSS custom properties** in `app/globals.css`. Copy the `[data-theme="dark"]`
720
+ block as a template. You must define every token listed in
721
+ [CSS Custom Property Tokens](#css-custom-property-tokens). Missing tokens will cause
722
+ components to render with broken styles.
723
+
724
+ ```css
725
+ [data-theme="midnight"] {
726
+ --bg: #0a0a1a;
727
+ --bg-secondary: ...;
728
+ /* Every single token from the list above */
729
+ }
730
+ ```
731
+
732
+ 5. **Optionally add body background** and component-level overrides:
733
+
734
+ ```css
735
+ [data-theme="midnight"] body {
736
+ background: linear-gradient(...);
737
+ }
738
+ ```
739
+
740
+ 6. **Test** -- The theme will automatically appear in:
741
+ - The onboarding wizard (step 1: "Choose a Theme")
742
+ - The theme selector (wherever themes are listed from the `THEMES` array)
743
+
744
+ No changes to `ThemeProvider`, settings page, or onboarding wizard code are needed.
745
+
746
+ ### CSS Custom Property Naming Conventions
747
+
748
+ ClawPort follows a consistent naming pattern for all CSS variables:
749
+
750
+ | Prefix | Category | Examples |
751
+ |---------------|--------------------------------|------------------------------------|
752
+ | `--bg-*` | Background colors | `--bg`, `--bg-secondary`, `--bg-tertiary` |
753
+ | `--material-*`| Apple translucent surfaces | `--material-regular`, `--material-thick` |
754
+ | `--fill-*` | Interactive fill states | `--fill-primary` through `--fill-quaternary` |
755
+ | `--separator*`| Dividers and borders | `--separator`, `--separator-opaque` |
756
+ | `--text-*` | Text colors (as theme tokens) | `--text-primary` through `--text-quaternary` |
757
+ | `--text-*` | Font sizes (as Tailwind theme) | `--text-caption2` through `--text-large-title` |
758
+ | `--accent*` | Brand accent | `--accent`, `--accent-fill` |
759
+ | `--system-*` | Semantic system colors | `--system-blue`, `--system-green`, etc. |
760
+ | `--shadow-*` | Box shadows | `--shadow-subtle`, `--shadow-card`, etc. |
761
+ | `--code-*` | Code block styling | `--code-bg`, `--code-border`, `--code-text` |
762
+ | `--sidebar-*` | Sidebar-specific | `--sidebar-bg`, `--sidebar-backdrop` |
763
+ | `--radius-*` | Border radii | `--radius-sm` through `--radius-2xl` |
764
+ | `--ease-*` | Easing curves | `--ease-spring`, `--ease-smooth`, `--ease-snappy` |
765
+ | `--space-*` | Spacing scale (4px grid) | `--space-1` through `--space-16` |
766
+ | `--weight-*` | Font weights | `--weight-regular` through `--weight-bold` |
767
+ | `--leading-*` | Line heights | `--leading-tight` through `--leading-relaxed` |
768
+ | `--tracking-*`| Letter spacing | `--tracking-tight`, `--tracking-normal`, `--tracking-wide` |
769
+ | `--font-*` | Font families | `--font-sans`, `--font-mono` |
770
+ | `--animate-*` | Tailwind animation tokens | `--animate-fade-in`, `--animate-slide-in`, etc. |
771
+ | `--inset-*` | Inner highlights | `--inset-shine` |
772
+
773
+ **Rules:**
774
+ - Theme-varying tokens (colors, shadows, materials) are defined per `[data-theme]` block.
775
+ - Static tokens (spacing, typography, radii, easing) are defined once in the `@theme` block
776
+ or the `:root` rule and shared across all themes.
777
+ - Components reference tokens via `var(--token-name)` in inline styles, not via Tailwind color
778
+ utilities.
779
+ - The `--text-*` namespace is shared between font-size tokens (in `@theme`) and text-color
780
+ tokens (in theme blocks). Context makes it unambiguous: `font-size: var(--text-body)` vs
781
+ `color: var(--text-primary)`.
782
+
783
+ ---
784
+
785
+ ## Key Files Reference
786
+
787
+ | File | Purpose |
788
+ |--------------------------------|----------------------------------------------|
789
+ | `app/globals.css` | All CSS custom properties, theme definitions, keyframes, utility classes |
790
+ | `lib/themes.ts` | Theme IDs, labels, emojis (`THEMES` array and `ThemeId` type) |
791
+ | `app/providers.tsx` | `ThemeProvider` -- manages `data-theme` attribute and localStorage |
792
+ | `lib/settings.ts` | `ClawPortSettings` interface, `DEFAULTS`, `loadSettings()`, `saveSettings()`, `hexToAccentFill()` |
793
+ | `app/settings-provider.tsx` | `SettingsProvider` -- all setter callbacks, accent color CSS variable application |
794
+ | `app/settings/page.tsx` | Settings UI -- accent color, branding, agent customization, reset |
795
+ | `components/OnboardingWizard.tsx` | First-run wizard -- applies theme, accent color, and branding settings |