Private
Public Access
Open the custom theme builder in a wider dedicated dialog (#46)
The theme editor used to expand inline inside ProfileModal, whose .modal is capped at min(380px, 100%) -- too narrow to comfortably see the live CustomThemePreview mockup it's built around. Pulled the editor out into a new ThemeBuilderModal (min(820px, 95vw), two-column layout above 680px) opened on top of the profile modal, same stacked-dialog pattern already used by ImageLightbox/FilePreviewModal. No changes to the theme data model, activation, save, or delete behavior. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -242,14 +242,6 @@
|
|||||||
margin-bottom: var(--sp-3) !important;
|
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 {
|
.custom-theme-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { useAuth } from '../context/AuthContext'
|
|||||||
import { hashIndex } from '../lib/avatar'
|
import { hashIndex } from '../lib/avatar'
|
||||||
import { applyTheme, DEFAULT_CUSTOM_COLORS } from '../lib/theme'
|
import { applyTheme, DEFAULT_CUSTOM_COLORS } from '../lib/theme'
|
||||||
import type { CustomTheme, CustomThemeColors } from '../types'
|
import type { CustomTheme, CustomThemeColors } from '../types'
|
||||||
import { CustomThemePreview } from './CustomThemePreview'
|
import { ThemeBuilderModal } from './ThemeBuilderModal'
|
||||||
import { UserAvatar } from './UserAvatar'
|
import { UserAvatar } from './UserAvatar'
|
||||||
import './Modal.css'
|
import './Modal.css'
|
||||||
|
|
||||||
@@ -388,72 +388,19 @@ export function ProfileModal({ onClose }: ProfileModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{editingTheme && (
|
{editingTheme && (
|
||||||
<div className="custom-theme-editor">
|
<ThemeBuilderModal
|
||||||
<input
|
colorFields={CUSTOM_COLOR_FIELDS}
|
||||||
type="text"
|
name={editNameDraft}
|
||||||
className="custom-theme-name-input"
|
onNameChange={setEditNameDraft}
|
||||||
value={editNameDraft}
|
|
||||||
onChange={(e) => setEditNameDraft(e.target.value)}
|
|
||||||
placeholder="Theme name"
|
|
||||||
maxLength={50}
|
|
||||||
/>
|
|
||||||
<CustomThemePreview
|
|
||||||
colors={editColorsDraft}
|
colors={editColorsDraft}
|
||||||
|
onColorChange={handleEditColorChange}
|
||||||
highlightedField={highlightedField}
|
highlightedField={highlightedField}
|
||||||
onHighlight={setHighlightedField}
|
onHighlight={setHighlightedField}
|
||||||
|
error={themeError}
|
||||||
|
saving={savingThemeEdit}
|
||||||
|
onSave={handleSaveThemeEdit}
|
||||||
|
onCancel={closeEditor}
|
||||||
/>
|
/>
|
||||||
<div className="custom-theme-grid">
|
|
||||||
{CUSTOM_COLOR_FIELDS.map((field) => (
|
|
||||||
<label
|
|
||||||
key={field.key}
|
|
||||||
className={`custom-theme-field${highlightedField === field.key ? ' custom-theme-field-highlighted' : ''}`}
|
|
||||||
onMouseEnter={() => setHighlightedField(field.key)}
|
|
||||||
onMouseLeave={() => setHighlightedField(null)}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="color"
|
|
||||||
value={editColorsDraft[field.key]}
|
|
||||||
onChange={(e) => handleEditColorChange(field.key, e.target.value)}
|
|
||||||
onFocus={() => setHighlightedField(field.key)}
|
|
||||||
onBlur={() => setHighlightedField(null)}
|
|
||||||
/>
|
|
||||||
<span>{field.label}</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="custom-theme-scheme">
|
|
||||||
<span>Native controls (scrollbars, form inputs)</span>
|
|
||||||
<div className="custom-theme-scheme-toggle">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`btn-secondary${editColorsDraft.color_scheme === 'light' ? ' custom-theme-scheme-active' : ''}`}
|
|
||||||
onClick={() => handleEditColorChange('color_scheme', 'light')}
|
|
||||||
>
|
|
||||||
Light
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`btn-secondary${editColorsDraft.color_scheme === 'dark' ? ' custom-theme-scheme-active' : ''}`}
|
|
||||||
onClick={() => handleEditColorChange('color_scheme', 'dark')}
|
|
||||||
>
|
|
||||||
Dark
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="modal-actions">
|
|
||||||
<button type="button" className="btn-secondary" onClick={closeEditor}>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn-primary"
|
|
||||||
onClick={handleSaveThemeEdit}
|
|
||||||
disabled={savingThemeEdit}
|
|
||||||
>
|
|
||||||
{savingThemeEdit ? 'Saving…' : 'Save'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<hr className="modal-divider" />
|
<hr className="modal-divider" />
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import type { CustomThemeColors } from '../types'
|
||||||
|
import { CustomThemePreview } from './CustomThemePreview'
|
||||||
|
import './Modal.css'
|
||||||
|
import './ThemeBuilderModal.css'
|
||||||
|
|
||||||
|
interface ColorField {
|
||||||
|
key: keyof Omit<CustomThemeColors, 'color_scheme'>
|
||||||
|
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 (
|
||||||
|
<div className="modal-scrim" onClick={onCancel}>
|
||||||
|
<div className="modal theme-builder-modal" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div className="modal-header">
|
||||||
|
<h2>Edit theme</h2>
|
||||||
|
<button type="button" className="modal-close" onClick={onCancel} aria-label="Close">
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="theme-builder-layout">
|
||||||
|
<div className="theme-builder-preview-col">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="custom-theme-name-input"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => onNameChange(e.target.value)}
|
||||||
|
placeholder="Theme name"
|
||||||
|
maxLength={50}
|
||||||
|
/>
|
||||||
|
<CustomThemePreview colors={colors} highlightedField={highlightedField} onHighlight={onHighlight} />
|
||||||
|
<div className="custom-theme-scheme">
|
||||||
|
<span>Native controls (scrollbars, form inputs)</span>
|
||||||
|
<div className="custom-theme-scheme-toggle">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`btn-secondary${colors.color_scheme === 'light' ? ' custom-theme-scheme-active' : ''}`}
|
||||||
|
onClick={() => onColorChange('color_scheme', 'light')}
|
||||||
|
>
|
||||||
|
Light
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`btn-secondary${colors.color_scheme === 'dark' ? ' custom-theme-scheme-active' : ''}`}
|
||||||
|
onClick={() => onColorChange('color_scheme', 'dark')}
|
||||||
|
>
|
||||||
|
Dark
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="custom-theme-grid theme-builder-grid">
|
||||||
|
{colorFields.map((field) => (
|
||||||
|
<label
|
||||||
|
key={field.key}
|
||||||
|
className={`custom-theme-field${highlightedField === field.key ? ' custom-theme-field-highlighted' : ''}`}
|
||||||
|
onMouseEnter={() => onHighlight(field.key)}
|
||||||
|
onMouseLeave={() => onHighlight(null)}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={colors[field.key]}
|
||||||
|
onChange={(e) => onColorChange(field.key, e.target.value)}
|
||||||
|
onFocus={() => onHighlight(field.key)}
|
||||||
|
onBlur={() => onHighlight(null)}
|
||||||
|
/>
|
||||||
|
<span>{field.label}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <p className="modal-error">{error}</p>}
|
||||||
|
<div className="modal-actions">
|
||||||
|
<button type="button" className="btn-secondary" onClick={onCancel}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn-primary" onClick={onSave} disabled={saving}>
|
||||||
|
{saving ? 'Saving…' : 'Save'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user