aback-ui 0.0.0-stage → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +85 -2
- package/dist/args.d.ts +15 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +91 -0
- package/dist/args.js.map +1 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +106 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/add.d.ts +29 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +82 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/diff.d.ts +13 -0
- package/dist/commands/diff.d.ts.map +1 -0
- package/dist/commands/diff.js +85 -0
- package/dist/commands/diff.js.map +1 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +45 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/init.d.ts +19 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +97 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/list.d.ts +11 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +42 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/view.d.ts +11 -0
- package/dist/commands/view.d.ts.map +1 -0
- package/dist/commands/view.js +43 -0
- package/dist/commands/view.js.map +1 -0
- package/dist/config.d.ts +66 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +297 -0
- package/dist/config.js.map +1 -0
- package/dist/diff.d.ts +30 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +121 -0
- package/dist/diff.js.map +1 -0
- package/dist/doctor.d.ts +35 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +203 -0
- package/dist/doctor.js.map +1 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +12 -0
- package/dist/errors.js.map +1 -0
- package/dist/help.d.ts +3 -0
- package/dist/help.d.ts.map +1 -0
- package/dist/help.js +48 -0
- package/dist/help.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/install.d.ts +53 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +119 -0
- package/dist/install.js.map +1 -0
- package/dist/logger.d.ts +14 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +47 -0
- package/dist/logger.js.map +1 -0
- package/dist/paths.d.ts +18 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +76 -0
- package/dist/paths.js.map +1 -0
- package/dist/prompt.d.ts +13 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +25 -0
- package/dist/prompt.js.map +1 -0
- package/dist/registry.d.ts +49 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +216 -0
- package/dist/registry.js.map +1 -0
- package/dist/types.d.ts +88 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +59 -4
- package/registry/accordion.json +32 -0
- package/registry/alert-dialog.json +28 -0
- package/registry/alert.json +31 -0
- package/registry/article-layout.json +38 -0
- package/registry/avatar.json +31 -0
- package/registry/badge.json +33 -0
- package/registry/breadcrumbs.json +32 -0
- package/registry/button.json +34 -0
- package/registry/calendar.json +29 -0
- package/registry/card.json +30 -0
- package/registry/checkbox.json +33 -0
- package/registry/code-block.json +29 -0
- package/registry/collapsible.json +28 -0
- package/registry/combobox.json +30 -0
- package/registry/command.json +34 -0
- package/registry/contact-form.json +37 -0
- package/registry/cookie-consent.json +28 -0
- package/registry/copy-button.json +33 -0
- package/registry/cta-banner.json +32 -0
- package/registry/dashboard-shell.json +32 -0
- package/registry/date-picker.json +32 -0
- package/registry/description-list.json +28 -0
- package/registry/dialog.json +33 -0
- package/registry/dropdown-menu.json +33 -0
- package/registry/dropzone.json +31 -0
- package/registry/empty-state.json +32 -0
- package/registry/faq.json +31 -0
- package/registry/feature-grid.json +33 -0
- package/registry/field.json +29 -0
- package/registry/hero.json +32 -0
- package/registry/hover-card.json +28 -0
- package/registry/index.json +1633 -0
- package/registry/input-group.json +30 -0
- package/registry/input.json +29 -0
- package/registry/kbd.json +29 -0
- package/registry/label.json +29 -0
- package/registry/layout.json +32 -0
- package/registry/newsletter.json +35 -0
- package/registry/not-found.json +34 -0
- package/registry/page-header.json +29 -0
- package/registry/pagination.json +25 -0
- package/registry/popover.json +28 -0
- package/registry/pricing.json +35 -0
- package/registry/progress.json +28 -0
- package/registry/radio-group.json +28 -0
- package/registry/rating.json +32 -0
- package/registry/reading-progress.json +29 -0
- package/registry/relative-time.json +29 -0
- package/registry/scroll-area.json +28 -0
- package/registry/section-heading.json +30 -0
- package/registry/select.json +32 -0
- package/registry/separator.json +32 -0
- package/registry/settings-section.json +32 -0
- package/registry/share-buttons.json +34 -0
- package/registry/sheet.json +34 -0
- package/registry/sign-in.json +31 -0
- package/registry/site-footer.json +31 -0
- package/registry/site-header.json +37 -0
- package/registry/skeleton.json +28 -0
- package/registry/skip-link.json +30 -0
- package/registry/slider.json +28 -0
- package/registry/spinner.json +32 -0
- package/registry/stat.json +32 -0
- package/registry/steps.json +31 -0
- package/registry/switch.json +32 -0
- package/registry/table-of-contents.json +32 -0
- package/registry/table.json +28 -0
- package/registry/tabs.json +29 -0
- package/registry/textarea.json +29 -0
- package/registry/theme-toggle.json +34 -0
- package/registry/theme.json +16 -0
- package/registry/toast.json +29 -0
- package/registry/toggle-group.json +29 -0
- package/registry/tooltip.json +29 -0
- package/registry/users-table.json +36 -0
- package/registry/utils.json +19 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "alert-dialog",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Alert Dialog",
|
|
6
|
+
"description": "A modal that requires an explicit response before an irreversible action runs.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"radix-ui"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://abackui.com/r/utils.json"
|
|
12
|
+
],
|
|
13
|
+
"categories": [
|
|
14
|
+
"Overlays"
|
|
15
|
+
],
|
|
16
|
+
"docs": "https://abackui.com/docs/components/alert-dialog",
|
|
17
|
+
"meta": {
|
|
18
|
+
"status": "beta"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
{
|
|
22
|
+
"path": "components/ui/alert-dialog.tsx",
|
|
23
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { AlertDialog as AlertDialogPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst AlertDialog = AlertDialogPrimitive.Root;\nconst AlertDialogTrigger = AlertDialogPrimitive.Trigger;\nconst AlertDialogPortal = AlertDialogPrimitive.Portal;\n\nconst buttonBase =\n \"inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap text-sm font-medium outline-none transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50\";\nconst buttonSizes = {\n default: \"h-9 px-4\",\n sm: \"h-8 px-3\",\n} as const;\n\nfunction AlertDialogOverlay({\n className,\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {\n return (\n <AlertDialogPrimitive.Overlay\n data-slot=\"alert-dialog-overlay\"\n className={cn(\n \"fixed inset-0 z-50 bg-black/40 backdrop-blur-sm dark:bg-black/60\",\n \"data-[state=open]:animate-in data-[state=open]:fade-in-0\",\n \"data-[state=closed]:animate-out data-[state=closed]:fade-out-0\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction AlertDialogContent({\n className,\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {\n return (\n <AlertDialogPortal>\n <AlertDialogOverlay />\n <AlertDialogPrimitive.Content\n data-slot=\"alert-dialog-content\"\n className={cn(\n \"fixed left-1/2 top-1/2 z-50 grid max-h-[calc(100dvh-2rem)] w-[calc(100%-2rem)] max-w-md -translate-x-1/2 -translate-y-1/2 gap-4 overflow-y-auto\",\n \"border border-border bg-popover p-6 text-popover-foreground shadow-2xl\",\n \"duration-200 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95\",\n \"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95\",\n className,\n )}\n {...props}\n />\n </AlertDialogPortal>\n );\n}\n\nfunction AlertDialogHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div data-slot=\"alert-dialog-header\" className={cn(\"flex flex-col gap-1.5\", className)} {...props} />;\n}\n\nfunction AlertDialogFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"alert-dialog-footer\"\n className={cn(\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\", className)}\n {...props}\n />\n );\n}\n\nfunction AlertDialogTitle({\n className,\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {\n return (\n <AlertDialogPrimitive.Title\n data-slot=\"alert-dialog-title\"\n className={cn(\"text-lg font-semibold leading-none tracking-tight\", className)}\n {...props}\n />\n );\n}\n\nfunction AlertDialogDescription({\n className,\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {\n return (\n <AlertDialogPrimitive.Description\n data-slot=\"alert-dialog-description\"\n className={cn(\"text-sm leading-relaxed text-muted-foreground\", className)}\n {...props}\n />\n );\n}\n\nfunction AlertDialogAction({\n className,\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {\n return (\n <AlertDialogPrimitive.Action\n data-slot=\"alert-dialog-action\"\n className={cn(\n buttonBase,\n buttonSizes.default,\n \"bg-primary text-primary-foreground hover:bg-primary/90\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction AlertDialogCancel({\n className,\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {\n return (\n <AlertDialogPrimitive.Cancel\n data-slot=\"alert-dialog-cancel\"\n className={cn(\n buttonBase,\n buttonSizes.default,\n \"border border-input bg-background text-foreground shadow-xs hover:bg-muted\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport {\n AlertDialog,\n AlertDialogTrigger,\n AlertDialogPortal,\n AlertDialogOverlay,\n AlertDialogContent,\n AlertDialogHeader,\n AlertDialogFooter,\n AlertDialogTitle,\n AlertDialogDescription,\n AlertDialogAction,\n AlertDialogCancel,\n};\n",
|
|
24
|
+
"type": "registry:ui",
|
|
25
|
+
"target": "components/ui/alert-dialog.tsx"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "alert",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Alert",
|
|
6
|
+
"description": "Inline status messages for information, success, warning, and destructive feedback.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"class-variance-authority"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://abackui.com/r/utils.json"
|
|
12
|
+
],
|
|
13
|
+
"categories": [
|
|
14
|
+
"Feedback"
|
|
15
|
+
],
|
|
16
|
+
"docs": "https://abackui.com/docs/components/alert",
|
|
17
|
+
"meta": {
|
|
18
|
+
"status": "stable",
|
|
19
|
+
"usedIn": [
|
|
20
|
+
"aback-launch"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
{
|
|
25
|
+
"path": "components/ui/alert.tsx",
|
|
26
|
+
"content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst alertVariants = cva(\n \"relative flex w-full items-start gap-2.5 border p-3 text-sm [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:translate-y-0.5\",\n {\n variants: {\n variant: {\n info:\n \"border-primary-200 bg-brand-wash/60 text-foreground dark:border-primary-800/60\",\n success: \"border-success/25 bg-success-soft text-success-strong\",\n warning: \"border-warning/25 bg-warning-soft text-warning-strong\",\n destructive: \"border-danger/25 bg-danger-soft text-danger-strong\",\n muted: \"border-border bg-muted/40 text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"info\",\n },\n },\n);\n\nexport interface AlertProps\n extends React.ComponentProps<\"div\">,\n VariantProps<typeof alertVariants> {}\n\nfunction Alert({ className, variant = \"info\", ...props }: AlertProps) {\n return (\n <div\n data-slot=\"alert\"\n data-variant={variant}\n className={cn(alertVariants({ variant }), className)}\n {...props}\n />\n );\n}\n\nfunction AlertTitle({ className, ...props }: React.ComponentProps<\"p\">) {\n return (\n <p\n data-slot=\"alert-title\"\n className={cn(\"font-medium leading-snug\", className)}\n {...props}\n />\n );\n}\n\nfunction AlertDescription({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"alert-description\"\n className={cn(\"text-sm leading-relaxed text-current\", className)}\n {...props}\n />\n );\n}\n\nexport { Alert, AlertTitle, AlertDescription, alertVariants };\n",
|
|
27
|
+
"type": "registry:ui",
|
|
28
|
+
"target": "components/ui/alert.tsx"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "article-layout",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "Article layout",
|
|
6
|
+
"description": "A long-form page with meta bar, scoped typography, sticky contents, share links, and reading progress.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@tabler/icons-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://abackui.com/r/avatar.json",
|
|
12
|
+
"https://abackui.com/r/badge.json",
|
|
13
|
+
"https://abackui.com/r/breadcrumbs.json",
|
|
14
|
+
"https://abackui.com/r/collapsible.json",
|
|
15
|
+
"https://abackui.com/r/reading-progress.json",
|
|
16
|
+
"https://abackui.com/r/share-buttons.json",
|
|
17
|
+
"https://abackui.com/r/table-of-contents.json"
|
|
18
|
+
],
|
|
19
|
+
"categories": [
|
|
20
|
+
"Content"
|
|
21
|
+
],
|
|
22
|
+
"docs": "https://abackui.com/docs/blocks/article-layout",
|
|
23
|
+
"meta": {
|
|
24
|
+
"status": "beta",
|
|
25
|
+
"usedIn": [
|
|
26
|
+
"aback-tools",
|
|
27
|
+
"aback-launch"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
{
|
|
32
|
+
"path": "components/blocks/article-layout.tsx",
|
|
33
|
+
"content": "\"use client\";\n\nimport { type ReactNode, useId, useRef } from \"react\";\nimport { IconCalendar, IconChevronDown, IconClock } from \"@tabler/icons-react\";\n\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Breadcrumbs, type BreadcrumbItem } from \"@/components/ui/breadcrumbs\";\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from \"@/components/ui/collapsible\";\nimport { ReadingProgress } from \"@/components/ui/reading-progress\";\nimport { ShareButtons } from \"@/components/ui/share-buttons\";\nimport { TableOfContents, type TableOfContentsItem } from \"@/components/ui/table-of-contents\";\n\nexport interface ArticleAuthor {\n name: string;\n role?: string;\n initials?: string;\n avatarUrl?: string;\n}\n\nexport interface ArticleLayoutBlockProps {\n title: string;\n /** The lead paragraph under the title. */\n description?: ReactNode;\n category?: string;\n author: ArticleAuthor;\n /** ISO date of publication. Rendered as a stable absolute date. */\n publishedAt: string;\n /** Such as \"6 min read\". Compute it from the real word count. */\n readingTime?: string;\n breadcrumbs?: BreadcrumbItem[];\n /** One entry per h2 or h3 in the body, by id. */\n toc?: TableOfContentsItem[];\n /** The canonical URL, for the share links. */\n shareUrl?: string;\n /** The article body. Plain h2, h3, p, ul, ol, blockquote, and code are styled. */\n children: ReactNode;\n}\n\n// Typography for the body, scoped to this block so it never leaks into the page.\nconst prose = [\n \"flex min-w-0 max-w-[72ch] flex-col gap-4 text-sm leading-relaxed text-muted-foreground\",\n \"[&_h2]:mt-6 [&_h2]:scroll-mt-24 [&_h2]:text-2xl [&_h2]:font-bold [&_h2]:tracking-tight [&_h2]:text-foreground\",\n \"[&_h3]:mt-2 [&_h3]:scroll-mt-24 [&_h3]:text-lg [&_h3]:font-semibold [&_h3]:text-foreground\",\n \"[&_strong]:font-semibold [&_strong]:text-foreground\",\n \"[&_a]:text-link [&_a]:underline [&_a]:underline-offset-4 [&_a:hover]:text-link-hover\",\n \"[&_ul]:ml-6 [&_ul]:list-disc [&_ul]:space-y-1.5 [&_ol]:ml-6 [&_ol]:list-decimal [&_ol]:space-y-1.5 [&_li]:marker:text-muted-foreground\",\n \"[&_blockquote]:border-l-4 [&_blockquote]:border-primary-300 [&_blockquote]:py-1 [&_blockquote]:pl-4 [&_blockquote]:italic dark:[&_blockquote]:border-primary-500\",\n \"[&_:not(pre)>code]:bg-muted [&_:not(pre)>code]:px-1 [&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-[0.9em] [&_:not(pre)>code]:text-foreground\",\n].join(\" \");\n\nfunction initialsOf(name: string) {\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n return parts.length > 1 ? `${parts[0]![0]}${parts[1]![0]}`.toUpperCase() : name.slice(0, 2).toUpperCase();\n}\n\n/**\n * A long-form article page: breadcrumbs, title, author and meta bar, a body\n * with scoped typography, a sticky \"On this page\" list, share links, and a\n * reading progress bar. The aside appears when the block's own container is\n * wide enough, so it adapts inside narrow layouts as well as on wide screens.\n */\nexport function ArticleLayoutBlock({\n title,\n description,\n category,\n author,\n publishedAt,\n readingTime,\n breadcrumbs,\n toc = [],\n shareUrl,\n children,\n}: ArticleLayoutBlockProps) {\n const headingId = useId();\n const article = useRef<HTMLElement>(null);\n const published = new Date(publishedAt);\n const validDate = !Number.isNaN(published.getTime());\n\n return (\n <div className=\"@container mx-auto w-full max-w-6xl px-4 py-10 md:px-6\">\n <ReadingProgress target={article} />\n <article ref={article} className=\"flex flex-col gap-6\" aria-labelledby={headingId}>\n <header className=\"flex flex-col gap-4\">\n {breadcrumbs?.length ? <Breadcrumbs items={breadcrumbs} /> : null}\n {category ? <Badge variant=\"brand\" className=\"self-start\">{category}</Badge> : null}\n <h1 id={headingId} className=\"max-w-3xl text-3xl font-bold leading-tight tracking-tight text-foreground text-balance sm:text-4xl\">\n {title}\n </h1>\n {description ? (\n <p className=\"max-w-[72ch] text-base leading-relaxed text-muted-foreground\">{description}</p>\n ) : null}\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-3 border border-border bg-muted/20 p-4\">\n <div className=\"flex items-center gap-3\">\n <Avatar size=\"lg\">\n {author.avatarUrl ? <AvatarImage src={author.avatarUrl} alt=\"\" /> : null}\n <AvatarFallback>{author.initials ?? initialsOf(author.name)}</AvatarFallback>\n </Avatar>\n <div className=\"flex flex-col\">\n <span className=\"text-sm font-semibold text-foreground\">{author.name}</span>\n {author.role ? <span className=\"text-xs text-muted-foreground\">{author.role}</span> : null}\n </div>\n </div>\n <span aria-hidden=\"true\" className=\"hidden h-8 w-px bg-border sm:block\" />\n <ul className=\"flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground\">\n {validDate ? (\n <li className=\"flex items-center gap-1.5\">\n <IconCalendar aria-hidden=\"true\" className=\"size-3.5\" />\n <span className=\"sr-only\">Published </span>\n <time dateTime={published.toISOString()}>\n {new Intl.DateTimeFormat(\"en-US\", { dateStyle: \"medium\", timeZone: \"UTC\" }).format(published)}\n </time>\n </li>\n ) : null}\n {readingTime ? (\n <li className=\"flex items-center gap-1.5\">\n <IconClock aria-hidden=\"true\" className=\"size-3.5\" />\n {readingTime}\n </li>\n ) : null}\n </ul>\n </div>\n </header>\n\n {toc.length ? (\n <Collapsible className=\"border border-border bg-muted/20 @3xl:hidden\">\n <CollapsibleTrigger className=\"group flex w-full items-center justify-between px-4 py-3 text-sm font-semibold text-foreground outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-focus-ring\">\n Contents\n <IconChevronDown\n aria-hidden=\"true\"\n className=\"size-4 text-muted-foreground transition-transform duration-200 group-data-[state=open]:rotate-180 motion-reduce:transition-none\"\n />\n </CollapsibleTrigger>\n <CollapsibleContent className=\"px-4 pb-4\">\n <TableOfContents items={toc} />\n </CollapsibleContent>\n </Collapsible>\n ) : null}\n\n <div className=\"grid gap-10 @3xl:grid-cols-[minmax(0,1fr)_240px] @3xl:items-start\">\n <div className=\"flex min-w-0 flex-col gap-10\">\n <div className={prose}>{children}</div>\n {shareUrl ? (\n <footer className=\"flex flex-wrap items-center justify-between gap-3 border-t border-border pt-6\">\n <span className=\"text-sm font-medium text-foreground\">Share this article</span>\n <ShareButtons url={shareUrl} title={title} networks={[\"x\", \"linkedin\", \"email\"]} />\n </footer>\n ) : null}\n </div>\n {toc.length ? (\n <aside className=\"hidden @3xl:sticky @3xl:top-20 @3xl:block @3xl:max-h-[calc(100vh-6rem)] @3xl:overflow-y-auto\">\n <TableOfContents items={toc} className=\"border border-border bg-muted/20 p-4\" />\n </aside>\n ) : null}\n </div>\n </article>\n </div>\n );\n}\n",
|
|
34
|
+
"type": "registry:block",
|
|
35
|
+
"target": "components/blocks/article-layout.tsx"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "avatar",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Avatar",
|
|
6
|
+
"description": "A square identity chip with an initials fallback, and an overlapping group with a counted overflow.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"radix-ui"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://abackui.com/r/utils.json"
|
|
12
|
+
],
|
|
13
|
+
"categories": [
|
|
14
|
+
"Data Display"
|
|
15
|
+
],
|
|
16
|
+
"docs": "https://abackui.com/docs/components/avatar",
|
|
17
|
+
"meta": {
|
|
18
|
+
"status": "beta",
|
|
19
|
+
"usedIn": [
|
|
20
|
+
"aback-launch"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
{
|
|
25
|
+
"path": "components/ui/avatar.tsx",
|
|
26
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Avatar as AvatarPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype AvatarSize = \"xs\" | \"sm\" | \"default\" | \"lg\" | \"xl\";\n\nconst sizeClasses: Record<AvatarSize, string> = {\n xs: \"size-6 text-[10px]\",\n sm: \"size-8 text-xs\",\n default: \"size-9 text-xs\",\n lg: \"size-11 text-sm\",\n xl: \"size-14 text-base\",\n};\n\nexport interface AvatarProps extends React.ComponentProps<typeof AvatarPrimitive.Root> {\n size?: AvatarSize;\n}\n\nfunction Avatar({ className, size = \"default\", ...props }: AvatarProps) {\n return (\n <AvatarPrimitive.Root\n data-slot=\"avatar\"\n data-size={size}\n className={cn(\n \"relative inline-flex shrink-0 overflow-hidden border border-border bg-muted\",\n sizeClasses[size],\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction AvatarImage({\n className,\n ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Image>) {\n return (\n <AvatarPrimitive.Image\n data-slot=\"avatar-image\"\n className={cn(\"aspect-square size-full object-cover\", className)}\n {...props}\n />\n );\n}\n\nfunction AvatarFallback({\n className,\n ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {\n return (\n <AvatarPrimitive.Fallback\n data-slot=\"avatar-fallback\"\n className={cn(\n \"flex size-full items-center justify-center bg-muted font-medium uppercase tracking-wide text-foreground\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport interface AvatarGroupProps extends React.ComponentProps<\"div\"> {\n /** Avatars shown before the rest collapse into a \"+N\" count. */\n max?: number;\n /** Total people the group stands for, when more exist than were rendered. */\n total?: number;\n /** Size of the overflow count. Match it to the avatars in the group. */\n size?: AvatarSize;\n}\n\n/**\n * Overlapping avatars for a team, reviewers, or recent participants. Extra\n * people collapse into a count with a text alternative. Name the group with\n * `aria-label` and keep each avatar's alt text, so the list is not only faces.\n */\nfunction AvatarGroup({\n max,\n total,\n size = \"default\",\n className,\n children,\n ...props\n}: AvatarGroupProps) {\n const items = React.Children.toArray(children);\n const visible = max === undefined ? items : items.slice(0, Math.max(0, max));\n const overflow = Math.max(0, (total ?? items.length) - visible.length);\n\n return (\n <div\n role=\"group\"\n data-slot=\"avatar-group\"\n className={cn(\n \"flex items-center -space-x-2 [&>[data-slot=avatar]]:border-2 [&>[data-slot=avatar]]:border-background\",\n className,\n )}\n {...props}\n >\n {visible}\n {overflow > 0 ? (\n <span\n data-slot=\"avatar-group-overflow\"\n className={cn(\n \"relative inline-flex shrink-0 items-center justify-center border-2 border-background bg-secondary font-medium tabular-nums text-secondary-foreground\",\n sizeClasses[size],\n )}\n >\n <span aria-hidden=\"true\">+{overflow}</span>\n <span className=\"sr-only\">and {overflow} more</span>\n </span>\n ) : null}\n </div>\n );\n}\n\nexport { Avatar, AvatarImage, AvatarFallback, AvatarGroup };\n",
|
|
27
|
+
"type": "registry:ui",
|
|
28
|
+
"target": "components/ui/avatar.tsx"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "badge",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Badge",
|
|
6
|
+
"description": "Compact labels for status, counts, and categories, with soft status variants and an optional dot.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"class-variance-authority"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://abackui.com/r/utils.json"
|
|
12
|
+
],
|
|
13
|
+
"categories": [
|
|
14
|
+
"Foundation"
|
|
15
|
+
],
|
|
16
|
+
"docs": "https://abackui.com/docs/components/badge",
|
|
17
|
+
"meta": {
|
|
18
|
+
"status": "stable",
|
|
19
|
+
"usedIn": [
|
|
20
|
+
"aback-tools",
|
|
21
|
+
"aback-launch",
|
|
22
|
+
"aback-group"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
{
|
|
27
|
+
"path": "components/ui/badge.tsx",
|
|
28
|
+
"content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst badgeVariants = cva(\n \"inline-flex shrink-0 items-center gap-1 border px-2.5 py-0.5 text-xs font-semibold leading-4 [&_svg]:size-3 [&_svg]:shrink-0\",\n {\n variants: {\n variant: {\n default: \"border-transparent bg-primary text-primary-foreground\",\n secondary: \"border-transparent bg-secondary text-secondary-foreground\",\n outline: \"border-border text-foreground\",\n brand:\n \"border-primary-200 bg-brand-chip text-primary-700 dark:border-primary-800/40 dark:text-primary-300\",\n success: \"border-success/20 bg-success-soft text-success-strong\",\n warning: \"border-warning/20 bg-warning-soft text-warning-strong\",\n danger: \"border-danger/20 bg-danger-soft text-danger-strong\",\n info: \"border-info/20 bg-info-soft text-info\",\n destructive: \"border-transparent bg-destructive text-destructive-foreground\",\n muted: \"border-transparent bg-muted/60 text-muted-foreground\",\n },\n },\n defaultVariants: {\n variant: \"secondary\",\n },\n },\n);\n\nexport interface BadgeProps\n extends React.ComponentProps<\"span\">,\n VariantProps<typeof badgeVariants> {\n /**\n * Leads with a small square status dot in the badge's text colour. The dot\n * is decorative; the badge text still has to say what the status is.\n */\n dot?: boolean;\n}\n\nfunction Badge({ className, variant = \"secondary\", dot = false, children, ...props }: BadgeProps) {\n return (\n <span\n data-slot=\"badge\"\n data-variant={variant}\n className={cn(badgeVariants({ variant }), dot && \"gap-1.5\", className)}\n {...props}\n >\n {dot ? (\n <span aria-hidden=\"true\" data-slot=\"badge-dot\" className=\"size-1.5 shrink-0 bg-current\" />\n ) : null}\n {children}\n </span>\n );\n}\n\nexport { Badge, badgeVariants };\n",
|
|
29
|
+
"type": "registry:ui",
|
|
30
|
+
"target": "components/ui/badge.tsx"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "breadcrumbs",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Breadcrumbs",
|
|
6
|
+
"description": "A hierarchical trail back to parent pages with current-page semantics.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@tabler/icons-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://abackui.com/r/utils.json"
|
|
12
|
+
],
|
|
13
|
+
"categories": [
|
|
14
|
+
"Navigation"
|
|
15
|
+
],
|
|
16
|
+
"docs": "https://abackui.com/docs/components/breadcrumbs",
|
|
17
|
+
"meta": {
|
|
18
|
+
"status": "stable",
|
|
19
|
+
"usedIn": [
|
|
20
|
+
"aback-tools",
|
|
21
|
+
"aback-launch"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
{
|
|
26
|
+
"path": "components/ui/breadcrumbs.tsx",
|
|
27
|
+
"content": "import * as React from \"react\";\nimport { IconChevronRight } from \"@tabler/icons-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface BreadcrumbItem {\n label: React.ReactNode;\n href?: string;\n}\n\nexport interface BreadcrumbsProps extends React.ComponentProps<\"nav\"> {\n items: BreadcrumbItem[];\n}\n\nfunction Breadcrumbs({ items, className, ...props }: BreadcrumbsProps) {\n return (\n <nav\n data-slot=\"breadcrumbs\"\n aria-label=\"Breadcrumb\"\n className={cn(\"text-sm\", className)}\n {...props}\n >\n <ol className=\"flex flex-wrap items-center gap-1\">\n {items.map((item, index) => {\n const isCurrent = index === items.length - 1;\n return (\n <li key={item.href ?? index} className=\"flex items-center gap-1\">\n {index > 0 ? (\n <IconChevronRight\n aria-hidden=\"true\"\n className=\"mx-0.5 size-3.5 shrink-0 text-muted-foreground\"\n />\n ) : null}\n {item.href && !isCurrent ? (\n <a\n href={item.href}\n className={cn(\n \"text-muted-foreground underline-offset-4 transition-colors hover:text-foreground hover:underline\",\n \"outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n )}\n >\n {item.label}\n </a>\n ) : (\n <span\n aria-current={isCurrent ? \"page\" : undefined}\n className={cn(isCurrent ? \"font-medium text-foreground\" : \"text-muted-foreground\")}\n >\n {item.label}\n </span>\n )}\n </li>\n );\n })}\n </ol>\n </nav>\n );\n}\n\nexport { Breadcrumbs };\n",
|
|
28
|
+
"type": "registry:ui",
|
|
29
|
+
"target": "components/ui/breadcrumbs.tsx"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "button",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Button",
|
|
6
|
+
"description": "The primary action, with ink, brand, outline, ghost, and destructive variants.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"class-variance-authority",
|
|
9
|
+
"radix-ui"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"https://abackui.com/r/utils.json"
|
|
13
|
+
],
|
|
14
|
+
"categories": [
|
|
15
|
+
"Foundation"
|
|
16
|
+
],
|
|
17
|
+
"docs": "https://abackui.com/docs/components/button",
|
|
18
|
+
"meta": {
|
|
19
|
+
"status": "stable",
|
|
20
|
+
"usedIn": [
|
|
21
|
+
"aback-tools",
|
|
22
|
+
"aback-launch",
|
|
23
|
+
"aback-group"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
{
|
|
28
|
+
"path": "components/ui/button.tsx",
|
|
29
|
+
"content": "import * as React from \"react\";\nimport { Slot } from \"radix-ui\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst buttonVariants = cva(\n [\n \"inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap text-sm font-medium\",\n \"outline-none transition-[color,background-color,border-color,transform,opacity] duration-200\",\n \"focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n \"disabled:pointer-events-none disabled:opacity-50 active:scale-[0.98]\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n ],\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n brand:\n \"bg-brand-fill text-on-brand-fill hover:bg-link-hover dark:hover:bg-primary-300\",\n destructive:\n \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n outline:\n \"border border-input bg-background text-foreground shadow-xs hover:bg-muted\",\n secondary: \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n ghost: \"text-foreground hover:bg-muted\",\n link: \"text-link underline-offset-4 hover:text-link-hover hover:underline\",\n },\n size: {\n xs: \"h-6 gap-1 px-2 text-xs [&_svg:not([class*='size-'])]:size-3\",\n sm: \"h-8 gap-1.5 px-3\",\n default: \"h-9 px-4 has-[>svg]:px-3\",\n lg: \"h-10 px-6\",\n icon: \"size-9\",\n \"icon-sm\": \"size-8\",\n \"icon-xs\": \"size-6 [&_svg:not([class*='size-'])]:size-3\",\n \"icon-lg\": \"size-10\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n },\n);\n\nexport interface ButtonProps\n extends React.ComponentProps<\"button\">,\n VariantProps<typeof buttonVariants> {\n asChild?: boolean;\n}\n\nfunction Button({\n className,\n variant = \"default\",\n size = \"default\",\n asChild = false,\n ...props\n}: ButtonProps) {\n const Comp = asChild ? Slot.Root : \"button\";\n\n return (\n <Comp\n data-slot=\"button\"\n data-variant={variant}\n data-size={size}\n className={cn(buttonVariants({ variant, size }), className)}\n {...props}\n />\n );\n}\n\nexport { Button, buttonVariants };\n",
|
|
30
|
+
"type": "registry:ui",
|
|
31
|
+
"target": "components/ui/button.tsx"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "calendar",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Calendar",
|
|
6
|
+
"description": "A square-cornered month grid with single, multiple, and range selection.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@tabler/icons-react",
|
|
9
|
+
"react-day-picker"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"https://abackui.com/r/utils.json"
|
|
13
|
+
],
|
|
14
|
+
"categories": [
|
|
15
|
+
"Data Display"
|
|
16
|
+
],
|
|
17
|
+
"docs": "https://abackui.com/docs/components/calendar",
|
|
18
|
+
"meta": {
|
|
19
|
+
"status": "beta"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
{
|
|
23
|
+
"path": "components/ui/calendar.tsx",
|
|
24
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n DateLib,\n DayPicker,\n type ClassNames,\n type CustomComponents,\n type DayPickerProps,\n type Labels,\n} from \"react-day-picker\";\nimport { IconChevronDown, IconChevronLeft, IconChevronRight, IconChevronUp } from \"@tabler/icons-react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/** The `{ from, to }` shape used by range mode, re-exported so consumers do not need a direct dependency. */\nexport type { DateRange } from \"react-day-picker\";\n\nexport type CalendarProps = DayPickerProps & {\n className?: string;\n /**\n * Override individual slots. Slots you omit keep the Aback styling, so a\n * consumer can adjust one element without restyling the whole calendar.\n */\n classNames?: Partial<ClassNames>;\n};\n\nconst DEFAULT_FORMAT: Intl.DateTimeFormatOptions = {\n month: \"short\",\n day: \"numeric\",\n year: \"numeric\",\n};\n\n/** Format a date in the visitor's locale, without pulling in a date library. */\nexport function formatCalendarDate(\n date: Date,\n locale?: string,\n options: Intl.DateTimeFormatOptions = DEFAULT_FORMAT,\n): string {\n return new Intl.DateTimeFormat(locale, options).format(date);\n}\n\nconst navButton =\n \"absolute top-0 z-10 inline-flex size-8 items-center justify-center border border-transparent text-muted-foreground transition-colors \" +\n \"hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring \" +\n \"disabled:pointer-events-none disabled:opacity-40\";\n\nconst defaultClassNames: Partial<ClassNames> = {\n root: \"relative w-fit\",\n months: \"flex flex-col gap-4 sm:flex-row sm:gap-6\",\n month: \"relative flex flex-col gap-3\",\n month_caption: \"flex h-8 items-center justify-center px-9\",\n caption_label: \"text-sm font-semibold text-foreground\",\n nav: \"contents\",\n button_previous: cn(navButton, \"left-0\"),\n button_next: cn(navButton, \"right-0\"),\n chevron: \"size-4\",\n month_grid: \"w-full border-collapse\",\n weekdays: \"\",\n weekday:\n \"h-8 w-9 p-0 text-[11px] font-medium uppercase tracking-wide text-muted-foreground\",\n week: \"\",\n day: \"p-0 text-center align-middle\",\n day_button:\n \"mx-auto flex size-9 items-center justify-center text-sm font-normal text-foreground transition-colors \" +\n \"hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-focus-ring \" +\n \"disabled:pointer-events-none disabled:opacity-40\",\n selected:\n \"[&>button]:bg-primary [&>button]:text-primary-foreground [&>button]:hover:bg-primary [&>button]:font-medium\",\n today: \"[&:not([data-selected=true])>button]:font-semibold [&:not([data-selected=true])>button]:text-brand\",\n outside: \"[&>button]:text-muted-foreground [&>button]:hover:text-foreground\",\n disabled: \"[&>button]:text-muted-foreground/40 [&>button]:line-through\",\n hidden: \"invisible\",\n range_middle:\n \"[&[data-selected=true].rdp-range_middle>button]:bg-muted \" +\n \"[&[data-selected=true].rdp-range_middle>button]:text-foreground \" +\n \"[&[data-selected=true].rdp-range_middle>button]:hover:bg-muted\",\n dropdowns: \"flex items-center justify-center gap-2 px-9\",\n dropdown_root: \"relative inline-flex items-center\",\n dropdown:\n \"h-8 appearance-none border border-input bg-background px-2 pr-6 text-sm text-foreground outline-none \" +\n \"focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-focus-ring\",\n footer: \"border-t border-border px-2 pt-3 text-xs text-muted-foreground\",\n};\n\n// A day button shows only its number, so its accessible name must contain that\n// number as a word (WCAG 2.5.3, Label in Name). The library default reads\n// \"November 1st\", which does not; this reads \"Sunday, November 1, 2026\".\nconst defaultLabels: Partial<Labels> = {\n labelDayButton: (date, modifiers, options, dateLib) => {\n let label = (dateLib ?? new DateLib(options)).format(date, \"EEEE, MMMM d, yyyy\");\n if (modifiers.today) label = `Today, ${label}`;\n if (modifiers.selected) label = `${label}, selected`;\n return label;\n },\n};\n\nconst defaultComponents: Partial<CustomComponents> = {\n Chevron: ({ orientation = \"left\", className, size = 16 }) => {\n const Icon =\n orientation === \"left\"\n ? IconChevronLeft\n : orientation === \"right\"\n ? IconChevronRight\n : orientation === \"up\"\n ? IconChevronUp\n : IconChevronDown;\n return <Icon aria-hidden=\"true\" className={cn(\"size-4\", className)} size={size} />;\n },\n};\n\n/**\n * A month calendar with single, multiple, and range selection.\n *\n * The calendar is a composed control: use it on its own, inside a `Popover`\n * (see `DatePicker`), or inside a dialog. Every date is a real button inside a\n * `grid`, so arrow-key navigation, Home/End, Page Up/Page Down, and the\n * announced selected state come from react-day-picker's accessible grid.\n */\nfunction Calendar({\n className,\n classNames,\n components,\n labels,\n showOutsideDays = true,\n navLayout = \"around\",\n ...props\n}: CalendarProps) {\n return (\n <DayPicker\n showOutsideDays={showOutsideDays}\n navLayout={navLayout}\n className={cn(\"w-fit bg-background p-3 text-foreground\", className)}\n classNames={{ ...defaultClassNames, ...classNames }}\n components={{ ...defaultComponents, ...components }}\n labels={{ ...defaultLabels, ...labels }}\n {...props}\n />\n );\n}\n\nexport { Calendar };\n",
|
|
25
|
+
"type": "registry:ui",
|
|
26
|
+
"target": "components/ui/calendar.tsx"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "card",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Card",
|
|
6
|
+
"description": "A bordered surface for grouping related content and actions.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://abackui.com/r/utils.json"
|
|
9
|
+
],
|
|
10
|
+
"categories": [
|
|
11
|
+
"Foundation"
|
|
12
|
+
],
|
|
13
|
+
"docs": "https://abackui.com/docs/components/card",
|
|
14
|
+
"meta": {
|
|
15
|
+
"status": "stable",
|
|
16
|
+
"usedIn": [
|
|
17
|
+
"aback-tools",
|
|
18
|
+
"aback-launch",
|
|
19
|
+
"aback-group"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
{
|
|
24
|
+
"path": "components/ui/card.tsx",
|
|
25
|
+
"content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Card({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card\"\n className={cn(\n \"flex flex-col gap-6 border border-border bg-card py-6 text-card-foreground shadow-sm\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction CardHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-header\"\n className={cn(\n \"grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto]\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction CardTitle({\n className,\n as: Comp = \"h3\",\n ...props\n}: React.ComponentProps<\"h3\"> & { as?: \"h2\" | \"h3\" | \"h4\" }) {\n return (\n <Comp\n data-slot=\"card-title\"\n className={cn(\"font-semibold leading-none tracking-tight\", className)}\n {...props}\n />\n );\n}\n\nfunction CardDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n return (\n <p\n data-slot=\"card-description\"\n className={cn(\"text-sm leading-relaxed text-muted-foreground\", className)}\n {...props}\n />\n );\n}\n\nfunction CardAction({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-action\"\n className={cn(\"col-start-2 row-span-2 row-start-1 self-start justify-self-end\", className)}\n {...props}\n />\n );\n}\n\nfunction CardContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div data-slot=\"card-content\" className={cn(\"px-6\", className)} {...props} />;\n}\n\nfunction CardFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-footer\"\n className={cn(\"flex items-center px-6\", className)}\n {...props}\n />\n );\n}\n\nexport {\n Card,\n CardHeader,\n CardTitle,\n CardDescription,\n CardAction,\n CardContent,\n CardFooter,\n};\n",
|
|
26
|
+
"type": "registry:ui",
|
|
27
|
+
"target": "components/ui/card.tsx"
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "checkbox",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Checkbox",
|
|
6
|
+
"description": "A tri-state checkbox with label support and visible focus.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"radix-ui",
|
|
9
|
+
"@tabler/icons-react"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"https://abackui.com/r/utils.json"
|
|
13
|
+
],
|
|
14
|
+
"categories": [
|
|
15
|
+
"Forms"
|
|
16
|
+
],
|
|
17
|
+
"docs": "https://abackui.com/docs/components/checkbox",
|
|
18
|
+
"meta": {
|
|
19
|
+
"status": "stable",
|
|
20
|
+
"usedIn": [
|
|
21
|
+
"aback-tools",
|
|
22
|
+
"aback-launch"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
{
|
|
27
|
+
"path": "components/ui/checkbox.tsx",
|
|
28
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Checkbox as CheckboxPrimitive } from \"radix-ui\";\nimport { IconCheck } from \"@tabler/icons-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Checkbox({\n className,\n ...props\n}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {\n return (\n <CheckboxPrimitive.Root\n data-slot=\"checkbox\"\n className={cn(\n \"peer size-4 shrink-0 cursor-pointer border border-input bg-transparent shadow-xs outline-none transition-colors duration-200\",\n \"focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n \"data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground\",\n \"data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground\",\n className,\n )}\n {...props}\n >\n <CheckboxPrimitive.Indicator\n data-slot=\"checkbox-indicator\"\n className=\"flex items-center justify-center text-current\"\n >\n <IconCheck aria-hidden=\"true\" className=\"size-3.5\" stroke={3} />\n </CheckboxPrimitive.Indicator>\n </CheckboxPrimitive.Root>\n );\n}\n\nexport { Checkbox };\n",
|
|
29
|
+
"type": "registry:ui",
|
|
30
|
+
"target": "components/ui/checkbox.tsx"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "code-block",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Code Block",
|
|
6
|
+
"description": "A captioned, scrollable code sample with a copy button and optional line numbers.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://abackui.com/r/utils.json",
|
|
9
|
+
"https://abackui.com/r/copy-button.json"
|
|
10
|
+
],
|
|
11
|
+
"categories": [
|
|
12
|
+
"Data Display"
|
|
13
|
+
],
|
|
14
|
+
"docs": "https://abackui.com/docs/components/code-block",
|
|
15
|
+
"meta": {
|
|
16
|
+
"status": "beta",
|
|
17
|
+
"usedIn": [
|
|
18
|
+
"aback-tools"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
{
|
|
23
|
+
"path": "components/ui/code-block.tsx",
|
|
24
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { CopyButton } from \"@/components/ui/copy-button\";\n\nexport interface CodeBlockProps extends Omit<React.ComponentProps<\"figure\">, \"children\"> {\n /** The exact source. It is what the copy button writes. */\n code: string;\n /** Shown in the caption bar, such as \"tsx\" or \"bash\". */\n language?: string;\n /** Shown in the caption bar and used to name the scroll region. */\n filename?: string;\n /** Show the copy button. */\n copyable?: boolean;\n /** Number the lines. Numbers are generated by CSS and never copied. */\n showLineNumbers?: boolean;\n /** Caps the height and scrolls, for long files. */\n maxHeight?: number | string;\n /** Accessible name of the copy button. */\n copyLabel?: string;\n}\n\n/**\n * A captioned code sample with a copy button. The scroll region is focusable\n * so a keyboard can scroll a long line, and it has a name. No highlighter is\n * bundled; the block renders plain text so it stays light and safe.\n */\nfunction CodeBlock({\n code,\n language,\n filename,\n copyable = true,\n showLineNumbers = false,\n maxHeight,\n copyLabel = \"Copy code\",\n className,\n ...props\n}: CodeBlockProps) {\n const lines = React.useMemo(() => code.replace(/\\n$/, \"\").split(\"\\n\"), [code]);\n const hasCaption = Boolean(filename || language || copyable);\n\n return (\n <figure\n data-slot=\"code-block\"\n className={cn(\"min-w-0 overflow-hidden border border-border bg-muted/30\", className)}\n {...props}\n >\n {hasCaption ? (\n <figcaption className=\"flex min-h-10 items-center justify-between gap-3 border-b border-border bg-muted px-4 py-1.5\">\n <span className=\"flex min-w-0 items-center gap-3\">\n {filename ? (\n <span className=\"truncate font-mono text-xs text-foreground\">{filename}</span>\n ) : null}\n {language ? (\n <span className=\"shrink-0 text-[10px] font-bold uppercase tracking-[0.12em] text-foreground\">\n {language}\n </span>\n ) : null}\n </span>\n {copyable ? <CopyButton value={code} label={copyLabel} /> : null}\n </figcaption>\n ) : null}\n <pre\n tabIndex={0}\n aria-label={filename ? `${filename} source` : \"Code sample\"}\n className=\"overflow-auto p-4 text-[13px] leading-relaxed text-foreground outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-focus-ring\"\n style={maxHeight !== undefined ? { maxHeight } : undefined}\n >\n <code className={cn(\"font-mono\", showLineNumbers && \"grid [counter-reset:line]\")}>\n {showLineNumbers\n ? lines.map((line, index) => (\n <span\n key={index}\n className=\"[counter-increment:line] before:mr-4 before:inline-block before:w-6 before:select-none before:text-right before:text-muted-foreground before:content-[counter(line)]\"\n >\n {line || \" \"}\n </span>\n ))\n : code}\n </code>\n </pre>\n </figure>\n );\n}\n\nexport { CodeBlock };\n",
|
|
25
|
+
"type": "registry:ui",
|
|
26
|
+
"target": "components/ui/code-block.tsx"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "collapsible",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Collapsible",
|
|
6
|
+
"description": "A single disclosure region for optional or advanced content.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"radix-ui"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://abackui.com/r/utils.json"
|
|
12
|
+
],
|
|
13
|
+
"categories": [
|
|
14
|
+
"Navigation"
|
|
15
|
+
],
|
|
16
|
+
"docs": "https://abackui.com/docs/components/collapsible",
|
|
17
|
+
"meta": {
|
|
18
|
+
"status": "beta"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
{
|
|
22
|
+
"path": "components/ui/collapsible.tsx",
|
|
23
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Collapsible as CollapsiblePrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Collapsible = CollapsiblePrimitive.Root;\nconst CollapsibleTrigger = CollapsiblePrimitive.Trigger;\n\nfunction CollapsibleContent({\n className,\n ...props\n}: React.ComponentProps<typeof CollapsiblePrimitive.Content>) {\n return (\n <CollapsiblePrimitive.Content\n data-slot=\"collapsible-content\"\n className={cn(\"overflow-hidden\", className)}\n {...props}\n />\n );\n}\n\nexport { Collapsible, CollapsibleTrigger, CollapsibleContent };\n",
|
|
24
|
+
"type": "registry:ui",
|
|
25
|
+
"target": "components/ui/collapsible.tsx"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "combobox",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Combobox",
|
|
6
|
+
"description": "A searchable single-select field built on Popover and Command.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@tabler/icons-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://abackui.com/r/utils.json",
|
|
12
|
+
"https://abackui.com/r/command.json",
|
|
13
|
+
"https://abackui.com/r/popover.json"
|
|
14
|
+
],
|
|
15
|
+
"categories": [
|
|
16
|
+
"Forms"
|
|
17
|
+
],
|
|
18
|
+
"docs": "https://abackui.com/docs/components/combobox",
|
|
19
|
+
"meta": {
|
|
20
|
+
"status": "beta"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
{
|
|
24
|
+
"path": "components/ui/combobox.tsx",
|
|
25
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { IconCheck, IconChevronDown } from \"@tabler/icons-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Command, CommandEmpty, CommandInput, CommandItem, CommandList } from \"@/components/ui/command\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\n\nexport interface ComboboxOption {\n value: string;\n label: string;\n /** Secondary line shown under the label in the list. */\n description?: string;\n disabled?: boolean;\n /** Extra words that should match the search without appearing as the label. */\n keywords?: string[];\n}\n\nexport interface ComboboxProps {\n options: ComboboxOption[];\n value?: string;\n onValueChange?: (value: string) => void;\n placeholder?: string;\n searchPlaceholder?: string;\n emptyMessage?: string;\n disabled?: boolean;\n /** Accessible name for the trigger when there is no visible `Label`. */\n \"aria-label\"?: string;\n \"aria-labelledby\"?: string;\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: boolean;\n id?: string;\n name?: string;\n className?: string;\n triggerClassName?: string;\n align?: \"start\" | \"center\" | \"end\";\n}\n\n/**\n * A single-select combobox: a button that opens a searchable list.\n *\n * Built from `Popover` and `Command`, so filtering, type-ahead, arrow keys,\n * Home/End, Enter, and Escape are real interactions rather than decoration.\n * The trigger keeps the current label, and the selected item is marked with a\n * check icon and `aria-selected` from the listbox.\n */\nfunction Combobox({\n options,\n value,\n onValueChange,\n placeholder = \"Select an option\",\n searchPlaceholder = \"Search…\",\n emptyMessage = \"No matching options.\",\n disabled,\n id,\n name,\n className,\n triggerClassName,\n align = \"start\",\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n}: ComboboxProps) {\n const [open, setOpen] = React.useState(false);\n const selected = options.find((option) => option.value === value);\n const generatedId = React.useId();\n const triggerId = id ?? generatedId;\n\n return (\n <div data-slot=\"combobox\" className={cn(\"relative w-full\", className)}>\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <button\n id={triggerId}\n type=\"button\"\n aria-expanded={open}\n aria-haspopup=\"listbox\"\n aria-controls={open ? `${triggerId}-listbox` : undefined}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n aria-describedby={ariaDescribedBy}\n aria-invalid={ariaInvalid}\n disabled={disabled}\n className={cn(\n \"flex h-9 w-full items-center justify-between gap-2 border border-input bg-background px-3 text-left text-sm text-foreground shadow-xs\",\n \"outline-none transition-[color,background-color,border-color,box-shadow] duration-200\",\n \"hover:bg-muted/40\",\n \"focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n \"aria-invalid:border-destructive aria-invalid:ring-destructive/20\",\n triggerClassName,\n )}\n >\n <span className={cn(\"truncate\", !selected && \"text-muted-foreground\")}>\n {selected?.label ?? placeholder}\n </span>\n <IconChevronDown\n aria-hidden=\"true\"\n className={cn(\n \"size-4 shrink-0 text-muted-foreground transition-transform duration-200\",\n open && \"rotate-180\",\n )}\n />\n </button>\n </PopoverTrigger>\n <PopoverContent\n align={align}\n aria-label={ariaLabel ?? \"Choose an option\"}\n className=\"w-[var(--radix-popover-trigger-width)] min-w-56 p-0\"\n >\n <Command label={ariaLabel ?? \"Options\"}>\n <CommandInput placeholder={searchPlaceholder} autoFocus />\n <CommandList id={`${triggerId}-listbox`}>\n <CommandEmpty>{emptyMessage}</CommandEmpty>\n {options.map((option) => (\n <CommandItem\n key={option.value}\n value={option.value}\n keywords={[option.label, ...(option.keywords ?? [])]}\n disabled={option.disabled}\n onSelect={() => {\n onValueChange?.(option.value);\n setOpen(false);\n }}\n className=\"items-start gap-2\"\n >\n <IconCheck\n aria-hidden=\"true\"\n className={cn(\n \"mt-0.5 size-4 shrink-0\",\n option.value === value ? \"opacity-100\" : \"opacity-0\",\n )}\n />\n <span className=\"flex min-w-0 flex-col gap-0.5\">\n <span className=\"truncate\">{option.label}</span>\n {option.description ? (\n <span className=\"truncate text-xs text-muted-foreground\">\n {option.description}\n </span>\n ) : null}\n </span>\n </CommandItem>\n ))}\n </CommandList>\n </Command>\n </PopoverContent>\n </Popover>\n {name ? <input type=\"hidden\" name={name} value={value ?? \"\"} /> : null}\n </div>\n );\n}\n\nexport { Combobox };\n",
|
|
26
|
+
"type": "registry:ui",
|
|
27
|
+
"target": "components/ui/combobox.tsx"
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "command",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Command",
|
|
6
|
+
"description": "A keyboard-first command palette with filtering, groups, and shortcuts.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@tabler/icons-react",
|
|
9
|
+
"cmdk"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"https://abackui.com/r/utils.json",
|
|
13
|
+
"https://abackui.com/r/dialog.json"
|
|
14
|
+
],
|
|
15
|
+
"categories": [
|
|
16
|
+
"Overlays"
|
|
17
|
+
],
|
|
18
|
+
"docs": "https://abackui.com/docs/components/command",
|
|
19
|
+
"meta": {
|
|
20
|
+
"status": "beta",
|
|
21
|
+
"usedIn": [
|
|
22
|
+
"aback-launch",
|
|
23
|
+
"aback-group"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
{
|
|
28
|
+
"path": "components/ui/command.tsx",
|
|
29
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Command as CommandPrimitive } from \"cmdk\";\nimport { IconSearch } from \"@tabler/icons-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from \"@/components/ui/dialog\";\n\nfunction Command({ className, ...props }: React.ComponentProps<typeof CommandPrimitive>) {\n return (\n <CommandPrimitive\n data-slot=\"command\"\n className={cn(\n \"flex h-full w-full flex-col overflow-hidden bg-popover text-popover-foreground\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport interface CommandDialogProps extends React.ComponentProps<typeof Dialog> {\n title?: string;\n description?: string;\n className?: string;\n /** Show the dialog's close button. Defaults to `true`; a search surface usually sets it to `false`. */\n showCloseButton?: boolean;\n}\n\nfunction CommandDialog({\n title = \"Command palette\",\n description = \"Search for a command to run.\",\n children,\n className,\n showCloseButton = true,\n ...props\n}: CommandDialogProps) {\n return (\n <Dialog {...props}>\n <DialogContent showCloseButton={showCloseButton} className={cn(\"overflow-hidden p-0\", className)}>\n <DialogHeader className=\"sr-only\">\n <DialogTitle>{title}</DialogTitle>\n <DialogDescription>{description}</DialogDescription>\n </DialogHeader>\n <Command label={title}>{children}</Command>\n </DialogContent>\n </Dialog>\n );\n}\n\nfunction CommandInput({\n className,\n wrapperClassName,\n children,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.Input> & {\n wrapperClassName?: string;\n children?: React.ReactNode;\n}) {\n return (\n <div\n data-slot=\"command-input-wrapper\"\n className={cn(\"flex h-11 items-center gap-2 border-b border-border px-3\", wrapperClassName)}\n >\n <IconSearch aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n <CommandPrimitive.Input\n data-slot=\"command-input\"\n className={cn(\n \"flex h-11 w-full bg-transparent py-3 text-sm text-foreground outline-none\",\n \"placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50\",\n \"focus-visible:outline-none\",\n className,\n )}\n {...props}\n />\n {children}\n </div>\n );\n}\n\nfunction CommandList({\n className,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.List>) {\n return (\n <CommandPrimitive.List\n data-slot=\"command-list\"\n className={cn(\"max-h-80 overflow-y-auto overflow-x-hidden p-1\", className)}\n {...props}\n />\n );\n}\n\nfunction CommandEmpty(props: React.ComponentProps<typeof CommandPrimitive.Empty>) {\n return (\n <CommandPrimitive.Empty\n data-slot=\"command-empty\"\n className=\"px-3 py-8 text-center text-sm text-muted-foreground\"\n {...props}\n />\n );\n}\n\nfunction CommandGroup({\n className,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.Group>) {\n return (\n <CommandPrimitive.Group\n data-slot=\"command-group\"\n className={cn(\n \"overflow-hidden text-foreground\",\n \"[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5\",\n \"[&_[cmdk-group-heading]]:text-[11px] [&_[cmdk-group-heading]]:font-semibold\",\n \"[&_[cmdk-group-heading]]:uppercase [&_[cmdk-group-heading]]:tracking-wider\",\n \"[&_[cmdk-group-heading]]:text-muted-foreground\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction CommandSeparator({\n className,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.Separator>) {\n return (\n <CommandPrimitive.Separator\n data-slot=\"command-separator\"\n className={cn(\"my-1 h-px bg-border\", className)}\n {...props}\n />\n );\n}\n\nfunction CommandItem({\n className,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.Item>) {\n return (\n <CommandPrimitive.Item\n data-slot=\"command-item\"\n className={cn(\n \"relative flex cursor-pointer select-none items-center gap-2 px-2 py-2 text-sm outline-none\",\n \"data-[selected=true]:bg-muted data-[selected=true]:text-foreground\",\n \"data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction CommandShortcut({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"command-shortcut\"\n className={cn(\"ml-auto font-mono text-xs tracking-wide text-muted-foreground\", className)}\n {...props}\n />\n );\n}\n\nexport {\n Command,\n CommandDialog,\n CommandInput,\n CommandList,\n CommandEmpty,\n CommandGroup,\n CommandItem,\n CommandSeparator,\n CommandShortcut,\n};\n",
|
|
30
|
+
"type": "registry:ui",
|
|
31
|
+
"target": "components/ui/command.tsx"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "contact-form",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "Contact form",
|
|
6
|
+
"description": "A contact form with field-level errors, focus on the first error, a character count, and submit states.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@tabler/icons-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://abackui.com/r/alert.json",
|
|
12
|
+
"https://abackui.com/r/button.json",
|
|
13
|
+
"https://abackui.com/r/field.json",
|
|
14
|
+
"https://abackui.com/r/input.json",
|
|
15
|
+
"https://abackui.com/r/select.json",
|
|
16
|
+
"https://abackui.com/r/spinner.json",
|
|
17
|
+
"https://abackui.com/r/textarea.json"
|
|
18
|
+
],
|
|
19
|
+
"categories": [
|
|
20
|
+
"Forms"
|
|
21
|
+
],
|
|
22
|
+
"docs": "https://abackui.com/docs/blocks/contact-form",
|
|
23
|
+
"meta": {
|
|
24
|
+
"status": "beta",
|
|
25
|
+
"usedIn": [
|
|
26
|
+
"aback-launch"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
{
|
|
31
|
+
"path": "components/blocks/contact-form.tsx",
|
|
32
|
+
"content": "\"use client\";\n\nimport { type FormEvent, useId, useRef, useState } from \"react\";\nimport { IconCircleCheck } from \"@tabler/icons-react\";\n\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport { Button } from \"@/components/ui/button\";\nimport { Field } from \"@/components/ui/field\";\nimport { Input } from \"@/components/ui/input\";\nimport { Select } from \"@/components/ui/select\";\nimport { Spinner } from \"@/components/ui/spinner\";\nimport { Textarea } from \"@/components/ui/textarea\";\n\nexport interface ContactValues {\n name: string;\n email: string;\n topic: string;\n message: string;\n}\n\nexport interface ContactResult {\n ok: boolean;\n /** Shown after submission: when you reply, or what went wrong. */\n message: string;\n}\n\nexport interface ContactFormBlockProps {\n /** Send the message. Validate again on the server; this form only checks the shape. */\n onSubmit: (values: ContactValues) => Promise<ContactResult>;\n topics?: { value: string; label: string }[];\n title?: string;\n description?: string;\n maxLength?: number;\n}\n\ntype Errors = Partial<Record<keyof ContactValues, string>>;\n\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\nfunction validate(values: ContactValues, maxLength: number): Errors {\n const errors: Errors = {};\n if (!values.name.trim()) errors.name = \"Enter your name.\";\n if (!values.email.trim()) errors.email = \"Enter your email address.\";\n else if (!EMAIL.test(values.email.trim())) errors.email = \"Enter an email address like you@company.com.\";\n if (!values.topic) errors.topic = \"Choose what your message is about.\";\n if (values.message.trim().length < 10) errors.message = \"Write at least 10 characters.\";\n else if (values.message.length > maxLength) errors.message = `Keep it under ${maxLength} characters.`;\n return errors;\n}\n\nconst FIELD_ORDER: (keyof ContactValues)[] = [\"name\", \"email\", \"topic\", \"message\"];\n\n/**\n * A contact form with field-level errors, a character count, and real\n * submission states. On an invalid submit it focuses the first field with an\n * error; on success it replaces the form with your confirmation.\n */\nexport function ContactFormBlock({\n onSubmit,\n topics = [\n { value: \"support\", label: \"Product support\" },\n { value: \"sales\", label: \"Sales and pricing\" },\n { value: \"press\", label: \"Press\" },\n { value: \"other\", label: \"Something else\" },\n ],\n title = \"Contact us\",\n description = \"We reply within two working days.\",\n maxLength = 2000,\n}: ContactFormBlockProps) {\n const headingId = useId();\n const [values, setValues] = useState<ContactValues>({ name: \"\", email: \"\", topic: \"\", message: \"\" });\n const [errors, setErrors] = useState<Errors>({});\n const [submitting, setSubmitting] = useState(false);\n const [result, setResult] = useState<ContactResult | null>(null);\n const form = useRef<HTMLFormElement>(null);\n\n function update<K extends keyof ContactValues>(key: K, value: ContactValues[K]) {\n setValues((current) => ({ ...current, [key]: value }));\n if (errors[key]) setErrors((current) => ({ ...current, [key]: undefined }));\n }\n\n async function handleSubmit(event: FormEvent<HTMLFormElement>) {\n event.preventDefault();\n const found = validate(values, maxLength);\n setErrors(found);\n const first = FIELD_ORDER.find((key) => found[key]);\n if (first) {\n form.current?.querySelector<HTMLElement>(`[name=\"${first}\"]`)?.focus();\n return;\n }\n setSubmitting(true);\n setResult(null);\n try {\n setResult(await onSubmit({ ...values, name: values.name.trim(), email: values.email.trim() }));\n } catch {\n setResult({ ok: false, message: \"Your message could not be sent. Try again in a moment.\" });\n } finally {\n setSubmitting(false);\n }\n }\n\n const describedBy = (key: keyof ContactValues, extra?: string) =>\n [errors[key] ? `contact-${key}-error` : null, extra].filter(Boolean).join(\" \") || undefined;\n\n if (result?.ok) {\n return (\n <section aria-labelledby={headingId} className=\"mx-auto w-full max-w-xl px-4 py-12 md:px-6\">\n <div role=\"status\" className=\"flex flex-col items-center gap-3 border border-border bg-card p-8 text-center\">\n <span aria-hidden=\"true\" className=\"flex size-12 items-center justify-center bg-success-soft text-success\">\n <IconCircleCheck className=\"size-6\" />\n </span>\n <h2 id={headingId} className=\"text-xl font-semibold text-foreground\">\n Message sent\n </h2>\n <p className=\"text-sm text-muted-foreground\">{result.message}</p>\n <Button variant=\"outline\" size=\"sm\" onClick={() => {\n setValues({ name: \"\", email: \"\", topic: \"\", message: \"\" });\n setResult(null);\n }}>\n Send another message\n </Button>\n </div>\n </section>\n );\n }\n\n return (\n <section aria-labelledby={headingId} className=\"mx-auto w-full max-w-xl px-4 py-12 md:px-6\">\n <div className=\"mb-6 flex flex-col gap-1.5\">\n <h2 id={headingId} className=\"text-2xl font-bold tracking-tight text-foreground\">\n {title}\n </h2>\n <p className=\"text-sm text-muted-foreground\">{description}</p>\n </div>\n\n <form ref={form} onSubmit={handleSubmit} noValidate className=\"flex flex-col gap-5\">\n <div className=\"grid gap-5 sm:grid-cols-2\">\n <Field htmlFor=\"contact-name\" label=\"Name\" required error={errors.name}>\n <Input\n id=\"contact-name\"\n name=\"name\"\n autoComplete=\"name\"\n value={values.name}\n onChange={(event) => update(\"name\", event.target.value)}\n aria-invalid={errors.name ? true : undefined}\n aria-describedby={describedBy(\"name\")}\n required\n />\n </Field>\n <Field htmlFor=\"contact-email\" label=\"Email\" required error={errors.email}>\n <Input\n id=\"contact-email\"\n name=\"email\"\n type=\"email\"\n autoComplete=\"email\"\n inputMode=\"email\"\n value={values.email}\n onChange={(event) => update(\"email\", event.target.value)}\n aria-invalid={errors.email ? true : undefined}\n aria-describedby={describedBy(\"email\")}\n required\n />\n </Field>\n </div>\n\n <Field htmlFor=\"contact-topic\" label=\"Topic\" required error={errors.topic}>\n <Select\n id=\"contact-topic\"\n name=\"topic\"\n value={values.topic}\n onChange={(event) => update(\"topic\", event.target.value)}\n aria-invalid={errors.topic ? true : undefined}\n aria-describedby={describedBy(\"topic\")}\n required\n >\n <option value=\"\" disabled>\n Choose a topic\n </option>\n {topics.map((topic) => (\n <option key={topic.value} value={topic.value}>\n {topic.label}\n </option>\n ))}\n </Select>\n </Field>\n\n <Field\n htmlFor=\"contact-message\"\n label=\"Message\"\n required\n error={errors.message}\n description={`${values.message.length} of ${maxLength} characters`}\n >\n <Textarea\n id=\"contact-message\"\n name=\"message\"\n rows={6}\n value={values.message}\n onChange={(event) => update(\"message\", event.target.value)}\n aria-invalid={errors.message ? true : undefined}\n aria-describedby={describedBy(\"message\", \"contact-message-description\")}\n required\n />\n </Field>\n\n {result && !result.ok ? (\n <Alert variant=\"destructive\" role=\"alert\">\n <AlertDescription>{result.message}</AlertDescription>\n </Alert>\n ) : null}\n\n <Button type=\"submit\" disabled={submitting} className=\"self-start\">\n {submitting ? <Spinner aria-hidden=\"true\" size=\"sm\" className=\"text-current\" /> : null}\n {submitting ? \"Sending\" : \"Send message\"}\n </Button>\n </form>\n </section>\n );\n}\n",
|
|
33
|
+
"type": "registry:block",
|
|
34
|
+
"target": "components/blocks/contact-form.tsx"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|