diff --git a/frontend/src/components/Modal.css b/frontend/src/components/Modal.css
index 78a7adb..a87c9f3 100644
--- a/frontend/src/components/Modal.css
+++ b/frontend/src/components/Modal.css
@@ -242,14 +242,6 @@
margin-bottom: var(--sp-3) !important;
}
-.custom-theme-editor {
- background: var(--ds-surface-2);
- border: 1px solid var(--ds-border);
- border-radius: var(--radius);
- padding: var(--sp-3);
- margin-bottom: var(--sp-4);
-}
-
.custom-theme-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
diff --git a/frontend/src/components/ProfileModal.tsx b/frontend/src/components/ProfileModal.tsx
index fb0231e..7b8f8fd 100644
--- a/frontend/src/components/ProfileModal.tsx
+++ b/frontend/src/components/ProfileModal.tsx
@@ -13,7 +13,7 @@ import { useAuth } from '../context/AuthContext'
import { hashIndex } from '../lib/avatar'
import { applyTheme, DEFAULT_CUSTOM_COLORS } from '../lib/theme'
import type { CustomTheme, CustomThemeColors } from '../types'
-import { CustomThemePreview } from './CustomThemePreview'
+import { ThemeBuilderModal } from './ThemeBuilderModal'
import { UserAvatar } from './UserAvatar'
import './Modal.css'
@@ -388,72 +388,19 @@ export function ProfileModal({ onClose }: ProfileModalProps) {
{editingTheme && (
-
-
setEditNameDraft(e.target.value)}
- placeholder="Theme name"
- maxLength={50}
- />
-
-
- {CUSTOM_COLOR_FIELDS.map((field) => (
-
- ))}
-
-
-
Native controls (scrollbars, form inputs)
-
-
-
-
-
-
-
-
-
-
+
)}
diff --git a/frontend/src/components/ThemeBuilderModal.css b/frontend/src/components/ThemeBuilderModal.css
new file mode 100644
index 0000000..423a089
--- /dev/null
+++ b/frontend/src/components/ThemeBuilderModal.css
@@ -0,0 +1,22 @@
+.theme-builder-modal {
+ width: min(820px, 95vw);
+ max-height: 88vh;
+}
+
+.theme-builder-layout {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
+ gap: var(--sp-5);
+ align-items: start;
+ margin-bottom: var(--sp-2);
+}
+
+.theme-builder-preview-col .custom-theme-preview {
+ margin-bottom: var(--sp-4);
+}
+
+@media (max-width: 680px) {
+ .theme-builder-layout {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/frontend/src/components/ThemeBuilderModal.tsx b/frontend/src/components/ThemeBuilderModal.tsx
new file mode 100644
index 0000000..7d6cac6
--- /dev/null
+++ b/frontend/src/components/ThemeBuilderModal.tsx
@@ -0,0 +1,119 @@
+import type { CustomThemeColors } from '../types'
+import { CustomThemePreview } from './CustomThemePreview'
+import './Modal.css'
+import './ThemeBuilderModal.css'
+
+interface ColorField {
+ key: keyof Omit
+ label: string
+}
+
+interface ThemeBuilderModalProps {
+ colorFields: ColorField[]
+ name: string
+ onNameChange: (name: string) => void
+ colors: CustomThemeColors
+ onColorChange: (key: keyof CustomThemeColors, value: string) => void
+ highlightedField: keyof CustomThemeColors | null
+ onHighlight: (field: keyof CustomThemeColors | null) => void
+ error: string | null
+ saving: boolean
+ onSave: () => void
+ onCancel: () => void
+}
+
+// Same editor as ProfileModal.tsx used to render inline, pulled out into its
+// own wider dialog (#46) -- the profile modal's .modal is capped at
+// min(380px, 100%), which cramped the CustomThemePreview mockup that's
+// supposed to make the color-to-UI mapping easy to see. Nested on top of
+// ProfileModal rather than replacing it, same stacked-dialog pattern as
+// ImageLightbox/FilePreviewModal opening over MessageList.
+export function ThemeBuilderModal({
+ colorFields,
+ name,
+ onNameChange,
+ colors,
+ onColorChange,
+ highlightedField,
+ onHighlight,
+ error,
+ saving,
+ onSave,
+ onCancel,
+}: ThemeBuilderModalProps) {
+ return (
+
+
e.stopPropagation()}>
+
+
Edit theme
+
+
+
+
+
+
onNameChange(e.target.value)}
+ placeholder="Theme name"
+ maxLength={50}
+ />
+
+
+
Native controls (scrollbars, form inputs)
+
+
+
+
+
+
+
+
+ {colorFields.map((field) => (
+
+ ))}
+
+
+
+ {error &&
{error}
}
+
+
+
+
+
+
+ )
+}