Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const config: any[] = [
"**/releases/**",
"packages/styles/dist.css",
"packages/angular/**",
"packages/shadcn/public",
]),
...tseslint.configs.recommended,
{
Expand All @@ -33,7 +34,7 @@ const config: any[] = [
},
{
// React package specific rules
files: ["packages/react/src/**/*.{ts,tsx}"],
files: ["packages/react/src/**/*.{ts,tsx}", "packages/shadcn/src/**/*.{ts,tsx}"],
plugins: { react: pluginReact, "react-hooks": pluginReactHooks },
languageOptions: {
parserOptions: {
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/components/policies.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export interface PolicyProps {
onNavigate?: (url: PolicyURL) => void;
}

const PolicyContext = createContext<PolicyProps | undefined>(undefined);
export const PolicyContext = createContext<PolicyProps | undefined>(undefined);

export function PolicyProvider({ children, policies }: { children: React.ReactNode; policies?: PolicyProps }) {
return <PolicyContext.Provider value={policies}>{children}</PolicyContext.Provider>;
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import { registerFramework } from "@firebase-ui/core";
import pkgJson from "../package.json";

export { PolicyContext } from "./components/policies";
export * from "./auth";
export * from "./hooks";
export * from "./components";
Expand Down
27 changes: 27 additions & 0 deletions packages/shadcn/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Shadcn registry is generated by the build script
registry.json

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
13 changes: 13 additions & 0 deletions packages/shadcn/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Firebase UI for Web - Shadcn

## Building the registry

To build the registry, run the `registry:build` script:

```
pnpm registry:build
```

Note, the script run (`build.ts`) expects a domain, which replaces the `{{ DOMAIN }}` field within the
`registy-spec.json`. This enables building the registry for different domains without updating the domain
in the actual `registry.json` file Shadcn expects.
28 changes: 28 additions & 0 deletions packages/shadcn/build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import fs from "fs";
import path from "path";
import { execSync } from "child_process";

// Get the domain from CLI args
const [, , domain] = process.argv;

if (!domain) {
console.error("Missing domain argument");
process.exit(1);
}

const registryPath = path.resolve("registry-spec.json");
const registryRaw = fs.readFileSync(registryPath, "utf8");

const replaced = registryRaw.replace(/{{\s*DOMAIN\s*}}/g, domain);
fs.writeFileSync("registry.json", replaced, "utf8");

const publicRDir = path.resolve("public", "r");
if (fs.existsSync(publicRDir)) {
execSync("rm -rf " + publicRDir, { stdio: "inherit" });
}

try {
execSync("shadcn build", { stdio: "inherit" });
} finally {
execSync("rm registry.json");
}
22 changes: 22 additions & 0 deletions packages/shadcn/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}
13 changes: 13 additions & 0 deletions packages/shadcn/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>shadcn</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
40 changes: 40 additions & 0 deletions packages/shadcn/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "@firebase-ui/shadcn",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"registry:build": "tsx build.ts https://ui.firebase.com",
"lint": "eslint .",
"preview": "vite preview"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.14",
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@vitejs/plugin-react": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"shadcn": "2.9.3-canary.0",
"tailwindcss": "catalog:",
"tw-animate-css": "^1.4.0",
"typescript": "catalog:",
"tsx": "^4.20.6",
"vite": "catalog:"
},
"dependencies": {
"@firebase-ui/react": "workspace:*",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.544.0",
"react-hook-form": "^7.64.0",
"tailwind-merge": "^3.3.1",
"zod": "catalog:"
}
}
17 changes: 17 additions & 0 deletions packages/shadcn/public/r/policies.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "policies",
"type": "registry:block",
"title": "Policies",
"description": "A component allowing users to navigate to the terms of service and privacy policy.",
"dependencies": [
"@firebase-ui/react"
],
"files": [
{
"path": "src/registry/policies.tsx",
"content": "import { cn } from \"@/lib/utils\";\nimport { getTranslation } from \"@firebase-ui/core\";\nimport { useUI, PolicyContext } from \"@firebase-ui/react\";\nimport { cloneElement, useContext } from \"react\";\n\nexport function Policies() {\n const ui = useUI();\n const policies = useContext(PolicyContext);\n\n if (!policies) {\n return null;\n }\n\n const { termsOfServiceUrl, privacyPolicyUrl, onNavigate } = policies;\n const termsAndPrivacyText = getTranslation(ui, \"messages\", \"termsAndPrivacy\");\n const parts = termsAndPrivacyText.split(/(\\{tos\\}|\\{privacy\\})/);\n\n const className = cn(\"hover:underline font-semibold\");\n const Handler = onNavigate ? (\n <button className={className} />\n ) : (\n <a target=\"_blank\" rel=\"noopener noreferrer\" className={className} />\n );\n\n return (\n <div className=\"text-text-muted text-center text-xs\">\n {parts.map((part: string, index: number) => {\n if (part === \"{tos}\") {\n return cloneElement(Handler, {\n key: index,\n onClick: onNavigate ? () => onNavigate(termsOfServiceUrl) : undefined,\n href: onNavigate ? undefined : termsOfServiceUrl,\n children: getTranslation(ui, \"labels\", \"termsOfService\"),\n });\n }\n\n if (part === \"{privacy}\") {\n return cloneElement(Handler, {\n key: index,\n onClick: onNavigate ? () => onNavigate(privacyPolicyUrl) : undefined,\n href: onNavigate ? undefined : privacyPolicyUrl,\n children: getTranslation(ui, \"labels\", \"privacyPolicy\"),\n });\n }\n\n return <span key={index}>{part}</span>;\n })}\n </div>\n );\n}\n",
"type": "registry:component"
}
]
}
23 changes: 23 additions & 0 deletions packages/shadcn/public/r/sign-in-auth-form.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "sign-in-auth-form",
"type": "registry:block",
"title": "Sign In Auth Form",
"description": "A form allowing users to sign in with email and password.",
"dependencies": [
"@firebase-ui/react"
],
"registryDependencies": [
"input",
"button",
"form",
"https://ui.firebase.com/r/policies"
],
"files": [
{
"path": "src/registry/sign-in-auth-form.tsx",
"content": "\"use client\";\n\nimport type { SignInAuthFormSchema } from \"@firebase-ui/core\";\nimport { useSignInAuthFormAction, useSignInAuthFormSchema, useUI, SignInAuthFormProps } from \"@firebase-ui/react\";\nimport { useForm } from \"react-hook-form\";\nimport { standardSchemaResolver } from \"@hookform/resolvers/standard-schema\";\nimport { FirebaseUIError, getTranslation } from \"@firebase-ui/core\";\n\nimport { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from \"@/components/ui/form\";\nimport { Input } from \"@/components/ui/input\";\nimport { Button } from \"@/components/ui/button\";\nimport { Policies } from \"./policies\";\n\nexport type { SignInAuthFormProps };\n\nexport function SignInAuthForm(props: SignInAuthFormProps) {\n const ui = useUI();\n const schema = useSignInAuthFormSchema();\n const action = useSignInAuthFormAction();\n\n const form = useForm<SignInAuthFormSchema>({\n resolver: standardSchemaResolver(schema),\n defaultValues: {\n email: \"\",\n password: \"\",\n },\n });\n\n async function onSubmit(values: SignInAuthFormSchema) {\n try {\n const credential = await action(values);\n props.onSignIn?.(credential);\n } catch (error) {\n const message = error instanceof FirebaseUIError ? error.message : String(error);\n form.setError(\"root\", { message });\n }\n }\n\n return (\n <Form {...form}>\n <form onSubmit={form.handleSubmit(onSubmit)} className=\"space-y-2\">\n <FormField\n control={form.control}\n name=\"email\"\n render={({ field }) => (\n <FormItem>\n <FormLabel>{getTranslation(ui, \"labels\", \"emailAddress\")}</FormLabel>\n <FormControl>\n <Input {...field} type=\"email\" />\n </FormControl>\n <FormMessage />\n </FormItem>\n )}\n />\n <FormField\n control={form.control}\n name=\"password\"\n render={({ field }) => (\n <FormItem>\n <FormLabel>{getTranslation(ui, \"labels\", \"password\")}</FormLabel>\n <FormControl>\n <div className=\"flex items-center gap-2\">\n <Input {...field} type=\"password\" className=\"flex-grow\" />\n {props.onForgotPasswordClick ? (\n <Button type=\"button\" variant=\"secondary\" onClick={props.onForgotPasswordClick}>\n {getTranslation(ui, \"labels\", \"forgotPassword\")}\n </Button>\n ) : null}\n </div>\n </FormControl>\n <FormMessage />\n </FormItem>\n )}\n />\n <Policies />\n <Button type=\"submit\" disabled={ui.state !== \"idle\"}>\n {getTranslation(ui, \"labels\", \"signIn\")}\n </Button>\n {form.formState.errors.root && <FormMessage>{form.formState.errors.root.message}</FormMessage>}\n {props.onRegisterClick ? (\n <>\n <Button type=\"button\" variant=\"secondary\" onClick={props.onRegisterClick}>\n {getTranslation(ui, \"prompts\", \"noAccount\")} {getTranslation(ui, \"labels\", \"register\")}\n </Button>\n </>\n ) : null}\n </form>\n </Form>\n );\n}\n",
"type": "registry:component"
}
]
}
34 changes: 34 additions & 0 deletions packages/shadcn/registry-spec.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"$schema": "https://ui.shadcn.com/schema/registry.json",
"name": "acme",
"homepage": "{{ DOMAIN }}",
"items": [
{
"name": "sign-in-auth-form",
"type": "registry:block",
"title": "Sign In Auth Form",
"description": "A form allowing users to sign in with email and password.",
"dependencies": ["@firebase-ui/react"],
"registryDependencies": ["input", "button", "form", "{{ DOMAIN }}/r/policies"],
"files": [
{
"path": "src/registry/sign-in-auth-form.tsx",
"type": "registry:component"
}
]
},
{
"name": "policies",
"type": "registry:block",
"title": "Policies",
"description": "A component allowing users to navigate to the terms of service and privacy policy.",
"dependencies": ["@firebase-ui/react"],
"files": [
{
"path": "src/registry/policies.tsx",
"type": "registry:component"
}
]
}
]
}
5 changes: 5 additions & 0 deletions packages/shadcn/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function App() {
return <></>;
}

export default App;
52 changes: 52 additions & 0 deletions packages/shadcn/src/components/ui/button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";

import { cn } from "@/lib/utils";

const buttonVariants = cva(
"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",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
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 rounded-md gap-1.5 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",
},
}
);

function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button";

return <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} />;
}

export { Button, buttonVariants };
Loading