Skip to content

Commit 19005f9

Browse files
committed
feat(theme): luxe toast variant + add Fraunces to font picker
- Toast: new luxe branch (cream card, Playfair italic title, bronze accent rail) - Toast now covers terracotta / luxe / brutalist with distinct treatments - Add Fraunces to availableFonts so terracotta's serif is pickable
1 parent f0d70dd commit 19005f9

2 files changed

Lines changed: 158 additions & 2 deletions

File tree

src/components/Toast.jsx

Lines changed: 157 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ const Toast = ({
2121
links,
2222
}) => {
2323
const visualSettings = useVisualSettings();
24-
const isTerracotta = visualSettings?.fezcodexTheme === 'terracotta';
24+
const theme = visualSettings?.fezcodexTheme;
25+
const isTerracotta = theme === 'terracotta';
26+
const isLuxe = theme === 'luxe';
2527

2628
useEffect(() => {
2729
const timer = setTimeout(() => {
@@ -200,7 +202,160 @@ const Toast = ({
200202
}
201203

202204
/* ============================================================
203-
* DEFAULT TOAST (brutalist / luxe / retained legacy styling)
205+
* LUXE TOAST — refined cream card with bronze rule + serif title
206+
* ============================================================ */
207+
if (isLuxe) {
208+
const luxeAccent = (() => {
209+
switch (type) {
210+
case 'error':
211+
return '#7A2020';
212+
case 'gold':
213+
return '#B88532';
214+
case 'techno':
215+
return '#355E3B';
216+
default:
217+
return '#8D4004';
218+
}
219+
})();
220+
221+
const luxeIcon = (() => {
222+
if (icon) return icon;
223+
const common = { weight: 'duotone', style: { color: luxeAccent } };
224+
switch (type) {
225+
case 'error':
226+
return <WarningCircleIcon {...common} />;
227+
case 'gold':
228+
return <TrophyIcon {...common} />;
229+
case 'techno':
230+
return <TerminalIcon {...common} />;
231+
default:
232+
return <CheckCircleIcon {...common} />;
233+
}
234+
})();
235+
236+
const luxeKicker = (() => {
237+
switch (type) {
238+
case 'error':
239+
return 'Alert';
240+
case 'gold':
241+
return 'Accolade';
242+
case 'techno':
243+
return 'System';
244+
default:
245+
return 'Notice';
246+
}
247+
})();
248+
249+
return (
250+
<motion.div
251+
layout
252+
initial={{ y: -20, opacity: 0 }}
253+
animate={{ y: 0, opacity: 1 }}
254+
exit={{ y: -20, opacity: 0 }}
255+
transition={{ type: 'spring', stiffness: 260, damping: 26 }}
256+
className="relative w-80 md:w-[380px] bg-[#FAFAF8] border border-[#1A1A1A]/10 shadow-[0_24px_48px_-24px_rgba(26,26,26,0.28)] rounded-sm overflow-hidden mb-4 group"
257+
>
258+
{/* top hairline accent */}
259+
<span
260+
aria-hidden="true"
261+
className="absolute top-0 left-0 right-0 h-[2px]"
262+
style={{ backgroundColor: luxeAccent, opacity: 0.55 }}
263+
/>
264+
{/* bottom timer */}
265+
<motion.div
266+
initial={{ width: '100%' }}
267+
animate={{ width: 0 }}
268+
transition={{ duration: duration / 1000, ease: 'linear' }}
269+
className="absolute bottom-0 left-0 h-[1px] z-20"
270+
style={{ backgroundColor: luxeAccent, opacity: 0.55 }}
271+
/>
272+
273+
<div className="px-6 py-5 flex gap-4 items-start">
274+
<div
275+
className="flex-shrink-0 mt-0.5 text-[22px] w-9 h-9 flex items-center justify-center rounded-full"
276+
style={{ backgroundColor: `${luxeAccent}12` }}
277+
>
278+
{luxeIcon}
279+
</div>
280+
281+
<div className="flex-grow min-w-0 space-y-1.5">
282+
<div
283+
className="font-outfit text-[9.5px] tracking-[0.24em] uppercase"
284+
style={{ color: luxeAccent }}
285+
>
286+
{luxeKicker}
287+
</div>
288+
<h4 className="font-playfairDisplay text-[19px] italic leading-tight text-[#1A1A1A]">
289+
{title}
290+
</h4>
291+
<p className="font-outfit text-[12.5px] leading-[1.55] text-[#1A1A1A]/70">
292+
{message}
293+
</p>
294+
295+
{links && links.length > 0 && (
296+
<div className="flex flex-wrap gap-2 pt-2.5">
297+
{links.map((link, index) => {
298+
const btnClass =
299+
'font-outfit text-[10px] tracking-[0.18em] uppercase px-3 py-1.5 border border-[#1A1A1A]/15 text-[#1A1A1A] hover:bg-[#1A1A1A] hover:text-[#FAFAF8] hover:border-[#1A1A1A] transition-colors rounded-sm';
300+
if (link.to)
301+
return (
302+
<Link
303+
key={index}
304+
to={link.to}
305+
className={btnClass}
306+
onClick={() => removeToast(id)}
307+
>
308+
{link.label}
309+
</Link>
310+
);
311+
if (link.href)
312+
return (
313+
<a
314+
key={index}
315+
href={link.href}
316+
target="_blank"
317+
rel="noopener noreferrer"
318+
className={btnClass}
319+
onClick={() => removeToast(id)}
320+
>
321+
{link.label}
322+
</a>
323+
);
324+
if (link.onClick)
325+
return (
326+
<button
327+
type="button"
328+
key={index}
329+
onClick={() => {
330+
link.onClick();
331+
removeToast(id);
332+
}}
333+
className={btnClass}
334+
>
335+
{link.label}
336+
</button>
337+
);
338+
return null;
339+
})}
340+
</div>
341+
)}
342+
</div>
343+
344+
<button
345+
type="button"
346+
onClick={() => removeToast(id)}
347+
className="flex-shrink-0 text-[#1A1A1A]/30 hover:text-[#1A1A1A] transition-colors p-1"
348+
aria-label="Dismiss"
349+
>
350+
<XIcon size={14} weight="bold" />
351+
</button>
352+
</div>
353+
</motion.div>
354+
);
355+
}
356+
357+
/* ============================================================
358+
* DEFAULT TOAST — brutalist dark card (legacy)
204359
* ============================================================ */
205360

206361
const getIcon = () => {

src/context/VisualSettingsContext.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const VisualSettingsProvider = ({ children }) => {
2323
{ id: 'font-ibm-plex-mono', name: 'IBM Plex Mono' },
2424
{ id: 'font-instr-serif', name: 'Instrument Serif' },
2525
{ id: 'font-nunito', name: 'Nunito' },
26+
{ id: 'font-fraunces', name: 'Fraunces' },
2627
];
2728

2829
const [headerFont, setHeaderFont] = usePersistentState(

0 commit comments

Comments
 (0)