Skip to content

Commit 2c8241e

Browse files
committed
feat(shadcn): Add basic sign-in-auth-form example
1 parent 3ef88da commit 2c8241e

File tree

13 files changed

+1472
-29
lines changed

13 files changed

+1472
-29
lines changed

packages/shadcn/package.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,23 @@
1717
"@vitejs/plugin-react": "catalog:",
1818
"react": "catalog:",
1919
"react-dom": "catalog:",
20+
"shadcn": "2.9.3-canary.0",
2021
"tailwindcss": "catalog:",
2122
"tw-animate-css": "^1.4.0",
2223
"typescript": "catalog:",
2324
"vite": "catalog:"
2425
},
2526
"dependencies": {
27+
"@firebase-ui/react": "workspace:*",
28+
"@hookform/resolvers": "^5.2.2",
29+
"@radix-ui/react-label": "^2.1.7",
30+
"@radix-ui/react-separator": "^1.1.7",
31+
"@radix-ui/react-slot": "^1.2.3",
2632
"class-variance-authority": "^0.7.1",
2733
"clsx": "^2.1.1",
2834
"lucide-react": "^0.544.0",
29-
"tailwind-merge": "^3.3.1"
35+
"react-hook-form": "^7.64.0",
36+
"tailwind-merge": "^3.3.1",
37+
"zod": "catalog:"
3038
}
3139
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
3+
"name": "hello-world",
4+
"type": "registry:block",
5+
"title": "Hello World",
6+
"description": "A simple hello world component.",
7+
"files": [
8+
{
9+
"path": "src/registry/default/sign-in-auth-form.tsx",
10+
"dependencies": ["@firebase-ui/react"],
11+
"content": "export function Hello() {\n return <div>Hello</div>;\n}\n\nif (import.meta.env.DEV) {\n console.log(\"!!!\");\n}\n",
12+
"type": "registry:component"
13+
}
14+
]
15+
}

packages/shadcn/registry.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema/registry.json",
3+
"name": "acme",
4+
"homepage": "https://acme.com",
5+
"items": [
6+
{
7+
"name": "sign-in-auth-form",
8+
"type": "registry:block",
9+
"title": "Sign In Auth Form",
10+
"description": "A form allowing users to sign in with email and password.",
11+
"files": [
12+
{
13+
"path": "src/registry/default/sign-in-auth-form.tsx",
14+
"type": "registry:component"
15+
}
16+
]
17+
}
18+
]
19+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import * as React from "react"
2+
import { Slot } from "@radix-ui/react-slot"
3+
import { cva, type VariantProps } from "class-variance-authority"
4+
5+
import { cn } from "@/lib/utils"
6+
7+
const buttonVariants = cva(
8+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9+
{
10+
variants: {
11+
variant: {
12+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
13+
destructive:
14+
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
15+
outline:
16+
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
17+
secondary:
18+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
19+
ghost:
20+
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
21+
link: "text-primary underline-offset-4 hover:underline",
22+
},
23+
size: {
24+
default: "h-9 px-4 py-2 has-[>svg]:px-3",
25+
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
26+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
27+
icon: "size-9",
28+
"icon-sm": "size-8",
29+
"icon-lg": "size-10",
30+
},
31+
},
32+
defaultVariants: {
33+
variant: "default",
34+
size: "default",
35+
},
36+
}
37+
)
38+
39+
function Button({
40+
className,
41+
variant,
42+
size,
43+
asChild = false,
44+
...props
45+
}: React.ComponentProps<"button"> &
46+
VariantProps<typeof buttonVariants> & {
47+
asChild?: boolean
48+
}) {
49+
const Comp = asChild ? Slot : "button"
50+
51+
return (
52+
<Comp
53+
data-slot="button"
54+
className={cn(buttonVariants({ variant, size, className }))}
55+
{...props}
56+
/>
57+
)
58+
}
59+
60+
export { Button, buttonVariants }
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
import { useMemo } from "react"
2+
import { cva, type VariantProps } from "class-variance-authority"
3+
4+
import { cn } from "@/lib/utils"
5+
import { Label } from "@/components/ui/label"
6+
import { Separator } from "@/components/ui/separator"
7+
8+
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
9+
return (
10+
<fieldset
11+
data-slot="field-set"
12+
className={cn(
13+
"flex flex-col gap-6",
14+
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
15+
className
16+
)}
17+
{...props}
18+
/>
19+
)
20+
}
21+
22+
function FieldLegend({
23+
className,
24+
variant = "legend",
25+
...props
26+
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
27+
return (
28+
<legend
29+
data-slot="field-legend"
30+
data-variant={variant}
31+
className={cn(
32+
"mb-3 font-medium",
33+
"data-[variant=legend]:text-base",
34+
"data-[variant=label]:text-sm",
35+
className
36+
)}
37+
{...props}
38+
/>
39+
)
40+
}
41+
42+
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
43+
return (
44+
<div
45+
data-slot="field-group"
46+
className={cn(
47+
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
48+
className
49+
)}
50+
{...props}
51+
/>
52+
)
53+
}
54+
55+
const fieldVariants = cva(
56+
"group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
57+
{
58+
variants: {
59+
orientation: {
60+
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
61+
horizontal: [
62+
"flex-row items-center",
63+
"[&>[data-slot=field-label]]:flex-auto",
64+
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
65+
],
66+
responsive: [
67+
"flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto",
68+
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
69+
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
70+
],
71+
},
72+
},
73+
defaultVariants: {
74+
orientation: "vertical",
75+
},
76+
}
77+
)
78+
79+
function Field({
80+
className,
81+
orientation = "vertical",
82+
...props
83+
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
84+
return (
85+
<div
86+
role="group"
87+
data-slot="field"
88+
data-orientation={orientation}
89+
className={cn(fieldVariants({ orientation }), className)}
90+
{...props}
91+
/>
92+
)
93+
}
94+
95+
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
96+
return (
97+
<div
98+
data-slot="field-content"
99+
className={cn(
100+
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
101+
className
102+
)}
103+
{...props}
104+
/>
105+
)
106+
}
107+
108+
function FieldLabel({
109+
className,
110+
...props
111+
}: React.ComponentProps<typeof Label>) {
112+
return (
113+
<Label
114+
data-slot="field-label"
115+
className={cn(
116+
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
117+
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
118+
"has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
119+
className
120+
)}
121+
{...props}
122+
/>
123+
)
124+
}
125+
126+
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
127+
return (
128+
<div
129+
data-slot="field-label"
130+
className={cn(
131+
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
132+
className
133+
)}
134+
{...props}
135+
/>
136+
)
137+
}
138+
139+
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
140+
return (
141+
<p
142+
data-slot="field-description"
143+
className={cn(
144+
"text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
145+
"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
146+
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
147+
className
148+
)}
149+
{...props}
150+
/>
151+
)
152+
}
153+
154+
function FieldSeparator({
155+
children,
156+
className,
157+
...props
158+
}: React.ComponentProps<"div"> & {
159+
children?: React.ReactNode
160+
}) {
161+
return (
162+
<div
163+
data-slot="field-separator"
164+
data-content={!!children}
165+
className={cn(
166+
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
167+
className
168+
)}
169+
{...props}
170+
>
171+
<Separator className="absolute inset-0 top-1/2" />
172+
{children && (
173+
<span
174+
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
175+
data-slot="field-separator-content"
176+
>
177+
{children}
178+
</span>
179+
)}
180+
</div>
181+
)
182+
}
183+
184+
function FieldError({
185+
className,
186+
children,
187+
errors,
188+
...props
189+
}: React.ComponentProps<"div"> & {
190+
errors?: Array<{ message?: string } | undefined>
191+
}) {
192+
const content = useMemo(() => {
193+
if (children) {
194+
return children
195+
}
196+
197+
if (!errors) {
198+
return null
199+
}
200+
201+
if (errors?.length === 1 && errors[0]?.message) {
202+
return errors[0].message
203+
}
204+
205+
return (
206+
<ul className="ml-4 flex list-disc flex-col gap-1">
207+
{errors.map(
208+
(error, index) =>
209+
error?.message && <li key={index}>{error.message}</li>
210+
)}
211+
</ul>
212+
)
213+
}, [children, errors])
214+
215+
if (!content) {
216+
return null
217+
}
218+
219+
return (
220+
<div
221+
role="alert"
222+
data-slot="field-error"
223+
className={cn("text-destructive text-sm font-normal", className)}
224+
{...props}
225+
>
226+
{content}
227+
</div>
228+
)
229+
}
230+
231+
export {
232+
Field,
233+
FieldLabel,
234+
FieldDescription,
235+
FieldError,
236+
FieldGroup,
237+
FieldLegend,
238+
FieldSeparator,
239+
FieldSet,
240+
FieldContent,
241+
FieldTitle,
242+
}

0 commit comments

Comments
 (0)