feat(web): UI component layer + dark-theme reskin
The TRM tokens were good but every screen hand-rolled inputs and buttons as bare outlines on near-black panels, so fields, cards and buttons were visually indistinguishable. Add a component layer (.input/.select/.textarea as recessed slots, .btn family with a FILLED primary, .card scaffolding) and adopt it across the booth/shift/login/tariff/site screens — several of which were still light-theme inline styles dropped on a dark background. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -108,7 +108,7 @@ export function ActiveSessions({ onPick }: { onPick: (identity: string) => void
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={reopen.isPending || !shiftReady}
|
disabled={reopen.isPending || !shiftReady}
|
||||||
onClick={() => handleReopen(s)}
|
onClick={() => handleReopen(s)}
|
||||||
className="shrink-0 rounded-term border border-term-cyan px-2 py-0.5 text-[10px] uppercase tracking-wider text-term-cyan hover:bg-term-cyan/10 disabled:opacity-50"
|
className="btn btn-pay btn-sm shrink-0"
|
||||||
title={shiftReady ? t("booth.openBarrierTitle") : t("shift.gateTitle")}
|
title={shiftReady ? t("booth.openBarrierTitle") : t("shift.gateTitle")}
|
||||||
>
|
>
|
||||||
{t("booth.openBarrier")}
|
{t("booth.openBarrier")}
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ export function BoothPayModal({ identity, onClose }: { identity: string; onClose
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleOpenShift}
|
onClick={handleOpenShift}
|
||||||
disabled={openingShift}
|
disabled={openingShift}
|
||||||
className="mt-2 rounded-term border border-term-green bg-term-green/10 px-3 py-1 text-[12px] font-semibold uppercase tracking-wider text-term-green disabled:opacity-50"
|
className="btn btn-go btn-sm mt-2"
|
||||||
>
|
>
|
||||||
{openingShift ? t("shift.opening") : t("shift.openNow")}
|
{openingShift ? t("shift.opening") : t("shift.openNow")}
|
||||||
</button>
|
</button>
|
||||||
@@ -263,11 +263,7 @@ export function BoothPayModal({ identity, onClose }: { identity: string; onClose
|
|||||||
key={tn}
|
key={tn}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setTender(tn)}
|
onClick={() => setTender(tn)}
|
||||||
className={`rounded-term border px-3 py-1 text-[12px] uppercase tracking-wider ${
|
className={tender === tn ? "btn btn-primary btn-sm" : "btn btn-sm"}
|
||||||
tender === tn
|
|
||||||
? "border-term-amber text-term-amber"
|
|
||||||
: "border-term-border text-term-muted hover:text-term-text"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{t(`pay.${tn}`)}
|
{t(`pay.${tn}`)}
|
||||||
</button>
|
</button>
|
||||||
@@ -279,6 +275,7 @@ export function BoothPayModal({ identity, onClose }: { identity: string; onClose
|
|||||||
<label className="flex items-center gap-2 text-[12px]">
|
<label className="flex items-center gap-2 text-[12px]">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
className="accent-term-amber"
|
||||||
checked={voucher}
|
checked={voucher}
|
||||||
onChange={(e) => setPrintVoucherChecked(e.target.checked)}
|
onChange={(e) => setPrintVoucherChecked(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
@@ -304,7 +301,7 @@ export function BoothPayModal({ identity, onClose }: { identity: string; onClose
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleReprintReceipt}
|
onClick={handleReprintReceipt}
|
||||||
disabled={reprinting}
|
disabled={reprinting}
|
||||||
className="rounded-term border border-term-border px-3 py-1.5 text-[12px] uppercase tracking-wider text-term-muted hover:text-term-text disabled:opacity-50"
|
className="btn btn-sm"
|
||||||
>
|
>
|
||||||
{reprinting ? t("pay.reprinting") : t("pay.reprintReceipt")}
|
{reprinting ? t("pay.reprinting") : t("pay.reprintReceipt")}
|
||||||
</button>
|
</button>
|
||||||
@@ -312,7 +309,7 @@ export function BoothPayModal({ identity, onClose }: { identity: string; onClose
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="rounded-term border border-term-amber px-4 py-1.5 text-[12px] uppercase tracking-wider text-term-amber"
|
className="btn btn-primary btn-sm"
|
||||||
>
|
>
|
||||||
{t("common.close")}
|
{t("common.close")}
|
||||||
</button>
|
</button>
|
||||||
@@ -322,7 +319,7 @@ export function BoothPayModal({ identity, onClose }: { identity: string; onClose
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="rounded-term border border-term-border px-3 py-1.5 text-[12px] uppercase tracking-wider text-term-muted hover:text-term-text"
|
className="btn btn-ghost btn-sm"
|
||||||
>
|
>
|
||||||
{t("common.cancel")}
|
{t("common.cancel")}
|
||||||
</button>
|
</button>
|
||||||
@@ -333,7 +330,7 @@ export function BoothPayModal({ identity, onClose }: { identity: string; onClose
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleOpenBarrier}
|
onClick={handleOpenBarrier}
|
||||||
disabled={!shiftReady || phase === "finishing"}
|
disabled={!shiftReady || phase === "finishing"}
|
||||||
className="rounded-term border border-term-cyan bg-term-cyan/10 px-4 py-1.5 text-[12px] font-semibold uppercase tracking-wider text-term-cyan disabled:opacity-50"
|
className="btn btn-pay btn-lg"
|
||||||
>
|
>
|
||||||
{phase === "finishing" ? t("pay.opening") : t("booth.openBarrier")}
|
{phase === "finishing" ? t("pay.opening") : t("booth.openBarrier")}
|
||||||
</button>
|
</button>
|
||||||
@@ -342,7 +339,7 @@ export function BoothPayModal({ identity, onClose }: { identity: string; onClose
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handlePayAndExit}
|
onClick={handlePayAndExit}
|
||||||
disabled={!shiftReady || phase === "paying" || phase === "finishing"}
|
disabled={!shiftReady || phase === "paying" || phase === "finishing"}
|
||||||
className="rounded-term border border-term-green bg-term-green/10 px-4 py-1.5 text-[12px] font-semibold uppercase tracking-wider text-term-green disabled:opacity-50"
|
className="btn btn-go btn-lg"
|
||||||
>
|
>
|
||||||
{phase === "paying"
|
{phase === "paying"
|
||||||
? t("pay.takingPayment")
|
? t("pay.takingPayment")
|
||||||
|
|||||||
@@ -111,12 +111,9 @@ function TicketInput({ onSubmit }: { onSubmit: (identity: string) => void }) {
|
|||||||
onChange={(e) => setValue(e.target.value)}
|
onChange={(e) => setValue(e.target.value)}
|
||||||
placeholder={t("booth.scanPlaceholder")}
|
placeholder={t("booth.scanPlaceholder")}
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
className="flex-1 rounded-term border border-term-border bg-term-bg px-3 py-2 text-lg tabular-nums text-term-text placeholder:text-term-muted focus:border-term-amber"
|
className="input h-11 flex-1 px-3 text-lg tabular-nums"
|
||||||
/>
|
/>
|
||||||
<button
|
<button type="submit" className="btn btn-primary btn-lg">
|
||||||
type="submit"
|
|
||||||
className="rounded-term border border-term-amber bg-term-amber/10 px-4 py-2 text-[12px] font-semibold uppercase tracking-wider text-term-amber"
|
|
||||||
>
|
|
||||||
{t("booth.open")}
|
{t("booth.open")}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
+23
-29
@@ -23,37 +23,31 @@ export function Login({ onLoggedIn }: { onLoggedIn: (u: SessionUser) => void })
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main style={{ fontFamily: "system-ui", maxWidth: 320, margin: "4rem auto" }}>
|
<main className="flex min-h-screen items-center justify-center bg-term-bg px-4">
|
||||||
<h1>{t("auth.title")}</h1>
|
<form onSubmit={submit} className="card w-full max-w-sm p-6">
|
||||||
<form onSubmit={submit}>
|
<h1 className="mb-5 text-h5 font-semibold uppercase tracking-widest text-term-amber">{t("auth.title")}</h1>
|
||||||
<div style={{ margin: "0.5rem 0" }}>
|
<div className="field mb-3">
|
||||||
<label>
|
<label className="label">{t("auth.username")}</label>
|
||||||
{t("auth.username")}
|
<input
|
||||||
<br />
|
className="input"
|
||||||
<input
|
value={username}
|
||||||
value={username}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
autoFocus
|
||||||
autoFocus
|
autoComplete="username"
|
||||||
autoComplete="username"
|
/>
|
||||||
style={{ width: "100%" }}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div style={{ margin: "0.5rem 0" }}>
|
<div className="field mb-3">
|
||||||
<label>
|
<label className="label">{t("auth.password")}</label>
|
||||||
{t("auth.password")}
|
<input
|
||||||
<br />
|
className="input"
|
||||||
<input
|
type="password"
|
||||||
type="password"
|
value={password}
|
||||||
value={password}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
autoComplete="current-password"
|
||||||
autoComplete="current-password"
|
/>
|
||||||
style={{ width: "100%" }}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
{error && <p style={{ color: "crimson" }}>{error}</p>}
|
{error && <p className="mb-3 text-[12px] text-term-red">{error}</p>}
|
||||||
<button type="submit" disabled={busy || !username || !password}>
|
<button type="submit" className="btn btn-primary btn-lg w-full" disabled={busy || !username || !password}>
|
||||||
{busy ? t("auth.signingIn") : t("auth.signIn")}
|
{busy ? t("auth.signingIn") : t("auth.signIn")}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -85,76 +85,78 @@ export function ShiftControl({ isAdmin = false }: { isAdmin?: boolean }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section style={{ marginTop: "1.5rem", padding: "0.75rem 1rem", border: "1px solid #ddd", borderRadius: 6, maxWidth: 460 }}>
|
<section className="card mt-6 max-w-md p-4">
|
||||||
<strong>{t("shift.label")}</strong>{" "}
|
<div className="flex flex-wrap items-center gap-2 text-[13px]">
|
||||||
{startedAt ? (
|
<strong className="uppercase tracking-wider text-term-muted">{t("shift.label")}</strong>
|
||||||
<>
|
{startedAt ? (
|
||||||
<span style={{ color: "#16a34a" }}>{t("shift.open")}</span> {t("shift.since")}{" "}
|
<>
|
||||||
{new Date(startedAt).toLocaleString()}{" "}
|
<span className="font-semibold text-term-green">{t("shift.open")}</span>
|
||||||
<button type="button" onClick={end} disabled={busy}>
|
<span className="text-term-muted">{t("shift.since")} {new Date(startedAt).toLocaleString()}</span>
|
||||||
{busy ? t("shift.ending") : t("shift.endShift")}
|
<button type="button" className="btn btn-sm btn-danger" onClick={end} disabled={busy}>
|
||||||
</button>
|
{busy ? t("shift.ending") : t("shift.endShift")}
|
||||||
</>
|
</button>
|
||||||
) : (
|
</>
|
||||||
<>
|
) : (
|
||||||
<span style={{ color: "#777" }}>{t("shift.notStarted")}</span>{" "}
|
<>
|
||||||
<button type="button" onClick={start} disabled={busy}>
|
<span className="text-term-muted">{t("shift.notStarted")}</span>
|
||||||
{busy ? t("shift.starting") : t("shift.startShift")}
|
<button type="button" className="btn btn-go btn-sm" onClick={start} disabled={busy}>
|
||||||
</button>
|
{busy ? t("shift.starting") : t("shift.startShift")}
|
||||||
</>
|
</button>
|
||||||
)}
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{/* Live drawer balance (what's in the till right now / inherited). */}
|
{/* Live drawer balance (what's in the till right now / inherited). */}
|
||||||
{drawerMinor != null && (
|
{drawerMinor != null && (
|
||||||
<div style={{ marginTop: "0.5rem", color: "#555" }}>
|
<div className="mt-2 text-[12px] text-term-text">
|
||||||
{t("shift.drawer")} <strong>{money(drawerMinor, currency)}</strong>
|
{t("shift.drawer")} <strong className="tabular-nums">{money(drawerMinor, currency)}</strong>
|
||||||
{startedAt && <span style={{ color: "#888" }}> {t("shift.openingFloatInherited")}</span>}
|
{startedAt && <span className="text-term-muted"> {t("shift.openingFloatInherited")}</span>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{err && <p style={{ color: "crimson", margin: "0.5rem 0 0" }}>{err}</p>}
|
{err && <p className="mt-2 text-[12px] text-term-red">{err}</p>}
|
||||||
|
|
||||||
{/* Admin: load / remove physical drawer cash (signed cash_movement). */}
|
{/* Admin: load / remove physical drawer cash (signed cash_movement). */}
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<div style={{ marginTop: "0.6rem", paddingTop: "0.5rem", borderTop: "1px solid #eee" }}>
|
<div className="mt-4 border-t border-term-border pt-3">
|
||||||
<div style={{ color: "#666", fontSize: "0.85rem", marginBottom: "0.35rem" }}>
|
<div className="mb-1.5 text-[11px] uppercase tracking-wider text-term-muted">
|
||||||
{t("shift.drawerCashAdmin")}
|
{t("shift.drawerCashAdmin")}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", gap: "0.4rem", alignItems: "center", flexWrap: "wrap" }}>
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<input
|
<input
|
||||||
|
className="input w-28"
|
||||||
value={moveAmount}
|
value={moveAmount}
|
||||||
onChange={(e) => setMoveAmount(e.target.value)}
|
onChange={(e) => setMoveAmount(e.target.value)}
|
||||||
placeholder={t("shift.amount")}
|
placeholder={t("shift.amount")}
|
||||||
inputMode="decimal"
|
inputMode="decimal"
|
||||||
style={{ width: 90 }}
|
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
|
className="input min-w-36 flex-1"
|
||||||
value={moveReason}
|
value={moveReason}
|
||||||
onChange={(e) => setMoveReason(e.target.value)}
|
onChange={(e) => setMoveReason(e.target.value)}
|
||||||
placeholder={t("shift.reasonPlaceholder")}
|
placeholder={t("shift.reasonPlaceholder")}
|
||||||
style={{ flex: 1, minWidth: 140 }}
|
|
||||||
/>
|
/>
|
||||||
<button type="button" onClick={() => move(1)}>{t("shift.load")}</button>
|
<button type="button" className="btn btn-go btn-sm" onClick={() => move(1)}>{t("shift.load")}</button>
|
||||||
<button type="button" onClick={() => move(-1)}>{t("shift.remove")}</button>
|
<button type="button" className="btn btn-danger btn-sm" onClick={() => move(-1)}>{t("shift.remove")}</button>
|
||||||
</div>
|
</div>
|
||||||
{moveMsg && <div style={{ marginTop: "0.35rem", color: "#555", fontSize: "0.85rem" }}>{moveMsg}</div>}
|
{moveMsg && <div className="mt-1.5 text-[12px] text-term-muted">{moveMsg}</div>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{report && (
|
{report && (
|
||||||
<div style={{ marginTop: "0.75rem", fontFamily: "ui-monospace, monospace", fontSize: "0.9em" }}>
|
<div className="mt-4 rounded-term border border-term-border bg-term-bg p-3 text-[12px] tabular-nums">
|
||||||
<div style={{ fontWeight: 600 }}>{t("shift.zReport")} — {report.operator}</div>
|
<div className="font-semibold text-term-text">{t("shift.zReport")} — {report.operator}</div>
|
||||||
<div>{t("shift.payments")} {report.paymentCount}</div>
|
<div className="text-term-text">{t("shift.payments")} {report.paymentCount}</div>
|
||||||
<div>{t("shift.cash")} {money(report.cashTotalMinor, report.currency)}</div>
|
<div className="text-term-text">{t("shift.cash")} {money(report.cashTotalMinor, report.currency)}</div>
|
||||||
<div>{t("shift.card")} {money(report.cardTotalMinor, report.currency)}</div>
|
<div className="text-term-text">{t("shift.card")} {money(report.cardTotalMinor, report.currency)}</div>
|
||||||
<div style={{ marginTop: "0.4rem", color: "#666" }}>{t("shift.drawerSection")}</div>
|
<div className="mt-2 text-[11px] uppercase tracking-wider text-term-muted">{t("shift.drawerSection")}</div>
|
||||||
<div>{t("shift.openingFloat")} {money(report.openingFloatMinor, report.currency)}</div>
|
<div className="text-term-text">{t("shift.openingFloat")} {money(report.openingFloatMinor, report.currency)}</div>
|
||||||
<div>{t("shift.cashTaken")} {money(report.cashTotalMinor, report.currency)}</div>
|
<div className="text-term-text">{t("shift.cashTaken")} {money(report.cashTotalMinor, report.currency)}</div>
|
||||||
<div>{t("shift.cashAdded")} {money(report.cashAddedMinor, report.currency)}</div>
|
<div className="text-term-text">{t("shift.cashAdded")} {money(report.cashAddedMinor, report.currency)}</div>
|
||||||
<div>{t("shift.cashRemoved")} {money(report.cashRemovedMinor, report.currency)}</div>
|
<div className="text-term-text">{t("shift.cashRemoved")} {money(report.cashRemovedMinor, report.currency)}</div>
|
||||||
<div style={{ fontWeight: 600 }}>
|
<div className="font-semibold text-term-text">
|
||||||
{t("shift.expectedDrawer")} {money(report.expectedDrawerMinor, report.currency)}
|
{t("shift.expectedDrawer")} {money(report.expectedDrawerMinor, report.currency)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ color: report.printed ? "#16a34a" : "#b45309", marginTop: "0.3rem" }}>
|
<div className={report.printed ? "mt-1 text-term-green" : "mt-1 text-term-amber"}>
|
||||||
{report.printed ? t("shift.printedToReceipt") : t("shift.recordedNoPrinter")}
|
{report.printed ? t("shift.printedToReceipt") : t("shift.recordedNoPrinter")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -62,44 +62,50 @@ export function SiteSettings({ canEdit }: { canEdit: boolean }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section style={{ marginTop: "1.5rem", padding: "0.75rem 1rem", border: "1px solid #ddd", borderRadius: 6, maxWidth: 460 }}>
|
<section className="card mt-6 max-w-md p-4">
|
||||||
<strong>{t("site.occupancy")}</strong>{" "}
|
<div className="flex flex-wrap items-center gap-1.5 text-[13px]">
|
||||||
{occ == null ? (
|
<strong className="uppercase tracking-wider text-term-muted">{t("site.occupancy")}</strong>
|
||||||
"…"
|
{occ == null ? (
|
||||||
) : (
|
<span className="text-term-muted">…</span>
|
||||||
<>
|
) : (
|
||||||
<span style={{ fontWeight: 600 }}>{occ.count}</span>
|
<>
|
||||||
{occ.capacity != null ? ` / ${occ.capacity}` : ` ${t("site.noCapacitySet")}`}
|
<span className="text-h5 font-semibold tabular-nums text-term-text">{occ.count}</span>
|
||||||
{occ.capacity != null && (
|
<span className="tabular-nums text-term-muted">
|
||||||
<span style={{ color: "#666" }}> · {occ.free} {t("site.free")}</span>
|
{occ.capacity != null ? `/ ${occ.capacity}` : t("site.noCapacitySet")}
|
||||||
)}
|
</span>
|
||||||
{occ.full && <span style={{ color: "crimson", marginLeft: "0.5rem", fontWeight: 600 }}>{t("site.full")}</span>}{" "}
|
{occ.capacity != null && (
|
||||||
<button type="button" onClick={reload} style={{ marginLeft: "0.5rem" }}>↻</button>
|
<span className="tabular-nums text-term-muted">· {occ.free} {t("site.free")}</span>
|
||||||
</>
|
)}
|
||||||
)}
|
{occ.full && <span className="font-semibold text-term-red">{t("site.full")}</span>}
|
||||||
|
<button type="button" className="btn btn-ghost btn-sm" onClick={reload}>↻</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<div style={{ marginTop: "0.6rem", display: "grid", gap: "0.5rem" }}>
|
<div className="mt-4 grid gap-3">
|
||||||
<label>
|
<div className="field">
|
||||||
{t("site.capacityLabel")}{" "}
|
<span className="label">{t("site.capacityLabel")}</span>
|
||||||
<input value={capInput} onChange={(e) => setCapInput(e.target.value)} style={{ width: 80 }} placeholder={t("site.capacityPlaceholder")} />
|
<input className="input w-32" value={capInput} onChange={(e) => setCapInput(e.target.value)} placeholder={t("site.capacityPlaceholder")} />
|
||||||
</label>
|
</div>
|
||||||
<label style={{ display: "flex", alignItems: "center", gap: "0.4rem" }}>
|
<label className="flex items-center gap-2 text-[12px] text-term-text">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
className="accent-term-amber"
|
||||||
checked={exitVoucherDefault}
|
checked={exitVoucherDefault}
|
||||||
onChange={(e) => setExitVoucherDefault(e.target.checked)}
|
onChange={(e) => setExitVoucherDefault(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
{t("site.printExitDefault")}
|
{t("site.printExitDefault")}
|
||||||
<span style={{ color: "#888", fontSize: "0.8rem" }}>{t("site.printExitHint")}</span>
|
<span className="hint">{t("site.printExitHint")}</span>
|
||||||
</label>
|
</label>
|
||||||
<div style={{ borderTop: "1px solid #eee", paddingTop: "0.5rem", color: "#666", fontSize: "0.85rem" }}>
|
<div className="border-t border-term-border pt-3 text-[11px] uppercase tracking-wider text-term-muted">
|
||||||
{t("site.parkDetails")}
|
{t("site.parkDetails")}
|
||||||
</div>
|
</div>
|
||||||
{META_FIELDS.map(({ key, labelKey, phKey, multiline }) => (
|
{META_FIELDS.map(({ key, labelKey, phKey, multiline }) => (
|
||||||
<label key={key} style={{ display: "flex", flexDirection: "column", fontSize: "0.85rem" }}>
|
<div key={key} className="field">
|
||||||
{t(labelKey)}
|
<span className="label">{t(labelKey)}</span>
|
||||||
{multiline ? (
|
{multiline ? (
|
||||||
<textarea
|
<textarea
|
||||||
|
className="textarea"
|
||||||
value={meta[key] ?? ""}
|
value={meta[key] ?? ""}
|
||||||
onChange={(e) => setMeta((m) => ({ ...m, [key]: e.target.value }))}
|
onChange={(e) => setMeta((m) => ({ ...m, [key]: e.target.value }))}
|
||||||
rows={2}
|
rows={2}
|
||||||
@@ -107,16 +113,17 @@ export function SiteSettings({ canEdit }: { canEdit: boolean }) {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<input
|
<input
|
||||||
|
className="input"
|
||||||
value={meta[key] ?? ""}
|
value={meta[key] ?? ""}
|
||||||
onChange={(e) => setMeta((m) => ({ ...m, [key]: e.target.value }))}
|
onChange={(e) => setMeta((m) => ({ ...m, [key]: e.target.value }))}
|
||||||
placeholder={phKey ? t(phKey) : undefined}
|
placeholder={phKey ? t(phKey) : undefined}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</label>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div>
|
<div className="flex items-center gap-3">
|
||||||
<button type="button" onClick={save}>{t("site.save")}</button>
|
<button type="button" className="btn btn-primary btn-sm" onClick={save}>{t("site.save")}</button>
|
||||||
{msg && <span style={{ marginLeft: "0.5rem", color: "#555" }}>{msg}</span>}
|
{msg && <span className="text-[12px] text-term-muted">{msg}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -284,12 +284,14 @@ export function TariffComposer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section style={{ marginTop: "2rem" }}>
|
<section className="mx-auto max-w-3xl px-4 py-6">
|
||||||
<h2>{t("tariff.title")}</h2>
|
<h2 className="mb-1 text-h4 font-semibold text-term-text">{t("tariff.title")}</h2>
|
||||||
{!state?.active ? (
|
{!state?.active ? (
|
||||||
<p style={{ color: "#b45309" }}>{t("tariff.noRateCard")}</p>
|
<p className="mb-4 rounded-term border border-term-amber/50 bg-term-amber/10 px-3 py-2 text-[12px] text-term-amber">
|
||||||
|
{t("tariff.noRateCard")}
|
||||||
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p style={{ color: "#555" }}>
|
<p className="mb-4 text-[12px] text-term-muted">
|
||||||
{t("tariff.activeSince", {
|
{t("tariff.activeSince", {
|
||||||
date: new Date(state.active.effectiveFrom).toLocaleString(),
|
date: new Date(state.active.effectiveFrom).toLocaleString(),
|
||||||
count: state.versions.length,
|
count: state.versions.length,
|
||||||
@@ -297,82 +299,84 @@ export function TariffComposer() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ display: "grid", gridTemplateColumns: "max-content 1fr", gap: "0.4rem 0.75rem", alignItems: "center", maxWidth: 460 }}>
|
<div className="card card-body grid grid-cols-[max-content_1fr] items-center gap-x-4 gap-y-2">
|
||||||
<label>{t("tariff.currency")}</label>
|
<label className="label">{t("tariff.currency")}</label>
|
||||||
<input value={form.currency} onChange={(e) => set("currency", e.target.value)} maxLength={3} style={{ width: 80 }} />
|
<input className="input w-24" value={form.currency} onChange={(e) => set("currency", e.target.value)} maxLength={3} />
|
||||||
<label>{t("tariff.freeEntryGrace")}</label>
|
<label className="label">{t("tariff.freeEntryGrace")}</label>
|
||||||
<input value={form.gracePeriodEntryMin} onChange={(e) => set("gracePeriodEntryMin", e.target.value)} />
|
<input className="input w-32" value={form.gracePeriodEntryMin} onChange={(e) => set("gracePeriodEntryMin", e.target.value)} />
|
||||||
<label>{t("tariff.billingIncrement")}</label>
|
<label className="label">{t("tariff.billingIncrement")}</label>
|
||||||
<input value={form.incrementMin} onChange={(e) => set("incrementMin", e.target.value)} />
|
<input className="input w-32" value={form.incrementMin} onChange={(e) => set("incrementMin", e.target.value)} />
|
||||||
<label>{t("tariff.lostTicketFee")}</label>
|
<label className="label">{t("tariff.lostTicketFee")}</label>
|
||||||
<input value={form.lostTicket} onChange={(e) => set("lostTicket", e.target.value)} />
|
<input className="input w-32" value={form.lostTicket} onChange={(e) => set("lostTicket", e.target.value)} />
|
||||||
<label>{t("tariff.exitGrace")}</label>
|
<label className="label">{t("tariff.exitGrace")}</label>
|
||||||
<input value={form.gracePeriodExitMin} onChange={(e) => set("gracePeriodExitMin", e.target.value)} />
|
<input className="input w-32" value={form.gracePeriodExitMin} onChange={(e) => set("gracePeriodExitMin", e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* The DEFAULT card — always-active rate. Front-and-centre; a site that never
|
{/* The DEFAULT card — always-active rate. Front-and-centre; a site that never
|
||||||
wants tiers just edits this and publishes a bare V1 structure. */}
|
wants tiers just edits this and publishes a bare V1 structure. */}
|
||||||
<h3 style={{ marginBottom: "0.25rem" }}>{t("tariff.defaultCard")}</h3>
|
<h3 className="mt-6 mb-0.5 text-h6 font-semibold uppercase tracking-wider text-term-text">{t("tariff.defaultCard")}</h3>
|
||||||
<p style={{ color: "#777", margin: "0 0 0.5rem", fontSize: "0.9em" }}>{t("tariff.defaultCardHint")}</p>
|
<p className="hint mb-2">{t("tariff.defaultCardHint")}</p>
|
||||||
<PricingEditor
|
<div className="card card-body">
|
||||||
t={t}
|
<PricingEditor
|
||||||
pricing={form.base}
|
t={t}
|
||||||
onMode={(mode) => updatePricing("base", (p) => ({ ...p, mode }))}
|
pricing={form.base}
|
||||||
onFlat={(flat) => updatePricing("base", (p) => ({ ...p, flat }))}
|
onMode={(mode) => updatePricing("base", (p) => ({ ...p, mode }))}
|
||||||
onCap={(dailyCap) => updatePricing("base", (p) => ({ ...p, dailyCap }))}
|
onFlat={(flat) => updatePricing("base", (p) => ({ ...p, flat }))}
|
||||||
onBlock={(i, patch) => setBlock("base", i, patch)}
|
onCap={(dailyCap) => updatePricing("base", (p) => ({ ...p, dailyCap }))}
|
||||||
onAddBlock={() => addBlock("base")}
|
onBlock={(i, patch) => setBlock("base", i, patch)}
|
||||||
onRemoveBlock={(i) => removeBlock("base", i)}
|
onAddBlock={() => addBlock("base")}
|
||||||
/>
|
onRemoveBlock={(i) => removeBlock("base", i)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Advanced: time & seasonal/category TIERS (opt-in). Empty ⇒ V1 is published. */}
|
{/* Advanced: time & seasonal/category TIERS (opt-in). Empty ⇒ V1 is published. */}
|
||||||
<details style={{ marginTop: "1.25rem" }} open={form.tiers.length > 0}>
|
<details className="mt-6" open={form.tiers.length > 0}>
|
||||||
<summary style={{ cursor: "pointer", fontWeight: 600 }}>{t("tariff.tiersAdvanced")}</summary>
|
<summary className="cursor-pointer text-h6 font-semibold uppercase tracking-wider text-term-text">{t("tariff.tiersAdvanced")}</summary>
|
||||||
<p style={{ color: "#777", margin: "0.4rem 0", fontSize: "0.9em" }}>{t("tariff.tiersHint")}</p>
|
<p className="hint mt-1.5 mb-2">{t("tariff.tiersHint")}</p>
|
||||||
{form.tiers.map((tr, i) => (
|
{form.tiers.map((tr, i) => (
|
||||||
<fieldset key={i} style={{ border: "1px solid #ddd", borderRadius: 6, padding: "0.6rem 0.8rem", marginBottom: "0.75rem" }}>
|
<fieldset key={i} className="card mb-3 p-4">
|
||||||
<legend style={{ display: "flex", gap: "0.5rem", alignItems: "center" }}>
|
<legend className="flex items-center gap-2 px-1">
|
||||||
<input
|
<input
|
||||||
|
className="input w-40"
|
||||||
value={tr.name}
|
value={tr.name}
|
||||||
onChange={(e) => setTier(i, { name: e.target.value })}
|
onChange={(e) => setTier(i, { name: e.target.value })}
|
||||||
placeholder={t("tariff.tierName")}
|
placeholder={t("tariff.tierName")}
|
||||||
style={{ width: 140 }}
|
|
||||||
/>
|
/>
|
||||||
<button type="button" onClick={() => removeTier(i)}>
|
<button type="button" className="btn btn-danger btn-sm" onClick={() => removeTier(i)}>
|
||||||
{t("tariff.remove")}
|
{t("tariff.remove")}
|
||||||
</button>
|
</button>
|
||||||
</legend>
|
</legend>
|
||||||
<div style={{ display: "grid", gridTemplateColumns: "max-content 1fr", gap: "0.35rem 0.75rem", alignItems: "center", maxWidth: 520 }}>
|
<div className="grid grid-cols-[max-content_1fr] items-center gap-x-4 gap-y-2">
|
||||||
<label>{t("tariff.tierPriority")}</label>
|
<label className="label">{t("tariff.tierPriority")}</label>
|
||||||
<input value={tr.priority} onChange={(e) => setTier(i, { priority: e.target.value })} style={{ width: 70 }} />
|
<input className="input w-20" value={tr.priority} onChange={(e) => setTier(i, { priority: e.target.value })} />
|
||||||
<label>{t("tariff.tierCategory")}</label>
|
<label className="label">{t("tariff.tierCategory")}</label>
|
||||||
<input value={tr.category} onChange={(e) => setTier(i, { category: e.target.value })} placeholder={t("tariff.tierCategoryPh")} style={{ width: 140 }} />
|
<input className="input w-40" value={tr.category} onChange={(e) => setTier(i, { category: e.target.value })} placeholder={t("tariff.tierCategoryPh")} />
|
||||||
<label>{t("tariff.tierDays")}</label>
|
<label className="label">{t("tariff.tierDays")}</label>
|
||||||
<span style={{ display: "flex", gap: "0.3rem", flexWrap: "wrap" }}>
|
<span className="flex flex-wrap gap-2">
|
||||||
{[1, 2, 3, 4, 5, 6, 0].map((d) => (
|
{[1, 2, 3, 4, 5, 6, 0].map((d) => (
|
||||||
<label key={d} style={{ display: "inline-flex", alignItems: "center", gap: "0.15rem", fontSize: "0.85em" }}>
|
<label key={d} className="inline-flex items-center gap-1 text-[12px] text-term-text">
|
||||||
<input type="checkbox" checked={tr.dow.includes(d)} onChange={() => toggleDow(i, d)} />
|
<input type="checkbox" className="accent-term-amber" checked={tr.dow.includes(d)} onChange={() => toggleDow(i, d)} />
|
||||||
{t(`tariff.dow${d}`)}
|
{t(`tariff.dow${d}`)}
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
<label>{t("tariff.tierHours")}</label>
|
<label className="label">{t("tariff.tierHours")}</label>
|
||||||
<span style={{ display: "inline-flex", gap: "0.3rem", alignItems: "center" }}>
|
<span className="inline-flex items-center gap-2">
|
||||||
<input value={tr.fromHour} onChange={(e) => setTier(i, { fromHour: e.target.value })} placeholder="22:00" style={{ width: 70 }} />
|
<input className="input w-20" value={tr.fromHour} onChange={(e) => setTier(i, { fromHour: e.target.value })} placeholder="22:00" />
|
||||||
<span>–</span>
|
<span className="text-term-muted">–</span>
|
||||||
<input value={tr.toHour} onChange={(e) => setTier(i, { toHour: e.target.value })} placeholder="06:00" style={{ width: 70 }} />
|
<input className="input w-20" value={tr.toHour} onChange={(e) => setTier(i, { toHour: e.target.value })} placeholder="06:00" />
|
||||||
{tr.fromHour && tr.toHour && tr.toHour <= tr.fromHour && (
|
{tr.fromHour && tr.toHour && tr.toHour <= tr.fromHour && (
|
||||||
<span style={{ color: "#777", fontSize: "0.8em" }}>{t("tariff.tierOvernight")}</span>
|
<span className="text-[11px] text-term-muted">{t("tariff.tierOvernight")}</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<label>{t("tariff.tierDates")}</label>
|
<label className="label">{t("tariff.tierDates")}</label>
|
||||||
<span style={{ display: "inline-flex", gap: "0.3rem", alignItems: "center" }}>
|
<span className="inline-flex items-center gap-2">
|
||||||
<input type="date" value={tr.dateFrom} onChange={(e) => setTier(i, { dateFrom: e.target.value })} />
|
<input type="date" className="input w-40" value={tr.dateFrom} onChange={(e) => setTier(i, { dateFrom: e.target.value })} />
|
||||||
<span>–</span>
|
<span className="text-term-muted">–</span>
|
||||||
<input type="date" value={tr.dateTo} onChange={(e) => setTier(i, { dateTo: e.target.value })} />
|
<input type="date" className="input w-40" value={tr.dateTo} onChange={(e) => setTier(i, { dateTo: e.target.value })} />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: "0.5rem" }}>
|
<div className="mt-3 border-t border-term-border pt-3">
|
||||||
<PricingEditor
|
<PricingEditor
|
||||||
t={t}
|
t={t}
|
||||||
pricing={tr.pricing}
|
pricing={tr.pricing}
|
||||||
@@ -386,19 +390,19 @@ export function TariffComposer() {
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
))}
|
))}
|
||||||
<button type="button" onClick={addTier}>
|
<button type="button" className="btn btn-sm" onClick={addTier}>
|
||||||
{t("tariff.addTier")}
|
{t("tariff.addTier")}
|
||||||
</button>
|
</button>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<div style={{ marginTop: "1rem" }}>
|
<div className="mt-6 flex items-center gap-3">
|
||||||
<button type="button" onClick={publish} disabled={saving}>
|
<button type="button" className="btn btn-primary btn-lg" onClick={publish} disabled={saving}>
|
||||||
{saving ? t("tariff.publishing") : t("tariff.publishNewVersion")}
|
{saving ? t("tariff.publishing") : t("tariff.publishNewVersion")}
|
||||||
</button>
|
</button>
|
||||||
|
{msg && (
|
||||||
|
<span className={msg.kind === "ok" ? "text-[12px] text-term-green" : "text-[12px] text-term-red"}>{msg.text}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{msg && (
|
|
||||||
<p style={{ color: msg.kind === "ok" ? "#16a34a" : "crimson", marginTop: "0.5rem" }}>{msg.text}</p>
|
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -417,29 +421,29 @@ function PricingEditor(props: {
|
|||||||
const { t, pricing: p } = props;
|
const { t, pricing: p } = props;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ display: "flex", gap: "1rem", marginBottom: "0.4rem", fontSize: "0.9em" }}>
|
<div className="mb-3 flex gap-4 text-[12px]">
|
||||||
<label style={{ display: "inline-flex", gap: "0.25rem", alignItems: "center" }}>
|
<label className="inline-flex items-center gap-1.5 text-term-text">
|
||||||
<input type="radio" checked={p.mode === "ladder"} onChange={() => props.onMode("ladder")} />
|
<input type="radio" className="accent-term-amber" checked={p.mode === "ladder"} onChange={() => props.onMode("ladder")} />
|
||||||
{t("tariff.modeLadder")}
|
{t("tariff.modeLadder")}
|
||||||
</label>
|
</label>
|
||||||
<label style={{ display: "inline-flex", gap: "0.25rem", alignItems: "center" }}>
|
<label className="inline-flex items-center gap-1.5 text-term-text">
|
||||||
<input type="radio" checked={p.mode === "flat"} onChange={() => props.onMode("flat")} />
|
<input type="radio" className="accent-term-amber" checked={p.mode === "flat"} onChange={() => props.onMode("flat")} />
|
||||||
{t("tariff.modeFlat")}
|
{t("tariff.modeFlat")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{p.mode === "flat" ? (
|
{p.mode === "flat" ? (
|
||||||
<div style={{ display: "inline-flex", gap: "0.4rem", alignItems: "center" }}>
|
<div className="inline-flex items-center gap-2">
|
||||||
<span style={{ color: "#777" }}>{t("tariff.pricePerIncrement")}</span>
|
<span className="label">{t("tariff.pricePerIncrement")}</span>
|
||||||
<input value={p.flat} onChange={(e) => props.onFlat(e.target.value)} style={{ width: 90 }} />
|
<input className="input w-28" value={p.flat} onChange={(e) => props.onFlat(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<table style={{ borderCollapse: "collapse" }}>
|
<table className="w-full border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr style={{ textAlign: "left", color: "#555" }}>
|
<tr className="text-left">
|
||||||
<th style={{ padding: "0 0.5rem" }}>{t("tariff.bandDuration")}</th>
|
<th className="label px-2 pb-1 font-normal">{t("tariff.bandDuration")}</th>
|
||||||
<th style={{ padding: "0 0.5rem" }}>{t("tariff.pricePerIncrement")}</th>
|
<th className="label px-2 pb-1 font-normal">{t("tariff.pricePerIncrement")}</th>
|
||||||
<th />
|
<th />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -448,32 +452,38 @@ function PricingEditor(props: {
|
|||||||
const isTail = i === p.blocks.length - 1;
|
const isTail = i === p.blocks.length - 1;
|
||||||
return (
|
return (
|
||||||
<tr key={i}>
|
<tr key={i}>
|
||||||
<td style={{ padding: "0.15rem 0.5rem" }}>
|
<td className="px-2 py-1">
|
||||||
{isTail ? (
|
{isTail ? (
|
||||||
<span style={{ color: "#777", fontStyle: "italic" }}>{t("tariff.thereafter")}</span>
|
<span className="italic text-term-muted">{t("tariff.thereafter")}</span>
|
||||||
) : (
|
) : (
|
||||||
<span style={{ display: "inline-flex", alignItems: "center", gap: "0.3rem" }}>
|
<span className="inline-flex items-center gap-2">
|
||||||
<input value={b.hours} onChange={(e) => props.onBlock(i, { hours: e.target.value })} placeholder={t("tariff.egHours")} style={{ width: 70 }} />
|
<input className="input w-20" value={b.hours} onChange={(e) => props.onBlock(i, { hours: e.target.value })} placeholder={t("tariff.egHours")} />
|
||||||
<span style={{ color: "#777" }}>{t("tariff.hoursUnit")}</span>
|
<span className="text-[11px] text-term-muted">{t("tariff.hoursUnit")}</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td style={{ padding: "0.15rem 0.5rem" }}>
|
<td className="px-2 py-1">
|
||||||
<input value={b.price} onChange={(e) => props.onBlock(i, { price: e.target.value })} style={{ width: 90 }} />
|
<input className="input w-28" value={b.price} onChange={(e) => props.onBlock(i, { price: e.target.value })} />
|
||||||
|
</td>
|
||||||
|
<td className="px-2">
|
||||||
|
{!isTail && (
|
||||||
|
<button type="button" className="btn btn-ghost btn-sm" onClick={() => props.onRemoveBlock(i)}>
|
||||||
|
{t("tariff.remove")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>{!isTail && <button type="button" onClick={() => props.onRemoveBlock(i)}>{t("tariff.remove")}</button>}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div style={{ marginTop: "0.4rem", display: "flex", gap: "1rem", alignItems: "center" }}>
|
<div className="mt-3 flex items-center gap-4">
|
||||||
<button type="button" onClick={props.onAddBlock}>
|
<button type="button" className="btn btn-sm" onClick={props.onAddBlock}>
|
||||||
{t("tariff.addBlock")}
|
{t("tariff.addBlock")}
|
||||||
</button>
|
</button>
|
||||||
<span style={{ display: "inline-flex", gap: "0.3rem", alignItems: "center", fontSize: "0.9em" }}>
|
<span className="inline-flex items-center gap-2">
|
||||||
<span style={{ color: "#777" }}>{t("tariff.dailyCap")}</span>
|
<span className="label">{t("tariff.dailyCap")}</span>
|
||||||
<input value={p.dailyCap} onChange={(e) => props.onCap(e.target.value)} placeholder={t("tariff.dailyCapPh")} style={{ width: 90 }} />
|
<input className="input w-28" value={p.dailyCap} onChange={(e) => props.onCap(e.target.value)} placeholder={t("tariff.dailyCapPh")} />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -194,3 +194,164 @@ body {
|
|||||||
outline: 1px solid var(--color-term-amber);
|
outline: 1px solid var(--color-term-amber);
|
||||||
outline-offset: 1px;
|
outline-offset: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
COMPONENT LAYER
|
||||||
|
The TRM tokens are good, but every screen hand-rolled inputs and
|
||||||
|
buttons as bare outlines on near-black panels, so a field, a card,
|
||||||
|
and a button were visually indistinguishable. These classes give
|
||||||
|
each control a real identity:
|
||||||
|
- inputs read as RECESSED slots (lighter fill + inset shadow)
|
||||||
|
- the primary button is FILLED (accent body, dark text) — the
|
||||||
|
one obvious action; neutrals are filled grey, not bare outlines
|
||||||
|
- explicit hover / active / focus / disabled states everywhere
|
||||||
|
Use @apply so the classes compose with Tailwind utilities.
|
||||||
|
============================================================ */
|
||||||
|
@layer components {
|
||||||
|
/* ---- Form fields: a recessed slot, clearly an input ---- */
|
||||||
|
.input,
|
||||||
|
.select,
|
||||||
|
.textarea {
|
||||||
|
@apply w-full rounded-term border bg-term-bg px-2.5 text-term-text
|
||||||
|
placeholder:text-term-muted;
|
||||||
|
border-color: #3a414c; /* lighter than panel borders */
|
||||||
|
height: var(--control-h-md);
|
||||||
|
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.45);
|
||||||
|
transition: border-color 120ms var(--ease-snap), box-shadow 120ms var(--ease-snap);
|
||||||
|
}
|
||||||
|
.textarea {
|
||||||
|
height: auto;
|
||||||
|
@apply py-2 leading-snug;
|
||||||
|
}
|
||||||
|
.input::placeholder,
|
||||||
|
.textarea::placeholder {
|
||||||
|
@apply text-term-muted;
|
||||||
|
}
|
||||||
|
.input:hover,
|
||||||
|
.select:hover,
|
||||||
|
.textarea:hover {
|
||||||
|
border-color: #4a525f;
|
||||||
|
}
|
||||||
|
.input:focus,
|
||||||
|
.select:focus,
|
||||||
|
.textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-term-amber);
|
||||||
|
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.45), 0 0 0 1px var(--color-term-amber);
|
||||||
|
}
|
||||||
|
.input:disabled,
|
||||||
|
.select:disabled,
|
||||||
|
.textarea:disabled {
|
||||||
|
@apply cursor-not-allowed opacity-50;
|
||||||
|
}
|
||||||
|
/* Small / dense variant for inline table cells */
|
||||||
|
.input-sm {
|
||||||
|
height: var(--control-h-sm);
|
||||||
|
@apply px-2 text-[12px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
@apply flex flex-col gap-1;
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
@apply text-[11px] uppercase tracking-wider text-term-muted;
|
||||||
|
}
|
||||||
|
.hint {
|
||||||
|
@apply text-[11px] leading-snug text-term-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Buttons: a button must look pressable, never like a field ---- */
|
||||||
|
.btn {
|
||||||
|
@apply inline-flex items-center justify-center gap-1.5 rounded-term border
|
||||||
|
px-3 text-[12px] font-semibold uppercase tracking-wider
|
||||||
|
transition-colors select-none;
|
||||||
|
height: var(--control-h-md);
|
||||||
|
/* Neutral default: a filled grey body, not a bare outline. */
|
||||||
|
background: var(--color-term-panel-2);
|
||||||
|
border-color: #3a414c;
|
||||||
|
color: var(--color-term-text);
|
||||||
|
}
|
||||||
|
.btn:hover:not(:disabled) {
|
||||||
|
background: #2a313b;
|
||||||
|
border-color: #4a525f;
|
||||||
|
}
|
||||||
|
.btn:active:not(:disabled) {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
.btn:disabled {
|
||||||
|
@apply cursor-not-allowed opacity-40;
|
||||||
|
}
|
||||||
|
.btn-sm {
|
||||||
|
height: var(--control-h-sm);
|
||||||
|
@apply px-2.5 text-[11px];
|
||||||
|
}
|
||||||
|
.btn-lg {
|
||||||
|
height: var(--control-h-lg);
|
||||||
|
@apply px-5 text-[13px];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Primary: FILLED amber, dark text — the unmistakable main action. */
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--color-term-amber);
|
||||||
|
border-color: var(--color-term-amber);
|
||||||
|
color: #0b0d10;
|
||||||
|
}
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background: #ffb733;
|
||||||
|
border-color: #ffb733;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Semantic filled variants (entry / payment / destructive). */
|
||||||
|
.btn-go {
|
||||||
|
background: var(--color-term-green);
|
||||||
|
border-color: var(--color-term-green);
|
||||||
|
color: #f2f2ee;
|
||||||
|
}
|
||||||
|
.btn-go:hover:not(:disabled) {
|
||||||
|
background: #38a85a;
|
||||||
|
border-color: #38a85a;
|
||||||
|
}
|
||||||
|
.btn-pay {
|
||||||
|
background: var(--color-term-cyan);
|
||||||
|
border-color: var(--color-term-cyan);
|
||||||
|
color: #f2f2ee;
|
||||||
|
}
|
||||||
|
.btn-pay:hover:not(:disabled) {
|
||||||
|
background: #2f74e0;
|
||||||
|
border-color: #2f74e0;
|
||||||
|
}
|
||||||
|
.btn-danger {
|
||||||
|
background: transparent;
|
||||||
|
border-color: var(--color-term-red);
|
||||||
|
color: var(--color-term-red);
|
||||||
|
}
|
||||||
|
.btn-danger:hover:not(:disabled) {
|
||||||
|
background: color-mix(in srgb, var(--color-term-red) 14%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ghost: lowest-emphasis (cancel, secondary nav) — text + hover only. */
|
||||||
|
.btn-ghost {
|
||||||
|
background: transparent;
|
||||||
|
border-color: transparent;
|
||||||
|
color: var(--color-term-muted);
|
||||||
|
}
|
||||||
|
.btn-ghost:hover:not(:disabled) {
|
||||||
|
background: var(--color-term-panel-2);
|
||||||
|
color: var(--color-term-text);
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Card: a panel that is clearly a container, not a field ---- */
|
||||||
|
.card {
|
||||||
|
@apply rounded-term border border-term-border bg-term-panel;
|
||||||
|
}
|
||||||
|
.card-head {
|
||||||
|
@apply flex items-center justify-between border-b border-term-border
|
||||||
|
bg-term-panel-2 px-4 py-2 text-[12px] uppercase tracking-wider text-term-muted;
|
||||||
|
}
|
||||||
|
.card-body {
|
||||||
|
@apply p-4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user