harebear-ui 0.1.0 → 0.2.2

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.
Files changed (42) hide show
  1. package/AGENTS.md +14 -11
  2. package/dist/components/ai/chat.js +4 -0
  3. package/dist/components/ai/code-block.d.ts +13 -7
  4. package/dist/components/ai/code-block.js +23 -13
  5. package/dist/components/auth/auth.d.ts +85 -0
  6. package/dist/components/auth/auth.js +325 -0
  7. package/dist/components/theme/theme-toggle.js +1 -0
  8. package/dist/components/ui/attachment.d.ts +24 -0
  9. package/dist/components/ui/attachment.js +100 -0
  10. package/dist/components/ui/bubble.d.ts +17 -0
  11. package/dist/components/ui/bubble.js +71 -0
  12. package/dist/components/ui/code-block.d.ts +26 -0
  13. package/dist/components/ui/code-block.js +123 -0
  14. package/dist/components/ui/combobox.d.ts +24 -0
  15. package/dist/components/ui/combobox.js +162 -0
  16. package/dist/components/ui/direction.d.ts +7 -0
  17. package/dist/components/ui/direction.js +12 -0
  18. package/dist/components/ui/field.js +1 -1
  19. package/dist/components/ui/input-group.js +1 -1
  20. package/dist/components/ui/marker.d.ts +11 -0
  21. package/dist/components/ui/marker.js +39 -0
  22. package/dist/components/ui/message-scroller.d.ts +10 -0
  23. package/dist/components/ui/message-scroller.js +59 -0
  24. package/dist/components/ui/message.d.ts +10 -0
  25. package/dist/components/ui/message.js +48 -0
  26. package/dist/components/ui/native-select.d.ts +8 -0
  27. package/dist/components/ui/native-select.js +37 -0
  28. package/dist/components/ui/questionnaire.d.ts +19 -0
  29. package/dist/components/ui/questionnaire.js +168 -0
  30. package/dist/components/ui/select.js +1 -1
  31. package/dist/components/ui/trace.d.ts +18 -0
  32. package/dist/components/ui/trace.js +74 -0
  33. package/dist/hooks/use-highlighted-code.d.ts +2 -0
  34. package/dist/hooks/use-highlighted-code.js +54 -0
  35. package/dist/index.d.ts +16 -2
  36. package/dist/index.js +16 -2
  37. package/dist/lib/highlighter.d.ts +9 -0
  38. package/dist/lib/highlighter.js +77 -0
  39. package/docs/COMPONENTS.md +115 -62
  40. package/llms.txt +6 -4
  41. package/package.json +8 -5
  42. 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 four reference screens (`#admin`, `#chat`, `#records`, `#home`). |
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. They are the visual spec and usage examples.
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 CodeBlockProps extends Omit<React.ComponentProps<"div">, "children"> {
3
+ export interface CodeSnippetProps extends Omit<React.ComponentProps<typeof CodeBlock>, "children"> {
3
4
  code: string;
4
- /** Label shown top-left, e.g. "bash". */
5
+ /** Language for the label and syntax highlighting, e.g. "bash", "tsx". Unknown languages render plain. */
5
6
  language?: string;
6
- /** Pre-highlighted content (e.g. from shiki). Falls back to plain `code`. */
7
- children?: React.ReactNode;
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
- /** Code panel with language label and copy button. No highlighter bundled; pass highlighted children if you have one. */
12
- declare function CodeBlock({ code, language, children, hideCopy, className, ...props }: CodeBlockProps): React.JSX.Element;
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 { CodeBlock, InlineCode, Prose };
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
- /** Code panel with language label and copy button. No highlighter bundled; pass highlighted children if you have one. */
9
- function CodeBlock({ code, language, children, hideCopy, className, ...props }) {
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
- return /* @__PURE__ */ jsxs("div", {
12
- "data-slot": "code-block",
13
- className: cn("overflow-hidden rounded-lg bg-muted", className),
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("div", {
16
- className: "flex min-h-10 items-center justify-between pt-2 pr-2 pl-4.5",
17
- children: [/* @__PURE__ */ jsx("span", {
18
- className: "font-mono text-xs font-medium text-muted-foreground",
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("pre", {
28
- className: "m-0 overflow-x-auto px-4.5 pt-1 pb-4.5 font-mono text-[13px] leading-[1.7]",
29
- children: /* @__PURE__ */ jsx("code", { children: children ?? code })
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 { CodeBlock, InlineCode, Prose };
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 };