From 62ef3ce2c8bfa5b0f9b5e43cfa8d0265c90abd9f Mon Sep 17 00:00:00 2001 From: Michael Chernigin Date: Wed, 3 Jun 2026 23:03:03 +0400 Subject: [PATCH] feat: switch to generated shadcn components --- README.md | 3 +- app/app.css | 202 +++--- app/components/article-reader.tsx | 38 +- app/components/link-list.tsx | 37 +- app/components/magic/soft-grid.tsx | 16 - app/components/ui/avatar.tsx | 110 +++ app/components/ui/badge.tsx | 49 +- app/components/ui/button.tsx | 79 ++- app/components/ui/card.tsx | 137 +++- app/components/ui/dropdown-menu.tsx | 269 ++++++++ app/components/ui/input.tsx | 36 +- app/db/schema.ts | 1 + app/lib/utils.ts | 18 +- app/root.tsx | 99 ++- app/routes/archive.tsx | 8 +- app/routes/home.tsx | 21 +- app/routes/login.tsx | 12 +- app/routes/oauth.callback.tsx | 5 +- app/services/oauth.server.ts | 54 +- app/services/session.server.ts | 3 +- bun.lock | 638 +++++++++++++++++- components.json | 25 + migrations/0001_bored_the_enforcers.sql | 1 + migrations/meta/0001_snapshot.json | 481 +++++++++++++ migrations/meta/_journal.json | 7 + .../changes/initial-read-later-app/design.md | 19 +- .../initial-read-later-app/proposal.md | 2 +- .../specs/responsive-reading-ui/spec.md | 8 +- .../changes/initial-read-later-app/tasks.md | 4 +- package.json | 6 +- tests/ui.test.tsx | 2 +- 31 files changed, 2043 insertions(+), 347 deletions(-) delete mode 100644 app/components/magic/soft-grid.tsx create mode 100644 app/components/ui/avatar.tsx create mode 100644 app/components/ui/dropdown-menu.tsx create mode 100644 components.json create mode 100644 migrations/0001_bored_the_enforcers.sql create mode 100644 migrations/meta/0001_snapshot.json diff --git a/README.md b/README.md index e92eebb..c9a33c7 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,7 @@ A small self-hosted read-later web app. Save links, extract readable article con - Bun - SQLite via Bun's native `bun:sqlite` - Drizzle ORM/migrations -- shadcn/ui-style local components -- restrained Magic UI-style visual polish +- shadcn/ui components - AT Protocol OAuth ## Development diff --git a/app/app.css b/app/app.css index 17300d5..518f685 100644 --- a/app/app.css +++ b/app/app.css @@ -1,129 +1,135 @@ @import "tailwindcss"; -@plugin "tailwindcss-animate"; +@import "tw-animate-css"; +@import "shadcn/tailwind.css"; +@import "@fontsource-variable/geist"; @custom-variant dark (&:is(.dark *)); -@theme { - --font-sans: Inter, ui-sans-serif, system-ui, sans-serif; - --font-serif: Charter, "Bitstream Charter", "Sitka Text", Cambria, serif; - - --color-background: hsl(var(--background)); - --color-foreground: hsl(var(--foreground)); - --color-card: hsl(var(--card)); - --color-card-foreground: hsl(var(--card-foreground)); - --color-popover: hsl(var(--popover)); - --color-popover-foreground: hsl(var(--popover-foreground)); - --color-primary: hsl(var(--primary)); - --color-primary-foreground: hsl(var(--primary-foreground)); - --color-secondary: hsl(var(--secondary)); - --color-secondary-foreground: hsl(var(--secondary-foreground)); - --color-muted: hsl(var(--muted)); - --color-muted-foreground: hsl(var(--muted-foreground)); - --color-accent: hsl(var(--accent)); - --color-accent-foreground: hsl(var(--accent-foreground)); - --color-destructive: hsl(var(--destructive)); - --color-destructive-foreground: hsl(var(--destructive-foreground)); - --color-border: hsl(var(--border)); - --color-input: hsl(var(--input)); - --color-ring: hsl(var(--ring)); - - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); -} - :root { - --background: 42 33% 97%; - --foreground: 30 12% 10%; - --card: 0 0% 100%; - --card-foreground: 30 12% 10%; - --popover: 0 0% 100%; - --popover-foreground: 30 12% 10%; - --primary: 25 70% 22%; - --primary-foreground: 42 33% 97%; - --secondary: 36 22% 91%; - --secondary-foreground: 30 12% 16%; - --muted: 36 20% 92%; - --muted-foreground: 30 8% 42%; - --accent: 38 42% 88%; - --accent-foreground: 30 12% 12%; - --destructive: 0 72% 46%; - --destructive-foreground: 0 0% 100%; - --border: 34 20% 84%; - --input: 34 20% 84%; - --ring: 25 70% 28%; - --radius: 0.9rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --radius: 0.625rem; + --chart-1: oklch(0.87 0 0); + --chart-2: oklch(0.556 0 0); + --chart-3: oklch(0.439 0 0); + --chart-4: oklch(0.371 0 0); + --chart-5: oklch(0.269 0 0); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); } .dark { - --background: 30 12% 8%; - --foreground: 42 25% 92%; - --card: 30 12% 11%; - --card-foreground: 42 25% 92%; - --popover: 30 12% 11%; - --popover-foreground: 42 25% 92%; - --primary: 38 68% 74%; - --primary-foreground: 30 12% 9%; - --secondary: 30 10% 16%; - --secondary-foreground: 42 25% 90%; - --muted: 30 10% 16%; - --muted-foreground: 35 12% 66%; - --accent: 30 11% 18%; - --accent-foreground: 42 25% 92%; - --destructive: 0 67% 52%; - --destructive-foreground: 0 0% 100%; - --border: 30 10% 22%; - --input: 30 10% 22%; - --ring: 38 68% 74%; + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.87 0 0); + --chart-2: oklch(0.556 0 0); + --chart-3: oklch(0.439 0 0); + --chart-4: oklch(0.371 0 0); + --chart-5: oklch(0.269 0 0); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); } -* { - border-color: hsl(var(--border)); +@theme inline { + --font-sans: "Geist Variable", sans-serif; + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --radius-sm: calc(var(--radius) * 0.6); + --radius-md: calc(var(--radius) * 0.8); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) * 1.4); } -body { - background: hsl(var(--background)); - color: hsl(var(--foreground)); - min-height: 100vh; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; +@layer base { + * { + @apply border-border outline-ring/50; + } + + html { + @apply font-sans; + } + + body { + @apply bg-background text-foreground; + } } .article-body { - font-family: var(--font-serif); - font-size: clamp(1.08rem, 0.35vw + 1rem, 1.28rem); - line-height: 1.78; + line-height: 1.75; } .article-body :where(p, ul, ol, blockquote, pre, figure) { - margin-block: 1.15em; -} - -.article-body :where(h1, h2, h3) { - font-family: var(--font-sans); - line-height: 1.2; - margin-block: 1.8em 0.6em; + margin-block: 1em; } .article-body :where(a) { - color: hsl(var(--primary)); text-decoration: underline; - text-decoration-thickness: 0.08em; - text-underline-offset: 0.18em; } .article-body :where(img) { - border-radius: 0.8rem; - margin-inline: auto; max-width: 100%; } .article-body :where(pre) { overflow-x: auto; - border-radius: 0.8rem; - background: hsl(var(--muted)); - padding: 1rem; - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; - font-size: 0.9em; } diff --git a/app/components/article-reader.tsx b/app/components/article-reader.tsx index 50f28ec..fc7ee3b 100644 --- a/app/components/article-reader.tsx +++ b/app/components/article-reader.tsx @@ -11,30 +11,34 @@ export function ArticleReader({ link, snapshot }: { link: SavedLink; snapshot: A const readTime = readingTime(snapshot?.wordCount); return ( -
-
+
+
-
+
{link.state !== "archived" ? (
) : null} @@ -42,18 +46,17 @@ export function ArticleReader({ link, snapshot }: { link: SavedLink; snapshot: A
-
-
+
+
{link.sourceDomain} - · {formatDate(link.createdAt)} - {readTime ? <>·{readTime} : null} + {readTime ? {readTime} : null} {link.extractionStatus}
-

{title}

- {snapshot?.author ?

By {snapshot.author}

: null} +

{title}

+ {snapshot?.author ?

By {snapshot.author}

: null}
{snapshot ? ( @@ -62,18 +65,21 @@ export function ArticleReader({ link, snapshot }: { link: SavedLink; snapshot: A
-

Extraction failed

+

Extraction failed

{link.extractionError || "The article could not be extracted."}

- +
) : ( -

Preparing readable article

+

Preparing readable article

The original link is available while extraction is pending.

diff --git a/app/components/link-list.tsx b/app/components/link-list.tsx index 7d64737..be98dee 100644 --- a/app/components/link-list.tsx +++ b/app/components/link-list.tsx @@ -1,6 +1,5 @@ import { Archive, ArrowUpRight, BookOpen, Clock3 } from "lucide-react"; import { Form, Link } from "react-router"; -import { SoftGrid } from "~/components/magic/soft-grid"; import { Badge } from "~/components/ui/badge"; import { Button } from "~/components/ui/button"; import { Card, CardContent } from "~/components/ui/card"; @@ -12,41 +11,37 @@ type LinkWithSnapshot = SavedLink & { snapshot: ArticleSnapshot | null }; export function LinkList({ links, emptyTitle, emptyDescription }: { links: LinkWithSnapshot[]; emptyTitle: string; emptyDescription: string }) { if (!links.length) { return ( - - - -
- -

{emptyTitle}

-

{emptyDescription}

-
+ + + +

{emptyTitle}

+

{emptyDescription}

); } return ( -
+
{links.map((item) => { const title = item.snapshot?.title || item.title || item.url; const readTime = readingTime(item.snapshot?.wordCount); return ( - - + +
- + {title}
{item.sourceDomain} - · {formatDate(item.createdAt)} {readTime ? ( - <> - · - {readTime} - + + + {readTime} + ) : null} {item.extractionStatus} @@ -55,14 +50,16 @@ export function LinkList({ links, emptyTitle, emptyDescription }: { links: LinkW
{item.state !== "archived" ? (
) : null} diff --git a/app/components/magic/soft-grid.tsx b/app/components/magic/soft-grid.tsx deleted file mode 100644 index 13798a4..0000000 --- a/app/components/magic/soft-grid.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { cn } from "~/lib/utils"; - -export function SoftGrid({ className }: { className?: string }) { - return ( -