@dowel-ui/react 0.5.0 → 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.
- package/README.md +1 -1
- package/dist/blocks/admin-dashboard/meta.js +31 -0
- package/dist/blocks/admin-dashboard/meta.js.map +1 -0
- package/dist/blocks/agent-console/meta.js +27 -0
- package/dist/blocks/agent-console/meta.js.map +1 -0
- package/dist/blocks/ai-dashboard/meta.js +25 -0
- package/dist/blocks/ai-dashboard/meta.js.map +1 -0
- package/dist/blocks/ai-workspace/meta.js +33 -0
- package/dist/blocks/ai-workspace/meta.js.map +1 -0
- package/dist/blocks/analytics/meta.js +24 -0
- package/dist/blocks/analytics/meta.js.map +1 -0
- package/dist/blocks/billing/meta.js +25 -0
- package/dist/blocks/billing/meta.js.map +1 -0
- package/dist/blocks/command-center/meta.js +28 -0
- package/dist/blocks/command-center/meta.js.map +1 -0
- package/dist/blocks/crm/meta.js +32 -0
- package/dist/blocks/crm/meta.js.map +1 -0
- package/dist/blocks/onboarding/meta.js +23 -0
- package/dist/blocks/onboarding/meta.js.map +1 -0
- package/dist/components/accordion/accordion.js +1 -1
- package/dist/components/accordion/accordion.js.map +1 -1
- package/dist/components/activity-feed/activity-feed.js +1 -1
- package/dist/components/activity-feed/activity-feed.js.map +1 -1
- package/dist/components/ai-agent-plan/ai-agent-plan.js +1 -1
- package/dist/components/ai-agent-plan/ai-agent-plan.js.map +1 -1
- package/dist/components/ai-extraction-review/ai-extraction-review.js +1 -1
- package/dist/components/ai-extraction-review/ai-extraction-review.js.map +1 -1
- package/dist/components/ai-prompt-input/ai-prompt-input.js +1 -1
- package/dist/components/ai-prompt-input/ai-prompt-input.js.map +1 -1
- package/dist/components/ai-reasoning/ai-reasoning.js +1 -1
- package/dist/components/ai-reasoning/ai-reasoning.js.map +1 -1
- package/dist/components/ai-response/ai-response.js +2 -2
- package/dist/components/ai-response/ai-response.js.map +1 -1
- package/dist/components/ai-tool/ai-tool.js +2 -2
- package/dist/components/ai-tool/ai-tool.js.map +1 -1
- package/dist/components/alert/alert.d.ts +1 -1
- package/dist/components/avatar/avatar.d.ts +1 -1
- package/dist/components/badge/badge.d.ts +1 -1
- package/dist/components/breadcrumb/breadcrumb.d.ts +52 -0
- package/dist/components/breadcrumb/breadcrumb.d.ts.map +1 -0
- package/dist/components/breadcrumb/breadcrumb.js +141 -0
- package/dist/components/breadcrumb/breadcrumb.js.map +1 -0
- package/dist/components/breadcrumb/index.d.ts +2 -0
- package/dist/components/breadcrumb/index.js +2 -0
- package/dist/components/breadcrumb/meta.js +17 -0
- package/dist/components/breadcrumb/meta.js.map +1 -0
- package/dist/components/button/button.d.ts +1 -1
- package/dist/components/calendar/calendar.js +2 -2
- package/dist/components/calendar/calendar.js.map +1 -1
- package/dist/components/code-block/code-block.js +1 -1
- package/dist/components/code-block/code-block.js.map +1 -1
- package/dist/components/collapsible/collapsible.d.ts +30 -0
- package/dist/components/collapsible/collapsible.d.ts.map +1 -0
- package/dist/components/collapsible/collapsible.js +38 -0
- package/dist/components/collapsible/collapsible.js.map +1 -0
- package/dist/components/collapsible/index.d.ts +2 -0
- package/dist/components/collapsible/index.js +2 -0
- package/dist/components/collapsible/meta.js +17 -0
- package/dist/components/collapsible/meta.js.map +1 -0
- package/dist/components/combobox/combobox.js +2 -2
- package/dist/components/combobox/combobox.js.map +1 -1
- package/dist/components/command/command.js +1 -1
- package/dist/components/command/command.js.map +1 -1
- package/dist/components/data-table/data-table.d.ts.map +1 -1
- package/dist/components/data-table/data-table.js +4 -1
- package/dist/components/data-table/data-table.js.map +1 -1
- package/dist/components/dialog/dialog.js +2 -2
- package/dist/components/dialog/dialog.js.map +1 -1
- package/dist/components/diff-viewer/diff-viewer.js +4 -4
- package/dist/components/diff-viewer/diff-viewer.js.map +1 -1
- package/dist/components/direction/direction.d.ts +35 -0
- package/dist/components/direction/direction.d.ts.map +1 -0
- package/dist/components/direction/direction.js +14 -0
- package/dist/components/direction/direction.js.map +1 -0
- package/dist/components/direction/index.d.ts +2 -0
- package/dist/components/direction/index.js +2 -0
- package/dist/components/direction/meta.js +17 -0
- package/dist/components/direction/meta.js.map +1 -0
- package/dist/components/drawer/drawer.js +1 -1
- package/dist/components/drawer/drawer.js.map +1 -1
- package/dist/components/dropdown-menu/dropdown-menu.d.ts.map +1 -1
- package/dist/components/dropdown-menu/dropdown-menu.js +9 -8
- package/dist/components/dropdown-menu/dropdown-menu.js.map +1 -1
- package/dist/components/input/input.js +1 -1
- package/dist/components/input/input.js.map +1 -1
- package/dist/components/log-viewer/log-viewer.js +2 -2
- package/dist/components/log-viewer/log-viewer.js.map +1 -1
- package/dist/components/meter/meter.js +1 -1
- package/dist/components/meter/meter.js.map +1 -1
- package/dist/components/pagination/pagination.d.ts.map +1 -1
- package/dist/components/pagination/pagination.js +3 -2
- package/dist/components/pagination/pagination.js.map +1 -1
- package/dist/components/permission-matrix/permission-matrix.js +3 -3
- package/dist/components/permission-matrix/permission-matrix.js.map +1 -1
- package/dist/components/progress/progress.d.ts +1 -1
- package/dist/components/record-diff/record-diff.js +4 -4
- package/dist/components/record-diff/record-diff.js.map +1 -1
- package/dist/components/select/select.js +2 -2
- package/dist/components/select/select.js.map +1 -1
- package/dist/components/sheet/sheet.d.ts.map +1 -1
- package/dist/components/sheet/sheet.js +2 -2
- package/dist/components/sheet/sheet.js.map +1 -1
- package/dist/components/sidebar/index.d.ts +2 -0
- package/dist/components/sidebar/index.js +2 -0
- package/dist/components/sidebar/meta.js +17 -0
- package/dist/components/sidebar/meta.js.map +1 -0
- package/dist/components/sidebar/sidebar.d.ts +69 -0
- package/dist/components/sidebar/sidebar.d.ts.map +1 -0
- package/dist/components/sidebar/sidebar.js +256 -0
- package/dist/components/sidebar/sidebar.js.map +1 -0
- package/dist/components/spinner/spinner.d.ts +1 -1
- package/dist/components/table/table.js +2 -2
- package/dist/components/table/table.js.map +1 -1
- package/dist/components/tags-input/tags-input.js +1 -1
- package/dist/components/tags-input/tags-input.js.map +1 -1
- package/dist/components/textarea/index.d.ts +2 -0
- package/dist/components/textarea/index.js +2 -0
- package/dist/components/textarea/meta.js +17 -0
- package/dist/components/textarea/meta.js.map +1 -0
- package/dist/components/textarea/textarea.d.ts +43 -0
- package/dist/components/textarea/textarea.d.ts.map +1 -0
- package/dist/components/textarea/textarea.js +119 -0
- package/dist/components/textarea/textarea.js.map +1 -0
- package/dist/components/time-range-picker/time-range-picker.js +3 -3
- package/dist/components/time-range-picker/time-range-picker.js.map +1 -1
- package/dist/components/toast/toast.d.ts +1 -1
- package/dist/components/toast/toast.d.ts.map +1 -1
- package/dist/components/toast/toast.js +2 -2
- package/dist/components/toast/toast.js.map +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.js +7 -2
- package/dist/lib/styles.d.ts +16 -1
- package/dist/lib/styles.d.ts.map +1 -1
- package/dist/lib/styles.js +16 -1
- package/dist/lib/styles.js.map +1 -1
- package/dist/registry/blocks.d.ts.map +1 -1
- package/dist/registry/blocks.js +27 -9
- package/dist/registry/blocks.js.map +1 -1
- package/dist/registry/components.d.ts.map +1 -1
- package/dist/registry/components.js +60 -50
- package/dist/registry/components.js.map +1 -1
- package/dist/registry/schema.d.ts +8 -0
- package/dist/registry/schema.d.ts.map +1 -1
- package/dist/registry/schema.js.map +1 -1
- package/package.json +5 -4
- package/src/components/accordion/accordion.tsx +1 -1
- package/src/components/activity-feed/activity-feed.tsx +1 -1
- package/src/components/ai-agent-plan/ai-agent-plan.tsx +0 -0
- package/src/components/ai-extraction-review/ai-extraction-review.tsx +1 -1
- package/src/components/ai-prompt-input/ai-prompt-input.tsx +1 -1
- package/src/components/ai-reasoning/ai-reasoning.tsx +1 -1
- package/src/components/ai-response/ai-response.tsx +2 -2
- package/src/components/ai-tool/ai-tool.tsx +2 -2
- package/src/components/breadcrumb/breadcrumb.tsx +157 -0
- package/src/components/breadcrumb/index.ts +10 -0
- package/src/components/breadcrumb/meta.ts +19 -0
- package/src/components/calendar/calendar.tsx +2 -2
- package/src/components/code-block/code-block.tsx +1 -1
- package/src/components/collapsible/collapsible.tsx +63 -0
- package/src/components/collapsible/index.ts +6 -0
- package/src/components/collapsible/meta.ts +16 -0
- package/src/components/combobox/combobox.tsx +2 -2
- package/src/components/command/command.tsx +1 -1
- package/src/components/data-table/data-table.tsx +14 -3
- package/src/components/dialog/dialog.tsx +2 -2
- package/src/components/diff-viewer/diff-viewer.tsx +4 -4
- package/src/components/direction/direction.tsx +38 -0
- package/src/components/direction/index.ts +1 -0
- package/src/components/direction/meta.ts +19 -0
- package/src/components/drawer/drawer.tsx +1 -1
- package/src/components/dropdown-menu/dropdown-menu.tsx +14 -8
- package/src/components/input/input.tsx +1 -1
- package/src/components/log-viewer/log-viewer.tsx +2 -2
- package/src/components/meter/meter.tsx +1 -1
- package/src/components/pagination/pagination.tsx +13 -2
- package/src/components/permission-matrix/permission-matrix.tsx +4 -4
- package/src/components/record-diff/record-diff.tsx +4 -4
- package/src/components/select/select.tsx +2 -2
- package/src/components/sheet/sheet.tsx +8 -4
- package/src/components/sidebar/index.ts +19 -0
- package/src/components/sidebar/meta.ts +25 -0
- package/src/components/sidebar/sidebar.tsx +374 -0
- package/src/components/table/table.tsx +3 -3
- package/src/components/tags-input/tags-input.tsx +1 -1
- package/src/components/textarea/index.ts +1 -0
- package/src/components/textarea/meta.ts +21 -0
- package/src/components/textarea/textarea.tsx +210 -0
- package/src/components/time-range-picker/time-range-picker.tsx +3 -3
- package/src/components/toast/toast.tsx +14 -6
- package/src/index.ts +5 -0
- package/src/lib/styles.ts +16 -0
- package/src/registry/blocks.ts +18 -0
- package/src/registry/components.ts +10 -0
- package/src/registry/schema.ts +8 -0
- package/src/blocks/admin-users/admin-users.tsx +0 -251
- package/src/blocks/admin-users/index.ts +0 -1
- package/src/blocks/admin-users/meta.ts +0 -27
- package/src/blocks/ai-chat/ai-chat.tsx +0 -280
- package/src/blocks/ai-chat/index.ts +0 -7
- package/src/blocks/ai-chat/meta.ts +0 -32
- package/src/blocks/dashboard/dashboard.tsx +0 -192
- package/src/blocks/dashboard/index.ts +0 -6
- package/src/blocks/dashboard/meta.ts +0 -18
- package/src/blocks/forgot-password/forgot-password.tsx +0 -158
- package/src/blocks/forgot-password/index.ts +0 -1
- package/src/blocks/forgot-password/meta.ts +0 -18
- package/src/blocks/login/index.ts +0 -1
- package/src/blocks/login/login.tsx +0 -178
- package/src/blocks/login/meta.ts +0 -18
- package/src/blocks/pricing/index.ts +0 -1
- package/src/blocks/pricing/meta.ts +0 -18
- package/src/blocks/pricing/pricing.tsx +0 -176
- package/src/blocks/settings/index.ts +0 -6
- package/src/blocks/settings/meta.ts +0 -29
- package/src/blocks/settings/settings.tsx +0 -317
- package/src/blocks/signup/index.ts +0 -1
- package/src/blocks/signup/meta.ts +0 -18
- package/src/blocks/signup/signup.tsx +0 -240
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import type { ReactNode } from "react";
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
ActivityContent,
|
|
7
|
-
ActivityDescription,
|
|
8
|
-
ActivityFeed,
|
|
9
|
-
ActivityIndicator,
|
|
10
|
-
ActivityItem,
|
|
11
|
-
ActivityTime,
|
|
12
|
-
ActivityTitle,
|
|
13
|
-
} from "@/components/activity-feed";
|
|
14
|
-
import { Badge } from "@/components/badge";
|
|
15
|
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/card";
|
|
16
|
-
import { Skeleton } from "@/components/skeleton";
|
|
17
|
-
import { cn } from "@/lib/utils";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* An overview page: headline numbers and what happened recently.
|
|
21
|
-
*
|
|
22
|
-
* The trend on a stat is stated in words as well as by direction and colour —
|
|
23
|
-
* "up 12.4%" rather than a green triangle — because a triangle is unreadable to
|
|
24
|
-
* a screen reader and ambiguous to anyone who cannot tell the colours apart.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
export interface DashboardStat {
|
|
28
|
-
id: string;
|
|
29
|
-
label: string;
|
|
30
|
-
value: string;
|
|
31
|
-
/** Signed change, e.g. 12.4 or -3.1. Omit when there is nothing to compare to. */
|
|
32
|
-
change?: number;
|
|
33
|
-
/** What the change is measured against. */
|
|
34
|
-
comparison?: string;
|
|
35
|
-
/** Whether a rise is good. Churn going up is not. */
|
|
36
|
-
higherIsBetter?: boolean;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface DashboardEvent {
|
|
40
|
-
id: string;
|
|
41
|
-
title: string;
|
|
42
|
-
detail?: string;
|
|
43
|
-
/** Machine-readable timestamp. */
|
|
44
|
-
at: string;
|
|
45
|
-
/** Human label, e.g. "12 minutes ago". */
|
|
46
|
-
label: string;
|
|
47
|
-
tone?: "default" | "success" | "warning" | "destructive";
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface DashboardBlockProps {
|
|
51
|
-
title?: string;
|
|
52
|
-
description?: string;
|
|
53
|
-
stats: DashboardStat[];
|
|
54
|
-
events?: DashboardEvent[];
|
|
55
|
-
/** Renders placeholders instead of content. */
|
|
56
|
-
loading?: boolean;
|
|
57
|
-
/** Anything to put beside the heading — a date range picker, an action. */
|
|
58
|
-
actions?: ReactNode;
|
|
59
|
-
className?: string;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function formatChange(change: number): string {
|
|
63
|
-
const rounded = Math.abs(change).toFixed(1);
|
|
64
|
-
return `${change >= 0 ? "up" : "down"} ${rounded}%`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function DashboardBlock({
|
|
68
|
-
title = "Overview",
|
|
69
|
-
description = "How things are going.",
|
|
70
|
-
stats,
|
|
71
|
-
events = [],
|
|
72
|
-
loading = false,
|
|
73
|
-
actions,
|
|
74
|
-
className,
|
|
75
|
-
}: DashboardBlockProps) {
|
|
76
|
-
return (
|
|
77
|
-
<div className={cn("flex flex-col gap-6", className)}>
|
|
78
|
-
<div className="flex flex-wrap items-end justify-between gap-3">
|
|
79
|
-
<div>
|
|
80
|
-
<h1 className="text-xl font-semibold tracking-tight">{title}</h1>
|
|
81
|
-
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
|
|
82
|
-
</div>
|
|
83
|
-
{actions}
|
|
84
|
-
</div>
|
|
85
|
-
|
|
86
|
-
<section aria-label="Key metrics" className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
87
|
-
{stats.map((stat) => {
|
|
88
|
-
const good =
|
|
89
|
-
stat.change === undefined
|
|
90
|
-
? undefined
|
|
91
|
-
: (stat.higherIsBetter ?? true)
|
|
92
|
-
? stat.change >= 0
|
|
93
|
-
: stat.change < 0;
|
|
94
|
-
|
|
95
|
-
return (
|
|
96
|
-
<Card key={stat.id}>
|
|
97
|
-
<CardHeader className="pb-2">
|
|
98
|
-
<CardDescription>{stat.label}</CardDescription>
|
|
99
|
-
{/* Not a heading: a number is data, and making it one both
|
|
100
|
-
breaks the heading order and litters the outline. */}
|
|
101
|
-
<p className="text-2xl leading-tight font-semibold tracking-tight tabular-nums">
|
|
102
|
-
{loading ? <Skeleton className="h-7 w-24" /> : stat.value}
|
|
103
|
-
</p>
|
|
104
|
-
</CardHeader>
|
|
105
|
-
<CardContent>
|
|
106
|
-
{loading ? (
|
|
107
|
-
<Skeleton className="h-4 w-32" />
|
|
108
|
-
) : stat.change === undefined ? null : (
|
|
109
|
-
<p className={cn("text-xs", good ? "text-success" : "text-destructive")}>
|
|
110
|
-
{/* In words: direction and magnitude, not a coloured arrow. */}
|
|
111
|
-
{formatChange(stat.change)}
|
|
112
|
-
{stat.comparison ? (
|
|
113
|
-
<span className="text-muted-foreground"> {stat.comparison}</span>
|
|
114
|
-
) : null}
|
|
115
|
-
</p>
|
|
116
|
-
)}
|
|
117
|
-
</CardContent>
|
|
118
|
-
</Card>
|
|
119
|
-
);
|
|
120
|
-
})}
|
|
121
|
-
</section>
|
|
122
|
-
|
|
123
|
-
{events.length > 0 || loading ? (
|
|
124
|
-
<Card>
|
|
125
|
-
<CardHeader>
|
|
126
|
-
<CardTitle asChild>
|
|
127
|
-
<h2>Recent activity</h2>
|
|
128
|
-
</CardTitle>
|
|
129
|
-
<CardDescription>The last few things that happened.</CardDescription>
|
|
130
|
-
</CardHeader>
|
|
131
|
-
<CardContent>
|
|
132
|
-
{loading ? (
|
|
133
|
-
<div className="flex flex-col gap-4">
|
|
134
|
-
{[0, 1, 2].map((index) => (
|
|
135
|
-
<div key={index} className="flex gap-3">
|
|
136
|
-
<Skeleton className="size-6 rounded-full" />
|
|
137
|
-
<div className="flex-1 space-y-2">
|
|
138
|
-
<Skeleton className="h-4 w-48" />
|
|
139
|
-
<Skeleton className="h-3 w-24" />
|
|
140
|
-
</div>
|
|
141
|
-
</div>
|
|
142
|
-
))}
|
|
143
|
-
</div>
|
|
144
|
-
) : (
|
|
145
|
-
<ActivityFeed>
|
|
146
|
-
{events.map((event, index) => (
|
|
147
|
-
<ActivityItem key={event.id} last={index === events.length - 1}>
|
|
148
|
-
<ActivityIndicator
|
|
149
|
-
className={cn(
|
|
150
|
-
event.tone === "success" && "border-success/40 text-success",
|
|
151
|
-
event.tone === "warning" && "border-warning/40 text-warning",
|
|
152
|
-
event.tone === "destructive" &&
|
|
153
|
-
"border-destructive/40 text-destructive",
|
|
154
|
-
)}
|
|
155
|
-
/>
|
|
156
|
-
<ActivityContent>
|
|
157
|
-
<ActivityTitle className="flex flex-wrap items-center gap-2">
|
|
158
|
-
{event.title}
|
|
159
|
-
{event.tone && event.tone !== "default" ? (
|
|
160
|
-
<Badge
|
|
161
|
-
size="sm"
|
|
162
|
-
variant={
|
|
163
|
-
event.tone === "success"
|
|
164
|
-
? "success"
|
|
165
|
-
: event.tone === "warning"
|
|
166
|
-
? "warning"
|
|
167
|
-
: "destructive"
|
|
168
|
-
}
|
|
169
|
-
>
|
|
170
|
-
{event.tone === "success"
|
|
171
|
-
? "Succeeded"
|
|
172
|
-
: event.tone === "warning"
|
|
173
|
-
? "Warning"
|
|
174
|
-
: "Failed"}
|
|
175
|
-
</Badge>
|
|
176
|
-
) : null}
|
|
177
|
-
</ActivityTitle>
|
|
178
|
-
{event.detail ? (
|
|
179
|
-
<ActivityDescription>{event.detail}</ActivityDescription>
|
|
180
|
-
) : null}
|
|
181
|
-
<ActivityTime dateTime={event.at}>{event.label}</ActivityTime>
|
|
182
|
-
</ActivityContent>
|
|
183
|
-
</ActivityItem>
|
|
184
|
-
))}
|
|
185
|
-
</ActivityFeed>
|
|
186
|
-
)}
|
|
187
|
-
</CardContent>
|
|
188
|
-
</Card>
|
|
189
|
-
) : null}
|
|
190
|
-
</div>
|
|
191
|
-
);
|
|
192
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { defineMeta } from "@/registry/schema";
|
|
2
|
-
|
|
3
|
-
export const meta = defineMeta({
|
|
4
|
-
name: "dashboard",
|
|
5
|
-
kind: "block",
|
|
6
|
-
title: "Dashboard",
|
|
7
|
-
description: "An overview page with headline metrics and a recent activity feed.",
|
|
8
|
-
category: "data",
|
|
9
|
-
status: "stable",
|
|
10
|
-
dependencies: [],
|
|
11
|
-
registryDependencies: ["activity-feed", "badge", "card", "skeleton"],
|
|
12
|
-
files: ["dashboard.tsx"],
|
|
13
|
-
a11y:
|
|
14
|
-
'Each metric states its change in words — "up 12.4%" — rather than by a coloured arrow, ' +
|
|
15
|
-
"which is unreadable to a screen reader and ambiguous to anyone who cannot distinguish the " +
|
|
16
|
-
"colours. Whether a rise is good is configurable, since churn going up is not. The metrics " +
|
|
17
|
-
"sit in a named region so they can be jumped to.",
|
|
18
|
-
});
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState, type FormEvent } from "react";
|
|
4
|
-
|
|
5
|
-
import { Button } from "@/components/button";
|
|
6
|
-
import {
|
|
7
|
-
Card,
|
|
8
|
-
CardContent,
|
|
9
|
-
CardDescription,
|
|
10
|
-
CardFooter,
|
|
11
|
-
CardHeader,
|
|
12
|
-
CardTitle,
|
|
13
|
-
} from "@/components/card";
|
|
14
|
-
import {
|
|
15
|
-
EmptyState,
|
|
16
|
-
EmptyStateDescription,
|
|
17
|
-
EmptyStateIcon,
|
|
18
|
-
EmptyStateTitle,
|
|
19
|
-
} from "@/components/empty-state";
|
|
20
|
-
import { Form, FormControl, FormField, FormLabel, FormMessage } from "@/components/form";
|
|
21
|
-
import { Input } from "@/components/input";
|
|
22
|
-
import { cn } from "@/lib/utils";
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* A password reset request.
|
|
26
|
-
*
|
|
27
|
-
* The confirmation deliberately does not say whether the address was found.
|
|
28
|
-
* Telling an anonymous visitor which emails have accounts turns this form into
|
|
29
|
-
* an account enumeration oracle, and the same neutral message covers both cases
|
|
30
|
-
* without being a lie.
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
export interface ForgotPasswordBlockProps {
|
|
34
|
-
onSubmit?: (values: { email: string }) => void;
|
|
35
|
-
/** Switches to the confirmation view. */
|
|
36
|
-
sent?: boolean;
|
|
37
|
-
error?: string;
|
|
38
|
-
pending?: boolean;
|
|
39
|
-
loginHref?: string;
|
|
40
|
-
className?: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function ForgotPasswordBlock({
|
|
44
|
-
onSubmit,
|
|
45
|
-
sent = false,
|
|
46
|
-
error,
|
|
47
|
-
pending = false,
|
|
48
|
-
loginHref = "/login",
|
|
49
|
-
className,
|
|
50
|
-
}: ForgotPasswordBlockProps) {
|
|
51
|
-
const [email, setEmail] = useState("");
|
|
52
|
-
const [touched, setTouched] = useState(false);
|
|
53
|
-
|
|
54
|
-
const emailError =
|
|
55
|
-
touched && !email.includes("@") ? "Enter a valid email address." : undefined;
|
|
56
|
-
|
|
57
|
-
function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
|
58
|
-
event.preventDefault();
|
|
59
|
-
setTouched(true);
|
|
60
|
-
if (!email.includes("@")) return;
|
|
61
|
-
onSubmit?.({ email });
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (sent) {
|
|
65
|
-
return (
|
|
66
|
-
<Card className={cn("w-full max-w-sm", className)}>
|
|
67
|
-
<CardContent className="pt-6">
|
|
68
|
-
{/* Announced, because it replaces the form the user just submitted. */}
|
|
69
|
-
<EmptyState role="status" aria-live="polite">
|
|
70
|
-
<EmptyStateIcon>
|
|
71
|
-
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
72
|
-
<rect
|
|
73
|
-
x="3"
|
|
74
|
-
y="5"
|
|
75
|
-
width="18"
|
|
76
|
-
height="14"
|
|
77
|
-
rx="2"
|
|
78
|
-
stroke="currentColor"
|
|
79
|
-
strokeWidth="2"
|
|
80
|
-
/>
|
|
81
|
-
<path
|
|
82
|
-
d="m3 7 9 6 9-6"
|
|
83
|
-
stroke="currentColor"
|
|
84
|
-
strokeWidth="2"
|
|
85
|
-
strokeLinecap="round"
|
|
86
|
-
/>
|
|
87
|
-
</svg>
|
|
88
|
-
</EmptyStateIcon>
|
|
89
|
-
<EmptyStateTitle>Check your inbox</EmptyStateTitle>
|
|
90
|
-
<EmptyStateDescription>
|
|
91
|
-
If an account exists for {email}, a reset link is on its way. It expires in an
|
|
92
|
-
hour.
|
|
93
|
-
</EmptyStateDescription>
|
|
94
|
-
</EmptyState>
|
|
95
|
-
</CardContent>
|
|
96
|
-
<CardFooter className="justify-center">
|
|
97
|
-
<a
|
|
98
|
-
href={loginHref}
|
|
99
|
-
className="text-sm font-medium text-primary underline underline-offset-4"
|
|
100
|
-
>
|
|
101
|
-
Back to sign in
|
|
102
|
-
</a>
|
|
103
|
-
</CardFooter>
|
|
104
|
-
</Card>
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return (
|
|
109
|
-
<Card className={cn("w-full max-w-sm", className)}>
|
|
110
|
-
<CardHeader>
|
|
111
|
-
<CardTitle>Reset your password</CardTitle>
|
|
112
|
-
<CardDescription>
|
|
113
|
-
Enter the address you signed up with and we will send you a link.
|
|
114
|
-
</CardDescription>
|
|
115
|
-
</CardHeader>
|
|
116
|
-
|
|
117
|
-
<CardContent>
|
|
118
|
-
<Form onSubmit={handleSubmit} className="gap-4">
|
|
119
|
-
{error ? (
|
|
120
|
-
<p role="alert" className="text-sm font-medium text-destructive">
|
|
121
|
-
{error}
|
|
122
|
-
</p>
|
|
123
|
-
) : null}
|
|
124
|
-
|
|
125
|
-
<FormField name="email" error={emailError}>
|
|
126
|
-
<FormLabel>Email</FormLabel>
|
|
127
|
-
<FormControl>
|
|
128
|
-
<Input
|
|
129
|
-
type="email"
|
|
130
|
-
autoComplete="email"
|
|
131
|
-
placeholder="you@example.com"
|
|
132
|
-
value={email}
|
|
133
|
-
disabled={pending}
|
|
134
|
-
onChange={(event) => {
|
|
135
|
-
setEmail(event.target.value);
|
|
136
|
-
}}
|
|
137
|
-
/>
|
|
138
|
-
</FormControl>
|
|
139
|
-
<FormMessage />
|
|
140
|
-
</FormField>
|
|
141
|
-
|
|
142
|
-
<Button type="submit" loading={pending} className="w-full">
|
|
143
|
-
Send reset link
|
|
144
|
-
</Button>
|
|
145
|
-
</Form>
|
|
146
|
-
</CardContent>
|
|
147
|
-
|
|
148
|
-
<CardFooter className="justify-center">
|
|
149
|
-
<a
|
|
150
|
-
href={loginHref}
|
|
151
|
-
className="text-sm text-muted-foreground underline underline-offset-4"
|
|
152
|
-
>
|
|
153
|
-
Back to sign in
|
|
154
|
-
</a>
|
|
155
|
-
</CardFooter>
|
|
156
|
-
</Card>
|
|
157
|
-
);
|
|
158
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { ForgotPasswordBlock, type ForgotPasswordBlockProps } from "./forgot-password";
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { defineMeta } from "@/registry/schema";
|
|
2
|
-
|
|
3
|
-
export const meta = defineMeta({
|
|
4
|
-
name: "forgot-password",
|
|
5
|
-
kind: "block",
|
|
6
|
-
title: "Forgot password",
|
|
7
|
-
description:
|
|
8
|
-
"A password reset request, with a confirmation that does not leak account existence.",
|
|
9
|
-
category: "form",
|
|
10
|
-
status: "stable",
|
|
11
|
-
dependencies: [],
|
|
12
|
-
registryDependencies: ["button", "card", "empty-state", "form", "input"],
|
|
13
|
-
files: ["forgot-password.tsx"],
|
|
14
|
-
a11y:
|
|
15
|
-
"The confirmation replaces the form, so it is a polite live region — otherwise a screen " +
|
|
16
|
-
"reader user submits and hears nothing. The wording is deliberately neutral about whether " +
|
|
17
|
-
"the address exists: confirming it would turn this form into an account enumeration oracle.",
|
|
18
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { LoginBlock, type LoginBlockProps } from "./login";
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState, type FormEvent } from "react";
|
|
4
|
-
|
|
5
|
-
import { Button } from "@/components/button";
|
|
6
|
-
import {
|
|
7
|
-
Card,
|
|
8
|
-
CardContent,
|
|
9
|
-
CardDescription,
|
|
10
|
-
CardFooter,
|
|
11
|
-
CardHeader,
|
|
12
|
-
CardTitle,
|
|
13
|
-
} from "@/components/card";
|
|
14
|
-
import { Checkbox } from "@/components/checkbox";
|
|
15
|
-
import { Form, FormControl, FormField, FormLabel, FormMessage } from "@/components/form";
|
|
16
|
-
import { Input } from "@/components/input";
|
|
17
|
-
import { Label } from "@/components/label";
|
|
18
|
-
import { Separator } from "@/components/separator";
|
|
19
|
-
import { cn } from "@/lib/utils";
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* A sign-in form.
|
|
23
|
-
*
|
|
24
|
-
* Wired for the parts that are easy to get wrong rather than left as markup:
|
|
25
|
-
* the email field is typed and autocompleted correctly, errors are announced,
|
|
26
|
-
* and the submit button reports its own busy state instead of leaving people
|
|
27
|
-
* wondering whether the click registered.
|
|
28
|
-
*
|
|
29
|
-
* Validation here is the minimum that makes the form usable offline. Real
|
|
30
|
-
* validation belongs to whatever the form is submitted to — pass `error` for a
|
|
31
|
-
* server response.
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
export interface LoginBlockProps {
|
|
35
|
-
/** Called with the submitted credentials. */
|
|
36
|
-
onSubmit?: (values: { email: string; password: string; remember: boolean }) => void;
|
|
37
|
-
/** A server-side error, shown above the fields. */
|
|
38
|
-
error?: string;
|
|
39
|
-
/** Disables the form and shows the submit button as busy. */
|
|
40
|
-
pending?: boolean;
|
|
41
|
-
/** Where "Forgot password?" goes. */
|
|
42
|
-
forgotHref?: string;
|
|
43
|
-
/** Where "Create an account" goes. */
|
|
44
|
-
signupHref?: string;
|
|
45
|
-
className?: string;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function LoginBlock({
|
|
49
|
-
onSubmit,
|
|
50
|
-
error,
|
|
51
|
-
pending = false,
|
|
52
|
-
forgotHref = "/forgot-password",
|
|
53
|
-
signupHref = "/signup",
|
|
54
|
-
className,
|
|
55
|
-
}: LoginBlockProps) {
|
|
56
|
-
const [email, setEmail] = useState("");
|
|
57
|
-
const [password, setPassword] = useState("");
|
|
58
|
-
const [remember, setRemember] = useState(false);
|
|
59
|
-
const [touched, setTouched] = useState(false);
|
|
60
|
-
|
|
61
|
-
const emailError =
|
|
62
|
-
touched && !email.includes("@") ? "Enter a valid email address." : undefined;
|
|
63
|
-
const passwordError = touched && password.length === 0 ? "Enter your password." : undefined;
|
|
64
|
-
|
|
65
|
-
function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
|
66
|
-
event.preventDefault();
|
|
67
|
-
setTouched(true);
|
|
68
|
-
if (!email.includes("@") || password.length === 0) return;
|
|
69
|
-
onSubmit?.({ email, password, remember });
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return (
|
|
73
|
-
<Card className={cn("w-full max-w-sm", className)}>
|
|
74
|
-
<CardHeader>
|
|
75
|
-
<CardTitle>Sign in</CardTitle>
|
|
76
|
-
<CardDescription>Welcome back. Enter your details to continue.</CardDescription>
|
|
77
|
-
</CardHeader>
|
|
78
|
-
|
|
79
|
-
<CardContent>
|
|
80
|
-
<Form onSubmit={handleSubmit} className="gap-4">
|
|
81
|
-
{/* A server error is assertive: it arrived after the user acted, and
|
|
82
|
-
it is the reason nothing happened. */}
|
|
83
|
-
{error ? (
|
|
84
|
-
<p role="alert" className="text-sm font-medium text-destructive">
|
|
85
|
-
{error}
|
|
86
|
-
</p>
|
|
87
|
-
) : null}
|
|
88
|
-
|
|
89
|
-
<FormField name="email" error={emailError}>
|
|
90
|
-
<FormLabel>Email</FormLabel>
|
|
91
|
-
<FormControl>
|
|
92
|
-
<Input
|
|
93
|
-
type="email"
|
|
94
|
-
autoComplete="email"
|
|
95
|
-
placeholder="you@example.com"
|
|
96
|
-
value={email}
|
|
97
|
-
disabled={pending}
|
|
98
|
-
onChange={(event) => {
|
|
99
|
-
setEmail(event.target.value);
|
|
100
|
-
}}
|
|
101
|
-
/>
|
|
102
|
-
</FormControl>
|
|
103
|
-
<FormMessage />
|
|
104
|
-
</FormField>
|
|
105
|
-
|
|
106
|
-
<FormField name="password" error={passwordError}>
|
|
107
|
-
<div className="flex items-center justify-between">
|
|
108
|
-
<FormLabel>Password</FormLabel>
|
|
109
|
-
<a
|
|
110
|
-
href={forgotHref}
|
|
111
|
-
className="text-xs font-medium text-muted-foreground underline underline-offset-4 hover:text-foreground"
|
|
112
|
-
>
|
|
113
|
-
Forgot password?
|
|
114
|
-
</a>
|
|
115
|
-
</div>
|
|
116
|
-
<FormControl>
|
|
117
|
-
<Input
|
|
118
|
-
type="password"
|
|
119
|
-
autoComplete="current-password"
|
|
120
|
-
value={password}
|
|
121
|
-
disabled={pending}
|
|
122
|
-
onChange={(event) => {
|
|
123
|
-
setPassword(event.target.value);
|
|
124
|
-
}}
|
|
125
|
-
/>
|
|
126
|
-
</FormControl>
|
|
127
|
-
<FormMessage />
|
|
128
|
-
</FormField>
|
|
129
|
-
|
|
130
|
-
<div className="flex items-center gap-2">
|
|
131
|
-
<Checkbox
|
|
132
|
-
id="remember-me"
|
|
133
|
-
checked={remember}
|
|
134
|
-
disabled={pending}
|
|
135
|
-
onCheckedChange={(checked) => {
|
|
136
|
-
setRemember(checked === true);
|
|
137
|
-
}}
|
|
138
|
-
/>
|
|
139
|
-
<Label htmlFor="remember-me" className="text-sm font-normal">
|
|
140
|
-
Keep me signed in
|
|
141
|
-
</Label>
|
|
142
|
-
</div>
|
|
143
|
-
|
|
144
|
-
<Button type="submit" loading={pending} className="w-full">
|
|
145
|
-
Sign in
|
|
146
|
-
</Button>
|
|
147
|
-
</Form>
|
|
148
|
-
|
|
149
|
-
<div className="my-5 flex items-center gap-3">
|
|
150
|
-
<Separator className="flex-1" />
|
|
151
|
-
<span className="text-2xs text-muted-foreground uppercase">or</span>
|
|
152
|
-
<Separator className="flex-1" />
|
|
153
|
-
</div>
|
|
154
|
-
|
|
155
|
-
<div className="grid gap-2">
|
|
156
|
-
<Button variant="outline" className="w-full" disabled={pending}>
|
|
157
|
-
Continue with GitHub
|
|
158
|
-
</Button>
|
|
159
|
-
<Button variant="outline" className="w-full" disabled={pending}>
|
|
160
|
-
Continue with Google
|
|
161
|
-
</Button>
|
|
162
|
-
</div>
|
|
163
|
-
</CardContent>
|
|
164
|
-
|
|
165
|
-
<CardFooter className="justify-center">
|
|
166
|
-
<p className="text-sm text-muted-foreground">
|
|
167
|
-
Don’t have an account?{" "}
|
|
168
|
-
<a
|
|
169
|
-
href={signupHref}
|
|
170
|
-
className="font-medium text-primary underline underline-offset-4"
|
|
171
|
-
>
|
|
172
|
-
Create one
|
|
173
|
-
</a>
|
|
174
|
-
</p>
|
|
175
|
-
</CardFooter>
|
|
176
|
-
</Card>
|
|
177
|
-
);
|
|
178
|
-
}
|
package/src/blocks/login/meta.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { defineMeta } from "@/registry/schema";
|
|
2
|
-
|
|
3
|
-
export const meta = defineMeta({
|
|
4
|
-
name: "login",
|
|
5
|
-
kind: "block",
|
|
6
|
-
title: "Login",
|
|
7
|
-
description: "A sign-in form with validation, social providers and a busy state.",
|
|
8
|
-
category: "form",
|
|
9
|
-
status: "stable",
|
|
10
|
-
dependencies: [],
|
|
11
|
-
registryDependencies: ["button", "card", "checkbox", "form", "input", "label", "separator"],
|
|
12
|
-
files: ["login.tsx"],
|
|
13
|
-
a11y:
|
|
14
|
-
"Email and password fields carry the right type and autocomplete, so password managers and " +
|
|
15
|
-
"mobile keyboards behave. Field errors are tied to their inputs through the Form wiring; a " +
|
|
16
|
-
"server error is an assertive alert, because it arrived after the user acted and explains why " +
|
|
17
|
-
"nothing happened. The submit button reports its own busy state rather than going quiet.",
|
|
18
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { PricingBlock, type PricingBlockProps, type PricingPlan } from "./pricing";
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { defineMeta } from "@/registry/schema";
|
|
2
|
-
|
|
3
|
-
export const meta = defineMeta({
|
|
4
|
-
name: "pricing",
|
|
5
|
-
kind: "block",
|
|
6
|
-
title: "Pricing",
|
|
7
|
-
description: "A pricing section with plan cards and a monthly/yearly toggle.",
|
|
8
|
-
category: "layout",
|
|
9
|
-
status: "stable",
|
|
10
|
-
dependencies: [],
|
|
11
|
-
registryDependencies: ["badge", "button", "card", "label", "switch"],
|
|
12
|
-
files: ["pricing.tsx"],
|
|
13
|
-
a11y:
|
|
14
|
-
"Switching the billing period rewrites every price on the page, so the change is announced " +
|
|
15
|
-
"politely rather than happening silently. Each plan's call to action names the plan — three " +
|
|
16
|
-
'buttons all reading "Choose" are indistinguishable out of context. Feature ticks are ' +
|
|
17
|
-
"decorative; the feature text carries the meaning.",
|
|
18
|
-
});
|