harebear-ui 0.1.0 → 0.2.3
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/AGENTS.md +14 -11
- package/dist/components/ai/chat.js +4 -0
- package/dist/components/ai/code-block.d.ts +13 -7
- package/dist/components/ai/code-block.js +23 -13
- package/dist/components/auth/auth.d.ts +85 -0
- package/dist/components/auth/auth.js +325 -0
- package/dist/components/theme/theme-toggle.js +1 -0
- package/dist/components/ui/attachment.d.ts +24 -0
- package/dist/components/ui/attachment.js +100 -0
- package/dist/components/ui/bubble.d.ts +17 -0
- package/dist/components/ui/bubble.js +71 -0
- package/dist/components/ui/code-block.d.ts +26 -0
- package/dist/components/ui/code-block.js +123 -0
- package/dist/components/ui/combobox.d.ts +24 -0
- package/dist/components/ui/combobox.js +162 -0
- package/dist/components/ui/direction.d.ts +7 -0
- package/dist/components/ui/direction.js +12 -0
- package/dist/components/ui/field.js +1 -1
- package/dist/components/ui/input-group.js +1 -1
- package/dist/components/ui/marker.d.ts +11 -0
- package/dist/components/ui/marker.js +39 -0
- package/dist/components/ui/message-scroller.d.ts +10 -0
- package/dist/components/ui/message-scroller.js +59 -0
- package/dist/components/ui/message.d.ts +10 -0
- package/dist/components/ui/message.js +48 -0
- package/dist/components/ui/native-select.d.ts +8 -0
- package/dist/components/ui/native-select.js +37 -0
- package/dist/components/ui/questionnaire.d.ts +19 -0
- package/dist/components/ui/questionnaire.js +168 -0
- package/dist/components/ui/select.js +1 -1
- package/dist/components/ui/trace.d.ts +18 -0
- package/dist/components/ui/trace.js +74 -0
- package/dist/hooks/use-highlighted-code.d.ts +2 -0
- package/dist/hooks/use-highlighted-code.js +54 -0
- package/dist/index.d.ts +16 -2
- package/dist/index.js +16 -2
- package/dist/lib/highlighter.d.ts +9 -0
- package/dist/lib/highlighter.js +77 -0
- package/docs/COMPONENTS.md +115 -62
- package/llms.txt +6 -4
- package/package.json +8 -5
- package/skills/harebear-ui/SKILL.md +3 -2
package/AGENTS.md
CHANGED
|
@@ -6,14 +6,14 @@ Consumers get compiled ESM + types from `dist/` and a Tailwind stylesheet (`hare
|
|
|
6
6
|
|
|
7
7
|
## Commands
|
|
8
8
|
|
|
9
|
-
| Command | What it does
|
|
10
|
-
| ------------------------ |
|
|
11
|
-
| `pnpm dev` | Playground at http://localhost:5199 with the
|
|
12
|
-
| `pnpm check` | Everything CI runs: lint, typecheck, tests, build, publint, attw. Run before you say you're done.
|
|
13
|
-
| `pnpm test` | Vitest. `pnpm test -u` updates the export snapshot after an intended API change.
|
|
14
|
-
| `pnpm gen` | Regenerate `src/index.ts` from component export lists. Run after adding or renaming exports.
|
|
15
|
-
| `pnpm format` | Biome format + safe fixes.
|
|
16
|
-
| `pnpm shadcn add <name>` | Pull a new shadcn component into `src/components/ui/`.
|
|
9
|
+
| Command | What it does |
|
|
10
|
+
| ------------------------ | ------------------------------------------------------------------------------------------------- |
|
|
11
|
+
| `pnpm dev` | Playground at http://localhost:5199 with the reference screens. |
|
|
12
|
+
| `pnpm check` | Everything CI runs: lint, typecheck, tests, build, publint, attw. Run before you say you're done. |
|
|
13
|
+
| `pnpm test` | Vitest. `pnpm test -u` updates the export snapshot after an intended API change. |
|
|
14
|
+
| `pnpm gen` | Regenerate `src/index.ts` from component export lists. Run after adding or renaming exports. |
|
|
15
|
+
| `pnpm format` | Biome format + safe fixes. |
|
|
16
|
+
| `pnpm shadcn add <name>` | Pull a new shadcn component into `src/components/ui/`. |
|
|
17
17
|
|
|
18
18
|
## Layout
|
|
19
19
|
|
|
@@ -26,9 +26,10 @@ src/
|
|
|
26
26
|
components/theme/ ThemeProvider, ThemeToggle, ThemeScript.
|
|
27
27
|
components/admin/ Layout and dashboard composites (AppShell, StatCard, PageHeader...).
|
|
28
28
|
components/data/ DataTable, TrendChart, Sparkline, BarList, StatusBars.
|
|
29
|
-
components/ai/ Chat composites (ChatThread, ChatMessage, PromptInput, ModelPicker...).
|
|
29
|
+
components/ai/ Chat composites (ChatThread, ChatMessage, PromptInput, ModelPicker, CodeSnippet...).
|
|
30
|
+
components/auth/ AuthLayout, LoginForm, SignupForm (parameterized shadcn login/signup blocks).
|
|
30
31
|
hooks/, lib/utils.ts Shared hooks and cn().
|
|
31
|
-
playground/screens/ Reference screens that use only public exports
|
|
32
|
+
playground/screens/ Reference screens that use only public exports (#admin, #chat, #records, #home, #auth, #catalog). Visual spec and usage examples.
|
|
32
33
|
docs/COMPONENTS.md Consumer-facing catalog. Shipped. Test-enforced to mention every export.
|
|
33
34
|
llms.txt, skills/ Consumer-facing agent docs. Shipped.
|
|
34
35
|
bin/harebear-ui.mjs `npx harebear-ui init|docs` for consuming apps.
|
|
@@ -37,7 +38,7 @@ test/ Vitest: API snapshot, docs coverage, token integrity, be
|
|
|
37
38
|
|
|
38
39
|
## Adding a component
|
|
39
40
|
|
|
40
|
-
1. Pick the folder by purpose (`admin`, `data`, `ai`; `ui` only for shadcn primitives).
|
|
41
|
+
1. Pick the folder by purpose (`admin`, `data`, `ai`, `auth`; `ui` only for shadcn primitives).
|
|
41
42
|
2. Follow the existing files: `"use client"` first line, named function component, props interface exported as `<Name>Props`, `data-slot="<kebab-name>"` on the root, `className` merged last with `cn()`, rest props spread on the root.
|
|
42
43
|
3. Style only with tokens (`bg-card`, `text-muted-foreground`, `bg-primary-soft`, `shadow-card`, `rounded-xl`). No hex, no palette colors. Status takes a `tone` (`success | warning | danger | info | neutral`).
|
|
43
44
|
4. `pnpm gen`, then add the component to `docs/COMPONENTS.md` (the docs test fails otherwise) and to the index in `llms.txt`.
|
|
@@ -50,6 +51,8 @@ test/ Vitest: API snapshot, docs coverage, token integrity, be
|
|
|
50
51
|
- Keep upstream file structure so `pnpm shadcn add <name> -o` diffs stay readable. Change class strings, add variants; avoid restructuring.
|
|
51
52
|
- shadcn sometimes rewrites the utils import to `from "cn"`. It must be `from "@/lib/utils"`. Remove any `cn` dependency it adds. Same for `next-themes` (sonner uses our `useTheme`).
|
|
52
53
|
- New shadcn files need `"use client"` as the first line.
|
|
54
|
+
- Restyle new primitives to harebear's look: form controls are 40px `rounded-md bg-muted border-transparent` with `focus-visible:border-primary focus-visible:bg-card focus-visible:ring-4 focus-visible:ring-primary/15`; replace `border-input` / `dark:bg-input/30` patterns; selected states use `bg-primary-soft`.
|
|
55
|
+
- The package includes every `registry:ui` item. Check for gaps with `pnpm shadcn list @shadcn --json` and compare names against `src/components/ui/`.
|
|
53
56
|
|
|
54
57
|
## Gotchas
|
|
55
58
|
|
|
@@ -81,6 +81,7 @@ function MessageActions({ copyText, onRegenerate, onFeedback, feedback, classNam
|
|
|
81
81
|
...props,
|
|
82
82
|
children: [
|
|
83
83
|
copyText !== void 0 && /* @__PURE__ */ jsx(Button, {
|
|
84
|
+
type: "button",
|
|
84
85
|
variant: "ghost",
|
|
85
86
|
size: "icon-sm",
|
|
86
87
|
"aria-label": "Copy response",
|
|
@@ -88,6 +89,7 @@ function MessageActions({ copyText, onRegenerate, onFeedback, feedback, classNam
|
|
|
88
89
|
children: copied ? /* @__PURE__ */ jsx(CheckIcon, {}) : /* @__PURE__ */ jsx(CopyIcon, {})
|
|
89
90
|
}),
|
|
90
91
|
onRegenerate && /* @__PURE__ */ jsx(Button, {
|
|
92
|
+
type: "button",
|
|
91
93
|
variant: "ghost",
|
|
92
94
|
size: "icon-sm",
|
|
93
95
|
"aria-label": "Regenerate",
|
|
@@ -95,6 +97,7 @@ function MessageActions({ copyText, onRegenerate, onFeedback, feedback, classNam
|
|
|
95
97
|
children: /* @__PURE__ */ jsx(RefreshCwIcon, {})
|
|
96
98
|
}),
|
|
97
99
|
onFeedback && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Button, {
|
|
100
|
+
type: "button",
|
|
98
101
|
variant: "ghost",
|
|
99
102
|
size: "icon-sm",
|
|
100
103
|
"aria-label": "Good response",
|
|
@@ -103,6 +106,7 @@ function MessageActions({ copyText, onRegenerate, onFeedback, feedback, classNam
|
|
|
103
106
|
onClick: () => onFeedback("up"),
|
|
104
107
|
children: /* @__PURE__ */ jsx(ThumbsUpIcon, {})
|
|
105
108
|
}), /* @__PURE__ */ jsx(Button, {
|
|
109
|
+
type: "button",
|
|
106
110
|
variant: "ghost",
|
|
107
111
|
size: "icon-sm",
|
|
108
112
|
"aria-label": "Bad response",
|
|
@@ -1,15 +1,21 @@
|
|
|
1
|
+
import { CodeBlock } from "../ui/code-block.js";
|
|
1
2
|
import * as React from "react";
|
|
2
|
-
export interface
|
|
3
|
+
export interface CodeSnippetProps extends Omit<React.ComponentProps<typeof CodeBlock>, "children"> {
|
|
3
4
|
code: string;
|
|
4
|
-
/**
|
|
5
|
+
/** Language for the label and syntax highlighting, e.g. "bash", "tsx". Unknown languages render plain. */
|
|
5
6
|
language?: string;
|
|
6
|
-
/**
|
|
7
|
-
|
|
7
|
+
/** Turn syntax highlighting off. Default on. */
|
|
8
|
+
highlight?: boolean;
|
|
9
|
+
lineNumbers?: boolean;
|
|
8
10
|
/** Hide the copy button. */
|
|
9
11
|
hideCopy?: boolean;
|
|
10
12
|
}
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
+
/**
|
|
14
|
+
* One-prop code panel: language label, copy button, shiki highlighting (loaded
|
|
15
|
+
* lazily per language). Built on the CodeBlock primitives; use those directly
|
|
16
|
+
* for diffs, highlighted lines, or custom headers.
|
|
17
|
+
*/
|
|
18
|
+
declare function CodeSnippet({ code, language, highlight, lineNumbers, hideCopy, className, ...props }: CodeSnippetProps): React.JSX.Element;
|
|
13
19
|
/** Inline monospace span for commands and identifiers. */
|
|
14
20
|
declare function InlineCode({ className, ...props }: React.ComponentProps<"code">): React.JSX.Element;
|
|
15
21
|
/**
|
|
@@ -18,4 +24,4 @@ declare function InlineCode({ className, ...props }: React.ComponentProps<"code"
|
|
|
18
24
|
* with react-markdown or any HTML renderer.
|
|
19
25
|
*/
|
|
20
26
|
declare function Prose({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
21
|
-
export {
|
|
27
|
+
export { CodeSnippet, InlineCode, Prose };
|
|
@@ -1,32 +1,42 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Button } from "../ui/button.js";
|
|
3
|
+
import { CodeBlock, CodeBlockContent, CodeBlockHeader, CodeBlockLine, CodeBlockTitle } from "../ui/code-block.js";
|
|
3
4
|
import { useCopyToClipboard } from "../../hooks/use-copy-to-clipboard.js";
|
|
5
|
+
import { useHighlightedCode } from "../../hooks/use-highlighted-code.js";
|
|
4
6
|
import { cn } from "../../lib/utils.js";
|
|
5
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
8
|
import { CheckIcon, CopyIcon } from "lucide-react";
|
|
7
9
|
// src/components/ai/code-block.tsx
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
+
/**
|
|
11
|
+
* One-prop code panel: language label, copy button, shiki highlighting (loaded
|
|
12
|
+
* lazily per language). Built on the CodeBlock primitives; use those directly
|
|
13
|
+
* for diffs, highlighted lines, or custom headers.
|
|
14
|
+
*/
|
|
15
|
+
function CodeSnippet({ code, language, highlight = true, lineNumbers, hideCopy, className, ...props }) {
|
|
10
16
|
const { copied, copy } = useCopyToClipboard();
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
17
|
+
const lines = useHighlightedCode(code, highlight && language ? language : "text");
|
|
18
|
+
return /* @__PURE__ */ jsxs(CodeBlock, {
|
|
19
|
+
"data-slot": "code-snippet",
|
|
20
|
+
className,
|
|
14
21
|
...props,
|
|
15
|
-
children: [/* @__PURE__ */ jsxs(
|
|
16
|
-
className: "
|
|
17
|
-
children: [/* @__PURE__ */ jsx(
|
|
18
|
-
className: "font-mono text-xs
|
|
22
|
+
children: [(language || !hideCopy) && /* @__PURE__ */ jsxs(CodeBlockHeader, {
|
|
23
|
+
className: "min-h-10",
|
|
24
|
+
children: [/* @__PURE__ */ jsx(CodeBlockTitle, {
|
|
25
|
+
className: "font-mono text-xs text-muted-foreground",
|
|
19
26
|
children: language
|
|
20
27
|
}), !hideCopy && /* @__PURE__ */ jsxs(Button, {
|
|
28
|
+
type: "button",
|
|
21
29
|
variant: "ghost",
|
|
22
30
|
size: "sm",
|
|
31
|
+
className: "ms-auto",
|
|
23
32
|
onClick: () => copy(code),
|
|
24
33
|
"aria-label": "Copy code",
|
|
25
34
|
children: [copied ? /* @__PURE__ */ jsx(CheckIcon, {}) : /* @__PURE__ */ jsx(CopyIcon, {}), copied ? "Copied" : "Copy"]
|
|
26
35
|
})]
|
|
27
|
-
}), /* @__PURE__ */ jsx(
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
}), /* @__PURE__ */ jsx(CodeBlockContent, {
|
|
37
|
+
lineNumbers,
|
|
38
|
+
className: "pb-4.5",
|
|
39
|
+
children: lines.map((line, i) => /* @__PURE__ */ jsx(CodeBlockLine, { children: line }, i))
|
|
30
40
|
})]
|
|
31
41
|
});
|
|
32
42
|
}
|
|
@@ -50,4 +60,4 @@ function Prose({ className, ...props }) {
|
|
|
50
60
|
...props
|
|
51
61
|
});
|
|
52
62
|
}
|
|
53
|
-
export {
|
|
63
|
+
export { CodeSnippet, InlineCode, Prose };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface AuthLayoutProps {
|
|
3
|
+
/**
|
|
4
|
+
* - "centered": form in a card, centered on the page (shadcn login-01 / signup-01)
|
|
5
|
+
* - "muted": same on a muted page background (login-03 / signup-03)
|
|
6
|
+
* - "split": two columns, form left and a cover image right; no card (login-02 / signup-02)
|
|
7
|
+
* - "card-image": one wide card with the form and an image side by side (login-04 / signup-04)
|
|
8
|
+
*/
|
|
9
|
+
variant?: "centered" | "muted" | "split" | "card-image";
|
|
10
|
+
/** Logo/product name shown above the form (top-left in "split"). Usually <AppBrand />. */
|
|
11
|
+
brand?: React.ReactNode;
|
|
12
|
+
/** Cover image for "split" and "card-image". */
|
|
13
|
+
image?: {
|
|
14
|
+
src: string;
|
|
15
|
+
alt: string;
|
|
16
|
+
};
|
|
17
|
+
/** Small print under the form, e.g. terms links. */
|
|
18
|
+
footer?: React.ReactNode;
|
|
19
|
+
className?: string;
|
|
20
|
+
children: React.ReactNode;
|
|
21
|
+
}
|
|
22
|
+
/** Full-page wrapper for sign-in and sign-up screens. Put LoginForm or SignupForm inside. */
|
|
23
|
+
declare function AuthLayout({ variant, brand, image, footer, className, children }: AuthLayoutProps): React.JSX.Element;
|
|
24
|
+
export interface AuthProvider {
|
|
25
|
+
/** Passed to onProviderClick, e.g. "github". */
|
|
26
|
+
id: string;
|
|
27
|
+
/** Display name, e.g. "GitHub". */
|
|
28
|
+
label: string;
|
|
29
|
+
icon?: React.ReactNode;
|
|
30
|
+
}
|
|
31
|
+
/** GitHub mark, for AuthProvider icons. */
|
|
32
|
+
declare function GitHubIcon(props: React.ComponentProps<"svg">): React.JSX.Element;
|
|
33
|
+
declare function GoogleIcon(props: React.ComponentProps<"svg">): React.JSX.Element;
|
|
34
|
+
/** Apple mark, for AuthProvider icons. */
|
|
35
|
+
declare function AppleIcon(props: React.ComponentProps<"svg">): React.JSX.Element;
|
|
36
|
+
interface AuthFormBase extends Omit<React.ComponentProps<"form">, "onSubmit" | "title"> {
|
|
37
|
+
title?: React.ReactNode;
|
|
38
|
+
description?: React.ReactNode;
|
|
39
|
+
/** Social/SSO buttons shown under the main button. */
|
|
40
|
+
providers?: AuthProvider[];
|
|
41
|
+
onProviderClick?: (id: string) => void;
|
|
42
|
+
/** Shows a spinner on the submit button and disables the form. */
|
|
43
|
+
loading?: boolean;
|
|
44
|
+
/** Form-level error, e.g. "Invalid email or password". */
|
|
45
|
+
error?: React.ReactNode;
|
|
46
|
+
/** Put the provider buttons above the email form instead of below. */
|
|
47
|
+
providersFirst?: boolean;
|
|
48
|
+
}
|
|
49
|
+
export interface LoginValues {
|
|
50
|
+
email: string;
|
|
51
|
+
/** Absent in "email-only" mode. */
|
|
52
|
+
password?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface LoginFormProps extends AuthFormBase {
|
|
55
|
+
/** Called with the entered values. Omit to let the form submit natively (e.g. to an `action`). */
|
|
56
|
+
onSubmit?: (values: LoginValues) => void | Promise<void>;
|
|
57
|
+
/** "email-only" drops the password field for magic-link or OTP flows (shadcn login-05). */
|
|
58
|
+
mode?: "password" | "email-only";
|
|
59
|
+
forgotPasswordHref?: string;
|
|
60
|
+
signupHref?: string;
|
|
61
|
+
submitLabel?: React.ReactNode;
|
|
62
|
+
}
|
|
63
|
+
/** Email + password sign-in form with optional SSO buttons. Pair with AuthLayout. */
|
|
64
|
+
declare function LoginForm({ title, description, onSubmit, mode, providers, onProviderClick, providersFirst, forgotPasswordHref, signupHref, submitLabel, loading, error, className, ...props }: LoginFormProps): React.JSX.Element;
|
|
65
|
+
export interface SignupValues {
|
|
66
|
+
name?: string;
|
|
67
|
+
email: string;
|
|
68
|
+
password: string;
|
|
69
|
+
}
|
|
70
|
+
export interface SignupFormProps extends AuthFormBase {
|
|
71
|
+
onSubmit?: (values: SignupValues) => void | Promise<void>;
|
|
72
|
+
/** Ask for a full name. Default true. */
|
|
73
|
+
nameField?: boolean;
|
|
74
|
+
/** Ask for the password twice; mismatches are reported before onSubmit. Default true. */
|
|
75
|
+
confirmPassword?: boolean;
|
|
76
|
+
/** Hint under the password field. */
|
|
77
|
+
passwordHint?: React.ReactNode;
|
|
78
|
+
/** Minimum password length enforced by the browser. Default 8. */
|
|
79
|
+
minPasswordLength?: number;
|
|
80
|
+
loginHref?: string;
|
|
81
|
+
submitLabel?: React.ReactNode;
|
|
82
|
+
}
|
|
83
|
+
/** Account creation form: name, email, password (+ confirm), optional SSO. Pair with AuthLayout. */
|
|
84
|
+
declare function SignupForm({ title, description, onSubmit, nameField, confirmPassword, passwordHint, minPasswordLength, providers, onProviderClick, providersFirst, loginHref, submitLabel, loading, error, className, ...props }: SignupFormProps): React.JSX.Element;
|
|
85
|
+
export { AppleIcon, AuthLayout, GitHubIcon, GoogleIcon, LoginForm, SignupForm };
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Button } from "../ui/button.js";
|
|
3
|
+
import { Card, CardContent } from "../ui/card.js";
|
|
4
|
+
import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldSeparator } from "../ui/field.js";
|
|
5
|
+
import { Input } from "../ui/input.js";
|
|
6
|
+
import { Spinner } from "../ui/spinner.js";
|
|
7
|
+
import { cn } from "../../lib/utils.js";
|
|
8
|
+
import * as React from "react";
|
|
9
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
// src/components/auth/auth.tsx
|
|
11
|
+
/** Full-page wrapper for sign-in and sign-up screens. Put LoginForm or SignupForm inside. */
|
|
12
|
+
function AuthLayout({ variant = "centered", brand, image, footer, className, children }) {
|
|
13
|
+
const img = image && /* @__PURE__ */ jsx("img", {
|
|
14
|
+
src: image.src,
|
|
15
|
+
alt: image.alt,
|
|
16
|
+
className: "absolute inset-0 size-full object-cover dark:brightness-[0.6] dark:grayscale-[0.3]"
|
|
17
|
+
});
|
|
18
|
+
const small = footer && /* @__PURE__ */ jsx("div", {
|
|
19
|
+
className: "text-center text-xs text-balance text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 hover:[&_a]:text-foreground",
|
|
20
|
+
children: footer
|
|
21
|
+
});
|
|
22
|
+
if (variant === "split") return /* @__PURE__ */ jsxs("div", {
|
|
23
|
+
"data-slot": "auth-layout",
|
|
24
|
+
"data-variant": variant,
|
|
25
|
+
className: cn("grid min-h-svh lg:grid-cols-2", className),
|
|
26
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
27
|
+
className: "flex flex-col gap-4 p-6 md:p-10",
|
|
28
|
+
children: [brand && /* @__PURE__ */ jsx("div", {
|
|
29
|
+
className: "flex justify-center md:justify-start",
|
|
30
|
+
children: brand
|
|
31
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
32
|
+
className: "flex flex-1 items-center justify-center",
|
|
33
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
34
|
+
className: "flex w-full max-w-sm flex-col gap-6",
|
|
35
|
+
children: [children, small]
|
|
36
|
+
})
|
|
37
|
+
})]
|
|
38
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
39
|
+
className: "relative hidden bg-muted lg:block",
|
|
40
|
+
children: img
|
|
41
|
+
})]
|
|
42
|
+
});
|
|
43
|
+
const wide = variant === "card-image";
|
|
44
|
+
return /* @__PURE__ */ jsx("div", {
|
|
45
|
+
"data-slot": "auth-layout",
|
|
46
|
+
"data-variant": variant,
|
|
47
|
+
className: cn("flex min-h-svh flex-col items-center justify-center p-6 md:p-10", variant === "muted" && "bg-muted", className),
|
|
48
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
49
|
+
className: cn("flex w-full flex-col gap-6", wide ? "max-w-sm md:max-w-4xl" : "max-w-sm"),
|
|
50
|
+
children: [
|
|
51
|
+
brand && /* @__PURE__ */ jsx("div", {
|
|
52
|
+
className: "flex justify-center",
|
|
53
|
+
children: brand
|
|
54
|
+
}),
|
|
55
|
+
wide ? /* @__PURE__ */ jsx(Card, {
|
|
56
|
+
className: "overflow-hidden py-0",
|
|
57
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
58
|
+
className: "grid md:grid-cols-2",
|
|
59
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
60
|
+
className: "p-6 md:p-8",
|
|
61
|
+
children
|
|
62
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
63
|
+
className: "relative hidden min-h-80 bg-muted md:block",
|
|
64
|
+
children: img
|
|
65
|
+
})]
|
|
66
|
+
})
|
|
67
|
+
}) : /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(CardContent, { children }) }),
|
|
68
|
+
small
|
|
69
|
+
]
|
|
70
|
+
})
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function ProviderButtons({ providers, verb, onProviderClick, disabled }) {
|
|
74
|
+
const compact = providers.length >= 3 && providers.every((p) => p.icon);
|
|
75
|
+
return /* @__PURE__ */ jsx(Field, {
|
|
76
|
+
className: cn(compact && "grid gap-3"),
|
|
77
|
+
style: compact ? { gridTemplateColumns: `repeat(${providers.length}, minmax(0, 1fr))` } : void 0,
|
|
78
|
+
children: providers.map((p) => /* @__PURE__ */ jsxs(Button, {
|
|
79
|
+
type: "button",
|
|
80
|
+
variant: "outline",
|
|
81
|
+
disabled,
|
|
82
|
+
onClick: () => onProviderClick?.(p.id),
|
|
83
|
+
"aria-label": compact ? `${verb} with ${p.label}` : void 0,
|
|
84
|
+
children: [p.icon, !compact && `${verb} with ${p.label}`]
|
|
85
|
+
}, p.id))
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function BrandSvg({ d, ...props }) {
|
|
89
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
90
|
+
viewBox: "0 0 24 24",
|
|
91
|
+
"aria-hidden": "true",
|
|
92
|
+
...props,
|
|
93
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
94
|
+
d,
|
|
95
|
+
fill: "currentColor"
|
|
96
|
+
})
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/** GitHub mark, for AuthProvider icons. */
|
|
100
|
+
function GitHubIcon(props) {
|
|
101
|
+
return /* @__PURE__ */ jsx(BrandSvg, {
|
|
102
|
+
d: "M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12",
|
|
103
|
+
...props
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/** Google "G" mark, for AuthProvider icons. */
|
|
107
|
+
const GOOGLE_PATH = "M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z";
|
|
108
|
+
function GoogleIcon(props) {
|
|
109
|
+
return /* @__PURE__ */ jsx(BrandSvg, {
|
|
110
|
+
d: GOOGLE_PATH,
|
|
111
|
+
...props
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const APPLE_PATH = "M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701";
|
|
115
|
+
/** Apple mark, for AuthProvider icons. */
|
|
116
|
+
function AppleIcon(props) {
|
|
117
|
+
return /* @__PURE__ */ jsx(BrandSvg, {
|
|
118
|
+
d: APPLE_PATH,
|
|
119
|
+
...props
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function Header({ title, description }) {
|
|
123
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
124
|
+
className: "flex flex-col items-center gap-1.5 text-center",
|
|
125
|
+
children: [/* @__PURE__ */ jsx("h1", {
|
|
126
|
+
className: "text-2xl font-semibold tracking-[-0.02em]",
|
|
127
|
+
children: title
|
|
128
|
+
}), description && /* @__PURE__ */ jsx("p", {
|
|
129
|
+
className: "text-sm text-balance text-muted-foreground",
|
|
130
|
+
children: description
|
|
131
|
+
})]
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function readForm(form) {
|
|
135
|
+
return Object.fromEntries([...new FormData(form).entries()].map(([k, v]) => [k, typeof v === "string" ? v : ""]));
|
|
136
|
+
}
|
|
137
|
+
/** Email + password sign-in form with optional SSO buttons. Pair with AuthLayout. */
|
|
138
|
+
function LoginForm({ title = "Welcome back", description, onSubmit, mode = "password", providers = [], onProviderClick, providersFirst, forgotPasswordHref, signupHref, submitLabel, loading, error, className, ...props }) {
|
|
139
|
+
const id = React.useId();
|
|
140
|
+
const emailOnly = mode === "email-only";
|
|
141
|
+
const desc = description ?? (emailOnly ? "Enter your email and we'll send you a sign-in link" : "Sign in to your account");
|
|
142
|
+
const sso = providers.length > 0 && /* @__PURE__ */ jsx(ProviderButtons, {
|
|
143
|
+
providers,
|
|
144
|
+
verb: "Continue",
|
|
145
|
+
onProviderClick,
|
|
146
|
+
disabled: loading
|
|
147
|
+
});
|
|
148
|
+
return /* @__PURE__ */ jsx("form", {
|
|
149
|
+
"data-slot": "login-form",
|
|
150
|
+
className: cn("flex flex-col gap-6", className),
|
|
151
|
+
onSubmit: onSubmit ? (e) => {
|
|
152
|
+
e.preventDefault();
|
|
153
|
+
const v = readForm(e.currentTarget);
|
|
154
|
+
onSubmit(emailOnly ? { email: v.email ?? "" } : {
|
|
155
|
+
email: v.email ?? "",
|
|
156
|
+
password: v.password ?? ""
|
|
157
|
+
});
|
|
158
|
+
} : void 0,
|
|
159
|
+
"aria-busy": loading || void 0,
|
|
160
|
+
...props,
|
|
161
|
+
children: /* @__PURE__ */ jsxs(FieldGroup, { children: [
|
|
162
|
+
/* @__PURE__ */ jsx(Header, {
|
|
163
|
+
title,
|
|
164
|
+
description: desc
|
|
165
|
+
}),
|
|
166
|
+
providersFirst && sso,
|
|
167
|
+
providersFirst && sso && /* @__PURE__ */ jsx(FieldSeparator, { children: "Or continue with email" }),
|
|
168
|
+
/* @__PURE__ */ jsxs(Field, { children: [/* @__PURE__ */ jsx(FieldLabel, {
|
|
169
|
+
htmlFor: `${id}-email`,
|
|
170
|
+
children: "Email"
|
|
171
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
172
|
+
id: `${id}-email`,
|
|
173
|
+
name: "email",
|
|
174
|
+
type: "email",
|
|
175
|
+
autoComplete: "email",
|
|
176
|
+
placeholder: "you@example.com",
|
|
177
|
+
required: true,
|
|
178
|
+
disabled: loading
|
|
179
|
+
})] }),
|
|
180
|
+
!emailOnly && /* @__PURE__ */ jsxs(Field, { children: [/* @__PURE__ */ jsxs("div", {
|
|
181
|
+
className: "flex items-center",
|
|
182
|
+
children: [/* @__PURE__ */ jsx(FieldLabel, {
|
|
183
|
+
htmlFor: `${id}-password`,
|
|
184
|
+
children: "Password"
|
|
185
|
+
}), forgotPasswordHref && /* @__PURE__ */ jsx("a", {
|
|
186
|
+
href: forgotPasswordHref,
|
|
187
|
+
className: "ms-auto text-sm text-muted-foreground underline-offset-4 hover:text-foreground hover:underline",
|
|
188
|
+
children: "Forgot password?"
|
|
189
|
+
})]
|
|
190
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
191
|
+
id: `${id}-password`,
|
|
192
|
+
name: "password",
|
|
193
|
+
type: "password",
|
|
194
|
+
autoComplete: "current-password",
|
|
195
|
+
required: true,
|
|
196
|
+
disabled: loading
|
|
197
|
+
})] }),
|
|
198
|
+
error && /* @__PURE__ */ jsx(FieldError, { children: error }),
|
|
199
|
+
/* @__PURE__ */ jsx(Field, { children: /* @__PURE__ */ jsxs(Button, {
|
|
200
|
+
type: "submit",
|
|
201
|
+
disabled: loading,
|
|
202
|
+
children: [loading && /* @__PURE__ */ jsx(Spinner, {}), submitLabel ?? (emailOnly ? "Send sign-in link" : "Sign in")]
|
|
203
|
+
}) }),
|
|
204
|
+
!providersFirst && sso && /* @__PURE__ */ jsx(FieldSeparator, { children: "Or continue with" }),
|
|
205
|
+
!providersFirst && sso,
|
|
206
|
+
signupHref && /* @__PURE__ */ jsxs(FieldDescription, {
|
|
207
|
+
className: "text-center",
|
|
208
|
+
children: ["Don't have an account? ", /* @__PURE__ */ jsx("a", {
|
|
209
|
+
href: signupHref,
|
|
210
|
+
children: "Sign up"
|
|
211
|
+
})]
|
|
212
|
+
})
|
|
213
|
+
] })
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
/** Account creation form: name, email, password (+ confirm), optional SSO. Pair with AuthLayout. */
|
|
217
|
+
function SignupForm({ title = "Create your account", description = "Enter your details to get started", onSubmit, nameField = true, confirmPassword = true, passwordHint, minPasswordLength = 8, providers = [], onProviderClick, providersFirst, loginHref, submitLabel = "Create account", loading, error, className, ...props }) {
|
|
218
|
+
const id = React.useId();
|
|
219
|
+
const sso = providers.length > 0 && /* @__PURE__ */ jsx(ProviderButtons, {
|
|
220
|
+
providers,
|
|
221
|
+
verb: "Sign up",
|
|
222
|
+
onProviderClick,
|
|
223
|
+
disabled: loading
|
|
224
|
+
});
|
|
225
|
+
return /* @__PURE__ */ jsx("form", {
|
|
226
|
+
"data-slot": "signup-form",
|
|
227
|
+
className: cn("flex flex-col gap-6", className),
|
|
228
|
+
onSubmit: (e) => {
|
|
229
|
+
const form = e.currentTarget;
|
|
230
|
+
const v = readForm(form);
|
|
231
|
+
const confirm = form.elements.namedItem("confirm");
|
|
232
|
+
if (confirm) {
|
|
233
|
+
confirm.setCustomValidity(v.confirm === v.password ? "" : "Passwords do not match");
|
|
234
|
+
if (!confirm.reportValidity()) {
|
|
235
|
+
e.preventDefault();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (!onSubmit) return;
|
|
240
|
+
e.preventDefault();
|
|
241
|
+
onSubmit({
|
|
242
|
+
...nameField ? { name: v.name ?? "" } : {},
|
|
243
|
+
email: v.email ?? "",
|
|
244
|
+
password: v.password ?? ""
|
|
245
|
+
});
|
|
246
|
+
},
|
|
247
|
+
"aria-busy": loading || void 0,
|
|
248
|
+
...props,
|
|
249
|
+
children: /* @__PURE__ */ jsxs(FieldGroup, { children: [
|
|
250
|
+
/* @__PURE__ */ jsx(Header, {
|
|
251
|
+
title,
|
|
252
|
+
description
|
|
253
|
+
}),
|
|
254
|
+
providersFirst && sso,
|
|
255
|
+
providersFirst && sso && /* @__PURE__ */ jsx(FieldSeparator, { children: "Or sign up with email" }),
|
|
256
|
+
nameField && /* @__PURE__ */ jsxs(Field, { children: [/* @__PURE__ */ jsx(FieldLabel, {
|
|
257
|
+
htmlFor: `${id}-name`,
|
|
258
|
+
children: "Full name"
|
|
259
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
260
|
+
id: `${id}-name`,
|
|
261
|
+
name: "name",
|
|
262
|
+
autoComplete: "name",
|
|
263
|
+
required: true,
|
|
264
|
+
disabled: loading
|
|
265
|
+
})] }),
|
|
266
|
+
/* @__PURE__ */ jsxs(Field, { children: [/* @__PURE__ */ jsx(FieldLabel, {
|
|
267
|
+
htmlFor: `${id}-email`,
|
|
268
|
+
children: "Email"
|
|
269
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
270
|
+
id: `${id}-email`,
|
|
271
|
+
name: "email",
|
|
272
|
+
type: "email",
|
|
273
|
+
autoComplete: "email",
|
|
274
|
+
placeholder: "you@example.com",
|
|
275
|
+
required: true,
|
|
276
|
+
disabled: loading
|
|
277
|
+
})] }),
|
|
278
|
+
/* @__PURE__ */ jsxs(Field, { children: [
|
|
279
|
+
/* @__PURE__ */ jsx(FieldLabel, {
|
|
280
|
+
htmlFor: `${id}-password`,
|
|
281
|
+
children: "Password"
|
|
282
|
+
}),
|
|
283
|
+
/* @__PURE__ */ jsx(Input, {
|
|
284
|
+
id: `${id}-password`,
|
|
285
|
+
name: "password",
|
|
286
|
+
type: "password",
|
|
287
|
+
autoComplete: "new-password",
|
|
288
|
+
onInput: (e) => (e.currentTarget.form?.elements.namedItem("confirm"))?.setCustomValidity(""),
|
|
289
|
+
minLength: minPasswordLength,
|
|
290
|
+
required: true,
|
|
291
|
+
disabled: loading
|
|
292
|
+
}),
|
|
293
|
+
/* @__PURE__ */ jsx(FieldDescription, { children: passwordHint ?? `At least ${minPasswordLength} characters.` })
|
|
294
|
+
] }),
|
|
295
|
+
confirmPassword && /* @__PURE__ */ jsxs(Field, { children: [/* @__PURE__ */ jsx(FieldLabel, {
|
|
296
|
+
htmlFor: `${id}-confirm`,
|
|
297
|
+
children: "Confirm password"
|
|
298
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
299
|
+
id: `${id}-confirm`,
|
|
300
|
+
name: "confirm",
|
|
301
|
+
type: "password",
|
|
302
|
+
autoComplete: "new-password",
|
|
303
|
+
required: true,
|
|
304
|
+
disabled: loading,
|
|
305
|
+
onInput: (e) => e.currentTarget.setCustomValidity("")
|
|
306
|
+
})] }),
|
|
307
|
+
error && /* @__PURE__ */ jsx(FieldError, { children: error }),
|
|
308
|
+
/* @__PURE__ */ jsx(Field, { children: /* @__PURE__ */ jsxs(Button, {
|
|
309
|
+
type: "submit",
|
|
310
|
+
disabled: loading,
|
|
311
|
+
children: [loading && /* @__PURE__ */ jsx(Spinner, {}), submitLabel]
|
|
312
|
+
}) }),
|
|
313
|
+
!providersFirst && sso && /* @__PURE__ */ jsx(FieldSeparator, { children: "Or continue with" }),
|
|
314
|
+
!providersFirst && sso,
|
|
315
|
+
loginHref && /* @__PURE__ */ jsxs(FieldDescription, {
|
|
316
|
+
className: "text-center",
|
|
317
|
+
children: ["Already have an account? ", /* @__PURE__ */ jsx("a", {
|
|
318
|
+
href: loginHref,
|
|
319
|
+
children: "Sign in"
|
|
320
|
+
})]
|
|
321
|
+
})
|
|
322
|
+
] })
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
export { AppleIcon, AuthLayout, GitHubIcon, GoogleIcon, LoginForm, SignupForm };
|
|
@@ -9,6 +9,7 @@ function ThemeToggle(props) {
|
|
|
9
9
|
const { resolvedMode, toggleMode } = useTheme();
|
|
10
10
|
const dark = resolvedMode === "dark";
|
|
11
11
|
return /* @__PURE__ */ jsx(Button, {
|
|
12
|
+
type: "button",
|
|
12
13
|
variant: "ghost",
|
|
13
14
|
size: "icon",
|
|
14
15
|
"aria-label": dark ? "Switch to light mode" : "Switch to dark mode",
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Button } from "./button.js";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { VariantProps } from "class-variance-authority";
|
|
4
|
+
declare const attachmentVariants: (props?: ({
|
|
5
|
+
size?: "default" | "sm" | "xs" | null | undefined;
|
|
6
|
+
orientation?: "horizontal" | "vertical" | null | undefined;
|
|
7
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
8
|
+
declare function Attachment({ className, state, size, orientation, ...props }: React.ComponentProps<"div"> & VariantProps<typeof attachmentVariants> & {
|
|
9
|
+
state?: "idle" | "uploading" | "processing" | "error" | "done";
|
|
10
|
+
}): React.JSX.Element;
|
|
11
|
+
declare const attachmentMediaVariants: (props?: ({
|
|
12
|
+
variant?: "icon" | "image" | null | undefined;
|
|
13
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
14
|
+
declare function AttachmentMedia({ className, variant, ...props }: React.ComponentProps<"div"> & VariantProps<typeof attachmentMediaVariants>): React.JSX.Element;
|
|
15
|
+
declare function AttachmentContent({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
16
|
+
declare function AttachmentTitle({ className, ...props }: React.ComponentProps<"span">): React.JSX.Element;
|
|
17
|
+
declare function AttachmentDescription({ className, ...props }: React.ComponentProps<"span">): React.JSX.Element;
|
|
18
|
+
declare function AttachmentActions({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
19
|
+
declare function AttachmentAction({ className, variant, size, ...props }: React.ComponentProps<typeof Button>): React.JSX.Element;
|
|
20
|
+
declare function AttachmentTrigger({ className, asChild, type, ...props }: React.ComponentProps<"button"> & {
|
|
21
|
+
asChild?: boolean;
|
|
22
|
+
}): React.JSX.Element;
|
|
23
|
+
declare function AttachmentGroup({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
24
|
+
export { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentGroup, AttachmentMedia, AttachmentTitle, AttachmentTrigger };
|