@hex-core/components 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,157 @@
1
+ "use client";
2
+ import { cva } from 'class-variance-authority';
3
+ import { clsx } from 'clsx';
4
+ import { twMerge } from 'tailwind-merge';
5
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
6
+
7
+ function cn(...inputs) {
8
+ return twMerge(clsx(inputs));
9
+ }
10
+ var attachmentVariants = cva(
11
+ [
12
+ "group/attachment relative inline-flex items-center gap-[var(--gap-sm,0.5rem)] rounded-md border border-border bg-card",
13
+ "transition-all duration-[var(--duration-normal,200ms)] ease-out"
14
+ ].join(" "),
15
+ {
16
+ variants: {
17
+ variant: {
18
+ file: "px-[var(--space-3,0.75rem)] py-[var(--space-2,0.5rem)] max-w-xs",
19
+ image: "p-0 overflow-hidden"
20
+ }
21
+ },
22
+ defaultVariants: { variant: "file" }
23
+ }
24
+ );
25
+ function formatSize(bytes) {
26
+ if (bytes < 1024) return `${bytes} B`;
27
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
28
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
29
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
30
+ }
31
+ function detectVariant(file) {
32
+ const preview = "preview" in file ? file.preview : void 0;
33
+ const isImage = file.type.startsWith("image/");
34
+ return isImage && preview ? "image" : "file";
35
+ }
36
+ function resolvePreview(file) {
37
+ return "preview" in file ? file.preview : void 0;
38
+ }
39
+ function Attachment({
40
+ className,
41
+ variant,
42
+ file,
43
+ onRemove,
44
+ progress,
45
+ ref,
46
+ ...props
47
+ }) {
48
+ const preview = resolvePreview(file);
49
+ const detected = detectVariant(file);
50
+ const resolvedVariant = variant === "image" && !preview ? "file" : variant ?? detected;
51
+ const showProgress = typeof progress === "number" && progress >= 0 && progress < 1;
52
+ const progressPercent = showProgress ? Math.round(progress * 100) : 0;
53
+ return /* @__PURE__ */ jsxs(
54
+ "div",
55
+ {
56
+ ref,
57
+ className: cn(attachmentVariants({ variant: resolvedVariant }), className),
58
+ ...props,
59
+ children: [
60
+ resolvedVariant === "image" && preview ? (
61
+ // Intentional plain <img> (not next/image) — Attachment is
62
+ // framework-agnostic. Consumers can swap in next/image at the
63
+ // callsite when they want optimization.
64
+ /* @__PURE__ */ jsx(
65
+ "img",
66
+ {
67
+ src: preview,
68
+ alt: file.name,
69
+ className: "block h-20 w-20 object-cover"
70
+ }
71
+ )
72
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
73
+ /* @__PURE__ */ jsx(FileIcon, { className: "h-5 w-5 shrink-0 text-muted-foreground" }),
74
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col", children: [
75
+ /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-medium text-foreground", children: file.name }),
76
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: formatSize(file.size) })
77
+ ] })
78
+ ] }),
79
+ showProgress ? /* @__PURE__ */ jsx(
80
+ "div",
81
+ {
82
+ role: "progressbar",
83
+ "aria-valuemin": 0,
84
+ "aria-valuemax": 100,
85
+ "aria-valuenow": progressPercent,
86
+ "aria-label": `Uploading ${file.name}`,
87
+ className: "absolute inset-x-0 bottom-0 h-1 overflow-hidden rounded-b-md bg-muted",
88
+ children: /* @__PURE__ */ jsx(
89
+ "div",
90
+ {
91
+ className: "h-full bg-primary transition-[width] duration-[var(--duration-normal,200ms)] ease-out",
92
+ style: { width: `${progressPercent}%` }
93
+ }
94
+ )
95
+ }
96
+ ) : null,
97
+ onRemove ? /* @__PURE__ */ jsx(
98
+ "button",
99
+ {
100
+ type: "button",
101
+ onClick: onRemove,
102
+ "aria-label": `Remove ${file.name}`,
103
+ className: cn(
104
+ "absolute -right-2 -top-2 inline-flex h-5 w-5 items-center justify-center rounded-full",
105
+ "bg-card border border-border text-foreground shadow-sm",
106
+ "transition-all duration-[var(--duration-normal,200ms)] ease-out",
107
+ "hover:bg-accent hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
108
+ "active:scale-90"
109
+ ),
110
+ children: /* @__PURE__ */ jsxs(
111
+ "svg",
112
+ {
113
+ xmlns: "http://www.w3.org/2000/svg",
114
+ viewBox: "0 0 24 24",
115
+ fill: "none",
116
+ stroke: "currentColor",
117
+ strokeWidth: "2.5",
118
+ strokeLinecap: "round",
119
+ strokeLinejoin: "round",
120
+ className: "size-3",
121
+ "aria-hidden": "true",
122
+ children: [
123
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
124
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
125
+ ]
126
+ }
127
+ )
128
+ }
129
+ ) : null
130
+ ]
131
+ }
132
+ );
133
+ }
134
+ function FileIcon(props) {
135
+ return /* @__PURE__ */ jsxs(
136
+ "svg",
137
+ {
138
+ xmlns: "http://www.w3.org/2000/svg",
139
+ viewBox: "0 0 24 24",
140
+ fill: "none",
141
+ stroke: "currentColor",
142
+ strokeWidth: "2",
143
+ strokeLinecap: "round",
144
+ strokeLinejoin: "round",
145
+ "aria-hidden": "true",
146
+ ...props,
147
+ children: [
148
+ /* @__PURE__ */ jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
149
+ /* @__PURE__ */ jsx("polyline", { points: "14 2 14 8 20 8" })
150
+ ]
151
+ }
152
+ );
153
+ }
154
+
155
+ export { Attachment, attachmentVariants };
156
+ //# sourceMappingURL=attachment.js.map
157
+ //# sourceMappingURL=attachment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/utils.ts","../src/ai/attachment/attachment.tsx"],"names":[],"mappings":"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","file":"attachment.js","sourcesContent":["import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge class names with Tailwind CSS conflict resolution.\n * @param inputs - Class values (strings, arrays, objects) to merge\n * @returns A single merged class string with Tailwind conflicts resolved\n */\nexport function cn(...inputs: ClassValue[]) {\n\treturn twMerge(clsx(inputs));\n}\n","\"use client\";\n\nimport { type VariantProps, cva } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/utils.js\";\n\n/** A file-shaped attachment metadata object accepted by `<Attachment>`. */\nexport interface AttachmentFile {\n\t/** Display name. */\n\tname: string;\n\t/** Bytes. Used to render a human-readable size. */\n\tsize: number;\n\t/** MIME type. Drives the auto-detected `variant`. */\n\ttype: string;\n\t/**\n\t * Optional preview URL for images. When `type` starts with `image/`\n\t * AND `preview` is set, the attachment renders a thumbnail; otherwise\n\t * it falls back to a typed file icon + name + size.\n\t */\n\tpreview?: string;\n}\n\nconst attachmentVariants = cva(\n\t[\n\t\t\"group/attachment relative inline-flex items-center gap-[var(--gap-sm,0.5rem)] rounded-md border border-border bg-card\",\n\t\t\"transition-all duration-[var(--duration-normal,200ms)] ease-out\",\n\t].join(\" \"),\n\t{\n\t\tvariants: {\n\t\t\tvariant: {\n\t\t\t\tfile: \"px-[var(--space-3,0.75rem)] py-[var(--space-2,0.5rem)] max-w-xs\",\n\t\t\t\timage: \"p-0 overflow-hidden\",\n\t\t\t},\n\t\t},\n\t\tdefaultVariants: { variant: \"file\" },\n\t},\n);\n\nexport interface AttachmentProps\n\textends Omit<React.HTMLAttributes<HTMLDivElement>, \"onProgress\">,\n\t\tVariantProps<typeof attachmentVariants> {\n\t/** Forwarded ref onto the root element. */\n\tref?: React.Ref<HTMLDivElement>;\n\t/** Native `File` OR a metadata object. */\n\tfile: File | AttachmentFile;\n\t/** Click handler for the remove button. When provided, a × button overlays the attachment. */\n\tonRemove?: () => void;\n\t/**\n\t * Upload progress in `[0, 1)`. Values `>= 1` (or `undefined`) hide the\n\t * progressbar — pass `undefined` once the upload completes. Internally\n\t * scaled to `aria-valuenow` on a 0–100 progressbar so AT engines\n\t * announce \"42 percent\" rather than \"0.42 of 1.\"\n\t */\n\tprogress?: number;\n}\n\n/**\n * Format a byte count as a human-readable size (`\"24.3 KB\"`, `\"1.2 MB\"`).\n *\n * @param bytes - Raw byte count.\n * @returns A short human-readable string.\n */\nfunction formatSize(bytes: number): string {\n\tif (bytes < 1024) return `${bytes} B`;\n\tif (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n\tif (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n\treturn `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;\n}\n\n/**\n * Decide whether to render the image variant (thumbnail) vs the file\n * variant (icon + name + size). Images need both an image MIME type AND\n * a `preview` URL — without preview we can't actually show the image, so\n * we fall back to the file variant.\n *\n * @param file - The attachment input.\n * @returns `\"image\"` when both conditions hold, else `\"file\"`.\n */\nfunction detectVariant(file: File | AttachmentFile): \"image\" | \"file\" {\n\tconst preview = \"preview\" in file ? file.preview : undefined;\n\tconst isImage = file.type.startsWith(\"image/\");\n\treturn isImage && preview ? \"image\" : \"file\";\n}\n\n/**\n * Resolve the preview URL for an attachment input. Native `File`s don't\n * carry a preview; consumers create one via `URL.createObjectURL(file)`\n * before handing the file in.\n *\n * @param file - The attachment input.\n * @returns The preview URL, or undefined.\n */\nfunction resolvePreview(file: File | AttachmentFile): string | undefined {\n\treturn \"preview\" in file ? file.preview : undefined;\n}\n\n/**\n * A file / image attachment thumbnail with an optional remove affordance\n * and optional upload-progress overlay. Composes with the existing\n * `Composer` AI primitive (drop into Composer's children slot to render\n * as part of the message draft).\n *\n * Auto-detects the visual variant: image MIME + `preview` URL → image\n * thumbnail; everything else → typed file icon with name + size.\n *\n * Distinct from {@link Dropzone} (the upload-input affordance) and\n * static {@link Card} previews (no remove/progress UI).\n *\n * @example\n * ```tsx\n * <Attachment\n * file={{ name: \"screenshot.png\", size: 124000, type: \"image/png\", preview: objectUrl }}\n * onRemove={() => removeFromDraft(\"screenshot.png\")}\n * />\n * ```\n */\nfunction Attachment({\n\tclassName,\n\tvariant,\n\tfile,\n\tonRemove,\n\tprogress,\n\tref,\n\t...props\n}: AttachmentProps) {\n\tconst preview = resolvePreview(file);\n\t// M3: when caller forces variant=\"image\" but no preview is available,\n\t// fall back to \"file\" so the wrapper padding matches the rendered\n\t// content (file icon + name + size needs px/py; image uses zero-pad).\n\tconst detected = detectVariant(file);\n\tconst resolvedVariant: \"image\" | \"file\" =\n\t\tvariant === \"image\" && !preview ? \"file\" : (variant ?? detected);\n\tconst showProgress = typeof progress === \"number\" && progress >= 0 && progress < 1;\n\t// M5: scale to 0–100 so AT announces \"42 percent\" not \"0.42 of 1.\"\n\tconst progressPercent = showProgress ? Math.round(progress * 100) : 0;\n\n\treturn (\n\t\t<div\n\t\t\tref={ref}\n\t\t\tclassName={cn(attachmentVariants({ variant: resolvedVariant }), className)}\n\t\t\t{...props}\n\t\t>\n\t\t\t{resolvedVariant === \"image\" && preview ? (\n\t\t\t\t// Intentional plain <img> (not next/image) — Attachment is\n\t\t\t\t// framework-agnostic. Consumers can swap in next/image at the\n\t\t\t\t// callsite when they want optimization.\n\t\t\t\t<img\n\t\t\t\t\tsrc={preview}\n\t\t\t\t\talt={file.name}\n\t\t\t\t\tclassName=\"block h-20 w-20 object-cover\"\n\t\t\t\t/>\n\t\t\t) : (\n\t\t\t\t<>\n\t\t\t\t\t<FileIcon className=\"h-5 w-5 shrink-0 text-muted-foreground\" />\n\t\t\t\t\t<div className=\"flex min-w-0 flex-col\">\n\t\t\t\t\t\t<span className=\"truncate text-sm font-medium text-foreground\">{file.name}</span>\n\t\t\t\t\t\t<span className=\"text-xs text-muted-foreground\">{formatSize(file.size)}</span>\n\t\t\t\t\t</div>\n\t\t\t\t</>\n\t\t\t)}\n\n\t\t\t{showProgress ? (\n\t\t\t\t<div\n\t\t\t\t\trole=\"progressbar\"\n\t\t\t\t\taria-valuemin={0}\n\t\t\t\t\taria-valuemax={100}\n\t\t\t\t\taria-valuenow={progressPercent}\n\t\t\t\t\taria-label={`Uploading ${file.name}`}\n\t\t\t\t\tclassName=\"absolute inset-x-0 bottom-0 h-1 overflow-hidden rounded-b-md bg-muted\"\n\t\t\t\t>\n\t\t\t\t\t<div\n\t\t\t\t\t\tclassName=\"h-full bg-primary transition-[width] duration-[var(--duration-normal,200ms)] ease-out\"\n\t\t\t\t\t\tstyle={{ width: `${progressPercent}%` }}\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t) : null}\n\n\t\t\t{onRemove ? (\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tonClick={onRemove}\n\t\t\t\t\taria-label={`Remove ${file.name}`}\n\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\"absolute -right-2 -top-2 inline-flex h-5 w-5 items-center justify-center rounded-full\",\n\t\t\t\t\t\t\"bg-card border border-border text-foreground shadow-sm\",\n\t\t\t\t\t\t\"transition-all duration-[var(--duration-normal,200ms)] ease-out\",\n\t\t\t\t\t\t\"hover:bg-accent hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1\",\n\t\t\t\t\t\t\"active:scale-90\",\n\t\t\t\t\t)}\n\t\t\t\t>\n\t\t\t\t\t<svg\n\t\t\t\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\t\tfill=\"none\"\n\t\t\t\t\t\tstroke=\"currentColor\"\n\t\t\t\t\t\tstrokeWidth=\"2.5\"\n\t\t\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\t\t\tclassName=\"size-3\"\n\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n\t\t\t\t\t\t<line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n\t\t\t\t\t</svg>\n\t\t\t\t</button>\n\t\t\t) : null}\n\t\t</div>\n\t);\n}\n\n/** Generic file icon (page-with-fold). */\nfunction FileIcon(props: React.SVGAttributes<SVGElement>) {\n\treturn (\n\t\t<svg\n\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\tviewBox=\"0 0 24 24\"\n\t\t\tfill=\"none\"\n\t\t\tstroke=\"currentColor\"\n\t\t\tstrokeWidth=\"2\"\n\t\t\tstrokeLinecap=\"round\"\n\t\t\tstrokeLinejoin=\"round\"\n\t\t\taria-hidden=\"true\"\n\t\t\t{...props}\n\t\t>\n\t\t\t<path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\" />\n\t\t\t<polyline points=\"14 2 14 8 20 8\" />\n\t\t</svg>\n\t);\n}\n\nexport { Attachment, attachmentVariants };\n"]}
@@ -35,7 +35,7 @@ var LABEL_TO_LANG = {
35
35
  prompt: "text",
36
36
  plain: "text"
37
37
  };
38
- var DEFAULT_THEMES = { light: "github-light", dark: "github-dark" };
38
+ var DEFAULT_THEMES = { light: "github-light-high-contrast", dark: "github-dark" };
39
39
  var cachedCodeToHtml = cache(
40
40
  async (code, lang, themesKey, themes) => {
41
41
  return codeToHtml(code, { lang, themes, defaultColor: false });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/lib/utils.ts","../src/ai/code-block/code-block.tsx"],"names":[],"mappings":"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","file":"code-block.js","sourcesContent":["import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge class names with Tailwind CSS conflict resolution.\n * @param inputs - Class values (strings, arrays, objects) to merge\n * @returns A single merged class string with Tailwind conflicts resolved\n */\nexport function cn(...inputs: ClassValue[]) {\n\treturn twMerge(clsx(inputs));\n}\n","import * as React from \"react\";\nimport { cache } from \"react\";\nimport { codeToHtml } from \"shiki\";\nimport { cn } from \"../../lib/utils.js\";\nimport { CodeBlockCopy } from \"./code-block-copy.js\";\n\n/**\n * Languages we surface in the prop type. Plain literal union of real\n * Shiki grammar IDs — kept literal (not `Extract<BundledLanguage, …>`)\n * because Shiki's full bundled-language union is 600+ literals, and\n * deriving from it forced the rollup-dts pass past a 4GB heap. The\n * literals here are all standard Shiki grammar IDs; if Shiki removes\n * one upstream, `codeToHtml` will throw at runtime — acceptable trade.\n */\nexport type SupportedLang =\n\t| \"bash\"\n\t| \"ts\"\n\t| \"tsx\"\n\t| \"js\"\n\t| \"jsx\"\n\t| \"json\"\n\t| \"css\"\n\t| \"html\"\n\t| \"md\"\n\t| \"py\"\n\t| \"text\";\n\nconst LABEL_TO_LANG: Record<string, SupportedLang> = {\n\tpnpm: \"bash\",\n\tnpm: \"bash\",\n\tyarn: \"bash\",\n\tbun: \"bash\",\n\tbash: \"bash\",\n\tsh: \"bash\",\n\tshell: \"bash\",\n\tzsh: \"bash\",\n\tts: \"ts\",\n\ttypescript: \"ts\",\n\ttsx: \"tsx\",\n\tjs: \"js\",\n\tjavascript: \"js\",\n\tjsx: \"jsx\",\n\tjson: \"json\",\n\tcss: \"css\",\n\thtml: \"html\",\n\tmd: \"md\",\n\tmarkdown: \"md\",\n\tpy: \"py\",\n\tpython: \"py\",\n\ttext: \"text\",\n\tprompt: \"text\",\n\tplain: \"text\",\n};\n\nconst DEFAULT_THEMES = { light: \"github-light\", dark: \"github-dark\" } as const;\n\n/**\n * Per-render-tree memoization of `codeToHtml`. React's `cache()` dedupes\n * identical (code, lang, theme) combos within a single RSC render pass —\n * useful when a page renders many copies of the same install snippet.\n * Shiki itself caches grammars/themes per process, so this layer only\n * eliminates duplicate parse + tokenize work within one request.\n */\nconst cachedCodeToHtml = cache(\n\tasync (code: string, lang: SupportedLang, themesKey: string, themes: { light: string; dark: string }) => {\n\t\tvoid themesKey;\n\t\treturn codeToHtml(code, { lang, themes, defaultColor: false });\n\t},\n);\n\nfunction resolveLang(label?: string, language?: SupportedLang): SupportedLang {\n\tif (language) return language;\n\tif (label) {\n\t\tconst fromLabel = LABEL_TO_LANG[label.toLowerCase()];\n\t\tif (fromLabel) return fromLabel;\n\t}\n\treturn \"text\";\n}\n\n/**\n * Syntax-highlighted code block with a language-label header and a copy\n * button. Highlighting runs server-side via Shiki using a dual-theme\n * (default: github-light / github-dark) so the same HTML flips on\n * `data-theme`/`prefers-color-scheme` without client-side rehydration.\n *\n * Async Server Component — render inside RSC trees or pre-render in\n * static contexts. For client-side streaming code blocks, plug Streamdown's\n * built-in code rendering via `Markdown components={{ pre: … }}` instead.\n *\n * @example\n * <CodeBlock label=\"pnpm\" code=\"pnpm add @hex-core/components\" />\n * @example\n * <CodeBlock language=\"tsx\" code={src} />\n */\nexport interface CodeBlockProps {\n\tcode: string;\n\t/** Optional header label (e.g. \"pnpm\", \"tsx\"). Inferred from `language` if omitted. */\n\tlabel?: string;\n\t/** Explicit Shiki grammar key. Overrides inference from `label`. */\n\tlanguage?: SupportedLang;\n\t/** Override the default github-light / github-dark theme pair. */\n\tthemes?: { light: string; dark: string };\n\tclassName?: string;\n}\n\n/**\n * Async Server Component that highlights `code` server-side and emits the\n * resulting HTML with a header + copy island.\n *\n * @param props - code, optional label/language/themes\n * @returns A bordered card wrapping the highlighted code\n */\nasync function CodeBlock({\n\tcode,\n\tlabel,\n\tlanguage,\n\tthemes = DEFAULT_THEMES,\n\tclassName,\n}: CodeBlockProps) {\n\tconst lang = resolveLang(label, language);\n\tconst themesKey = `${themes.light}|${themes.dark}`;\n\tconst html = await cachedCodeToHtml(code, lang, themesKey, themes);\n\tconst displayLabel = label ?? lang;\n\n\treturn (\n\t\t<div\n\t\t\tclassName={cn(\n\t\t\t\t\"group relative overflow-hidden rounded-lg border bg-card text-card-foreground\",\n\t\t\t\tclassName,\n\t\t\t)}\n\t\t>\n\t\t\t<div className=\"flex items-center justify-between border-b bg-muted/40 px-3 py-1.5\">\n\t\t\t\t<span className=\"font-mono text-xs font-medium text-muted-foreground\">{displayLabel}</span>\n\t\t\t\t<CodeBlockCopy code={code} />\n\t\t\t</div>\n\t\t\t<div\n\t\t\t\tdata-shiki=\"\"\n\t\t\t\tclassName=\"overflow-x-auto p-4 font-mono text-sm [&_pre]:!bg-transparent\"\n\t\t\t\t// biome-ignore lint/security/noDangerouslySetInnerHtml: Shiki output is trusted server-rendered HTML\n\t\t\t\tdangerouslySetInnerHTML={{ __html: html }}\n\t\t\t/>\n\t\t</div>\n\t);\n}\n\nexport { CodeBlock };\n"]}
1
+ {"version":3,"sources":["../src/lib/utils.ts","../src/ai/code-block/code-block.tsx"],"names":[],"mappings":"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","file":"code-block.js","sourcesContent":["import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge class names with Tailwind CSS conflict resolution.\n * @param inputs - Class values (strings, arrays, objects) to merge\n * @returns A single merged class string with Tailwind conflicts resolved\n */\nexport function cn(...inputs: ClassValue[]) {\n\treturn twMerge(clsx(inputs));\n}\n","import * as React from \"react\";\nimport { cache } from \"react\";\nimport { codeToHtml } from \"shiki\";\nimport { cn } from \"../../lib/utils.js\";\nimport { CodeBlockCopy } from \"./code-block-copy.js\";\n\n/**\n * Languages we surface in the prop type. Plain literal union of real\n * Shiki grammar IDs — kept literal (not `Extract<BundledLanguage, …>`)\n * because Shiki's full bundled-language union is 600+ literals, and\n * deriving from it forced the rollup-dts pass past a 4GB heap. The\n * literals here are all standard Shiki grammar IDs; if Shiki removes\n * one upstream, `codeToHtml` will throw at runtime — acceptable trade.\n */\nexport type SupportedLang =\n\t| \"bash\"\n\t| \"ts\"\n\t| \"tsx\"\n\t| \"js\"\n\t| \"jsx\"\n\t| \"json\"\n\t| \"css\"\n\t| \"html\"\n\t| \"md\"\n\t| \"py\"\n\t| \"text\";\n\nconst LABEL_TO_LANG: Record<string, SupportedLang> = {\n\tpnpm: \"bash\",\n\tnpm: \"bash\",\n\tyarn: \"bash\",\n\tbun: \"bash\",\n\tbash: \"bash\",\n\tsh: \"bash\",\n\tshell: \"bash\",\n\tzsh: \"bash\",\n\tts: \"ts\",\n\ttypescript: \"ts\",\n\ttsx: \"tsx\",\n\tjs: \"js\",\n\tjavascript: \"js\",\n\tjsx: \"jsx\",\n\tjson: \"json\",\n\tcss: \"css\",\n\thtml: \"html\",\n\tmd: \"md\",\n\tmarkdown: \"md\",\n\tpy: \"py\",\n\tpython: \"py\",\n\ttext: \"text\",\n\tprompt: \"text\",\n\tplain: \"text\",\n};\n\nconst DEFAULT_THEMES = { light: \"github-light-high-contrast\", dark: \"github-dark\" } as const;\n\n/**\n * Per-render-tree memoization of `codeToHtml`. React's `cache()` dedupes\n * identical (code, lang, theme) combos within a single RSC render pass —\n * useful when a page renders many copies of the same install snippet.\n * Shiki itself caches grammars/themes per process, so this layer only\n * eliminates duplicate parse + tokenize work within one request.\n */\nconst cachedCodeToHtml = cache(\n\tasync (code: string, lang: SupportedLang, themesKey: string, themes: { light: string; dark: string }) => {\n\t\tvoid themesKey;\n\t\treturn codeToHtml(code, { lang, themes, defaultColor: false });\n\t},\n);\n\nfunction resolveLang(label?: string, language?: SupportedLang): SupportedLang {\n\tif (language) return language;\n\tif (label) {\n\t\tconst fromLabel = LABEL_TO_LANG[label.toLowerCase()];\n\t\tif (fromLabel) return fromLabel;\n\t}\n\treturn \"text\";\n}\n\n/**\n * Syntax-highlighted code block with a language-label header and a copy\n * button. Highlighting runs server-side via Shiki using a dual-theme\n * (default: github-light / github-dark) so the same HTML flips on\n * `data-theme`/`prefers-color-scheme` without client-side rehydration.\n *\n * Async Server Component — render inside RSC trees or pre-render in\n * static contexts. For client-side streaming code blocks, plug Streamdown's\n * built-in code rendering via `Markdown components={{ pre: … }}` instead.\n *\n * @example\n * <CodeBlock label=\"pnpm\" code=\"pnpm add @hex-core/components\" />\n * @example\n * <CodeBlock language=\"tsx\" code={src} />\n */\nexport interface CodeBlockProps {\n\tcode: string;\n\t/** Optional header label (e.g. \"pnpm\", \"tsx\"). Inferred from `language` if omitted. */\n\tlabel?: string;\n\t/** Explicit Shiki grammar key. Overrides inference from `label`. */\n\tlanguage?: SupportedLang;\n\t/** Override the default github-light / github-dark theme pair. */\n\tthemes?: { light: string; dark: string };\n\tclassName?: string;\n}\n\n/**\n * Async Server Component that highlights `code` server-side and emits the\n * resulting HTML with a header + copy island.\n *\n * @param props - code, optional label/language/themes\n * @returns A bordered card wrapping the highlighted code\n */\nasync function CodeBlock({\n\tcode,\n\tlabel,\n\tlanguage,\n\tthemes = DEFAULT_THEMES,\n\tclassName,\n}: CodeBlockProps) {\n\tconst lang = resolveLang(label, language);\n\tconst themesKey = `${themes.light}|${themes.dark}`;\n\tconst html = await cachedCodeToHtml(code, lang, themesKey, themes);\n\tconst displayLabel = label ?? lang;\n\n\treturn (\n\t\t<div\n\t\t\tclassName={cn(\n\t\t\t\t\"group relative overflow-hidden rounded-lg border bg-card text-card-foreground\",\n\t\t\t\tclassName,\n\t\t\t)}\n\t\t>\n\t\t\t<div className=\"flex items-center justify-between border-b bg-muted/40 px-3 py-1.5\">\n\t\t\t\t<span className=\"font-mono text-xs font-medium text-muted-foreground\">{displayLabel}</span>\n\t\t\t\t<CodeBlockCopy code={code} />\n\t\t\t</div>\n\t\t\t<div\n\t\t\t\tdata-shiki=\"\"\n\t\t\t\tclassName=\"overflow-x-auto p-4 font-mono text-sm [&_pre]:!bg-transparent\"\n\t\t\t\t// biome-ignore lint/security/noDangerouslySetInnerHtml: Shiki output is trusted server-rendered HTML\n\t\t\t\tdangerouslySetInnerHTML={{ __html: html }}\n\t\t\t/>\n\t\t</div>\n\t);\n}\n\nexport { CodeBlock };\n"]}
package/dist/dropzone.js CHANGED
@@ -121,31 +121,31 @@ function Dropzone({
121
121
  isDisabled: disabled,
122
122
  openFileDialog
123
123
  };
124
- return /* @__PURE__ */ jsxs(
125
- "div",
126
- {
127
- role: "button",
128
- tabIndex: disabled ? -1 : 0,
129
- "aria-label": ariaLabel,
130
- "aria-disabled": disabled || void 0,
131
- "data-drag-over": isDragOver || void 0,
132
- onClick: openFileDialog,
133
- onKeyDown: handleKeyDown,
134
- onDragEnter: handleDragEnter,
135
- onDragOver: handleDragOver,
136
- onDragLeave: handleDragLeave,
137
- onDrop: handleDrop,
138
- className: cn(
139
- "flex w-full cursor-pointer select-none flex-col items-center justify-center gap-[var(--space-2,0.5rem)] rounded-md border-2 border-dashed border-input bg-background px-[var(--space-6,1.5rem)] py-[var(--space-8,2rem)] text-center text-sm transition-all duration-[var(--duration-normal,200ms)] ease-out",
140
- "hover:bg-accent hover:text-accent-foreground",
141
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
142
- isDragOver && "border-primary bg-accent text-accent-foreground",
143
- disabled && "pointer-events-none opacity-50",
144
- className
145
- ),
146
- ...rest,
147
- children: [
148
- typeof children === "function" ? children(renderState) : children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
124
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
125
+ /* @__PURE__ */ jsx(
126
+ "div",
127
+ {
128
+ role: "button",
129
+ tabIndex: disabled ? -1 : 0,
130
+ "aria-label": ariaLabel,
131
+ "aria-disabled": disabled || void 0,
132
+ "data-drag-over": isDragOver || void 0,
133
+ onClick: openFileDialog,
134
+ onKeyDown: handleKeyDown,
135
+ onDragEnter: handleDragEnter,
136
+ onDragOver: handleDragOver,
137
+ onDragLeave: handleDragLeave,
138
+ onDrop: handleDrop,
139
+ className: cn(
140
+ "flex w-full cursor-pointer select-none flex-col items-center justify-center gap-[var(--space-2,0.5rem)] rounded-md border-2 border-dashed border-input bg-background px-[var(--space-6,1.5rem)] py-[var(--space-8,2rem)] text-center text-sm transition-all duration-[var(--duration-normal,200ms)] ease-out",
141
+ "hover:bg-accent hover:text-accent-foreground",
142
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
143
+ isDragOver && "border-primary bg-accent text-accent-foreground",
144
+ disabled && "pointer-events-none opacity-50",
145
+ className
146
+ ),
147
+ ...rest,
148
+ children: typeof children === "function" ? children(renderState) : children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
149
149
  /* @__PURE__ */ jsxs(
150
150
  "svg",
151
151
  {
@@ -167,25 +167,27 @@ function Dropzone({
167
167
  ),
168
168
  /* @__PURE__ */ jsx("span", { className: "font-medium", children: isDragOver ? "Drop files to upload" : "Drag files here or click to browse" }),
169
169
  accept ? /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: accept }) : null
170
- ] }),
171
- /* @__PURE__ */ jsx(
172
- "input",
173
- {
174
- ref: inputRef,
175
- type: "file",
176
- accept,
177
- multiple,
178
- disabled,
179
- className: "sr-only",
180
- onChange: (e) => {
181
- emit(e.target.files);
182
- e.target.value = "";
183
- }
184
- }
185
- )
186
- ]
187
- }
188
- );
170
+ ] })
171
+ }
172
+ ),
173
+ /* @__PURE__ */ jsx(
174
+ "input",
175
+ {
176
+ ref: inputRef,
177
+ type: "file",
178
+ accept,
179
+ multiple,
180
+ disabled,
181
+ "aria-hidden": "true",
182
+ tabIndex: -1,
183
+ className: "sr-only",
184
+ onChange: (e) => {
185
+ emit(e.target.files);
186
+ e.target.value = "";
187
+ }
188
+ }
189
+ )
190
+ ] });
189
191
  }
190
192
  Dropzone.displayName = "Dropzone";
191
193
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/lib/utils.ts","../src/components/dropzone/dropzone.tsx"],"names":[],"mappings":"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","file":"dropzone.js","sourcesContent":["import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge class names with Tailwind CSS conflict resolution.\n * @param inputs - Class values (strings, arrays, objects) to merge\n * @returns A single merged class string with Tailwind conflicts resolved\n */\nexport function cn(...inputs: ClassValue[]) {\n\treturn twMerge(clsx(inputs));\n}\n","\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"../../lib/utils.js\";\n\ninterface DropzoneProps\n\textends Omit<\n\t\tReact.HTMLAttributes<HTMLDivElement>,\n\t\t\"onChange\" | \"onDrop\" | \"children\"\n\t> {\n\t/** Fired with the accepted file list every time the user picks or drops files. */\n\tonFilesSelected?: (files: File[]) => void;\n\t/**\n\t * Fired when files are dropped/picked but ALL of them are filtered out by\n\t * `accept` / `maxSize` / `maxFiles`. Useful for surfacing \"file too large\"\n\t * or \"wrong type\" toasts to the user. Receives the rejected File[].\n\t */\n\tonFilesRejected?: (files: File[]) => void;\n\t/** `accept` attribute forwarded to the hidden file input (e.g. \"image/*\", \".csv\"). */\n\taccept?: string;\n\t/** Allow multiple files. Default true. */\n\tmultiple?: boolean;\n\t/** Maximum total file count (after dedupe). Excess files are dropped silently — surface in your handler. */\n\tmaxFiles?: number;\n\t/** Maximum size per file in bytes. Files over the cap are filtered before onFilesSelected fires. */\n\tmaxSize?: number;\n\t/** Disable interaction. */\n\tdisabled?: boolean;\n\t/** Optional render override for the dropzone body. Receives drag state. */\n\tchildren?: React.ReactNode | ((state: DropzoneRenderState) => React.ReactNode);\n\t/** Required accessible name for the drop area. */\n\t\"aria-label\": string;\n}\n\ninterface DropzoneRenderState {\n\tisDragOver: boolean;\n\tisDisabled: boolean;\n\topenFileDialog: () => void;\n}\n\n/** Apply `accept` / `maxSize` / `maxFiles` filters before emitting to onFilesSelected. */\nfunction filterFiles(\n\tfiles: FileList | File[],\n\t{ accept, maxSize, maxFiles }: { accept?: string; maxSize?: number; maxFiles?: number },\n): File[] {\n\tconst list = Array.from(files);\n\tconst acceptList = accept\n\t\t? accept\n\t\t\t\t.split(\",\")\n\t\t\t\t.map((s) => s.trim())\n\t\t\t\t.filter(Boolean)\n\t\t: undefined;\n\n\tconst matchAccept = (file: File): boolean => {\n\t\tif (!acceptList) return true;\n\t\treturn acceptList.some((entry) => {\n\t\t\tif (entry.startsWith(\".\")) {\n\t\t\t\treturn file.name.toLowerCase().endsWith(entry.toLowerCase());\n\t\t\t}\n\t\t\tif (entry.endsWith(\"/*\")) {\n\t\t\t\tconst prefix = entry.slice(0, -1); // \"image/\"\n\t\t\t\treturn file.type.startsWith(prefix);\n\t\t\t}\n\t\t\treturn file.type === entry;\n\t\t});\n\t};\n\n\tconst sized =\n\t\ttypeof maxSize === \"number\" ? list.filter((f) => f.size <= maxSize) : list;\n\tconst accepted = sized.filter(matchAccept);\n\tif (typeof maxFiles === \"number\") return accepted.slice(0, maxFiles);\n\treturn accepted;\n}\n\n/**\n * Drag-and-drop file input built on the native HTML5 drag-drop API plus a\n * visually-hidden (sr-only) `<input type=\"file\">` for screen-reader and\n * keyboard access.\n *\n * Two interaction surfaces:\n * - The visible drop area is a `role=\"button\"` div with `tabIndex=0` and the\n * required `aria-label`. Click, Enter, or Space proxies through to click the\n * hidden input, opening the system file dialog.\n * - The hidden input itself remains in the accessibility tree (sr-only, NOT\n * `aria-hidden`) so AT-driven file pickers can find it directly.\n *\n * Pass `children` as a node (default placeholder) or a function receiving\n * `{ isDragOver, isDisabled, openFileDialog }` for full layout control.\n * @returns A drop area + hidden file input pair that yields a File[].\n */\nfunction Dropzone({\n\tonFilesSelected,\n\tonFilesRejected,\n\taccept,\n\tmultiple = true,\n\tmaxFiles,\n\tmaxSize,\n\tdisabled = false,\n\tchildren,\n\tclassName,\n\t\"aria-label\": ariaLabel,\n\t...rest\n}: DropzoneProps) {\n\tconst inputRef = React.useRef<HTMLInputElement>(null);\n\tconst [isDragOver, setIsDragOver] = React.useState(false);\n\tconst dragCounter = React.useRef(0);\n\n\tconst emit = React.useCallback(\n\t\t(files: FileList | File[] | null | undefined) => {\n\t\t\tif (!files || disabled) return;\n\t\t\tconst all = Array.from(files);\n\t\t\tif (all.length === 0) return;\n\t\t\tconst accepted = filterFiles(all, { accept, maxSize, maxFiles });\n\t\t\tif (accepted.length === 0) {\n\t\t\t\tonFilesRejected?.(all);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst finalAccepted = !multiple ? accepted.slice(0, 1) : accepted;\n\t\t\tconst rejected = all.filter((f) => !finalAccepted.includes(f));\n\t\t\tonFilesSelected?.(finalAccepted);\n\t\t\tif (rejected.length > 0) onFilesRejected?.(rejected);\n\t\t},\n\t\t[\n\t\t\taccept,\n\t\t\tdisabled,\n\t\t\tmaxFiles,\n\t\t\tmaxSize,\n\t\t\tmultiple,\n\t\t\tonFilesSelected,\n\t\t\tonFilesRejected,\n\t\t],\n\t);\n\n\t/*\n\t * Reset the drag counter + isDragOver when the user cancels a drag outside\n\t * the dropzone (Esc, drag off the page, switch tab). Without this, the\n\t * counter can stay >0 and the dropzone gets stuck in its hover style.\n\t */\n\tReact.useEffect(() => {\n\t\tconst reset = () => {\n\t\t\tdragCounter.current = 0;\n\t\t\tsetIsDragOver(false);\n\t\t};\n\t\twindow.addEventListener(\"dragend\", reset);\n\t\twindow.addEventListener(\"drop\", reset);\n\t\treturn () => {\n\t\t\twindow.removeEventListener(\"dragend\", reset);\n\t\t\twindow.removeEventListener(\"drop\", reset);\n\t\t};\n\t}, []);\n\n\tconst openFileDialog = React.useCallback(() => {\n\t\tif (disabled) return;\n\t\tinputRef.current?.click();\n\t}, [disabled]);\n\n\tconst handleDragEnter = (e: React.DragEvent<HTMLDivElement>) => {\n\t\tif (disabled) return;\n\t\te.preventDefault();\n\t\tdragCounter.current += 1;\n\t\tif (e.dataTransfer.types.includes(\"Files\")) setIsDragOver(true);\n\t};\n\tconst handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {\n\t\tif (disabled) return;\n\t\te.preventDefault();\n\t\te.dataTransfer.dropEffect = \"copy\";\n\t};\n\tconst handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {\n\t\tif (disabled) return;\n\t\te.preventDefault();\n\t\tdragCounter.current = Math.max(0, dragCounter.current - 1);\n\t\tif (dragCounter.current === 0) setIsDragOver(false);\n\t};\n\tconst handleDrop = (e: React.DragEvent<HTMLDivElement>) => {\n\t\tif (disabled) return;\n\t\te.preventDefault();\n\t\tdragCounter.current = 0;\n\t\tsetIsDragOver(false);\n\t\temit(e.dataTransfer.files);\n\t};\n\tconst handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n\t\tif (disabled) return;\n\t\tif (e.key === \"Enter\" || e.key === \" \") {\n\t\t\te.preventDefault();\n\t\t\topenFileDialog();\n\t\t}\n\t};\n\n\tconst renderState: DropzoneRenderState = {\n\t\tisDragOver,\n\t\tisDisabled: disabled,\n\t\topenFileDialog,\n\t};\n\n\treturn (\n\t\t<div\n\t\t\trole=\"button\"\n\t\t\ttabIndex={disabled ? -1 : 0}\n\t\t\taria-label={ariaLabel}\n\t\t\taria-disabled={disabled || undefined}\n\t\t\tdata-drag-over={isDragOver || undefined}\n\t\t\tonClick={openFileDialog}\n\t\t\tonKeyDown={handleKeyDown}\n\t\t\tonDragEnter={handleDragEnter}\n\t\t\tonDragOver={handleDragOver}\n\t\t\tonDragLeave={handleDragLeave}\n\t\t\tonDrop={handleDrop}\n\t\t\tclassName={cn(\n\t\t\t\t\"flex w-full cursor-pointer select-none flex-col items-center justify-center gap-[var(--space-2,0.5rem)] rounded-md border-2 border-dashed border-input bg-background px-[var(--space-6,1.5rem)] py-[var(--space-8,2rem)] text-center text-sm transition-all duration-[var(--duration-normal,200ms)] ease-out\",\n\t\t\t\t\"hover:bg-accent hover:text-accent-foreground\",\n\t\t\t\t\"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n\t\t\t\tisDragOver && \"border-primary bg-accent text-accent-foreground\",\n\t\t\t\tdisabled && \"pointer-events-none opacity-50\",\n\t\t\t\tclassName,\n\t\t\t)}\n\t\t\t{...rest}\n\t\t>\n\t\t\t{typeof children === \"function\"\n\t\t\t\t? children(renderState)\n\t\t\t\t: (children ?? (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\t\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\t\t\t\tfill=\"none\"\n\t\t\t\t\t\t\t\tstroke=\"currentColor\"\n\t\t\t\t\t\t\t\tstrokeWidth=\"2\"\n\t\t\t\t\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\t\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\t\t\t\t\tclassName=\"h-6 w-6 text-muted-foreground\"\n\t\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" />\n\t\t\t\t\t\t\t\t<polyline points=\"17 8 12 3 7 8\" />\n\t\t\t\t\t\t\t\t<line x1=\"12\" y1=\"3\" x2=\"12\" y2=\"15\" />\n\t\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t\t<span className=\"font-medium\">\n\t\t\t\t\t\t\t\t{isDragOver ? \"Drop files to upload\" : \"Drag files here or click to browse\"}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t{accept ? (\n\t\t\t\t\t\t\t\t<span className=\"text-xs text-muted-foreground\">{accept}</span>\n\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t</>\n\t\t\t\t\t))}\n\t\t\t<input\n\t\t\t\tref={inputRef}\n\t\t\t\ttype=\"file\"\n\t\t\t\taccept={accept}\n\t\t\t\tmultiple={multiple}\n\t\t\t\tdisabled={disabled}\n\t\t\t\tclassName=\"sr-only\"\n\t\t\t\t/*\n\t\t\t\t * Intentionally NOT aria-hidden + NOT tabIndex=-1: the input\n\t\t\t\t * stays in the a11y tree so AT-driven file pickers (NVDA's\n\t\t\t\t * forms mode, JAWS) can find it. Visually hidden via sr-only.\n\t\t\t\t */\n\t\t\t\tonChange={(e) => {\n\t\t\t\t\temit(e.target.files);\n\t\t\t\t\t// Reset so picking the same file twice still fires onChange\n\t\t\t\t\te.target.value = \"\";\n\t\t\t\t}}\n\t\t\t/>\n\t\t</div>\n\t);\n}\nDropzone.displayName = \"Dropzone\";\n\nexport { Dropzone };\nexport type { DropzoneProps, DropzoneRenderState };\n"]}
1
+ {"version":3,"sources":["../src/lib/utils.ts","../src/components/dropzone/dropzone.tsx"],"names":[],"mappings":"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","file":"dropzone.js","sourcesContent":["import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge class names with Tailwind CSS conflict resolution.\n * @param inputs - Class values (strings, arrays, objects) to merge\n * @returns A single merged class string with Tailwind conflicts resolved\n */\nexport function cn(...inputs: ClassValue[]) {\n\treturn twMerge(clsx(inputs));\n}\n","\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"../../lib/utils.js\";\n\ninterface DropzoneProps\n\textends Omit<\n\t\tReact.HTMLAttributes<HTMLDivElement>,\n\t\t\"onChange\" | \"onDrop\" | \"children\"\n\t> {\n\t/** Fired with the accepted file list every time the user picks or drops files. */\n\tonFilesSelected?: (files: File[]) => void;\n\t/**\n\t * Fired when files are dropped/picked but ALL of them are filtered out by\n\t * `accept` / `maxSize` / `maxFiles`. Useful for surfacing \"file too large\"\n\t * or \"wrong type\" toasts to the user. Receives the rejected File[].\n\t */\n\tonFilesRejected?: (files: File[]) => void;\n\t/** `accept` attribute forwarded to the hidden file input (e.g. \"image/*\", \".csv\"). */\n\taccept?: string;\n\t/** Allow multiple files. Default true. */\n\tmultiple?: boolean;\n\t/** Maximum total file count (after dedupe). Excess files are dropped silently — surface in your handler. */\n\tmaxFiles?: number;\n\t/** Maximum size per file in bytes. Files over the cap are filtered before onFilesSelected fires. */\n\tmaxSize?: number;\n\t/** Disable interaction. */\n\tdisabled?: boolean;\n\t/** Optional render override for the dropzone body. Receives drag state. */\n\tchildren?: React.ReactNode | ((state: DropzoneRenderState) => React.ReactNode);\n\t/** Required accessible name for the drop area. */\n\t\"aria-label\": string;\n}\n\ninterface DropzoneRenderState {\n\tisDragOver: boolean;\n\tisDisabled: boolean;\n\topenFileDialog: () => void;\n}\n\n/** Apply `accept` / `maxSize` / `maxFiles` filters before emitting to onFilesSelected. */\nfunction filterFiles(\n\tfiles: FileList | File[],\n\t{ accept, maxSize, maxFiles }: { accept?: string; maxSize?: number; maxFiles?: number },\n): File[] {\n\tconst list = Array.from(files);\n\tconst acceptList = accept\n\t\t? accept\n\t\t\t\t.split(\",\")\n\t\t\t\t.map((s) => s.trim())\n\t\t\t\t.filter(Boolean)\n\t\t: undefined;\n\n\tconst matchAccept = (file: File): boolean => {\n\t\tif (!acceptList) return true;\n\t\treturn acceptList.some((entry) => {\n\t\t\tif (entry.startsWith(\".\")) {\n\t\t\t\treturn file.name.toLowerCase().endsWith(entry.toLowerCase());\n\t\t\t}\n\t\t\tif (entry.endsWith(\"/*\")) {\n\t\t\t\tconst prefix = entry.slice(0, -1); // \"image/\"\n\t\t\t\treturn file.type.startsWith(prefix);\n\t\t\t}\n\t\t\treturn file.type === entry;\n\t\t});\n\t};\n\n\tconst sized =\n\t\ttypeof maxSize === \"number\" ? list.filter((f) => f.size <= maxSize) : list;\n\tconst accepted = sized.filter(matchAccept);\n\tif (typeof maxFiles === \"number\") return accepted.slice(0, maxFiles);\n\treturn accepted;\n}\n\n/**\n * Drag-and-drop file input built on the native HTML5 drag-drop API plus a\n * visually-hidden (sr-only) `<input type=\"file\">` for screen-reader and\n * keyboard access.\n *\n * Two interaction surfaces:\n * - The visible drop area is a `role=\"button\"` div with `tabIndex=0` and the\n * required `aria-label`. Click, Enter, or Space proxies through to click the\n * hidden input, opening the system file dialog.\n * - The hidden input itself remains in the accessibility tree (sr-only, NOT\n * `aria-hidden`) so AT-driven file pickers can find it directly.\n *\n * Pass `children` as a node (default placeholder) or a function receiving\n * `{ isDragOver, isDisabled, openFileDialog }` for full layout control.\n * @returns A drop area + hidden file input pair that yields a File[].\n */\nfunction Dropzone({\n\tonFilesSelected,\n\tonFilesRejected,\n\taccept,\n\tmultiple = true,\n\tmaxFiles,\n\tmaxSize,\n\tdisabled = false,\n\tchildren,\n\tclassName,\n\t\"aria-label\": ariaLabel,\n\t...rest\n}: DropzoneProps) {\n\tconst inputRef = React.useRef<HTMLInputElement>(null);\n\tconst [isDragOver, setIsDragOver] = React.useState(false);\n\tconst dragCounter = React.useRef(0);\n\n\tconst emit = React.useCallback(\n\t\t(files: FileList | File[] | null | undefined) => {\n\t\t\tif (!files || disabled) return;\n\t\t\tconst all = Array.from(files);\n\t\t\tif (all.length === 0) return;\n\t\t\tconst accepted = filterFiles(all, { accept, maxSize, maxFiles });\n\t\t\tif (accepted.length === 0) {\n\t\t\t\tonFilesRejected?.(all);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst finalAccepted = !multiple ? accepted.slice(0, 1) : accepted;\n\t\t\tconst rejected = all.filter((f) => !finalAccepted.includes(f));\n\t\t\tonFilesSelected?.(finalAccepted);\n\t\t\tif (rejected.length > 0) onFilesRejected?.(rejected);\n\t\t},\n\t\t[\n\t\t\taccept,\n\t\t\tdisabled,\n\t\t\tmaxFiles,\n\t\t\tmaxSize,\n\t\t\tmultiple,\n\t\t\tonFilesSelected,\n\t\t\tonFilesRejected,\n\t\t],\n\t);\n\n\t/*\n\t * Reset the drag counter + isDragOver when the user cancels a drag outside\n\t * the dropzone (Esc, drag off the page, switch tab). Without this, the\n\t * counter can stay >0 and the dropzone gets stuck in its hover style.\n\t */\n\tReact.useEffect(() => {\n\t\tconst reset = () => {\n\t\t\tdragCounter.current = 0;\n\t\t\tsetIsDragOver(false);\n\t\t};\n\t\twindow.addEventListener(\"dragend\", reset);\n\t\twindow.addEventListener(\"drop\", reset);\n\t\treturn () => {\n\t\t\twindow.removeEventListener(\"dragend\", reset);\n\t\t\twindow.removeEventListener(\"drop\", reset);\n\t\t};\n\t}, []);\n\n\tconst openFileDialog = React.useCallback(() => {\n\t\tif (disabled) return;\n\t\tinputRef.current?.click();\n\t}, [disabled]);\n\n\tconst handleDragEnter = (e: React.DragEvent<HTMLDivElement>) => {\n\t\tif (disabled) return;\n\t\te.preventDefault();\n\t\tdragCounter.current += 1;\n\t\tif (e.dataTransfer.types.includes(\"Files\")) setIsDragOver(true);\n\t};\n\tconst handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {\n\t\tif (disabled) return;\n\t\te.preventDefault();\n\t\te.dataTransfer.dropEffect = \"copy\";\n\t};\n\tconst handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {\n\t\tif (disabled) return;\n\t\te.preventDefault();\n\t\tdragCounter.current = Math.max(0, dragCounter.current - 1);\n\t\tif (dragCounter.current === 0) setIsDragOver(false);\n\t};\n\tconst handleDrop = (e: React.DragEvent<HTMLDivElement>) => {\n\t\tif (disabled) return;\n\t\te.preventDefault();\n\t\tdragCounter.current = 0;\n\t\tsetIsDragOver(false);\n\t\temit(e.dataTransfer.files);\n\t};\n\tconst handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n\t\tif (disabled) return;\n\t\tif (e.key === \"Enter\" || e.key === \" \") {\n\t\t\te.preventDefault();\n\t\t\topenFileDialog();\n\t\t}\n\t};\n\n\tconst renderState: DropzoneRenderState = {\n\t\tisDragOver,\n\t\tisDisabled: disabled,\n\t\topenFileDialog,\n\t};\n\n\treturn (\n\t\t<>\n\t\t\t<div\n\t\t\t\trole=\"button\"\n\t\t\t\ttabIndex={disabled ? -1 : 0}\n\t\t\t\taria-label={ariaLabel}\n\t\t\t\taria-disabled={disabled || undefined}\n\t\t\t\tdata-drag-over={isDragOver || undefined}\n\t\t\t\tonClick={openFileDialog}\n\t\t\t\tonKeyDown={handleKeyDown}\n\t\t\t\tonDragEnter={handleDragEnter}\n\t\t\t\tonDragOver={handleDragOver}\n\t\t\t\tonDragLeave={handleDragLeave}\n\t\t\t\tonDrop={handleDrop}\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"flex w-full cursor-pointer select-none flex-col items-center justify-center gap-[var(--space-2,0.5rem)] rounded-md border-2 border-dashed border-input bg-background px-[var(--space-6,1.5rem)] py-[var(--space-8,2rem)] text-center text-sm transition-all duration-[var(--duration-normal,200ms)] ease-out\",\n\t\t\t\t\t\"hover:bg-accent hover:text-accent-foreground\",\n\t\t\t\t\t\"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n\t\t\t\t\tisDragOver && \"border-primary bg-accent text-accent-foreground\",\n\t\t\t\t\tdisabled && \"pointer-events-none opacity-50\",\n\t\t\t\t\tclassName,\n\t\t\t\t)}\n\t\t\t\t{...rest}\n\t\t\t>\n\t\t\t\t{typeof children === \"function\"\n\t\t\t\t\t? children(renderState)\n\t\t\t\t\t: (children ?? (\n\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\t\t\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\t\t\t\t\tfill=\"none\"\n\t\t\t\t\t\t\t\t\tstroke=\"currentColor\"\n\t\t\t\t\t\t\t\t\tstrokeWidth=\"2\"\n\t\t\t\t\t\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\t\t\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\t\t\t\t\t\tclassName=\"h-6 w-6 text-muted-foreground\"\n\t\t\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" />\n\t\t\t\t\t\t\t\t\t<polyline points=\"17 8 12 3 7 8\" />\n\t\t\t\t\t\t\t\t\t<line x1=\"12\" y1=\"3\" x2=\"12\" y2=\"15\" />\n\t\t\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t\t\t<span className=\"font-medium\">\n\t\t\t\t\t\t\t\t\t{isDragOver ? \"Drop files to upload\" : \"Drag files here or click to browse\"}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t{accept ? (\n\t\t\t\t\t\t\t\t\t<span className=\"text-xs text-muted-foreground\">{accept}</span>\n\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t))}\n\t\t\t</div>\n\t\t\t{/*\n\t\t\t * Input lives OUTSIDE the role=\"button\" container to satisfy axe\n\t\t\t * nested-interactive. It is aria-hidden (outer button is the AT\n\t\t\t * surface) and clicked programmatically via inputRef.current?.click().\n\t\t\t */}\n\t\t\t<input\n\t\t\t\tref={inputRef}\n\t\t\t\ttype=\"file\"\n\t\t\t\taccept={accept}\n\t\t\t\tmultiple={multiple}\n\t\t\t\tdisabled={disabled}\n\t\t\t\taria-hidden=\"true\"\n\t\t\t\ttabIndex={-1}\n\t\t\t\tclassName=\"sr-only\"\n\t\t\t\tonChange={(e) => {\n\t\t\t\t\temit(e.target.files);\n\t\t\t\t\t// Reset so picking the same file twice still fires onChange\n\t\t\t\t\te.target.value = \"\";\n\t\t\t\t}}\n\t\t\t/>\n\t\t</>\n\t);\n}\nDropzone.displayName = \"Dropzone\";\n\nexport { Dropzone };\nexport type { DropzoneProps, DropzoneRenderState };\n"]}
@@ -0,0 +1,3 @@
1
+ export { EmptyProps_alias_1 as EmptyProps } from './_tsup-dts-rollup.js';
2
+ export { Empty_alias_1 as Empty } from './_tsup-dts-rollup.js';
3
+ export { emptyVariants_alias_1 as emptyVariants } from './_tsup-dts-rollup.js';
package/dist/empty.js ADDED
@@ -0,0 +1,94 @@
1
+ "use client";
2
+ import { cva } from 'class-variance-authority';
3
+ import * as React from 'react';
4
+ import { clsx } from 'clsx';
5
+ import { twMerge } from 'tailwind-merge';
6
+ import { jsxs, jsx } from 'react/jsx-runtime';
7
+
8
+ // src/primitives/empty/empty.tsx
9
+ function cn(...inputs) {
10
+ return twMerge(clsx(inputs));
11
+ }
12
+ var emptyVariants = cva(
13
+ [
14
+ "flex flex-col items-center justify-center text-center",
15
+ "rounded-md border border-dashed border-border bg-muted/30"
16
+ ].join(" "),
17
+ {
18
+ variants: {
19
+ size: {
20
+ sm: "gap-[var(--space-2,0.5rem)] px-[var(--space-4,1rem)] py-[var(--space-6,1.5rem)] text-sm",
21
+ default: "gap-[var(--space-3,0.75rem)] px-[var(--space-6,1.5rem)] py-[var(--space-8,2rem)]",
22
+ lg: "gap-[var(--space-4,1rem)] px-[var(--space-8,2rem)] py-[var(--space-12,3rem)]"
23
+ }
24
+ },
25
+ defaultVariants: { size: "default" }
26
+ }
27
+ );
28
+ var emptyIconWrapperVariants = cva(
29
+ "flex shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground [&_svg]:size-5",
30
+ {
31
+ variants: {
32
+ size: {
33
+ sm: "h-9 w-9",
34
+ default: "h-12 w-12 [&_svg]:size-6",
35
+ lg: "h-16 w-16 [&_svg]:size-7"
36
+ }
37
+ },
38
+ defaultVariants: { size: "default" }
39
+ }
40
+ );
41
+ var emptyTitleVariants = cva("font-semibold text-foreground", {
42
+ variants: {
43
+ size: {
44
+ sm: "text-sm",
45
+ default: "text-base",
46
+ lg: "text-lg"
47
+ }
48
+ },
49
+ defaultVariants: { size: "default" }
50
+ });
51
+ var emptyDescriptionVariants = cva("max-w-md text-muted-foreground", {
52
+ variants: {
53
+ size: {
54
+ sm: "text-xs",
55
+ default: "text-sm",
56
+ lg: "text-base"
57
+ }
58
+ },
59
+ defaultVariants: { size: "default" }
60
+ });
61
+ function Empty({
62
+ className,
63
+ size,
64
+ icon,
65
+ title,
66
+ description,
67
+ action,
68
+ titleAs = "h3",
69
+ ref,
70
+ ...props
71
+ }) {
72
+ const titleId = React.useId();
73
+ const TitleComp = titleAs;
74
+ return /* @__PURE__ */ jsxs(
75
+ "div",
76
+ {
77
+ ref,
78
+ role: "region",
79
+ "aria-labelledby": titleId,
80
+ className: cn(emptyVariants({ size }), className),
81
+ ...props,
82
+ children: [
83
+ icon ? /* @__PURE__ */ jsx("div", { className: emptyIconWrapperVariants({ size }), "aria-hidden": "true", children: icon }) : null,
84
+ /* @__PURE__ */ jsx(TitleComp, { id: titleId, className: emptyTitleVariants({ size }), children: title }),
85
+ description ? /* @__PURE__ */ jsx("div", { className: emptyDescriptionVariants({ size }), children: description }) : null,
86
+ action ? /* @__PURE__ */ jsx("div", { className: "mt-[var(--space-2,0.5rem)]", children: action }) : null
87
+ ]
88
+ }
89
+ );
90
+ }
91
+
92
+ export { Empty, emptyVariants };
93
+ //# sourceMappingURL=empty.js.map
94
+ //# sourceMappingURL=empty.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/utils.ts","../src/primitives/empty/empty.tsx"],"names":[],"mappings":"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","file":"empty.js","sourcesContent":["import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge class names with Tailwind CSS conflict resolution.\n * @param inputs - Class values (strings, arrays, objects) to merge\n * @returns A single merged class string with Tailwind conflicts resolved\n */\nexport function cn(...inputs: ClassValue[]) {\n\treturn twMerge(clsx(inputs));\n}\n","import { type VariantProps, cva } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/utils.js\";\n\nconst emptyVariants = cva(\n\t[\n\t\t\"flex flex-col items-center justify-center text-center\",\n\t\t\"rounded-md border border-dashed border-border bg-muted/30\",\n\t].join(\" \"),\n\t{\n\t\tvariants: {\n\t\t\tsize: {\n\t\t\t\tsm: \"gap-[var(--space-2,0.5rem)] px-[var(--space-4,1rem)] py-[var(--space-6,1.5rem)] text-sm\",\n\t\t\t\tdefault: \"gap-[var(--space-3,0.75rem)] px-[var(--space-6,1.5rem)] py-[var(--space-8,2rem)]\",\n\t\t\t\tlg: \"gap-[var(--space-4,1rem)] px-[var(--space-8,2rem)] py-[var(--space-12,3rem)]\",\n\t\t\t},\n\t\t},\n\t\tdefaultVariants: { size: \"default\" },\n\t},\n);\n\nconst emptyIconWrapperVariants = cva(\n\t\"flex shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground [&_svg]:size-5\",\n\t{\n\t\tvariants: {\n\t\t\tsize: {\n\t\t\t\tsm: \"h-9 w-9\",\n\t\t\t\tdefault: \"h-12 w-12 [&_svg]:size-6\",\n\t\t\t\tlg: \"h-16 w-16 [&_svg]:size-7\",\n\t\t\t},\n\t\t},\n\t\tdefaultVariants: { size: \"default\" },\n\t},\n);\n\nconst emptyTitleVariants = cva(\"font-semibold text-foreground\", {\n\tvariants: {\n\t\tsize: {\n\t\t\tsm: \"text-sm\",\n\t\t\tdefault: \"text-base\",\n\t\t\tlg: \"text-lg\",\n\t\t},\n\t},\n\tdefaultVariants: { size: \"default\" },\n});\n\nconst emptyDescriptionVariants = cva(\"max-w-md text-muted-foreground\", {\n\tvariants: {\n\t\tsize: {\n\t\t\tsm: \"text-xs\",\n\t\t\tdefault: \"text-sm\",\n\t\t\tlg: \"text-base\",\n\t\t},\n\t},\n\tdefaultVariants: { size: \"default\" },\n});\n\n/** Heading element used to render the Empty title. Defaults to `h3`. */\ntype EmptyTitleAs = \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" | \"p\";\n\nexport interface EmptyProps\n\textends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\">,\n\t\tVariantProps<typeof emptyVariants> {\n\t/** Forwarded ref onto the root region element. */\n\tref?: React.Ref<HTMLDivElement>;\n\t/** Optional icon (typically an `<svg>`) rendered in a circular muted container. */\n\ticon?: React.ReactNode;\n\t/** Required heading copy. Becomes the region's accessible name via `aria-labelledby`. */\n\ttitle: React.ReactNode;\n\t/** Optional supporting copy that explains why the slot is empty + what to do next. */\n\tdescription?: React.ReactNode;\n\t/** Optional call-to-action — typically a `<Button>` that creates the missing record. */\n\taction?: React.ReactNode;\n\t/** Heading level for the title — pick to match surrounding hierarchy (default `h3`). */\n\ttitleAs?: EmptyTitleAs;\n}\n\n/**\n * A \"zero-state\" surface for lists, dashboards, and search results that have\n * no content to show. Use to explain *why* the slot is empty and *what to do*\n * next; pair the `action` slot with a button that creates the missing record.\n *\n * Distinct from {@link Loading} (transient, has a measurable wait) and\n * {@link ErrorState} (something failed and may need a retry). If you're\n * thinking \"show a message because the request just hasn't returned yet,\"\n * reach for `Loading` — Empty is for \"the request returned, and there's\n * nothing to show.\"\n *\n * @example\n * ```tsx\n * <Empty\n * icon={<InboxIcon />}\n * title=\"No messages yet\"\n * description=\"When someone sends you a message, it'll show up here.\"\n * action={<Button>Compose</Button>}\n * />\n * ```\n *\n * @returns A region landmark labeled by the title.\n */\nfunction Empty({\n\tclassName,\n\tsize,\n\ticon,\n\ttitle,\n\tdescription,\n\taction,\n\ttitleAs = \"h3\",\n\tref,\n\t...props\n}: EmptyProps) {\n\tconst titleId = React.useId();\n\tconst TitleComp = titleAs;\n\treturn (\n\t\t<div\n\t\t\tref={ref}\n\t\t\trole=\"region\"\n\t\t\taria-labelledby={titleId}\n\t\t\tclassName={cn(emptyVariants({ size }), className)}\n\t\t\t{...props}\n\t\t>\n\t\t\t{icon ? (\n\t\t\t\t<div className={emptyIconWrapperVariants({ size })} aria-hidden=\"true\">\n\t\t\t\t\t{icon}\n\t\t\t\t</div>\n\t\t\t) : null}\n\t\t\t<TitleComp id={titleId} className={emptyTitleVariants({ size })}>\n\t\t\t\t{title}\n\t\t\t</TitleComp>\n\t\t\t{description ? (\n\t\t\t\t<div className={emptyDescriptionVariants({ size })}>{description}</div>\n\t\t\t) : null}\n\t\t\t{action ? <div className=\"mt-[var(--space-2,0.5rem)]\">{action}</div> : null}\n\t\t</div>\n\t);\n}\n\nexport { Empty, emptyVariants };\n"]}
@@ -0,0 +1,3 @@
1
+ export { ErrorStateProps_alias_1 as ErrorStateProps } from './_tsup-dts-rollup.js';
2
+ export { ErrorState_alias_1 as ErrorState } from './_tsup-dts-rollup.js';
3
+ export { errorStateVariants_alias_1 as errorStateVariants } from './_tsup-dts-rollup.js';
@@ -0,0 +1,67 @@
1
+ "use client";
2
+ import { cva } from 'class-variance-authority';
3
+ import { clsx } from 'clsx';
4
+ import { twMerge } from 'tailwind-merge';
5
+ import { jsxs, jsx } from 'react/jsx-runtime';
6
+
7
+ // src/primitives/error-state/error-state.tsx
8
+ function cn(...inputs) {
9
+ return twMerge(clsx(inputs));
10
+ }
11
+ var errorStateVariants = cva(
12
+ [
13
+ "flex flex-col items-center justify-center text-center",
14
+ "rounded-md border px-[var(--space-6,1.5rem)] py-[var(--space-8,2rem)] gap-[var(--space-3,0.75rem)]"
15
+ ].join(" "),
16
+ {
17
+ variants: {
18
+ variant: {
19
+ default: "border-border bg-muted/30",
20
+ destructive: "border-destructive/30 bg-destructive/5"
21
+ }
22
+ },
23
+ defaultVariants: { variant: "default" }
24
+ }
25
+ );
26
+ var errorIconWrapperVariants = cva(
27
+ "flex h-12 w-12 shrink-0 items-center justify-center rounded-full [&_svg]:size-6",
28
+ {
29
+ variants: {
30
+ variant: {
31
+ default: "bg-muted text-muted-foreground",
32
+ destructive: "bg-destructive/10 text-destructive"
33
+ }
34
+ },
35
+ defaultVariants: { variant: "default" }
36
+ }
37
+ );
38
+ function ErrorState({
39
+ className,
40
+ variant,
41
+ icon,
42
+ title = "Something went wrong",
43
+ message,
44
+ action,
45
+ ref,
46
+ ...props
47
+ }) {
48
+ return /* @__PURE__ */ jsxs(
49
+ "div",
50
+ {
51
+ ref,
52
+ role: "alert",
53
+ className: cn(errorStateVariants({ variant }), className),
54
+ ...props,
55
+ children: [
56
+ icon ? /* @__PURE__ */ jsx("div", { className: errorIconWrapperVariants({ variant }), "aria-hidden": "true", children: icon }) : null,
57
+ /* @__PURE__ */ jsx("div", { className: "font-semibold text-foreground", children: title }),
58
+ /* @__PURE__ */ jsx("div", { className: "max-w-md text-sm text-muted-foreground", children: message }),
59
+ action ? /* @__PURE__ */ jsx("div", { className: "mt-[var(--space-2,0.5rem)]", children: action }) : null
60
+ ]
61
+ }
62
+ );
63
+ }
64
+
65
+ export { ErrorState, errorStateVariants };
66
+ //# sourceMappingURL=error-state.js.map
67
+ //# sourceMappingURL=error-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/utils.ts","../src/primitives/error-state/error-state.tsx"],"names":[],"mappings":";;;;;;AAQO,SAAS,MAAM,MAAA,EAAsB;AAC3C,EAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAC,CAAA;AAC5B;ACNA,IAAM,kBAAA,GAAqB,GAAA;AAAA,EAC1B;AAAA,IACC,uDAAA;AAAA,IACA;AAAA,GACD,CAAE,KAAK,GAAG,CAAA;AAAA,EACV;AAAA,IACC,QAAA,EAAU;AAAA,MACT,OAAA,EAAS;AAAA,QACR,OAAA,EAAS,2BAAA;AAAA,QACT,WAAA,EAAa;AAAA;AACd,KACD;AAAA,IACA,eAAA,EAAiB,EAAE,OAAA,EAAS,SAAA;AAAU;AAExC;AAEA,IAAM,wBAAA,GAA2B,GAAA;AAAA,EAChC,iFAAA;AAAA,EACA;AAAA,IACC,QAAA,EAAU;AAAA,MACT,OAAA,EAAS;AAAA,QACR,OAAA,EAAS,gCAAA;AAAA,QACT,WAAA,EAAa;AAAA;AACd,KACD;AAAA,IACA,eAAA,EAAiB,EAAE,OAAA,EAAS,SAAA;AAAU;AAExC,CAAA;AA6CA,SAAS,UAAA,CAAW;AAAA,EACnB,SAAA;AAAA,EACA,OAAA;AAAA,EACA,IAAA;AAAA,EACA,KAAA,GAAQ,sBAAA;AAAA,EACR,OAAA;AAAA,EACA,MAAA;AAAA,EACA,GAAA;AAAA,EACA,GAAG;AACJ,CAAA,EAAoB;AACnB,EAAA,uBACC,IAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACA,GAAA;AAAA,MACA,IAAA,EAAK,OAAA;AAAA,MACL,WAAW,EAAA,CAAG,kBAAA,CAAmB,EAAE,OAAA,EAAS,GAAG,SAAS,CAAA;AAAA,MACvD,GAAG,KAAA;AAAA,MAEH,QAAA,EAAA;AAAA,QAAA,IAAA,mBACA,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAW,wBAAA,CAAyB,EAAE,OAAA,EAAS,CAAA,EAAG,aAAA,EAAY,MAAA,EACjE,QAAA,EAAA,IAAA,EACF,CAAA,GACG,IAAA;AAAA,wBACJ,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,+BAAA,EAAiC,QAAA,EAAA,KAAA,EAAM,CAAA;AAAA,wBACtD,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,wCAAA,EAA0C,QAAA,EAAA,OAAA,EAAQ,CAAA;AAAA,QAChE,yBAAS,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,4BAAA,EAA8B,kBAAO,CAAA,GAAS;AAAA;AAAA;AAAA,GACxE;AAEF","file":"error-state.js","sourcesContent":["import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge class names with Tailwind CSS conflict resolution.\n * @param inputs - Class values (strings, arrays, objects) to merge\n * @returns A single merged class string with Tailwind conflicts resolved\n */\nexport function cn(...inputs: ClassValue[]) {\n\treturn twMerge(clsx(inputs));\n}\n","import { type VariantProps, cva } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { cn } from \"../../lib/utils.js\";\n\nconst errorStateVariants = cva(\n\t[\n\t\t\"flex flex-col items-center justify-center text-center\",\n\t\t\"rounded-md border px-[var(--space-6,1.5rem)] py-[var(--space-8,2rem)] gap-[var(--space-3,0.75rem)]\",\n\t].join(\" \"),\n\t{\n\t\tvariants: {\n\t\t\tvariant: {\n\t\t\t\tdefault: \"border-border bg-muted/30\",\n\t\t\t\tdestructive: \"border-destructive/30 bg-destructive/5\",\n\t\t\t},\n\t\t},\n\t\tdefaultVariants: { variant: \"default\" },\n\t},\n);\n\nconst errorIconWrapperVariants = cva(\n\t\"flex h-12 w-12 shrink-0 items-center justify-center rounded-full [&_svg]:size-6\",\n\t{\n\t\tvariants: {\n\t\t\tvariant: {\n\t\t\t\tdefault: \"bg-muted text-muted-foreground\",\n\t\t\t\tdestructive: \"bg-destructive/10 text-destructive\",\n\t\t\t},\n\t\t},\n\t\tdefaultVariants: { variant: \"default\" },\n\t},\n);\n\nexport interface ErrorStateProps\n\textends Omit<React.HTMLAttributes<HTMLDivElement>, \"title\">,\n\t\tVariantProps<typeof errorStateVariants> {\n\t/** Forwarded ref onto the alert region. */\n\tref?: React.Ref<HTMLDivElement>;\n\t/** Optional icon (typically an alert / x-circle SVG). */\n\ticon?: React.ReactNode;\n\t/** Optional heading copy. Falls back to a generic \"Something went wrong\" if omitted. */\n\ttitle?: React.ReactNode;\n\t/** Required body copy explaining what failed. */\n\tmessage: React.ReactNode;\n\t/**\n\t * Optional call-to-action — typically a `<Button>` with `onClick={refetch}`.\n\t * Slot pattern (matching `Empty.action`) so consumers control the button's\n\t * variant / loading state / asChild composition without ErrorState\n\t * re-implementing those concerns.\n\t */\n\taction?: React.ReactNode;\n}\n\n/**\n * A surface for rendering a failed-fetch / failed-action state. Visually\n * similar to {@link Empty} but ships with a destructive-tone bias and\n * mounts with `role=\"alert\"` so screen readers announce the failure on\n * first render.\n *\n * Distinct from {@link Empty} (request returned, no items) and\n * {@link Loading} (request still in flight). For inline form-field\n * errors, use Form's `<FormMessage>` instead. For blocking destructive\n * confirmations, use AlertDialog.\n *\n * @example\n * ```tsx\n * <ErrorState\n * icon={<AlertCircleIcon />}\n * title=\"Couldn't load messages\"\n * message=\"The server didn't respond. Check your connection and try again.\"\n * action={<Button onClick={refetch}>Retry</Button>}\n * />\n * ```\n *\n * @returns A `role=\"alert\"` region with an optional action slot.\n */\nfunction ErrorState({\n\tclassName,\n\tvariant,\n\ticon,\n\ttitle = \"Something went wrong\",\n\tmessage,\n\taction,\n\tref,\n\t...props\n}: ErrorStateProps) {\n\treturn (\n\t\t<div\n\t\t\tref={ref}\n\t\t\trole=\"alert\"\n\t\t\tclassName={cn(errorStateVariants({ variant }), className)}\n\t\t\t{...props}\n\t\t>\n\t\t\t{icon ? (\n\t\t\t\t<div className={errorIconWrapperVariants({ variant })} aria-hidden=\"true\">\n\t\t\t\t\t{icon}\n\t\t\t\t</div>\n\t\t\t) : null}\n\t\t\t<div className=\"font-semibold text-foreground\">{title}</div>\n\t\t\t<div className=\"max-w-md text-sm text-muted-foreground\">{message}</div>\n\t\t\t{action ? <div className=\"mt-[var(--space-2,0.5rem)]\">{action}</div> : null}\n\t\t</div>\n\t);\n}\n\nexport { ErrorState, errorStateVariants };\n"]}