create-dowel-app 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,66 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+
5
+ import type { LedgerAction } from "@/components/ui/ai-action-ledger";
6
+ import type { PlanStep } from "@/components/ui/ai-agent-plan";
7
+ import type { ApprovalDecision } from "@/components/ui/ai-approval-request";
8
+ import { AgentConsoleBlock } from "@/components/blocks/agent-console";
9
+
10
+ const PLAN: PlanStep[] = [
11
+ { id: "1", title: "Scan the contact table", status: "done", detail: "18,402 records" },
12
+ { id: "2", title: "Group likely duplicates", status: "done", detail: "41 groups" },
13
+ { id: "3", title: "Merge each group", status: "running" },
14
+ { id: "4", title: "Notify the record owners", status: "pending" },
15
+ ];
16
+
17
+ const ACTIONS: LedgerAction[] = [
18
+ {
19
+ id: "a1",
20
+ summary: "Merged 3 contacts into Acme Inc.",
21
+ reversibility: "revertible",
22
+ status: "applied",
23
+ },
24
+ {
25
+ id: "a2",
26
+ summary: "Refunded $40.00 to Acme Inc.",
27
+ reversibility: "compensable",
28
+ status: "applied",
29
+ },
30
+ {
31
+ id: "a3",
32
+ summary: "Emailed 12 record owners",
33
+ reversibility: "irreversible",
34
+ status: "applied",
35
+ },
36
+ ];
37
+
38
+ export default function AgentsPage() {
39
+ const [decision, setDecision] = useState<ApprovalDecision | undefined>(undefined);
40
+
41
+ return (
42
+ <AgentConsoleBlock
43
+ title="Deduplicate contacts"
44
+ description="Replace this with a real run from your own agent."
45
+ state={decision ? "working" : "waiting"}
46
+ plan={PLAN}
47
+ tokensUsed={42_180}
48
+ tokenLimit={128_000}
49
+ approval={{
50
+ tool: "delete_contacts",
51
+ summary: "Delete 3 contacts that look like duplicates of existing records.",
52
+ arguments: { ids: "c_8812, c_8813, c_8901", reason: "duplicate of c_1204" },
53
+ fields: [
54
+ { name: "ids", label: "Contact ids", readOnly: true },
55
+ { name: "reason", label: "Reason" },
56
+ ],
57
+ irreversible: "Deleted contacts cannot be restored.",
58
+ decision,
59
+ }}
60
+ onApprovalDecision={setDecision}
61
+ actions={ACTIONS}
62
+ onRevert={() => undefined}
63
+ onStop={() => undefined}
64
+ />
65
+ );
66
+ }
@@ -0,0 +1,42 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+
5
+ import { AiChatBlock, type ChatMessage } from "@/components/blocks/ai-chat";
6
+
7
+ /**
8
+ * Wire `onSend` to your own endpoint.
9
+ *
10
+ * The reply here is canned so the page works the moment it is generated. Swap
11
+ * the timeout for a fetch and stream tokens into the last message — the block
12
+ * renders `streaming` as a caret rather than as a spinner that hides the text
13
+ * already written.
14
+ */
15
+ export default function ChatPage() {
16
+ const [messages, setMessages] = useState<ChatMessage[]>([]);
17
+ const [busy, setBusy] = useState(false);
18
+
19
+ const send = (content: string) => {
20
+ const question: ChatMessage = { id: crypto.randomUUID(), from: "user", content };
21
+ setMessages((previous) => [...previous, question]);
22
+ setBusy(true);
23
+
24
+ window.setTimeout(() => {
25
+ setMessages((previous) => [
26
+ ...previous,
27
+ {
28
+ id: crypto.randomUUID(),
29
+ from: "assistant",
30
+ content: "Replace this with a response from your model.",
31
+ },
32
+ ]);
33
+ setBusy(false);
34
+ }, 600);
35
+ };
36
+
37
+ return (
38
+ <div className="h-[calc(100dvh-8rem)]">
39
+ <AiChatBlock messages={messages} onSend={send} busy={busy} waiting={busy} />
40
+ </div>
41
+ );
42
+ }
@@ -0,0 +1,58 @@
1
+ import { AiDashboardBlock } from "@/components/blocks/ai-dashboard";
2
+
3
+ export default function UsagePage() {
4
+ return (
5
+ <AiDashboardBlock
6
+ tokens={15_480_000}
7
+ previousTokens={11_200_000}
8
+ spend="$252.50"
9
+ spendValue={252.5}
10
+ previousSpendValue={198.4}
11
+ runs={3_470}
12
+ previousRuns={2_910}
13
+ failureRate={0.041}
14
+ previousFailureRate={0.062}
15
+ models={[
16
+ { id: "opus", model: "claude-opus-5", runs: 118, tokens: 4_240_000, cost: "$182.40" },
17
+ {
18
+ id: "sonnet",
19
+ model: "claude-sonnet-5",
20
+ runs: 942,
21
+ tokens: 8_060_000,
22
+ cost: "$61.20",
23
+ },
24
+ {
25
+ id: "haiku",
26
+ model: "claude-haiku-4-5",
27
+ runs: 2_410,
28
+ tokens: 3_180_000,
29
+ cost: "$8.90",
30
+ },
31
+ ]}
32
+ recentRuns={[
33
+ {
34
+ id: "r1",
35
+ title: "Deduplicate contacts",
36
+ state: "working",
37
+ model: "claude-opus-5",
38
+ tokens: 42_180,
39
+ href: "/app/agents",
40
+ },
41
+ {
42
+ id: "r2",
43
+ title: "Draft weekly digest",
44
+ state: "waiting",
45
+ model: "claude-sonnet-5",
46
+ tokens: 18_400,
47
+ },
48
+ {
49
+ id: "r3",
50
+ title: "Classify inbound tickets",
51
+ state: "done",
52
+ model: "claude-haiku-4-5",
53
+ tokens: 6_120,
54
+ },
55
+ ]}
56
+ />
57
+ );
58
+ }
@@ -0,0 +1,25 @@
1
+ import Link from "next/link";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+
5
+ export default function Home() {
6
+ return (
7
+ <main className="mx-auto flex min-h-dvh max-w-3xl flex-col justify-center gap-6 px-6 py-16">
8
+ <h1 className="text-3xl font-semibold tracking-tight text-balance">__PROJECT_NAME__</h1>
9
+ <p className="text-pretty text-muted-foreground">
10
+ An AI product scaffolded with __LIBRARY_NAME__. The chat surface, agent console and
11
+ usage dashboard are source files in this repository — including the parts most component
12
+ sets do not ship: the approval before a tool runs, and the ledger of what it did
13
+ afterwards.
14
+ </p>
15
+ <div className="flex flex-wrap gap-3">
16
+ <Button asChild>
17
+ <Link href="/app">Open the app</Link>
18
+ </Button>
19
+ <Button variant="outline" asChild>
20
+ <a href="__DOCS_URL__">Documentation</a>
21
+ </Button>
22
+ </div>
23
+ </main>
24
+ );
25
+ }
@@ -0,0 +1,48 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ import {
4
+ Sidebar,
5
+ SidebarContent,
6
+ SidebarGroup,
7
+ SidebarHeader,
8
+ SidebarInset,
9
+ SidebarProvider,
10
+ SidebarTrigger,
11
+ } from "@/components/ui/sidebar";
12
+
13
+ import { AppNav, type AppNavLink } from "@/components/app-nav";
14
+
15
+ const LINKS: AppNavLink[] = __APP_LINKS__;
16
+
17
+ export default function AppLayout({ children }: { children: ReactNode }) {
18
+ return (
19
+ <SidebarProvider>
20
+ <div className="flex min-h-dvh">
21
+ <Sidebar label="Application">
22
+ <SidebarHeader>
23
+ <SidebarTrigger />
24
+ <span className="truncate font-semibold tracking-tight">__PROJECT_NAME__</span>
25
+ </SidebarHeader>
26
+
27
+ <SidebarContent>
28
+ <SidebarGroup>
29
+ <AppNav links={LINKS} />
30
+ </SidebarGroup>
31
+ </SidebarContent>
32
+ </Sidebar>
33
+
34
+ <SidebarInset className="px-6 py-8">
35
+ {/* Skip link first in the tab order, so a keyboard user is not made
36
+ to walk the navigation on every page. */}
37
+ <a
38
+ href="#content"
39
+ className="sr-only rounded-md px-2 py-1 text-sm underline-offset-4 focus-visible:not-sr-only focus-visible:ring-2 focus-visible:ring-ring/55"
40
+ >
41
+ Skip to content
42
+ </a>
43
+ <div id="content">{children}</div>
44
+ </SidebarInset>
45
+ </div>
46
+ </SidebarProvider>
47
+ );
48
+ }
@@ -0,0 +1,41 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { usePathname } from "next/navigation";
5
+
6
+ import {
7
+ SidebarMenu,
8
+ SidebarMenuButton,
9
+ SidebarMenuItem,
10
+ SidebarMenuLabel,
11
+ } from "@/components/ui/sidebar";
12
+
13
+ export interface AppNavLink {
14
+ href: string;
15
+ label: string;
16
+ }
17
+
18
+ /**
19
+ * The application's navigation entries.
20
+ *
21
+ * `asChild` so Next's Link does the routing while the sidebar does the styling
22
+ * and the semantics — `aria-current` on the active entry comes from
23
+ * SidebarMenuButton rather than being hand-wired here.
24
+ */
25
+ export function AppNav({ links }: { links: AppNavLink[] }) {
26
+ const pathname = usePathname();
27
+
28
+ return (
29
+ <SidebarMenu>
30
+ {links.map((link) => (
31
+ <SidebarMenuItem key={link.href}>
32
+ <SidebarMenuButton asChild isActive={pathname === link.href}>
33
+ <Link href={link.href}>
34
+ <SidebarMenuLabel>{link.label}</SidebarMenuLabel>
35
+ </Link>
36
+ </SidebarMenuButton>
37
+ </SidebarMenuItem>
38
+ ))}
39
+ </SidebarMenu>
40
+ );
41
+ }
@@ -0,0 +1,16 @@
1
+ node_modules
2
+ .next
3
+ out
4
+ build
5
+ .DS_Store
6
+ *.pem
7
+
8
+ npm-debug.log*
9
+ yarn-debug.log*
10
+ yarn-error.log*
11
+ .pnpm-debug.log*
12
+
13
+ .env*.local
14
+ .vercel
15
+ *.tsbuildinfo
16
+ next-env.d.ts
@@ -0,0 +1,5 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const config: NextConfig = {};
4
+
5
+ export default config;
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "__PROJECT_NAME__",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "next dev",
8
+ "build": "next build",
9
+ "start": "next start",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "next": "16.3.3",
14
+ "react": "19.2.8",
15
+ "react-dom": "19.2.8"
16
+ },
17
+ "devDependencies": {
18
+ "@tailwindcss/postcss": "4.3.3",
19
+ "@types/node": "26.4.0",
20
+ "@types/react": "19.2.18",
21
+ "@types/react-dom": "19.2.5",
22
+ "tailwindcss": "4.3.3",
23
+ "typescript": "6.0.3"
24
+ }
25
+ }
@@ -0,0 +1,5 @@
1
+ export default {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
@@ -0,0 +1 @@
1
+ @import "tailwindcss";
@@ -0,0 +1,22 @@
1
+ import type { Metadata } from "next";
2
+ import type { ReactNode } from "react";
3
+
4
+ import "./globals.css";
5
+
6
+ export const metadata: Metadata = {
7
+ title: "__PROJECT_NAME__",
8
+ description: "Built with __LIBRARY_NAME__.",
9
+ };
10
+
11
+ export default function RootLayout({ children }: { children: ReactNode }) {
12
+ return (
13
+ /*
14
+ `data-theme` selects the preset and the `dark` class selects the mode;
15
+ they are independent, so every preset works in both. Swap the preset here,
16
+ or drive it from a theme switcher — no component file changes either way.
17
+ */
18
+ <html lang="en" data-theme="__THEME__" suppressHydrationWarning>
19
+ <body className="bg-background text-foreground antialiased">{children}</body>
20
+ </html>
21
+ );
22
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["dom", "dom.iterable", "ES2022"],
5
+ "module": "preserve",
6
+ "moduleResolution": "bundler",
7
+ "jsx": "preserve",
8
+ "allowJs": true,
9
+ "strict": true,
10
+ "noEmit": true,
11
+ "esModuleInterop": true,
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "incremental": true,
15
+ "skipLibCheck": true,
16
+ "plugins": [{ "name": "next" }],
17
+ "paths": {
18
+ "@/*": ["./src/*"]
19
+ }
20
+ },
21
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
22
+ "exclude": ["node_modules"]
23
+ }
@@ -0,0 +1,62 @@
1
+ "use client";
2
+
3
+ import { AnalyticsBlock, type AnalyticsPoint } from "@/components/blocks/analytics";
4
+
5
+ /*
6
+ A client component, because of the `format` below.
7
+
8
+ The blocks are client components, and a function cannot cross the server
9
+ boundary into one — React has no way to send it. So a page that hands a block
10
+ a formatter, a comparator or an event handler has to be a client component
11
+ itself. Data-only props are fine from the server; the moment a function is
12
+ among them, this directive is required, and the build says so rather than
13
+ failing at runtime.
14
+ */
15
+
16
+ const DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
17
+
18
+ const SERIES: AnalyticsPoint[] = [1240, 1810, 1520, 2140, 2380, 1180, 940].map(
19
+ (value, index) => ({
20
+ at: `2026-03-0${String(index + 1)}`,
21
+ label: DAYS[index] ?? "",
22
+ value,
23
+ }),
24
+ );
25
+
26
+ export default function AnalyticsPage() {
27
+ return (
28
+ <AnalyticsBlock
29
+ metrics={[
30
+ {
31
+ id: "visitors",
32
+ label: "Visitors",
33
+ value: 11_210,
34
+ previous: 9_840,
35
+ comparisonLabel: "vs last week",
36
+ },
37
+ {
38
+ id: "signups",
39
+ label: "Signups",
40
+ value: 284,
41
+ previous: 231,
42
+ comparisonLabel: "vs last week",
43
+ },
44
+ {
45
+ id: "bounce",
46
+ label: "Bounce rate",
47
+ value: 38,
48
+ previous: 44,
49
+ polarity: "lower-is-better",
50
+ format: (value) => `${String(value)}%`,
51
+ },
52
+ { id: "sessions", label: "Sessions", value: 18_400, previous: 16_120 },
53
+ ]}
54
+ series={SERIES}
55
+ breakdown={[
56
+ { id: "search", label: "Organic search", value: 5820 },
57
+ { id: "direct", label: "Direct", value: 2940 },
58
+ { id: "referral", label: "Referral", value: 1610 },
59
+ ]}
60
+ />
61
+ );
62
+ }
@@ -0,0 +1,36 @@
1
+ import { BillingBlock } from "@/components/blocks/billing";
2
+
3
+ export default function BillingPage() {
4
+ return (
5
+ <BillingBlock
6
+ plan={{
7
+ name: "Team",
8
+ price: "$240",
9
+ interval: "per month, billed annually",
10
+ renewsAt: "2027-03-03",
11
+ renewsLabel: "3 March 2027",
12
+ }}
13
+ usage={[
14
+ { id: "seats", label: "Seats", used: 8, limit: 10, unit: "seats" },
15
+ { id: "storage", label: "Storage", used: 128, limit: 250, unit: "GB" },
16
+ ]}
17
+ paymentMethod={{ brand: "Visa", last4: "4242", expires: "04/2029" }}
18
+ invoices={[
19
+ {
20
+ id: "in_2",
21
+ at: "2026-02-01",
22
+ label: "1 February 2026",
23
+ amount: "$240.00",
24
+ status: "paid",
25
+ },
26
+ {
27
+ id: "in_1",
28
+ at: "2026-01-01",
29
+ label: "1 January 2026",
30
+ amount: "$240.00",
31
+ status: "paid",
32
+ },
33
+ ]}
34
+ />
35
+ );
36
+ }
@@ -0,0 +1,69 @@
1
+ import {
2
+ DashboardBlock,
3
+ type DashboardEvent,
4
+ type DashboardStat,
5
+ } from "@/components/blocks/dashboard";
6
+ import { OnboardingBlock, type OnboardingStep } from "@/components/blocks/onboarding";
7
+
8
+ /**
9
+ * Replace these with your own data.
10
+ *
11
+ * They are inline rather than fetched so the page renders the moment it is
12
+ * generated — a scaffold that needs a database before it shows anything is a
13
+ * scaffold nobody sees working.
14
+ */
15
+ const STATS: DashboardStat[] = [
16
+ {
17
+ id: "mrr",
18
+ label: "Monthly revenue",
19
+ value: "$48,120",
20
+ change: 12.4,
21
+ comparison: "on last month",
22
+ },
23
+ {
24
+ id: "users",
25
+ label: "Active users",
26
+ value: "2,410",
27
+ change: 4.2,
28
+ comparison: "on last week",
29
+ },
30
+ { id: "churn", label: "Churn", value: "1.8%", change: 0.4, higherIsBetter: false },
31
+ { id: "uptime", label: "Uptime", value: "99.98%" },
32
+ ];
33
+
34
+ const EVENTS: DashboardEvent[] = [
35
+ {
36
+ id: "1",
37
+ title: "Deployed to production",
38
+ at: "2026-03-04T09:12:00Z",
39
+ label: "12 minutes ago",
40
+ tone: "success",
41
+ },
42
+ {
43
+ id: "2",
44
+ title: "New customer: Acme Inc.",
45
+ at: "2026-03-04T08:40:00Z",
46
+ label: "44 minutes ago",
47
+ },
48
+ ];
49
+
50
+ const STEPS: OnboardingStep[] = [
51
+ { id: "account", title: "Create your account", status: "done" },
52
+ {
53
+ id: "team",
54
+ title: "Invite your team",
55
+ status: "current",
56
+ actionLabel: "Invite",
57
+ estimate: "2 minutes",
58
+ },
59
+ { id: "billing", title: "Add a payment method", status: "todo", actionLabel: "Add" },
60
+ ];
61
+
62
+ export default function AppPage() {
63
+ return (
64
+ <div className="flex flex-col gap-8">
65
+ <DashboardBlock stats={STATS} events={EVENTS} />
66
+ <OnboardingBlock steps={STEPS} />
67
+ </div>
68
+ );
69
+ }
@@ -0,0 +1,51 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+
5
+ import {
6
+ SettingsBlock,
7
+ type SettingsNotification,
8
+ type SettingsProfile,
9
+ } from "@/components/blocks/settings";
10
+
11
+ export default function SettingsPage() {
12
+ const [profile, setProfile] = useState<SettingsProfile>({
13
+ name: "Ada Lovelace",
14
+ email: "ada@example.com",
15
+ bio: "Replace this with the signed-in user.",
16
+ });
17
+
18
+ const [notifications, setNotifications] = useState<SettingsNotification[]>([
19
+ {
20
+ id: "product",
21
+ label: "Product updates",
22
+ description: "New features and changes worth knowing about.",
23
+ enabled: true,
24
+ },
25
+ {
26
+ id: "billing",
27
+ label: "Billing",
28
+ description: "Invoices, failed payments and plan changes.",
29
+ enabled: true,
30
+ },
31
+ {
32
+ id: "digest",
33
+ label: "Weekly digest",
34
+ description: "A summary of activity every Monday.",
35
+ enabled: false,
36
+ },
37
+ ]);
38
+
39
+ return (
40
+ <SettingsBlock
41
+ profile={profile}
42
+ notifications={notifications}
43
+ onSaveProfile={setProfile}
44
+ onToggleNotification={(id, enabled) => {
45
+ setNotifications((previous) =>
46
+ previous.map((entry) => (entry.id === id ? { ...entry, enabled } : entry)),
47
+ );
48
+ }}
49
+ />
50
+ );
51
+ }
@@ -0,0 +1,24 @@
1
+ import Link from "next/link";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+
5
+ export default function Home() {
6
+ return (
7
+ <main className="mx-auto flex min-h-dvh max-w-3xl flex-col justify-center gap-6 px-6 py-16">
8
+ <h1 className="text-3xl font-semibold tracking-tight text-balance">__PROJECT_NAME__</h1>
9
+ <p className="text-pretty text-muted-foreground">
10
+ A SaaS application scaffolded with __LIBRARY_NAME__. The dashboard, analytics, billing,
11
+ settings and onboarding surfaces are source files in this repository — open one and
12
+ change it.
13
+ </p>
14
+ <div className="flex flex-wrap gap-3">
15
+ <Button asChild>
16
+ <Link href="/app">Open the app</Link>
17
+ </Button>
18
+ <Button variant="outline" asChild>
19
+ <a href="__DOCS_URL__">Documentation</a>
20
+ </Button>
21
+ </div>
22
+ </main>
23
+ );
24
+ }