diff --git a/.changeset/social-ravens-stay.md b/.changeset/social-ravens-stay.md new file mode 100644 index 0000000000..b17ca923cb --- /dev/null +++ b/.changeset/social-ravens-stay.md @@ -0,0 +1,5 @@ +--- +"shadcn": minor +--- + +add style sera diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 936c3193e6..4254113f29 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -9,5 +9,6 @@ "WebFetch(domain:github.com)" ], "deny": [] - } + }, + "outputStyle": "Explanatory" } diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3441008c1d..98bca1942d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,6 +39,9 @@ jobs: key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- + - name: Install Bun + uses: oven-sh/setup-bun@v2 + - name: Install dependencies run: pnpm install diff --git a/apps/v4/app/(app)/(root)/page.tsx b/apps/v4/app/(app)/(root)/page.tsx index 599b4cbb5b..f9bbb62761 100644 --- a/apps/v4/app/(app)/(root)/page.tsx +++ b/apps/v4/app/(app)/(root)/page.tsx @@ -3,15 +3,12 @@ import Image from "next/image" import Link from "next/link" import { Announcement } from "@/components/announcement" -import { ExamplesNav } from "@/components/examples-nav" import { PageActions, PageHeader, PageHeaderDescription, PageHeaderHeading, } from "@/components/page-header" -import { PageNav } from "@/components/page-nav" -import { ThemeSelector } from "@/components/theme-selector" import { Button } from "@/registry/new-york-v4/ui/button" import { RootComponents } from "./components" diff --git a/apps/v4/app/(app)/(styles)/sera/article-directory/components/article-directory.tsx b/apps/v4/app/(app)/(styles)/sera/article-directory/components/article-directory.tsx new file mode 100644 index 0000000000..ec3cc7882a --- /dev/null +++ b/apps/v4/app/(app)/(styles)/sera/article-directory/components/article-directory.tsx @@ -0,0 +1,216 @@ +"use client" + +import { ChevronLeftIcon, ChevronRightIcon, SearchIcon } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Badge } from "@/styles/base-sera/ui/badge" +import { + Card, + CardContent, + CardFooter, + CardHeader, +} from "@/styles/base-sera/ui/card" +import { + InputGroup, + InputGroupAddon, + InputGroupInput, +} from "@/styles/base-sera/ui/input-group" +import { + Pagination, + PaginationContent, + PaginationItem, + PaginationLink, +} from "@/styles/base-sera/ui/pagination" +import { Progress, ProgressValue } from "@/styles/base-sera/ui/progress" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/styles/base-sera/ui/table" + +const ARTICLE_ROWS = [ + { + title: "The Future of Sustainable Architecture", + wordProgress: "1.4k / 2.6k words", + author: "Elena Rostova", + issue: "Summer 2024", + status: "in-revision", + statusLabel: "In revision", + progress: 45, + }, + { + title: "Brutalism's Second Act", + wordProgress: "2.1k / 2.5k words", + author: "Marcus Chen", + issue: "Summer 2024", + status: "final-edit", + statusLabel: "Final edit", + progress: 90, + }, + { + title: "The Typography of Public Spaces", + wordProgress: "0.5k / 1.5k words", + author: "Sarah Jenkins", + issue: "Autumn 2024", + status: "drafting", + statusLabel: "Drafting", + progress: 20, + }, + { + title: "Rethinking Urban Canopies", + wordProgress: "1.8k / 1.8k words", + author: "David O'Connor", + issue: "Summer 2024", + status: "published", + statusLabel: "Published", + progress: 100, + }, + { + title: "Light, Glass, and the Modern Museum", + wordProgress: "1.2k / 2.0k words", + author: "Amara Osei", + issue: "Autumn 2024", + status: "in-revision", + statusLabel: "In revision", + progress: 55, + }, + { + title: "Concrete Utopias: Housing in the 21st Century", + wordProgress: "3.0k / 3.0k words", + author: "Tomás Herrera", + issue: "Summer 2024", + status: "published", + statusLabel: "Published", + progress: 100, + }, + { + title: "Designing for Silence", + wordProgress: "0.8k / 2.2k words", + author: "Ingrid Solberg", + issue: "Winter 2024", + status: "drafting", + statusLabel: "Drafting", + progress: 30, + }, + { + title: "The Invisible Infrastructure of Cities", + wordProgress: "2.4k / 2.8k words", + author: "James Whitfield", + issue: "Autumn 2024", + status: "final-edit", + statusLabel: "Final edit", + progress: 85, + }, +] as const + +const STATUS_BADGE_VARIANT = { + "in-revision": "outline", + "final-edit": "default", + drafting: "ghost", + published: "secondary", +} as const + +const STATUS_DOT_CLASSNAME = { + "in-revision": "bg-amber-600/80", + "final-edit": "bg-foreground/90", + drafting: "bg-muted-foreground/60", + published: "bg-emerald-600/80", +} + +export function ArticleDirectory() { + return ( + + + + + + + + + + + + + + Title + Author + Issue + Status + Progress + + + + {ARTICLE_ROWS.map((row) => ( + + +
+

+ {row.title} +

+

+ {row.wordProgress} +

+
+
+ {row.author} + {row.issue} + + + + {row.statusLabel} + + + + + + {(formattedValue) => `${formattedValue}`} + + + +
+ ))} +
+
+
+ + + + + + + + + {[1, 2, 3].map((page) => ( + + + {page} + + + ))} + + + + + + + + +
+ ) +} diff --git a/apps/v4/app/(app)/(styles)/sera/article-directory/components/preview-header.tsx b/apps/v4/app/(app)/(styles)/sera/article-directory/components/preview-header.tsx new file mode 100644 index 0000000000..e03a2a951d --- /dev/null +++ b/apps/v4/app/(app)/(styles)/sera/article-directory/components/preview-header.tsx @@ -0,0 +1,47 @@ +import { ArrowLeftIcon, PlusIcon } from "lucide-react" + +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/styles/base-sera/ui/breadcrumb" +import { Button } from "@/styles/base-sera/ui/button" +import { ButtonGroup } from "@/styles/base-sera/ui/button-group" + +export function PreviewHeader() { + return ( +
+
+
+ + + + + + Editorial Dashboard + + + + +

+ Article Directory +

+
+
+ + + +
+
+
+ ) +} diff --git a/apps/v4/app/(app)/(styles)/sera/article-directory/index.tsx b/apps/v4/app/(app)/(styles)/sera/article-directory/index.tsx new file mode 100644 index 0000000000..5413269751 --- /dev/null +++ b/apps/v4/app/(app)/(styles)/sera/article-directory/index.tsx @@ -0,0 +1,16 @@ +import { Separator } from "@/styles/base-sera/ui/separator" + +import { ArticleDirectory as ArticleDirectoryList } from "./components/article-directory" +import { PreviewHeader } from "./components/preview-header" + +export function ArticleDirectory() { + return ( +
+ + +
+ +
+
+ ) +} diff --git a/apps/v4/app/(app)/(styles)/sera/audience-analytics/components/demographics.tsx b/apps/v4/app/(app)/(styles)/sera/audience-analytics/components/demographics.tsx new file mode 100644 index 0000000000..3c086c8e04 --- /dev/null +++ b/apps/v4/app/(app)/(styles)/sera/audience-analytics/components/demographics.tsx @@ -0,0 +1,56 @@ +"use client" + +import * as React from "react" +import { MoveRightIcon } from "lucide-react" + +import { Button } from "@/styles/base-sera/ui/button" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/styles/base-sera/ui/card" +import { + Progress, + ProgressLabel, + ProgressValue, +} from "@/styles/base-sera/ui/progress" + +const DEMOGRAPHIC_DATA = [ + { age: "18 - 24", percentage: 22 }, + { age: "25 - 34", percentage: 64 }, + { age: "35 - 44", percentage: 12 }, + { age: "45+", percentage: 5 }, +] + +export function Demographics({ ...props }: React.ComponentProps) { + return ( + + + Demographics + Reader Profile + + + {DEMOGRAPHIC_DATA.map((item) => ( + + {item.age} + + {(formattedValue) => `${formattedValue}`} + + + ))} + + + + + + ) +} diff --git a/apps/v4/app/(app)/(styles)/sera/audience-analytics/components/metrics-grid.tsx b/apps/v4/app/(app)/(styles)/sera/audience-analytics/components/metrics-grid.tsx new file mode 100644 index 0000000000..55fe6c1a1e --- /dev/null +++ b/apps/v4/app/(app)/(styles)/sera/audience-analytics/components/metrics-grid.tsx @@ -0,0 +1,93 @@ +import { TrendingDownIcon, TrendingUpIcon } from "lucide-react" + +import { cn } from "@/lib/utils" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/styles/base-sera/ui/card" + +type Metric = { + label: string + value: string + comparison: string + change: string + trend: "up" | "down" +} + +const METRIC_CARDS: Metric[] = [ + { + label: "Total visitors", + value: "248.5k", + comparison: "12.4%", + change: "vs last period", + trend: "up", + }, + { + label: "Unique readers", + value: "182.1k", + comparison: "8.7%", + change: "vs last period", + trend: "up", + }, + { + label: "Avg. time on page", + value: "3m 42s", + comparison: "1.2%", + change: "vs last period", + trend: "down", + }, + { + label: "Bounce rate", + value: "42.8%", + comparison: "3.5%", + change: "vs last period", + trend: "down", + }, +] + +export function MetricsGrid() { + return ( + <> + {METRIC_CARDS.map((metric) => ( + + ))} + + ) +} + +function MetricCard({ + metric, + className, +}: { + metric: Metric + className: string +}) { + return ( + + + + {metric.label} + + + {metric.value} + + + {metric.trend === "up" ? ( + + ) : ( + + )}{" "} + {metric.comparison}{" "} + {metric.change} + + + + ) +} diff --git a/apps/v4/app/(app)/(styles)/sera/audience-analytics/components/preview-header.tsx b/apps/v4/app/(app)/(styles)/sera/audience-analytics/components/preview-header.tsx new file mode 100644 index 0000000000..eb7ecc5c7a --- /dev/null +++ b/apps/v4/app/(app)/(styles)/sera/audience-analytics/components/preview-header.tsx @@ -0,0 +1,103 @@ +"use client" + +import * as React from "react" +import { ChevronDownIcon, DownloadIcon } from "lucide-react" + +import { Button } from "@/styles/base-sera/ui/button" +import { ButtonGroup } from "@/styles/base-sera/ui/button-group" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, +} from "@/styles/base-sera/ui/dropdown-menu" + +const EXPORT_DATE_OPTIONS = [ + { + label: "Last 7 days", + value: "last-7-days", + }, + { + label: "Last 30 days", + value: "last-30-days", + }, + { + label: "This month", + value: "this-month", + }, + { + label: "Last month", + value: "last-month", + }, +] + +export function PreviewHeader() { + const [selectedDateRange, setSelectedDateRange] = + React.useState("last-30-days") + + const selectedDateRangeLabel = React.useMemo(() => { + const selectedOption = EXPORT_DATE_OPTIONS.find( + (option) => option.value === selectedDateRange + ) + + if (!selectedOption) { + return "Last 30 days" + } + + return selectedOption.label + }, [selectedDateRange]) + + return ( +
+
+
+

+ Audience Analytics +

+
+ Editorial Performance Dashboard +
+
+ + + + } + > + {selectedDateRangeLabel}{" "} + + + + + + {EXPORT_DATE_OPTIONS.map((option) => ( + + {option.label} + + ))} + + + + + + +
+
+ ) +} diff --git a/apps/v4/app/(app)/(styles)/sera/audience-analytics/components/top-editorial.tsx b/apps/v4/app/(app)/(styles)/sera/audience-analytics/components/top-editorial.tsx new file mode 100644 index 0000000000..e210198bb5 --- /dev/null +++ b/apps/v4/app/(app)/(styles)/sera/audience-analytics/components/top-editorial.tsx @@ -0,0 +1,257 @@ +"use client" + +import * as React from "react" +import { ArrowDownIcon, MoreHorizontalIcon } from "lucide-react" + +import { Button } from "@/styles/base-sera/ui/button" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/styles/base-sera/ui/card" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/styles/base-sera/ui/dropdown-menu" +import { Spinner } from "@/styles/base-sera/ui/spinner" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/styles/base-sera/ui/table" +import { + ToggleGroup, + ToggleGroupItem, +} from "@/styles/base-sera/ui/toggle-group" + +type EditorialMetric = "views" | "time" | "shares" + +type EditorialRow = { + rank: number + title: string + author: string + published: string + pageviews: string + avgTime: string +} + +const METRIC_LABEL: Record = { + views: "VIEWS", + time: "TIME", + shares: "SHARES", +} + +const EDITORIAL_ROWS: EditorialRow[] = [ + { + rank: 1, + title: "The New Vanguard of Minimalist Architecture", + author: "Elena Rostova", + published: "Oct 12", + pageviews: "45.2k", + avgTime: "04:15", + }, + { + rank: 2, + title: "Autumn Sartorial Code: Deconstructed Classics", + author: "Julian Vance", + published: "Oct 05", + pageviews: "38.9k", + avgTime: "03:42", + }, + { + rank: 3, + title: "Interview: Director Sofia Coppola on The Aesthetics of Isolation", + author: "Marcus Trent", + published: "Sep 28", + pageviews: "31.4k", + avgTime: "06:20", + }, + { + rank: 4, + title: "Sourcing Ceramics from Kyoto's Oldest Kilns", + author: "Sarah Lin", + published: "Oct 18", + pageviews: "22.1k", + avgTime: "02:55", + }, + { + rank: 5, + title: "Field Notes from Copenhagen Design Week", + author: "Noah Bennett", + published: "Oct 21", + pageviews: "19.7k", + avgTime: "03:18", + }, + { + rank: 6, + title: "A Studio Visit with Milan's Most Elusive Lighting Designer", + author: "Claire Duval", + published: "Oct 09", + pageviews: "17.4k", + avgTime: "04:02", + }, + { + rank: 7, + title: "Collecting the New Avant-Garde in Contemporary Furniture", + author: "Tommy Rhodes", + published: "Sep 30", + pageviews: "15.9k", + avgTime: "03:36", + }, + { + rank: 8, + title: "Inside Lisbon's Quiet Culinary Renaissance", + author: "Amara Iqbal", + published: "Oct 14", + pageviews: "14.2k", + avgTime: "05:08", + }, + { + rank: 9, + title: "Why Slow Interiors Are Defining the Next Luxury Wave", + author: "Henry Vale", + published: "Oct 03", + pageviews: "12.7k", + avgTime: "03:11", + }, + { + rank: 10, + title: "The Return of Print: Independent Magazine Covers to Watch", + author: "Mina Okafor", + published: "Sep 26", + pageviews: "11.3k", + avgTime: "02:49", + }, +] + +type TopEditorialProps = React.ComponentProps & { + selectedMetric?: EditorialMetric +} + +export function TopEditorial({ + selectedMetric = "views", + ...props +}: TopEditorialProps) { + const [visibleCount, setVisibleCount] = React.useState(5) + const [isLoadingMore, setIsLoadingMore] = React.useState(false) + const hasMoreRows = visibleCount < EDITORIAL_ROWS.length + const visibleRows = EDITORIAL_ROWS.slice(0, visibleCount) + + const handleLoadMore = React.useCallback(() => { + if (!hasMoreRows || isLoadingMore) { + return + } + + setIsLoadingMore(true) + + window.setTimeout(() => { + setVisibleCount(EDITORIAL_ROWS.length) + setIsLoadingMore(false) + }, 2000) + }, [hasMoreRows, isLoadingMore]) + + return ( + + +
+
+ Top Editorials + Ranked by engagement +
+ + {(["views", "time", "shares"] as const).map((metric) => { + return ( + + {METRIC_LABEL[metric]} + + ) + })} + +
+
+ + + + + # + Title + Published + Page Views + Read Time + Actions + + + + {visibleRows.map((row) => ( + + + {row.rank} + + +
+

+ {row.title} +

+

+ By {row.author} +

+
+
+ {row.published} + {row.pageviews} + {row.avgTime} + + + } + aria-label={`Open actions for ${row.title}`} + > + + + + Edit + Publish + + Delete + + + + +
+ ))} +
+
+
+ + {hasMoreRows ? ( + + ) : null} + +
+ ) +} diff --git a/apps/v4/app/(app)/(styles)/sera/audience-analytics/components/traffic-overview-deferred.tsx b/apps/v4/app/(app)/(styles)/sera/audience-analytics/components/traffic-overview-deferred.tsx new file mode 100644 index 0000000000..e8c893380b --- /dev/null +++ b/apps/v4/app/(app)/(styles)/sera/audience-analytics/components/traffic-overview-deferred.tsx @@ -0,0 +1,57 @@ +"use client" + +import * as React from "react" +import dynamic from "next/dynamic" + +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/styles/base-sera/ui/card" + +const TrafficOverviewContent = dynamic( + () => import("./traffic-overview").then((mod) => mod.TrafficOverview), + { + ssr: false, + loading: () => , + } +) + +export function TrafficOverviewDeferred({ + className, + ...props +}: React.ComponentProps) { + return ( +
+ +
+ ) +} + +function TrafficOverviewFallback() { + return ( + + + Traffic Overview + + Traffic for the last 30 days has increased by 12.4% compared to the + previous period. + + + +