Initial commit: Publisher dashboard monorepo

Turborepo + Bun monorepo with:
- apps/publisher-dashboard: Svelte 5 SPA with SvelteKit, Tailwind CSS v4, shadcn-svelte
- packages/publisher-utils: Shared TypeScript utilities

Features:
- Performance dashboard page with metrics, charts, and data tables
- shadcn-svelte UI components with OKLCH color system
- Biome for linting/formatting with Svelte-specific overrides

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
RevIQ
2026-01-06 17:25:21 +08:00
commit a1db9ca3c5
65 changed files with 2461 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
# Publisher Dashboard
A Svelte 5 SPA built with SvelteKit, Tailwind CSS v4, and shadcn-svelte.
## Development
```bash
# From the monorepo root
bun run dev
# Or from this directory
bun run dev
```
## Adding shadcn-svelte Components
This project uses [shadcn-svelte](https://shadcn-svelte.com/) for UI components.
### Adding a new component
From the `apps/publisher-dashboard` directory, run:
```bash
bunx shadcn-svelte@latest add <component-name>
```
For example, to add the Button component:
```bash
bunx shadcn-svelte@latest add button
```
To add multiple components at once:
```bash
bunx shadcn-svelte@latest add button card input
```
### Available components
See the full list at: https://shadcn-svelte.com/docs/components
Common components:
- `button` - Buttons with variants
- `card` - Card containers
- `input` - Text inputs
- `label` - Form labels
- `select` - Select dropdowns
- `dialog` - Modal dialogs
- `dropdown-menu` - Dropdown menus
- `table` - Data tables
- `tabs` - Tab navigation
- `toast` / `sonner` - Toast notifications
### Using components
After adding a component, import it from `$lib/components/ui`:
```svelte
<script lang="ts">
import { Button } from "$lib/components/ui/button";
</script>
<Button variant="outline">Click me</Button>
```
### Utility function
The `cn()` utility for merging Tailwind classes is available at `$lib/utils`:
```svelte
<script lang="ts">
import { cn } from "$lib/utils";
</script>
<div class={cn("p-4", someCondition && "bg-primary")}>
Content
</div>
```
## Project Structure
```
src/
├── app.css # Global styles + Tailwind + shadcn theme
├── app.html # HTML template
├── lib/
│ ├── components/
│ │ └── ui/ # shadcn-svelte components go here
│ └── utils/
│ └── cn.ts # Tailwind class merge utility
└── routes/ # SvelteKit file-based routing
├── +layout.svelte
├── +layout.ts
└── +page.svelte
```

View File

@@ -0,0 +1,16 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"tailwind": {
"css": "src/app.css",
"baseColor": "zinc"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils",
"ui": "$lib/components/ui",
"hooks": "$lib/hooks",
"lib": "$lib"
},
"typescript": true,
"registry": "https://shadcn-svelte.com/registry"
}

View File

@@ -0,0 +1,35 @@
{
"name": "@publisher/dashboard",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"typecheck": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json"
},
"dependencies": {
"@publisher/utils": "workspace:*",
"bits-ui": "^2.15.4",
"clsx": "^2.1.1",
"tailwind-merge": "^3.4.0",
"tailwind-variants": "^3.2.2",
"tslib": "catalog:"
},
"devDependencies": {
"@internationalized/date": "^3.10.1",
"@lucide/svelte": "^0.562.0",
"@macalinao/tsconfig": "catalog:",
"@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.21.1",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@tailwindcss/vite": "^4.1.4",
"svelte": "^5.28.2",
"svelte-check": "^4.2.1",
"tailwindcss": "^4.1.4",
"tw-animate-css": "^1.4.0",
"typescript": "catalog:",
"vite": "^7.3.0"
}
}

View File

@@ -0,0 +1,121 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.21 0.006 285.885);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.705 0.015 286.067);
}
.dark {
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.92 0.004 286.32);
--primary-foreground: oklch(0.21 0.006 285.885);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--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.552 0.016 285.938);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.21 0.006 285.885);
--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.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.552 0.016 285.938);
}
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--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);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

13
apps/publisher-dashboard/src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="icon" href="data:,">
<meta name="viewport" content="width=device-width, initial-scale=1">
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -0,0 +1,32 @@
<script lang="ts">
import * as Card from "$lib/components/ui/card";
import { cn } from "$lib/utils.js";
interface Props {
class?: string;
}
let { class: className }: Props = $props();
const filters = [
{ label: "For 9 countries, on weekends", href: "#" },
{ label: "North America region", href: "#" },
];
</script>
<Card.Root class={cn("", className)}>
<Card.Header class="pb-3">
<Card.Title class="text-sm font-medium text-muted-foreground">Frequent filters</Card.Title>
</Card.Header>
<Card.Content class="space-y-2">
{#each filters as filter}
<a
href={filter.href}
class="block text-sm font-medium text-foreground underline underline-offset-4 transition-colors hover:text-primary"
>
{filter.label}
</a>
{/each}
</Card.Content>
</Card.Root>

View File

@@ -0,0 +1,4 @@
export { default as FrequentFilters } from "./frequent-filters.svelte";
export { default as MetricCard } from "./metric-card.svelte";
export { default as PeakTrafficChart } from "./peak-traffic-chart.svelte";
export { default as PerformanceTable } from "./performance-table.svelte";

View File

@@ -0,0 +1,78 @@
<script lang="ts">
import * as Card from "$lib/components/ui/card";
import { cn } from "$lib/utils.js";
interface Props {
label: string;
value: string;
change: number;
sparklineData?: number[];
class?: string;
}
let {
label,
value,
change,
sparklineData = [],
class: className,
}: Props = $props();
const isPositive = $derived(change >= 0);
const changeColor = $derived(isPositive ? "text-emerald-600" : "text-red-500");
const sparklineColor = $derived(isPositive ? "#10b981" : "#ef4444");
// Generate SVG path from sparkline data
function generateSparklinePath(data: number[]): string {
if (data.length < 2) {
return "";
}
const min = Math.min(...data);
const max = Math.max(...data);
const range = max - min || 1;
const width = 80;
const height = 32;
const padding = 2;
const points = data.map((val, i) => {
const x = (i / (data.length - 1)) * width;
const y = height - padding - ((val - min) / range) * (height - padding * 2);
return `${x},${y}`;
});
return `M${points.join(" L")}`;
}
const sparklinePath = $derived(generateSparklinePath(sparklineData));
</script>
<Card.Root class={cn("relative overflow-hidden", className)}>
<Card.Content class="p-4">
<div class="flex items-start justify-between">
<div class="space-y-1">
<p class="text-sm text-muted-foreground">{label}</p>
<div class="flex items-baseline gap-2">
<span class="text-2xl font-semibold tracking-tight">{value}</span>
<span class={cn("text-sm font-medium", changeColor)}>
{isPositive ? "+" : ""}{change.toFixed(2)}%
</span>
</div>
</div>
{#if sparklineData.length > 0}
<div class="flex-shrink-0">
<svg width="80" height="32" class="overflow-visible">
<path
d={sparklinePath}
fill="none"
stroke={sparklineColor}
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</div>
{/if}
</div>
</Card.Content>
</Card.Root>

View File

@@ -0,0 +1,105 @@
<script lang="ts">
import * as Card from "$lib/components/ui/card";
import { cn } from "$lib/utils.js";
interface Props {
class?: string;
}
let { class: className }: Props = $props();
const hours = [
"0:00",
"3:00",
"6:00",
"9:00",
"12:00",
"15:00",
"18:00",
"21:00",
];
const days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
// Mock heatmap data - values from 0-100 representing traffic intensity
const heatmapData = [
[20, 35, 45, 60, 75, 55, 30], // 0:00
[15, 25, 40, 55, 65, 45, 25], // 3:00
[25, 40, 55, 70, 80, 60, 35], // 6:00
[35, 50, 65, 80, 90, 70, 45], // 9:00
[40, 55, 70, 85, 95, 75, 50], // 12:00
[45, 60, 75, 90, 85, 70, 45], // 15:00
[50, 65, 80, 75, 70, 55, 40], // 18:00
[35, 50, 60, 55, 50, 40, 30], // 21:00
];
function getBarHeight(value: number): number {
return (value / 100) * 40;
}
function getBarColor(value: number): string {
if (value >= 80) {
return "#374151";
}
if (value >= 60) {
return "#6b7280";
}
if (value >= 40) {
return "#9ca3af";
}
return "#d1d5db";
}
</script>
<Card.Root class={cn("", className)}>
<Card.Header class="pb-4">
<Card.Title class="text-base font-medium">Peak Traffic Hours</Card.Title>
</Card.Header>
<Card.Content>
<div class="flex gap-4">
<!-- Y-axis labels -->
<div class="flex flex-col justify-between py-1 text-xs text-muted-foreground">
{#each hours as hour}
<span class="flex h-[40px] items-end">{hour}</span>
{/each}
</div>
<!-- Chart area -->
<div class="flex-1">
<div class="flex gap-2">
{#each days as _, dayIndex}
<div class="flex flex-1 flex-col gap-1">
{#each hours as __, hourIndex}
<div class="flex h-[40px] items-end justify-center">
<div
class="w-full max-w-[24px] rounded-sm transition-all"
style="height: {getBarHeight(heatmapData[hourIndex][dayIndex])}px; background-color: {getBarColor(heatmapData[hourIndex][dayIndex])}"
></div>
</div>
{/each}
</div>
{/each}
</div>
<!-- X-axis labels -->
<div class="mt-2 flex gap-2">
{#each days as day}
<div class="flex-1 text-center text-xs text-muted-foreground">{day}</div>
{/each}
</div>
</div>
</div>
<!-- Legend -->
<div class="mt-4 flex items-center justify-center gap-4 text-xs text-muted-foreground">
<div class="flex items-center gap-1.5">
<div class="h-3 w-3 rounded-sm bg-muted"></div>
<span>This month</span>
</div>
<div class="flex items-center gap-1.5">
<div class="h-3 w-3 rounded-sm bg-gray-300"></div>
<span>Last month</span>
</div>
</div>
</Card.Content>
</Card.Root>

View File

@@ -0,0 +1,141 @@
<script lang="ts">
import { Badge } from "$lib/components/ui/badge";
import * as Card from "$lib/components/ui/card";
import * as Table from "$lib/components/ui/table";
import * as Tabs from "$lib/components/ui/tabs";
import { cn } from "$lib/utils.js";
interface Props {
class?: string;
}
let { class: className }: Props = $props();
const tabs = [
{ id: "domain", label: "Domain", count: 3 },
{ id: "country", label: "Country", count: 6 },
{ id: "source", label: "Source", count: null },
{ id: "ad-unit", label: "Ad Unit", count: null },
{ id: "key-value", label: "Key Value", count: null },
];
let activeTab = $state("ad-unit");
const tableData = [
{
id: 1,
name: "#ad-unitName",
revenue: "0,000,000",
revPercent: "22.00%",
impressions: "2,422,000",
impPercent: "####",
},
{
id: 2,
name: "#ad-unitName",
revenue: "0,000,000",
revPercent: "22.00%",
impressions: "2,422,000",
impPercent: "####",
},
{
id: 3,
name: "#ad-unitName",
revenue: "0,000,000",
revPercent: "22.00%",
impressions: "2,422,000",
impPercent: "####",
},
{
id: 4,
name: "#ad-unitName",
revenue: "0,000,000",
revPercent: "22.00%",
impressions: "2,422,000",
impPercent: "####",
},
{
id: 5,
name: "#ad-unitName",
revenue: "0,000,000",
revPercent: "22.00%",
impressions: "2,422,000",
impPercent: "####",
},
{
id: 6,
name: "#ad-unitName",
revenue: "0,000,000",
revPercent: "22.00%",
impressions: "2,422,000",
impPercent: "####",
},
];
</script>
<Card.Root class={cn("", className)}>
<Card.Header class="pb-3">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<Card.Title class="text-base font-medium">Performance by</Card.Title>
<svg class="h-4 w-4 text-muted-foreground" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="m9 18 6-6-6-6" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</div>
<Tabs.Root bind:value={activeTab}>
<Tabs.List class="h-auto gap-1 bg-transparent p-0">
{#each tabs as tab}
<Tabs.Trigger
value={tab.id}
class="gap-1.5 px-3 py-1.5 text-sm font-normal data-[state=active]:bg-transparent data-[state=active]:font-medium data-[state=active]:shadow-none"
>
{tab.label}
{#if tab.count}
<Badge variant="secondary" class="ml-1 h-5 min-w-5 rounded-full px-1.5 text-xs">
{tab.count}
</Badge>
{/if}
</Tabs.Trigger>
{/each}
</Tabs.List>
</Tabs.Root>
</div>
</Card.Header>
<Card.Content class="p-0">
<Table.Root>
<Table.Header>
<Table.Row class="hover:bg-transparent">
<Table.Head class="w-12"></Table.Head>
<Table.Head class="font-normal text-muted-foreground">Ad unit</Table.Head>
<Table.Head class="text-right font-normal text-muted-foreground">Total revenue</Table.Head>
<Table.Head class="text-right font-normal text-muted-foreground">
<div class="flex items-center justify-end gap-1">
% of rev.
<svg class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="m18 15-6-6-6 6" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</div>
</Table.Head>
<Table.Head class="text-right font-normal text-muted-foreground">Total impressions</Table.Head>
<Table.Head class="text-right font-normal text-muted-foreground">% of impressions</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each tableData as row}
<Table.Row>
<Table.Cell class="w-12">
<div class="h-4 w-4 rounded border border-border bg-muted"></div>
</Table.Cell>
<Table.Cell class="font-mono text-sm text-muted-foreground">{row.name}</Table.Cell>
<Table.Cell class="text-right tabular-nums">{row.revenue}</Table.Cell>
<Table.Cell class="text-right font-semibold tabular-nums">{row.revPercent}</Table.Cell>
<Table.Cell class="text-right tabular-nums">{row.impressions}</Table.Cell>
<Table.Cell class="text-right tabular-nums text-muted-foreground">{row.impPercent}</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</Card.Content>
</Card.Root>

View File

@@ -0,0 +1,68 @@
<script lang="ts">
import { Badge } from "$lib/components/ui/badge";
import { Button } from "$lib/components/ui/button";
import { cn } from "$lib/utils.js";
interface Props {
title: string;
class?: string;
}
let { title, class: className }: Props = $props();
const filters = [
{ label: "This month", removable: true },
{ label: "in 6 countries, in 3 domains", removable: true },
];
</script>
<header
class={cn(
"flex h-16 items-center justify-between border-b border-border bg-card px-6",
className
)}
>
<h1 class="text-xl font-semibold tracking-tight text-foreground">{title}</h1>
<div class="flex items-center gap-3">
<!-- Filter Badges -->
{#each filters as filter}
<Badge variant="secondary" class="gap-1.5 px-3 py-1.5 text-sm font-normal">
{filter.label}
{#if filter.removable}
<button class="ml-1 rounded-full hover:bg-muted-foreground/20 p-0.5" aria-label="Remove filter">
<svg class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 6L6 18M6 6l12 12" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
{/if}
</Badge>
{/each}
<!-- Search -->
<Button variant="ghost" size="icon" class="h-9 w-9">
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.35-4.35" stroke-linecap="round" />
</svg>
<span class="sr-only">Search</span>
</Button>
<!-- Settings -->
<Button variant="ghost" size="icon" class="h-9 w-9">
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3" />
<path d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83" stroke-linecap="round" />
</svg>
<span class="sr-only">Settings</span>
</Button>
<!-- Create New Button -->
<Button class="gap-2">
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 5v14M5 12h14" stroke-linecap="round" />
</svg>
Create new
</Button>
</div>
</header>

View File

@@ -0,0 +1,112 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
interface Props {
class?: string;
}
let { class: className }: Props = $props();
const navItems = [
{ icon: "home", href: "/", label: "Home" },
{ icon: "chart", href: "/performance", label: "Performance" },
{ icon: "document", href: "/reports", label: "Reports" },
];
</script>
<aside
class={cn(
"flex h-screen w-16 flex-col items-center border-r border-border bg-card py-4",
className
)}
>
<!-- Logo -->
<a
href="/"
aria-label="Home"
class="mb-8 flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-orange-500 to-orange-600 shadow-md transition-transform hover:scale-105"
>
<svg
class="h-6 w-6 text-white"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
</svg>
</a>
<!-- Navigation -->
<nav class="flex flex-1 flex-col items-center gap-2">
{#each navItems as item}
<a
href={item.href}
class="group relative flex h-10 w-10 items-center justify-center rounded-lg text-muted-foreground transition-all hover:bg-accent hover:text-foreground"
aria-label={item.label}
>
{#if item.icon === "home"}
<svg
class="h-5 w-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
>
<path
d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path d="M9 22V12h6v10" stroke-linecap="round" stroke-linejoin="round" />
</svg>
{:else if item.icon === "chart"}
<svg
class="h-5 w-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
>
<path
d="M18 20V10M12 20V4M6 20v-6"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
{:else if item.icon === "document"}
<svg
class="h-5 w-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
>
<path
d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8l-6-6z"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path d="M14 2v6h6M16 13H8M16 17H8M10 9H8" stroke-linecap="round" stroke-linejoin="round" />
</svg>
{/if}
<!-- Tooltip -->
<span
class="pointer-events-none absolute left-full ml-2 whitespace-nowrap rounded-md bg-foreground px-2 py-1 text-xs text-background opacity-0 shadow-lg transition-opacity group-hover:opacity-100"
>
{item.label}
</span>
</a>
{/each}
</nav>
<!-- User Avatar -->
<div class="mt-auto">
<button
class="flex h-10 w-10 items-center justify-center rounded-full bg-gradient-to-br from-emerald-400 to-teal-500 text-sm font-medium text-white shadow-sm transition-transform hover:scale-105"
>
JD
</button>
</div>
</aside>

View File

@@ -0,0 +1,26 @@
<script lang="ts">
import type { Snippet } from "svelte";
import { cn } from "$lib/utils.js";
import AppHeader from "./app-header.svelte";
import AppSidebar from "./app-sidebar.svelte";
interface Props {
title: string;
children: Snippet;
class?: string;
}
let { title, children, class: className }: Props = $props();
</script>
<div class="flex h-screen overflow-hidden bg-background">
<AppSidebar />
<div class="flex flex-1 flex-col overflow-hidden">
<AppHeader {title} />
<main class={cn("flex-1 overflow-auto p-6", className)}>
{@render children()}
</main>
</div>
</div>

View File

@@ -0,0 +1,3 @@
export { default as AppHeader } from "./app-header.svelte";
export { default as AppSidebar } from "./app-sidebar.svelte";
export { default as DashboardLayout } from "./dashboard-layout.svelte";

View File

@@ -0,0 +1,51 @@
<script lang="ts" module>
import { tv, type VariantProps } from "tailwind-variants";
export const badgeVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3",
variants: {
variant: {
default:
"bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent",
secondary:
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent",
destructive:
"bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
});
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
</script>
<script lang="ts">
import type { HTMLAnchorAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
href,
class: className,
variant = "default",
children,
...restProps
}: WithElementRef<HTMLAnchorAttributes> & {
variant?: BadgeVariant;
} = $props();
</script>
<svelte:element
this={href ? "a" : "span"}
bind:this={ref}
data-slot="badge"
{href}
class={cn(badgeVariants({ variant }), className)}
{...restProps}
>
{@render children?.()}
</svelte:element>

View File

@@ -0,0 +1,5 @@
export {
type BadgeVariant,
badgeVariants,
default as Badge,
} from "./badge.svelte";

View File

@@ -0,0 +1,88 @@
<script lang="ts" module>
import type {
HTMLAnchorAttributes,
HTMLButtonAttributes,
} from "svelte/elements";
import { tv, type VariantProps } from "tailwind-variants";
import { cn, type WithElementRef } from "$lib/utils.js";
export const buttonVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
variants: {
variant: {
default:
"bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs",
destructive:
"bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white shadow-xs",
outline:
"bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border shadow-xs",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-xs",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
</script>
<script lang="ts">
let {
class: className,
variant = "default",
size = "default",
ref = $bindable(null),
href = undefined,
type = "button",
disabled,
children,
...restProps
}: ButtonProps = $props();
</script>
{#if href}
<a
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
href={disabled ? undefined : href}
aria-disabled={disabled}
role={disabled ? "link" : undefined}
tabindex={disabled ? -1 : undefined}
{...restProps}
>
{@render children?.()}
</a>
{:else}
<button
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
{type}
{disabled}
{...restProps}
>
{@render children?.()}
</button>
{/if}

View File

@@ -0,0 +1,17 @@
import Root, {
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants,
} from "./button.svelte";
export {
Root,
type ButtonProps as Props,
//
Root as Button,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
};

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-action"
class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,15 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div bind:this={ref} data-slot="card-content" class={cn("px-6", className)} {...restProps}>
{@render children?.()}
</div>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
</script>
<p
bind:this={ref}
data-slot="card-description"
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
>
{@render children?.()}
</p>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-footer"
class={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-header"
class={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-title"
class={cn("leading-none font-semibold", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card"
class={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,25 @@
import Root from "./card.svelte";
import Action from "./card-action.svelte";
import Content from "./card-content.svelte";
import Description from "./card-description.svelte";
import Footer from "./card-footer.svelte";
import Header from "./card-header.svelte";
import Title from "./card-title.svelte";
export {
Root,
Content,
Description,
Footer,
Header,
Title,
Action,
//
Root as Card,
Content as CardContent,
Description as CardDescription,
Footer as CardFooter,
Header as CardHeader,
Title as CardTitle,
Action as CardAction,
};

View File

@@ -0,0 +1,7 @@
import Root from "./separator.svelte";
export {
Root,
//
Root as Separator,
};

View File

@@ -0,0 +1,21 @@
<script lang="ts">
import { Separator as SeparatorPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
"data-slot": dataSlot = "separator",
...restProps
}: SeparatorPrimitive.RootProps = $props();
</script>
<SeparatorPrimitive.Root
bind:ref
data-slot={dataSlot}
class={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
)}
{...restProps}
/>

View File

@@ -0,0 +1,28 @@
import Root from "./table.svelte";
import Body from "./table-body.svelte";
import Caption from "./table-caption.svelte";
import Cell from "./table-cell.svelte";
import Footer from "./table-footer.svelte";
import Head from "./table-head.svelte";
import Header from "./table-header.svelte";
import Row from "./table-row.svelte";
export {
Root,
Body,
Caption,
Cell,
Footer,
Head,
Header,
Row,
//
Root as Table,
Body as TableBody,
Caption as TableCaption,
Cell as TableCell,
Footer as TableFooter,
Head as TableHead,
Header as TableHeader,
Row as TableRow,
};

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
</script>
<tbody
bind:this={ref}
data-slot="table-body"
class={cn("[&_tr:last-child]:border-0", className)}
{...restProps}
>
{@render children?.()}
</tbody>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>
<caption
bind:this={ref}
data-slot="table-caption"
class={cn("text-muted-foreground mt-4 text-sm", className)}
{...restProps}
>
{@render children?.()}
</caption>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLTdAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLTdAttributes> = $props();
</script>
<td
bind:this={ref}
data-slot="table-cell"
class={cn(
"bg-clip-padding p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pe-0",
className
)}
{...restProps}
>
{@render children?.()}
</td>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
</script>
<tfoot
bind:this={ref}
data-slot="table-footer"
class={cn("bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", className)}
{...restProps}
>
{@render children?.()}
</tfoot>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLThAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLThAttributes> = $props();
</script>
<th
bind:this={ref}
data-slot="table-head"
class={cn(
"text-foreground h-10 bg-clip-padding px-2 text-start align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pe-0",
className
)}
{...restProps}
>
{@render children?.()}
</th>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLTableSectionElement>> = $props();
</script>
<thead
bind:this={ref}
data-slot="table-header"
class={cn("[&_tr]:border-b", className)}
{...restProps}
>
{@render children?.()}
</thead>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLTableRowElement>> = $props();
</script>
<tr
bind:this={ref}
data-slot="table-row"
class={cn(
"hover:[&,&>svelte-css-wrapper]:[&>th,td]:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className
)}
{...restProps}
>
{@render children?.()}
</tr>

View File

@@ -0,0 +1,22 @@
<script lang="ts">
import type { HTMLTableAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLTableAttributes> = $props();
</script>
<div data-slot="table-container" class="relative w-full overflow-x-auto">
<table
bind:this={ref}
data-slot="table"
class={cn("w-full caption-bottom text-sm", className)}
{...restProps}
>
{@render children?.()}
</table>
</div>

View File

@@ -0,0 +1,16 @@
import Root from "./tabs.svelte";
import Content from "./tabs-content.svelte";
import List from "./tabs-list.svelte";
import Trigger from "./tabs-trigger.svelte";
export {
Root,
Content,
List,
Trigger,
//
Root as Tabs,
Content as TabsContent,
List as TabsList,
Trigger as TabsTrigger,
};

View File

@@ -0,0 +1,17 @@
<script lang="ts">
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: TabsPrimitive.ContentProps = $props();
</script>
<TabsPrimitive.Content
bind:ref
data-slot="tabs-content"
class={cn("flex-1 outline-none", className)}
{...restProps}
/>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: TabsPrimitive.ListProps = $props();
</script>
<TabsPrimitive.List
bind:ref
data-slot="tabs-list"
class={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
className
)}
{...restProps}
/>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: TabsPrimitive.TriggerProps = $props();
</script>
<TabsPrimitive.Trigger
bind:ref
data-slot="tabs-trigger"
class={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...restProps}
/>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
value = $bindable(""),
class: className,
...restProps
}: TabsPrimitive.RootProps = $props();
</script>
<TabsPrimitive.Root
bind:ref
bind:value
data-slot="tabs"
class={cn("flex flex-col gap-2", className)}
{...restProps}
/>

View File

@@ -0,0 +1,17 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type WithoutChildren<T> = T extends { children?: any }
? Omit<T, "children">
: T;
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & {
ref?: U | null;
};

View File

@@ -0,0 +1,14 @@
<script lang="ts">
import "../app.css";
import type { Snippet } from "svelte";
interface Props {
children: Snippet;
}
let { children }: Props = $props();
</script>
<main class="mx-auto max-w-7xl p-4">
{@render children()}
</main>

View File

@@ -0,0 +1,3 @@
// Disable SSR for SPA mode
export const ssr = false;
export const prerender = true;

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import { greet } from "@publisher/utils";
const message = greet("Publisher Dashboard");
</script>
<svelte:head>
<title>Publisher Dashboard</title>
</svelte:head>
<div class="space-y-6">
<h1 class="text-3xl font-bold tracking-tight">Publisher Dashboard</h1>
<p class="text-muted-foreground">{message}</p>
<nav class="flex gap-4">
<a
href="/settings"
class="text-primary underline-offset-4 hover:underline"
>
Settings
</a>
</nav>
</div>

View File

@@ -0,0 +1,2 @@
export const ssr = false;
export const prerender = true;

View File

@@ -0,0 +1,68 @@
<script lang="ts">
import FrequentFilters from "$lib/components/dashboard/frequent-filters.svelte";
import MetricCard from "$lib/components/dashboard/metric-card.svelte";
import PeakTrafficChart from "$lib/components/dashboard/peak-traffic-chart.svelte";
import PerformanceTable from "$lib/components/dashboard/performance-table.svelte";
import DashboardLayout from "$lib/components/layout/dashboard-layout.svelte";
const metrics = [
{
label: "Average daily revenue",
value: "$64,252.02",
change: 2.82,
sparklineData: [30, 35, 32, 40, 45, 42, 50, 55, 52, 58, 62, 60],
},
{
label: "Cost per mille",
value: "$4.32",
change: 0.82,
sparklineData: [20, 22, 25, 23, 28, 30, 32, 35, 33, 38, 40, 42],
},
{
label: "Revenue per mille",
value: "$4.32",
change: 0.82,
sparklineData: [25, 28, 30, 32, 35, 38, 40, 42, 45, 48, 50, 52],
},
{
label: "Invalid Traffic",
value: "52.01%",
change: -0.82,
sparklineData: [60, 58, 55, 52, 50, 48, 45, 42, 40, 38, 35, 32],
},
];
</script>
<svelte:head>
<title>SSP Network Performance - Publisher Dashboard</title>
</svelte:head>
<DashboardLayout title="SSP Network Performance">
<div class="space-y-6">
<!-- Metric Cards Row -->
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{#each metrics as metric}
<MetricCard
label={metric.label}
value={metric.value}
change={metric.change}
sparklineData={metric.sparklineData}
/>
{/each}
</div>
<!-- Main Content Grid -->
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
<!-- Performance Table - 2 columns -->
<div class="lg:col-span-2">
<PerformanceTable />
</div>
<!-- Right Sidebar -->
<div class="space-y-6">
<PeakTrafficChart />
<FrequentFilters />
</div>
</div>
</div>
</DashboardLayout>

View File

@@ -0,0 +1,14 @@
<svelte:head>
<title>Settings - Publisher Dashboard</title>
</svelte:head>
<div class="space-y-6">
<h1 class="text-3xl font-bold tracking-tight">Settings</h1>
<p class="text-muted-foreground">Configure your publisher settings here.</p>
<nav>
<a href="/" class="text-primary underline-offset-4 hover:underline">
Back to Home
</a>
</nav>
</div>

View File

@@ -0,0 +1,20 @@
import adapter from "@sveltejs/adapter-static";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
fallback: "index.html", // SPA mode - all routes fall back to index.html
}),
paths: {
base: "",
},
prerender: {
handleHttpError: "warn",
},
},
};
export default config;

View File

@@ -0,0 +1,15 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler",
"verbatimModuleSyntax": true
}
}

View File

@@ -0,0 +1,7 @@
import { sveltekit } from "@sveltejs/kit/vite";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [tailwindcss(), sveltekit()],
});