From dddda1923872dfe88c6329c80adfd6749a910a6c Mon Sep 17 00:00:00 2001 From: Keith Smith Date: Mon, 17 Aug 2026 12:13:53 -0600 Subject: [PATCH] Show a live mockup mapping each custom theme field to the real UI The 12 color pickers (Background, Sidebar background, Surface, Border, etc.) gave no indication of what each one actually affects without trial and error. Adds a miniature, self-contained mockup of the real chat UI above the picker grid -- a sidebar with room rows, a message with an avatar/mention/role badge, a composer, a danger button -- styled from inline styles bound to the draft colors (not the --ds-* custom properties, since those reflect whatever theme is currently active, not necessarily the one being edited). Hovering or focusing either a color input or its matching element in the mockup highlights both, using a fixed amber outline that stays visible regardless of the theme's own palette -- makes the mapping between the 12 fields and where they actually show up immediately obvious in either direction, without needing to activate the theme and go look around the rest of the app. Co-Authored-By: Claude Sonnet 5 --- .../src/components/CustomThemePreview.css | 108 ++++++++++++++++++ .../src/components/CustomThemePreview.tsx | 103 +++++++++++++++++ frontend/src/components/Modal.css | 14 +++ frontend/src/components/ProfileModal.tsx | 18 ++- 4 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/CustomThemePreview.css create mode 100644 frontend/src/components/CustomThemePreview.tsx diff --git a/frontend/src/components/CustomThemePreview.css b/frontend/src/components/CustomThemePreview.css new file mode 100644 index 0000000..9294f68 --- /dev/null +++ b/frontend/src/components/CustomThemePreview.css @@ -0,0 +1,108 @@ +.custom-theme-preview { + display: flex; + border-radius: var(--radius); + overflow: hidden; + border: 1px solid var(--ds-border); + margin-bottom: var(--sp-3); + font-size: 0.74rem; + /* Isolated from the outer picker grid's own hover/highlight rules -- + each swatch's highlight ring is drawn on itself, not inherited. */ + position: relative; +} + +.ctp-hoverable { + cursor: default; + transition: box-shadow 0.1s ease; +} + +.ctp-highlighted { + box-shadow: 0 0 0 2px #fbbf24, 0 0 8px 1px rgba(251, 191, 36, 0.6); + position: relative; + z-index: 1; +} + +.ctp-sidebar { + width: 96px; + flex: none; + padding: 8px 6px; + display: flex; + flex-direction: column; + gap: 4px; +} + +.ctp-room-row { + padding: 5px 7px; + border-radius: 5px; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.ctp-room-row-quiet { + border: 1px solid transparent; + background: transparent; +} + +.ctp-main { + flex: 1; + min-width: 0; + padding: 10px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.ctp-message { + display: flex; + align-items: flex-start; + gap: 6px; +} + +.ctp-avatar { + width: 22px; + height: 22px; + border-radius: 50%; + flex: none; +} + +.ctp-message-body { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; +} + +.ctp-mention { + border-radius: 4px; + padding: 0 3px; + font-weight: 700; +} + +.ctp-badge { + flex: none; + align-self: flex-start; + border-radius: var(--radius-pill); + padding: 1px 7px; + font-size: 0.68rem; + font-weight: 800; + color: #07080f; +} + +.ctp-composer { + border: 1px solid transparent; + border-radius: 6px; + padding: 5px 8px; +} + +.ctp-danger-btn { + align-self: flex-start; + background: transparent; + border: 1px solid transparent; + border-radius: 6px; + padding: 4px 8px; + font-size: 0.72rem; + font-weight: 700; + cursor: pointer; +} diff --git a/frontend/src/components/CustomThemePreview.tsx b/frontend/src/components/CustomThemePreview.tsx new file mode 100644 index 0000000..1563f4d --- /dev/null +++ b/frontend/src/components/CustomThemePreview.tsx @@ -0,0 +1,103 @@ +import type { CustomThemeColors } from '../types' +import './CustomThemePreview.css' + +interface CustomThemePreviewProps { + colors: CustomThemeColors + highlightedField: keyof CustomThemeColors | null + onHighlight: (field: keyof CustomThemeColors | null) => void +} + +// A miniature, self-contained mockup of the real chat UI, styled entirely +// from inline styles bound to the draft colors -- deliberately not the +// --ds-* custom properties, since those reflect whatever theme is actually +// active right now, not necessarily the one being edited (see +// ProfileModal.tsx's handleEditColorChange comment). Hovering either a +// swatch here or the matching color input elsewhere highlights both, so +// it's clear at a glance which of the 12 fields controls which part of the +// real UI, in both directions. +export function CustomThemePreview({ colors, highlightedField, onHighlight }: CustomThemePreviewProps) { + function hoverClass(field: keyof CustomThemeColors, extra = ''): string { + const highlighted = highlightedField === field ? ' ctp-highlighted' : '' + return `ctp-hoverable${extra ? ` ${extra}` : ''}${highlighted}` + } + + function hoverHandlers(field: keyof CustomThemeColors) { + return { + onMouseEnter: () => onHighlight(field), + onMouseLeave: () => onHighlight(null), + } + } + + return ( +
+
+
+ # general +
+
+ # random +
+
+
+
+
+
+ + + Alice + {' '} + + 2:30 PM + + +
+ Hey{' '} + + @bob + {' '} + check this out +
+
+ + Owner + +
+
+ Message… +
+ +
+
+ ) +} diff --git a/frontend/src/components/Modal.css b/frontend/src/components/Modal.css index c0b7a69..78a7adb 100644 --- a/frontend/src/components/Modal.css +++ b/frontend/src/components/Modal.css @@ -263,6 +263,20 @@ font-size: 0.8rem; color: var(--ds-text); cursor: pointer; + border-radius: 6px; + padding: 3px 4px; + margin: -3px -4px; + transition: background 0.1s ease; +} + +/* Mirrors CustomThemePreview.css's .ctp-highlighted -- same fixed, theme- + independent color so it stays visible regardless of what's actually + being edited. Set when either this field's input OR the matching swatch + in the live mockup above is hovered/focused, so the mapping works in + both directions. */ +.custom-theme-field-highlighted { + background: rgba(251, 191, 36, 0.15); + box-shadow: 0 0 0 1px #fbbf24; } .custom-theme-field input[type='color'] { diff --git a/frontend/src/components/ProfileModal.tsx b/frontend/src/components/ProfileModal.tsx index c096097..fb0231e 100644 --- a/frontend/src/components/ProfileModal.tsx +++ b/frontend/src/components/ProfileModal.tsx @@ -13,6 +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 { UserAvatar } from './UserAvatar' import './Modal.css' @@ -56,6 +57,7 @@ export function ProfileModal({ onClose }: ProfileModalProps) { const [editNameDraft, setEditNameDraft] = useState('') const [editColorsDraft, setEditColorsDraft] = useState(DEFAULT_CUSTOM_COLORS) const [savingThemeEdit, setSavingThemeEdit] = useState(false) + const [highlightedField, setHighlightedField] = useState(null) const [currentPassword, setCurrentPassword] = useState('') const [newPassword, setNewPassword] = useState('') @@ -158,6 +160,7 @@ export function ProfileModal({ onClose }: ProfileModalProps) { setEditNameDraft(theme.name) setEditColorsDraft(theme.colors) setThemeError(null) + setHighlightedField(null) } function closeEditor() { @@ -168,6 +171,7 @@ export function ProfileModal({ onClose }: ProfileModalProps) { applyTheme(user.theme, user.active_custom_theme.colors) } setEditingThemeId(null) + setHighlightedField(null) } function handleEditColorChange(key: keyof CustomThemeColors, value: string) { @@ -393,13 +397,25 @@ export function ProfileModal({ onClose }: ProfileModalProps) { placeholder="Theme name" maxLength={50} /> +
{CUSTOM_COLOR_FIELDS.map((field) => ( -