Files
famapp/src/modules/lists/components/shared-view.tsx
T
ginnoirandClaude Opus 4.7 9612a54e52
Release / build-and-push (push) Has been cancelled
Apply paper-and-ink design system across all surfaces
Replaces the generic shadcn/ui gray theme + horizontal top-bar shell with
the paper-and-ink language from the Claude Design handoff bundle: warm
off-white paper, near-black ink, Source Serif 4 + Inter, hairline borders,
muted ink accents (clay/indigo/sage/plum/ochre) used functionally for
calendars and share scopes.

Theme switcher expanded from 2 dimensions (theme × mode) to 7: palette ×
mode × fontPair × density × dashLayout × calView × navStyle. All exposed
in Settings → Appearance and persisted on the users row. Pre-paint script
applies all four data-* attributes from localStorage so reload doesn't
flash.

App shell restructured to a CSS-grid driven by data-nav on <html>: sidebar
on desktop, bottom-nav + FAB under 760px. Four desktop nav modes wired
(sidebar/rail/top/fab-only). Topbar gets a search-→-CommandPalette button,
notification bell, "+ New" quick-add, avatar.

Dashboard, calendar, lists, notes, settings, login, public share viewer,
and quick-add sheet all reskinned. Dashboard editor gains a Preset menu
(classic/split/glance) that fills the layout from the registered widgets.
FullCalendar wrapped in .fc-skin and inherits all paper-and-ink tokens via
CSS variable overrides. Public share viewer (/s/<token>) rebuilt around
ShareFrame: expiration banner, brand strip, eyebrow chip, 38px serif
title, mini-day + mini-map cards, share-rows.

Schema: drops users.theme; adds theme_palette, theme_font_pair,
theme_density, theme_dash_layout, theme_cal_view, theme_nav_style with
defaults that match the design (clay / serif-sans / regular / classic /
month / rail-desktop). Migration 0014_paper_ink_theme.

Middleware sets x-pathname so the AppShell server component can render
bare for /s/* and /login without a route-group refactor.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 01:03:21 -05:00

135 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { ListChecks } from "lucide-react";
import { useOptimistic, useTransition } from "react";
import type { ListShareData, ListShareItem } from "../server/share-queries";
import { toggleShareListItem } from "../server/share-actions";
import { ShareEyebrow } from "@/components/share/share-eyebrow";
export function ListSharedView({
data,
canWrite,
token,
}: {
data: ListShareData;
canWrite: boolean;
token: string;
}) {
const [optimisticItems, setOptimisticItems] = useOptimistic(
data.items,
(current: ListShareItem[], { id, done }: { id: string; done: boolean }) =>
current.map((item) => (item.id === id ? { ...item, done } : item)),
);
const [, startTransition] = useTransition();
function toggle(item: ListShareItem) {
if (!canWrite) return;
const next = !item.done;
startTransition(async () => {
setOptimisticItems({ id: item.id, done: next });
await toggleShareListItem(token, item.id);
});
}
const open = optimisticItems.filter((i) => !i.done);
const done = optimisticItems.filter((i) => i.done);
return (
<>
<ShareEyebrow>
<ListChecks className="size-3" />
List
</ShareEyebrow>
<h1
className="serif"
style={{
fontSize: "clamp(28px, 6vw, 38px)",
lineHeight: 1.15,
letterSpacing: "-0.02em",
color: "var(--ink)",
margin: "0 0 8px",
textWrap: "pretty",
}}
>
{data.name}
</h1>
<p className="muted text-[13px] capitalize mb-6">{data.type}</p>
{optimisticItems.length === 0 ? (
<p className="muted text-[13.5px]">This list is empty.</p>
) : (
<div
className="rounded-[var(--r-md)] overflow-hidden"
style={{
background: "var(--card)",
border: "0.5px solid var(--hair)",
}}
>
{open.length > 0 && (
<>
{open.map((item) => (
<ItemRow key={item.id} item={item} canWrite={canWrite} onToggle={toggle} />
))}
</>
)}
{done.length > 0 && (
<details className="group">
<summary
className="cursor-pointer select-none eyebrow"
style={{ padding: "12px 14px 8px", borderTop: "0.5px solid var(--hair)" }}
>
Done · {done.length}
</summary>
{done.map((item) => (
<ItemRow key={item.id} item={item} canWrite={canWrite} onToggle={toggle} />
))}
</details>
)}
</div>
)}
</>
);
}
function ItemRow({
item,
canWrite,
onToggle,
}: {
item: ListShareItem;
canWrite: boolean;
onToggle: (item: ListShareItem) => void;
}) {
return (
<div className={`list-row ${item.done ? "checked" : ""}`}>
<button
type="button"
aria-label={`Complete ${item.text}`}
aria-pressed={item.done}
className={`checkbox ${item.done ? "on" : ""}`}
disabled={!canWrite}
onClick={() => onToggle(item)}
>
{item.done && (
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="white"
strokeWidth="2.4"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M5 12l5 5L20 7" />
</svg>
)}
</button>
<span className="row-text flex-1 text-[13.5px]">
{item.text}
{item.qty && <span className="ml-1 text-[11.5px] muted">×{item.qty}</span>}
</span>
</div>
);
}