mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-15 11:51:34 +00:00
Compare commits
1 Commits
shadcn@4.8
...
shadcn/nex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
505298ca0f |
5
.changeset/angry-stars-pick.md
Normal file
5
.changeset/angry-stars-pick.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"shadcn": patch
|
||||
---
|
||||
|
||||
fix failing version derivation test
|
||||
@@ -12,25 +12,20 @@ import { HugeiconsIcon } from "@hugeicons/react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useConfig } from "@/hooks/use-config"
|
||||
import { copyToClipboardWithMeta } from "@/components/copy-button"
|
||||
import {
|
||||
BASES,
|
||||
buildThemeForPreset,
|
||||
DEFAULT_CONFIG,
|
||||
type BaseName,
|
||||
type DesignSystemConfig,
|
||||
} from "@/registry/config"
|
||||
import { BASES, type BaseName } from "@/registry/config"
|
||||
import { Button } from "@/styles/base-nova/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/styles/base-nova/ui/dialog"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
@@ -46,10 +41,6 @@ import {
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/styles/base-nova/ui/tabs"
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/styles/base-nova/ui/toggle-group"
|
||||
import { usePresetCode } from "@/app/(app)/create/hooks/use-design-system"
|
||||
import {
|
||||
useDesignSystemSearchParams,
|
||||
@@ -70,54 +61,6 @@ const SHADCN_VERSION = process.env.NEXT_PUBLIC_RC ? "@rc" : "@latest"
|
||||
const PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "bun"] as const
|
||||
type PackageManager = (typeof PACKAGE_MANAGERS)[number]
|
||||
|
||||
const APPLY_MODES = [
|
||||
{
|
||||
value: "full",
|
||||
title: "Full preset",
|
||||
description:
|
||||
"Everything from the preset, including components, theme, and fonts.",
|
||||
flag: null,
|
||||
label: "full preset",
|
||||
},
|
||||
{
|
||||
value: "theme",
|
||||
title: "Theme only",
|
||||
description:
|
||||
"Theme tokens only, like colors, radii, and shadows. Components stay as they are.",
|
||||
flag: "--only theme",
|
||||
label: "--only theme",
|
||||
},
|
||||
{
|
||||
value: "font",
|
||||
title: "Fonts only",
|
||||
description:
|
||||
"Only preset fonts for body and headings. Components stay as they are.",
|
||||
flag: "--only font",
|
||||
label: "--only font",
|
||||
},
|
||||
] as const
|
||||
type ApplyMode = (typeof APPLY_MODES)[number]["value"]
|
||||
type ProjectFormTab = "new-project" | "existing-project" | "theme"
|
||||
type CopyTarget = "command" | "apply" | "theme"
|
||||
type ThemeCssVars = NonNullable<
|
||||
ReturnType<typeof buildThemeForPreset>["cssVars"]
|
||||
>
|
||||
|
||||
function formatCssVarsRule(selector: string, cssVars?: Record<string, string>) {
|
||||
const declarations = Object.entries(cssVars ?? {})
|
||||
.map(([key, value]) => ` --${key}: ${value};`)
|
||||
.join("\n")
|
||||
|
||||
return `${selector} {\n${declarations}\n}`
|
||||
}
|
||||
|
||||
function formatThemeCss(cssVars: ThemeCssVars) {
|
||||
return [
|
||||
formatCssVarsRule(":root", cssVars.light),
|
||||
formatCssVarsRule(".dark", cssVars.dark),
|
||||
].join("\n\n")
|
||||
}
|
||||
|
||||
export function ProjectForm({
|
||||
className,
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
@@ -125,12 +68,7 @@ export function ProjectForm({
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
const presetCode = usePresetCode()
|
||||
const [config, setConfig] = useConfig()
|
||||
const [copiedTarget, setCopiedTarget] = React.useState<CopyTarget | null>(
|
||||
null
|
||||
)
|
||||
const [applyMode, setApplyMode] = React.useState<ApplyMode>("full")
|
||||
const [activeTab, setActiveTab] =
|
||||
React.useState<ProjectFormTab>("new-project")
|
||||
const [hasCopied, setHasCopied] = React.useState(false)
|
||||
|
||||
const packageManager = (config.packageManager || "pnpm") as PackageManager
|
||||
const framework = React.useMemo(
|
||||
@@ -179,85 +117,12 @@ export function ProjectForm({
|
||||
|
||||
const command = commands[packageManager]
|
||||
|
||||
const applyCommands = React.useMemo(() => {
|
||||
const presetFlag = ` --preset ${presetCode}`
|
||||
const onlyFlag =
|
||||
applyMode === "theme"
|
||||
? " --only theme"
|
||||
: applyMode === "font"
|
||||
? " --only font"
|
||||
: ""
|
||||
const flags = `${presetFlag}${onlyFlag}`
|
||||
|
||||
return IS_LOCAL_DEV
|
||||
? {
|
||||
pnpm: `shadcn apply${flags}`,
|
||||
npm: `shadcn apply${flags}`,
|
||||
yarn: `shadcn apply${flags}`,
|
||||
bun: `shadcn apply${flags}`,
|
||||
}
|
||||
: {
|
||||
pnpm: `pnpm dlx shadcn${SHADCN_VERSION} apply${flags}`,
|
||||
npm: `npx shadcn${SHADCN_VERSION} apply${flags}`,
|
||||
yarn: `yarn dlx shadcn${SHADCN_VERSION} apply${flags}`,
|
||||
bun: `bunx --bun shadcn${SHADCN_VERSION} apply${flags}`,
|
||||
}
|
||||
}, [applyMode, presetCode])
|
||||
|
||||
const applyCommand = applyCommands[packageManager]
|
||||
const themeConfig = React.useMemo<DesignSystemConfig>(() => {
|
||||
const isRadiusLocked = params.style === "lyra" || params.style === "sera"
|
||||
|
||||
return {
|
||||
...DEFAULT_CONFIG,
|
||||
base: params.base,
|
||||
style: params.style,
|
||||
baseColor: params.baseColor,
|
||||
theme: params.theme,
|
||||
chartColor: params.chartColor,
|
||||
iconLibrary: params.iconLibrary,
|
||||
font: params.font,
|
||||
fontHeading: params.fontHeading,
|
||||
menuAccent: params.menuAccent,
|
||||
menuColor: params.menuColor,
|
||||
radius: isRadiusLocked ? "none" : params.radius,
|
||||
template: params.template,
|
||||
rtl: params.rtl,
|
||||
pointer: params.pointer,
|
||||
}
|
||||
}, [
|
||||
params.base,
|
||||
params.baseColor,
|
||||
params.chartColor,
|
||||
params.font,
|
||||
params.fontHeading,
|
||||
params.iconLibrary,
|
||||
params.menuAccent,
|
||||
params.menuColor,
|
||||
params.pointer,
|
||||
params.radius,
|
||||
params.rtl,
|
||||
params.style,
|
||||
params.template,
|
||||
params.theme,
|
||||
])
|
||||
|
||||
const themeCss = React.useMemo(() => {
|
||||
const theme = buildThemeForPreset(themeConfig)
|
||||
|
||||
if (!theme.cssVars) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return formatThemeCss(theme.cssVars)
|
||||
}, [themeConfig])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (copiedTarget) {
|
||||
const timer = setTimeout(() => setCopiedTarget(null), 2000)
|
||||
if (hasCopied) {
|
||||
const timer = setTimeout(() => setHasCopied(false), 2000)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [copiedTarget])
|
||||
}, [hasCopied])
|
||||
|
||||
const handleCopy = React.useCallback(() => {
|
||||
const properties: Record<string, string> = {
|
||||
@@ -270,316 +135,159 @@ export function ProjectForm({
|
||||
name: "copy_npm_command",
|
||||
properties,
|
||||
})
|
||||
setCopiedTarget("command")
|
||||
setHasCopied(true)
|
||||
}, [command, params.template])
|
||||
|
||||
const handleCopyApply = React.useCallback(() => {
|
||||
copyToClipboardWithMeta(applyCommand, {
|
||||
name: "copy_apply_command",
|
||||
properties: {
|
||||
command: applyCommand,
|
||||
applyMode,
|
||||
},
|
||||
})
|
||||
setCopiedTarget("apply")
|
||||
}, [applyCommand, applyMode])
|
||||
|
||||
const handleCopyTheme = React.useCallback(() => {
|
||||
copyToClipboardWithMeta(themeCss, {
|
||||
name: "copy_theme_code",
|
||||
properties: {
|
||||
preset: presetCode,
|
||||
baseColor: params.baseColor,
|
||||
theme: params.theme,
|
||||
format: "css",
|
||||
},
|
||||
})
|
||||
setCopiedTarget("theme")
|
||||
}, [params.baseColor, params.theme, presetCode, themeCss])
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger render={<Button className={cn(className)} />}>
|
||||
Get Code
|
||||
Create Project
|
||||
</DialogTrigger>
|
||||
<DialogContent className="dark top-[64px] no-scrollbar flex max-h-[calc(100svh-2rem)] translate-y-0 flex-col rounded-2xl p-0 shadow-xl **:data-[slot=dialog-close]:top-4.5 **:data-[slot=dialog-close]:right-4 **:data-[slot=field-separator]:h-2 sm:max-w-md">
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-0 overflow-hidden rounded-2xl">
|
||||
<DialogHeader className="border-b px-6 py-5">
|
||||
<ToggleGroup
|
||||
value={[activeTab]}
|
||||
onValueChange={(values) =>
|
||||
setActiveTab((values[0] as typeof activeTab) ?? "new-project")
|
||||
}
|
||||
aria-label="Project type"
|
||||
spacing={2}
|
||||
className="**:data-[slot=toggle-group-item]:data-pressed:bg-neutral-700/70"
|
||||
>
|
||||
<ToggleGroupItem value="new-project">New Project</ToggleGroupItem>
|
||||
<ToggleGroupItem value="existing-project">
|
||||
Existing Project
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="theme">Theme</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</DialogHeader>
|
||||
{activeTab === "new-project" && (
|
||||
<div className="no-scrollbar overflow-y-auto">
|
||||
<FieldGroup className="px-6 py-4">
|
||||
<Field className="gap-3">
|
||||
<FieldLabel>Template</FieldLabel>
|
||||
<TemplateGrid
|
||||
template={params.template}
|
||||
setParams={setParams}
|
||||
<DialogContent className="dark no-scrollbar max-h-[calc(100svh-2rem)] overflow-y-auto rounded-2xl p-6 shadow-xl **:data-[slot=field-separator]:h-2 sm:max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Project</DialogTitle>
|
||||
<DialogDescription>
|
||||
Pick a template and configure your project.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div>
|
||||
<FieldGroup>
|
||||
<FieldSeparator className="-mx-6" />
|
||||
<Field className="-mt-2 gap-3">
|
||||
<FieldLabel>Template</FieldLabel>
|
||||
<TemplateGrid template={params.template} setParams={setParams} />
|
||||
</Field>
|
||||
<FieldSeparator className="-mx-6" />
|
||||
<Field className="-mt-2">
|
||||
<FieldLabel>Base</FieldLabel>
|
||||
<BaseGrid base={params.base} setParams={setParams} />
|
||||
</Field>
|
||||
<FieldSeparator className="-mx-6" />
|
||||
<FieldSet>
|
||||
<FieldLegend variant="label" className="sr-only">
|
||||
Options
|
||||
</FieldLegend>
|
||||
<Field orientation="horizontal">
|
||||
<FieldLabel htmlFor="pointer">
|
||||
<HugeiconsIcon
|
||||
icon={HandPointingRight04Icon}
|
||||
className="size-4 -rotate-90"
|
||||
/>
|
||||
</Field>
|
||||
<FieldSeparator className="-mx-6" />
|
||||
<Field>
|
||||
<FieldLabel>Base</FieldLabel>
|
||||
<BaseGrid base={params.base} setParams={setParams} />
|
||||
</Field>
|
||||
<FieldSeparator className="-mx-6" />
|
||||
<FieldSet>
|
||||
<FieldLegend variant="label" className="sr-only">
|
||||
Options
|
||||
</FieldLegend>
|
||||
<Field orientation="horizontal">
|
||||
<FieldLabel htmlFor="pointer">
|
||||
<HugeiconsIcon
|
||||
icon={HandPointingRight04Icon}
|
||||
className="size-4 -rotate-90"
|
||||
/>
|
||||
Use pointer on buttons
|
||||
</FieldLabel>
|
||||
<Switch
|
||||
id="pointer"
|
||||
checked={params.pointer}
|
||||
onCheckedChange={(checked) =>
|
||||
setParams({ pointer: checked === true })
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<FieldSeparator className="-mx-6" />
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
data-disabled={hasMonorepo ? undefined : "true"}
|
||||
>
|
||||
<FieldLabel htmlFor="monorepo">
|
||||
<span
|
||||
className="size-4 text-neutral-100 [&_svg]:size-4 [&_svg]:fill-current"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: TURBOREPO_LOGO,
|
||||
}}
|
||||
/>
|
||||
Create a monorepo
|
||||
</FieldLabel>
|
||||
<Switch
|
||||
id="monorepo"
|
||||
checked={params.template?.endsWith("-monorepo") ?? false}
|
||||
disabled={!hasMonorepo}
|
||||
onCheckedChange={(checked) => {
|
||||
const framework = getFramework(
|
||||
params.template ?? "next"
|
||||
)
|
||||
setParams({
|
||||
template: getTemplateValue(
|
||||
framework,
|
||||
checked === true
|
||||
) as typeof params.template,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<FieldSeparator className="-mx-6" />
|
||||
<Field orientation="horizontal">
|
||||
<FieldLabel htmlFor="rtl">
|
||||
<HugeiconsIcon icon={Globe02Icon} className="size-4" />
|
||||
Enable RTL support
|
||||
</FieldLabel>
|
||||
<Switch
|
||||
id="rtl"
|
||||
checked={params.rtl}
|
||||
onCheckedChange={(checked) =>
|
||||
setParams({ rtl: checked === true })
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
<DialogFooter className="m-0 min-w-0 p-6">
|
||||
<div className="flex w-full min-w-0 flex-col gap-3">
|
||||
<Tabs
|
||||
value={packageManager}
|
||||
onValueChange={(value) => {
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
packageManager: value as PackageManager,
|
||||
}))
|
||||
Use pointer on buttons
|
||||
</FieldLabel>
|
||||
<Switch
|
||||
id="pointer"
|
||||
checked={params.pointer}
|
||||
onCheckedChange={(checked) =>
|
||||
setParams({ pointer: checked === true })
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<FieldSeparator className="-mx-6" />
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
data-disabled={hasMonorepo ? undefined : "true"}
|
||||
>
|
||||
<FieldLabel htmlFor="monorepo">
|
||||
<span
|
||||
className="size-4 text-neutral-100 [&_svg]:size-4 [&_svg]:fill-current"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: TURBOREPO_LOGO,
|
||||
}}
|
||||
className="min-w-0 gap-0 overflow-hidden rounded-xl border-0 ring-1 ring-border"
|
||||
>
|
||||
<div className="flex items-center gap-2 py-1 pr-1.5 pl-1">
|
||||
<TabsList className="bg-transparent font-mono">
|
||||
{PACKAGE_MANAGERS.map((manager) => {
|
||||
return (
|
||||
<TabsTrigger
|
||||
key={manager}
|
||||
value={manager}
|
||||
className="py-0 leading-none data-[state=active]:shadow-none"
|
||||
>
|
||||
{manager}
|
||||
</TabsTrigger>
|
||||
)
|
||||
})}
|
||||
</TabsList>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
className="ml-auto"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{copiedTarget === "command" ? (
|
||||
<HugeiconsIcon icon={Tick02Icon} />
|
||||
) : (
|
||||
<HugeiconsIcon icon={Copy01Icon} />
|
||||
)}
|
||||
<span className="sr-only">Copy command</span>
|
||||
</Button>
|
||||
</div>
|
||||
{Object.entries(commands).map(([key, cmd]) => {
|
||||
return (
|
||||
<TabsContent key={key} value={key}>
|
||||
<div className="relative overflow-hidden border-t bg-popover p-3">
|
||||
<div className="no-scrollbar overflow-x-auto">
|
||||
<code className="font-mono text-sm whitespace-nowrap">
|
||||
{cmd}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)
|
||||
})}
|
||||
</Tabs>
|
||||
<Button onClick={handleCopy} className="h-9 w-full">
|
||||
{copiedTarget === "command" ? "Copied" : "Copy Command"}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === "existing-project" && (
|
||||
<div className="no-scrollbar overflow-y-auto">
|
||||
<FieldGroup className="px-6 py-4">
|
||||
<FieldSet className="gap-3">
|
||||
<FieldLegend variant="label">Apply Preset</FieldLegend>
|
||||
<FieldDescription>
|
||||
Pick which parts of the preset to apply.
|
||||
</FieldDescription>
|
||||
<ApplyModeGrid mode={applyMode} setMode={setApplyMode} />
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
<DialogFooter className="m-0 min-w-0 p-6">
|
||||
<div className="flex w-full min-w-0 flex-col gap-3">
|
||||
<Tabs
|
||||
value={packageManager}
|
||||
onValueChange={(value) => {
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
packageManager: value as PackageManager,
|
||||
}))
|
||||
}}
|
||||
className="min-w-0 gap-0 overflow-hidden rounded-xl border-0 ring-1 ring-border"
|
||||
>
|
||||
<div className="flex items-center gap-2 py-1 pr-1.5 pl-1">
|
||||
<TabsList className="bg-transparent font-mono">
|
||||
{PACKAGE_MANAGERS.map((manager) => {
|
||||
return (
|
||||
<TabsTrigger
|
||||
key={manager}
|
||||
value={manager}
|
||||
className="py-0 leading-none data-[state=active]:shadow-none"
|
||||
>
|
||||
{manager}
|
||||
</TabsTrigger>
|
||||
)
|
||||
})}
|
||||
</TabsList>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
className="ml-auto"
|
||||
onClick={handleCopyApply}
|
||||
>
|
||||
{copiedTarget === "apply" ? (
|
||||
<HugeiconsIcon icon={Tick02Icon} />
|
||||
) : (
|
||||
<HugeiconsIcon icon={Copy01Icon} />
|
||||
)}
|
||||
<span className="sr-only">Copy command</span>
|
||||
</Button>
|
||||
</div>
|
||||
{Object.entries(applyCommands).map(([key, cmd]) => {
|
||||
return (
|
||||
<TabsContent key={key} value={key}>
|
||||
<div className="relative overflow-hidden border-t bg-popover p-3">
|
||||
<div className="no-scrollbar overflow-x-auto">
|
||||
<code className="font-mono text-sm whitespace-nowrap">
|
||||
{cmd}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)
|
||||
})}
|
||||
</Tabs>
|
||||
<Button onClick={handleCopyApply} className="h-9 w-full">
|
||||
{copiedTarget === "apply" ? "Copied" : "Copy Command"}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === "theme" && (
|
||||
<div className="no-scrollbar overflow-y-auto">
|
||||
<FieldGroup className="min-w-0 px-6 py-4">
|
||||
<FieldSet className="min-w-0 gap-3">
|
||||
<FieldLegend variant="label">Theme Tokens</FieldLegend>
|
||||
<FieldDescription>
|
||||
Copy the CSS variables for this preset.
|
||||
</FieldDescription>
|
||||
<div className="w-full min-w-0 overflow-hidden rounded-xl border-0 ring-1 ring-border">
|
||||
<div className="flex items-center gap-2 py-1 pr-1.5 pl-3">
|
||||
<div className="min-w-0 truncate font-mono text-sm text-muted-foreground">
|
||||
globals.css
|
||||
</div>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
className="ml-auto"
|
||||
onClick={handleCopyTheme}
|
||||
>
|
||||
{copiedTarget === "theme" ? (
|
||||
<HugeiconsIcon icon={Tick02Icon} />
|
||||
) : (
|
||||
<HugeiconsIcon icon={Copy01Icon} />
|
||||
)}
|
||||
<span className="sr-only">Copy theme</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative no-scrollbar max-h-[45svh] overflow-auto border-t bg-popover p-3">
|
||||
<pre className="min-w-max font-mono leading-normal whitespace-pre">
|
||||
<code>{themeCss}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
<DialogFooter className="m-0 min-w-0 p-6">
|
||||
<Button onClick={handleCopyTheme} className="h-9 w-full">
|
||||
{copiedTarget === "theme" ? "Copied" : "Copy Theme"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
Create a monorepo
|
||||
</FieldLabel>
|
||||
<Switch
|
||||
id="monorepo"
|
||||
checked={params.template?.endsWith("-monorepo") ?? false}
|
||||
disabled={!hasMonorepo}
|
||||
onCheckedChange={(checked) => {
|
||||
const framework = getFramework(params.template ?? "next")
|
||||
setParams({
|
||||
template: getTemplateValue(
|
||||
framework,
|
||||
checked === true
|
||||
) as typeof params.template,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<FieldSeparator className="-mx-6" />
|
||||
<Field orientation="horizontal">
|
||||
<FieldLabel htmlFor="rtl">
|
||||
<HugeiconsIcon icon={Globe02Icon} className="size-4" />
|
||||
Enable RTL support
|
||||
</FieldLabel>
|
||||
<Switch
|
||||
id="rtl"
|
||||
checked={params.rtl}
|
||||
onCheckedChange={(checked) =>
|
||||
setParams({ rtl: checked === true })
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</div>
|
||||
<DialogFooter className="-mx-6 -mb-6 min-w-0">
|
||||
<div className="flex w-full min-w-0 flex-col gap-3">
|
||||
<Tabs
|
||||
value={packageManager}
|
||||
onValueChange={(value) => {
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
packageManager: value as PackageManager,
|
||||
}))
|
||||
}}
|
||||
className="min-w-0 gap-0 overflow-hidden rounded-xl border-0 ring-1 ring-border"
|
||||
>
|
||||
<div className="flex items-center gap-2 py-1 pr-1.5 pl-1">
|
||||
<TabsList className="bg-transparent font-mono">
|
||||
{PACKAGE_MANAGERS.map((manager) => {
|
||||
return (
|
||||
<TabsTrigger
|
||||
key={manager}
|
||||
value={manager}
|
||||
className="py-0 leading-none data-[state=active]:shadow-none"
|
||||
>
|
||||
{manager}
|
||||
</TabsTrigger>
|
||||
)
|
||||
})}
|
||||
</TabsList>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
className="ml-auto"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{hasCopied ? (
|
||||
<HugeiconsIcon icon={Tick02Icon} />
|
||||
) : (
|
||||
<HugeiconsIcon icon={Copy01Icon} />
|
||||
)}
|
||||
<span className="sr-only">Copy command</span>
|
||||
</Button>
|
||||
</div>
|
||||
{Object.entries(commands).map(([key, cmd]) => {
|
||||
return (
|
||||
<TabsContent key={key} value={key}>
|
||||
<div className="relative overflow-hidden border-t bg-popover p-3">
|
||||
<div className="no-scrollbar overflow-x-auto">
|
||||
<code className="font-mono text-sm whitespace-nowrap">
|
||||
{cmd}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)
|
||||
})}
|
||||
</Tabs>
|
||||
<Button onClick={handleCopy} className="h-9 w-full">
|
||||
{hasCopied ? "Copied" : "Copy Command"}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
@@ -644,34 +352,6 @@ const TemplateGrid = React.memo(function TemplateGrid({
|
||||
)
|
||||
})
|
||||
|
||||
const ApplyModeGrid = React.memo(function ApplyModeGrid({
|
||||
mode,
|
||||
setMode,
|
||||
}: {
|
||||
mode: ApplyMode
|
||||
setMode: (mode: ApplyMode) => void
|
||||
}) {
|
||||
return (
|
||||
<RadioGroup
|
||||
value={mode}
|
||||
onValueChange={(value) => setMode(value as ApplyMode)}
|
||||
aria-label="Apply"
|
||||
>
|
||||
{APPLY_MODES.map((option) => (
|
||||
<FieldLabel key={option.value} htmlFor={`apply-${option.value}`}>
|
||||
<Field orientation="horizontal">
|
||||
<RadioGroupItem value={option.value} id={`apply-${option.value}`} />
|
||||
<FieldContent>
|
||||
<FieldTitle>{option.title}</FieldTitle>
|
||||
<FieldDescription>{option.description}</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</RadioGroup>
|
||||
)
|
||||
})
|
||||
|
||||
const BaseGrid = React.memo(function BaseGrid({
|
||||
base,
|
||||
setParams,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { type Style, type StyleName } from "@/registry/config"
|
||||
import { PRESETS, type Style, type StyleName } from "@/registry/config"
|
||||
import { LockButton } from "@/app/(app)/create/components/lock-button"
|
||||
import {
|
||||
Picker,
|
||||
@@ -53,7 +53,24 @@ export function StylePicker({
|
||||
<PickerRadioGroup
|
||||
value={currentStyle?.name}
|
||||
onValueChange={(value) => {
|
||||
setParams({ style: value as StyleName })
|
||||
const styleName = value as StyleName
|
||||
const preset = PRESETS.find(
|
||||
(p) => p.base === params.base && p.style === styleName
|
||||
)
|
||||
setParams({
|
||||
style: styleName,
|
||||
...(preset && {
|
||||
baseColor: preset.baseColor,
|
||||
theme: preset.theme,
|
||||
chartColor: preset.chartColor,
|
||||
iconLibrary: preset.iconLibrary,
|
||||
font: preset.font,
|
||||
fontHeading: preset.fontHeading,
|
||||
menuAccent: preset.menuAccent,
|
||||
menuColor: preset.menuColor,
|
||||
radius: preset.radius,
|
||||
}),
|
||||
})
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"[CLI](/docs/cli)",
|
||||
"monorepo",
|
||||
"skills",
|
||||
"v0",
|
||||
"javascript",
|
||||
"blocks",
|
||||
"figma",
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
---
|
||||
title: May 2026 - Registry Include and Validate
|
||||
description: Organize and validate source registries.
|
||||
date: 2026-05-20
|
||||
---
|
||||
|
||||
This release adds two updates for registry authors:
|
||||
|
||||
- `include` for composing large source registries from multiple `registry.json`
|
||||
files.
|
||||
- `shadcn registry validate` for checking source registries before publishing.
|
||||
|
||||
This makes it easier to maintain source and dynamic registries without keeping
|
||||
one large `registry.json` file by hand.
|
||||
|
||||
Registry authors can now organize a large source registry across multiple
|
||||
`registry.json` files and compose them with `shadcn build`.
|
||||
|
||||
```txt /registry.json/
|
||||
registry.json
|
||||
components
|
||||
└── ui
|
||||
├── button.tsx
|
||||
├── input.tsx
|
||||
└── registry.json
|
||||
hooks
|
||||
├── registry.json
|
||||
├── use-media-query.ts
|
||||
└── use-toggle.ts
|
||||
```
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```json title="registry.json" showLineNumbers {6-7}
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry.json",
|
||||
"name": "acme",
|
||||
"homepage": "https://acme.com",
|
||||
"include": [
|
||||
"components/ui/registry.json",
|
||||
"hooks/registry.json"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Included `registry.json` files are valid registry files for composition and may
|
||||
omit `name` and `homepage`. Only the root `registry.json` must define the
|
||||
registry metadata.
|
||||
|
||||
```json title="components/ui/registry.json" showLineNumbers
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry.json",
|
||||
"items": [
|
||||
{
|
||||
"name": "button",
|
||||
"type": "registry:ui",
|
||||
"files": [
|
||||
{
|
||||
"path": "button.tsx",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Build output
|
||||
|
||||
`shadcn build` resolves included registries and writes a flattened
|
||||
`registry.json` without `include`. Item file paths are preserved from the root
|
||||
registry, so a file declared in `components/ui/registry.json` is written as
|
||||
`components/ui/button.tsx` in the built registry item.
|
||||
|
||||
## Validate your registry
|
||||
|
||||
You can now validate a source registry before publishing or serving it.
|
||||
|
||||
```bash
|
||||
npx shadcn registry validate
|
||||
```
|
||||
|
||||
Validation runs against the source registry files directly. You do not need to
|
||||
run `shadcn build` first.
|
||||
|
||||
The command checks the root `registry.json`, included registry files, item
|
||||
schema errors, duplicate item names, include rules, and local item file paths.
|
||||
Validation reports all actionable errors it can find in one run.
|
||||
|
||||
## Registry loaders
|
||||
|
||||
The `shadcn/registry` package also exports `loadRegistry` and
|
||||
`loadRegistryItem` for dynamic registry routes.
|
||||
|
||||
```ts title="app/r/registry.json/route.ts" showLineNumbers
|
||||
import { loadRegistry } from "shadcn/registry"
|
||||
|
||||
export async function GET() {
|
||||
const registry = await loadRegistry()
|
||||
|
||||
return Response.json(registry)
|
||||
}
|
||||
```
|
||||
|
||||
```ts title="app/r/[name].json/route.ts" showLineNumbers
|
||||
import { loadRegistryItem } from "shadcn/registry"
|
||||
|
||||
export async function GET(
|
||||
_: Request,
|
||||
{ params }: { params: Promise<{ name: string }> }
|
||||
) {
|
||||
const { name } = await params
|
||||
const item = await loadRegistryItem(name)
|
||||
|
||||
return Response.json(item)
|
||||
}
|
||||
```
|
||||
|
||||
See the [registry.json documentation](/docs/registry/registry-json#include) and
|
||||
[getting started guide](/docs/registry/getting-started#structure-your-registry)
|
||||
for more details.
|
||||
@@ -156,7 +156,7 @@ The `Field` family is designed for composing accessible forms. A typical field i
|
||||
|
||||
## Form
|
||||
|
||||
See the [Form](/docs/forms) documentation for building forms with the `Field` component and [React Hook Form](/docs/forms/react-hook-form), [Tanstack Form](/docs/forms/tanstack-form), or [Formisch](/docs/forms/formisch).
|
||||
See the [Form](/docs/forms) documentation for building forms with the `Field` component and [React Hook Form](/docs/forms/react-hook-form) or [Tanstack Form](/docs/forms/tanstack-form).
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -129,9 +129,3 @@ To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl)
|
||||
## API Reference
|
||||
|
||||
See the [Base UI Toggle Group](https://base-ui.com/react/components/toggle-group#api-reference) documentation.
|
||||
|
||||
## Changelog
|
||||
|
||||
### 2026-05-17 Default Spacing
|
||||
|
||||
Changed the default `spacing` from `0` to `2` so toggle groups render with space between items by default. Use `spacing={0}` for connected items.
|
||||
|
||||
@@ -156,7 +156,7 @@ The `Field` family is designed for composing accessible forms. A typical field i
|
||||
|
||||
## Form
|
||||
|
||||
See the [Form](/docs/forms) documentation for building forms with the `Field` component and [React Hook Form](/docs/forms/react-hook-form), [Tanstack Form](/docs/forms/tanstack-form), or [Formisch](/docs/forms/formisch).
|
||||
See the [Form](/docs/forms) documentation for building forms with the `Field` component and [React Hook Form](/docs/forms/react-hook-form) or [Tanstack Form](/docs/forms/tanstack-form).
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -129,9 +129,3 @@ To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl)
|
||||
## API Reference
|
||||
|
||||
See the [Radix Toggle Group](https://www.radix-ui.com/docs/primitives/components/toggle-group#api-reference) documentation.
|
||||
|
||||
## Changelog
|
||||
|
||||
### 2026-05-17 Default Spacing
|
||||
|
||||
Changed the default `spacing` from `0` to `2` so toggle groups render with space between items by default. Use `spacing={0}` for connected items.
|
||||
|
||||
@@ -1,669 +0,0 @@
|
||||
---
|
||||
title: Formisch
|
||||
description: Build forms in React using Formisch and Valibot.
|
||||
links:
|
||||
doc: https://formisch.dev
|
||||
---
|
||||
|
||||
import { InfoIcon } from "lucide-react"
|
||||
|
||||
This guide covers building forms with [Formisch](https://formisch.dev), the lightweight, schema-first, and fully type-safe form library for React. We'll create forms with the `<Field />` component, validate them with Valibot schemas, handle errors, and ensure accessibility.
|
||||
|
||||
## Demo
|
||||
|
||||
We'll build the following form. It has a simple text input and a textarea. On submit, we'll validate the form data and display any errors.
|
||||
|
||||
<Callout icon={<InfoIcon />}>
|
||||
**Note:** For the purpose of this demo, we have intentionally disabled browser
|
||||
validation to show how schema validation and form errors work in Formisch. It
|
||||
is recommended to add basic browser validation in your production code.
|
||||
</Callout>
|
||||
|
||||
<ComponentPreview
|
||||
name="form-formisch-demo"
|
||||
className="sm:[&_.preview]:h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
## Approach
|
||||
|
||||
This form leverages Formisch for headless, schema-first form handling. We'll build our form using the `<Field />` component, which gives you **complete flexibility over the markup and styling**.
|
||||
|
||||
- Uses Formisch's `useForm` hook for form state management.
|
||||
- `<Form />` component to wrap the native `<form>` element with submit handling.
|
||||
- `<Field />` render-prop component for controlled inputs.
|
||||
- Schema validation using [Valibot](https://valibot.dev).
|
||||
- Type-safe field paths inferred from the schema.
|
||||
|
||||
## Form Methods
|
||||
|
||||
Formisch exposes form operations as **top-level functions** rather than methods on a form object. Import only what you need:
|
||||
|
||||
```ts
|
||||
import { getInput, insert, reset, submit } from "@formisch/react"
|
||||
```
|
||||
|
||||
Every method follows the same signature: the **first parameter is always the form store**, and the **second parameter (if necessary) is always a config object**.
|
||||
|
||||
```ts
|
||||
// Read a field value
|
||||
const email = getInput(form, { path: ["email"] })
|
||||
|
||||
// Reset the form with new initial values
|
||||
reset(form, { initialInput: { email: "", password: "" } })
|
||||
|
||||
// Move an item in a field array
|
||||
move(form, { path: ["items"], from: 0, to: 3 })
|
||||
```
|
||||
|
||||
This design keeps the API flexible and consistent across all methods. You'll see the same `(form, config)` shape used throughout this guide for reading state (`getInput`, `getErrors`), writing state (`setInput`, `setErrors`), form control (`submit`, `validate`, `focus`), and array operations (`insert`, `remove`, `move`, `swap`, `replace`). See the [full methods reference](https://formisch.dev/react/guides/form-methods) for details.
|
||||
|
||||
## Anatomy
|
||||
|
||||
Here's a basic example of a form using the `<Field />` component from Formisch and the shadcn `<Field />` component.
|
||||
|
||||
```tsx showLineNumbers {3-21}
|
||||
<Form of={form} onSubmit={handleSubmit}>
|
||||
<FieldGroup>
|
||||
<FormischField of={form} path={["title"]}>
|
||||
{(field) => (
|
||||
<Field data-invalid={field.errors !== null}>
|
||||
<FieldLabel htmlFor="form-title">Bug Title</FieldLabel>
|
||||
<Input
|
||||
{...field.props}
|
||||
id="form-title"
|
||||
value={field.input}
|
||||
aria-invalid={field.errors !== null}
|
||||
placeholder="Login button not working on mobile"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<FieldDescription>
|
||||
Provide a concise title for your bug report.
|
||||
</FieldDescription>
|
||||
{field.errors && (
|
||||
<FieldError errors={field.errors.map((message) => ({ message }))} />
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
</FormischField>
|
||||
</FieldGroup>
|
||||
</Form>
|
||||
```
|
||||
|
||||
<Callout icon={<InfoIcon />}>
|
||||
**Note:** Formisch ships its own `Field` component. To avoid a name clash with
|
||||
the shadcn `Field`, the examples below import the Formisch one as
|
||||
`FormischField` and keep the shadcn `Field` under its original name. In your
|
||||
own code you can alias either side — just be consistent.
|
||||
</Callout>
|
||||
|
||||
## Form
|
||||
|
||||
### Create a form schema
|
||||
|
||||
We'll start by defining the shape of our form using a Valibot schema. Formisch infers all input and output types directly from this schema.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx"
|
||||
import * as v from "valibot"
|
||||
|
||||
const FormSchema = v.object({
|
||||
title: v.pipe(
|
||||
v.string(),
|
||||
v.minLength(5, "Bug title must be at least 5 characters."),
|
||||
v.maxLength(32, "Bug title must be at most 32 characters.")
|
||||
),
|
||||
description: v.pipe(
|
||||
v.string(),
|
||||
v.minLength(20, "Description must be at least 20 characters."),
|
||||
v.maxLength(100, "Description must be at most 100 characters.")
|
||||
),
|
||||
})
|
||||
```
|
||||
|
||||
### Set up the form
|
||||
|
||||
Next, we'll use the `useForm` hook from Formisch to create our form instance. The schema is passed directly to `useForm` — there is no resolver step.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {1-2,21-25}
|
||||
import { Form, Field as FormischField, useForm } from "@formisch/react"
|
||||
import type { SubmitHandler } from "@formisch/react"
|
||||
import * as v from "valibot"
|
||||
|
||||
const FormSchema = v.object({
|
||||
title: v.pipe(
|
||||
v.string(),
|
||||
v.minLength(5, "Bug title must be at least 5 characters."),
|
||||
v.maxLength(32, "Bug title must be at most 32 characters.")
|
||||
),
|
||||
description: v.pipe(
|
||||
v.string(),
|
||||
v.minLength(20, "Description must be at least 20 characters."),
|
||||
v.maxLength(100, "Description must be at most 100 characters.")
|
||||
),
|
||||
})
|
||||
|
||||
export function BugReportForm() {
|
||||
const form = useForm({
|
||||
schema: FormSchema,
|
||||
initialInput: {
|
||||
title: "",
|
||||
description: "",
|
||||
},
|
||||
})
|
||||
|
||||
const handleSubmit: SubmitHandler<typeof FormSchema> = (output) => {
|
||||
// Do something with the validated form values.
|
||||
console.log(output)
|
||||
}
|
||||
|
||||
return (
|
||||
<Form of={form} onSubmit={handleSubmit}>
|
||||
{/* ... */}
|
||||
{/* Build the form here */}
|
||||
{/* ... */}
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The `<Form />` component wraps a native `<form>` element. It calls `event.preventDefault()`, runs validation, and only invokes `onSubmit` when the data is valid. The `output` you receive is fully typed from the schema.
|
||||
|
||||
### Build the form
|
||||
|
||||
We can now build the form using the `<Field />` component from Formisch and the shadcn `<Field />` component.
|
||||
|
||||
<ComponentSource
|
||||
src="/registry/new-york-v4/examples/form-formisch-demo.tsx"
|
||||
title="form.tsx"
|
||||
/>
|
||||
|
||||
### Done
|
||||
|
||||
That's it. You now have a fully accessible form with client-side validation.
|
||||
|
||||
When you submit the form, the `handleSubmit` function will be called with the validated form data. If the form data is invalid, Formisch will populate `field.errors` for each invalid field and the UI will display them.
|
||||
|
||||
## Validation
|
||||
|
||||
### Client-side Validation
|
||||
|
||||
Formisch validates your form data using the Valibot schema you pass to `useForm`. There is no resolver — the schema is the single source of truth for both runtime validation and static types.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {1,3-6,11}
|
||||
import { useForm } from "@formisch/react"
|
||||
|
||||
const FormSchema = v.object({
|
||||
title: v.string(),
|
||||
description: v.optional(v.string()),
|
||||
})
|
||||
|
||||
export function ExampleForm() {
|
||||
const form = useForm({
|
||||
schema: FormSchema,
|
||||
initialInput: {
|
||||
title: "",
|
||||
description: "",
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Validation Modes
|
||||
|
||||
Formisch separates the **first** validation from **subsequent** validations. You configure them with the `validate` and `revalidate` options on `useForm`.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {3-4}
|
||||
const form = useForm({
|
||||
schema: FormSchema,
|
||||
validate: "blur",
|
||||
revalidate: "input",
|
||||
})
|
||||
```
|
||||
|
||||
| Option | Value | Description |
|
||||
| ------------ | ----------- | --------------------------------------------------------------- |
|
||||
| `validate` | `"submit"` | Validate on form submission (default). |
|
||||
| `validate` | `"blur"` | Validate when a field loses focus. |
|
||||
| `validate` | `"input"` | Validate on every input change. |
|
||||
| `validate` | `"initial"` | Validate immediately on form creation. |
|
||||
| `revalidate` | `"input"` | Revalidate on every input change after the first run (default). |
|
||||
| `revalidate` | `"blur"` | Revalidate on blur after the first run. |
|
||||
| `revalidate` | `"submit"` | Revalidate only on form submission. |
|
||||
|
||||
## Displaying Errors
|
||||
|
||||
Display errors next to the field using `<FieldError />`. Formisch returns errors as an array of strings, so map them to the shape `<FieldError />` expects. For styling and accessibility:
|
||||
|
||||
- Add the `data-invalid` prop to the `<Field />` component.
|
||||
- Add the `aria-invalid` prop to the form control such as `<Input />`, `<SelectTrigger />`, `<Checkbox />`, etc.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {3,10,12-14}
|
||||
<FormischField of={form} path={["email"]}>
|
||||
{(field) => (
|
||||
<Field data-invalid={field.errors !== null}>
|
||||
<FieldLabel htmlFor="form-email">Email</FieldLabel>
|
||||
<Input
|
||||
{...field.props}
|
||||
id="form-email"
|
||||
value={field.input}
|
||||
type="email"
|
||||
aria-invalid={field.errors !== null}
|
||||
/>
|
||||
{field.errors && (
|
||||
<FieldError errors={field.errors.map((message) => ({ message }))} />
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
</FormischField>
|
||||
```
|
||||
|
||||
## Working with Different Field Types
|
||||
|
||||
Formisch exposes two ways to bind a field to an element:
|
||||
|
||||
- **Native HTML elements** (like `<Input />` and `<Textarea />`) — spread `field.props` and provide `value={field.input}`. Formisch wires up `name`, `ref`, `onChange`, `onBlur`, and `onFocus` for you.
|
||||
- **Component-library inputs** (like Radix-based `<Select />`, `<Checkbox />`, `<RadioGroup />`, `<Switch />`) — read the value from `field.input` and call `field.onChange(value)` to update it.
|
||||
|
||||
### Input
|
||||
|
||||
- For input fields, spread `field.props` and provide `value={field.input}`.
|
||||
- To show errors, add the `aria-invalid` prop to the `<Input />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-formisch-input"
|
||||
className="sm:[&_.preview]:h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {5-8}
|
||||
<FormischField of={form} path={["username"]}>
|
||||
{(field) => (
|
||||
<Field data-invalid={field.errors !== null}>
|
||||
<FieldLabel htmlFor="form-username">Username</FieldLabel>
|
||||
<Input
|
||||
{...field.props}
|
||||
id="form-username"
|
||||
value={field.input}
|
||||
aria-invalid={field.errors !== null}
|
||||
/>
|
||||
{field.errors && (
|
||||
<FieldError errors={field.errors.map((message) => ({ message }))} />
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
</FormischField>
|
||||
```
|
||||
|
||||
### Textarea
|
||||
|
||||
- For textarea fields, spread `field.props` and provide `value={field.input}`.
|
||||
- To show errors, add the `aria-invalid` prop to the `<Textarea />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-formisch-textarea"
|
||||
className="sm:[&_.preview]:h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {7-10}
|
||||
<FormischField of={form} path={["about"]}>
|
||||
{(field) => (
|
||||
<Field data-invalid={field.errors !== null}>
|
||||
<FieldLabel htmlFor="form-about">More about you</FieldLabel>
|
||||
<Textarea
|
||||
{...field.props}
|
||||
id="form-about"
|
||||
value={field.input}
|
||||
aria-invalid={field.errors !== null}
|
||||
placeholder="I'm a software engineer..."
|
||||
className="min-h-[120px]"
|
||||
/>
|
||||
<FieldDescription>
|
||||
Tell us more about yourself. This will be used to help us personalize
|
||||
your experience.
|
||||
</FieldDescription>
|
||||
{field.errors && (
|
||||
<FieldError errors={field.errors.map((message) => ({ message }))} />
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
</FormischField>
|
||||
```
|
||||
|
||||
### Select
|
||||
|
||||
- For select components, read `field.input` and call `field.onChange` from `<Select />`'s `onValueChange`.
|
||||
- To show errors, add the `aria-invalid` prop to the `<SelectTrigger />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-formisch-select"
|
||||
className="sm:[&_.preview]:h-[500px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {15-19}
|
||||
<FormischField of={form} path={["language"]}>
|
||||
{(field) => (
|
||||
<Field orientation="responsive" data-invalid={field.errors !== null}>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="form-language">Spoken Language</FieldLabel>
|
||||
<FieldDescription>
|
||||
For best results, select the language you speak.
|
||||
</FieldDescription>
|
||||
{field.errors && (
|
||||
<FieldError errors={field.errors.map((message) => ({ message }))} />
|
||||
)}
|
||||
</FieldContent>
|
||||
<Select value={field.input} onValueChange={field.onChange}>
|
||||
<SelectTrigger
|
||||
id="form-language"
|
||||
aria-invalid={field.errors !== null}
|
||||
className="min-w-[120px]"
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="item-aligned">
|
||||
<SelectItem value="auto">Auto</SelectItem>
|
||||
<SelectItem value="en">English</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
)}
|
||||
</FormischField>
|
||||
```
|
||||
|
||||
### Checkbox
|
||||
|
||||
- For checkbox arrays, read `field.input` and update it from `onCheckedChange` using `field.onChange`.
|
||||
- To show errors, add the `aria-invalid` prop to the `<Checkbox />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
- Remember to add `data-slot="checkbox-group"` to the `<FieldGroup />` component for proper styling and spacing.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-formisch-checkbox"
|
||||
className="sm:[&_.preview]:h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {16,19-25}
|
||||
<FormischField of={form} path={["tasks"]}>
|
||||
{(field) => (
|
||||
<FieldSet>
|
||||
<FieldLegend variant="label">Tasks</FieldLegend>
|
||||
<FieldDescription>
|
||||
Get notified when tasks you've created have updates.
|
||||
</FieldDescription>
|
||||
<FieldGroup data-slot="checkbox-group">
|
||||
{tasks.map((task) => (
|
||||
<Field
|
||||
key={task.id}
|
||||
orientation="horizontal"
|
||||
data-invalid={field.errors !== null}
|
||||
>
|
||||
<Checkbox
|
||||
id={`form-checkbox-${task.id}`}
|
||||
aria-invalid={field.errors !== null}
|
||||
checked={field.input?.includes(task.id) ?? false}
|
||||
onCheckedChange={(checked) => {
|
||||
const current = field.input ?? []
|
||||
field.onChange(
|
||||
checked === true
|
||||
? [...current, task.id]
|
||||
: current.filter((value) => value !== task.id)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldLabel
|
||||
htmlFor={`form-checkbox-${task.id}`}
|
||||
className="font-normal"
|
||||
>
|
||||
{task.label}
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
))}
|
||||
</FieldGroup>
|
||||
{field.errors && (
|
||||
<FieldError errors={field.errors.map((message) => ({ message }))} />
|
||||
)}
|
||||
</FieldSet>
|
||||
)}
|
||||
</FormischField>
|
||||
```
|
||||
|
||||
### Radio Group
|
||||
|
||||
- For radio groups, read `field.input` and call `field.onChange` from `onValueChange`.
|
||||
- To show errors, add the `aria-invalid` prop to the `<RadioGroupItem />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-formisch-radiogroup"
|
||||
className="sm:[&_.preview]:h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {9-13,21}
|
||||
<FormischField of={form} path={["plan"]}>
|
||||
{(field) => (
|
||||
<FieldSet>
|
||||
<FieldLegend>Plan</FieldLegend>
|
||||
<FieldDescription>
|
||||
You can upgrade or downgrade your plan at any time.
|
||||
</FieldDescription>
|
||||
<RadioGroup value={field.input} onValueChange={field.onChange}>
|
||||
{plans.map((plan) => (
|
||||
<FieldLabel key={plan.id} htmlFor={`form-radiogroup-${plan.id}`}>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
data-invalid={field.errors !== null}
|
||||
>
|
||||
<FieldContent>
|
||||
<FieldTitle>{plan.title}</FieldTitle>
|
||||
<FieldDescription>{plan.description}</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value={plan.id}
|
||||
id={`form-radiogroup-${plan.id}`}
|
||||
aria-invalid={field.errors !== null}
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</RadioGroup>
|
||||
{field.errors && (
|
||||
<FieldError errors={field.errors.map((message) => ({ message }))} />
|
||||
)}
|
||||
</FieldSet>
|
||||
)}
|
||||
</FormischField>
|
||||
```
|
||||
|
||||
### Switch
|
||||
|
||||
- For switches, read `field.input` and call `field.onChange` from `onCheckedChange`.
|
||||
- To show errors, add the `aria-invalid` prop to the `<Switch />` component and the `data-invalid` prop to the `<Field />` component.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-formisch-switch"
|
||||
className="sm:[&_.preview]:h-[500px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {15-19}
|
||||
<FormischField of={form} path={["twoFactor"]}>
|
||||
{(field) => (
|
||||
<Field orientation="horizontal" data-invalid={field.errors !== null}>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="form-twoFactor">
|
||||
Multi-factor authentication
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Enable multi-factor authentication to secure your account.
|
||||
</FieldDescription>
|
||||
{field.errors && (
|
||||
<FieldError errors={field.errors.map((message) => ({ message }))} />
|
||||
)}
|
||||
</FieldContent>
|
||||
<Switch
|
||||
id="form-twoFactor"
|
||||
checked={field.input ?? false}
|
||||
onCheckedChange={field.onChange}
|
||||
aria-invalid={field.errors !== null}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
</FormischField>
|
||||
```
|
||||
|
||||
### Complex Forms
|
||||
|
||||
Here is an example of a more complex form with multiple fields and validation.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-formisch-complex"
|
||||
className="sm:[&_.preview]:h-[1300px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
## Resetting the Form
|
||||
|
||||
Formisch exposes a top-level `reset` function. Pass the form store to reset it to its initial input.
|
||||
|
||||
```tsx showLineNumbers
|
||||
<Button type="button" variant="outline" onClick={() => reset(form)}>
|
||||
Reset
|
||||
</Button>
|
||||
```
|
||||
|
||||
You can also reset to new initial values, or reset while keeping the user's current input:
|
||||
|
||||
```tsx showLineNumbers
|
||||
// Reset to a fresh set of initial values
|
||||
reset(form, { initialInput: { title: "", description: "" } })
|
||||
|
||||
// Sync the baseline to new server data, but keep the user's edits
|
||||
reset(form, { initialInput: serverData, keepInput: true })
|
||||
```
|
||||
|
||||
## Array Fields
|
||||
|
||||
Formisch provides a `<FieldArray />` component and a set of helper functions for managing dynamic array fields. Use it whenever you need to add, remove, or reorder items.
|
||||
|
||||
<ComponentPreview
|
||||
name="form-formisch-array"
|
||||
className="sm:[&_.preview]:h-[700px]"
|
||||
chromeLessOnMobile
|
||||
/>
|
||||
|
||||
### Using FieldArray
|
||||
|
||||
`<FieldArray />` follows the same render-prop pattern as `<Field />`. Its `items` array contains a stable key per item that you should use as the React `key`.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx" {1,7-22}
|
||||
import {
|
||||
Field as FormischField,
|
||||
FieldArray,
|
||||
insert,
|
||||
remove,
|
||||
} from "@formisch/react"
|
||||
|
||||
export function ExampleForm() {
|
||||
// ... form config
|
||||
|
||||
return (
|
||||
<FieldArray of={form} path={["emails"]}>
|
||||
{(fieldArray) => (
|
||||
<FieldGroup className="gap-4">
|
||||
{fieldArray.items.map((item, index) => (
|
||||
<FormischField
|
||||
key={item}
|
||||
of={form}
|
||||
path={["emails", index, "address"]}
|
||||
>
|
||||
{(field) => /* ... */}
|
||||
</FormischField>
|
||||
))}
|
||||
</FieldGroup>
|
||||
)}
|
||||
</FieldArray>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Array Field Structure
|
||||
|
||||
Wrap your array fields in a `<FieldSet />` with a `<FieldLegend />` and `<FieldDescription />`.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx"
|
||||
<FieldSet className="gap-4">
|
||||
<FieldLegend variant="label">Email Addresses</FieldLegend>
|
||||
<FieldDescription>
|
||||
Add up to 5 email addresses where we can contact you.
|
||||
</FieldDescription>
|
||||
<FieldGroup className="gap-4">{/* Array items go here */}</FieldGroup>
|
||||
</FieldSet>
|
||||
```
|
||||
|
||||
### Adding Items
|
||||
|
||||
Use the `insert` function to add new items to the array. By default new items are appended to the end. You can also pass an `at` index to insert at a specific position.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx"
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
insert(form, { path: ["emails"], initialInput: { address: "" } })
|
||||
}
|
||||
disabled={fieldArray.items.length >= 5}
|
||||
>
|
||||
Add Email Address
|
||||
</Button>
|
||||
```
|
||||
|
||||
### Removing Items
|
||||
|
||||
Use the `remove` function with an `at` index to remove items from the array.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx"
|
||||
import { remove } from "@formisch/react"
|
||||
|
||||
{
|
||||
fieldArray.items.length > 1 && (
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={() => remove(form, { path: ["emails"], at: index })}
|
||||
aria-label={`Remove email ${index + 1}`}
|
||||
>
|
||||
<XIcon />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Formisch also exposes `move`, `swap`, and `replace` for reordering and replacing items. They follow the same `(form, config)` signature.
|
||||
|
||||
### Array Validation
|
||||
|
||||
Use Valibot's `array` and pipeline validators to constrain array fields.
|
||||
|
||||
```tsx showLineNumbers title="form.tsx"
|
||||
const FormSchema = v.object({
|
||||
emails: v.pipe(
|
||||
v.array(
|
||||
v.object({
|
||||
address: v.pipe(
|
||||
v.string(),
|
||||
v.nonEmpty("Enter an email address."),
|
||||
v.email("Enter a valid email address.")
|
||||
),
|
||||
})
|
||||
),
|
||||
v.minLength(1, "Add at least one email address."),
|
||||
v.maxLength(5, "You can add up to 5 email addresses.")
|
||||
),
|
||||
})
|
||||
```
|
||||
@@ -25,24 +25,6 @@ Start by selecting your framework. Then follow the instructions to learn how to
|
||||
</svg>
|
||||
<p className="mt-2 font-medium">TanStack Form</p>
|
||||
</LinkedCard>
|
||||
<LinkedCard href="/docs/forms/formisch">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
className="size-10"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="20"
|
||||
d="M90.9 291.7 49 299.4v125.5l193.3 63.8 221-63.8V304.7l-62-13.7m-352.2 8.6 193.2 53.6V488m221-183.2-221.1 48.4m-171-67.7 147.5 47 212.7-48.3L437 76 271.9 25 72.1 66.3Zm1-219.3L220.5 118l-1.8 214m1.8-214.3 216.2-41.6m-84.4 159.6-.9 1.9c-5.2 11.3-16.2 34.8-33.5 35.5h-1a22 22 0 0 1-9.7-2.2 26 26 0 0 1-7.4-5.5c-4.3-4.4-7.4-10-10-14.5q-1.2-2.4-2.7-4.7a54 54 0 0 0 21.7 4.1 74 74 0 0 0 23.6-4 70 70 0 0 0 11.4-5 64 64 0 0 0 8.5-5.6M247.6 168c19.5-20.8 34.8-12.3 36-18.4 1.5-7-12.2-7.7-22.2-2.6s-18 17.7-13.8 21m101.2-33.2c1.6 2.6 8.7-1.6 19.5-1.2s15.8 5.7 18 4.1-4.6-14.7-17.1-15.2-22 9.7-20.4 12.3m21.2 16.3c9.4.6 17.1 12.8 16.1 27.6s-10.4 25.9-19.8 25.2-17.1-12.9-16.1-27.7 10.4-25.8 19.8-25.1m-97.4 19c11 1.2 19 15.1 17.4 30.5s-12.3 27.4-23.4 26.2-19-15-17.4-30.5 12.3-27.3 23.4-26.2"
|
||||
/>
|
||||
</svg>
|
||||
<p className="mt-2 font-medium">Formisch</p>
|
||||
</LinkedCard>
|
||||
<LinkedCard href="#" className="border border-dashed bg-transparent">
|
||||
<svg
|
||||
role="img"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"pages": ["react-hook-form", "tanstack-form", "formisch"]
|
||||
"pages": ["react-hook-form", "tanstack-form"]
|
||||
}
|
||||
|
||||
@@ -9,9 +9,7 @@ If you're starting a new registry project, you can use the [registry template](h
|
||||
|
||||
## Requirements
|
||||
|
||||
You are free to design and host your custom registry as you see fit. The only requirement is that your registry catalog and registry items must be valid JSON files that conform to the [registry schema specification](/docs/registry/registry-json) and [registry-item schema specification](/docs/registry/registry-item-json).
|
||||
|
||||
Your registry can be a Next.js, Vite, Vue, Svelte, PHP or any other framework as long as it supports serving JSON over HTTP.
|
||||
You are free to design and host your custom registry as you see fit. The only requirement is that your registry items must be valid JSON files that conform to the [registry-item schema specification](/docs/registry/registry-item-json).
|
||||
|
||||
If you'd like to see an example of a registry, we have a [template project](https://github.com/shadcn-ui/registry-template) for you to use as a starting point.
|
||||
|
||||
@@ -21,7 +19,11 @@ The `registry.json` is the entry point for the registry. It contains the registr
|
||||
|
||||
Your registry must have this file (or JSON payload) present at the root of the registry endpoint. The registry endpoint is the URL where your registry is hosted.
|
||||
|
||||
Here's an example `registry.json` file:
|
||||
The `shadcn` CLI will automatically generate this file for you when you run the `build` command.
|
||||
|
||||
## Add a registry.json file
|
||||
|
||||
Create a `registry.json` file in the root of your project. Your project can be a Next.js, Vite, Vue, Svelte, PHP or any other framework as long as it supports serving JSON over HTTP.
|
||||
|
||||
```json title="registry.json" showLineNumbers
|
||||
{
|
||||
@@ -29,204 +31,44 @@ Here's an example `registry.json` file:
|
||||
"name": "acme",
|
||||
"homepage": "https://acme.com",
|
||||
"items": [
|
||||
{
|
||||
"name": "button",
|
||||
"type": "registry:ui",
|
||||
"title": "Button",
|
||||
"description": "A simple button component.",
|
||||
"files": [
|
||||
{
|
||||
"path": "components/ui/button.tsx",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Structure your registry
|
||||
|
||||
You can structure your source registry in one of two ways:
|
||||
|
||||
- Define all items in a single root `registry.json`.
|
||||
- Use a root `registry.json` with `include` to compose multiple `registry.json` files.
|
||||
|
||||
### Option A: Single registry.json
|
||||
|
||||
Create a `registry.json` file in the root of your project. Add all your registry items to the `items` array. This is the simplest way to define a registry.
|
||||
|
||||
```json title="registry.json" showLineNumbers
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry.json",
|
||||
"name": "acme",
|
||||
"homepage": "https://acme.com",
|
||||
"items": [
|
||||
{
|
||||
"name": "button",
|
||||
"type": "registry:ui",
|
||||
"title": "Button",
|
||||
"description": "A simple button component.",
|
||||
"files": [
|
||||
{
|
||||
"path": "components/ui/button.tsx",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "hello-world",
|
||||
"type": "registry:block",
|
||||
"title": "Hello World",
|
||||
"description": "A simple hello world component.",
|
||||
"registryDependencies": ["button"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/default/hello-world/hello-world.tsx",
|
||||
"type": "registry:component"
|
||||
}
|
||||
]
|
||||
}
|
||||
// ...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
This `registry.json` file must conform to the [registry schema specification](/docs/registry/registry-json).
|
||||
|
||||
### Option B: Using include
|
||||
## Add a registry item
|
||||
|
||||
For larger registries, you can use `include` to compose your source registry
|
||||
from multiple `registry.json` files.
|
||||
### Create your component
|
||||
|
||||
```txt
|
||||
registry.json
|
||||
components
|
||||
└── ui
|
||||
├── button.tsx
|
||||
├── input.tsx
|
||||
└── registry.json
|
||||
hooks
|
||||
├── registry.json
|
||||
├── use-media-query.ts
|
||||
└── use-toggle.ts
|
||||
```
|
||||
Add your first component. Here's an example of a simple `<HelloWorld />` component:
|
||||
|
||||
The root `registry.json` defines the registry metadata and includes the nested
|
||||
registry files.
|
||||
```tsx title="registry/new-york/hello-world/hello-world.tsx" showLineNumbers
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```json title="registry.json" showLineNumbers
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry.json",
|
||||
"name": "acme",
|
||||
"homepage": "https://acme.com",
|
||||
"include": [
|
||||
"components/ui/registry.json",
|
||||
"hooks/registry.json"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Included `registry.json` files are valid registry files for composition and may
|
||||
omit `name` and `homepage`. Only the root `registry.json` must define the
|
||||
registry metadata.
|
||||
|
||||
```json title="components/ui/registry.json" showLineNumbers
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry.json",
|
||||
"items": [
|
||||
{
|
||||
"name": "button",
|
||||
"type": "registry:ui",
|
||||
"files": [
|
||||
{
|
||||
"path": "button.tsx",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "input",
|
||||
"type": "registry:ui",
|
||||
"files": [
|
||||
{
|
||||
"path": "input.tsx",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```json title="hooks/registry.json" showLineNumbers
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry.json",
|
||||
"items": [
|
||||
{
|
||||
"name": "use-toggle",
|
||||
"type": "registry:hook",
|
||||
"files": [
|
||||
{
|
||||
"path": "use-toggle.ts",
|
||||
"type": "registry:hook"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "use-media-query",
|
||||
"type": "registry:hook",
|
||||
"files": [
|
||||
{
|
||||
"path": "use-media-query.ts",
|
||||
"type": "registry:hook"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
When using `include`, file paths are relative to the `registry.json` file that
|
||||
declares the item.
|
||||
|
||||
## Add an item
|
||||
|
||||
### Create a UI component
|
||||
|
||||
Add your first item. Here's an example of a simple `<Button />` component:
|
||||
|
||||
```tsx title="components/ui/button.tsx" showLineNumbers
|
||||
import * as React from "react"
|
||||
|
||||
export function Button(props: React.ComponentProps<"button">) {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
className="rounded-md bg-neutral-900 px-4 py-2 text-sm font-medium text-white"
|
||||
/>
|
||||
)
|
||||
export function HelloWorld() {
|
||||
return <Button>Hello World</Button>
|
||||
}
|
||||
```
|
||||
|
||||
<Callout className="mt-6">
|
||||
**Note:** This example places the component in the `components/ui` directory.
|
||||
You can place it anywhere in your project as long as you set the correct path
|
||||
in the `registry.json` file.
|
||||
**Note:** This example places the component in the `registry/new-york`
|
||||
directory. You can place it anywhere in your project as long as you set the
|
||||
correct path in the `registry.json` file and you follow the `registry/[NAME]`
|
||||
directory structure.
|
||||
</Callout>
|
||||
|
||||
```txt
|
||||
components
|
||||
└── ui
|
||||
└── button.tsx
|
||||
registry
|
||||
└── new-york
|
||||
└── hello-world
|
||||
└── hello-world.tsx
|
||||
```
|
||||
|
||||
### Add the item to the registry
|
||||
### Add your component to the registry
|
||||
|
||||
To add your component to the registry, add an item definition to `registry.json`.
|
||||
If you are using `include`, add the item to the included `registry.json` file
|
||||
that owns the component. For example, add a UI component to
|
||||
`components/ui/registry.json`.
|
||||
To add your component to the registry, you need to add your component definition to `registry.json`.
|
||||
|
||||
```json title="registry.json" showLineNumbers {6-17}
|
||||
{
|
||||
@@ -235,14 +77,14 @@ that owns the component. For example, add a UI component to
|
||||
"homepage": "https://acme.com",
|
||||
"items": [
|
||||
{
|
||||
"name": "button",
|
||||
"type": "registry:ui",
|
||||
"title": "Button",
|
||||
"description": "A simple button component.",
|
||||
"name": "hello-world",
|
||||
"type": "registry:block",
|
||||
"title": "Hello World",
|
||||
"description": "A simple hello world component.",
|
||||
"files": [
|
||||
{
|
||||
"path": "components/ui/button.tsx",
|
||||
"type": "registry:ui"
|
||||
"path": "registry/new-york/hello-world/hello-world.tsx",
|
||||
"type": "registry:component"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -252,144 +94,76 @@ that owns the component. For example, add a UI component to
|
||||
|
||||
You define your registry item by adding a `name`, `type`, `title`, `description` and `files`.
|
||||
|
||||
For every file you add, you must specify the `path` and `type` of the file. In a single-file registry, the `path` is relative to the root of your project. When using `include`, the `path` is relative to the `registry.json` file that declares the item. The `type` is the type of the file.
|
||||
For every file you add, you must specify the `path` and `type` of the file. The `path` is the relative path to the file from the root of your project. The `type` is the type of the file.
|
||||
|
||||
You can read more about the registry item schema and file types in the [registry item schema docs](/docs/registry/registry-item-json).
|
||||
|
||||
## Serve your registry
|
||||
## Build your registry
|
||||
|
||||
You can serve your registry as static JSON files or from dynamic route handlers.
|
||||
|
||||
### Option A: Static JSON files
|
||||
|
||||
Run the build command to generate static registry JSON files.
|
||||
### Install the shadcn CLI
|
||||
|
||||
```bash
|
||||
npx shadcn@latest build
|
||||
npm install shadcn@latest
|
||||
```
|
||||
|
||||
If your source registry uses `include`, `shadcn build` resolves the included
|
||||
registries and writes a flattened registry to your output directory. The
|
||||
generated `registry.json` does not contain `include`.
|
||||
### Add a build script
|
||||
|
||||
Add a `registry:build` script to your `package.json` file.
|
||||
|
||||
```json title="package.json" showLineNumbers
|
||||
{
|
||||
"scripts": {
|
||||
"registry:build": "shadcn build"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Run the build script
|
||||
|
||||
Run the build script to generate the registry JSON files.
|
||||
|
||||
```bash
|
||||
npm run registry:build
|
||||
```
|
||||
|
||||
<Callout className="mt-6">
|
||||
**Note:** By default, the build command will generate the registry JSON files
|
||||
in `public/r` e.g `public/r/button.json`. You can change the output directory by passing the `--output` option. See the [shadcn build command](/docs/cli#build) for more information.
|
||||
**Note:** By default, the build script will generate the registry JSON files
|
||||
in `public/r` e.g `public/r/hello-world.json`.
|
||||
|
||||
You can change the output directory by passing the `--output` option. See the [shadcn build command](/docs/cli#build) for more information.
|
||||
|
||||
</Callout>
|
||||
|
||||
If you're running your registry on Next.js, you can serve these files by running
|
||||
the `next` server. The command might differ for other frameworks.
|
||||
## Serve your registry
|
||||
|
||||
If you're running your registry on Next.js, you can now serve your registry by running the `next` server. The command might differ for other frameworks.
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Your files will now be served at `http://localhost:3000/r/[NAME].json` eg. `http://localhost:3000/r/button.json`.
|
||||
Your files will now be served at `http://localhost:3000/r/[NAME].json` eg. `http://localhost:3000/r/hello-world.json`.
|
||||
|
||||
### Option B: Dynamic route handlers
|
||||
|
||||
If you want to serve registry JSON from your source `registry.json` at request
|
||||
time, use the producer-side loader APIs from `shadcn/registry`.
|
||||
|
||||
Install `shadcn` as a runtime dependency:
|
||||
|
||||
```bash
|
||||
npm install shadcn
|
||||
```
|
||||
|
||||
Use `loadRegistry` to serve the registry catalog.
|
||||
|
||||
```ts title="app/r/registry.json/route.ts" showLineNumbers
|
||||
import { loadRegistry } from "shadcn/registry"
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const registry = await loadRegistry()
|
||||
|
||||
return Response.json(registry)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
return Response.json({ error: "Failed to load registry." }, { status: 500 })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use `loadRegistryItem` to serve individual registry items.
|
||||
|
||||
```ts title="app/r/[name].json/route.ts" showLineNumbers
|
||||
import { loadRegistryItem, RegistryItemNotFoundError } from "shadcn/registry"
|
||||
|
||||
export async function GET(
|
||||
_request: Request,
|
||||
context: {
|
||||
params: Promise<{
|
||||
name: string
|
||||
}>
|
||||
}
|
||||
) {
|
||||
const { name } = await context.params
|
||||
|
||||
try {
|
||||
const item = await loadRegistryItem(name)
|
||||
|
||||
return Response.json(item)
|
||||
} catch (error) {
|
||||
if (error instanceof RegistryItemNotFoundError) {
|
||||
return Response.json(
|
||||
{ error: `Registry item "${name}" was not found.` },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
console.error(error)
|
||||
|
||||
return Response.json(
|
||||
{ error: "Failed to load registry item." },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Both loaders resolve `include` before returning JSON, so route handlers can use
|
||||
the same source `registry.json` structure without running `shadcn build`.
|
||||
|
||||
<Accordion type="single" collapsible>
|
||||
<AccordionItem value="content-negotiation">
|
||||
<AccordionTrigger>Content negotiation</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
## Content negotiation
|
||||
|
||||
The `shadcn` CLI supports **HTTP Content Negotiation**. This allows you to host your registry at any endpoint — including the root of your domain — and serve different content depending on who is asking.
|
||||
|
||||
From a single URL, you can serve:
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<strong>HTML</strong> to browsers — a landing page, documentation, or
|
||||
marketing site.
|
||||
</li>
|
||||
<li>
|
||||
<strong>JSON</strong> to the <code>shadcn</code> CLI — an installable
|
||||
registry item.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Markdown</strong> to AI agents and LLMs — a machine-readable version
|
||||
of your content.
|
||||
</li>
|
||||
</ul>
|
||||
- **HTML** to browsers — a landing page, documentation, or marketing site.
|
||||
- **JSON** to the `shadcn` CLI — an installable registry item.
|
||||
- **Markdown** to AI agents and LLMs — a machine-readable version of your content.
|
||||
|
||||
The client signals its preference using the `Accept` request header, and your server decides what to return.
|
||||
|
||||
#### Request headers
|
||||
### Request headers
|
||||
|
||||
When the CLI makes a request to a registry, it sends the following headers:
|
||||
|
||||
- **User-Agent**: `shadcn`
|
||||
- **Accept**: `application/vnd.shadcn.v1+json, application/json;q=0.9`
|
||||
|
||||
#### Root hosting
|
||||
### Root hosting
|
||||
|
||||
By checking these headers on your server, you can route CLI traffic to an installable registry item while keeping browser traffic flowing to your documentation or homepage.
|
||||
|
||||
@@ -466,179 +240,34 @@ app.get("/", (req, res) => {
|
||||
|
||||
This enables:
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Branded Registry URLs</strong>:{" "}
|
||||
<code>shadcn add https://ui.example.com</code>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Shorter URLs</strong>: Users type your domain root, not{" "}
|
||||
<code>/r/</code> or <code>/registry/</code> sub-paths.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Easy Mnemonics</strong>: Easier for users to remember and share your
|
||||
registry.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</AccordionContent>
|
||||
|
||||
</AccordionItem>
|
||||
|
||||
</Accordion>
|
||||
|
||||
## Test your registry
|
||||
|
||||
After your registry is being served, test it with the same CLI commands that
|
||||
other developers will use.
|
||||
|
||||
### Using URL
|
||||
|
||||
Use the catalog URL for commands that discover items, like `list` and `search`.
|
||||
Use item URLs for commands that read or install a specific item, like `view` and
|
||||
`add`.
|
||||
|
||||
#### List items
|
||||
|
||||
Start by confirming that the registry catalog can be discovered.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest list http://localhost:3000/r/registry.json
|
||||
```
|
||||
|
||||
#### Search items
|
||||
|
||||
Search the registry by query.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest search http://localhost:3000/r/registry.json --query button
|
||||
```
|
||||
|
||||
#### View an item
|
||||
|
||||
Then view one registry item by name.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest view http://localhost:3000/r/button.json
|
||||
```
|
||||
|
||||
#### Add an item
|
||||
|
||||
To test the install flow, run `add` from a project where you want to install the
|
||||
item.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add http://localhost:3000/r/button.json
|
||||
```
|
||||
|
||||
### Using namespace
|
||||
|
||||
#### Add the registry
|
||||
|
||||
You can also test your registry with a namespace. From a project with a
|
||||
`components.json` file, add your registry URL template to the project.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest registry add @acme=http://localhost:3000/r/{name}.json
|
||||
```
|
||||
|
||||
The `{name}` placeholder must resolve to an item JSON file. For example,
|
||||
`@acme/button` resolves to `http://localhost:3000/r/button.json`. The catalog is
|
||||
still served separately at `http://localhost:3000/r/registry.json`.
|
||||
|
||||
#### List items
|
||||
|
||||
Then list the items in your registry.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest list @acme
|
||||
```
|
||||
|
||||
#### Search items
|
||||
|
||||
Search the registry by query.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest search @acme --query button
|
||||
```
|
||||
|
||||
#### View an item
|
||||
|
||||
View one registry item by name.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest view @acme/button
|
||||
```
|
||||
|
||||
#### Add an item
|
||||
|
||||
To test the install flow, run `add` from a project where you want to install the
|
||||
item.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add @acme/button
|
||||
```
|
||||
|
||||
See the [Namespaced Registries](/docs/registry/namespace) docs for more
|
||||
information.
|
||||
- **Branded Registry URLs**: `shadcn add https://ui.example.com`
|
||||
- **Shorter URLs**: Users type your domain root, not `/r/` or `/registry/` sub-paths.
|
||||
- **Easy Mnemonics**: Easier for users to remember and share your registry.
|
||||
|
||||
## Publish your registry
|
||||
|
||||
To make your registry available to other developers, publish your project to a
|
||||
public URL. Once deployed, users can install items directly from item URLs, or
|
||||
they can add your registry as a namespace in their project.
|
||||
|
||||
### Share namespace setup instructions
|
||||
|
||||
If you want users to install items with a namespace like `@acme/button`, tell
|
||||
them to add your registry URL template to their project. The `{name}`
|
||||
placeholder is replaced by the item name when the CLI resolves the registry
|
||||
item.
|
||||
|
||||
The template must resolve to item JSON files. For example, `@acme/button`
|
||||
resolves to `https://acme.com/r/button.json`. Your registry catalog should still
|
||||
be served separately at `https://acme.com/r/registry.json`.
|
||||
|
||||
They can add the namespace with the CLI.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest registry add @acme=https://acme.com/r/{name}.json
|
||||
```
|
||||
|
||||
Or they can add it manually under the `registries` field in their
|
||||
`components.json` file.
|
||||
|
||||
```json title="components.json" showLineNumbers
|
||||
{
|
||||
"registries": {
|
||||
"@acme": "https://acme.com/r/{name}.json"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Users can then consume items from your registry by namespace.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add @acme/button
|
||||
```
|
||||
|
||||
### Add your namespace to the registry index
|
||||
|
||||
If your registry is open source and publicly available, you can submit your
|
||||
namespace to the official registry index. This lets users add your namespace by
|
||||
name instead of pasting the full URL template.
|
||||
|
||||
See the [Registry Index](/docs/registry/registry-index) docs for the submission
|
||||
requirements.
|
||||
To make your registry available to other developers, you can publish it by deploying your project to a public URL.
|
||||
|
||||
## Guidelines
|
||||
|
||||
Here are some guidelines to follow when building components for a registry.
|
||||
|
||||
- Place your registry item in the `registry/[STYLE]/[NAME]` directory. I'm using `default` as an example. It can be anything you want as long as it's nested under the `registry` directory.
|
||||
- For blocks, the following properties are required: `name`, `description`, `type` and `files`.
|
||||
- Place your registry item in the `registry/[STYLE]/[NAME]` directory. I'm using `new-york` as an example. It can be anything you want as long as it's nested under the `registry` directory.
|
||||
- The following properties are required for the block definition: `name`, `description`, `type` and `files`.
|
||||
- It is recommended to add a proper name and description to your registry item. This helps LLMs understand the component and its purpose.
|
||||
- Make sure to list all registry dependencies in `registryDependencies`. A registry dependency is the name of the component in the registry eg. `input`, `button`, `card`, etc or a URL to a registry item eg. `http://localhost:3000/r/editor.json`.
|
||||
- Make sure to list all dependencies in `dependencies`. A dependency is the name of the package in the registry eg. `zod`, `sonner`, etc. To set a version, you can use the `name@version` format eg. `zod@^3.20.0`.
|
||||
- **Imports should always use the `@/registry` path.** eg. `import { HelloWorld } from "@/registry/default/hello-world/hello-world"`
|
||||
- **Imports should always use the `@/registry` path.** eg. `import { HelloWorld } from "@/registry/new-york/hello-world/hello-world"`
|
||||
- Ideally, place your files within a registry item in `components`, `hooks`, `lib` directories.
|
||||
|
||||
## Install using the CLI
|
||||
|
||||
To install a registry item using the `shadcn` CLI, use the `add` command followed by the URL of the registry item.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add http://localhost:3000/r/hello-world.json
|
||||
```
|
||||
|
||||
See the [Namespaced
|
||||
Registries](/docs/registry/namespace) docs for more information on
|
||||
how to install registry items from a namespaced registry.
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
"pages": [
|
||||
"index",
|
||||
"getting-started",
|
||||
"registry-index",
|
||||
"examples",
|
||||
"namespace",
|
||||
"authentication",
|
||||
"examples",
|
||||
"mcp",
|
||||
"registry-index",
|
||||
"open-in-v0",
|
||||
"registry-json",
|
||||
"registry-item-json"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Registry Directory
|
||||
title: Add a Registry
|
||||
description: Open Source Registry Index
|
||||
---
|
||||
|
||||
@@ -17,7 +17,7 @@ You can see the full list at [https://ui.shadcn.com/r/registries.json](https://u
|
||||
|
||||
Once you have submitted your request, it will be validated and reviewed by the team.
|
||||
|
||||
## Requirements
|
||||
### Requirements
|
||||
|
||||
1. The registry must be open source and publicly accessible.
|
||||
2. The registry must be a valid JSON file that conforms to the [registry schema specification](/docs/registry/registry-json).
|
||||
|
||||
@@ -24,7 +24,7 @@ The `registry.json` schema is used to define your custom component registry.
|
||||
"dependencies": ["is-even@3.0.0", "motion"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/default/hello-world/hello-world.tsx",
|
||||
"path": "registry/new-york/hello-world/hello-world.tsx",
|
||||
"type": "registry:component"
|
||||
}
|
||||
]
|
||||
@@ -33,22 +33,6 @@ The `registry.json` schema is used to define your custom component registry.
|
||||
}
|
||||
```
|
||||
|
||||
You can also organize a large registry across multiple `registry.json` files
|
||||
using `include`.
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```json title="registry.json" showLineNumbers
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry.json",
|
||||
"name": "acme",
|
||||
"homepage": "https://acme.com",
|
||||
"include": [
|
||||
"components/ui/registry.json",
|
||||
"hooks/registry.json"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Definitions
|
||||
|
||||
You can see the JSON Schema for `registry.json` [here](https://ui.shadcn.com/schema/registry.json).
|
||||
@@ -83,61 +67,6 @@ The homepage of your registry. This is used for data attributes and other metada
|
||||
}
|
||||
```
|
||||
|
||||
### include
|
||||
|
||||
The `include` property is used to compose a registry from other `registry.json`
|
||||
files.
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```json title="registry.json" showLineNumbers
|
||||
{
|
||||
"include": [
|
||||
"components/ui/registry.json",
|
||||
"hooks/registry.json"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Each include path must be a relative path to an explicit `registry.json` file.
|
||||
Folder shorthand is not supported.
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```json title="registry.json" showLineNumbers
|
||||
{
|
||||
"include": [
|
||||
"components/ui/registry.json"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Included `registry.json` files may omit `name` and `homepage`. These fields are
|
||||
required only on the root `registry.json`.
|
||||
|
||||
```json title="components/ui/registry.json" showLineNumbers
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry.json",
|
||||
"items": [
|
||||
{
|
||||
"name": "button",
|
||||
"type": "registry:ui",
|
||||
"files": [
|
||||
{
|
||||
"path": "button.tsx",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
When `shadcn build` resolves includes, item file paths are read relative to the
|
||||
`registry.json` file that declares the item. The generated registry output is
|
||||
flattened and does not contain `include`.
|
||||
|
||||
Registry item names must be unique across the resolved registry, including all
|
||||
included files.
|
||||
|
||||
### items
|
||||
|
||||
The `items` in your registry. Each item must implement the [registry-item schema specification](https://ui.shadcn.com/schema/registry-item.json).
|
||||
@@ -158,7 +87,7 @@ The `items` in your registry. Each item must implement the [registry-item schema
|
||||
"dependencies": ["is-even@3.0.0", "motion"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/default/hello-world/hello-world.tsx",
|
||||
"path": "registry/new-york/hello-world/hello-world.tsx",
|
||||
"type": "registry:component"
|
||||
}
|
||||
]
|
||||
@@ -167,7 +96,4 @@ The `items` in your registry. Each item must implement the [registry-item schema
|
||||
}
|
||||
```
|
||||
|
||||
The root `registry.json` must define at least one of `items` or `include`. If
|
||||
`items` is omitted, it defaults to an empty array.
|
||||
|
||||
See the [registry-item schema documentation](/docs/registry/registry-item-json) for more information.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export const PAGES_NEW = [
|
||||
"/create",
|
||||
"/docs/registry",
|
||||
"/docs/registry/getting-started",
|
||||
"/docs/cli",
|
||||
"/docs/changelog",
|
||||
"/docs/skills",
|
||||
]
|
||||
|
||||
export const PAGES_UPDATED = ["/docs/components/button"]
|
||||
|
||||
@@ -21,7 +21,6 @@ const eventSchema = z.object({
|
||||
"copy_create_share_url",
|
||||
"copy_registry_add_command",
|
||||
"copy_preset_command",
|
||||
"copy_apply_command",
|
||||
]),
|
||||
// declare type AllowedPropertyValues = string | number | boolean | null
|
||||
properties: z
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@faker-js/faker": "^10.1.0",
|
||||
"@formisch/react": "^0.4.4",
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
"@hugeicons/core-free-icons": "^1.2.1",
|
||||
"@hugeicons/react": "^1.1.1",
|
||||
@@ -61,14 +60,14 @@
|
||||
"lru-cache": "^11.2.4",
|
||||
"lucide-react": "0.474.0",
|
||||
"motion": "^12.12.1",
|
||||
"next": "16.1.6",
|
||||
"next": "16.3.0-canary.16",
|
||||
"next-themes": "0.4.6",
|
||||
"nuqs": "^2.8.9",
|
||||
"postcss": "^8.5.1",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "19.2.3",
|
||||
"react": "19.2.6",
|
||||
"react-day-picker": "^9.7.0",
|
||||
"react-dom": "19.2.3",
|
||||
"react-dom": "19.2.6",
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-qr-code": "^2.0.18",
|
||||
"react-resizable-panels": "^4",
|
||||
@@ -77,13 +76,12 @@
|
||||
"rehype-pretty-code": "^0.14.1",
|
||||
"rimraf": "^6.0.1",
|
||||
"server-only": "^0.0.1",
|
||||
"shadcn": "4.8.0",
|
||||
"shadcn": "4.7.0",
|
||||
"shiki": "^1.10.1",
|
||||
"sonner": "^2.0.0",
|
||||
"swr": "^2.3.6",
|
||||
"tailwind-merge": "^3.0.1",
|
||||
"ts-morph": "26.0.0",
|
||||
"valibot": "^1.4.0",
|
||||
"vaul": "1.1.2",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
|
||||
@@ -124,7 +124,6 @@
|
||||
- [Forms Overview](https://ui.shadcn.com/docs/forms): Guide to building forms with shadcn/ui.
|
||||
- [React Hook Form](https://ui.shadcn.com/docs/forms/react-hook-form): Using shadcn/ui with React Hook Form.
|
||||
- [TanStack Form](https://ui.shadcn.com/docs/forms/tanstack-form): Using shadcn/ui with TanStack Form.
|
||||
- [Formisch](https://ui.shadcn.com/docs/forms/formisch): Using shadcn/ui with Formisch.
|
||||
- [Forms - Next.js](https://ui.shadcn.com/docs/forms/next): Building forms in Next.js with Server Actions.
|
||||
|
||||
## Advanced
|
||||
|
||||
@@ -137,12 +137,6 @@
|
||||
"url": "https://boldkit.dev/r/{name}.json",
|
||||
"description": "Neubrutalism component library with 43 components, 42 SVG shapes, thick borders, and hard shadows. Supports React, Vue, and Nuxt. Built on shadcn/ui."
|
||||
},
|
||||
{
|
||||
"name": "@bklit",
|
||||
"homepage": "https://ui.bklit.com",
|
||||
"url": "https://ui.bklit.com/r/{name}.json",
|
||||
"description": "Open-source composable chart components for React — line, area, bar, pie, radar, maps, and more. Built with Visx, Motion, and shadcn/ui."
|
||||
},
|
||||
{
|
||||
"name": "@bundui",
|
||||
"homepage": "https://bundui.io",
|
||||
@@ -185,12 +179,6 @@
|
||||
"url": "https://commercn.com/r/{name}.json",
|
||||
"description": "Shadcn UI Blocks for Ecommerce websites"
|
||||
},
|
||||
{
|
||||
"name": "@corr",
|
||||
"homepage": "https://ui.corr.sh",
|
||||
"url": "https://ui.corr.sh/r/{name}.json",
|
||||
"description": "A collection of shadcn-based React components, charts, animated components, and blocks built over time."
|
||||
},
|
||||
{
|
||||
"name": "@coss",
|
||||
"homepage": "https://coss.com/ui",
|
||||
@@ -301,7 +289,7 @@
|
||||
},
|
||||
{
|
||||
"name": "@glasscn",
|
||||
"homepage": "https://glasscn-components.vercel.app/",
|
||||
"homepage": "https://glasscn.vercel.app/",
|
||||
"url": "https://glasscn-components.vercel.app/r/{name}.json",
|
||||
"description": "A shadcn-compatible registry of glassmorphism components inspired by Apple"
|
||||
},
|
||||
@@ -459,7 +447,7 @@
|
||||
"name": "@ncdai",
|
||||
"homepage": "https://chanhdai.com/components",
|
||||
"url": "https://chanhdai.com/r/{name}.json",
|
||||
"description": "Pixel-perfect, uniquely crafted."
|
||||
"description": "A collection of reusable components."
|
||||
},
|
||||
{
|
||||
"name": "@nteract",
|
||||
@@ -521,12 +509,6 @@
|
||||
"url": "https://gsap.pacekit.dev/r/{name}.json",
|
||||
"description": "Animated GSAP components crafted for smooth interaction and rich detail."
|
||||
},
|
||||
{
|
||||
"name": "@paddle",
|
||||
"homepage": "https://developer.paddle.com/",
|
||||
"url": "https://developer.paddle.com/r/{name}.json",
|
||||
"description": "Drop-in components for building checkouts, pricing pages, and subscription management screens using Paddle Billing."
|
||||
},
|
||||
{
|
||||
"name": "@pastecn",
|
||||
"homepage": "https://pastecn.com",
|
||||
@@ -773,12 +755,6 @@
|
||||
"url": "https://onboarding-tour.vercel.app/r/{name}.json",
|
||||
"description": "A component for building onboarding tours. Designed to integrate with shadcn/ui."
|
||||
},
|
||||
{
|
||||
"name": "@trophy-ui",
|
||||
"homepage": "https://ui.trophy.so",
|
||||
"url": "https://ui.trophy.so/r/{name}.json",
|
||||
"description": "Open-source gamification UI components for streaks, achievements, leaderboards, points, and more. Built on shadcn/ui and Tailwind CSS."
|
||||
},
|
||||
{
|
||||
"name": "@uitripled",
|
||||
"homepage": "https://ui.tripled.work",
|
||||
@@ -1162,23 +1138,5 @@
|
||||
"homepage": "https://framecn.vercel.app",
|
||||
"url": "https://framecn.vercel.app/r/{name}.json",
|
||||
"description": "Beautiful videos, made simple. Ready to use, customizable video components for React."
|
||||
},
|
||||
{
|
||||
"name": "@turbopills-ui",
|
||||
"homepage": "https://www.turbopills.com/ui/docs",
|
||||
"url": "https://ui.turbopills.com/r/{name}.json",
|
||||
"description": "Beautiful, accessible, and customizable React components for your telehealth applications."
|
||||
},
|
||||
{
|
||||
"name": "@nexus-labs",
|
||||
"homepage": "https://nexus-ui.com",
|
||||
"url": "https://nexus-ui.com/r/{name}.json",
|
||||
"description": "Motion-native animated components for Next.js — backgrounds, heroes, inputs, carousels, and more. Open source. Copy-ready TypeScript."
|
||||
},
|
||||
{
|
||||
"name": "@wensity",
|
||||
"homepage": "https://wensity.com",
|
||||
"url": "https://raw.githubusercontent.com/ksparth12/wensity-shadcn-registry/main/{name}.json",
|
||||
"description": "Motion-rich React components for AI interfaces, SaaS blocks, and cinematic interactions. Free Wensity components only."
|
||||
}
|
||||
]
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/base-luma/ui/toggle-group.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Toggle as TogglePrimitive } from \"@base-ui/react/toggle\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"@base-ui/react/toggle-group\"\nimport { type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/registry/base-luma/lib/utils\"\nimport { toggleVariants } from \"@/registry/base-luma/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 2,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 2,\n orientation = \"horizontal\",\n children,\n ...props\n}: ToggleGroupPrimitive.Props &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }) {\n return (\n <ToggleGroupPrimitive\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] data-[spacing=0]:data-[variant=outline]:rounded-3xl data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider\n value={{ variant, size, spacing, orientation }}\n >\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <TogglePrimitive\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-3 group-data-[spacing=0]/toggle-group:shadow-none focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-2.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-2.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-3xl group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-3xl group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-3xl group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-3xl data-[state=on]:bg-muted group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </TogglePrimitive>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Toggle as TogglePrimitive } from \"@base-ui/react/toggle\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"@base-ui/react/toggle-group\"\nimport { type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/registry/base-luma/lib/utils\"\nimport { toggleVariants } from \"@/registry/base-luma/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 0,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 0,\n orientation = \"horizontal\",\n children,\n ...props\n}: ToggleGroupPrimitive.Props &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }) {\n return (\n <ToggleGroupPrimitive\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] data-[spacing=0]:data-[variant=outline]:rounded-3xl data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider\n value={{ variant, size, spacing, orientation }}\n >\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <TogglePrimitive\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-3 group-data-[spacing=0]/toggle-group:shadow-none focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-2.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-2.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-3xl group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-3xl group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-3xl group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-3xl data-[state=on]:bg-muted group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </TogglePrimitive>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/base-lyra/ui/toggle-group.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Toggle as TogglePrimitive } from \"@base-ui/react/toggle\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"@base-ui/react/toggle-group\"\nimport { type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/registry/base-lyra/lib/utils\"\nimport { toggleVariants } from \"@/registry/base-lyra/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 2,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 2,\n orientation = \"horizontal\",\n children,\n ...props\n}: ToggleGroupPrimitive.Props &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }) {\n return (\n <ToggleGroupPrimitive\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-none data-[size=sm]:rounded-none data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider\n value={{ variant, size, spacing, orientation }}\n >\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <TogglePrimitive\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-none group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-none group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-none group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-none group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </TogglePrimitive>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Toggle as TogglePrimitive } from \"@base-ui/react/toggle\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"@base-ui/react/toggle-group\"\nimport { type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/registry/base-lyra/lib/utils\"\nimport { toggleVariants } from \"@/registry/base-lyra/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 0,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 0,\n orientation = \"horizontal\",\n children,\n ...props\n}: ToggleGroupPrimitive.Props &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }) {\n return (\n <ToggleGroupPrimitive\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-none data-[size=sm]:rounded-none data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider\n value={{ variant, size, spacing, orientation }}\n >\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <TogglePrimitive\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-none group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-none group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-none group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-none group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </TogglePrimitive>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/base-maia/ui/toggle-group.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Toggle as TogglePrimitive } from \"@base-ui/react/toggle\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"@base-ui/react/toggle-group\"\nimport { type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/registry/base-maia/lib/utils\"\nimport { toggleVariants } from \"@/registry/base-maia/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 2,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 2,\n orientation = \"horizontal\",\n children,\n ...props\n}: ToggleGroupPrimitive.Props &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }) {\n return (\n <ToggleGroupPrimitive\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] data-[spacing=0]:data-[variant=outline]:rounded-4xl data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider\n value={{ variant, size, spacing, orientation }}\n >\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <TogglePrimitive\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-3 group-data-[spacing=0]/toggle-group:shadow-none focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-2.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-2.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-3xl group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-3xl group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-3xl group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-3xl data-[state=on]:bg-muted group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </TogglePrimitive>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Toggle as TogglePrimitive } from \"@base-ui/react/toggle\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"@base-ui/react/toggle-group\"\nimport { type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/registry/base-maia/lib/utils\"\nimport { toggleVariants } from \"@/registry/base-maia/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 0,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 0,\n orientation = \"horizontal\",\n children,\n ...props\n}: ToggleGroupPrimitive.Props &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }) {\n return (\n <ToggleGroupPrimitive\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] data-[spacing=0]:data-[variant=outline]:rounded-4xl data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider\n value={{ variant, size, spacing, orientation }}\n >\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <TogglePrimitive\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-3 group-data-[spacing=0]/toggle-group:shadow-none focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-2.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-2.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-3xl group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-3xl group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-3xl group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-3xl data-[state=on]:bg-muted group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </TogglePrimitive>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/base-mira/ui/toggle-group.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Toggle as TogglePrimitive } from \"@base-ui/react/toggle\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"@base-ui/react/toggle-group\"\nimport { type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/registry/base-mira/lib/utils\"\nimport { toggleVariants } from \"@/registry/base-mira/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 2,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 2,\n orientation = \"horizontal\",\n children,\n ...props\n}: ToggleGroupPrimitive.Props &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }) {\n return (\n <ToggleGroupPrimitive\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-md data-[size=sm]:rounded-[min(var(--radius-md),8px)] data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider\n value={{ variant, size, spacing, orientation }}\n >\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <TogglePrimitive\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-md group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-md group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-md group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-md group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </TogglePrimitive>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Toggle as TogglePrimitive } from \"@base-ui/react/toggle\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"@base-ui/react/toggle-group\"\nimport { type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/registry/base-mira/lib/utils\"\nimport { toggleVariants } from \"@/registry/base-mira/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 0,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 0,\n orientation = \"horizontal\",\n children,\n ...props\n}: ToggleGroupPrimitive.Props &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }) {\n return (\n <ToggleGroupPrimitive\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-md data-[size=sm]:rounded-[min(var(--radius-md),8px)] data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider\n value={{ variant, size, spacing, orientation }}\n >\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <TogglePrimitive\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-md group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-md group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-md group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-md group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </TogglePrimitive>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/base-nova/ui/toggle-group.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Toggle as TogglePrimitive } from \"@base-ui/react/toggle\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"@base-ui/react/toggle-group\"\nimport { type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/registry/base-nova/lib/utils\"\nimport { toggleVariants } from \"@/registry/base-nova/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 2,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 2,\n orientation = \"horizontal\",\n children,\n ...props\n}: ToggleGroupPrimitive.Props &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }) {\n return (\n <ToggleGroupPrimitive\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-lg data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider\n value={{ variant, size, spacing, orientation }}\n >\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <TogglePrimitive\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-lg group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-lg group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-lg group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-lg group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </TogglePrimitive>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Toggle as TogglePrimitive } from \"@base-ui/react/toggle\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"@base-ui/react/toggle-group\"\nimport { type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/registry/base-nova/lib/utils\"\nimport { toggleVariants } from \"@/registry/base-nova/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 0,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 0,\n orientation = \"horizontal\",\n children,\n ...props\n}: ToggleGroupPrimitive.Props &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }) {\n return (\n <ToggleGroupPrimitive\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-lg data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider\n value={{ variant, size, spacing, orientation }}\n >\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <TogglePrimitive\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-lg group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-lg group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-lg group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-lg group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </TogglePrimitive>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/base-sera/ui/toggle-group.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Toggle as TogglePrimitive } from \"@base-ui/react/toggle\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"@base-ui/react/toggle-group\"\nimport { type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/registry/base-sera/lib/utils\"\nimport { toggleVariants } from \"@/registry/base-sera/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 2,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 2,\n orientation = \"horizontal\",\n children,\n ...props\n}: ToggleGroupPrimitive.Props &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }) {\n return (\n <ToggleGroupPrimitive\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] data-[spacing=0]:data-[variant=outline]:rounded-none data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider\n value={{ variant, size, spacing, orientation }}\n >\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <TogglePrimitive\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-6 group-data-[spacing=0]/toggle-group:shadow-none focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-4 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-4 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-none group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-none group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-none group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-none data-[state=on]:bg-muted data-[state=on]:text-foreground group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </TogglePrimitive>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Toggle as TogglePrimitive } from \"@base-ui/react/toggle\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"@base-ui/react/toggle-group\"\nimport { type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/registry/base-sera/lib/utils\"\nimport { toggleVariants } from \"@/registry/base-sera/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 0,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 0,\n orientation = \"horizontal\",\n children,\n ...props\n}: ToggleGroupPrimitive.Props &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }) {\n return (\n <ToggleGroupPrimitive\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] data-[spacing=0]:data-[variant=outline]:rounded-none data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider\n value={{ variant, size, spacing, orientation }}\n >\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <TogglePrimitive\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-6 group-data-[spacing=0]/toggle-group:shadow-none focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-4 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-4 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-none group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-none group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-none group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-none data-[state=on]:bg-muted data-[state=on]:text-foreground group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </TogglePrimitive>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/base-vega/ui/toggle-group.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Toggle as TogglePrimitive } from \"@base-ui/react/toggle\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"@base-ui/react/toggle-group\"\nimport { type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/registry/base-vega/lib/utils\"\nimport { toggleVariants } from \"@/registry/base-vega/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 2,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 2,\n orientation = \"horizontal\",\n children,\n ...props\n}: ToggleGroupPrimitive.Props &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }) {\n return (\n <ToggleGroupPrimitive\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=0]:data-[variant=outline]:shadow-xs data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider\n value={{ variant, size, spacing, orientation }}\n >\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <TogglePrimitive\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 group-data-[spacing=0]/toggle-group:shadow-none focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-md group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-md group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-md group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-md data-[state=on]:bg-muted group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </TogglePrimitive>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Toggle as TogglePrimitive } from \"@base-ui/react/toggle\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"@base-ui/react/toggle-group\"\nimport { type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/registry/base-vega/lib/utils\"\nimport { toggleVariants } from \"@/registry/base-vega/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 0,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 0,\n orientation = \"horizontal\",\n children,\n ...props\n}: ToggleGroupPrimitive.Props &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }) {\n return (\n <ToggleGroupPrimitive\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=0]:data-[variant=outline]:shadow-xs data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider\n value={{ variant, size, spacing, orientation }}\n >\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <TogglePrimitive\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 group-data-[spacing=0]/toggle-group:shadow-none focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-md group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-md group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-md group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-md data-[state=on]:bg-muted group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </TogglePrimitive>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
],
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "form-formisch-input",
|
||||
"dependencies": [
|
||||
"@formisch/react",
|
||||
"valibot"
|
||||
],
|
||||
"registryDependencies": [
|
||||
"field",
|
||||
"input",
|
||||
"button",
|
||||
"card"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-formisch-input.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Form, Field as FormischField, reset, useForm } from \"@formisch/react\"\nimport type { SubmitHandler } from \"@formisch/react\"\nimport { toast } from \"sonner\"\nimport * as v from \"valibot\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\n\nconst FormSchema = v.object({\n username: v.pipe(\n v.string(),\n v.minLength(3, \"Username must be at least 3 characters.\"),\n v.maxLength(10, \"Username must be at most 10 characters.\"),\n v.regex(\n /^[a-zA-Z0-9_]+$/,\n \"Username can only contain letters, numbers, and underscores.\"\n )\n ),\n})\n\nexport default function FormFormischInput() {\n const form = useForm({\n schema: FormSchema,\n initialInput: {\n username: \"\",\n },\n })\n\n const handleSubmit: SubmitHandler<typeof FormSchema> = (output) => {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"mt-2 w-[320px] overflow-x-auto rounded-md bg-code p-4 text-code-foreground\">\n <code>{JSON.stringify(output, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n }\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Profile Settings</CardTitle>\n <CardDescription>\n Update your profile information below.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <Form of={form} id=\"form-formisch-input\" onSubmit={handleSubmit}>\n <FieldGroup>\n <FormischField of={form} path={[\"username\"]}>\n {(field) => (\n <Field data-invalid={field.errors !== null}>\n <FieldLabel htmlFor=\"form-formisch-input-username\">\n Username\n </FieldLabel>\n <Input\n {...field.props}\n id=\"form-formisch-input-username\"\n value={field.input ?? \"\"}\n aria-invalid={field.errors !== null}\n placeholder=\"shadcn\"\n autoComplete=\"username\"\n />\n <FieldDescription>\n This is your public display name. Must be between 3 and 10\n characters. Must only contain letters, numbers, and\n underscores.\n </FieldDescription>\n {field.errors && (\n <FieldError\n errors={field.errors.map((message) => ({ message }))}\n />\n )}\n </Field>\n )}\n </FormischField>\n </FieldGroup>\n </Form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => reset(form)}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-formisch-input\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
|
||||
"type": "registry:example"
|
||||
}
|
||||
],
|
||||
"type": "registry:example"
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "form-formisch-radiogroup",
|
||||
"dependencies": [
|
||||
"@formisch/react",
|
||||
"valibot"
|
||||
],
|
||||
"registryDependencies": [
|
||||
"field",
|
||||
"radio-group",
|
||||
"button",
|
||||
"card"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-formisch-radiogroup.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Form, Field as FormischField, reset, useForm } from \"@formisch/react\"\nimport type { SubmitHandler } from \"@formisch/react\"\nimport { toast } from \"sonner\"\nimport * as v from \"valibot\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldContent,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n FieldLegend,\n FieldSet,\n FieldTitle,\n} from \"@/registry/new-york-v4/ui/field\"\nimport {\n RadioGroup,\n RadioGroupItem,\n} from \"@/registry/new-york-v4/ui/radio-group\"\n\nconst plans = [\n {\n id: \"starter\",\n title: \"Starter (100K tokens/month)\",\n description: \"For everyday use with basic features.\",\n },\n {\n id: \"pro\",\n title: \"Pro (1M tokens/month)\",\n description: \"For advanced AI usage with more features.\",\n },\n {\n id: \"enterprise\",\n title: \"Enterprise (Unlimited tokens)\",\n description: \"For large teams and heavy usage.\",\n },\n] as const\n\nconst FormSchema = v.object({\n plan: v.pipe(\n v.string(),\n v.minLength(1, \"You must select a subscription plan to continue.\")\n ),\n})\n\nexport default function FormFormischRadioGroup() {\n const form = useForm({\n schema: FormSchema,\n initialInput: {\n plan: \"\",\n },\n })\n\n const handleSubmit: SubmitHandler<typeof FormSchema> = (output) => {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"mt-2 w-[320px] overflow-x-auto rounded-md bg-code p-4 text-code-foreground\">\n <code>{JSON.stringify(output, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n }\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Subscription Plan</CardTitle>\n <CardDescription>\n See pricing and features for each plan.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <Form of={form} id=\"form-formisch-radiogroup\" onSubmit={handleSubmit}>\n <FieldGroup>\n <FormischField of={form} path={[\"plan\"]}>\n {(field) => (\n <FieldSet data-invalid={field.errors !== null}>\n <FieldLegend>Plan</FieldLegend>\n <FieldDescription>\n You can upgrade or downgrade your plan at any time.\n </FieldDescription>\n <RadioGroup\n value={field.input ?? \"\"}\n onValueChange={(value) => field.onChange(value)}\n aria-invalid={field.errors !== null}\n >\n {plans.map((plan) => (\n <FieldLabel\n key={plan.id}\n htmlFor={`form-formisch-radiogroup-${plan.id}`}\n >\n <Field\n orientation=\"horizontal\"\n data-invalid={field.errors !== null}\n >\n <FieldContent>\n <FieldTitle>{plan.title}</FieldTitle>\n <FieldDescription>\n {plan.description}\n </FieldDescription>\n </FieldContent>\n <RadioGroupItem\n value={plan.id}\n id={`form-formisch-radiogroup-${plan.id}`}\n aria-invalid={field.errors !== null}\n />\n </Field>\n </FieldLabel>\n ))}\n </RadioGroup>\n {field.errors && (\n <FieldError\n errors={field.errors.map((message) => ({ message }))}\n />\n )}\n </FieldSet>\n )}\n </FormischField>\n </FieldGroup>\n </Form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => reset(form)}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-formisch-radiogroup\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
|
||||
"type": "registry:example"
|
||||
}
|
||||
],
|
||||
"type": "registry:example"
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "form-formisch-select",
|
||||
"dependencies": [
|
||||
"@formisch/react",
|
||||
"valibot"
|
||||
],
|
||||
"registryDependencies": [
|
||||
"field",
|
||||
"select",
|
||||
"button",
|
||||
"card"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-formisch-select.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Form, Field as FormischField, reset, useForm } from \"@formisch/react\"\nimport type { SubmitHandler } from \"@formisch/react\"\nimport { toast } from \"sonner\"\nimport * as v from \"valibot\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldContent,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectSeparator,\n SelectTrigger,\n SelectValue,\n} from \"@/registry/new-york-v4/ui/select\"\n\nconst spokenLanguages = [\n { label: \"English\", value: \"en\" },\n { label: \"Spanish\", value: \"es\" },\n { label: \"French\", value: \"fr\" },\n { label: \"German\", value: \"de\" },\n { label: \"Italian\", value: \"it\" },\n { label: \"Chinese\", value: \"zh\" },\n { label: \"Japanese\", value: \"ja\" },\n] as const\n\nconst FormSchema = v.object({\n language: v.pipe(\n v.string(),\n v.minLength(1, \"Please select your spoken language.\"),\n v.check(\n (value) => value !== \"auto\",\n \"Auto-detection is not allowed. Please select a specific language.\"\n )\n ),\n})\n\nexport default function FormFormischSelect() {\n const form = useForm({\n schema: FormSchema,\n initialInput: {\n language: \"\",\n },\n })\n\n const handleSubmit: SubmitHandler<typeof FormSchema> = (output) => {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"mt-2 w-[320px] overflow-x-auto rounded-md bg-code p-4 text-code-foreground\">\n <code>{JSON.stringify(output, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n }\n\n return (\n <Card className=\"w-full sm:max-w-lg\">\n <CardHeader>\n <CardTitle>Language Preferences</CardTitle>\n <CardDescription>\n Select your preferred spoken language.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <Form of={form} id=\"form-formisch-select\" onSubmit={handleSubmit}>\n <FieldGroup>\n <FormischField of={form} path={[\"language\"]}>\n {(field) => (\n <Field\n orientation=\"responsive\"\n data-invalid={field.errors !== null}\n >\n <FieldContent>\n <FieldLabel htmlFor=\"form-formisch-select-language\">\n Spoken Language\n </FieldLabel>\n <FieldDescription>\n For best results, select the language you speak.\n </FieldDescription>\n {field.errors && (\n <FieldError\n errors={field.errors.map((message) => ({ message }))}\n />\n )}\n </FieldContent>\n <Select\n value={field.input ?? \"\"}\n onValueChange={(value) => field.onChange(value)}\n >\n <SelectTrigger\n id=\"form-formisch-select-language\"\n aria-invalid={field.errors !== null}\n className=\"min-w-[120px]\"\n >\n <SelectValue placeholder=\"Select\" />\n </SelectTrigger>\n <SelectContent position=\"item-aligned\">\n <SelectItem value=\"auto\">Auto</SelectItem>\n <SelectSeparator />\n {spokenLanguages.map((language) => (\n <SelectItem key={language.value} value={language.value}>\n {language.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </Field>\n )}\n </FormischField>\n </FieldGroup>\n </Form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => reset(form)}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-formisch-select\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
|
||||
"type": "registry:example"
|
||||
}
|
||||
],
|
||||
"type": "registry:example"
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "form-formisch-switch",
|
||||
"dependencies": [
|
||||
"@formisch/react",
|
||||
"valibot"
|
||||
],
|
||||
"registryDependencies": [
|
||||
"field",
|
||||
"switch",
|
||||
"button",
|
||||
"card"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-formisch-switch.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Form, Field as FormischField, reset, useForm } from \"@formisch/react\"\nimport type { SubmitHandler } from \"@formisch/react\"\nimport { toast } from \"sonner\"\nimport * as v from \"valibot\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldContent,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Switch } from \"@/registry/new-york-v4/ui/switch\"\n\nconst FormSchema = v.object({\n twoFactor: v.pipe(\n v.boolean(),\n v.check(\n (value) => value === true,\n \"It is highly recommended to enable two-factor authentication.\"\n )\n ),\n})\n\nexport default function FormFormischSwitch() {\n const form = useForm({\n schema: FormSchema,\n initialInput: {\n twoFactor: false,\n },\n })\n\n const handleSubmit: SubmitHandler<typeof FormSchema> = (output) => {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"mt-2 w-[320px] overflow-x-auto rounded-md bg-code p-4 text-code-foreground\">\n <code>{JSON.stringify(output, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n }\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Security Settings</CardTitle>\n <CardDescription>\n Manage your account security preferences.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <Form of={form} id=\"form-formisch-switch\" onSubmit={handleSubmit}>\n <FieldGroup>\n <FormischField of={form} path={[\"twoFactor\"]}>\n {(field) => (\n <Field\n orientation=\"horizontal\"\n data-invalid={field.errors !== null}\n >\n <FieldContent>\n <FieldLabel htmlFor=\"form-formisch-switch-twoFactor\">\n Multi-factor authentication\n </FieldLabel>\n <FieldDescription>\n Enable multi-factor authentication to secure your account.\n </FieldDescription>\n {field.errors && (\n <FieldError\n errors={field.errors.map((message) => ({ message }))}\n />\n )}\n </FieldContent>\n <Switch\n id=\"form-formisch-switch-twoFactor\"\n checked={field.input ?? false}\n onCheckedChange={(checked) => field.onChange(checked)}\n aria-invalid={field.errors !== null}\n />\n </Field>\n )}\n </FormischField>\n </FieldGroup>\n </Form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => reset(form)}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-formisch-switch\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
|
||||
"type": "registry:example"
|
||||
}
|
||||
],
|
||||
"type": "registry:example"
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "form-formisch-textarea",
|
||||
"dependencies": [
|
||||
"@formisch/react",
|
||||
"valibot"
|
||||
],
|
||||
"registryDependencies": [
|
||||
"field",
|
||||
"textarea",
|
||||
"button",
|
||||
"card"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-formisch-textarea.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Form, Field as FormischField, reset, useForm } from \"@formisch/react\"\nimport type { SubmitHandler } from \"@formisch/react\"\nimport { toast } from \"sonner\"\nimport * as v from \"valibot\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/new-york-v4/ui/card\"\nimport {\n Field,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/registry/new-york-v4/ui/field\"\nimport { Textarea } from \"@/registry/new-york-v4/ui/textarea\"\n\nconst FormSchema = v.object({\n about: v.pipe(\n v.string(),\n v.minLength(10, \"Please provide at least 10 characters.\"),\n v.maxLength(200, \"Please keep it under 200 characters.\")\n ),\n})\n\nexport default function FormFormischTextarea() {\n const form = useForm({\n schema: FormSchema,\n initialInput: {\n about: \"\",\n },\n })\n\n const handleSubmit: SubmitHandler<typeof FormSchema> = (output) => {\n toast(\"You submitted the following values:\", {\n description: (\n <pre className=\"mt-2 w-[320px] overflow-x-auto rounded-md bg-code p-4 text-code-foreground\">\n <code>{JSON.stringify(output, null, 2)}</code>\n </pre>\n ),\n position: \"bottom-right\",\n classNames: {\n content: \"flex flex-col gap-2\",\n },\n style: {\n \"--border-radius\": \"calc(var(--radius) + 4px)\",\n } as React.CSSProperties,\n })\n }\n\n return (\n <Card className=\"w-full sm:max-w-md\">\n <CardHeader>\n <CardTitle>Personalization</CardTitle>\n <CardDescription>\n Customize your experience by telling us more about yourself.\n </CardDescription>\n </CardHeader>\n <CardContent>\n <Form of={form} id=\"form-formisch-textarea\" onSubmit={handleSubmit}>\n <FieldGroup>\n <FormischField of={form} path={[\"about\"]}>\n {(field) => (\n <Field data-invalid={field.errors !== null}>\n <FieldLabel htmlFor=\"form-formisch-textarea-about\">\n More about you\n </FieldLabel>\n <Textarea\n {...field.props}\n id=\"form-formisch-textarea-about\"\n value={field.input ?? \"\"}\n aria-invalid={field.errors !== null}\n placeholder=\"I'm a software engineer...\"\n className=\"min-h-[120px]\"\n />\n <FieldDescription>\n Tell us more about yourself. This will be used to help us\n personalize your experience.\n </FieldDescription>\n {field.errors && (\n <FieldError\n errors={field.errors.map((message) => ({ message }))}\n />\n )}\n </Field>\n )}\n </FormischField>\n </FieldGroup>\n </Form>\n </CardContent>\n <CardFooter>\n <Field orientation=\"horizontal\">\n <Button type=\"button\" variant=\"outline\" onClick={() => reset(form)}>\n Reset\n </Button>\n <Button type=\"submit\" form=\"form-formisch-textarea\">\n Save\n </Button>\n </Field>\n </CardFooter>\n </Card>\n )\n}\n",
|
||||
"type": "registry:example"
|
||||
}
|
||||
],
|
||||
"type": "registry:example"
|
||||
}
|
||||
@@ -4011,134 +4011,6 @@
|
||||
],
|
||||
"type": "registry:example"
|
||||
},
|
||||
{
|
||||
"name": "form-formisch-demo",
|
||||
"dependencies": ["@formisch/react", "valibot"],
|
||||
"registryDependencies": [
|
||||
"field",
|
||||
"input",
|
||||
"input-group",
|
||||
"button",
|
||||
"card"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-formisch-demo.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
],
|
||||
"type": "registry:example"
|
||||
},
|
||||
{
|
||||
"name": "form-formisch-input",
|
||||
"dependencies": ["@formisch/react", "valibot"],
|
||||
"registryDependencies": ["field", "input", "button", "card"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-formisch-input.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
],
|
||||
"type": "registry:example"
|
||||
},
|
||||
{
|
||||
"name": "form-formisch-textarea",
|
||||
"dependencies": ["@formisch/react", "valibot"],
|
||||
"registryDependencies": ["field", "textarea", "button", "card"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-formisch-textarea.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
],
|
||||
"type": "registry:example"
|
||||
},
|
||||
{
|
||||
"name": "form-formisch-select",
|
||||
"dependencies": ["@formisch/react", "valibot"],
|
||||
"registryDependencies": ["field", "select", "button", "card"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-formisch-select.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
],
|
||||
"type": "registry:example"
|
||||
},
|
||||
{
|
||||
"name": "form-formisch-checkbox",
|
||||
"dependencies": ["@formisch/react", "valibot"],
|
||||
"registryDependencies": ["field", "checkbox", "button", "card"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-formisch-checkbox.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
],
|
||||
"type": "registry:example"
|
||||
},
|
||||
{
|
||||
"name": "form-formisch-radiogroup",
|
||||
"dependencies": ["@formisch/react", "valibot"],
|
||||
"registryDependencies": ["field", "radio-group", "button", "card"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-formisch-radiogroup.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
],
|
||||
"type": "registry:example"
|
||||
},
|
||||
{
|
||||
"name": "form-formisch-switch",
|
||||
"dependencies": ["@formisch/react", "valibot"],
|
||||
"registryDependencies": ["field", "switch", "button", "card"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-formisch-switch.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
],
|
||||
"type": "registry:example"
|
||||
},
|
||||
{
|
||||
"name": "form-formisch-array",
|
||||
"dependencies": ["@formisch/react", "valibot"],
|
||||
"registryDependencies": [
|
||||
"field",
|
||||
"input",
|
||||
"input-group",
|
||||
"button",
|
||||
"card"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-formisch-array.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
],
|
||||
"type": "registry:example"
|
||||
},
|
||||
{
|
||||
"name": "form-formisch-complex",
|
||||
"dependencies": ["@formisch/react", "valibot"],
|
||||
"registryDependencies": [
|
||||
"field",
|
||||
"button",
|
||||
"card",
|
||||
"checkbox",
|
||||
"radio-group",
|
||||
"select",
|
||||
"switch"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/examples/form-formisch-complex.tsx",
|
||||
"type": "registry:example"
|
||||
}
|
||||
],
|
||||
"type": "registry:example"
|
||||
},
|
||||
{
|
||||
"name": "drawer-dialog",
|
||||
"registryDependencies": ["drawer", "dialog"],
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/radix-luma/ui/toggle-group.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { type VariantProps } from \"class-variance-authority\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"radix-ui\"\n\nimport { cn } from \"@/registry/radix-luma/lib/utils\"\nimport { toggleVariants } from \"@/registry/radix-luma/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 2,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 2,\n orientation = \"horizontal\",\n children,\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }) {\n return (\n <ToggleGroupPrimitive.Root\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] data-[spacing=0]:data-[variant=outline]:rounded-3xl data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider\n value={{ variant, size, spacing, orientation }}\n >\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive.Root>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &\n VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <ToggleGroupPrimitive.Item\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-3 group-data-[spacing=0]/toggle-group:shadow-none focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-2.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-2.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-3xl group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-3xl group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-3xl group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-3xl data-[state=on]:bg-muted group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </ToggleGroupPrimitive.Item>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { type VariantProps } from \"class-variance-authority\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"radix-ui\"\n\nimport { cn } from \"@/registry/radix-luma/lib/utils\"\nimport { toggleVariants } from \"@/registry/radix-luma/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 0,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 0,\n orientation = \"horizontal\",\n children,\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }) {\n return (\n <ToggleGroupPrimitive.Root\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] data-[spacing=0]:data-[variant=outline]:rounded-3xl data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider\n value={{ variant, size, spacing, orientation }}\n >\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive.Root>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &\n VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <ToggleGroupPrimitive.Item\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-3 group-data-[spacing=0]/toggle-group:shadow-none focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-2.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-2.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-3xl group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-3xl group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-3xl group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-3xl data-[state=on]:bg-muted group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </ToggleGroupPrimitive.Item>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/radix-lyra/ui/toggle-group.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { type VariantProps } from \"class-variance-authority\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"radix-ui\"\n\nimport { cn } from \"@/registry/radix-lyra/lib/utils\"\nimport { toggleVariants } from \"@/registry/radix-lyra/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 2,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 2,\n orientation = \"horizontal\",\n children,\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }) {\n return (\n <ToggleGroupPrimitive.Root\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-none data-[size=sm]:rounded-none data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider\n value={{ variant, size, spacing, orientation }}\n >\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive.Root>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &\n VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <ToggleGroupPrimitive.Item\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-none group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-none group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-none group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-none group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </ToggleGroupPrimitive.Item>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { type VariantProps } from \"class-variance-authority\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"radix-ui\"\n\nimport { cn } from \"@/registry/radix-lyra/lib/utils\"\nimport { toggleVariants } from \"@/registry/radix-lyra/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 0,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 0,\n orientation = \"horizontal\",\n children,\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }) {\n return (\n <ToggleGroupPrimitive.Root\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-none data-[size=sm]:rounded-none data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider\n value={{ variant, size, spacing, orientation }}\n >\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive.Root>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &\n VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <ToggleGroupPrimitive.Item\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-none group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-none group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-none group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-none group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </ToggleGroupPrimitive.Item>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/radix-maia/ui/toggle-group.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { type VariantProps } from \"class-variance-authority\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"radix-ui\"\n\nimport { cn } from \"@/registry/radix-maia/lib/utils\"\nimport { toggleVariants } from \"@/registry/radix-maia/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 2,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 2,\n orientation = \"horizontal\",\n children,\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }) {\n return (\n <ToggleGroupPrimitive.Root\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] data-[spacing=0]:data-[variant=outline]:rounded-4xl data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider\n value={{ variant, size, spacing, orientation }}\n >\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive.Root>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &\n VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <ToggleGroupPrimitive.Item\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-3 group-data-[spacing=0]/toggle-group:shadow-none focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-2.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-2.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-3xl group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-3xl group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-3xl group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-3xl data-[state=on]:bg-muted group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </ToggleGroupPrimitive.Item>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { type VariantProps } from \"class-variance-authority\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"radix-ui\"\n\nimport { cn } from \"@/registry/radix-maia/lib/utils\"\nimport { toggleVariants } from \"@/registry/radix-maia/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 0,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 0,\n orientation = \"horizontal\",\n children,\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }) {\n return (\n <ToggleGroupPrimitive.Root\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] data-[spacing=0]:data-[variant=outline]:rounded-4xl data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider\n value={{ variant, size, spacing, orientation }}\n >\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive.Root>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &\n VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <ToggleGroupPrimitive.Item\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-3 group-data-[spacing=0]/toggle-group:shadow-none focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-2.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-2.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-3xl group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-3xl group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-3xl group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-3xl data-[state=on]:bg-muted group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </ToggleGroupPrimitive.Item>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/radix-mira/ui/toggle-group.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { type VariantProps } from \"class-variance-authority\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"radix-ui\"\n\nimport { cn } from \"@/registry/radix-mira/lib/utils\"\nimport { toggleVariants } from \"@/registry/radix-mira/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 2,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 2,\n orientation = \"horizontal\",\n children,\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }) {\n return (\n <ToggleGroupPrimitive.Root\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-md data-[size=sm]:rounded-[min(var(--radius-md),8px)] data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider\n value={{ variant, size, spacing, orientation }}\n >\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive.Root>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &\n VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <ToggleGroupPrimitive.Item\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-md group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-md group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-md group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-md group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </ToggleGroupPrimitive.Item>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { type VariantProps } from \"class-variance-authority\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"radix-ui\"\n\nimport { cn } from \"@/registry/radix-mira/lib/utils\"\nimport { toggleVariants } from \"@/registry/radix-mira/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 0,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 0,\n orientation = \"horizontal\",\n children,\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }) {\n return (\n <ToggleGroupPrimitive.Root\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-md data-[size=sm]:rounded-[min(var(--radius-md),8px)] data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider\n value={{ variant, size, spacing, orientation }}\n >\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive.Root>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &\n VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <ToggleGroupPrimitive.Item\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-md group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-md group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-md group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-md group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </ToggleGroupPrimitive.Item>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/radix-nova/ui/toggle-group.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { type VariantProps } from \"class-variance-authority\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"radix-ui\"\n\nimport { cn } from \"@/registry/radix-nova/lib/utils\"\nimport { toggleVariants } from \"@/registry/radix-nova/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 2,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 2,\n orientation = \"horizontal\",\n children,\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }) {\n return (\n <ToggleGroupPrimitive.Root\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-lg data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider\n value={{ variant, size, spacing, orientation }}\n >\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive.Root>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &\n VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <ToggleGroupPrimitive.Item\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-lg group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-lg group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-lg group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-lg group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </ToggleGroupPrimitive.Item>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { type VariantProps } from \"class-variance-authority\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"radix-ui\"\n\nimport { cn } from \"@/registry/radix-nova/lib/utils\"\nimport { toggleVariants } from \"@/registry/radix-nova/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 0,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 0,\n orientation = \"horizontal\",\n children,\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }) {\n return (\n <ToggleGroupPrimitive.Root\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-lg data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider\n value={{ variant, size, spacing, orientation }}\n >\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive.Root>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &\n VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <ToggleGroupPrimitive.Item\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-lg group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-lg group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-lg group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-lg group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </ToggleGroupPrimitive.Item>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/radix-sera/ui/toggle-group.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { type VariantProps } from \"class-variance-authority\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"radix-ui\"\n\nimport { cn } from \"@/registry/radix-sera/lib/utils\"\nimport { toggleVariants } from \"@/registry/radix-sera/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 2,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 2,\n orientation = \"horizontal\",\n children,\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }) {\n return (\n <ToggleGroupPrimitive.Root\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] data-[spacing=0]:data-[variant=outline]:rounded-none data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider\n value={{ variant, size, spacing, orientation }}\n >\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive.Root>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &\n VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <ToggleGroupPrimitive.Item\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-6 group-data-[spacing=0]/toggle-group:shadow-none focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-4 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-4 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-none group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-none group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-none group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-none data-[state=on]:bg-muted data-[state=on]:text-foreground group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </ToggleGroupPrimitive.Item>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { type VariantProps } from \"class-variance-authority\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"radix-ui\"\n\nimport { cn } from \"@/registry/radix-sera/lib/utils\"\nimport { toggleVariants } from \"@/registry/radix-sera/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 0,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 0,\n orientation = \"horizontal\",\n children,\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }) {\n return (\n <ToggleGroupPrimitive.Root\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] data-[spacing=0]:data-[variant=outline]:rounded-none data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider\n value={{ variant, size, spacing, orientation }}\n >\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive.Root>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &\n VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <ToggleGroupPrimitive.Item\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-6 group-data-[spacing=0]/toggle-group:shadow-none focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-4 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-4 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-none group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-none group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-none group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-none data-[state=on]:bg-muted data-[state=on]:text-foreground group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </ToggleGroupPrimitive.Item>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/radix-vega/ui/toggle-group.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { type VariantProps } from \"class-variance-authority\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"radix-ui\"\n\nimport { cn } from \"@/registry/radix-vega/lib/utils\"\nimport { toggleVariants } from \"@/registry/radix-vega/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 2,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 2,\n orientation = \"horizontal\",\n children,\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }) {\n return (\n <ToggleGroupPrimitive.Root\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=0]:data-[variant=outline]:shadow-xs data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider\n value={{ variant, size, spacing, orientation }}\n >\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive.Root>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &\n VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <ToggleGroupPrimitive.Item\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 group-data-[spacing=0]/toggle-group:shadow-none focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-md group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-md group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-md group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-md data-[state=on]:bg-muted group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </ToggleGroupPrimitive.Item>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { type VariantProps } from \"class-variance-authority\"\nimport { ToggleGroup as ToggleGroupPrimitive } from \"radix-ui\"\n\nimport { cn } from \"@/registry/radix-vega/lib/utils\"\nimport { toggleVariants } from \"@/registry/radix-vega/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }\n>({\n size: \"default\",\n variant: \"default\",\n spacing: 0,\n orientation: \"horizontal\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n spacing = 0,\n orientation = \"horizontal\",\n children,\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &\n VariantProps<typeof toggleVariants> & {\n spacing?: number\n orientation?: \"horizontal\" | \"vertical\"\n }) {\n return (\n <ToggleGroupPrimitive.Root\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n style={{ \"--gap\": spacing } as React.CSSProperties}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=0]:data-[variant=outline]:shadow-xs data-vertical:flex-col data-vertical:items-stretch\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider\n value={{ variant, size, spacing, orientation }}\n >\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive.Root>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant = \"default\",\n size = \"default\",\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &\n VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <ToggleGroupPrimitive.Item\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n data-spacing={context.spacing}\n className={cn(\n \"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 group-data-[spacing=0]/toggle-group:shadow-none focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-md group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-md group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-md group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-md data-[state=on]:bg-muted group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t\",\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n className\n )}\n {...props}\n >\n {children}\n </ToggleGroupPrimitive.Item>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -3,31 +3,20 @@
|
||||
"description": "A shadcn registry of components, hooks, pages, etc.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "The registry name. Required when this file is used as the root registry, optional for included registry chunks.",
|
||||
"type": "string"
|
||||
},
|
||||
"homepage": {
|
||||
"description": "The registry homepage. Required when this file is used as the root registry, optional for included registry chunks.",
|
||||
"type": "string"
|
||||
},
|
||||
"include": {
|
||||
"type": "array",
|
||||
"description": "An array of relative paths to registry.json files to include in this registry.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "https://ui.shadcn.com/schema/registry-item.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"anyOf": [{ "required": ["items"] }, { "required": ["include"] }]
|
||||
"required": ["name", "homepage", "items"],
|
||||
"uniqueItems": true,
|
||||
"minItems": 1
|
||||
}
|
||||
|
||||
@@ -7431,257 +7431,6 @@ export const Index: Record<string, Record<string, any>> = {
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"form-formisch-demo": {
|
||||
name: "form-formisch-demo",
|
||||
title: "undefined",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field", "input", "input-group", "button", "card"],
|
||||
files: [
|
||||
{
|
||||
path: "registry/new-york-v4/examples/form-formisch-demo.tsx",
|
||||
type: "registry:example",
|
||||
target: "",
|
||||
},
|
||||
],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import(
|
||||
"@/registry/new-york-v4/examples/form-formisch-demo"
|
||||
)
|
||||
const exportName =
|
||||
Object.keys(mod).find(
|
||||
(key) =>
|
||||
typeof mod[key] === "function" || typeof mod[key] === "object"
|
||||
) || "form-formisch-demo"
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"form-formisch-input": {
|
||||
name: "form-formisch-input",
|
||||
title: "undefined",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field", "input", "button", "card"],
|
||||
files: [
|
||||
{
|
||||
path: "registry/new-york-v4/examples/form-formisch-input.tsx",
|
||||
type: "registry:example",
|
||||
target: "",
|
||||
},
|
||||
],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import(
|
||||
"@/registry/new-york-v4/examples/form-formisch-input"
|
||||
)
|
||||
const exportName =
|
||||
Object.keys(mod).find(
|
||||
(key) =>
|
||||
typeof mod[key] === "function" || typeof mod[key] === "object"
|
||||
) || "form-formisch-input"
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"form-formisch-textarea": {
|
||||
name: "form-formisch-textarea",
|
||||
title: "undefined",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field", "textarea", "button", "card"],
|
||||
files: [
|
||||
{
|
||||
path: "registry/new-york-v4/examples/form-formisch-textarea.tsx",
|
||||
type: "registry:example",
|
||||
target: "",
|
||||
},
|
||||
],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import(
|
||||
"@/registry/new-york-v4/examples/form-formisch-textarea"
|
||||
)
|
||||
const exportName =
|
||||
Object.keys(mod).find(
|
||||
(key) =>
|
||||
typeof mod[key] === "function" || typeof mod[key] === "object"
|
||||
) || "form-formisch-textarea"
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"form-formisch-select": {
|
||||
name: "form-formisch-select",
|
||||
title: "undefined",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field", "select", "button", "card"],
|
||||
files: [
|
||||
{
|
||||
path: "registry/new-york-v4/examples/form-formisch-select.tsx",
|
||||
type: "registry:example",
|
||||
target: "",
|
||||
},
|
||||
],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import(
|
||||
"@/registry/new-york-v4/examples/form-formisch-select"
|
||||
)
|
||||
const exportName =
|
||||
Object.keys(mod).find(
|
||||
(key) =>
|
||||
typeof mod[key] === "function" || typeof mod[key] === "object"
|
||||
) || "form-formisch-select"
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"form-formisch-checkbox": {
|
||||
name: "form-formisch-checkbox",
|
||||
title: "undefined",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field", "checkbox", "button", "card"],
|
||||
files: [
|
||||
{
|
||||
path: "registry/new-york-v4/examples/form-formisch-checkbox.tsx",
|
||||
type: "registry:example",
|
||||
target: "",
|
||||
},
|
||||
],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import(
|
||||
"@/registry/new-york-v4/examples/form-formisch-checkbox"
|
||||
)
|
||||
const exportName =
|
||||
Object.keys(mod).find(
|
||||
(key) =>
|
||||
typeof mod[key] === "function" || typeof mod[key] === "object"
|
||||
) || "form-formisch-checkbox"
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"form-formisch-radiogroup": {
|
||||
name: "form-formisch-radiogroup",
|
||||
title: "undefined",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field", "radio-group", "button", "card"],
|
||||
files: [
|
||||
{
|
||||
path: "registry/new-york-v4/examples/form-formisch-radiogroup.tsx",
|
||||
type: "registry:example",
|
||||
target: "",
|
||||
},
|
||||
],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import(
|
||||
"@/registry/new-york-v4/examples/form-formisch-radiogroup"
|
||||
)
|
||||
const exportName =
|
||||
Object.keys(mod).find(
|
||||
(key) =>
|
||||
typeof mod[key] === "function" || typeof mod[key] === "object"
|
||||
) || "form-formisch-radiogroup"
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"form-formisch-switch": {
|
||||
name: "form-formisch-switch",
|
||||
title: "undefined",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field", "switch", "button", "card"],
|
||||
files: [
|
||||
{
|
||||
path: "registry/new-york-v4/examples/form-formisch-switch.tsx",
|
||||
type: "registry:example",
|
||||
target: "",
|
||||
},
|
||||
],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import(
|
||||
"@/registry/new-york-v4/examples/form-formisch-switch"
|
||||
)
|
||||
const exportName =
|
||||
Object.keys(mod).find(
|
||||
(key) =>
|
||||
typeof mod[key] === "function" || typeof mod[key] === "object"
|
||||
) || "form-formisch-switch"
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"form-formisch-array": {
|
||||
name: "form-formisch-array",
|
||||
title: "undefined",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field", "input", "input-group", "button", "card"],
|
||||
files: [
|
||||
{
|
||||
path: "registry/new-york-v4/examples/form-formisch-array.tsx",
|
||||
type: "registry:example",
|
||||
target: "",
|
||||
},
|
||||
],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import(
|
||||
"@/registry/new-york-v4/examples/form-formisch-array"
|
||||
)
|
||||
const exportName =
|
||||
Object.keys(mod).find(
|
||||
(key) =>
|
||||
typeof mod[key] === "function" || typeof mod[key] === "object"
|
||||
) || "form-formisch-array"
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"form-formisch-complex": {
|
||||
name: "form-formisch-complex",
|
||||
title: "undefined",
|
||||
description: "",
|
||||
type: "registry:example",
|
||||
registryDependencies: [
|
||||
"field",
|
||||
"button",
|
||||
"card",
|
||||
"checkbox",
|
||||
"radio-group",
|
||||
"select",
|
||||
"switch",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "registry/new-york-v4/examples/form-formisch-complex.tsx",
|
||||
type: "registry:example",
|
||||
target: "",
|
||||
},
|
||||
],
|
||||
component: React.lazy(async () => {
|
||||
const mod = await import(
|
||||
"@/registry/new-york-v4/examples/form-formisch-complex"
|
||||
)
|
||||
const exportName =
|
||||
Object.keys(mod).find(
|
||||
(key) =>
|
||||
typeof mod[key] === "function" || typeof mod[key] === "object"
|
||||
) || "form-formisch-complex"
|
||||
return { default: mod.default || mod[exportName] }
|
||||
}),
|
||||
categories: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
"drawer-dialog": {
|
||||
name: "drawer-dialog",
|
||||
title: "undefined",
|
||||
|
||||
@@ -16,7 +16,7 @@ const ToggleGroupContext = React.createContext<
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
spacing: 2,
|
||||
spacing: 0,
|
||||
orientation: "horizontal",
|
||||
})
|
||||
|
||||
@@ -24,7 +24,7 @@ function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
spacing = 2,
|
||||
spacing = 0,
|
||||
orientation = "horizontal",
|
||||
children,
|
||||
...props
|
||||
|
||||
@@ -15,7 +15,7 @@ const ToggleGroupContext = React.createContext<
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
spacing: 2,
|
||||
spacing: 0,
|
||||
orientation: "horizontal",
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
spacing = 2,
|
||||
spacing = 0,
|
||||
orientation = "horizontal",
|
||||
children,
|
||||
...props
|
||||
|
||||
@@ -4,7 +4,6 @@ import publicSchema from "../public/schema.json"
|
||||
import {
|
||||
buildPartialRegistryBase,
|
||||
buildRegistryBase,
|
||||
buildThemeForPreset,
|
||||
DEFAULT_CONFIG,
|
||||
designSystemConfigSchema,
|
||||
parseRegistryBaseParts,
|
||||
@@ -153,31 +152,6 @@ describe("buildRegistryBase", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("buildThemeForPreset", () => {
|
||||
it("builds a copyable registry theme item", () => {
|
||||
const result = buildThemeForPreset({
|
||||
...DEFAULT_CONFIG,
|
||||
baseColor: "taupe",
|
||||
theme: "taupe",
|
||||
chartColor: "taupe",
|
||||
menuAccent: "bold",
|
||||
radius: "large",
|
||||
})
|
||||
|
||||
expect(result).toMatchObject({
|
||||
$schema: "https://ui.shadcn.com/schema/registry-item.json",
|
||||
name: "taupe-taupe",
|
||||
type: "registry:theme",
|
||||
})
|
||||
expect(result.cssVars?.light?.radius).toBe("0.875rem")
|
||||
expect(result.cssVars?.light?.accent).toBe(result.cssVars?.light?.primary)
|
||||
expect(result.cssVars?.dark?.accent).toBe(result.cssVars?.dark?.primary)
|
||||
expect(result.cssVars?.light?.background).toBeDefined()
|
||||
expect(result.cssVars?.dark?.background).toBeDefined()
|
||||
expect(result.css).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("parseRegistryBaseParts", () => {
|
||||
it("returns undefined parts when only is omitted", () => {
|
||||
expect(parseRegistryBaseParts(null)).toEqual({
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
type IconLibrary,
|
||||
type IconLibraryName,
|
||||
} from "shadcn/icons"
|
||||
import { registryItemSchema, type RegistryItem } from "shadcn/schema"
|
||||
import { type RegistryItem } from "shadcn/schema"
|
||||
import { z } from "zod"
|
||||
|
||||
import { BASE_COLORS, type BaseColor } from "@/registry/base-colors"
|
||||
@@ -13,7 +13,6 @@ import { STYLES, type Style } from "@/registry/styles"
|
||||
import { THEMES, type Theme } from "@/registry/themes"
|
||||
|
||||
const SHADCN_VERSION = "latest"
|
||||
const DEFAULT_RADIUS_VALUE = "0.625rem"
|
||||
|
||||
export { BASES, type Base }
|
||||
export { STYLES, type Style }
|
||||
@@ -578,28 +577,6 @@ export function buildRegistryTheme(config: DesignSystemConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
export function buildThemeForPreset(config: DesignSystemConfig) {
|
||||
const registryTheme = buildRegistryTheme(config)
|
||||
const radius = RADII.find((r) => r.name === config.radius)
|
||||
const radiusValue =
|
||||
config.radius === "default"
|
||||
? (registryTheme.cssVars?.light?.radius ?? DEFAULT_RADIUS_VALUE)
|
||||
: (radius?.value ?? registryTheme.cssVars?.light?.radius)
|
||||
|
||||
return registryItemSchema.parse({
|
||||
$schema: "https://ui.shadcn.com/schema/registry-item.json",
|
||||
name: registryTheme.name,
|
||||
type: "registry:theme",
|
||||
cssVars: {
|
||||
...registryTheme.cssVars,
|
||||
light: {
|
||||
...registryTheme.cssVars.light,
|
||||
...(radiusValue && { radius: radiusValue }),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Builds a registry:base item from a design system config.
|
||||
export function buildRegistryBase(config: DesignSystemConfig) {
|
||||
const baseItem = getBase(config.base)
|
||||
|
||||
@@ -160,13 +160,6 @@
|
||||
"description": "Neubrutalism component library with 43 components, 42 SVG shapes, thick borders, and hard shadows. Supports React, Vue, and Nuxt. Built on shadcn/ui.",
|
||||
"logo": "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' fill='none'><rect x='5' y='5' width='90' height='90' fill='var(--background)' stroke='var(--foreground)' stroke-width='6'/><rect x='20' y='20' width='25' height='25' fill='var(--foreground)'/><rect x='55' y='20' width='25' height='25' fill='var(--foreground)'/><rect x='20' y='55' width='25' height='25' fill='var(--foreground)'/><rect x='55' y='55' width='25' height='25' fill='var(--foreground)'/></svg>"
|
||||
},
|
||||
{
|
||||
"name": "@bklit",
|
||||
"homepage": "https://ui.bklit.com",
|
||||
"url": "https://ui.bklit.com/r/{name}.json",
|
||||
"description": "Open-source composable chart components for React — line, area, bar, pie, radar, maps, and more. Built with Visx, Motion, and shadcn/ui.",
|
||||
"logo": "<svg class='h-auto max-h-full w-full max-w-full text-foreground' fill='none' viewBox='0 0 112 179' xmlns='http://www.w3.org/2000/svg'><title>Bklit Logo</title><path d='M11.082 88.6498C11.082 88.6498 24.4884 66.4873 55.407 66.4873C84.2319 66.4873 99.732 88.6498 99.732 88.6498C99.732 88.6498 86.1801 110.813 55.407 110.812C26.7612 110.812 11.082 88.6498 11.082 88.6498Z' fill='url(#bklit-paint0_radial_375_479)'></path><g style='mix-blend-mode:normal'><path d='M11.082 88.6498C11.082 88.6498 24.4884 66.4873 55.407 66.4873C84.2319 66.4873 99.732 88.6498 99.732 88.6498C99.732 88.6498 86.1801 110.813 55.407 110.812C26.7612 110.812 11.082 88.6498 11.082 88.6498Z' fill='url(#bklit-paint1_radial)'></path></g><g style='mix-blend-mode:normal'><path d='M110.812 121.894C110.812 148.67 90.0415 177.3 55.4062 177.3V110.812C55.4062 110.812 83.2175 112.19 99.7312 88.6499C108.153 99.9149 110.812 111.675 110.812 121.894Z' fill='url(#bklit-paint2_linear)'></path></g><g style='mix-blend-mode:normal'><path d='M0 121.894C0 148.67 20.771 177.3 55.4062 177.3V110.812C55.4062 110.812 29.4726 112.262 11.0813 88.6499C2.65942 99.9149 0 111.675 0 121.894Z' fill='url(#bklit-paint3_linear)'></path></g><g style='mix-blend-mode:normal'><path d='M0 55.4061C0 28.6295 20.771 0 55.4063 0L55.4063 66.4874C55.4063 66.4874 27.595 65.1102 11.0813 88.6499C2.65942 77.3849 0 65.6252 0 55.4061Z' fill='url(#bklit-paint4_linear)'></path></g><g style='mix-blend-mode:normal'><path d='M110.812 55.4062C110.813 28.6295 90.0415 0 55.4063 0L55.4063 66.4874C55.4063 66.4874 81.5638 65.1362 99.7312 88.6499C108.153 77.3849 110.812 65.6252 110.812 55.4062Z' fill='url(#bklit-paint5_linear)'></path></g><defs><radialGradient id='bklit-paint0_radial_375_479' cx='50%' cy='50%' r='50%' gradientUnits='objectBoundingBox'><stop offset='0' stop-color='currentColor'></stop><stop offset='1' stop-color='currentColor' stop-opacity='0.75'></stop></radialGradient><linearGradient gradientUnits='objectBoundingBox' id='bklit-paint1_radial' x1='0' x2='1' y1='0' y2='0'><stop offset='0' stop-color='rgba(255, 255, 255)'></stop><stop offset='0.5' stop-color='rgba(255, 255, 255, 0.8)'></stop><stop offset='1' stop-color='rgba(255, 255, 255)'></stop></linearGradient><linearGradient gradientUnits='userSpaceOnUse' id='bklit-paint2_linear' x1='55.4062' x2='110.813' y1='177.3' y2='66.6642'><stop stop-color='currentColor'></stop><stop offset='1' stop-color='rgba(255, 255, 255, 0.5)'></stop></linearGradient><linearGradient gradientUnits='userSpaceOnUse' id='bklit-paint3_linear' x1='55.4063' x2='0' y1='177.3' y2='66.6642'><stop stop-color='currentColor' stop-opacity='0'></stop><stop offset='0.745192' stop-color='rgba(120,120,120, 0.5)'></stop></linearGradient><linearGradient gradientUnits='userSpaceOnUse' id='bklit-paint4_linear' x1='55.4063' x2='0' y1='0' y2='110.636'><stop stop-color='currentColor'></stop><stop offset='1' stop-color='rgba(255, 255, 255, 0.5)'></stop></linearGradient><linearGradient gradientUnits='userSpaceOnUse' id='bklit-paint5_linear' x1='55.4063' x2='110.813' y1='0' y2='110.636'><stop stop-color='currentColor' stop-opacity='0'></stop><stop offset='0.75' stop-color='rgba(120,120,120, 0.5)'></stop></linearGradient></defs></svg>"
|
||||
},
|
||||
{
|
||||
"name": "@bundui",
|
||||
"homepage": "https://bundui.io",
|
||||
@@ -216,13 +209,6 @@
|
||||
"description": "Shadcn UI Blocks for Ecommerce websites",
|
||||
"logo": "<svg width='400' height='400' viewBox='0 0 298 347' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M149 13L291.028 95V259L149 341L6.97183 259V95L149 13Z' fill='var(--background)'/><path d='M132.82 9.99006L21.54 77.1701C11.62 83.1601 5.5 94.2301 5.5 106.21V240.56C5.5 252.54 11.61 263.61 21.54 269.6L132.82 336.78C142.74 342.77 154.97 342.77 164.9 336.78L276.18 269.6C286.1 263.61 292.22 252.54 292.22 240.56V106.21C292.22 94.2301 286.11 83.1601 276.18 77.1701L164.89 9.99006C154.97 4.00006 142.74 4.00006 132.81 9.99006H132.82ZM132.82 313.69L40.67 258.06C30.75 252.07 24.63 241 24.63 229.02V117.76C24.63 105.78 30.74 94.7101 40.67 88.7201L132.82 33.0901C142.74 27.1001 154.97 27.1001 164.9 33.0901L257.05 88.7201C266.97 94.7101 273.09 105.78 273.09 117.76V229.02C273.09 241 266.98 252.07 257.05 258.06L164.9 313.69C154.98 319.68 142.75 319.68 132.82 313.69Z' fill='var(--foreground)' stroke='var(--foreground)' stroke-width='11' stroke-miterlimit='10'/><path d='M66.38 239.42C61.04 239.61 55.96 236.91 53.12 232.38C48.73 225.37 50.87 216.09 57.88 211.71L223.9 107.87C226.13 106.47 228.7 105.69 231.31 105.6C236.65 105.41 241.73 108.11 244.57 112.64C246.7 116.04 247.37 120.06 246.47 123.96C245.57 127.86 243.2 131.18 239.81 133.31L73.79 237.15C71.56 238.55 68.99 239.33 66.38 239.42Z' fill='var(--foreground)'/><path d='M226.632 112.041L60.4685 215.992C55.8285 218.895 54.4202 225.01 57.323 229.65L57.4184 229.802C60.3212 234.442 66.4358 235.85 71.0757 232.948L237.239 128.996C241.879 126.094 243.287 119.979 240.384 115.339L240.289 115.187C237.386 110.547 231.271 109.138 226.632 112.041Z' fill='var(--foreground)'/><path d='M138.74 271.19C133.86 271.23 129.3 268.48 126.83 264.01C124.45 259.71 124.28 254.47 126.53 249.83C127.72 247.38 129.63 245.42 131.87 244.03L214.37 192.67C216.44 191.38 218.8 190.69 221.19 190.67C226.07 190.63 230.63 193.39 233.1 197.85C234.95 201.19 235.46 205.1 234.55 208.86C233.63 212.61 231.39 215.76 228.24 217.72L145.57 269.19C143.5 270.48 141.14 271.17 138.75 271.19H138.74Z' fill='var(--foreground)'/><path d='M130.8 261.64L130.72 261.5C128.19 256.93 129.63 251.05 133.92 248.38L216.76 196.81C221.06 194.13 226.59 195.67 229.13 200.24L229.21 200.38C231.74 204.95 230.3 210.83 226.01 213.5L143.17 265.07C138.87 267.75 133.34 266.21 130.8"
|
||||
},
|
||||
{
|
||||
"name": "@corr",
|
||||
"homepage": "https://ui.corr.sh",
|
||||
"url": "https://ui.corr.sh/r/{name}.json",
|
||||
"description": "A collection of shadcn-based React components, charts, animated components, and blocks built over time.",
|
||||
"logo": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 64\"><rect fill=\"#111111\" x=\"6\" y=\"6\" width=\"52\" height=\"52\" rx=\"14\"/><path fill=\"none\" stroke=\"#ffffff\" stroke-width=\"4.5\" stroke-linejoin=\"round\" stroke-linecap=\"round\" d=\"M32 17 45 24.5v15L32 47 19 39.5v-15L32 17Z\"/><path fill=\"none\" stroke=\"#ffffff\" stroke-width=\"4.5\" stroke-linejoin=\"round\" stroke-linecap=\"round\" d=\"M19 24.5 32 32l13-7.5\"/><path fill=\"none\" stroke=\"#ffffff\" stroke-width=\"4.5\" stroke-linejoin=\"round\" stroke-linecap=\"round\" d=\"M32 32v15\"/></svg>"
|
||||
},
|
||||
{
|
||||
"name": "@coss",
|
||||
"homepage": "https://coss.com/ui",
|
||||
@@ -352,7 +338,7 @@
|
||||
},
|
||||
{
|
||||
"name": "@glasscn",
|
||||
"homepage": "https://glasscn-components.vercel.app/",
|
||||
"homepage": "https://glasscn.vercel.app/",
|
||||
"url": "https://glasscn-components.vercel.app/r/{name}.json",
|
||||
"description": "A shadcn-compatible registry of glassmorphism components inspired by Apple",
|
||||
"logo": "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 80 80' fill='none'><rect x='0.5' y='0.5' width='51' height='51' rx='13.5' fill='var(--background)' fill-opacity='.08' stroke='var(--foreground)' stroke-opacity='.2'/><rect x='14.5' y='14.5' width='51' height='51' rx='13.5' fill='var(--background)' fill-opacity='.12' stroke='var(--foreground)' stroke-opacity='.3'/><rect x='28.5' y='28.5' width='51' height='51' rx='13.5' fill='var(--background)' fill-opacity='.18' stroke='var(--foreground)' stroke-opacity='.45'/><line x1='34' y1='36' x2='34' y2='56' stroke='var(--foreground)' stroke-opacity='.35' stroke-width='1.5' stroke-linecap='round'/></svg>"
|
||||
@@ -536,7 +522,7 @@
|
||||
"name": "@ncdai",
|
||||
"homepage": "https://chanhdai.com/components",
|
||||
"url": "https://chanhdai.com/r/{name}.json",
|
||||
"description": "Pixel-perfect, uniquely crafted.",
|
||||
"description": "A collection of reusable components.",
|
||||
"logo": "<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 640 640'><path fill='var(--foreground)' d='M0 0h640v640H0z'/><path fill='var(--background)' d='M256 448H128v-64h128v64ZM512 256H384v128h128v64H320V192h192v64ZM128 384H64V256h64v128ZM576 384h-64V256h64v128ZM256 256H128v-64h128v64Z'/></svg>"
|
||||
},
|
||||
{
|
||||
@@ -609,13 +595,6 @@
|
||||
"description": "Animated GSAP components crafted for smooth interaction and rich detail.",
|
||||
"logo": "<svg xmlns='http://www.w3.org/2000/svg' width='512' height='512' fill='none' viewBox='0 0 512 512'><g clip-path='url(#clip0_832_289)'><path fill='var(--foregorund)' d='M256 0C51.2 0 0 51.2 0 256s51.2 256 256 256 256-51.2 256-256S460.8 0 256 0'/><path stroke='var(--background)' stroke-width='24' d='M381.631 89.102 156.848 226.934c-1.77 1.101-3.439 2.653-4.844 4.505s-2.5 3.94-3.177 6.062c-.673 2.062-.875 4.084-.584 5.83.288 1.658 1.077 2.946 2.257 3.686l98.286 32.478-122.2 137.576a7.9 7.9 0 0 0-1.306 2.72c-.234.895-.22 1.742.04 2.404q.369 1.032 1.341 1.214c.64.125 1.387-.039 2.127-.468l224.795-137.85c1.77-1.102 3.438-2.654 4.844-4.506 1.405-1.852 2.5-3.94 3.177-6.061.666-2.057.864-4.072.571-5.813-.282-1.663-1.066-2.957-2.245-3.704l-87.131-44.529L383.845 94.953a7.9 7.9 0 0 0 1.306-2.72c.233-.895.219-1.741-.04-2.404-.257-.632-.727-1.058-1.341-1.214-.641-.125-1.4.058-2.139.487Z'/></g></svg>"
|
||||
},
|
||||
{
|
||||
"name": "@paddle",
|
||||
"homepage": "https://developer.paddle.com/",
|
||||
"url": "https://developer.paddle.com/r/{name}.json",
|
||||
"description": "Drop-in components for building checkouts, pricing pages, and subscription management screens using Paddle Billing.",
|
||||
"logo": "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='none'><path fill='var(--foreground)' d='M28 0H4C1.79086 0 0 1.79086 0 4V28C0 30.2091 1.79086 32 4 32H28C30.2091 32 32 30.2091 32 28V4C32 1.79086 30.2091 0 28 0Z'/><path fill='var(--background)' d='M26 15.0174V16.9696C24.8052 16.9699 23.6222 17.2038 22.5186 17.6579C21.415 18.1119 20.4124 18.7773 19.5682 19.616C18.7239 20.4546 18.0546 21.4501 17.5985 22.5455C17.1423 23.641 16.9082 24.8148 16.9097 26H15.0903C15.0897 23.6086 14.1318 21.3154 12.4272 19.6244C10.7225 17.9334 8.41072 16.9832 6 16.9826V15.0304C7.19478 15.0301 8.37779 14.7962 9.48141 14.3421C10.585 13.8881 11.5876 13.2227 12.4318 12.384C13.2761 11.5453 13.9454 10.5499 14.4015 9.45445C14.8577 8.35904 15.0917 7.18519 15.0903 6H16.9097C16.9103 8.39137 17.8682 10.6846 19.5728 12.3756C21.2775 14.0666 23.5893 15.0168 26 15.0174Z'/></svg>"
|
||||
},
|
||||
{
|
||||
"name": "@pastecn",
|
||||
"homepage": "https://pastecn.com",
|
||||
@@ -903,13 +882,6 @@
|
||||
"description": "A component for building onboarding tours. Designed to integrate with shadcn/ui.",
|
||||
"logo": "<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='lucide lucide-binoculars-icon lucide-binoculars fill-none!'><path d='M10 10h4'/><path d='M19 7V4a1 1 0 0 0-1-1h-2a1 1 0 0 0-1 1v3'/><path d='M20 21a2 2 0 0 0 2-2v-3.851c0-1.39-2-2.962-2-4.829V8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v11a2 2 0 0 0 2 2z'/><path d='M 22 16 L 2 16'/><path d='M4 21a2 2 0 0 1-2-2v-3.851c0-1.39 2-2.962 2-4.829V8a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v11a2 2 0 0 1-2 2z'/><path d='M9 7V4a1 1 0 0 0-1-1H6a1 1 0 0 0-1 1v3'/></svg>"
|
||||
},
|
||||
{
|
||||
"name": "@trophy-ui",
|
||||
"homepage": "https://ui.trophy.so",
|
||||
"url": "https://ui.trophy.so/r/{name}.json",
|
||||
"description": "Open-source gamification UI components for streaks, achievements, leaderboards, points, and more. Built on shadcn/ui and Tailwind CSS.",
|
||||
"logo": "<svg width='32' height='51' viewBox='0 0 40 64' fill='none' xmlns='http://www.w3.org/2000/svg'><rect x='32' y='70.75' width='13' height='15' transform='rotate(180 28 64.75)' fill='var(--foreground)'/><rect x='32' y='55.75' width='13' height='13' transform='rotate(180 28 49.75)' fill='var(--foreground)'/><rect x='19' y='55.75' width='15' height='13' transform='rotate(180 15 49.75)' fill='var(--foreground)'/><rect x='53.5' y='49.25' width='13' height='15' transform='rotate(180 49.5 43.25)' fill='var(--foreground)'/><rect x='53.5' y='34.25' width='13' height='13' transform='rotate(180 49.5 28.25)' fill='var(--foreground)'/><rect x='40.5' y='34.25' width='15' height='13' transform='rotate(180 36.5 28.25)' fill='var(--foreground)'/></svg>"
|
||||
},
|
||||
{
|
||||
"name": "@uitripled",
|
||||
"homepage": "https://ui.tripled.work",
|
||||
@@ -1357,26 +1329,5 @@
|
||||
"url": "https://framecn.vercel.app/r/{name}.json",
|
||||
"description": "Beautiful videos, made simple. Ready to use, customizable video components for React.",
|
||||
"logo": "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='4'/><g stroke='currentColor' fill='none' stroke-linecap='round' stroke-linejoin='round'><path d='M6.72 4.36v2.36M27.96 25.6H25.6m-4.13 0H13.8c-3.337 0-5.007 0-6.043-1.037S6.72 21.857 6.72 18.52v-7.67M25.6 27.96v-11.8c0-4.45 0-6.675-1.383-8.057S20.61 6.72 16.16 6.72H4.36' stroke-width='1.77'/><path d='m21.824 16.16-5.664 5.664M20.691 9.93 9.93 20.69' stroke-width='2.266'/></g></svg>"
|
||||
},
|
||||
{
|
||||
"name": "@turbopills-ui",
|
||||
"homepage": "https://www.turbopills.com/ui/docs",
|
||||
"url": "https://ui.turbopills.com/r/{name}.json",
|
||||
"description": "Beautiful, accessible, and customizable React components for your telehealth applications.",
|
||||
"logo": "<svg xmlns='http://www.w3.org/2000/svg' version='1.0' width='597.000000pt' height='525.000000pt' viewBox='0 0 597.000000 525.000000' preserveAspectRatio='xMidYMid meet'><g transform='translate(0.000000,525.000000) scale(0.100000,-0.100000)' fill='var(--foreground, currentColor)' stroke='none'><path d='M1816 5223 l-1809 -3 6 -108 c24 -383 241 -748 579 -969 113 -74 205 -118 338 -162 178 -58 237 -64 678 -70 l404 -6 14 33 c56 136 213 367 364 538 283 320 747 605 1163 716 48 12 87 26 87 30 0 5 -3 7 -7 6 -5 -2 -822 -4 -1817 -5z'/><path d='M4630 5209 c-983 -101 -1863 -612 -2297 -1334 -249 -414 -394 -925 -462 -1625 -28 -287 -29 -310 -19 -304 4 3 8 12 8 20 0 19 87 192 163 324 413 718 1068 1274 1797 1522 228 78 466 118 703 118 506 0 811 110 1079 389 135 139 217 274 283 462 43 123 60 208 70 342 l7 97 -623 -1 c-343 -1 -662 -6 -709 -10z'/><path d='M3876 3584 c-339 -138 -733 -410 -1041 -719 -347 -347 -596 -694 -840 -1170 -188 -367 -356 -851 -435 -1250 -31 -157 -60 -344 -60 -389 l0 -39 183 7 c224 8 336 20 497 52 676 135 1179 580 1326 1174 19 74 56 257 83 405 94 515 222 1211 256 1390 50 268 97 545 93 555 -2 4 -29 -2 -62 -16z'/></g></svg>"
|
||||
},
|
||||
{
|
||||
"name": "@nexus-labs",
|
||||
"homepage": "https://nexus-ui.com",
|
||||
"url": "https://nexus-ui.com/r/{name}.json",
|
||||
"description": "Motion-native animated components for Next.js — backgrounds, heroes, inputs, carousels, and more. Open source. Copy-ready TypeScript.",
|
||||
"logo": "<svg width='32' height='32' viewBox='0 0 64 64' fill='none' xmlns='http://www.w3.org/2000/svg'><rect width='64' height='64' rx='12' fill='#0a0a0a'/><circle cx='32' cy='10' r='3.5' fill='white'/><circle cx='9' cy='52' r='3.5' fill='white'/><circle cx='55' cy='52' r='3.5' fill='white'/><path d='M32 13.5 L32 33 M12 51 L32 33 M52 51 L32 33' stroke='white' stroke-width='2.5' stroke-linecap='round'/><circle cx='32' cy='33' r='5.5' fill='oklch(0.72 0.18 250)'/><circle cx='32' cy='33' r='5.5' fill='none' stroke='white' stroke-width='1.25'/></svg>"
|
||||
},
|
||||
{
|
||||
"name": "@wensity",
|
||||
"homepage": "https://wensity.com",
|
||||
"url": "https://raw.githubusercontent.com/ksparth12/wensity-shadcn-registry/main/{name}.json",
|
||||
"description": "Motion-rich React components for AI interfaces, SaaS blocks, and cinematic interactions. Free Wensity components only.",
|
||||
"logo": "<svg width='512' height='512' viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'><rect width='512' height='512' rx='112' fill='var(--foreground)'/><path d='M112 152h58l36 160 43-160h49l43 160 36-160h58l-62 208h-62l-38-141-38 141h-62L112 152Z' fill='var(--background)'/></svg>"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1241,122 +1241,6 @@ export const examples: Registry["items"] = [
|
||||
],
|
||||
dependencies: ["@tanstack/react-form", "zod"],
|
||||
},
|
||||
{
|
||||
name: "form-formisch-demo",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field", "input", "input-group", "button", "card"],
|
||||
files: [
|
||||
{
|
||||
path: "examples/form-formisch-demo.tsx",
|
||||
type: "registry:example",
|
||||
},
|
||||
],
|
||||
dependencies: ["@formisch/react", "valibot"],
|
||||
},
|
||||
{
|
||||
name: "form-formisch-input",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field", "input", "button", "card"],
|
||||
files: [
|
||||
{
|
||||
path: "examples/form-formisch-input.tsx",
|
||||
type: "registry:example",
|
||||
},
|
||||
],
|
||||
dependencies: ["@formisch/react", "valibot"],
|
||||
},
|
||||
{
|
||||
name: "form-formisch-textarea",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field", "textarea", "button", "card"],
|
||||
files: [
|
||||
{
|
||||
path: "examples/form-formisch-textarea.tsx",
|
||||
type: "registry:example",
|
||||
},
|
||||
],
|
||||
dependencies: ["@formisch/react", "valibot"],
|
||||
},
|
||||
{
|
||||
name: "form-formisch-select",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field", "select", "button", "card"],
|
||||
files: [
|
||||
{
|
||||
path: "examples/form-formisch-select.tsx",
|
||||
type: "registry:example",
|
||||
},
|
||||
],
|
||||
dependencies: ["@formisch/react", "valibot"],
|
||||
},
|
||||
{
|
||||
name: "form-formisch-checkbox",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field", "checkbox", "button", "card"],
|
||||
files: [
|
||||
{
|
||||
path: "examples/form-formisch-checkbox.tsx",
|
||||
type: "registry:example",
|
||||
},
|
||||
],
|
||||
dependencies: ["@formisch/react", "valibot"],
|
||||
},
|
||||
{
|
||||
name: "form-formisch-radiogroup",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field", "radio-group", "button", "card"],
|
||||
files: [
|
||||
{
|
||||
path: "examples/form-formisch-radiogroup.tsx",
|
||||
type: "registry:example",
|
||||
},
|
||||
],
|
||||
dependencies: ["@formisch/react", "valibot"],
|
||||
},
|
||||
{
|
||||
name: "form-formisch-switch",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field", "switch", "button", "card"],
|
||||
files: [
|
||||
{
|
||||
path: "examples/form-formisch-switch.tsx",
|
||||
type: "registry:example",
|
||||
},
|
||||
],
|
||||
dependencies: ["@formisch/react", "valibot"],
|
||||
},
|
||||
{
|
||||
name: "form-formisch-array",
|
||||
type: "registry:example",
|
||||
registryDependencies: ["field", "input", "input-group", "button", "card"],
|
||||
files: [
|
||||
{
|
||||
path: "examples/form-formisch-array.tsx",
|
||||
type: "registry:example",
|
||||
},
|
||||
],
|
||||
dependencies: ["@formisch/react", "valibot"],
|
||||
},
|
||||
{
|
||||
name: "form-formisch-complex",
|
||||
type: "registry:example",
|
||||
registryDependencies: [
|
||||
"field",
|
||||
"button",
|
||||
"card",
|
||||
"checkbox",
|
||||
"radio-group",
|
||||
"select",
|
||||
"switch",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "examples/form-formisch-complex.tsx",
|
||||
type: "registry:example",
|
||||
},
|
||||
],
|
||||
dependencies: ["@formisch/react", "valibot"],
|
||||
},
|
||||
{
|
||||
name: "drawer-dialog",
|
||||
type: "registry:example",
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
FieldArray,
|
||||
Form,
|
||||
Field as FormischField,
|
||||
insert,
|
||||
remove,
|
||||
reset,
|
||||
useForm,
|
||||
} from "@formisch/react"
|
||||
import type { SubmitHandler } from "@formisch/react"
|
||||
import { XIcon } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
import * as v from "valibot"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
|
||||
const FormSchema = v.object({
|
||||
emails: v.pipe(
|
||||
v.array(
|
||||
v.object({
|
||||
address: v.pipe(
|
||||
v.string(),
|
||||
v.nonEmpty("Enter an email address."),
|
||||
v.email("Enter a valid email address.")
|
||||
),
|
||||
})
|
||||
),
|
||||
v.minLength(1, "Add at least one email address."),
|
||||
v.maxLength(5, "You can add up to 5 email addresses.")
|
||||
),
|
||||
})
|
||||
|
||||
export default function FormFormischArray() {
|
||||
const form = useForm({
|
||||
schema: FormSchema,
|
||||
initialInput: {
|
||||
emails: [{ address: "" }, { address: "" }],
|
||||
},
|
||||
})
|
||||
|
||||
const handleSubmit: SubmitHandler<typeof FormSchema> = (output) => {
|
||||
toast("You submitted the following values:", {
|
||||
description: (
|
||||
<pre className="mt-2 w-[320px] overflow-x-auto rounded-md bg-code p-4 text-code-foreground">
|
||||
<code>{JSON.stringify(output, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
position: "bottom-right",
|
||||
classNames: {
|
||||
content: "flex flex-col gap-2",
|
||||
},
|
||||
style: {
|
||||
"--border-radius": "calc(var(--radius) + 4px)",
|
||||
} as React.CSSProperties,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="w-full sm:max-w-md">
|
||||
<CardHeader className="border-b">
|
||||
<CardTitle>Contact Emails</CardTitle>
|
||||
<CardDescription>Manage your contact email addresses.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form of={form} id="form-formisch-array" onSubmit={handleSubmit}>
|
||||
<FieldArray of={form} path={["emails"]}>
|
||||
{(fieldArray) => (
|
||||
<FieldSet className="gap-4">
|
||||
<FieldLegend variant="label">Email Addresses</FieldLegend>
|
||||
<FieldDescription>
|
||||
Add up to 5 email addresses where we can contact you.
|
||||
</FieldDescription>
|
||||
<FieldGroup className="gap-4">
|
||||
{fieldArray.items.map((item, index) => (
|
||||
<FormischField
|
||||
key={item}
|
||||
of={form}
|
||||
path={["emails", index, "address"]}
|
||||
>
|
||||
{(field) => (
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
data-invalid={field.errors !== null}
|
||||
>
|
||||
<FieldContent>
|
||||
<InputGroup>
|
||||
<InputGroupInput
|
||||
{...field.props}
|
||||
id={`form-formisch-array-email-${index}`}
|
||||
value={field.input ?? ""}
|
||||
aria-invalid={field.errors !== null}
|
||||
placeholder="name@example.com"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
/>
|
||||
{fieldArray.items.length > 1 && (
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={() =>
|
||||
remove(form, {
|
||||
path: ["emails"],
|
||||
at: index,
|
||||
})
|
||||
}
|
||||
aria-label={`Remove email ${index + 1}`}
|
||||
>
|
||||
<XIcon />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
)}
|
||||
</InputGroup>
|
||||
{field.errors && (
|
||||
<FieldError
|
||||
errors={field.errors.map((message) => ({
|
||||
message,
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
</FieldContent>
|
||||
</Field>
|
||||
)}
|
||||
</FormischField>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
insert(form, {
|
||||
path: ["emails"],
|
||||
initialInput: { address: "" },
|
||||
})
|
||||
}
|
||||
disabled={fieldArray.items.length >= 5}
|
||||
>
|
||||
Add Email Address
|
||||
</Button>
|
||||
</FieldGroup>
|
||||
{fieldArray.errors && (
|
||||
<FieldError
|
||||
errors={fieldArray.errors.map((message) => ({ message }))}
|
||||
/>
|
||||
)}
|
||||
</FieldSet>
|
||||
)}
|
||||
</FieldArray>
|
||||
</Form>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t">
|
||||
<Field orientation="horizontal">
|
||||
<Button type="button" variant="outline" onClick={() => reset(form)}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button type="submit" form="form-formisch-array">
|
||||
Save
|
||||
</Button>
|
||||
</Field>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Form, Field as FormischField, reset, useForm } from "@formisch/react"
|
||||
import type { SubmitHandler } from "@formisch/react"
|
||||
import { toast } from "sonner"
|
||||
import * as v from "valibot"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
|
||||
const tasks = [
|
||||
{
|
||||
id: "push",
|
||||
label: "Push notifications",
|
||||
},
|
||||
{
|
||||
id: "email",
|
||||
label: "Email notifications",
|
||||
},
|
||||
] as const
|
||||
|
||||
const FormSchema = v.object({
|
||||
responses: v.boolean(),
|
||||
tasks: v.pipe(
|
||||
v.array(v.string()),
|
||||
v.minLength(1, "Please select at least one notification type."),
|
||||
v.check(
|
||||
(value) => value.every((task) => tasks.some((t) => t.id === task)),
|
||||
"Invalid notification type selected."
|
||||
)
|
||||
),
|
||||
})
|
||||
|
||||
export default function FormFormischCheckbox() {
|
||||
const form = useForm({
|
||||
schema: FormSchema,
|
||||
initialInput: {
|
||||
responses: true,
|
||||
tasks: [],
|
||||
},
|
||||
})
|
||||
|
||||
const handleSubmit: SubmitHandler<typeof FormSchema> = (output) => {
|
||||
toast("You submitted the following values:", {
|
||||
description: (
|
||||
<pre className="mt-2 w-[320px] overflow-x-auto rounded-md bg-code p-4 text-code-foreground">
|
||||
<code>{JSON.stringify(output, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
position: "bottom-right",
|
||||
classNames: {
|
||||
content: "flex flex-col gap-2",
|
||||
},
|
||||
style: {
|
||||
"--border-radius": "calc(var(--radius) + 4px)",
|
||||
} as React.CSSProperties,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="w-full sm:max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle>Notifications</CardTitle>
|
||||
<CardDescription>Manage your notification preferences.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form of={form} id="form-formisch-checkbox" onSubmit={handleSubmit}>
|
||||
<FieldGroup>
|
||||
<FormischField of={form} path={["responses"]}>
|
||||
{(field) => (
|
||||
<div>
|
||||
<FieldSet data-invalid={field.errors !== null}>
|
||||
<FieldLegend variant="label">Responses</FieldLegend>
|
||||
<FieldDescription>
|
||||
Get notified for requests that take time, like research or
|
||||
image generation.
|
||||
</FieldDescription>
|
||||
<FieldGroup data-slot="checkbox-group">
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox
|
||||
id="form-formisch-checkbox-responses"
|
||||
checked={field.input ?? false}
|
||||
onCheckedChange={(checked) =>
|
||||
field.onChange(checked === true)
|
||||
}
|
||||
disabled
|
||||
/>
|
||||
<FieldLabel
|
||||
htmlFor="form-formisch-checkbox-responses"
|
||||
className="font-normal"
|
||||
>
|
||||
Push notifications
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
{field.errors && (
|
||||
<FieldError
|
||||
errors={field.errors.map((message) => ({ message }))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</FormischField>
|
||||
<FieldSeparator />
|
||||
<FormischField of={form} path={["tasks"]}>
|
||||
{(field) => (
|
||||
<FieldGroup>
|
||||
<FieldSet data-invalid={field.errors !== null}>
|
||||
<FieldLegend variant="label">Tasks</FieldLegend>
|
||||
<FieldDescription>
|
||||
Get notified when tasks you've created have updates.
|
||||
</FieldDescription>
|
||||
<FieldGroup data-slot="checkbox-group">
|
||||
{tasks.map((task) => {
|
||||
const current = field.input ?? []
|
||||
return (
|
||||
<Field
|
||||
key={task.id}
|
||||
orientation="horizontal"
|
||||
data-invalid={field.errors !== null}
|
||||
>
|
||||
<Checkbox
|
||||
id={`form-formisch-checkbox-${task.id}`}
|
||||
aria-invalid={field.errors !== null}
|
||||
checked={current.includes(task.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
field.onChange(
|
||||
checked === true
|
||||
? [...current, task.id]
|
||||
: current.filter(
|
||||
(value) => value !== task.id
|
||||
)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldLabel
|
||||
htmlFor={`form-formisch-checkbox-${task.id}`}
|
||||
className="font-normal"
|
||||
>
|
||||
{task.label}
|
||||
</FieldLabel>
|
||||
</Field>
|
||||
)
|
||||
})}
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
{field.errors && (
|
||||
<FieldError
|
||||
errors={field.errors.map((message) => ({ message }))}
|
||||
/>
|
||||
)}
|
||||
</FieldGroup>
|
||||
)}
|
||||
</FormischField>
|
||||
</FieldGroup>
|
||||
</Form>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Field orientation="horizontal">
|
||||
<Button type="button" variant="outline" onClick={() => reset(form)}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button type="submit" form="form-formisch-checkbox">
|
||||
Save
|
||||
</Button>
|
||||
</Field>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,305 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Form, Field as FormischField, reset, useForm } from "@formisch/react"
|
||||
import type { SubmitHandler } from "@formisch/react"
|
||||
import { toast } from "sonner"
|
||||
import * as v from "valibot"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
|
||||
const addons = [
|
||||
{
|
||||
id: "analytics",
|
||||
title: "Analytics",
|
||||
description: "Advanced analytics and reporting",
|
||||
},
|
||||
{
|
||||
id: "backup",
|
||||
title: "Backup",
|
||||
description: "Automated daily backups",
|
||||
},
|
||||
{
|
||||
id: "support",
|
||||
title: "Priority Support",
|
||||
description: "24/7 premium customer support",
|
||||
},
|
||||
] as const
|
||||
|
||||
const FormSchema = v.object({
|
||||
plan: v.pipe(
|
||||
v.string(),
|
||||
v.minLength(1, "Please select a subscription plan"),
|
||||
v.check(
|
||||
(value) => value === "basic" || value === "pro",
|
||||
"Invalid plan selection. Please choose Basic or Pro"
|
||||
)
|
||||
),
|
||||
billingPeriod: v.pipe(
|
||||
v.string(),
|
||||
v.minLength(1, "Please select a billing period")
|
||||
),
|
||||
addons: v.pipe(
|
||||
v.array(v.string()),
|
||||
v.minLength(1, "Please select at least one add-on"),
|
||||
v.maxLength(3, "You can select up to 3 add-ons"),
|
||||
v.check(
|
||||
(value) => value.every((addon) => addons.some((a) => a.id === addon)),
|
||||
"You selected an invalid add-on"
|
||||
)
|
||||
),
|
||||
emailNotifications: v.boolean(),
|
||||
})
|
||||
|
||||
export default function FormFormischComplex() {
|
||||
const form = useForm({
|
||||
schema: FormSchema,
|
||||
initialInput: {
|
||||
plan: "basic",
|
||||
billingPeriod: "",
|
||||
addons: [],
|
||||
emailNotifications: false,
|
||||
},
|
||||
})
|
||||
|
||||
const handleSubmit: SubmitHandler<typeof FormSchema> = (output) => {
|
||||
toast("You submitted the following values:", {
|
||||
description: (
|
||||
<pre className="mt-2 w-[320px] overflow-x-auto rounded-md bg-code p-4 text-code-foreground">
|
||||
<code>{JSON.stringify(output, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
position: "bottom-right",
|
||||
classNames: {
|
||||
content: "flex flex-col gap-2",
|
||||
},
|
||||
style: {
|
||||
"--border-radius": "calc(var(--radius) + 4px)",
|
||||
} as React.CSSProperties,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-sm">
|
||||
<CardHeader className="border-b">
|
||||
<CardTitle>You're almost there!</CardTitle>
|
||||
<CardDescription>
|
||||
Choose your subscription plan and billing period.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form of={form} id="form-formisch-complex" onSubmit={handleSubmit}>
|
||||
<FieldGroup>
|
||||
<FormischField of={form} path={["plan"]}>
|
||||
{(field) => (
|
||||
<FieldSet data-invalid={field.errors !== null}>
|
||||
<FieldLegend variant="label">Subscription Plan</FieldLegend>
|
||||
<FieldDescription>
|
||||
Choose your subscription plan.
|
||||
</FieldDescription>
|
||||
<RadioGroup
|
||||
value={field.input ?? ""}
|
||||
onValueChange={(value) => field.onChange(value)}
|
||||
aria-invalid={field.errors !== null}
|
||||
>
|
||||
<FieldLabel htmlFor="form-formisch-complex-basic">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Basic</FieldTitle>
|
||||
<FieldDescription>
|
||||
For individuals and small teams
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value="basic"
|
||||
id="form-formisch-complex-basic"
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<FieldLabel htmlFor="form-formisch-complex-pro">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent>
|
||||
<FieldTitle>Pro</FieldTitle>
|
||||
<FieldDescription>
|
||||
For businesses with higher demands
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value="pro"
|
||||
id="form-formisch-complex-pro"
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
</RadioGroup>
|
||||
{field.errors && (
|
||||
<FieldError
|
||||
errors={field.errors.map((message) => ({ message }))}
|
||||
/>
|
||||
)}
|
||||
</FieldSet>
|
||||
)}
|
||||
</FormischField>
|
||||
<FieldSeparator />
|
||||
<FormischField of={form} path={["billingPeriod"]}>
|
||||
{(field) => (
|
||||
<Field data-invalid={field.errors !== null}>
|
||||
<FieldLabel htmlFor="form-formisch-complex-billingPeriod">
|
||||
Billing Period
|
||||
</FieldLabel>
|
||||
<Select
|
||||
value={field.input ?? ""}
|
||||
onValueChange={(value) => field.onChange(value)}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="form-formisch-complex-billingPeriod"
|
||||
aria-invalid={field.errors !== null}
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="monthly">Monthly</SelectItem>
|
||||
<SelectItem value="yearly">Yearly</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldDescription>
|
||||
Choose how often you want to be billed.
|
||||
</FieldDescription>
|
||||
{field.errors && (
|
||||
<FieldError
|
||||
errors={field.errors.map((message) => ({ message }))}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
</FormischField>
|
||||
<FieldSeparator />
|
||||
<FormischField of={form} path={["addons"]}>
|
||||
{(field) => {
|
||||
const current = field.input ?? []
|
||||
return (
|
||||
<FieldSet>
|
||||
<FieldLegend>Add-ons</FieldLegend>
|
||||
<FieldDescription>
|
||||
Select additional features you'd like to include.
|
||||
</FieldDescription>
|
||||
<FieldGroup data-slot="checkbox-group">
|
||||
{addons.map((addon) => (
|
||||
<Field
|
||||
key={addon.id}
|
||||
orientation="horizontal"
|
||||
data-invalid={field.errors !== null}
|
||||
>
|
||||
<Checkbox
|
||||
id={`form-formisch-complex-${addon.id}`}
|
||||
aria-invalid={field.errors !== null}
|
||||
checked={current.includes(addon.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
field.onChange(
|
||||
checked === true
|
||||
? [...current, addon.id]
|
||||
: current.filter(
|
||||
(value) => value !== addon.id
|
||||
)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldLabel
|
||||
htmlFor={`form-formisch-complex-${addon.id}`}
|
||||
>
|
||||
{addon.title}
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
{addon.description}
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
</Field>
|
||||
))}
|
||||
</FieldGroup>
|
||||
{field.errors && (
|
||||
<FieldError
|
||||
errors={field.errors.map((message) => ({ message }))}
|
||||
/>
|
||||
)}
|
||||
</FieldSet>
|
||||
)
|
||||
}}
|
||||
</FormischField>
|
||||
<FieldSeparator />
|
||||
<FormischField of={form} path={["emailNotifications"]}>
|
||||
{(field) => (
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
data-invalid={field.errors !== null}
|
||||
>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="form-formisch-complex-emailNotifications">
|
||||
Email Notifications
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Receive email updates about your subscription
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch
|
||||
id="form-formisch-complex-emailNotifications"
|
||||
checked={field.input ?? false}
|
||||
onCheckedChange={(checked) => field.onChange(checked)}
|
||||
aria-invalid={field.errors !== null}
|
||||
/>
|
||||
{field.errors && (
|
||||
<FieldError
|
||||
errors={field.errors.map((message) => ({ message }))}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
</FormischField>
|
||||
</FieldGroup>
|
||||
</Form>
|
||||
</CardContent>
|
||||
<CardFooter className="border-t">
|
||||
<Field>
|
||||
<Button type="submit" form="form-formisch-complex">
|
||||
Save Preferences
|
||||
</Button>
|
||||
<Button type="button" variant="outline" onClick={() => reset(form)}>
|
||||
Reset
|
||||
</Button>
|
||||
</Field>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Form, Field as FormischField, reset, useForm } from "@formisch/react"
|
||||
import type { SubmitHandler } from "@formisch/react"
|
||||
import { toast } from "sonner"
|
||||
import * as v from "valibot"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupText,
|
||||
InputGroupTextarea,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
|
||||
const FormSchema = v.object({
|
||||
title: v.pipe(
|
||||
v.string(),
|
||||
v.minLength(5, "Bug title must be at least 5 characters."),
|
||||
v.maxLength(32, "Bug title must be at most 32 characters.")
|
||||
),
|
||||
description: v.pipe(
|
||||
v.string(),
|
||||
v.minLength(20, "Description must be at least 20 characters."),
|
||||
v.maxLength(100, "Description must be at most 100 characters.")
|
||||
),
|
||||
})
|
||||
|
||||
export default function BugReportForm() {
|
||||
const form = useForm({
|
||||
schema: FormSchema,
|
||||
initialInput: {
|
||||
title: "",
|
||||
description: "",
|
||||
},
|
||||
})
|
||||
|
||||
const handleSubmit: SubmitHandler<typeof FormSchema> = (output) => {
|
||||
toast("You submitted the following values:", {
|
||||
description: (
|
||||
<pre className="mt-2 w-[320px] overflow-x-auto rounded-md bg-code p-4 text-code-foreground">
|
||||
<code>{JSON.stringify(output, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
position: "bottom-right",
|
||||
classNames: {
|
||||
content: "flex flex-col gap-2",
|
||||
},
|
||||
style: {
|
||||
"--border-radius": "calc(var(--radius) + 4px)",
|
||||
} as React.CSSProperties,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="w-full sm:max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle>Bug Report</CardTitle>
|
||||
<CardDescription>
|
||||
Help us improve by reporting bugs you encounter.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form of={form} id="form-formisch-demo" onSubmit={handleSubmit}>
|
||||
<FieldGroup>
|
||||
<FormischField of={form} path={["title"]}>
|
||||
{(field) => (
|
||||
<Field data-invalid={field.errors !== null}>
|
||||
<FieldLabel htmlFor="form-formisch-demo-title">
|
||||
Bug Title
|
||||
</FieldLabel>
|
||||
<Input
|
||||
{...field.props}
|
||||
id="form-formisch-demo-title"
|
||||
value={field.input ?? ""}
|
||||
aria-invalid={field.errors !== null}
|
||||
placeholder="Login button not working on mobile"
|
||||
autoComplete="off"
|
||||
/>
|
||||
{field.errors && (
|
||||
<FieldError
|
||||
errors={field.errors.map((message) => ({ message }))}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
</FormischField>
|
||||
<FormischField of={form} path={["description"]}>
|
||||
{(field) => (
|
||||
<Field data-invalid={field.errors !== null}>
|
||||
<FieldLabel htmlFor="form-formisch-demo-description">
|
||||
Description
|
||||
</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
{...field.props}
|
||||
id="form-formisch-demo-description"
|
||||
value={field.input ?? ""}
|
||||
placeholder="I'm having an issue with the login button on mobile."
|
||||
rows={6}
|
||||
className="min-h-24 resize-none"
|
||||
aria-invalid={field.errors !== null}
|
||||
/>
|
||||
<InputGroupAddon align="block-end">
|
||||
<InputGroupText className="tabular-nums">
|
||||
{(field.input ?? "").length}/100 characters
|
||||
</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<FieldDescription>
|
||||
Include steps to reproduce, expected behavior, and what
|
||||
actually happened.
|
||||
</FieldDescription>
|
||||
{field.errors && (
|
||||
<FieldError
|
||||
errors={field.errors.map((message) => ({ message }))}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
</FormischField>
|
||||
</FieldGroup>
|
||||
</Form>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Field orientation="horizontal">
|
||||
<Button type="button" variant="outline" onClick={() => reset(form)}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button type="submit" form="form-formisch-demo">
|
||||
Submit
|
||||
</Button>
|
||||
</Field>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Form, Field as FormischField, reset, useForm } from "@formisch/react"
|
||||
import type { SubmitHandler } from "@formisch/react"
|
||||
import { toast } from "sonner"
|
||||
import * as v from "valibot"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
|
||||
const FormSchema = v.object({
|
||||
username: v.pipe(
|
||||
v.string(),
|
||||
v.minLength(3, "Username must be at least 3 characters."),
|
||||
v.maxLength(10, "Username must be at most 10 characters."),
|
||||
v.regex(
|
||||
/^[a-zA-Z0-9_]+$/,
|
||||
"Username can only contain letters, numbers, and underscores."
|
||||
)
|
||||
),
|
||||
})
|
||||
|
||||
export default function FormFormischInput() {
|
||||
const form = useForm({
|
||||
schema: FormSchema,
|
||||
initialInput: {
|
||||
username: "",
|
||||
},
|
||||
})
|
||||
|
||||
const handleSubmit: SubmitHandler<typeof FormSchema> = (output) => {
|
||||
toast("You submitted the following values:", {
|
||||
description: (
|
||||
<pre className="mt-2 w-[320px] overflow-x-auto rounded-md bg-code p-4 text-code-foreground">
|
||||
<code>{JSON.stringify(output, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
position: "bottom-right",
|
||||
classNames: {
|
||||
content: "flex flex-col gap-2",
|
||||
},
|
||||
style: {
|
||||
"--border-radius": "calc(var(--radius) + 4px)",
|
||||
} as React.CSSProperties,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="w-full sm:max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle>Profile Settings</CardTitle>
|
||||
<CardDescription>
|
||||
Update your profile information below.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form of={form} id="form-formisch-input" onSubmit={handleSubmit}>
|
||||
<FieldGroup>
|
||||
<FormischField of={form} path={["username"]}>
|
||||
{(field) => (
|
||||
<Field data-invalid={field.errors !== null}>
|
||||
<FieldLabel htmlFor="form-formisch-input-username">
|
||||
Username
|
||||
</FieldLabel>
|
||||
<Input
|
||||
{...field.props}
|
||||
id="form-formisch-input-username"
|
||||
value={field.input ?? ""}
|
||||
aria-invalid={field.errors !== null}
|
||||
placeholder="shadcn"
|
||||
autoComplete="username"
|
||||
/>
|
||||
<FieldDescription>
|
||||
This is your public display name. Must be between 3 and 10
|
||||
characters. Must only contain letters, numbers, and
|
||||
underscores.
|
||||
</FieldDescription>
|
||||
{field.errors && (
|
||||
<FieldError
|
||||
errors={field.errors.map((message) => ({ message }))}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
</FormischField>
|
||||
</FieldGroup>
|
||||
</Form>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Field orientation="horizontal">
|
||||
<Button type="button" variant="outline" onClick={() => reset(form)}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button type="submit" form="form-formisch-input">
|
||||
Save
|
||||
</Button>
|
||||
</Field>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Form, Field as FormischField, reset, useForm } from "@formisch/react"
|
||||
import type { SubmitHandler } from "@formisch/react"
|
||||
import { toast } from "sonner"
|
||||
import * as v from "valibot"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
|
||||
const plans = [
|
||||
{
|
||||
id: "starter",
|
||||
title: "Starter (100K tokens/month)",
|
||||
description: "For everyday use with basic features.",
|
||||
},
|
||||
{
|
||||
id: "pro",
|
||||
title: "Pro (1M tokens/month)",
|
||||
description: "For advanced AI usage with more features.",
|
||||
},
|
||||
{
|
||||
id: "enterprise",
|
||||
title: "Enterprise (Unlimited tokens)",
|
||||
description: "For large teams and heavy usage.",
|
||||
},
|
||||
] as const
|
||||
|
||||
const FormSchema = v.object({
|
||||
plan: v.pipe(
|
||||
v.string(),
|
||||
v.minLength(1, "You must select a subscription plan to continue.")
|
||||
),
|
||||
})
|
||||
|
||||
export default function FormFormischRadioGroup() {
|
||||
const form = useForm({
|
||||
schema: FormSchema,
|
||||
initialInput: {
|
||||
plan: "",
|
||||
},
|
||||
})
|
||||
|
||||
const handleSubmit: SubmitHandler<typeof FormSchema> = (output) => {
|
||||
toast("You submitted the following values:", {
|
||||
description: (
|
||||
<pre className="mt-2 w-[320px] overflow-x-auto rounded-md bg-code p-4 text-code-foreground">
|
||||
<code>{JSON.stringify(output, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
position: "bottom-right",
|
||||
classNames: {
|
||||
content: "flex flex-col gap-2",
|
||||
},
|
||||
style: {
|
||||
"--border-radius": "calc(var(--radius) + 4px)",
|
||||
} as React.CSSProperties,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="w-full sm:max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle>Subscription Plan</CardTitle>
|
||||
<CardDescription>
|
||||
See pricing and features for each plan.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form of={form} id="form-formisch-radiogroup" onSubmit={handleSubmit}>
|
||||
<FieldGroup>
|
||||
<FormischField of={form} path={["plan"]}>
|
||||
{(field) => (
|
||||
<FieldSet data-invalid={field.errors !== null}>
|
||||
<FieldLegend>Plan</FieldLegend>
|
||||
<FieldDescription>
|
||||
You can upgrade or downgrade your plan at any time.
|
||||
</FieldDescription>
|
||||
<RadioGroup
|
||||
value={field.input ?? ""}
|
||||
onValueChange={(value) => field.onChange(value)}
|
||||
aria-invalid={field.errors !== null}
|
||||
>
|
||||
{plans.map((plan) => (
|
||||
<FieldLabel
|
||||
key={plan.id}
|
||||
htmlFor={`form-formisch-radiogroup-${plan.id}`}
|
||||
>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
data-invalid={field.errors !== null}
|
||||
>
|
||||
<FieldContent>
|
||||
<FieldTitle>{plan.title}</FieldTitle>
|
||||
<FieldDescription>
|
||||
{plan.description}
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value={plan.id}
|
||||
id={`form-formisch-radiogroup-${plan.id}`}
|
||||
aria-invalid={field.errors !== null}
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</RadioGroup>
|
||||
{field.errors && (
|
||||
<FieldError
|
||||
errors={field.errors.map((message) => ({ message }))}
|
||||
/>
|
||||
)}
|
||||
</FieldSet>
|
||||
)}
|
||||
</FormischField>
|
||||
</FieldGroup>
|
||||
</Form>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Field orientation="horizontal">
|
||||
<Button type="button" variant="outline" onClick={() => reset(form)}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button type="submit" form="form-formisch-radiogroup">
|
||||
Save
|
||||
</Button>
|
||||
</Field>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Form, Field as FormischField, reset, useForm } from "@formisch/react"
|
||||
import type { SubmitHandler } from "@formisch/react"
|
||||
import { toast } from "sonner"
|
||||
import * as v from "valibot"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
|
||||
const spokenLanguages = [
|
||||
{ label: "English", value: "en" },
|
||||
{ label: "Spanish", value: "es" },
|
||||
{ label: "French", value: "fr" },
|
||||
{ label: "German", value: "de" },
|
||||
{ label: "Italian", value: "it" },
|
||||
{ label: "Chinese", value: "zh" },
|
||||
{ label: "Japanese", value: "ja" },
|
||||
] as const
|
||||
|
||||
const FormSchema = v.object({
|
||||
language: v.pipe(
|
||||
v.string(),
|
||||
v.minLength(1, "Please select your spoken language."),
|
||||
v.check(
|
||||
(value) => value !== "auto",
|
||||
"Auto-detection is not allowed. Please select a specific language."
|
||||
)
|
||||
),
|
||||
})
|
||||
|
||||
export default function FormFormischSelect() {
|
||||
const form = useForm({
|
||||
schema: FormSchema,
|
||||
initialInput: {
|
||||
language: "",
|
||||
},
|
||||
})
|
||||
|
||||
const handleSubmit: SubmitHandler<typeof FormSchema> = (output) => {
|
||||
toast("You submitted the following values:", {
|
||||
description: (
|
||||
<pre className="mt-2 w-[320px] overflow-x-auto rounded-md bg-code p-4 text-code-foreground">
|
||||
<code>{JSON.stringify(output, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
position: "bottom-right",
|
||||
classNames: {
|
||||
content: "flex flex-col gap-2",
|
||||
},
|
||||
style: {
|
||||
"--border-radius": "calc(var(--radius) + 4px)",
|
||||
} as React.CSSProperties,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="w-full sm:max-w-lg">
|
||||
<CardHeader>
|
||||
<CardTitle>Language Preferences</CardTitle>
|
||||
<CardDescription>
|
||||
Select your preferred spoken language.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form of={form} id="form-formisch-select" onSubmit={handleSubmit}>
|
||||
<FieldGroup>
|
||||
<FormischField of={form} path={["language"]}>
|
||||
{(field) => (
|
||||
<Field
|
||||
orientation="responsive"
|
||||
data-invalid={field.errors !== null}
|
||||
>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="form-formisch-select-language">
|
||||
Spoken Language
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
For best results, select the language you speak.
|
||||
</FieldDescription>
|
||||
{field.errors && (
|
||||
<FieldError
|
||||
errors={field.errors.map((message) => ({ message }))}
|
||||
/>
|
||||
)}
|
||||
</FieldContent>
|
||||
<Select
|
||||
value={field.input ?? ""}
|
||||
onValueChange={(value) => field.onChange(value)}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="form-formisch-select-language"
|
||||
aria-invalid={field.errors !== null}
|
||||
className="min-w-[120px]"
|
||||
>
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="item-aligned">
|
||||
<SelectItem value="auto">Auto</SelectItem>
|
||||
<SelectSeparator />
|
||||
{spokenLanguages.map((language) => (
|
||||
<SelectItem key={language.value} value={language.value}>
|
||||
{language.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
)}
|
||||
</FormischField>
|
||||
</FieldGroup>
|
||||
</Form>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Field orientation="horizontal">
|
||||
<Button type="button" variant="outline" onClick={() => reset(form)}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button type="submit" form="form-formisch-select">
|
||||
Save
|
||||
</Button>
|
||||
</Field>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Form, Field as FormischField, reset, useForm } from "@formisch/react"
|
||||
import type { SubmitHandler } from "@formisch/react"
|
||||
import { toast } from "sonner"
|
||||
import * as v from "valibot"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
|
||||
const FormSchema = v.object({
|
||||
twoFactor: v.pipe(
|
||||
v.boolean(),
|
||||
v.check(
|
||||
(value) => value === true,
|
||||
"It is highly recommended to enable two-factor authentication."
|
||||
)
|
||||
),
|
||||
})
|
||||
|
||||
export default function FormFormischSwitch() {
|
||||
const form = useForm({
|
||||
schema: FormSchema,
|
||||
initialInput: {
|
||||
twoFactor: false,
|
||||
},
|
||||
})
|
||||
|
||||
const handleSubmit: SubmitHandler<typeof FormSchema> = (output) => {
|
||||
toast("You submitted the following values:", {
|
||||
description: (
|
||||
<pre className="mt-2 w-[320px] overflow-x-auto rounded-md bg-code p-4 text-code-foreground">
|
||||
<code>{JSON.stringify(output, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
position: "bottom-right",
|
||||
classNames: {
|
||||
content: "flex flex-col gap-2",
|
||||
},
|
||||
style: {
|
||||
"--border-radius": "calc(var(--radius) + 4px)",
|
||||
} as React.CSSProperties,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="w-full sm:max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle>Security Settings</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your account security preferences.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form of={form} id="form-formisch-switch" onSubmit={handleSubmit}>
|
||||
<FieldGroup>
|
||||
<FormischField of={form} path={["twoFactor"]}>
|
||||
{(field) => (
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
data-invalid={field.errors !== null}
|
||||
>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor="form-formisch-switch-twoFactor">
|
||||
Multi-factor authentication
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
Enable multi-factor authentication to secure your account.
|
||||
</FieldDescription>
|
||||
{field.errors && (
|
||||
<FieldError
|
||||
errors={field.errors.map((message) => ({ message }))}
|
||||
/>
|
||||
)}
|
||||
</FieldContent>
|
||||
<Switch
|
||||
id="form-formisch-switch-twoFactor"
|
||||
checked={field.input ?? false}
|
||||
onCheckedChange={(checked) => field.onChange(checked)}
|
||||
aria-invalid={field.errors !== null}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
</FormischField>
|
||||
</FieldGroup>
|
||||
</Form>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Field orientation="horizontal">
|
||||
<Button type="button" variant="outline" onClick={() => reset(form)}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button type="submit" form="form-formisch-switch">
|
||||
Save
|
||||
</Button>
|
||||
</Field>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Form, Field as FormischField, reset, useForm } from "@formisch/react"
|
||||
import type { SubmitHandler } from "@formisch/react"
|
||||
import { toast } from "sonner"
|
||||
import * as v from "valibot"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/registry/new-york-v4/ui/card"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
|
||||
const FormSchema = v.object({
|
||||
about: v.pipe(
|
||||
v.string(),
|
||||
v.minLength(10, "Please provide at least 10 characters."),
|
||||
v.maxLength(200, "Please keep it under 200 characters.")
|
||||
),
|
||||
})
|
||||
|
||||
export default function FormFormischTextarea() {
|
||||
const form = useForm({
|
||||
schema: FormSchema,
|
||||
initialInput: {
|
||||
about: "",
|
||||
},
|
||||
})
|
||||
|
||||
const handleSubmit: SubmitHandler<typeof FormSchema> = (output) => {
|
||||
toast("You submitted the following values:", {
|
||||
description: (
|
||||
<pre className="mt-2 w-[320px] overflow-x-auto rounded-md bg-code p-4 text-code-foreground">
|
||||
<code>{JSON.stringify(output, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
position: "bottom-right",
|
||||
classNames: {
|
||||
content: "flex flex-col gap-2",
|
||||
},
|
||||
style: {
|
||||
"--border-radius": "calc(var(--radius) + 4px)",
|
||||
} as React.CSSProperties,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="w-full sm:max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle>Personalization</CardTitle>
|
||||
<CardDescription>
|
||||
Customize your experience by telling us more about yourself.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form of={form} id="form-formisch-textarea" onSubmit={handleSubmit}>
|
||||
<FieldGroup>
|
||||
<FormischField of={form} path={["about"]}>
|
||||
{(field) => (
|
||||
<Field data-invalid={field.errors !== null}>
|
||||
<FieldLabel htmlFor="form-formisch-textarea-about">
|
||||
More about you
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
{...field.props}
|
||||
id="form-formisch-textarea-about"
|
||||
value={field.input ?? ""}
|
||||
aria-invalid={field.errors !== null}
|
||||
placeholder="I'm a software engineer..."
|
||||
className="min-h-[120px]"
|
||||
/>
|
||||
<FieldDescription>
|
||||
Tell us more about yourself. This will be used to help us
|
||||
personalize your experience.
|
||||
</FieldDescription>
|
||||
{field.errors && (
|
||||
<FieldError
|
||||
errors={field.errors.map((message) => ({ message }))}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
</FormischField>
|
||||
</FieldGroup>
|
||||
</Form>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Field orientation="horizontal">
|
||||
<Button type="button" variant="outline" onClick={() => reset(form)}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button type="submit" form="form-formisch-textarea">
|
||||
Save
|
||||
</Button>
|
||||
</Field>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -16,7 +16,7 @@ const ToggleGroupContext = React.createContext<
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
spacing: 2,
|
||||
spacing: 0,
|
||||
orientation: "horizontal",
|
||||
})
|
||||
|
||||
@@ -24,7 +24,7 @@ function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
spacing = 2,
|
||||
spacing = 0,
|
||||
orientation = "horizontal",
|
||||
children,
|
||||
...props
|
||||
|
||||
@@ -16,7 +16,7 @@ const ToggleGroupContext = React.createContext<
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
spacing: 2,
|
||||
spacing: 0,
|
||||
orientation: "horizontal",
|
||||
})
|
||||
|
||||
@@ -24,7 +24,7 @@ function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
spacing = 2,
|
||||
spacing = 0,
|
||||
orientation = "horizontal",
|
||||
children,
|
||||
...props
|
||||
|
||||
@@ -16,7 +16,7 @@ const ToggleGroupContext = React.createContext<
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
spacing: 2,
|
||||
spacing: 0,
|
||||
orientation: "horizontal",
|
||||
})
|
||||
|
||||
@@ -24,7 +24,7 @@ function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
spacing = 2,
|
||||
spacing = 0,
|
||||
orientation = "horizontal",
|
||||
children,
|
||||
...props
|
||||
|
||||
@@ -16,7 +16,7 @@ const ToggleGroupContext = React.createContext<
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
spacing: 2,
|
||||
spacing: 0,
|
||||
orientation: "horizontal",
|
||||
})
|
||||
|
||||
@@ -24,7 +24,7 @@ function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
spacing = 2,
|
||||
spacing = 0,
|
||||
orientation = "horizontal",
|
||||
children,
|
||||
...props
|
||||
|
||||
@@ -16,7 +16,7 @@ const ToggleGroupContext = React.createContext<
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
spacing: 2,
|
||||
spacing: 0,
|
||||
orientation: "horizontal",
|
||||
})
|
||||
|
||||
@@ -24,7 +24,7 @@ function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
spacing = 2,
|
||||
spacing = 0,
|
||||
orientation = "horizontal",
|
||||
children,
|
||||
...props
|
||||
|
||||
@@ -16,7 +16,7 @@ const ToggleGroupContext = React.createContext<
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
spacing: 2,
|
||||
spacing: 0,
|
||||
orientation: "horizontal",
|
||||
})
|
||||
|
||||
@@ -24,7 +24,7 @@ function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
spacing = 2,
|
||||
spacing = 0,
|
||||
orientation = "horizontal",
|
||||
children,
|
||||
...props
|
||||
|
||||
@@ -16,7 +16,7 @@ const ToggleGroupContext = React.createContext<
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
spacing: 2,
|
||||
spacing: 0,
|
||||
orientation: "horizontal",
|
||||
})
|
||||
|
||||
@@ -24,7 +24,7 @@ function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
spacing = 2,
|
||||
spacing = 0,
|
||||
orientation = "horizontal",
|
||||
children,
|
||||
...props
|
||||
|
||||
@@ -16,7 +16,7 @@ const ToggleGroupContext = React.createContext<
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
spacing: 2,
|
||||
spacing: 0,
|
||||
orientation: "horizontal",
|
||||
})
|
||||
|
||||
@@ -24,7 +24,7 @@ function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
spacing = 2,
|
||||
spacing = 0,
|
||||
orientation = "horizontal",
|
||||
children,
|
||||
...props
|
||||
|
||||
@@ -15,7 +15,7 @@ const ToggleGroupContext = React.createContext<
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
spacing: 2,
|
||||
spacing: 0,
|
||||
orientation: "horizontal",
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
spacing = 2,
|
||||
spacing = 0,
|
||||
orientation = "horizontal",
|
||||
children,
|
||||
...props
|
||||
|
||||
@@ -15,7 +15,7 @@ const ToggleGroupContext = React.createContext<
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
spacing: 2,
|
||||
spacing: 0,
|
||||
orientation: "horizontal",
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
spacing = 2,
|
||||
spacing = 0,
|
||||
orientation = "horizontal",
|
||||
children,
|
||||
...props
|
||||
|
||||
@@ -15,7 +15,7 @@ const ToggleGroupContext = React.createContext<
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
spacing: 2,
|
||||
spacing: 0,
|
||||
orientation: "horizontal",
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
spacing = 2,
|
||||
spacing = 0,
|
||||
orientation = "horizontal",
|
||||
children,
|
||||
...props
|
||||
|
||||
@@ -15,7 +15,7 @@ const ToggleGroupContext = React.createContext<
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
spacing: 2,
|
||||
spacing: 0,
|
||||
orientation: "horizontal",
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
spacing = 2,
|
||||
spacing = 0,
|
||||
orientation = "horizontal",
|
||||
children,
|
||||
...props
|
||||
|
||||
@@ -15,7 +15,7 @@ const ToggleGroupContext = React.createContext<
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
spacing: 2,
|
||||
spacing: 0,
|
||||
orientation: "horizontal",
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
spacing = 2,
|
||||
spacing = 0,
|
||||
orientation = "horizontal",
|
||||
children,
|
||||
...props
|
||||
|
||||
@@ -15,7 +15,7 @@ const ToggleGroupContext = React.createContext<
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
spacing: 2,
|
||||
spacing: 0,
|
||||
orientation: "horizontal",
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
spacing = 2,
|
||||
spacing = 0,
|
||||
orientation = "horizontal",
|
||||
children,
|
||||
...props
|
||||
|
||||
@@ -15,7 +15,7 @@ const ToggleGroupContext = React.createContext<
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
spacing: 2,
|
||||
spacing: 0,
|
||||
orientation: "horizontal",
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
spacing = 2,
|
||||
spacing = 0,
|
||||
orientation = "horizontal",
|
||||
children,
|
||||
...props
|
||||
|
||||
@@ -15,7 +15,7 @@ const ToggleGroupContext = React.createContext<
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
spacing: 2,
|
||||
spacing: 0,
|
||||
orientation: "horizontal",
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
spacing = 2,
|
||||
spacing = 0,
|
||||
orientation = "horizontal",
|
||||
children,
|
||||
...props
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
# @shadcn/ui
|
||||
|
||||
## 4.8.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#10715](https://github.com/shadcn-ui/ui/pull/10715) [`51e3cfaf32faeff2589e5c74d81ffd109f509e93`](https://github.com/shadcn-ui/ui/commit/51e3cfaf32faeff2589e5c74d81ffd109f509e93) Thanks [@shadcn](https://github.com/shadcn)! - add shadcn registry validate command
|
||||
|
||||
- [#10708](https://github.com/shadcn-ui/ui/pull/10708) [`c8ab3801ecf97c0350ac0234a25e61f19ccaba62`](https://github.com/shadcn-ui/ui/commit/c8ab3801ecf97c0350ac0234a25e61f19ccaba62) Thanks [@shadcn](https://github.com/shadcn)! - add include to registry.json
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#10567](https://github.com/shadcn-ui/ui/pull/10567) [`1c4a53a37adeba36dbd5c07980c5bb6d295cea9e`](https://github.com/shadcn-ui/ui/commit/1c4a53a37adeba36dbd5c07980c5bb6d295cea9e) Thanks [@shadcn](https://github.com/shadcn)! - fix failing version derivation test
|
||||
|
||||
## 4.7.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "shadcn",
|
||||
"version": "4.8.0",
|
||||
"version": "4.7.0",
|
||||
"description": "Add components to your apps.",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import * as fs from "fs/promises"
|
||||
import { tmpdir } from "os"
|
||||
import * as path from "path"
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
|
||||
import { build } from "./build"
|
||||
|
||||
vi.mock("@/src/utils/handle-error", () => ({
|
||||
handleError: vi.fn((error) => {
|
||||
throw error
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock("@/src/utils/spinner", () => ({
|
||||
spinner: () => ({
|
||||
start: vi.fn(),
|
||||
succeed: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
describe("build command", () => {
|
||||
it("writes flattened registries for source registries that use include", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
include: ["components/ui/registry.json"],
|
||||
}),
|
||||
"components/ui/registry.json": JSON.stringify({
|
||||
items: [
|
||||
{
|
||||
name: "button",
|
||||
type: "registry:ui",
|
||||
files: [
|
||||
{
|
||||
path: "button.tsx",
|
||||
type: "registry:ui",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
"components/ui/button.tsx": "export function Button() {}",
|
||||
})
|
||||
|
||||
await build.parseAsync(
|
||||
["node", "shadcn", "registry.json", "--cwd", cwd, "--output", "public/r"],
|
||||
{ from: "node" }
|
||||
)
|
||||
|
||||
const outputDir = path.join(cwd, "public/r")
|
||||
const registry = JSON.parse(
|
||||
await fs.readFile(path.join(outputDir, "registry.json"), "utf-8")
|
||||
)
|
||||
const button = JSON.parse(
|
||||
await fs.readFile(path.join(outputDir, "button.json"), "utf-8")
|
||||
)
|
||||
|
||||
expect(registry).toMatchObject({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
items: [
|
||||
{
|
||||
name: "button",
|
||||
files: [
|
||||
{
|
||||
path: "components/ui/button.tsx",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
expect(registry).not.toHaveProperty("include")
|
||||
expect(registry.items[0].files[0]).not.toHaveProperty("content")
|
||||
expect(button).toMatchObject({
|
||||
$schema: "https://ui.shadcn.com/schema/registry-item.json",
|
||||
name: "button",
|
||||
files: [
|
||||
{
|
||||
path: "components/ui/button.tsx",
|
||||
content: "export function Button() {}",
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
async function createFixture(files: Record<string, string>) {
|
||||
const cwd = await fs.mkdtemp(path.join(tmpdir(), "shadcn-build-"))
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(files).map(async ([filePath, content]) => {
|
||||
const targetPath = path.join(cwd, filePath)
|
||||
await fs.mkdir(path.dirname(targetPath), { recursive: true })
|
||||
await fs.writeFile(targetPath, content)
|
||||
})
|
||||
)
|
||||
|
||||
return cwd
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import { preFlightBuild } from "@/src/preflights/preflight-build"
|
||||
import {
|
||||
createRegistryCatalog,
|
||||
createRegistryItem,
|
||||
readRegistryWithIncludes,
|
||||
} from "@/src/registry/loader"
|
||||
import { SHADCN_URL } from "@/src/registry/constants"
|
||||
import { registryItemSchema, registrySchema } from "@/src/schema"
|
||||
import { handleError } from "@/src/utils/handle-error"
|
||||
import { highlighter } from "@/src/utils/highlighter"
|
||||
import { logger } from "@/src/utils/logger"
|
||||
import { spinner } from "@/src/utils/spinner"
|
||||
import { Command } from "commander"
|
||||
@@ -32,64 +30,67 @@ export const build = new Command()
|
||||
"the working directory. defaults to the current directory.",
|
||||
process.cwd()
|
||||
)
|
||||
.action(async (registryFile: string, opts) => {
|
||||
.action(async (registry: string, opts) => {
|
||||
try {
|
||||
const options = buildOptionsSchema.parse({
|
||||
cwd: path.resolve(opts.cwd),
|
||||
registryFile,
|
||||
registryFile: registry,
|
||||
outputDir: opts.output,
|
||||
})
|
||||
|
||||
const { resolvePaths } = await preFlightBuild(options)
|
||||
const registryResult = await readRegistryWithIncludes(
|
||||
resolvePaths.registryFile,
|
||||
{
|
||||
cwd: resolvePaths.cwd,
|
||||
}
|
||||
)
|
||||
const resolvedRegistry = registryResult.registry
|
||||
const registryRootDir = registryResult.usesInclude
|
||||
? path.dirname(resolvePaths.registryFile)
|
||||
: resolvePaths.cwd
|
||||
const registryCatalog = createRegistryCatalog(
|
||||
registryResult,
|
||||
registryRootDir,
|
||||
resolvePaths.cwd
|
||||
)
|
||||
const content = await fs.readFile(resolvePaths.registryFile, "utf-8")
|
||||
|
||||
const result = registrySchema.safeParse(JSON.parse(content))
|
||||
|
||||
if (!result.success) {
|
||||
logger.error(
|
||||
`Invalid registry file found at ${highlighter.info(
|
||||
resolvePaths.registryFile
|
||||
)}.`
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const buildSpinner = spinner("Building registry...")
|
||||
for (const registryItem of resolvedRegistry.items) {
|
||||
for (const registryItem of result.data.items) {
|
||||
buildSpinner.start(`Building ${registryItem.name}...`)
|
||||
|
||||
const registryItemForBuild = await createRegistryItem(
|
||||
registryItem,
|
||||
registryResult,
|
||||
registryRootDir,
|
||||
resolvePaths.cwd
|
||||
)
|
||||
// Add the schema to the registry item.
|
||||
registryItem["$schema"] =
|
||||
"https://ui.shadcn.com/schema/registry-item.json"
|
||||
|
||||
// Loop through each file in the files array.
|
||||
for (const file of registryItem.files ?? []) {
|
||||
file["content"] = await fs.readFile(
|
||||
path.resolve(resolvePaths.cwd, file.path),
|
||||
"utf-8"
|
||||
)
|
||||
}
|
||||
|
||||
// Validate the registry item.
|
||||
const result = registryItemSchema.safeParse(registryItem)
|
||||
if (!result.success) {
|
||||
logger.error(
|
||||
`Invalid registry item found for ${highlighter.info(
|
||||
registryItem.name
|
||||
)}.`
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
// Write the registry item to the output directory.
|
||||
await fs.writeFile(
|
||||
path.resolve(
|
||||
resolvePaths.outputDir,
|
||||
`${registryItemForBuild.name}.json`
|
||||
),
|
||||
JSON.stringify(registryItemForBuild, null, 2)
|
||||
path.resolve(resolvePaths.outputDir, `${result.data.name}.json`),
|
||||
JSON.stringify(result.data, null, 2)
|
||||
)
|
||||
}
|
||||
|
||||
if (registryResult.usesInclude) {
|
||||
await fs.writeFile(
|
||||
path.resolve(resolvePaths.outputDir, "registry.json"),
|
||||
JSON.stringify(registryCatalog, null, 2)
|
||||
)
|
||||
} else {
|
||||
// Copy registry.json to the output directory.
|
||||
await fs.copyFile(
|
||||
resolvePaths.registryFile,
|
||||
path.resolve(resolvePaths.outputDir, "registry.json")
|
||||
)
|
||||
}
|
||||
// Copy registry.json to the output directory.
|
||||
await fs.copyFile(
|
||||
resolvePaths.registryFile,
|
||||
path.resolve(resolvePaths.outputDir, "registry.json")
|
||||
)
|
||||
|
||||
buildSpinner.succeed("Building registry.")
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { add } from "@/src/commands/registry/add"
|
||||
import { validate } from "@/src/commands/registry/validate"
|
||||
import { Command } from "commander"
|
||||
|
||||
export const registry = new Command()
|
||||
.name("registry")
|
||||
.description("manage registries")
|
||||
.addCommand(add)
|
||||
.addCommand(validate)
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
import * as fs from "fs/promises"
|
||||
import { tmpdir } from "os"
|
||||
import * as path from "path"
|
||||
import { logger } from "@/src/utils/logger"
|
||||
import { spinner } from "@/src/utils/spinner"
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||
|
||||
import { validate } from "./validate"
|
||||
|
||||
vi.mock("@/src/utils/handle-error", () => ({
|
||||
handleError: vi.fn((error) => {
|
||||
throw error
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock("@/src/utils/highlighter", () => ({
|
||||
highlighter: {
|
||||
error: (value: string) => value,
|
||||
info: (value: string) => value,
|
||||
success: (value: string) => value,
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("@/src/utils/logger", () => ({
|
||||
logger: {
|
||||
break: vi.fn(),
|
||||
error: vi.fn(),
|
||||
log: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock("@/src/utils/spinner", () => ({
|
||||
spinner: vi.fn(() => ({
|
||||
fail: vi.fn(),
|
||||
start: vi.fn().mockReturnThis(),
|
||||
succeed: vi.fn(),
|
||||
})),
|
||||
}))
|
||||
|
||||
describe("registry validate command", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
process.exitCode = undefined
|
||||
})
|
||||
|
||||
it("prints success with checked counts", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
items: [],
|
||||
}),
|
||||
})
|
||||
|
||||
await validate.parseAsync(["registry.json", "--cwd", cwd], {
|
||||
from: "user",
|
||||
})
|
||||
|
||||
const validationSpinner = vi.mocked(spinner).mock.results[0].value
|
||||
const summarySpinner = vi.mocked(spinner).mock.results[1].value
|
||||
expect(validationSpinner.succeed).toHaveBeenCalledWith("Registry is valid.")
|
||||
expect(spinner).toHaveBeenCalledWith("Checked 1 registry file and 0 items.")
|
||||
expect(summarySpinner.succeed).toHaveBeenCalled()
|
||||
expect(logger.log).toHaveBeenCalledWith(" - registry.json")
|
||||
expect(
|
||||
vi.mocked(logger.log).mock.calls.map(([message]) => message)
|
||||
).toEqual([" - registry.json"])
|
||||
expect(process.exitCode).toBeUndefined()
|
||||
})
|
||||
|
||||
it("prints grouped diagnostics and sets a failing exit code", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
include: ["components/ui/registry.json"],
|
||||
}),
|
||||
"components/ui/registry.json": JSON.stringify({
|
||||
items: [
|
||||
{
|
||||
name: "button",
|
||||
type: "registry:ui",
|
||||
files: [
|
||||
{
|
||||
path: "missing.tsx",
|
||||
type: "registry:ui",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
await validate.parseAsync(["registry.json", "--cwd", cwd], {
|
||||
from: "user",
|
||||
})
|
||||
|
||||
const validationSpinner = vi.mocked(spinner).mock.results[0].value
|
||||
expect(validationSpinner.fail).toHaveBeenCalledWith(
|
||||
"Registry validation failed."
|
||||
)
|
||||
expect(spinner).toHaveBeenCalledTimes(1)
|
||||
expect(logger.log).toHaveBeenCalledWith(
|
||||
" Checked 2 registry files and 1 item."
|
||||
)
|
||||
expect(logger.log).toHaveBeenCalledWith(" - registry.json")
|
||||
expect(logger.log).toHaveBeenCalledWith(" - components/ui/registry.json")
|
||||
expect(logger.log).toHaveBeenCalledWith("components/ui/registry.json")
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
' - items[0] "button" file "missing.tsx": File "missing.tsx" was not found or could not be read.'
|
||||
)
|
||||
expect(
|
||||
vi.mocked(logger.log).mock.calls.map(([message]) => message)
|
||||
).toEqual([
|
||||
" Checked 2 registry files and 1 item.",
|
||||
" - registry.json",
|
||||
" - components/ui/registry.json",
|
||||
"components/ui/registry.json",
|
||||
" Make sure the file path is relative to the registry.json file that declares the item.",
|
||||
])
|
||||
expect(process.exitCode).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
async function createFixture(files: Record<string, string>) {
|
||||
const cwd = await fs.mkdtemp(path.join(tmpdir(), "shadcn-validate-command-"))
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(files).map(async ([filePath, content]) => {
|
||||
const targetPath = path.join(cwd, filePath)
|
||||
await fs.mkdir(path.dirname(targetPath), { recursive: true })
|
||||
await fs.writeFile(targetPath, content)
|
||||
})
|
||||
)
|
||||
|
||||
return cwd
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
import * as path from "path"
|
||||
import { validateRegistry } from "@/src/registry/validate"
|
||||
import { handleError } from "@/src/utils/handle-error"
|
||||
import { highlighter } from "@/src/utils/highlighter"
|
||||
import { logger } from "@/src/utils/logger"
|
||||
import { spinner } from "@/src/utils/spinner"
|
||||
import { Command } from "commander"
|
||||
import { z } from "zod"
|
||||
|
||||
const validateOptionsSchema = z.object({
|
||||
cwd: z.string(),
|
||||
registryFile: z.string(),
|
||||
})
|
||||
|
||||
export const validate = new Command()
|
||||
.name("validate")
|
||||
.description("validate a shadcn registry")
|
||||
.argument("[registry]", "path to registry.json file", "./registry.json")
|
||||
.option(
|
||||
"-c, --cwd <cwd>",
|
||||
"the working directory. defaults to the current directory.",
|
||||
process.cwd()
|
||||
)
|
||||
.action(async (registryFile: string, opts) => {
|
||||
let validationSpinner: ReturnType<typeof spinner> | undefined
|
||||
|
||||
try {
|
||||
const options = validateOptionsSchema.parse({
|
||||
cwd: path.resolve(opts.cwd),
|
||||
registryFile,
|
||||
})
|
||||
validationSpinner = spinner("Validating registry.").start()
|
||||
const report = await validateRegistry(options)
|
||||
|
||||
printRegistryValidationReport(report, validationSpinner)
|
||||
|
||||
if (!report.valid) {
|
||||
process.exitCode = 1
|
||||
}
|
||||
} catch (error) {
|
||||
validationSpinner?.fail("Registry validation failed.")
|
||||
logger.break()
|
||||
handleError(error)
|
||||
}
|
||||
})
|
||||
|
||||
function printRegistryValidationReport(
|
||||
report: Awaited<ReturnType<typeof validateRegistry>>,
|
||||
validationSpinner: ReturnType<typeof spinner>
|
||||
) {
|
||||
if (report.valid) {
|
||||
validationSpinner.succeed("Registry is valid.")
|
||||
printRegistryValidationStats(report, { success: true })
|
||||
return
|
||||
}
|
||||
|
||||
validationSpinner.fail("Registry validation failed.")
|
||||
printRegistryValidationStats(report)
|
||||
logger.break()
|
||||
|
||||
for (const [registryFile, diagnostics] of Array.from(
|
||||
groupDiagnostics(report)
|
||||
)) {
|
||||
logger.log(highlighter.info(formatPath(registryFile, report.cwd)))
|
||||
|
||||
for (const diagnostic of diagnostics) {
|
||||
logger.error(` - ${formatDiagnostic(diagnostic)}`)
|
||||
if (diagnostic.suggestion) {
|
||||
logger.log(` ${diagnostic.suggestion}`)
|
||||
}
|
||||
}
|
||||
|
||||
logger.break()
|
||||
}
|
||||
}
|
||||
|
||||
function printRegistryValidationStats(
|
||||
report: Awaited<ReturnType<typeof validateRegistry>>,
|
||||
options: {
|
||||
success?: boolean
|
||||
} = {}
|
||||
) {
|
||||
const message = `Checked ${formatCount(
|
||||
report.registryFiles,
|
||||
"registry file",
|
||||
"registry files"
|
||||
)} and ${formatCount(report.items, "item", "items")}.`
|
||||
|
||||
if (options.success) {
|
||||
printSuccess(message)
|
||||
} else {
|
||||
logger.log(` ${message}`)
|
||||
}
|
||||
|
||||
for (const registryFile of report.registryFilePaths) {
|
||||
logger.log(` - ${formatPath(registryFile, report.cwd)}`)
|
||||
}
|
||||
}
|
||||
|
||||
function groupDiagnostics(
|
||||
report: Awaited<ReturnType<typeof validateRegistry>>
|
||||
) {
|
||||
const groups = new Map<string, typeof report.diagnostics>()
|
||||
|
||||
for (const diagnostic of report.diagnostics) {
|
||||
const diagnostics = groups.get(diagnostic.registryFile) ?? []
|
||||
diagnostics.push(diagnostic)
|
||||
groups.set(diagnostic.registryFile, diagnostics)
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
function formatDiagnostic(
|
||||
diagnostic: Awaited<
|
||||
ReturnType<typeof validateRegistry>
|
||||
>["diagnostics"][number]
|
||||
) {
|
||||
const context = []
|
||||
|
||||
if (diagnostic.itemIndex !== undefined) {
|
||||
context.push(`items[${diagnostic.itemIndex}]`)
|
||||
}
|
||||
|
||||
if (diagnostic.itemName) {
|
||||
context.push(`"${diagnostic.itemName}"`)
|
||||
}
|
||||
|
||||
if (diagnostic.includePath) {
|
||||
context.push(`include "${diagnostic.includePath}"`)
|
||||
}
|
||||
|
||||
if (diagnostic.filePath) {
|
||||
context.push(`file "${diagnostic.filePath}"`)
|
||||
}
|
||||
|
||||
if (!context.length) {
|
||||
return diagnostic.message
|
||||
}
|
||||
|
||||
return `${context.join(" ")}: ${diagnostic.message}`
|
||||
}
|
||||
|
||||
function formatPath(filePath: string, cwd: string) {
|
||||
const relativePath = path.relative(cwd, filePath)
|
||||
|
||||
if (
|
||||
relativePath &&
|
||||
!relativePath.startsWith("..") &&
|
||||
!path.isAbsolute(relativePath)
|
||||
) {
|
||||
return relativePath.split(path.sep).join("/")
|
||||
}
|
||||
|
||||
if (!relativePath) {
|
||||
return "."
|
||||
}
|
||||
|
||||
return filePath
|
||||
}
|
||||
|
||||
function printSuccess(message: string) {
|
||||
spinner(message).succeed()
|
||||
}
|
||||
|
||||
function formatCount(count: number, singular: string, plural: string) {
|
||||
return `${count} ${count === 1 ? singular : plural}`
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
RegistryNotFoundError,
|
||||
RegistryParseError,
|
||||
RegistryUnauthorizedError,
|
||||
RegistryValidationError,
|
||||
} from "@/src/registry/errors"
|
||||
import { http, HttpResponse } from "msw"
|
||||
import { setupServer } from "msw/node"
|
||||
@@ -862,36 +861,6 @@ describe("getRegistry", () => {
|
||||
expect(result.items).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should reject source registries that use include", async () => {
|
||||
server.use(
|
||||
http.get("https://source.com/registry.json", () => {
|
||||
return HttpResponse.json({
|
||||
name: "@source/registry",
|
||||
homepage: "https://source.com",
|
||||
include: ["registry/ui/registry.json"],
|
||||
items: [],
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
const mockConfig = {
|
||||
style: "new-york",
|
||||
tailwind: { baseColor: "neutral", cssVariables: true },
|
||||
registries: {
|
||||
"@source": {
|
||||
url: "https://source.com/{name}.json",
|
||||
},
|
||||
},
|
||||
} as any
|
||||
|
||||
await expect(
|
||||
getRegistry("@source", { config: mockConfig })
|
||||
).rejects.toThrow(RegistryValidationError)
|
||||
await expect(
|
||||
getRegistry("@source", { config: mockConfig })
|
||||
).rejects.toThrow("must serve a resolved registry catalog")
|
||||
})
|
||||
|
||||
it("should handle 404 error from registry endpoint", async () => {
|
||||
server.use(
|
||||
http.get("https://notfound.com/registry.json", () => {
|
||||
@@ -1127,12 +1096,9 @@ describe("getRegistry", () => {
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(RegistryParseError)
|
||||
if (error instanceof RegistryParseError) {
|
||||
expect(error.message).toContain("Failed to parse registry catalog")
|
||||
expect(error.message).toContain("Failed to parse registry")
|
||||
expect(error.message).toContain("@parsetest/registry")
|
||||
expect(error.context?.item).toBe("@parsetest/registry")
|
||||
expect(error.suggestion).toContain(
|
||||
"https://ui.shadcn.com/schema/registry.json"
|
||||
)
|
||||
expect(error.parseError).toBeDefined()
|
||||
if (error.parseError instanceof z.ZodError) {
|
||||
expect(error.parseError.errors.length).toBeGreaterThan(0)
|
||||
@@ -1208,28 +1174,6 @@ describe("getRegistry", () => {
|
||||
})
|
||||
})
|
||||
|
||||
it("should reject direct URL source registries that use include", async () => {
|
||||
const registryUrl = "https://example.com/source-registry.json"
|
||||
|
||||
server.use(
|
||||
http.get(registryUrl, () => {
|
||||
return HttpResponse.json({
|
||||
name: "source-registry",
|
||||
homepage: "https://example.com",
|
||||
include: ["registry/ui/registry.json"],
|
||||
items: [],
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
await expect(getRegistry(registryUrl)).rejects.toThrow(
|
||||
RegistryValidationError
|
||||
)
|
||||
await expect(getRegistry(registryUrl)).rejects.toThrow(
|
||||
"must serve a resolved registry catalog"
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle malformed URL gracefully", async () => {
|
||||
const badUrl = "not-a-valid-url"
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
RegistryInvalidNamespaceError,
|
||||
RegistryNotFoundError,
|
||||
RegistryParseError,
|
||||
RegistryValidationError,
|
||||
} from "@/src/registry/errors"
|
||||
import { fetchRegistry } from "@/src/registry/fetcher"
|
||||
import {
|
||||
@@ -52,7 +51,11 @@ export async function getRegistry(
|
||||
|
||||
if (isUrl(name)) {
|
||||
const [result] = await fetchRegistry([name], { useCache })
|
||||
return parseRegistryCatalog(name, result)
|
||||
try {
|
||||
return registrySchema.parse(result)
|
||||
} catch (error) {
|
||||
throw new RegistryParseError(name, error)
|
||||
}
|
||||
}
|
||||
|
||||
if (!name.startsWith("@")) {
|
||||
@@ -81,38 +84,10 @@ export async function getRegistry(
|
||||
|
||||
const [result] = await fetchRegistry([urlAndHeaders.url], { useCache })
|
||||
|
||||
return parseRegistryCatalog(registryName, result)
|
||||
}
|
||||
|
||||
function parseRegistryCatalog(name: string, result: unknown) {
|
||||
try {
|
||||
const registry = registrySchema.parse(result)
|
||||
|
||||
if (registry.include?.length) {
|
||||
throw new RegistryValidationError(
|
||||
`Registry catalog "${name}" uses "include", but consumer registry endpoints must serve a resolved registry catalog. Run "npx shadcn build" and serve the built registry.json, or use loadRegistry() in a dynamic route.`,
|
||||
{
|
||||
context: {
|
||||
registry: name,
|
||||
include: registry.include,
|
||||
},
|
||||
suggestion:
|
||||
"Serve a flattened registry.json for CLI consumers. Source registry.json files with include are supported by shadcn build and loadRegistry().",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return registry
|
||||
return registrySchema.parse(result)
|
||||
} catch (error) {
|
||||
if (error instanceof RegistryValidationError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw new RegistryParseError(name, error, {
|
||||
subject: "registry catalog",
|
||||
suggestion:
|
||||
"The registry catalog may be corrupted or have an invalid format. Please make sure it returns a valid registry.json object. See https://ui.shadcn.com/schema/registry.json.",
|
||||
})
|
||||
throw new RegistryParseError(registryName, error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -214,24 +214,14 @@ export class RegistryNotConfiguredError extends RegistryError {
|
||||
export class RegistryLocalFileError extends RegistryError {
|
||||
constructor(
|
||||
public readonly filePath: string,
|
||||
cause?: unknown,
|
||||
options: {
|
||||
message?: string
|
||||
context?: Record<string, unknown>
|
||||
suggestion?: string
|
||||
} = {}
|
||||
cause?: unknown
|
||||
) {
|
||||
super(
|
||||
options.message ?? `Failed to read local registry file: ${filePath}`,
|
||||
{
|
||||
code: RegistryErrorCode.LOCAL_FILE_ERROR,
|
||||
cause,
|
||||
context: { filePath, ...options.context },
|
||||
suggestion:
|
||||
options.suggestion ??
|
||||
"Check if the file exists and you have read permissions.",
|
||||
}
|
||||
)
|
||||
super(`Failed to read local registry file: ${filePath}`, {
|
||||
code: RegistryErrorCode.LOCAL_FILE_ERROR,
|
||||
cause,
|
||||
context: { filePath },
|
||||
suggestion: "Check if the file exists and you have read permissions.",
|
||||
})
|
||||
this.name = "RegistryLocalFileError"
|
||||
}
|
||||
}
|
||||
@@ -241,18 +231,12 @@ export class RegistryParseError extends RegistryError {
|
||||
|
||||
constructor(
|
||||
public readonly item: string,
|
||||
parseError: unknown,
|
||||
options: {
|
||||
subject?: string
|
||||
context?: Record<string, unknown>
|
||||
suggestion?: string
|
||||
} = {}
|
||||
parseError: unknown
|
||||
) {
|
||||
const subject = options.subject ?? "registry item"
|
||||
let message = `Failed to parse ${subject}: ${item}`
|
||||
let message = `Failed to parse registry item: ${item}`
|
||||
|
||||
if (parseError instanceof z.ZodError) {
|
||||
message = `Failed to parse ${subject}: ${item}\n${parseError.errors
|
||||
message = `Failed to parse registry item: ${item}\n${parseError.errors
|
||||
.map((e) => ` - ${e.path.join(".")}: ${e.message}`)
|
||||
.join("\n")}`
|
||||
}
|
||||
@@ -260,10 +244,8 @@ export class RegistryParseError extends RegistryError {
|
||||
super(message, {
|
||||
code: RegistryErrorCode.PARSE_ERROR,
|
||||
cause: parseError,
|
||||
context: { item, ...options.context },
|
||||
suggestion:
|
||||
options.suggestion ??
|
||||
`The registry item may be corrupted or have an invalid format. Please make sure it returns a valid JSON object. See ${SHADCN_URL}/schema/registry-item.json.`,
|
||||
context: { item },
|
||||
suggestion: `The registry item may be corrupted or have an invalid format. Please make sure it returns a valid JSON object. See ${SHADCN_URL}/schema/registry-item.json.`,
|
||||
})
|
||||
|
||||
this.parseError = parseError
|
||||
@@ -271,44 +253,6 @@ export class RegistryParseError extends RegistryError {
|
||||
}
|
||||
}
|
||||
|
||||
export class RegistryValidationError extends RegistryError {
|
||||
constructor(
|
||||
message: string,
|
||||
options: {
|
||||
registryFile?: string
|
||||
cause?: unknown
|
||||
context?: Record<string, unknown>
|
||||
suggestion?: string
|
||||
} = {}
|
||||
) {
|
||||
super(message, {
|
||||
code: RegistryErrorCode.VALIDATION_ERROR,
|
||||
cause: options.cause,
|
||||
context: {
|
||||
...(options.registryFile ? { registryFile: options.registryFile } : {}),
|
||||
...options.context,
|
||||
},
|
||||
suggestion:
|
||||
options.suggestion ??
|
||||
"Update the registry.json file and try running the command again.",
|
||||
})
|
||||
this.name = "RegistryValidationError"
|
||||
}
|
||||
}
|
||||
|
||||
export class RegistryItemNotFoundError extends RegistryError {
|
||||
constructor(public readonly itemName: string) {
|
||||
super(`Registry item "${itemName}" was not found.`, {
|
||||
code: RegistryErrorCode.NOT_FOUND,
|
||||
statusCode: 404,
|
||||
context: { itemName },
|
||||
suggestion:
|
||||
"Check that the item name exists in the resolved registry catalog.",
|
||||
})
|
||||
this.name = "RegistryItemNotFoundError"
|
||||
}
|
||||
}
|
||||
|
||||
export class RegistryMissingEnvironmentVariablesError extends RegistryError {
|
||||
constructor(
|
||||
public readonly registryName: string,
|
||||
|
||||
@@ -9,13 +9,6 @@ export {
|
||||
export { searchRegistries } from "./search"
|
||||
|
||||
export {
|
||||
loadRegistry,
|
||||
loadRegistryItem,
|
||||
type LoadRegistryOptions,
|
||||
} from "./loader"
|
||||
|
||||
export {
|
||||
RegistryErrorCode,
|
||||
RegistryError,
|
||||
RegistryNotFoundError,
|
||||
RegistryUnauthorizedError,
|
||||
@@ -24,8 +17,6 @@ export {
|
||||
RegistryNotConfiguredError,
|
||||
RegistryLocalFileError,
|
||||
RegistryParseError,
|
||||
RegistryValidationError,
|
||||
RegistryItemNotFoundError,
|
||||
RegistriesIndexParseError,
|
||||
RegistryMissingEnvironmentVariablesError,
|
||||
RegistryInvalidNamespaceError,
|
||||
|
||||
@@ -1,605 +0,0 @@
|
||||
import * as fs from "fs/promises"
|
||||
import { tmpdir } from "os"
|
||||
import * as path from "path"
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import {
|
||||
RegistryErrorCode,
|
||||
RegistryItemNotFoundError,
|
||||
RegistryLocalFileError,
|
||||
RegistryParseError,
|
||||
RegistryValidationError,
|
||||
} from "./errors"
|
||||
import {
|
||||
getRegistryItemFileRootPath,
|
||||
getRegistryItemFileSource,
|
||||
loadRegistry,
|
||||
loadRegistryItem,
|
||||
readRegistryWithIncludes,
|
||||
} from "./loader"
|
||||
|
||||
describe("readRegistryWithIncludes", () => {
|
||||
it("resolves explicit registry.json includes before local items", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
include: ["registry/ui/registry.json", "registry/hooks/registry.json"],
|
||||
items: [
|
||||
{
|
||||
name: "root-item",
|
||||
type: "registry:item",
|
||||
},
|
||||
],
|
||||
}),
|
||||
"registry/ui/registry.json": JSON.stringify({
|
||||
items: [
|
||||
{
|
||||
name: "button",
|
||||
type: "registry:ui",
|
||||
files: [
|
||||
{
|
||||
path: "button.tsx",
|
||||
type: "registry:ui",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
"registry/ui/button.tsx": "export function Button() {}",
|
||||
"registry/hooks/registry.json": JSON.stringify({
|
||||
name: "example-hooks",
|
||||
homepage: "https://example.com",
|
||||
items: [
|
||||
{
|
||||
name: "use-toggle",
|
||||
type: "registry:hook",
|
||||
files: [
|
||||
{
|
||||
path: "use-toggle.ts",
|
||||
type: "registry:hook",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
"registry/hooks/use-toggle.ts": "export function useToggle() {}",
|
||||
})
|
||||
|
||||
const result = await readRegistryWithIncludes("registry.json", { cwd })
|
||||
|
||||
expect(result.usesInclude).toBe(true)
|
||||
expect(result.registry).toMatchObject({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
items: [
|
||||
{ name: "button" },
|
||||
{ name: "use-toggle" },
|
||||
{ name: "root-item" },
|
||||
],
|
||||
})
|
||||
expect(result.registry).not.toHaveProperty("include")
|
||||
expect(
|
||||
getRegistryItemFileSource("button", "button.tsx", result.itemSources, cwd)
|
||||
).toBe(path.join(cwd, "registry/ui/button.tsx"))
|
||||
expect(
|
||||
getRegistryItemFileRootPath(
|
||||
"button",
|
||||
"button.tsx",
|
||||
result.itemSources,
|
||||
cwd,
|
||||
cwd
|
||||
)
|
||||
).toBe("registry/ui/button.tsx")
|
||||
})
|
||||
|
||||
it("rejects root registries without name and homepage", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
include: ["registry/ui/registry.json"],
|
||||
}),
|
||||
"registry/ui/registry.json": JSON.stringify({
|
||||
items: [],
|
||||
}),
|
||||
})
|
||||
|
||||
await expect(
|
||||
readRegistryWithIncludes("registry.json", { cwd })
|
||||
).rejects.toThrow('root registry.json must define "name" and "homepage"')
|
||||
await expect(
|
||||
readRegistryWithIncludes("registry.json", { cwd })
|
||||
).rejects.toBeInstanceOf(RegistryValidationError)
|
||||
})
|
||||
|
||||
it("reports invalid registry JSON as a parse error", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": "{",
|
||||
})
|
||||
|
||||
await expect(
|
||||
readRegistryWithIncludes("registry.json", { cwd })
|
||||
).rejects.toBeInstanceOf(RegistryParseError)
|
||||
})
|
||||
|
||||
it("rejects include targets that are not registry.json files", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
include: ["registry/ui.json"],
|
||||
items: [],
|
||||
}),
|
||||
"registry/ui.json": JSON.stringify({
|
||||
name: "example-ui",
|
||||
homepage: "https://example.com",
|
||||
items: [],
|
||||
}),
|
||||
})
|
||||
|
||||
await expect(
|
||||
readRegistryWithIncludes("registry.json", { cwd })
|
||||
).rejects.toThrow('Use a path like "./registry/ui/registry.json"')
|
||||
await expect(
|
||||
readRegistryWithIncludes("registry.json", { cwd })
|
||||
).rejects.toBeInstanceOf(RegistryValidationError)
|
||||
})
|
||||
|
||||
it("rejects remote include paths", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
include: ["https://example.com/registry.json"],
|
||||
items: [],
|
||||
}),
|
||||
})
|
||||
|
||||
await expect(
|
||||
readRegistryWithIncludes("registry.json", { cwd })
|
||||
).rejects.toThrow("remote includes are not supported")
|
||||
await expect(
|
||||
readRegistryWithIncludes("registry.json", { cwd })
|
||||
).rejects.toBeInstanceOf(RegistryValidationError)
|
||||
})
|
||||
|
||||
it("rejects absolute include paths", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry/ui/registry.json": JSON.stringify({
|
||||
items: [],
|
||||
}),
|
||||
})
|
||||
await fs.writeFile(
|
||||
path.join(cwd, "registry.json"),
|
||||
JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
include: [path.join(cwd, "registry/ui/registry.json")],
|
||||
items: [],
|
||||
})
|
||||
)
|
||||
|
||||
await expect(
|
||||
readRegistryWithIncludes("registry.json", { cwd })
|
||||
).rejects.toThrow("include paths must be relative")
|
||||
await expect(
|
||||
readRegistryWithIncludes("registry.json", { cwd })
|
||||
).rejects.toBeInstanceOf(RegistryValidationError)
|
||||
})
|
||||
|
||||
it("rejects include cycles", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
include: ["./registry.json"],
|
||||
items: [],
|
||||
}),
|
||||
})
|
||||
|
||||
await expect(
|
||||
readRegistryWithIncludes("registry.json", { cwd })
|
||||
).rejects.toThrow("Registry include cycle detected")
|
||||
await expect(
|
||||
readRegistryWithIncludes("registry.json", { cwd })
|
||||
).rejects.toBeInstanceOf(RegistryValidationError)
|
||||
})
|
||||
|
||||
it("rejects include trees that exceed the maximum depth", async () => {
|
||||
const files: Record<string, string> = {
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
include: ["registry-1/registry.json"],
|
||||
items: [],
|
||||
}),
|
||||
}
|
||||
|
||||
for (let index = 1; index <= 33; index++) {
|
||||
const registryPath = `${Array.from(
|
||||
{ length: index },
|
||||
(_, nestedIndex) => `registry-${nestedIndex + 1}`
|
||||
).join("/")}/registry.json`
|
||||
files[registryPath] = JSON.stringify({
|
||||
include:
|
||||
index < 33 ? [`registry-${index + 1}/registry.json`] : undefined,
|
||||
items: [],
|
||||
})
|
||||
}
|
||||
|
||||
const cwd = await createFixture(files)
|
||||
|
||||
await expect(
|
||||
readRegistryWithIncludes("registry.json", { cwd })
|
||||
).rejects.toThrow("Registry include tree is too deep")
|
||||
await expect(
|
||||
readRegistryWithIncludes("registry.json", { cwd })
|
||||
).rejects.toBeInstanceOf(RegistryValidationError)
|
||||
})
|
||||
|
||||
it("rejects duplicate include files before duplicate item validation", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
include: ["registry/ui/registry.json", "registry/ui/./registry.json"],
|
||||
items: [],
|
||||
}),
|
||||
"registry/ui/registry.json": JSON.stringify({
|
||||
items: [
|
||||
{
|
||||
name: "button",
|
||||
type: "registry:ui",
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
await expect(
|
||||
readRegistryWithIncludes("registry.json", { cwd })
|
||||
).rejects.toThrow("Registry file included more than once")
|
||||
await expect(
|
||||
readRegistryWithIncludes("registry.json", { cwd })
|
||||
).rejects.toBeInstanceOf(RegistryValidationError)
|
||||
})
|
||||
|
||||
it("rejects duplicate item names in the resolved catalog", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
include: ["registry/ui/registry.json"],
|
||||
items: [
|
||||
{
|
||||
name: "button",
|
||||
type: "registry:block",
|
||||
},
|
||||
],
|
||||
}),
|
||||
"registry/ui/registry.json": JSON.stringify({
|
||||
name: "example-ui",
|
||||
homepage: "https://example.com",
|
||||
items: [
|
||||
{
|
||||
name: "button",
|
||||
type: "registry:ui",
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
await expect(
|
||||
readRegistryWithIncludes("registry.json", { cwd })
|
||||
).rejects.toThrow("Rename one of these items")
|
||||
await expect(
|
||||
readRegistryWithIncludes("registry.json", { cwd })
|
||||
).rejects.toBeInstanceOf(RegistryValidationError)
|
||||
})
|
||||
|
||||
it("rejects parent traversal in item file paths for include composition", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
include: ["registry/ui/registry.json"],
|
||||
items: [],
|
||||
}),
|
||||
"registry/ui/registry.json": JSON.stringify({
|
||||
name: "example-ui",
|
||||
homepage: "https://example.com",
|
||||
items: [
|
||||
{
|
||||
name: "button",
|
||||
type: "registry:ui",
|
||||
files: [
|
||||
{
|
||||
path: "../button.tsx",
|
||||
type: "registry:ui",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
await expect(
|
||||
readRegistryWithIncludes("registry.json", { cwd })
|
||||
).rejects.toThrow("file paths cannot use parent-directory traversal")
|
||||
await expect(
|
||||
readRegistryWithIncludes("registry.json", { cwd })
|
||||
).rejects.toBeInstanceOf(RegistryValidationError)
|
||||
})
|
||||
|
||||
it("keeps legacy single-file registries compatible", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.flat.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
items: [
|
||||
{
|
||||
name: "button",
|
||||
type: "registry:ui",
|
||||
files: [
|
||||
{
|
||||
path: "../button.tsx",
|
||||
type: "registry:ui",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await readRegistryWithIncludes("registry.flat.json", {
|
||||
cwd,
|
||||
})
|
||||
|
||||
expect(result.usesInclude).toBe(false)
|
||||
expect(result.registry.items).toHaveLength(1)
|
||||
})
|
||||
|
||||
it("keeps legacy file paths cwd-relative for nested single-file registries", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry/registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
items: [
|
||||
{
|
||||
name: "button",
|
||||
type: "registry:ui",
|
||||
files: [
|
||||
{
|
||||
path: "components/button.tsx",
|
||||
type: "registry:ui",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
"components/button.tsx": "export function Button() {}",
|
||||
})
|
||||
|
||||
const registry = await loadRegistry({
|
||||
cwd,
|
||||
registryFile: "registry/registry.json",
|
||||
})
|
||||
|
||||
expect(registry.items[0].files?.[0].path).toBe("components/button.tsx")
|
||||
})
|
||||
|
||||
it("preserves registry dependencies for install-time resolution", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
include: ["registry/blocks/registry.json"],
|
||||
items: [],
|
||||
}),
|
||||
"registry/blocks/registry.json": JSON.stringify({
|
||||
name: "example-blocks",
|
||||
homepage: "https://example.com",
|
||||
items: [
|
||||
{
|
||||
name: "login-form",
|
||||
type: "registry:block",
|
||||
registryDependencies: [
|
||||
"button",
|
||||
"@acme/button",
|
||||
"https://example.com/r/input.json",
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await readRegistryWithIncludes("registry.json", { cwd })
|
||||
|
||||
expect(result.registry.items[0].registryDependencies).toEqual([
|
||||
"button",
|
||||
"@acme/button",
|
||||
"https://example.com/r/input.json",
|
||||
])
|
||||
})
|
||||
|
||||
it("resolves a local registry catalog for dynamic registry routes", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
include: ["registry/ui/registry.json"],
|
||||
}),
|
||||
"registry/ui/registry.json": JSON.stringify({
|
||||
items: [
|
||||
{
|
||||
name: "button",
|
||||
type: "registry:ui",
|
||||
files: [
|
||||
{
|
||||
path: "button.tsx",
|
||||
type: "registry:ui",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
"registry/ui/button.tsx": "export function Button() {}",
|
||||
})
|
||||
|
||||
const registry = await loadRegistry({ cwd })
|
||||
|
||||
expect(registry).toMatchObject({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
items: [
|
||||
{
|
||||
name: "button",
|
||||
files: [
|
||||
{
|
||||
path: "registry/ui/button.tsx",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
expect(registry).not.toHaveProperty("include")
|
||||
expect(registry.items[0].files?.[0]).not.toHaveProperty("content")
|
||||
})
|
||||
|
||||
it("resolves a local registry item for dynamic item routes", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
include: ["registry/ui/registry.json"],
|
||||
}),
|
||||
"registry/ui/registry.json": JSON.stringify({
|
||||
items: [
|
||||
{
|
||||
name: "button",
|
||||
type: "registry:ui",
|
||||
files: [
|
||||
{
|
||||
path: "button.tsx",
|
||||
type: "registry:ui",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
"registry/ui/button.tsx": "export function Button() {}",
|
||||
})
|
||||
|
||||
const item = await loadRegistryItem("button", {
|
||||
cwd,
|
||||
})
|
||||
|
||||
expect(item).toMatchObject({
|
||||
$schema: "https://ui.shadcn.com/schema/registry-item.json",
|
||||
name: "button",
|
||||
files: [
|
||||
{
|
||||
path: "registry/ui/button.tsx",
|
||||
content: "export function Button() {}",
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it("reports missing item files with item and source context", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
include: ["registry/ui/registry.json"],
|
||||
}),
|
||||
"registry/ui/registry.json": JSON.stringify({
|
||||
items: [
|
||||
{
|
||||
name: "button",
|
||||
type: "registry:ui",
|
||||
files: [
|
||||
{
|
||||
path: "button.tsx",
|
||||
type: "registry:ui",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
await expect(loadRegistryItem("button", { cwd })).rejects.toThrow(
|
||||
'Failed to read file "button.tsx" for registry item "button"'
|
||||
)
|
||||
await expect(loadRegistryItem("button", { cwd })).rejects.toBeInstanceOf(
|
||||
RegistryLocalFileError
|
||||
)
|
||||
})
|
||||
|
||||
it("uses the selected item source when duplicate names exist in a flat registry", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
items: [
|
||||
{
|
||||
name: "button",
|
||||
type: "registry:ui",
|
||||
files: [
|
||||
{
|
||||
path: "missing-button.tsx",
|
||||
type: "registry:ui",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "button",
|
||||
type: "registry:ui",
|
||||
files: [
|
||||
{
|
||||
path: "button.tsx",
|
||||
type: "registry:ui",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
"button.tsx": "export function Button() {}",
|
||||
})
|
||||
|
||||
await expect(loadRegistryItem("button", { cwd })).rejects.toThrow(
|
||||
"registry.json items[0]"
|
||||
)
|
||||
})
|
||||
|
||||
it("throws a typed error when a registry item is not found", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
items: [],
|
||||
}),
|
||||
})
|
||||
|
||||
await expect(loadRegistryItem("button", { cwd })).rejects.toBeInstanceOf(
|
||||
RegistryItemNotFoundError
|
||||
)
|
||||
await expect(loadRegistryItem("button", { cwd })).rejects.toMatchObject({
|
||||
code: RegistryErrorCode.NOT_FOUND,
|
||||
itemName: "button",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
async function createFixture(files: Record<string, string>) {
|
||||
const cwd = await fs.mkdtemp(path.join(tmpdir(), "shadcn-registry-"))
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(files).map(async ([filePath, content]) => {
|
||||
const targetPath = path.join(cwd, filePath)
|
||||
await fs.mkdir(path.dirname(targetPath), { recursive: true })
|
||||
await fs.writeFile(targetPath, content)
|
||||
})
|
||||
)
|
||||
|
||||
return cwd
|
||||
}
|
||||
@@ -1,648 +0,0 @@
|
||||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import {
|
||||
RegistryItemNotFoundError,
|
||||
RegistryLocalFileError,
|
||||
RegistryParseError,
|
||||
RegistryValidationError,
|
||||
} from "@/src/registry/errors"
|
||||
import { isUrl } from "@/src/registry/utils"
|
||||
import {
|
||||
registryChunkSchema,
|
||||
registryItemSchema,
|
||||
type Registry,
|
||||
type RegistryItem,
|
||||
} from "@/src/schema"
|
||||
import { z } from "zod"
|
||||
|
||||
type RegistryChunk = z.infer<typeof registryChunkSchema>
|
||||
|
||||
const MAX_INCLUDE_DEPTH = 32
|
||||
|
||||
type RegistryItemSource = {
|
||||
registryFile: string
|
||||
registryDir: string
|
||||
itemIndex: number
|
||||
}
|
||||
|
||||
type RegistryLoadResult = {
|
||||
registry: Registry
|
||||
itemSources: Map<string, RegistryItemSource>
|
||||
itemSourcesByItem: Map<RegistryItem, RegistryItemSource>
|
||||
usesInclude: boolean
|
||||
}
|
||||
|
||||
export type LoadRegistryOptions = {
|
||||
cwd?: string
|
||||
registryFile?: string
|
||||
}
|
||||
|
||||
export async function loadRegistry(options?: LoadRegistryOptions) {
|
||||
const { cwd, registryFile } = resolveLoadRegistryOptions(options)
|
||||
const result = await readRegistryWithIncludes(registryFile, { cwd })
|
||||
const rootDir = getRegistryRootDir(result, cwd, registryFile)
|
||||
|
||||
return createRegistryCatalog(result, rootDir, cwd)
|
||||
}
|
||||
|
||||
export async function loadRegistryItem(
|
||||
itemName: string,
|
||||
options?: LoadRegistryOptions
|
||||
) {
|
||||
const { cwd, registryFile } = resolveLoadRegistryOptions(options)
|
||||
const result = await readRegistryWithIncludes(registryFile, { cwd })
|
||||
const item = result.registry.items.find((item) => item.name === itemName)
|
||||
|
||||
if (!item) {
|
||||
throw new RegistryItemNotFoundError(itemName)
|
||||
}
|
||||
|
||||
const rootDir = getRegistryRootDir(result, cwd, registryFile)
|
||||
|
||||
return createRegistryItem(item, result, rootDir, cwd)
|
||||
}
|
||||
|
||||
export async function readRegistryWithIncludes(
|
||||
registryFile: string,
|
||||
options: {
|
||||
cwd: string
|
||||
}
|
||||
) {
|
||||
const rootFile = path.resolve(options.cwd, registryFile)
|
||||
const content = await readRegistryJson(rootFile)
|
||||
const rootRegistry = parseRegistry(content, rootFile)
|
||||
validateRootRegistry(rootRegistry, rootFile)
|
||||
const context = {
|
||||
cwd: path.resolve(options.cwd),
|
||||
itemSources: new Map<string, RegistryItemSource>(),
|
||||
itemSourcesByItem: new Map<RegistryItem, RegistryItemSource>(),
|
||||
firstIncludedFrom: new Map<string, string>(),
|
||||
}
|
||||
const usesInclude = !!rootRegistry.include?.length
|
||||
|
||||
if (!usesInclude) {
|
||||
rootRegistry.items.forEach((item, itemIndex) => {
|
||||
const source = {
|
||||
registryFile: rootFile,
|
||||
registryDir: context.cwd,
|
||||
itemIndex,
|
||||
}
|
||||
context.itemSources.set(item.name, source)
|
||||
context.itemSourcesByItem.set(item, source)
|
||||
})
|
||||
|
||||
return {
|
||||
registry: rootRegistry,
|
||||
itemSources: context.itemSources,
|
||||
itemSourcesByItem: context.itemSourcesByItem,
|
||||
usesInclude,
|
||||
}
|
||||
}
|
||||
|
||||
if (path.basename(rootFile) !== "registry.json") {
|
||||
throw new RegistryValidationError(
|
||||
`Invalid registry file at ${rootFile}: registries that use include must be named registry.json.`,
|
||||
{ registryFile: rootFile }
|
||||
)
|
||||
}
|
||||
|
||||
const result = await readRegistryFile(rootFile, rootRegistry, context, [])
|
||||
|
||||
validateDuplicateItems(result.items, context.itemSourcesByItem)
|
||||
|
||||
const { include, ...registry } = result
|
||||
validateRootRegistry(registry, rootFile)
|
||||
|
||||
return {
|
||||
registry,
|
||||
itemSources: context.itemSources,
|
||||
itemSourcesByItem: context.itemSourcesByItem,
|
||||
usesInclude,
|
||||
}
|
||||
}
|
||||
|
||||
export function createRegistryCatalog(
|
||||
result: RegistryLoadResult,
|
||||
rootDir: string,
|
||||
fallbackDir: string
|
||||
) {
|
||||
return {
|
||||
...result.registry,
|
||||
items: result.registry.items.map((item) =>
|
||||
stripRegistryItemFileContent(
|
||||
rewriteRegistryItemFilePaths(
|
||||
item,
|
||||
result.itemSourcesByItem,
|
||||
rootDir,
|
||||
fallbackDir
|
||||
)
|
||||
)
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
export async function createRegistryItem(
|
||||
item: RegistryItem,
|
||||
result: RegistryLoadResult,
|
||||
rootDir: string,
|
||||
fallbackDir: string
|
||||
) {
|
||||
const registryItem = {
|
||||
...rewriteRegistryItemFilePaths(
|
||||
item,
|
||||
result.itemSourcesByItem,
|
||||
rootDir,
|
||||
fallbackDir
|
||||
),
|
||||
$schema: "https://ui.shadcn.com/schema/registry-item.json",
|
||||
}
|
||||
|
||||
for (let index = 0; index < (item.files?.length ?? 0); index++) {
|
||||
const sourceFile = item.files?.[index]
|
||||
const file = registryItem.files?.[index]
|
||||
if (!file || !sourceFile) {
|
||||
continue
|
||||
}
|
||||
|
||||
const source = result.itemSourcesByItem.get(item)
|
||||
const sourcePath = getRegistryItemFileSourceForItem(
|
||||
item,
|
||||
sourceFile.path,
|
||||
result.itemSourcesByItem,
|
||||
fallbackDir
|
||||
)
|
||||
file.content = await readRegistryItemFileContent(
|
||||
item.name,
|
||||
sourceFile.path,
|
||||
sourcePath,
|
||||
source
|
||||
)
|
||||
}
|
||||
|
||||
return registryItemSchema.parse(registryItem)
|
||||
}
|
||||
|
||||
async function readRegistryItemFileContent(
|
||||
itemName: string,
|
||||
filePath: string,
|
||||
sourcePath: string,
|
||||
source: RegistryItemSource | undefined
|
||||
) {
|
||||
try {
|
||||
return await fs.readFile(sourcePath, "utf-8")
|
||||
} catch (error) {
|
||||
throw new RegistryLocalFileError(sourcePath, error, {
|
||||
message: `Failed to read file "${filePath}" for registry item "${itemName}" (${formatItemSource(
|
||||
source
|
||||
)}). Expected file at ${sourcePath}.`,
|
||||
context: {
|
||||
itemName,
|
||||
itemFilePath: filePath,
|
||||
sourcePath,
|
||||
},
|
||||
suggestion:
|
||||
"Make sure the file path is relative to the registry.json file that declares the item.",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function rewriteRegistryItemFilePaths(
|
||||
item: RegistryItem,
|
||||
itemSourcesByItem: Map<RegistryItem, RegistryItemSource>,
|
||||
rootDir: string,
|
||||
fallbackDir: string
|
||||
) {
|
||||
return {
|
||||
...item,
|
||||
files: item.files?.map((file) => ({
|
||||
...file,
|
||||
path: getRegistryItemFileRootPathForItem(
|
||||
item,
|
||||
file.path,
|
||||
itemSourcesByItem,
|
||||
rootDir,
|
||||
fallbackDir
|
||||
),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
function stripRegistryItemFileContent(item: RegistryItem) {
|
||||
return {
|
||||
...item,
|
||||
files: item.files?.map(({ content, ...file }) => file),
|
||||
}
|
||||
}
|
||||
|
||||
export function getRegistryItemFileSource(
|
||||
itemName: string,
|
||||
filePath: string,
|
||||
itemSources: Map<string, RegistryItemSource>,
|
||||
fallbackDir: string
|
||||
) {
|
||||
const source = itemSources.get(itemName)
|
||||
return path.resolve(source?.registryDir ?? fallbackDir, filePath)
|
||||
}
|
||||
|
||||
function getRegistryItemFileSourceForItem(
|
||||
item: RegistryItem,
|
||||
filePath: string,
|
||||
itemSourcesByItem: Map<RegistryItem, RegistryItemSource>,
|
||||
fallbackDir: string
|
||||
) {
|
||||
const source = itemSourcesByItem.get(item)
|
||||
return path.resolve(source?.registryDir ?? fallbackDir, filePath)
|
||||
}
|
||||
|
||||
export function getRegistryItemFileRootPath(
|
||||
itemName: string,
|
||||
filePath: string,
|
||||
itemSources: Map<string, RegistryItemSource>,
|
||||
rootDir: string,
|
||||
fallbackDir: string
|
||||
) {
|
||||
const sourcePath = getRegistryItemFileSource(
|
||||
itemName,
|
||||
filePath,
|
||||
itemSources,
|
||||
fallbackDir
|
||||
)
|
||||
|
||||
return path.relative(rootDir, sourcePath).split(path.sep).join("/")
|
||||
}
|
||||
|
||||
function getRegistryItemFileRootPathForItem(
|
||||
item: RegistryItem,
|
||||
filePath: string,
|
||||
itemSourcesByItem: Map<RegistryItem, RegistryItemSource>,
|
||||
rootDir: string,
|
||||
fallbackDir: string
|
||||
) {
|
||||
const sourcePath = getRegistryItemFileSourceForItem(
|
||||
item,
|
||||
filePath,
|
||||
itemSourcesByItem,
|
||||
fallbackDir
|
||||
)
|
||||
|
||||
return path.relative(rootDir, sourcePath).split(path.sep).join("/")
|
||||
}
|
||||
|
||||
async function readRegistryFile(
|
||||
registryFile: string,
|
||||
registry: RegistryChunk,
|
||||
context: {
|
||||
cwd: string
|
||||
itemSources: Map<string, RegistryItemSource>
|
||||
itemSourcesByItem: Map<RegistryItem, RegistryItemSource>
|
||||
firstIncludedFrom: Map<string, string>
|
||||
},
|
||||
chain: string[]
|
||||
): Promise<RegistryChunk> {
|
||||
validateRegistryFileWithinRoot(registryFile, context.cwd)
|
||||
|
||||
if (chain.length >= MAX_INCLUDE_DEPTH) {
|
||||
throw new RegistryValidationError(
|
||||
`Registry include tree is too deep at ${registryFile}. The maximum include depth is ${MAX_INCLUDE_DEPTH}.`,
|
||||
{
|
||||
registryFile,
|
||||
context: {
|
||||
maxDepth: MAX_INCLUDE_DEPTH,
|
||||
},
|
||||
suggestion:
|
||||
"Flatten part of the registry include tree or reduce nested include depth.",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (chain.includes(registryFile)) {
|
||||
throw new RegistryValidationError(
|
||||
formatIncludeCycle([...chain, registryFile]),
|
||||
{
|
||||
registryFile,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const includedFrom = chain.at(-1) ?? registryFile
|
||||
const existingSource = context.firstIncludedFrom.get(registryFile)
|
||||
if (existingSource) {
|
||||
throw new RegistryValidationError(
|
||||
`Registry file included more than once: ${registryFile}.\n` +
|
||||
` - first included from ${existingSource}\n` +
|
||||
` - included again from ${includedFrom}\n` +
|
||||
`Each registry.json file can only appear once in the resolved include tree. Remove one include or move shared items into a single included registry.json.`,
|
||||
{
|
||||
registryFile,
|
||||
context: {
|
||||
firstSource: existingSource,
|
||||
secondSource: includedFrom,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
context.firstIncludedFrom.set(registryFile, includedFrom)
|
||||
|
||||
const nextChain = [...chain, registryFile]
|
||||
const registryDir = path.dirname(registryFile)
|
||||
|
||||
const includedItems: RegistryItem[] = []
|
||||
for (const includePath of registry.include ?? []) {
|
||||
const includedRegistryFile = resolveIncludePath(
|
||||
includePath,
|
||||
registryDir,
|
||||
context.cwd,
|
||||
registryFile
|
||||
)
|
||||
const content = await readRegistryJson(includedRegistryFile)
|
||||
const parsedRegistry = parseRegistry(content, includedRegistryFile)
|
||||
const includedRegistry = await readRegistryFile(
|
||||
includedRegistryFile,
|
||||
parsedRegistry,
|
||||
context,
|
||||
nextChain
|
||||
)
|
||||
includedItems.push(...includedRegistry.items)
|
||||
}
|
||||
|
||||
registry.items.forEach((item, itemIndex) => {
|
||||
validateRegistryItemFiles(item, registryFile, registryDir)
|
||||
context.itemSources.set(item.name, {
|
||||
registryFile,
|
||||
registryDir,
|
||||
itemIndex,
|
||||
})
|
||||
context.itemSourcesByItem.set(item, {
|
||||
registryFile,
|
||||
registryDir,
|
||||
itemIndex,
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
...registry,
|
||||
items: [...includedItems, ...registry.items],
|
||||
}
|
||||
}
|
||||
|
||||
async function readRegistryJson(registryFile: string) {
|
||||
try {
|
||||
return await fs.readFile(registryFile, "utf-8")
|
||||
} catch (error) {
|
||||
throw new RegistryLocalFileError(registryFile, error, {
|
||||
message: `Failed to read registry file at ${registryFile}.`,
|
||||
context: { registryFile },
|
||||
suggestion:
|
||||
"Check that the registry.json file exists and that the path is correct.",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function parseRegistry(content: string, registryFile: string) {
|
||||
let json: unknown
|
||||
try {
|
||||
json = JSON.parse(content)
|
||||
} catch (error) {
|
||||
throw new RegistryParseError(registryFile, error, {
|
||||
subject: "registry file",
|
||||
context: { registryFile },
|
||||
suggestion:
|
||||
"Fix the JSON syntax in the registry.json file and try again.",
|
||||
})
|
||||
}
|
||||
|
||||
const result = registryChunkSchema.safeParse(json)
|
||||
if (!result.success) {
|
||||
throw new RegistryValidationError(
|
||||
`Invalid registry file at ${registryFile}:\n${formatZodIssues(
|
||||
result.error
|
||||
)}`,
|
||||
{
|
||||
registryFile,
|
||||
cause: result.error,
|
||||
suggestion:
|
||||
"Update the registry.json file so it matches the registry schema.",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return result.data
|
||||
}
|
||||
|
||||
function validateRootRegistry(
|
||||
registry: RegistryChunk,
|
||||
registryFile: string
|
||||
): asserts registry is Registry {
|
||||
const missingFields = []
|
||||
|
||||
if (!registry.name) {
|
||||
missingFields.push("name")
|
||||
}
|
||||
|
||||
if (!registry.homepage) {
|
||||
missingFields.push("homepage")
|
||||
}
|
||||
|
||||
if (missingFields.length) {
|
||||
throw new RegistryValidationError(
|
||||
`Invalid root registry file at ${registryFile}: root registry.json must define ${missingFields
|
||||
.map((field) => `"${field}"`)
|
||||
.join(" and ")}. Included registry.json files may omit these fields.`,
|
||||
{ registryFile }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function resolveIncludePath(
|
||||
includePath: string,
|
||||
registryDir: string,
|
||||
cwd: string,
|
||||
registryFile: string
|
||||
) {
|
||||
if (isUrl(includePath)) {
|
||||
throw new RegistryValidationError(
|
||||
`Invalid include "${includePath}" in ${registryFile}: remote includes are not supported by shadcn build. Use a relative path to a registry.json file in the same repository.`,
|
||||
{
|
||||
registryFile,
|
||||
context: { includePath },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (path.isAbsolute(includePath)) {
|
||||
throw new RegistryValidationError(
|
||||
`Invalid include "${includePath}" in ${registryFile}: include paths must be relative. Use a path like "./registry/ui/registry.json".`,
|
||||
{
|
||||
registryFile,
|
||||
context: { includePath },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (hasParentTraversal(includePath)) {
|
||||
throw new RegistryValidationError(
|
||||
`Invalid include "${includePath}" in ${registryFile}: include paths cannot use parent-directory traversal. Keep included registry.json files inside the registry root.`,
|
||||
{
|
||||
registryFile,
|
||||
context: { includePath },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (path.basename(includePath) !== "registry.json") {
|
||||
throw new RegistryValidationError(
|
||||
`Invalid include "${includePath}" in ${registryFile}: include paths must explicitly reference a registry.json file. Use a path like "./registry/ui/registry.json".`,
|
||||
{
|
||||
registryFile,
|
||||
context: { includePath },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const resolvedPath = path.resolve(registryDir, includePath)
|
||||
validateRegistryFileWithinRoot(resolvedPath, cwd)
|
||||
|
||||
return resolvedPath
|
||||
}
|
||||
|
||||
function validateRegistryFileWithinRoot(registryFile: string, cwd: string) {
|
||||
if (!isPathInside(registryFile, cwd)) {
|
||||
throw new RegistryValidationError(
|
||||
`Invalid registry file at ${registryFile}: registry includes must stay inside ${cwd}.`,
|
||||
{
|
||||
registryFile,
|
||||
context: { cwd },
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function resolveLoadRegistryOptions(options?: LoadRegistryOptions) {
|
||||
return {
|
||||
cwd: path.resolve(options?.cwd ?? process.cwd()),
|
||||
registryFile: options?.registryFile ?? "registry.json",
|
||||
}
|
||||
}
|
||||
|
||||
function getRegistryRootDir(
|
||||
result: Pick<RegistryLoadResult, "usesInclude">,
|
||||
cwd: string,
|
||||
registryFile: string
|
||||
) {
|
||||
return result.usesInclude
|
||||
? path.dirname(path.resolve(cwd, registryFile))
|
||||
: cwd
|
||||
}
|
||||
|
||||
function validateRegistryItemFiles(
|
||||
item: RegistryItem,
|
||||
registryFile: string,
|
||||
registryDir: string
|
||||
) {
|
||||
for (const file of item.files ?? []) {
|
||||
if (isUrl(file.path)) {
|
||||
throw new RegistryValidationError(
|
||||
`Invalid file path "${file.path}" for item "${item.name}" in ${registryFile}: remote file paths are not supported by shadcn build.`,
|
||||
{
|
||||
registryFile,
|
||||
context: { itemName: item.name, filePath: file.path },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (path.isAbsolute(file.path)) {
|
||||
throw new RegistryValidationError(
|
||||
`Invalid file path "${file.path}" for item "${item.name}" in ${registryFile}: file paths must be relative.`,
|
||||
{
|
||||
registryFile,
|
||||
context: { itemName: item.name, filePath: file.path },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (hasParentTraversal(file.path)) {
|
||||
throw new RegistryValidationError(
|
||||
`Invalid file path "${file.path}" for item "${item.name}" in ${registryFile}: file paths cannot use parent-directory traversal.`,
|
||||
{
|
||||
registryFile,
|
||||
context: { itemName: item.name, filePath: file.path },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const resolvedPath = path.resolve(registryDir, file.path)
|
||||
if (!isPathInside(resolvedPath, registryDir)) {
|
||||
throw new RegistryValidationError(
|
||||
`Invalid file path "${file.path}" for item "${item.name}" in ${registryFile}: file paths must stay inside the registry chunk directory.`,
|
||||
{
|
||||
registryFile,
|
||||
context: { itemName: item.name, filePath: file.path },
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateDuplicateItems(
|
||||
items: RegistryItem[],
|
||||
itemSources: Map<RegistryItem, RegistryItemSource>
|
||||
) {
|
||||
const seen = new Map<string, RegistryItem>()
|
||||
|
||||
for (const item of items) {
|
||||
const existing = seen.get(item.name)
|
||||
if (!existing) {
|
||||
seen.set(item.name, item)
|
||||
continue
|
||||
}
|
||||
|
||||
const firstSource = itemSources.get(existing)
|
||||
const secondSource = itemSources.get(item)
|
||||
throw new RegistryValidationError(
|
||||
`Duplicate registry item name "${item.name}". Registry item names must be unique.\n` +
|
||||
` - ${formatItemSource(firstSource)}\n` +
|
||||
` - ${formatItemSource(secondSource)}\n` +
|
||||
`Rename one of these items so each name is unique across the resolved registry.`,
|
||||
{
|
||||
context: {
|
||||
itemName: item.name,
|
||||
firstSource: formatItemSource(firstSource),
|
||||
secondSource: formatItemSource(secondSource),
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function hasParentTraversal(filePath: string) {
|
||||
return filePath.split(/[\\/]+/).includes("..")
|
||||
}
|
||||
|
||||
function isPathInside(filePath: string, root: string) {
|
||||
const relative = path.relative(root, filePath)
|
||||
return !!relative && !relative.startsWith("..") && !path.isAbsolute(relative)
|
||||
}
|
||||
|
||||
function formatIncludeCycle(chain: string[]) {
|
||||
return `Registry include cycle detected:\n${chain
|
||||
.map((file) => ` - ${file}`)
|
||||
.join("\n")}`
|
||||
}
|
||||
|
||||
function formatItemSource(source: RegistryItemSource | undefined) {
|
||||
if (!source) {
|
||||
return "unknown source"
|
||||
}
|
||||
|
||||
return `${source.registryFile} items[${source.itemIndex}]`
|
||||
}
|
||||
|
||||
function formatZodIssues(error: z.ZodError) {
|
||||
return error.errors
|
||||
.map((issue) => {
|
||||
const issuePath = issue.path.length ? issue.path.join(".") : "(root)"
|
||||
return ` - ${issuePath}: ${issue.message}`
|
||||
})
|
||||
.join("\n")
|
||||
}
|
||||
@@ -1,10 +1,6 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import {
|
||||
registryChunkSchema,
|
||||
registryConfigSchema,
|
||||
registrySchema,
|
||||
} from "./schema"
|
||||
import { registryConfigSchema } from "./schema"
|
||||
|
||||
describe("registryConfigSchema", () => {
|
||||
it("should accept valid registry names starting with @", () => {
|
||||
@@ -51,33 +47,3 @@ describe("registryConfigSchema", () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("registrySchema", () => {
|
||||
it("should accept registry chunks with includes", () => {
|
||||
const result = registryChunkSchema.safeParse({
|
||||
include: ["./registry/ui/registry.json"],
|
||||
})
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.data.items).toEqual([])
|
||||
}
|
||||
})
|
||||
|
||||
it("should require name and homepage for root registries", () => {
|
||||
const result = registrySchema.safeParse({
|
||||
include: ["./registry/ui/registry.json"],
|
||||
})
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it("should reject registries without items or include", () => {
|
||||
const result = registryChunkSchema.safeParse({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
})
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -198,37 +198,11 @@ export type RegistryBaseItem = Extract<RegistryItem, { type: "registry:base" }>
|
||||
// Helper type for registry:font items specifically.
|
||||
export type RegistryFontItem = Extract<RegistryItem, { type: "registry:font" }>
|
||||
|
||||
const registryBaseSchema = z
|
||||
.object({
|
||||
$schema: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
homepage: z.string().optional(),
|
||||
include: z.array(z.string()).optional(),
|
||||
items: z.array(registryItemSchema).optional(),
|
||||
})
|
||||
.refine(
|
||||
(registry) =>
|
||||
registry.items !== undefined || registry.include !== undefined,
|
||||
{
|
||||
message: "Registry must define at least one of `items` or `include`.",
|
||||
path: ["items"],
|
||||
}
|
||||
)
|
||||
|
||||
export const registryChunkSchema = registryBaseSchema.transform((registry) => ({
|
||||
...registry,
|
||||
items: registry.items ?? [],
|
||||
}))
|
||||
|
||||
export const registrySchema = registryChunkSchema.pipe(
|
||||
z.object({
|
||||
$schema: z.string().optional(),
|
||||
name: z.string(),
|
||||
homepage: z.string(),
|
||||
include: z.array(z.string()).optional(),
|
||||
items: z.array(registryItemSchema),
|
||||
})
|
||||
)
|
||||
export const registrySchema = z.object({
|
||||
name: z.string(),
|
||||
homepage: z.string(),
|
||||
items: z.array(registryItemSchema),
|
||||
})
|
||||
|
||||
export type Registry = z.infer<typeof registrySchema>
|
||||
|
||||
|
||||
@@ -1,594 +0,0 @@
|
||||
import * as fs from "fs/promises"
|
||||
import { tmpdir } from "os"
|
||||
import * as path from "path"
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import { validateRegistry } from "./validate"
|
||||
|
||||
describe("validateRegistry", () => {
|
||||
it("validates a buildable source registry with include", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
include: ["components/ui/registry.json"],
|
||||
}),
|
||||
"components/ui/registry.json": JSON.stringify({
|
||||
items: [
|
||||
{
|
||||
name: "button",
|
||||
type: "registry:ui",
|
||||
registryDependencies: [
|
||||
"input",
|
||||
"@acme/dialog",
|
||||
"https://example.com/r/card.json",
|
||||
],
|
||||
files: [
|
||||
{
|
||||
path: "button.tsx",
|
||||
type: "registry:ui",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
"components/ui/button.tsx": "export function Button() {}",
|
||||
})
|
||||
|
||||
const report = await validateRegistry({
|
||||
cwd,
|
||||
registryFile: "registry.json",
|
||||
})
|
||||
|
||||
expect(report.valid).toBe(true)
|
||||
expect(report.registryFiles).toBe(2)
|
||||
expect(
|
||||
report.registryFilePaths.map((filePath) => path.relative(cwd, filePath))
|
||||
).toEqual(["registry.json", path.join("components", "ui", "registry.json")])
|
||||
expect(report.items).toBe(1)
|
||||
expect(report.diagnostics).toEqual([])
|
||||
})
|
||||
|
||||
it("validates an empty source registry", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
items: [],
|
||||
}),
|
||||
})
|
||||
|
||||
const report = await validateRegistry({
|
||||
cwd,
|
||||
registryFile: "registry.json",
|
||||
})
|
||||
|
||||
expect(report.valid).toBe(true)
|
||||
expect(report.registryFiles).toBe(1)
|
||||
expect(report.items).toBe(0)
|
||||
})
|
||||
|
||||
it("preserves cwd-relative files for legacy single-file registries", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
items: [
|
||||
{
|
||||
name: "button",
|
||||
type: "registry:ui",
|
||||
files: [
|
||||
{
|
||||
path: "button.tsx",
|
||||
type: "registry:ui",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
"button.tsx": "export function Button() {}",
|
||||
})
|
||||
|
||||
const report = await validateRegistry({
|
||||
cwd,
|
||||
registryFile: "registry.json",
|
||||
})
|
||||
|
||||
expect(report.valid).toBe(true)
|
||||
expect(report.diagnostics).toEqual([])
|
||||
})
|
||||
|
||||
it("collects independent diagnostics across include branches", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
include: ["components/ui.json", "hooks/registry.json"],
|
||||
items: [
|
||||
{
|
||||
name: "button",
|
||||
type: "registry:ui",
|
||||
},
|
||||
],
|
||||
}),
|
||||
"hooks/registry.json": JSON.stringify({
|
||||
items: [
|
||||
{
|
||||
name: "button",
|
||||
type: "registry:hook",
|
||||
files: [
|
||||
{
|
||||
path: "missing.ts",
|
||||
type: "registry:hook",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
const report = await validateRegistry({
|
||||
cwd,
|
||||
registryFile: "registry.json",
|
||||
})
|
||||
|
||||
expect(report.valid).toBe(false)
|
||||
expect(report.diagnostics.map((diagnostic) => diagnostic.message)).toEqual(
|
||||
expect.arrayContaining([
|
||||
'Include "components/ui.json" must explicitly reference a registry.json file.',
|
||||
expect.stringContaining('Duplicate registry item name "button"'),
|
||||
'File "missing.ts" was not found or could not be read.',
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("continues validating valid items when another item is invalid", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
items: [
|
||||
{
|
||||
name: "button",
|
||||
type: "registry:ui",
|
||||
files: [
|
||||
{
|
||||
path: "missing.tsx",
|
||||
type: "registry:ui",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "brand-font",
|
||||
type: "registry:font",
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
const report = await validateRegistry({
|
||||
cwd,
|
||||
registryFile: "registry.json",
|
||||
})
|
||||
|
||||
expect(report.valid).toBe(false)
|
||||
expect(report.items).toBe(2)
|
||||
expect(report.diagnostics).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
itemIndex: 0,
|
||||
itemName: "button",
|
||||
message: 'File "missing.tsx" was not found or could not be read.',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
itemIndex: 1,
|
||||
itemName: "brand-font",
|
||||
message: "font: Required",
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("reports all root-level issues for an empty object", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({}),
|
||||
})
|
||||
|
||||
const report = await validateRegistry({
|
||||
cwd,
|
||||
registryFile: "registry.json",
|
||||
})
|
||||
|
||||
expect(report.valid).toBe(false)
|
||||
expect(report.diagnostics.map((diagnostic) => diagnostic.message)).toEqual(
|
||||
expect.arrayContaining([
|
||||
'Root registry.json must define "name".',
|
||||
'Root registry.json must define "homepage".',
|
||||
"Registry must define at least one of `items` or `include`.",
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("filters internal registry item types from item type diagnostics", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
items: [
|
||||
{
|
||||
name: "button",
|
||||
type: "registry:unknown",
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
const report = await validateRegistry({
|
||||
cwd,
|
||||
registryFile: "registry.json",
|
||||
})
|
||||
|
||||
expect(report.valid).toBe(false)
|
||||
expect(report.diagnostics).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining("Invalid registry item type"),
|
||||
}),
|
||||
])
|
||||
)
|
||||
expect(
|
||||
report.diagnostics.some(
|
||||
(diagnostic) =>
|
||||
diagnostic.message.includes("registry:example") ||
|
||||
diagnostic.message.includes("registry:internal")
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it("requires the root registry file to be named registry.json", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.flat.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
items: [],
|
||||
}),
|
||||
})
|
||||
|
||||
const report = await validateRegistry({
|
||||
cwd,
|
||||
registryFile: "registry.flat.json",
|
||||
})
|
||||
|
||||
expect(report.valid).toBe(false)
|
||||
expect(report.diagnostics).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
message: "Root source registry file must be named registry.json.",
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("reports missing root registry files as validation diagnostics", async () => {
|
||||
const cwd = await fs.mkdtemp(path.join(tmpdir(), "shadcn-validate-"))
|
||||
|
||||
const report = await validateRegistry({
|
||||
cwd,
|
||||
registryFile: "registry.json",
|
||||
})
|
||||
|
||||
expect(report.valid).toBe(false)
|
||||
expect(report.registryFiles).toBe(1)
|
||||
expect(report.diagnostics).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
message: "Registry file was not found or could not be read.",
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("reports include cycles", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
include: ["registry.json"],
|
||||
}),
|
||||
})
|
||||
|
||||
const report = await validateRegistry({
|
||||
cwd,
|
||||
registryFile: "registry.json",
|
||||
})
|
||||
|
||||
expect(report.valid).toBe(false)
|
||||
expect(report.diagnostics).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining("Registry include cycle detected"),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("reports include paths that are remote, absolute, or parent-traversing", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
include: [
|
||||
"https://example.com/registry.json",
|
||||
path.join(cwdRoot(), "registry.json"),
|
||||
"../registry.json",
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
const report = await validateRegistry({
|
||||
cwd,
|
||||
registryFile: "registry.json",
|
||||
})
|
||||
|
||||
expect(report.valid).toBe(false)
|
||||
expect(report.diagnostics.map((diagnostic) => diagnostic.message)).toEqual(
|
||||
expect.arrayContaining([
|
||||
'Remote include "https://example.com/registry.json" is not supported.',
|
||||
expect.stringContaining("must be relative"),
|
||||
'Include "../registry.json" cannot use parent-directory traversal.',
|
||||
])
|
||||
)
|
||||
expect(report.diagnostics).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
includePath: "../registry.json",
|
||||
suggestion:
|
||||
"Registry includes must descend from the including chunk. Move shared registries into the registry root and include them from there.",
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("reports root registry files outside cwd", async () => {
|
||||
const cwd = await fs.mkdtemp(path.join(tmpdir(), "shadcn-validate-"))
|
||||
const outside = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
items: [],
|
||||
}),
|
||||
})
|
||||
|
||||
const report = await validateRegistry({
|
||||
cwd,
|
||||
registryFile: path.relative(cwd, path.join(outside, "registry.json")),
|
||||
})
|
||||
|
||||
expect(report.valid).toBe(false)
|
||||
expect(report.registryFiles).toBe(0)
|
||||
expect(report.diagnostics).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining(
|
||||
"Root registry file must stay inside"
|
||||
),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("reports include trees that are too deep", async () => {
|
||||
const files: Record<string, string> = {}
|
||||
const depth = 33
|
||||
|
||||
for (let index = 0; index <= depth; index++) {
|
||||
const filePath =
|
||||
index === 0
|
||||
? "registry.json"
|
||||
: path.join(...getIncludeSegments(index), "registry.json")
|
||||
const nextPath =
|
||||
index === depth
|
||||
? undefined
|
||||
: path.join(...getIncludeSegments(index + 1), "registry.json")
|
||||
|
||||
files[filePath] = JSON.stringify({
|
||||
...(index === 0
|
||||
? {
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
}
|
||||
: {}),
|
||||
...(nextPath
|
||||
? { include: [path.relative(path.dirname(filePath), nextPath)] }
|
||||
: { items: [] }),
|
||||
})
|
||||
}
|
||||
|
||||
const cwd = await createFixture(files)
|
||||
|
||||
const report = await validateRegistry({
|
||||
cwd,
|
||||
registryFile: "registry.json",
|
||||
})
|
||||
|
||||
expect(report.valid).toBe(false)
|
||||
expect(report.diagnostics).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining("Registry include tree is too deep"),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("reports registry files included through multiple branches", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
include: [
|
||||
"components/registry.json",
|
||||
"components/shared/registry.json",
|
||||
],
|
||||
}),
|
||||
"components/registry.json": JSON.stringify({
|
||||
include: ["shared/registry.json"],
|
||||
}),
|
||||
"components/shared/registry.json": JSON.stringify({
|
||||
items: [],
|
||||
}),
|
||||
})
|
||||
|
||||
const report = await validateRegistry({
|
||||
cwd,
|
||||
registryFile: "registry.json",
|
||||
})
|
||||
|
||||
expect(report.valid).toBe(false)
|
||||
expect(report.registryFiles).toBe(3)
|
||||
expect(
|
||||
report.registryFilePaths.map((filePath) => path.relative(cwd, filePath))
|
||||
).toEqual([
|
||||
"registry.json",
|
||||
path.join("components", "registry.json"),
|
||||
path.join("components", "shared", "registry.json"),
|
||||
])
|
||||
expect(report.diagnostics).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
message: expect.stringContaining(
|
||||
"Registry file included more than once"
|
||||
),
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("reports missing root registry metadata", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
items: [],
|
||||
}),
|
||||
})
|
||||
|
||||
const report = await validateRegistry({
|
||||
cwd,
|
||||
registryFile: "registry.json",
|
||||
})
|
||||
|
||||
expect(report.valid).toBe(false)
|
||||
expect(report.diagnostics.map((diagnostic) => diagnostic.message)).toEqual(
|
||||
expect.arrayContaining([
|
||||
'Root registry.json must define "name".',
|
||||
'Root registry.json must define "homepage".',
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("reports invalid JSON and missing includes without validating dependency names", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
include: ["components/ui/registry.json", "hooks/registry.json"],
|
||||
items: [
|
||||
{
|
||||
name: "card",
|
||||
type: "registry:ui",
|
||||
registryDependencies: ["input"],
|
||||
},
|
||||
],
|
||||
}),
|
||||
"components/ui/registry.json": "{",
|
||||
})
|
||||
|
||||
const report = await validateRegistry({
|
||||
cwd,
|
||||
registryFile: "registry.json",
|
||||
})
|
||||
|
||||
expect(report.valid).toBe(false)
|
||||
expect(report.registryFiles).toBe(3)
|
||||
expect(
|
||||
report.registryFilePaths.map((filePath) => path.relative(cwd, filePath))
|
||||
).toEqual([
|
||||
"registry.json",
|
||||
path.join("components", "ui", "registry.json"),
|
||||
path.join("hooks", "registry.json"),
|
||||
])
|
||||
expect(report.diagnostics.map((diagnostic) => diagnostic.message)).toEqual(
|
||||
expect.arrayContaining([
|
||||
"Registry file contains invalid JSON.",
|
||||
"Registry file was not found or could not be read.",
|
||||
])
|
||||
)
|
||||
expect(
|
||||
report.diagnostics.some((diagnostic) =>
|
||||
diagnostic.message.includes("input")
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it("reports remote and parent-traversing item file paths", async () => {
|
||||
const cwd = await createFixture({
|
||||
"registry.json": JSON.stringify({
|
||||
name: "example",
|
||||
homepage: "https://example.com",
|
||||
include: ["components/ui/registry.json"],
|
||||
}),
|
||||
"components/ui/registry.json": JSON.stringify({
|
||||
items: [
|
||||
{
|
||||
name: "button",
|
||||
type: "registry:ui",
|
||||
files: [
|
||||
{
|
||||
path: "https://example.com/button.tsx",
|
||||
type: "registry:ui",
|
||||
},
|
||||
{
|
||||
path: "../shared/button.tsx",
|
||||
type: "registry:ui",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
const report = await validateRegistry({
|
||||
cwd,
|
||||
registryFile: "registry.json",
|
||||
})
|
||||
|
||||
expect(report.valid).toBe(false)
|
||||
expect(report.diagnostics.map((diagnostic) => diagnostic.message)).toEqual(
|
||||
expect.arrayContaining([
|
||||
'File path "https://example.com/button.tsx" cannot be remote.',
|
||||
'File path "../shared/button.tsx" cannot use parent-directory traversal.',
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
async function createFixture(files: Record<string, string>) {
|
||||
const cwd = await fs.mkdtemp(path.join(tmpdir(), "shadcn-validate-"))
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(files).map(async ([filePath, content]) => {
|
||||
const targetPath = path.join(cwd, filePath)
|
||||
await fs.mkdir(path.dirname(targetPath), { recursive: true })
|
||||
await fs.writeFile(targetPath, content)
|
||||
})
|
||||
)
|
||||
|
||||
return cwd
|
||||
}
|
||||
|
||||
function cwdRoot() {
|
||||
return path.parse(process.cwd()).root
|
||||
}
|
||||
|
||||
function getIncludeSegments(depth: number) {
|
||||
return Array.from({ length: depth }, (_, index) => `level-${index + 1}`)
|
||||
}
|
||||
@@ -1,723 +0,0 @@
|
||||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import { isUrl } from "@/src/registry/utils"
|
||||
import {
|
||||
registryItemSchema,
|
||||
registryItemTypeSchema,
|
||||
type RegistryItem,
|
||||
} from "@/src/schema"
|
||||
import { z } from "zod"
|
||||
|
||||
type RegistryChunk = {
|
||||
$schema?: string
|
||||
name?: string
|
||||
homepage?: string
|
||||
hasName?: boolean
|
||||
hasHomepage?: boolean
|
||||
include?: string[]
|
||||
items: RegistryItem[]
|
||||
}
|
||||
|
||||
type RegistryItemSource = {
|
||||
registryFile: string
|
||||
registryDir: string
|
||||
itemIndex: number
|
||||
}
|
||||
|
||||
type RegistryValidationDiagnostic = {
|
||||
registryFile: string
|
||||
message: string
|
||||
suggestion?: string
|
||||
itemName?: string
|
||||
itemIndex?: number
|
||||
includePath?: string
|
||||
filePath?: string
|
||||
}
|
||||
|
||||
type RegistryValidationContext = {
|
||||
cwd: string
|
||||
rootFile: string
|
||||
usesInclude: boolean
|
||||
diagnostics: RegistryValidationDiagnostic[]
|
||||
registryFiles: Set<string>
|
||||
checkedRegistryFiles: Set<string>
|
||||
itemsChecked: number
|
||||
itemSourcesByItem: Map<RegistryItem, RegistryItemSource>
|
||||
firstIncludedFrom: Map<string, string>
|
||||
}
|
||||
|
||||
const MAX_INCLUDE_DEPTH = 32
|
||||
const PUBLIC_REGISTRY_ITEM_TYPES = registryItemTypeSchema.options.filter(
|
||||
(type) => type !== "registry:example" && type !== "registry:internal"
|
||||
)
|
||||
const registryObjectSchema = z.record(z.string(), z.unknown())
|
||||
const registryIncludeSchema = z.array(z.string())
|
||||
const registryItemsSchema = z.array(z.unknown())
|
||||
|
||||
export async function validateRegistry(options: {
|
||||
cwd: string
|
||||
registryFile: string
|
||||
}) {
|
||||
const cwd = path.resolve(options.cwd)
|
||||
const rootFile = path.resolve(cwd, options.registryFile)
|
||||
const context: RegistryValidationContext = {
|
||||
cwd,
|
||||
rootFile,
|
||||
usesInclude: false,
|
||||
diagnostics: [],
|
||||
registryFiles: new Set(),
|
||||
checkedRegistryFiles: new Set(),
|
||||
itemsChecked: 0,
|
||||
itemSourcesByItem: new Map(),
|
||||
firstIncludedFrom: new Map(),
|
||||
}
|
||||
|
||||
if (path.basename(rootFile) !== "registry.json") {
|
||||
addDiagnostic(context, {
|
||||
registryFile: rootFile,
|
||||
message: "Root source registry file must be named registry.json.",
|
||||
suggestion:
|
||||
"Rename the file to registry.json and pass that file to shadcn registry validate.",
|
||||
})
|
||||
}
|
||||
|
||||
if (!isPathInside(rootFile, cwd)) {
|
||||
addDiagnostic(context, {
|
||||
registryFile: rootFile,
|
||||
message: `Root registry file must stay inside ${formatPath(cwd, cwd)}.`,
|
||||
suggestion:
|
||||
"Run the command from the registry root or pass a registry.json file inside --cwd.",
|
||||
})
|
||||
return createValidationResult(context, [])
|
||||
}
|
||||
|
||||
const rootRegistry = await readRegistryFile(rootFile, context)
|
||||
if (!rootRegistry) {
|
||||
return createValidationResult(context, [])
|
||||
}
|
||||
|
||||
context.usesInclude = !!rootRegistry.include?.length
|
||||
validateRootRegistry(rootRegistry, rootFile, context)
|
||||
|
||||
const items = await collectRegistryItems(rootFile, rootRegistry, context, [])
|
||||
|
||||
validateDuplicateItems(items, context)
|
||||
await validateRegistryItems(items, context)
|
||||
|
||||
return createValidationResult(context, items)
|
||||
}
|
||||
|
||||
async function collectRegistryItems(
|
||||
registryFile: string,
|
||||
registry: RegistryChunk,
|
||||
context: RegistryValidationContext,
|
||||
chain: string[]
|
||||
): Promise<RegistryItem[]> {
|
||||
if (chain.length >= MAX_INCLUDE_DEPTH) {
|
||||
addDiagnostic(context, {
|
||||
registryFile,
|
||||
message: `Registry include tree is too deep. The maximum include depth is ${MAX_INCLUDE_DEPTH}.`,
|
||||
suggestion:
|
||||
"Flatten part of the registry include tree or reduce nested include depth.",
|
||||
})
|
||||
return []
|
||||
}
|
||||
|
||||
if (chain.includes(registryFile)) {
|
||||
addDiagnostic(context, {
|
||||
registryFile,
|
||||
message: `Registry include cycle detected: ${formatIncludeCycle([
|
||||
...chain,
|
||||
registryFile,
|
||||
])}.`,
|
||||
suggestion: "Remove one include so the registry graph is acyclic.",
|
||||
})
|
||||
return []
|
||||
}
|
||||
|
||||
const includedFrom = chain.at(-1) ?? registryFile
|
||||
const existingSource = context.firstIncludedFrom.get(registryFile)
|
||||
if (existingSource) {
|
||||
addDiagnostic(context, {
|
||||
registryFile,
|
||||
message: `Registry file included more than once. First included from ${formatPath(
|
||||
existingSource,
|
||||
context.cwd
|
||||
)}, then included from ${formatPath(includedFrom, context.cwd)}.`,
|
||||
suggestion:
|
||||
"Remove one include or move shared items into a single included registry.json.",
|
||||
})
|
||||
return []
|
||||
}
|
||||
|
||||
context.registryFiles.add(registryFile)
|
||||
context.firstIncludedFrom.set(registryFile, includedFrom)
|
||||
|
||||
const registryDir = path.dirname(registryFile)
|
||||
const nextChain = [...chain, registryFile]
|
||||
const includedItems: RegistryItem[] = []
|
||||
|
||||
for (const includePath of registry.include ?? []) {
|
||||
const includedRegistryFile = resolveIncludePath(
|
||||
includePath,
|
||||
registryFile,
|
||||
registryDir,
|
||||
context
|
||||
)
|
||||
if (!includedRegistryFile) {
|
||||
continue
|
||||
}
|
||||
|
||||
const includedRegistry = await readRegistryFile(
|
||||
includedRegistryFile,
|
||||
context
|
||||
)
|
||||
if (!includedRegistry) {
|
||||
continue
|
||||
}
|
||||
|
||||
const items = await collectRegistryItems(
|
||||
includedRegistryFile,
|
||||
includedRegistry,
|
||||
context,
|
||||
nextChain
|
||||
)
|
||||
includedItems.push(...items)
|
||||
}
|
||||
|
||||
const itemRegistryDir =
|
||||
// Preserve legacy single-file registry behavior: item files resolve from cwd.
|
||||
!context.usesInclude && registryFile === context.rootFile
|
||||
? context.cwd
|
||||
: registryDir
|
||||
|
||||
registry.items.forEach((item, itemIndex) => {
|
||||
context.itemSourcesByItem.set(item, {
|
||||
registryFile,
|
||||
registryDir: itemRegistryDir,
|
||||
itemIndex,
|
||||
})
|
||||
})
|
||||
|
||||
return [...includedItems, ...registry.items]
|
||||
}
|
||||
|
||||
async function readRegistryFile(
|
||||
registryFile: string,
|
||||
context: RegistryValidationContext
|
||||
) {
|
||||
context.checkedRegistryFiles.add(registryFile)
|
||||
|
||||
let content: string
|
||||
try {
|
||||
content = await fs.readFile(registryFile, "utf-8")
|
||||
} catch {
|
||||
addDiagnostic(context, {
|
||||
registryFile,
|
||||
message: "Registry file was not found or could not be read.",
|
||||
suggestion: "Check that the registry.json file exists and is readable.",
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
let json: unknown
|
||||
try {
|
||||
json = JSON.parse(content)
|
||||
} catch {
|
||||
addDiagnostic(context, {
|
||||
registryFile,
|
||||
message: "Registry file contains invalid JSON.",
|
||||
suggestion: "Fix the JSON syntax in the registry.json file.",
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
return parseRegistryJson(json, registryFile, context)
|
||||
}
|
||||
|
||||
function validateRootRegistry(
|
||||
registry: RegistryChunk,
|
||||
registryFile: string,
|
||||
context: RegistryValidationContext
|
||||
) {
|
||||
if (!registry.name && !registry.hasName) {
|
||||
addDiagnostic(context, {
|
||||
registryFile,
|
||||
message: 'Root registry.json must define "name".',
|
||||
suggestion: 'Add a top-level "name" field to the root registry.json.',
|
||||
})
|
||||
}
|
||||
|
||||
if (!registry.homepage && !registry.hasHomepage) {
|
||||
addDiagnostic(context, {
|
||||
registryFile,
|
||||
message: 'Root registry.json must define "homepage".',
|
||||
suggestion: 'Add a top-level "homepage" field to the root registry.json.',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function resolveIncludePath(
|
||||
includePath: string,
|
||||
registryFile: string,
|
||||
registryDir: string,
|
||||
context: RegistryValidationContext
|
||||
) {
|
||||
if (isUrl(includePath)) {
|
||||
addDiagnostic(context, {
|
||||
registryFile,
|
||||
includePath,
|
||||
message: `Remote include "${includePath}" is not supported.`,
|
||||
suggestion:
|
||||
"Use a relative path to an explicit registry.json file in the same repository.",
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
if (path.isAbsolute(includePath)) {
|
||||
addDiagnostic(context, {
|
||||
registryFile,
|
||||
includePath,
|
||||
message: `Include "${includePath}" must be relative.`,
|
||||
suggestion: 'Use a path like "components/ui/registry.json".',
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
if (hasParentTraversal(includePath)) {
|
||||
addDiagnostic(context, {
|
||||
registryFile,
|
||||
includePath,
|
||||
message: `Include "${includePath}" cannot use parent-directory traversal.`,
|
||||
suggestion:
|
||||
"Registry includes must descend from the including chunk. Move shared registries into the registry root and include them from there.",
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
if (path.basename(includePath) !== "registry.json") {
|
||||
addDiagnostic(context, {
|
||||
registryFile,
|
||||
includePath,
|
||||
message: `Include "${includePath}" must explicitly reference a registry.json file.`,
|
||||
suggestion: 'Use a path like "components/ui/registry.json".',
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
const resolvedPath = path.resolve(registryDir, includePath)
|
||||
if (!isPathInside(resolvedPath, context.cwd)) {
|
||||
addDiagnostic(context, {
|
||||
registryFile,
|
||||
includePath,
|
||||
message: `Include "${includePath}" must stay inside ${formatPath(
|
||||
context.cwd,
|
||||
context.cwd
|
||||
)}.`,
|
||||
suggestion: "Keep included registry.json files inside the registry root.",
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
return resolvedPath
|
||||
}
|
||||
|
||||
function validateDuplicateItems(
|
||||
items: RegistryItem[],
|
||||
context: RegistryValidationContext
|
||||
) {
|
||||
const seen = new Map<string, RegistryItem>()
|
||||
|
||||
for (const item of items) {
|
||||
const existing = seen.get(item.name)
|
||||
if (!existing) {
|
||||
seen.set(item.name, item)
|
||||
continue
|
||||
}
|
||||
|
||||
const firstSource = context.itemSourcesByItem.get(existing)
|
||||
const secondSource = context.itemSourcesByItem.get(item)
|
||||
addDiagnostic(context, {
|
||||
registryFile: secondSource?.registryFile ?? context.rootFile,
|
||||
itemName: item.name,
|
||||
itemIndex: secondSource?.itemIndex,
|
||||
message: `Duplicate registry item name "${item.name}". First defined at ${formatItemSource(
|
||||
firstSource,
|
||||
context.cwd
|
||||
)}.`,
|
||||
suggestion:
|
||||
"Rename one of these items so each name is unique across the resolved registry.",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function validateRegistryItems(
|
||||
items: RegistryItem[],
|
||||
context: RegistryValidationContext
|
||||
) {
|
||||
const registryRootDir = getRegistryRootDir(context)
|
||||
|
||||
for (const item of items) {
|
||||
const source = context.itemSourcesByItem.get(item)
|
||||
const registryItem = {
|
||||
...rewriteRegistryItemFilePaths(item, context, registryRootDir),
|
||||
$schema: "https://ui.shadcn.com/schema/registry-item.json",
|
||||
}
|
||||
|
||||
for (let index = 0; index < (item.files?.length ?? 0); index++) {
|
||||
const file = item.files?.[index]
|
||||
if (!file || !source) {
|
||||
continue
|
||||
}
|
||||
|
||||
const sourcePath = validateRegistryItemFilePath(
|
||||
item,
|
||||
file.path,
|
||||
source,
|
||||
context
|
||||
)
|
||||
if (!sourcePath) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.readFile(sourcePath, "utf-8")
|
||||
} catch {
|
||||
addDiagnostic(context, {
|
||||
registryFile: source.registryFile,
|
||||
itemName: item.name,
|
||||
itemIndex: source.itemIndex,
|
||||
filePath: file.path,
|
||||
message: `File "${file.path}" was not found or could not be read.`,
|
||||
suggestion:
|
||||
"Make sure the file path is relative to the registry.json file that declares the item.",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const result = registryItemSchema.safeParse(registryItem)
|
||||
if (!result.success) {
|
||||
addZodDiagnostics(
|
||||
result.error,
|
||||
source?.registryFile ?? context.rootFile,
|
||||
context,
|
||||
{
|
||||
itemName: item.name,
|
||||
itemIndex: source?.itemIndex,
|
||||
suggestion:
|
||||
"Update the registry item so the built item matches the registry item schema.",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateRegistryItemFilePath(
|
||||
item: RegistryItem,
|
||||
filePath: string,
|
||||
source: RegistryItemSource,
|
||||
context: RegistryValidationContext
|
||||
) {
|
||||
if (isUrl(filePath)) {
|
||||
addDiagnostic(context, {
|
||||
registryFile: source.registryFile,
|
||||
itemName: item.name,
|
||||
itemIndex: source.itemIndex,
|
||||
filePath,
|
||||
message: `File path "${filePath}" cannot be remote.`,
|
||||
suggestion:
|
||||
"Use a local file path relative to the registry.json file that declares the item.",
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
if (path.isAbsolute(filePath)) {
|
||||
addDiagnostic(context, {
|
||||
registryFile: source.registryFile,
|
||||
itemName: item.name,
|
||||
itemIndex: source.itemIndex,
|
||||
filePath,
|
||||
message: `File path "${filePath}" must be relative.`,
|
||||
suggestion:
|
||||
"Use a local file path relative to the registry.json file that declares the item.",
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
if (hasParentTraversal(filePath)) {
|
||||
addDiagnostic(context, {
|
||||
registryFile: source.registryFile,
|
||||
itemName: item.name,
|
||||
itemIndex: source.itemIndex,
|
||||
filePath,
|
||||
message: `File path "${filePath}" cannot use parent-directory traversal.`,
|
||||
suggestion: "Keep item files inside the registry chunk directory.",
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
const sourcePath = path.resolve(source.registryDir, filePath)
|
||||
if (!isPathInside(sourcePath, source.registryDir)) {
|
||||
addDiagnostic(context, {
|
||||
registryFile: source.registryFile,
|
||||
itemName: item.name,
|
||||
itemIndex: source.itemIndex,
|
||||
filePath,
|
||||
message: `File path "${filePath}" must stay inside the registry chunk directory.`,
|
||||
suggestion:
|
||||
"Move the file into the same registry chunk directory or update the registry item path.",
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
return sourcePath
|
||||
}
|
||||
|
||||
function rewriteRegistryItemFilePaths(
|
||||
item: RegistryItem,
|
||||
context: RegistryValidationContext,
|
||||
rootDir: string
|
||||
) {
|
||||
const source = context.itemSourcesByItem.get(item)
|
||||
|
||||
return {
|
||||
...item,
|
||||
files: item.files?.map((file) => {
|
||||
const sourcePath = path.resolve(
|
||||
source?.registryDir ?? context.cwd,
|
||||
file.path
|
||||
)
|
||||
|
||||
return {
|
||||
...file,
|
||||
path: path.relative(rootDir, sourcePath).split(path.sep).join("/"),
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
function parseRegistryJson(
|
||||
json: unknown,
|
||||
registryFile: string,
|
||||
context: RegistryValidationContext
|
||||
) {
|
||||
const registryResult = registryObjectSchema.safeParse(json)
|
||||
if (!registryResult.success) {
|
||||
addZodDiagnostics(registryResult.error, registryFile, context, {
|
||||
suggestion: "Update the registry.json file so it matches the schema.",
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
const registry = registryResult.data
|
||||
const chunk: RegistryChunk = {
|
||||
$schema: getOptionalString(registry, "$schema", registryFile, context),
|
||||
name: getOptionalString(registry, "name", registryFile, context),
|
||||
homepage: getOptionalString(registry, "homepage", registryFile, context),
|
||||
hasName: registry.name !== undefined,
|
||||
hasHomepage: registry.homepage !== undefined,
|
||||
items: [],
|
||||
}
|
||||
|
||||
if (registry.include !== undefined) {
|
||||
const result = registryIncludeSchema.safeParse(registry.include)
|
||||
if (!result.success) {
|
||||
addZodDiagnostics(result.error, registryFile, context, {
|
||||
pathPrefix: ["include"],
|
||||
suggestion: "Update include so it is an array of registry.json paths.",
|
||||
})
|
||||
} else {
|
||||
chunk.include = result.data
|
||||
}
|
||||
}
|
||||
|
||||
if (registry.items !== undefined) {
|
||||
const result = registryItemsSchema.safeParse(registry.items)
|
||||
if (!result.success) {
|
||||
addZodDiagnostics(result.error, registryFile, context, {
|
||||
pathPrefix: ["items"],
|
||||
suggestion: "Update items so it is an array of registry items.",
|
||||
})
|
||||
} else {
|
||||
context.itemsChecked += result.data.length
|
||||
chunk.items = parseRegistryItems(result.data, registryFile, context)
|
||||
}
|
||||
}
|
||||
|
||||
if (registry.items === undefined && registry.include === undefined) {
|
||||
addDiagnostic(context, {
|
||||
registryFile,
|
||||
message: "Registry must define at least one of `items` or `include`.",
|
||||
suggestion:
|
||||
'Add an "items" array, an "include" array, or both to registry.json.',
|
||||
})
|
||||
}
|
||||
|
||||
return chunk
|
||||
}
|
||||
|
||||
function parseRegistryItems(
|
||||
items: unknown[],
|
||||
registryFile: string,
|
||||
context: RegistryValidationContext
|
||||
) {
|
||||
const registryItems: RegistryItem[] = []
|
||||
|
||||
items.forEach((item, itemIndex) => {
|
||||
const result = registryItemSchema.safeParse(item)
|
||||
if (!result.success) {
|
||||
addZodDiagnostics(result.error, registryFile, context, {
|
||||
itemName: getRawItemName(item),
|
||||
itemIndex,
|
||||
suggestion:
|
||||
"Update the registry item so it matches the registry item schema.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
registryItems.push(result.data)
|
||||
})
|
||||
|
||||
return registryItems
|
||||
}
|
||||
|
||||
function getOptionalString(
|
||||
registry: Record<string, unknown>,
|
||||
key: string,
|
||||
registryFile: string,
|
||||
context: RegistryValidationContext
|
||||
) {
|
||||
const value = registry[key]
|
||||
if (value === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
return value
|
||||
}
|
||||
|
||||
addDiagnostic(context, {
|
||||
registryFile,
|
||||
message: `${key}: Expected string, received ${typeof value}.`,
|
||||
suggestion: `Update "${key}" so it is a string.`,
|
||||
})
|
||||
}
|
||||
|
||||
function getRawItemName(item: unknown) {
|
||||
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const name = (item as Record<string, unknown>).name
|
||||
return typeof name === "string" ? name : undefined
|
||||
}
|
||||
|
||||
function addZodDiagnostics(
|
||||
error: z.ZodError,
|
||||
registryFile: string,
|
||||
context: RegistryValidationContext,
|
||||
options: {
|
||||
itemName?: string
|
||||
itemIndex?: number
|
||||
pathPrefix?: (string | number)[]
|
||||
suggestion?: string
|
||||
}
|
||||
) {
|
||||
for (const issue of error.errors) {
|
||||
addDiagnostic(context, {
|
||||
registryFile,
|
||||
itemName: options.itemName,
|
||||
itemIndex: options.itemIndex,
|
||||
message: formatZodIssue(issue, options.pathPrefix),
|
||||
suggestion: options.suggestion,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function addDiagnostic(
|
||||
context: RegistryValidationContext,
|
||||
diagnostic: RegistryValidationDiagnostic
|
||||
) {
|
||||
context.diagnostics.push(diagnostic)
|
||||
}
|
||||
|
||||
function createValidationResult(
|
||||
context: RegistryValidationContext,
|
||||
items: RegistryItem[]
|
||||
) {
|
||||
return {
|
||||
valid: context.diagnostics.length === 0,
|
||||
cwd: context.cwd,
|
||||
registryFiles: context.checkedRegistryFiles.size,
|
||||
registryFilePaths: Array.from(context.checkedRegistryFiles),
|
||||
items: context.itemsChecked,
|
||||
diagnostics: context.diagnostics,
|
||||
}
|
||||
}
|
||||
|
||||
function getRegistryRootDir(context: RegistryValidationContext) {
|
||||
return context.usesInclude ? path.dirname(context.rootFile) : context.cwd
|
||||
}
|
||||
|
||||
function hasParentTraversal(filePath: string) {
|
||||
return filePath.split(/[\\/]+/).includes("..")
|
||||
}
|
||||
|
||||
function isPathInside(filePath: string, root: string) {
|
||||
const relative = path.relative(root, filePath)
|
||||
return !!relative && !relative.startsWith("..") && !path.isAbsolute(relative)
|
||||
}
|
||||
|
||||
function formatIncludeCycle(chain: string[]) {
|
||||
return chain
|
||||
.map((file) => formatPath(file, path.dirname(chain[0])))
|
||||
.join(" -> ")
|
||||
}
|
||||
|
||||
function formatItemSource(source: RegistryItemSource | undefined, cwd: string) {
|
||||
if (!source) {
|
||||
return "unknown source"
|
||||
}
|
||||
|
||||
return `${formatPath(source.registryFile, cwd)} items[${source.itemIndex}]`
|
||||
}
|
||||
|
||||
function formatZodPath(issuePath: (string | number)[]) {
|
||||
return issuePath.length ? issuePath.join(".") : "(root)"
|
||||
}
|
||||
|
||||
function formatZodIssue(
|
||||
issue: z.ZodIssue,
|
||||
pathPrefix: (string | number)[] = []
|
||||
) {
|
||||
const path = [...pathPrefix, ...issue.path]
|
||||
|
||||
if (
|
||||
issue.code === z.ZodIssueCode.invalid_union_discriminator &&
|
||||
issue.path.at(-1) === "type"
|
||||
) {
|
||||
return `${formatZodPath(path)}: Invalid registry item type. Expected ${PUBLIC_REGISTRY_ITEM_TYPES.map(
|
||||
(type) => `"${type}"`
|
||||
).join(" | ")}.`
|
||||
}
|
||||
|
||||
return `${formatZodPath(path)}: ${issue.message}`
|
||||
}
|
||||
|
||||
function formatPath(filePath: string, cwd: string) {
|
||||
const relativePath = path.relative(cwd, filePath)
|
||||
|
||||
if (
|
||||
relativePath &&
|
||||
!relativePath.startsWith("..") &&
|
||||
!path.isAbsolute(relativePath)
|
||||
) {
|
||||
return relativePath.split(path.sep).join("/")
|
||||
}
|
||||
|
||||
if (!relativePath) {
|
||||
return "."
|
||||
}
|
||||
|
||||
return filePath
|
||||
}
|
||||
1640
pnpm-lock.yaml
generated
1640
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user