diff --git a/app/app/routes/app.settings.tsx b/app/app/routes/app.settings.tsx
index b0bdd51..76f5971 100644
--- a/app/app/routes/app.settings.tsx
+++ b/app/app/routes/app.settings.tsx
@@ -12,6 +12,7 @@ import {
Card,
Tabs,
TextField,
+ Select,
Button,
ButtonGroup,
Banner,
@@ -27,6 +28,18 @@ import { authenticate } from "../shopify.server";
import db from "../db.server";
import { decryptSecret, encryptSecret } from "../lib/crypto.server";
import { sendTestEmail } from "../lib/mailer.server";
+import { renderStep2 } from "../lib/recesso.view";
+import { statementTemplate } from "../lib/recesso.copy";
+import {
+ FONT_OPTIONS,
+ FONT_PRESETS,
+ RADIUS_MAX,
+ RADIUS_MIN,
+ WIDTH_MAX,
+ WIDTH_MIN,
+ isHexColor,
+ type ThemeTokens,
+} from "../lib/theme";
import {
DEFAULT_INTRO,
DEFAULT_NOTE,
@@ -69,6 +82,13 @@ 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) : "",
+ themeCustomCss: s?.themeCustomCss ?? "",
};
};
@@ -144,6 +164,15 @@ 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")),
+ themeCustomCss: String(f.get("themeCustomCss") ?? "").trim() || null,
};
// Password SMTP: cifrata solo se fornita; vuota = invariata.
@@ -160,6 +189,65 @@ 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 (
+