From 86a901d5983e1446ab310312c26382d0e01676ad Mon Sep 17 00:00:00 2001 From: tommaso Date: Fri, 10 Jul 2026 18:04:56 +0200 Subject: [PATCH] feat(theme): reduce Aspetto to a color-scheme preset, drop the token editor The per-token editor (accent, button background, button text, font, radius, max width, custom CSS) let a merchant produce an unreadable or inaccessible form: the accent token only drives the keyboard focus ring, so a light accent hid it; button background/text had no contrast guard; custom CSS was arbitrary CSS injected into the page. Per the merchant's call, the appearance is now a fixed, accessible design with a single choice: light (default), dark, or auto. - Settings "Aspetto" tab: keep only the Schema colore select; remove the color fields, ColorField, the hex/int/font validators and their state. - proxy loadTheme: read only themeScheme; stop applying the other tokens. - Preview iframe made non-interactive (sandbox="allow-scripts", inert, tabIndex -1, pointer-events:none) with scrolling moved to the wrapper, since it demonstrates a fixed design and must not look clickable. The token columns stay in the schema, unread, so the customization surface can be re-exposed later without a migration. theme.ts (the engine) is untouched. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Mv83a29B4eFv5ixoj6PoE1 --- app/app/routes/app.settings.tsx | 233 ++++++-------------------------- app/app/routes/proxy.tsx | 14 +- 2 files changed, 46 insertions(+), 201 deletions(-) diff --git a/app/app/routes/app.settings.tsx b/app/app/routes/app.settings.tsx index c36709f..8575901 100644 --- a/app/app/routes/app.settings.tsx +++ b/app/app/routes/app.settings.tsx @@ -32,14 +32,7 @@ import { renderStep2 } from "../lib/recesso.view"; import { statementTemplate } from "../lib/recesso.copy"; import { DEFAULT_SCHEME, - FONT_OPTIONS, - FONT_PRESETS, - RADIUS_MAX, - RADIUS_MIN, SCHEME_OPTIONS, - WIDTH_MAX, - WIDTH_MIN, - isHexColor, schemeOrNull, type ThemeTokens, } from "../lib/theme"; @@ -85,14 +78,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { // Esiste un SMTP di default a livello app (env/fly secret)? In prod oggi NO: // senza SMTP per-shop non parte nessuna ricevuta. appDefaultSmtp: !!process.env.SMTP_HOST, - themeAccent: s?.themeAccent ?? "", - themeButtonBg: s?.themeButtonBg ?? "", - themeButtonText: s?.themeButtonText ?? "", - themeRadius: s?.themeRadius != null ? String(s.themeRadius) : "", - themeFont: s?.themeFont ?? "system", - themeWidth: s?.themeWidth != null ? String(s.themeWidth) : "", themeScheme: s?.themeScheme ?? DEFAULT_SCHEME, - themeCustomCss: s?.themeCustomCss ?? "", }; }; @@ -168,16 +154,9 @@ export const action = async ({ request }: ActionFunctionArgs) => { smtpUser: String(f.get("smtpUser") ?? "").trim() || null, smtpSecure: f.get("smtpSecure") === "true", smtpFrom: String(f.get("smtpFrom") ?? "").trim() || null, - // Tema: si salva solo cio' che e' valido. Un valore sbagliato non viene - // scritto, cosi' il form ricade sul default invece di rompersi. - themeAccent: hexOrNull(f.get("themeAccent")), - themeButtonBg: hexOrNull(f.get("themeButtonBg")), - themeButtonText: hexOrNull(f.get("themeButtonText")), - themeRadius: intInRange(f.get("themeRadius"), RADIUS_MIN, RADIUS_MAX), - themeWidth: intInRange(f.get("themeWidth"), WIDTH_MIN, WIDTH_MAX), - themeFont: fontOrNull(f.get("themeFont")), + // Aspetto: solo lo schema colore. Le colonne dei token restano nel DB, + // inerti, per poter riesporre la personalizzazione senza migrazioni. themeScheme: schemeOrNull(f.get("themeScheme")), - themeCustomCss: String(f.get("themeCustomCss") ?? "").trim() || null, }; // Password SMTP: cifrata solo se fornita; vuota = invariata. @@ -194,64 +173,6 @@ export const action = async ({ request }: ActionFunctionArgs) => { return { ok: true, tested: false, error: null }; }; -function hexOrNull(v: FormDataEntryValue | null): string | null { - const s = String(v ?? "").trim(); - return isHexColor(s) ? s : null; -} -function intInRange( - v: FormDataEntryValue | null, - min: number, - max: number, -): number | null { - const n = Number(String(v ?? "").trim()); - if (!Number.isFinite(n) || n === 0) return null; - return Math.min(max, Math.max(min, Math.round(n))); -} -function fontOrNull(v: FormDataEntryValue | null): string | null { - const s = String(v ?? "").trim(); - return s && FONT_PRESETS[s] ? s : null; -} - -function ColorField(props: { - label: string; - value: string; - onChange: (v: string) => void; - placeholder?: string; - helpText?: string; -}) { - const valid = isHexColor(props.value); - const invalid = !valid && props.value.trim().length > 0; - return ( - -
- -
- props.onChange(e.currentTarget.value)} - style={{ - width: 40, - height: 36, - padding: 0, - border: "1px solid #c9cccf", - borderRadius: 8, - background: "none", - cursor: "pointer", - }} - /> -
- ); -} function opFrame(html: string, height = 130) { const doc = `${html}
`; @@ -303,14 +224,7 @@ export default function SettingsPage() { const [smtpSecure, setSmtpSecure] = useState(d.smtpSecure); const [smtpFrom, setSmtpFrom] = useState(d.smtpFrom); const [testTo, setTestTo] = useState(d.notifyEmail); - const [themeAccent, setThemeAccent] = useState(d.themeAccent); - const [themeButtonBg, setThemeButtonBg] = useState(d.themeButtonBg); - const [themeButtonText, setThemeButtonText] = useState(d.themeButtonText); - const [themeRadius, setThemeRadius] = useState(d.themeRadius); - const [themeFont, setThemeFont] = useState(d.themeFont); - const [themeWidth, setThemeWidth] = useState(d.themeWidth); const [themeScheme, setThemeScheme] = useState(d.themeScheme); - const [themeCustomCss, setThemeCustomCss] = useState(d.themeCustomCss); const [showSaved, setShowSaved] = useState(false); const saving = nav.state === "submitting"; @@ -358,16 +272,7 @@ export default function SettingsPage() { // Anteprima FEDELE: stesso renderer e stesso CSS dello storefront (recesso.view). const themePreview = useMemo(() => { - const tokens: ThemeTokens = { - accent: themeAccent, - buttonBg: themeButtonBg, - buttonText: themeButtonText, - radius: Number(themeRadius) || null, - font: themeFont, - width: Number(themeWidth) || null, - scheme: themeScheme, - customCss: themeCustomCss, - }; + const tokens: ThemeTokens = { scheme: themeScheme }; return renderStep2( { orderId: "gid://shopify/Order/0", @@ -378,16 +283,7 @@ export default function SettingsPage() { }, tokens, ); - }, [ - themeAccent, - themeButtonBg, - themeButtonText, - themeRadius, - themeFont, - themeWidth, - themeScheme, - themeCustomCss, - ]); + }, [themeScheme]); const handleTest = () => { setShowSaved(false); @@ -428,25 +324,12 @@ export default function SettingsPage() { fd.set("smtpPass", smtpPass); fd.set("smtpSecure", String(smtpSecure)); fd.set("smtpFrom", smtpFrom); - fd.set("themeAccent", themeAccent); - fd.set("themeButtonBg", themeButtonBg); - fd.set("themeButtonText", themeButtonText); - fd.set("themeRadius", themeRadius); - fd.set("themeFont", themeFont); - fd.set("themeWidth", themeWidth); fd.set("themeScheme", themeScheme); - fd.set("themeCustomCss", themeCustomCss); submit(fd, { method: "post" }); }; const resetTheme = () => { - setThemeAccent(""); - setThemeButtonBg(""); - setThemeButtonText(""); - setThemeRadius(""); - setThemeFont("system"); - setThemeWidth(""); - setThemeCustomCss(""); + setThemeScheme(DEFAULT_SCHEME); setShowSaved(false); }; @@ -870,8 +753,8 @@ export default function SettingsPage() { Aspetto del form - Campi vuoti = default. Un valore non valido non viene - salvato: il form ricade sul default invece di rompersi. + Il form usa una grafica gia' pronta, leggibile e + accessibile. Scegli solo se mostrarla chiara o scura. @@ -883,66 +766,6 @@ export default function SettingsPage() { helpText="Il form si apre in un riquadro sopra il tuo tema, che l'app non puo' leggere. Scegli 'Segue il sistema' solo se il tuo tema ha una versione scura." /> - - - - -