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": "cookie-consent",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "Cookie consent",
|
|
6
|
+
"description": "A non-blocking consent banner with equally prominent accept and decline, remembered per browser.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://abackui.com/r/button.json"
|
|
9
|
+
],
|
|
10
|
+
"categories": [
|
|
11
|
+
"Application"
|
|
12
|
+
],
|
|
13
|
+
"docs": "https://abackui.com/docs/blocks/cookie-consent",
|
|
14
|
+
"meta": {
|
|
15
|
+
"status": "beta",
|
|
16
|
+
"usedIn": [
|
|
17
|
+
"aback-launch"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
{
|
|
22
|
+
"path": "components/blocks/cookie-consent.tsx",
|
|
23
|
+
"content": "\"use client\";\n\nimport { useCallback, useSyncExternalStore, type ReactNode } from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\n\nexport type ConsentDecision = \"accepted\" | \"rejected\";\n\nexport interface CookieConsentBlockProps {\n /** What you are asking for and what happens either way. Be specific. */\n message: ReactNode;\n /** Called with the visitor's choice. Load optional scripts only after \"accepted\". */\n onDecision: (decision: ConsentDecision) => void;\n /** Where the choice is remembered. Pass null to keep nothing, for example in a preview. */\n storageKey?: string | null;\n acceptLabel?: string;\n rejectLabel?: string;\n /** Your cookie or privacy policy. */\n policyHref?: string;\n policyLabel?: string;\n}\n\nconst EVENT = \"aback-ui:consentchange\";\n\nfunction readDecision(key: string | null): ConsentDecision | null {\n if (!key) return null;\n try {\n const value = window.localStorage.getItem(key);\n return value === \"accepted\" || value === \"rejected\" ? value : null;\n } catch {\n return null;\n }\n}\n\n/**\n * A consent banner that does not block the page: no overlay, no scroll lock,\n * and two equally prominent buttons, so declining is as easy as accepting.\n * It appears until the visitor answers and remembers the answer.\n */\nexport function CookieConsentBlock({\n message,\n onDecision,\n storageKey = \"cookie-consent\",\n acceptLabel = \"Allow analytics\",\n rejectLabel = \"Decline\",\n policyHref,\n policyLabel = \"Cookie policy\",\n}: CookieConsentBlockProps) {\n const subscribe = useCallback((onChange: () => void) => {\n window.addEventListener(EVENT, onChange);\n window.addEventListener(\"storage\", onChange);\n return () => {\n window.removeEventListener(EVENT, onChange);\n window.removeEventListener(\"storage\", onChange);\n };\n }, []);\n // The server snapshot reports \"answered\" so the banner never flashes in the\n // HTML for a visitor who already chose; it appears after hydration if needed.\n const decided = useSyncExternalStore(\n subscribe,\n () => readDecision(storageKey ?? null) !== null,\n () => true,\n );\n\n if (decided && storageKey) return null;\n\n function decide(decision: ConsentDecision) {\n if (storageKey) {\n try {\n window.localStorage.setItem(storageKey, decision);\n } catch {\n // Not remembered, but the decision still applies to this page view.\n }\n window.dispatchEvent(new Event(EVENT));\n }\n onDecision(decision);\n }\n\n return (\n <div\n role=\"region\"\n aria-label=\"Cookie consent\"\n className=\"fixed inset-x-0 bottom-0 z-50 border-t border-border bg-card/95 p-3 shadow-[0_-2px_8px_rgb(0_0_0/0.06)] backdrop-blur-sm sm:p-4\"\n >\n <div className=\"mx-auto flex max-w-6xl flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-xs leading-relaxed text-muted-foreground sm:text-sm\">\n {message}\n {policyHref ? (\n <>\n {\" \"}\n <a\n href={policyHref}\n className=\"font-medium text-link underline underline-offset-4 hover:text-link-hover\"\n >\n {policyLabel}\n </a>\n </>\n ) : null}\n </p>\n <div className=\"flex shrink-0 items-center gap-2\">\n <Button type=\"button\" size=\"sm\" variant=\"outline\" className=\"flex-1 sm:flex-none\" onClick={() => decide(\"rejected\")}>\n {rejectLabel}\n </Button>\n <Button type=\"button\" size=\"sm\" variant=\"brand\" className=\"flex-1 sm:flex-none\" onClick={() => decide(\"accepted\")}>\n {acceptLabel}\n </Button>\n </div>\n </div>\n </div>\n );\n}\n",
|
|
24
|
+
"type": "registry:block",
|
|
25
|
+
"target": "components/blocks/cookie-consent.tsx"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "copy-button",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Copy Button",
|
|
6
|
+
"description": "Copies a value and confirms it in place, with an announced result and a failure state.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@tabler/icons-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://abackui.com/r/utils.json",
|
|
12
|
+
"https://abackui.com/r/button.json"
|
|
13
|
+
],
|
|
14
|
+
"categories": [
|
|
15
|
+
"Foundation"
|
|
16
|
+
],
|
|
17
|
+
"docs": "https://abackui.com/docs/components/copy-button",
|
|
18
|
+
"meta": {
|
|
19
|
+
"status": "beta",
|
|
20
|
+
"usedIn": [
|
|
21
|
+
"aback-tools",
|
|
22
|
+
"aback-launch"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
{
|
|
27
|
+
"path": "components/ui/copy-button.tsx",
|
|
28
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { IconCheck, IconCopy, IconX } from \"@tabler/icons-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Button, type ButtonProps } from \"@/components/ui/button\";\n\n/**\n * Writes text to the clipboard. Falls back to a temporary textarea and\n * `execCommand(\"copy\")` where the async Clipboard API is unavailable, such as\n * an insecure origin or an older embedded browser. Rejects when both fail.\n */\nasync function copyToClipboard(value: string): Promise<void> {\n if (typeof navigator !== \"undefined\" && navigator.clipboard?.writeText) {\n await navigator.clipboard.writeText(value);\n return;\n }\n\n const textarea = document.createElement(\"textarea\");\n textarea.value = value;\n textarea.setAttribute(\"readonly\", \"\");\n textarea.style.position = \"fixed\";\n textarea.style.opacity = \"0\";\n document.body.appendChild(textarea);\n textarea.select();\n const copied = document.execCommand(\"copy\");\n document.body.removeChild(textarea);\n if (!copied) throw new Error(\"Copy failed\");\n}\n\ntype CopyState = \"idle\" | \"copied\" | \"error\";\n\nexport interface CopyButtonProps\n extends Omit<ButtonProps, \"value\" | \"onClick\" | \"children\" | \"asChild\" | \"onCopy\"> {\n /** The exact text written to the clipboard. An empty string disables the button. */\n value: string;\n /** Accessible name and visible text before copying. */\n label?: string;\n /** Text shown and announced after a successful copy. */\n copiedLabel?: string;\n /** Text shown and announced when the browser refuses the write. */\n errorLabel?: string;\n /** Render the label next to the icon instead of an icon-only button. */\n showLabel?: boolean;\n /** How long the copied or error state stays visible, in milliseconds. */\n timeout?: number;\n onCopy?: (value: string) => void;\n onCopyError?: (error: unknown) => void;\n}\n\n/**\n * Copies a value and confirms it in place: the icon and label swap for a\n * moment, and a polite live region announces the result. It never toasts,\n * so the confirmation stays next to the thing that was copied.\n */\nfunction CopyButton({\n value,\n label = \"Copy\",\n copiedLabel = \"Copied\",\n errorLabel = \"Copy failed\",\n showLabel = false,\n timeout = 2000,\n onCopy,\n onCopyError,\n variant = \"ghost\",\n size,\n disabled,\n className,\n ...props\n}: CopyButtonProps) {\n const [state, setState] = React.useState<CopyState>(\"idle\");\n const timer = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n\n React.useEffect(() => {\n return () => {\n if (timer.current) clearTimeout(timer.current);\n };\n }, []);\n\n function settle(next: CopyState) {\n setState(next);\n if (timer.current) clearTimeout(timer.current);\n timer.current = setTimeout(() => setState(\"idle\"), timeout);\n }\n\n async function handleClick() {\n try {\n await copyToClipboard(value);\n settle(\"copied\");\n onCopy?.(value);\n } catch (error) {\n settle(\"error\");\n onCopyError?.(error);\n }\n }\n\n const currentLabel = state === \"copied\" ? copiedLabel : state === \"error\" ? errorLabel : label;\n const Icon = state === \"copied\" ? IconCheck : state === \"error\" ? IconX : IconCopy;\n\n return (\n <>\n <Button\n type=\"button\"\n data-slot=\"copy-button\"\n data-state={state}\n variant={variant}\n size={size ?? (showLabel ? \"sm\" : \"icon-sm\")}\n disabled={disabled || value.length === 0}\n aria-label={showLabel ? undefined : currentLabel}\n title={showLabel ? undefined : currentLabel}\n onClick={handleClick}\n className={cn(className)}\n {...props}\n >\n <Icon\n aria-hidden=\"true\"\n className={cn(\n state === \"copied\" && \"text-success\",\n state === \"error\" && \"text-danger\",\n )}\n />\n {showLabel ? <span>{currentLabel}</span> : null}\n </Button>\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {state === \"copied\" ? `${copiedLabel} to clipboard` : state === \"error\" ? errorLabel : \"\"}\n </span>\n </>\n );\n}\n\nexport { CopyButton, copyToClipboard };\n",
|
|
29
|
+
"type": "registry:ui",
|
|
30
|
+
"target": "components/ui/copy-button.tsx"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "cta-banner",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "Call to action",
|
|
6
|
+
"description": "A closing banner with one primary and one optional secondary action, in brand or neutral tone.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@tabler/icons-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://abackui.com/r/utils.json",
|
|
12
|
+
"https://abackui.com/r/button.json"
|
|
13
|
+
],
|
|
14
|
+
"categories": [
|
|
15
|
+
"Marketing"
|
|
16
|
+
],
|
|
17
|
+
"docs": "https://abackui.com/docs/blocks/cta-banner",
|
|
18
|
+
"meta": {
|
|
19
|
+
"status": "beta",
|
|
20
|
+
"usedIn": [
|
|
21
|
+
"aback-launch"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
{
|
|
26
|
+
"path": "components/blocks/cta-banner.tsx",
|
|
27
|
+
"content": "import { useId, type ReactNode } from \"react\";\nimport { IconArrowRight } from \"@tabler/icons-react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface CtaBannerBlockProps {\n title: ReactNode;\n description?: ReactNode;\n primaryAction: { label: string; href: string };\n secondaryAction?: { label: string; href: string };\n /** \"brand\" is a soft Aback Blue wash; \"muted\" is neutral for dense pages. */\n tone?: \"brand\" | \"muted\";\n}\n\n/**\n * A closing call to action for the end of a page or a section. One primary\n * action, at most one secondary. It stacks on phones and lines up on one row\n * from md.\n */\nexport function CtaBannerBlock({\n title,\n description,\n primaryAction,\n secondaryAction,\n tone = \"brand\",\n}: CtaBannerBlockProps) {\n const headingId = useId();\n return (\n <section aria-labelledby={headingId} className=\"mx-auto w-full max-w-6xl px-4 py-12 md:px-6\">\n <div\n className={cn(\n \"flex flex-col gap-6 border p-6 sm:p-10 md:flex-row md:items-center md:justify-between\",\n tone === \"brand\"\n ? \"border-primary-200 bg-brand-wash dark:border-primary-800/60\"\n : \"border-border bg-muted/40\",\n )}\n >\n <div className=\"flex max-w-2xl flex-col gap-2\">\n <h2 id={headingId} className=\"text-2xl font-bold tracking-tight text-foreground text-balance\">\n {title}\n </h2>\n {description ? (\n <p className=\"text-sm leading-relaxed text-muted-foreground sm:text-base\">{description}</p>\n ) : null}\n </div>\n <div className=\"flex shrink-0 flex-col gap-2 sm:flex-row\">\n {secondaryAction ? (\n <Button variant=\"outline\" size=\"lg\" asChild>\n <a href={secondaryAction.href}>{secondaryAction.label}</a>\n </Button>\n ) : null}\n <Button variant=\"brand\" size=\"lg\" asChild>\n <a href={primaryAction.href}>\n {primaryAction.label}\n <IconArrowRight aria-hidden=\"true\" />\n </a>\n </Button>\n </div>\n </div>\n </section>\n );\n}\n",
|
|
28
|
+
"type": "registry:block",
|
|
29
|
+
"target": "components/blocks/cta-banner.tsx"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "dashboard-shell",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "Dashboard shell",
|
|
6
|
+
"description": "A responsive application shell with persistent navigation, a mobile sheet, top bar, and page header.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@tabler/icons-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://abackui.com/r/utils.json",
|
|
12
|
+
"https://abackui.com/r/avatar.json",
|
|
13
|
+
"https://abackui.com/r/button.json",
|
|
14
|
+
"https://abackui.com/r/dropdown-menu.json",
|
|
15
|
+
"https://abackui.com/r/sheet.json"
|
|
16
|
+
],
|
|
17
|
+
"categories": [
|
|
18
|
+
"Application"
|
|
19
|
+
],
|
|
20
|
+
"docs": "https://abackui.com/docs/blocks/dashboard-shell",
|
|
21
|
+
"meta": {
|
|
22
|
+
"status": "beta"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
{
|
|
26
|
+
"path": "components/blocks/dashboard-shell.tsx",
|
|
27
|
+
"content": "\"use client\";\n\nimport { useState, type ReactNode } from \"react\";\nimport {\n IconChevronDown,\n IconLogout,\n IconMenu,\n IconSearch,\n IconSettings,\n} from \"@tabler/icons-react\";\n\nimport { Avatar, AvatarFallback } from \"@/components/ui/avatar\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport {\n Sheet,\n SheetContent,\n SheetDescription,\n SheetHeader,\n SheetTitle,\n SheetTrigger,\n} from \"@/components/ui/sheet\";\n\nexport interface DashboardNavItem {\n title: string;\n href: string;\n icon?: ReactNode;\n /** A short count or state, such as an unread total. */\n badge?: string;\n}\n\nexport interface DashboardUser {\n name: string;\n email: string;\n initials?: string;\n}\n\nexport interface DashboardShellProps {\n children: ReactNode;\n nav: DashboardNavItem[];\n /** The `href` of the current section. Matched exactly. */\n activeHref?: string;\n title: string;\n description?: string;\n actions?: ReactNode;\n appName?: string;\n user?: DashboardUser;\n /** A real destination for account settings. The menu item is omitted when absent. */\n accountHref?: string;\n onSignOut?: () => void;\n /** Renders a search trigger in the top bar when provided. */\n onSearch?: () => void;\n}\n\nfunction initialsFor(user: DashboardUser) {\n if (user.initials) return user.initials;\n return user.name\n .split(/\\s+/)\n .filter(Boolean)\n .slice(0, 2)\n .map((part) => part[0]?.toUpperCase() ?? \"\")\n .join(\"\");\n}\n\nfunction NavLinks({\n nav,\n activeHref,\n onNavigate,\n}: {\n nav: DashboardNavItem[];\n activeHref?: string;\n onNavigate?: () => void;\n}) {\n return (\n <ul className=\"flex flex-col gap-0.5\">\n {nav.map((item) => {\n const active = item.href === activeHref;\n return (\n <li key={item.href}>\n <a\n href={item.href}\n aria-current={active ? \"page\" : undefined}\n onClick={onNavigate}\n className={\n active\n ? \"flex items-center gap-2.5 border-l-2 border-primary-500 bg-primary-50 px-3 py-2 text-sm font-medium text-primary-700 transition-colors dark:bg-primary-950/40 dark:text-primary-300\"\n : \"flex items-center gap-2.5 border-l-2 border-transparent px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted/60 hover:text-foreground\"\n }\n >\n {item.icon ? (\n <span aria-hidden=\"true\" className=\"shrink-0 [&_svg]:size-4\">\n {item.icon}\n </span>\n ) : null}\n <span className=\"min-w-0 flex-1 truncate\">{item.title}</span>\n {item.badge ? (\n <span className=\"shrink-0 bg-muted px-1.5 py-0.5 text-[10px] font-semibold tabular-nums text-muted-foreground\">\n {item.badge}\n </span>\n ) : null}\n </a>\n </li>\n );\n })}\n </ul>\n );\n}\n\nfunction UserMenu({\n user,\n accountHref,\n onSignOut,\n}: {\n user: DashboardUser;\n accountHref?: string;\n onSignOut?: () => void;\n}) {\n const identity = (\n <>\n <Avatar size=\"sm\">\n <AvatarFallback aria-hidden=\"true\">{initialsFor(user)}</AvatarFallback>\n </Avatar>\n <span className=\"min-w-0 flex-1 text-left\">\n <span className=\"block truncate text-sm font-medium text-foreground\">{user.name}</span>\n <span className=\"block truncate text-xs text-muted-foreground\">{user.email}</span>\n </span>\n </>\n );\n\n if (!accountHref && !onSignOut) {\n return <div className=\"flex items-center gap-2.5\">{identity}</div>;\n }\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n className=\"flex w-full items-center gap-2.5 p-1 text-left transition-colors hover:bg-muted/60 focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card focus-visible:outline-none\"\n aria-label={`Account menu for ${user.name}`}\n >\n {identity}\n <IconChevronDown aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"w-56\">\n <DropdownMenuLabel>\n <span className=\"block truncate text-sm font-medium\">{user.name}</span>\n <span className=\"block truncate text-xs font-normal text-muted-foreground\">\n {user.email}\n </span>\n </DropdownMenuLabel>\n <DropdownMenuSeparator />\n {accountHref ? (\n <DropdownMenuItem asChild>\n <a href={accountHref}>\n <IconSettings aria-hidden=\"true\" />\n Account settings\n </a>\n </DropdownMenuItem>\n ) : null}\n {onSignOut ? (\n <DropdownMenuItem variant=\"destructive\" onSelect={onSignOut}>\n <IconLogout aria-hidden=\"true\" />\n Sign out\n </DropdownMenuItem>\n ) : null}\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n\n/**\n * A responsive application shell: a persistent sidebar from `md` up, a sheet\n * below it, a top bar, and the page header. It owns no routing, data, or auth.\n * Pass real `href`s, your user, and the page content; `onSearch` and\n * `onSignOut` are optional, so the shell never renders a control without an\n * action behind it.\n *\n * The content region is a div, not a landmark. Wrap the shell in your app's\n * `<main>`, or replace the content container with `<main>` when the shell owns\n * the whole page.\n */\nexport function DashboardShellBlock({\n children,\n nav,\n activeHref,\n title,\n description,\n actions,\n appName = \"Aback\",\n user,\n accountHref,\n onSignOut,\n onSearch,\n}: DashboardShellProps) {\n const [mobileOpen, setMobileOpen] = useState(false);\n const brandMark = (\n <span\n aria-hidden=\"true\"\n className=\"flex size-6 shrink-0 items-center justify-center bg-primary text-[11px] font-bold text-primary-foreground\"\n >\n {appName.slice(0, 1).toUpperCase()}\n </span>\n );\n\n return (\n <div className=\"flex min-h-[640px] w-full border border-border bg-background text-foreground\">\n <aside className=\"hidden w-56 shrink-0 flex-col border-r border-border bg-card md:flex\">\n <div className=\"flex h-14 items-center gap-2 border-b border-border px-4\">\n {brandMark}\n <span className=\"truncate text-sm font-semibold\">{appName}</span>\n </div>\n\n <nav aria-label=\"Primary\" className=\"flex-1 overflow-y-auto p-2\">\n <NavLinks nav={nav} activeHref={activeHref} />\n </nav>\n\n {user ? (\n <div className=\"border-t border-border p-2.5\">\n <UserMenu user={user} accountHref={accountHref} onSignOut={onSignOut} />\n </div>\n ) : null}\n </aside>\n\n <div className=\"flex min-w-0 flex-1 flex-col\">\n <header className=\"flex h-14 shrink-0 items-center gap-2 border-b border-border px-3 sm:px-4\">\n <Sheet open={mobileOpen} onOpenChange={setMobileOpen}>\n <SheetTrigger asChild>\n <Button variant=\"ghost\" size=\"icon-sm\" className=\"md:hidden\" aria-label=\"Open navigation\">\n <IconMenu aria-hidden=\"true\" />\n </Button>\n </SheetTrigger>\n <SheetContent side=\"left\" className=\"w-72 p-0\">\n <SheetHeader className=\"border-b border-border p-4\">\n <SheetTitle className=\"flex items-center gap-2 text-sm font-semibold\">\n {brandMark}\n {appName}\n </SheetTitle>\n <SheetDescription className=\"sr-only\">\n Sections available in {appName}.\n </SheetDescription>\n </SheetHeader>\n <nav aria-label=\"Primary\" className=\"p-2\">\n <NavLinks\n nav={nav}\n activeHref={activeHref}\n onNavigate={() => setMobileOpen(false)}\n />\n </nav>\n {user ? (\n <div className=\"mt-auto border-t border-border p-3\">\n <UserMenu user={user} accountHref={accountHref} onSignOut={onSignOut} />\n </div>\n ) : null}\n </SheetContent>\n </Sheet>\n\n <span className=\"truncate text-sm font-semibold md:hidden\">{appName}</span>\n\n {onSearch ? (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={onSearch}\n className=\"ml-auto gap-2 text-muted-foreground\"\n >\n <IconSearch aria-hidden=\"true\" />\n <span className=\"hidden sm:inline\">Search</span>\n <span className=\"sr-only sm:hidden\">Search</span>\n </Button>\n ) : null}\n\n {user ? (\n <div className=\"ml-auto md:hidden\">\n <UserMenu user={user} accountHref={accountHref} onSignOut={onSignOut} />\n </div>\n ) : null}\n </header>\n\n <div className=\"flex flex-1 flex-col\">\n <div className=\"flex flex-wrap items-start justify-between gap-3 border-b border-border px-4 py-5 sm:px-6\">\n <div className=\"min-w-0\">\n <h1 className=\"truncate text-lg font-semibold tracking-tight\">{title}</h1>\n {description ? (\n <p className=\"mt-0.5 text-sm text-muted-foreground\">{description}</p>\n ) : null}\n </div>\n {actions ? <div className=\"flex shrink-0 items-center gap-2\">{actions}</div> : null}\n </div>\n\n <div className=\"flex-1 p-4 sm:p-6\">{children}</div>\n </div>\n </div>\n </div>\n );\n}\n",
|
|
28
|
+
"type": "registry:block",
|
|
29
|
+
"target": "components/blocks/dashboard-shell.tsx"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "date-picker",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Date Picker",
|
|
6
|
+
"description": "A date field and a range field, with real keyboard behavior and native form support.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@tabler/icons-react",
|
|
9
|
+
"react-day-picker"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"https://abackui.com/r/utils.json",
|
|
13
|
+
"https://abackui.com/r/button.json",
|
|
14
|
+
"https://abackui.com/r/calendar.json",
|
|
15
|
+
"https://abackui.com/r/popover.json"
|
|
16
|
+
],
|
|
17
|
+
"categories": [
|
|
18
|
+
"Forms"
|
|
19
|
+
],
|
|
20
|
+
"docs": "https://abackui.com/docs/components/date-picker",
|
|
21
|
+
"meta": {
|
|
22
|
+
"status": "beta"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
{
|
|
26
|
+
"path": "components/ui/date-picker.tsx",
|
|
27
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { IconCalendar, IconX } from \"@tabler/icons-react\";\nimport type { DateRange } from \"react-day-picker\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\nimport { Calendar, formatCalendarDate } from \"@/components/ui/calendar\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\n\nfunction toIsoDate(date: Date): string {\n const year = date.getFullYear();\n const month = `${date.getMonth() + 1}`.padStart(2, \"0\");\n const day = `${date.getDate()}`.padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n\nfunction selectionBounds(min?: Date, max?: Date) {\n const matchers = [];\n if (min) matchers.push({ before: min });\n if (max) matchers.push({ after: max });\n return matchers.length > 0 ? matchers : undefined;\n}\n\nexport interface DatePickerProps {\n value?: Date;\n onChange?: (date: Date | undefined) => void;\n placeholder?: string;\n /** Reject dates before this day. */\n min?: Date;\n /** Reject dates after this day. */\n max?: Date;\n disabled?: boolean;\n /** Show the \"Clear\" action inside the calendar. Defaults to `true`. */\n clearable?: boolean;\n /** Locale used for the trigger label, for example `en-GB`. */\n locale?: string;\n /** Replace the default `Oct 24, 2026` label. */\n format?: (date: Date) => string;\n /** Month shown before the visitor picks a date. */\n defaultMonth?: Date;\n id?: string;\n /** Renders a hidden input so the value submits with a native form. */\n name?: string;\n className?: string;\n align?: \"start\" | \"center\" | \"end\";\n \"aria-describedby\"?: string;\n \"aria-invalid\"?: boolean;\n \"aria-labelledby\"?: string;\n}\n\n/**\n * A date field: a button that opens a calendar in a popover.\n *\n * The trigger is a real button, so the field is reachable and operable by\n * keyboard. Selecting a day closes the popover and returns focus to the\n * trigger; Escape closes without changing the value. When `name` is set, a\n * hidden input carries the `yyyy-mm-dd` value into native form submission.\n */\nfunction DatePicker({\n value,\n onChange,\n placeholder = \"Pick a date\",\n min,\n max,\n disabled,\n clearable = true,\n locale,\n format,\n defaultMonth,\n id,\n name,\n className,\n align = \"start\",\n \"aria-describedby\": ariaDescribedBy,\n \"aria-invalid\": ariaInvalid,\n \"aria-labelledby\": ariaLabelledBy,\n}: DatePickerProps) {\n const [open, setOpen] = React.useState(false);\n const label = value ? (format ? format(value) : formatCalendarDate(value, locale)) : placeholder;\n\n function select(date: Date | undefined) {\n onChange?.(date);\n if (date) setOpen(false);\n }\n\n return (\n <div data-slot=\"date-picker\" className={cn(\"relative w-full\", className)}>\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n id={id}\n type=\"button\"\n variant=\"outline\"\n disabled={disabled}\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n aria-describedby={ariaDescribedBy}\n aria-invalid={ariaInvalid}\n aria-labelledby={ariaLabelledBy}\n className={cn(\n \"w-full justify-start gap-2 font-normal\",\n !value && \"text-muted-foreground\",\n )}\n >\n <IconCalendar aria-hidden=\"true\" className=\"size-4 shrink-0\" />\n <span className=\"truncate\">{label}</span>\n </Button>\n </PopoverTrigger>\n <PopoverContent align={align} aria-label=\"Choose a date\" className=\"w-auto p-0\">\n <Calendar\n mode=\"single\"\n selected={value}\n onSelect={select}\n defaultMonth={value ?? defaultMonth}\n disabled={selectionBounds(min, max)}\n autoFocus\n />\n {clearable && value ? (\n <div className=\"border-t border-border p-1\">\n <button\n type=\"button\"\n className=\"flex w-full items-center gap-2 px-2 py-1.5 text-left text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-focus-ring\"\n onClick={() => {\n onChange?.(undefined);\n setOpen(false);\n }}\n >\n <IconX aria-hidden=\"true\" className=\"size-3.5\" />\n Clear date\n </button>\n </div>\n ) : null}\n </PopoverContent>\n </Popover>\n {name ? <input type=\"hidden\" name={name} value={value ? toIsoDate(value) : \"\"} /> : null}\n </div>\n );\n}\n\nexport interface DateRangePickerProps {\n value?: DateRange;\n onChange?: (range: DateRange | undefined) => void;\n placeholder?: string;\n min?: Date;\n max?: Date;\n disabled?: boolean;\n clearable?: boolean;\n locale?: string;\n format?: (date: Date) => string;\n /** Number of months shown side by side on wide screens. Defaults to `1`. */\n numberOfMonths?: number;\n defaultMonth?: Date;\n id?: string;\n className?: string;\n align?: \"start\" | \"center\" | \"end\";\n \"aria-describedby\"?: string;\n \"aria-labelledby\"?: string;\n}\n\n/**\n * A range field for reports, filters, and booking windows.\n *\n * The popover stays open until both ends are chosen, so the visitor can see\n * the in-progress range. A completed range is announced through the calendar\n * grid's `aria-multiselectable` and per-day selected state.\n */\nfunction DateRangePicker({\n value,\n onChange,\n placeholder = \"Pick a date range\",\n min,\n max,\n disabled,\n clearable = true,\n locale,\n format,\n numberOfMonths = 1,\n defaultMonth,\n id,\n className,\n align = \"start\",\n \"aria-describedby\": ariaDescribedBy,\n \"aria-labelledby\": ariaLabelledBy,\n}: DateRangePickerProps) {\n const [open, setOpen] = React.useState(false);\n const formatOne = (date: Date) =>\n format ? format(date) : formatCalendarDate(date, locale);\n\n const sameDay =\n value?.from && value.to ? value.from.toDateString() === value.to.toDateString() : false;\n const label = value?.from\n ? value.to && !sameDay\n ? `${formatOne(value.from)} – ${formatOne(value.to)}`\n : formatOne(value.from)\n : placeholder;\n\n function select(range: DateRange | undefined) {\n onChange?.(range);\n // A first click in react-day-picker produces a one-day range. Keeping the\n // calendar open lets the visitor finish the second end; a range that spans\n // two different days is complete and closes the popover.\n if (range?.from && range.to && range.from.toDateString() !== range.to.toDateString()) {\n setOpen(false);\n }\n }\n\n return (\n <div data-slot=\"date-range-picker\" className={cn(\"relative w-full\", className)}>\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n id={id}\n type=\"button\"\n variant=\"outline\"\n disabled={disabled}\n aria-haspopup=\"dialog\"\n aria-expanded={open}\n aria-describedby={ariaDescribedBy}\n aria-labelledby={ariaLabelledBy}\n className={cn(\n \"w-full justify-start gap-2 font-normal\",\n !value?.from && \"text-muted-foreground\",\n )}\n >\n <IconCalendar aria-hidden=\"true\" className=\"size-4 shrink-0\" />\n <span className=\"truncate\">{label}</span>\n </Button>\n </PopoverTrigger>\n <PopoverContent align={align} aria-label=\"Choose a date range\" className=\"w-auto p-0\">\n <Calendar\n mode=\"range\"\n selected={value}\n onSelect={select}\n defaultMonth={value?.from ?? defaultMonth}\n numberOfMonths={numberOfMonths}\n disabled={selectionBounds(min, max)}\n autoFocus\n />\n {clearable && value?.from ? (\n <div className=\"border-t border-border p-1\">\n <button\n type=\"button\"\n className=\"flex w-full items-center gap-2 px-2 py-1.5 text-left text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-focus-ring\"\n onClick={() => {\n onChange?.(undefined);\n setOpen(false);\n }}\n >\n <IconX aria-hidden=\"true\" className=\"size-3.5\" />\n Clear range\n </button>\n </div>\n ) : null}\n </PopoverContent>\n </Popover>\n </div>\n );\n}\n\nexport { DatePicker, DateRangePicker };\n",
|
|
28
|
+
"type": "registry:ui",
|
|
29
|
+
"target": "components/ui/date-picker.tsx"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "description-list",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Description List",
|
|
6
|
+
"description": "Key and value rows for metadata, breakdowns, and totals, as a real description list.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://abackui.com/r/utils.json"
|
|
9
|
+
],
|
|
10
|
+
"categories": [
|
|
11
|
+
"Data Display"
|
|
12
|
+
],
|
|
13
|
+
"docs": "https://abackui.com/docs/components/description-list",
|
|
14
|
+
"meta": {
|
|
15
|
+
"status": "beta",
|
|
16
|
+
"usedIn": [
|
|
17
|
+
"aback-tools"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
{
|
|
22
|
+
"path": "components/ui/description-list.tsx",
|
|
23
|
+
"content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype DetailsTone = \"neutral\" | \"success\" | \"warning\" | \"danger\" | \"info\";\n\nconst toneText: Record<DetailsTone, string> = {\n neutral: \"text-foreground\",\n success: \"text-success\",\n warning: \"text-warning\",\n danger: \"text-danger\",\n info: \"text-info\",\n};\n\nexport interface DescriptionListProps extends React.ComponentProps<\"dl\"> {\n /** Draws the outer border. Turn it off when the list sits inside a card. */\n bordered?: boolean;\n}\n\n/**\n * Key and value rows: file metadata, a price breakdown, request headers, or\n * account details. A real `<dl>`, so each term labels its value for assistive\n * technology. Rows wrap on narrow screens instead of truncating the value.\n */\nfunction DescriptionList({ bordered = true, className, ...props }: DescriptionListProps) {\n return (\n <dl\n data-slot=\"description-list\"\n className={cn(\"divide-y divide-border text-sm\", bordered && \"border border-border\", className)}\n {...props}\n />\n );\n}\n\nexport interface DescriptionListItemProps extends React.ComponentProps<\"div\"> {\n /** A bold row with a heavier rule, for a total. */\n emphasis?: boolean;\n}\n\nfunction DescriptionListItem({ emphasis = false, className, ...props }: DescriptionListItemProps) {\n return (\n <div\n data-slot=\"description-list-item\"\n data-emphasis={emphasis ? \"\" : undefined}\n className={cn(\n \"flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1 px-3 py-2.5\",\n emphasis && \"border-t-2 border-t-border font-semibold [&>dt]:text-foreground\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport interface DescriptionListTermProps extends React.ComponentProps<\"dt\"> {\n /** Monospace keys for technical fields such as header names. */\n mono?: boolean;\n}\n\nfunction DescriptionListTerm({ mono = false, className, ...props }: DescriptionListTermProps) {\n return (\n <dt\n data-slot=\"description-list-term\"\n className={cn(\n \"min-w-[4.5rem] text-muted-foreground\",\n mono && \"font-mono text-xs\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport interface DescriptionListDetailsProps extends React.ComponentProps<\"dd\"> {\n /** Colours the value. Keep a sign or a word with it, such as \"-$12\" or \"saved\". */\n tone?: DetailsTone;\n}\n\nfunction DescriptionListDetails({ tone = \"neutral\", className, ...props }: DescriptionListDetailsProps) {\n return (\n <dd\n data-slot=\"description-list-details\"\n className={cn(\"text-right tabular-nums break-words\", toneText[tone], className)}\n {...props}\n />\n );\n}\n\nexport { DescriptionList, DescriptionListDetails, DescriptionListItem, DescriptionListTerm };\n",
|
|
24
|
+
"type": "registry:ui",
|
|
25
|
+
"target": "components/ui/description-list.tsx"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "dialog",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Dialog",
|
|
6
|
+
"description": "A modal dialog with focus trapping, Escape dismissal, and focus return.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"radix-ui",
|
|
9
|
+
"@tabler/icons-react"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"https://abackui.com/r/utils.json"
|
|
13
|
+
],
|
|
14
|
+
"categories": [
|
|
15
|
+
"Overlays"
|
|
16
|
+
],
|
|
17
|
+
"docs": "https://abackui.com/docs/components/dialog",
|
|
18
|
+
"meta": {
|
|
19
|
+
"status": "stable",
|
|
20
|
+
"usedIn": [
|
|
21
|
+
"aback-launch",
|
|
22
|
+
"aback-group"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
{
|
|
27
|
+
"path": "components/ui/dialog.tsx",
|
|
28
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Dialog as DialogPrimitive } from \"radix-ui\";\nimport { IconX } from \"@tabler/icons-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Dialog = DialogPrimitive.Root;\nconst DialogTrigger = DialogPrimitive.Trigger;\nconst DialogClose = DialogPrimitive.Close;\nconst DialogPortal = DialogPrimitive.Portal;\n\nfunction DialogOverlay({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {\n return (\n <DialogPrimitive.Overlay\n data-slot=\"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 DialogContent({\n className,\n children,\n showCloseButton = true,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Content> & {\n showCloseButton?: boolean;\n}) {\n return (\n <DialogPortal>\n <DialogOverlay />\n <DialogPrimitive.Content\n data-slot=\"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-lg -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 {children}\n {showCloseButton ? (\n <DialogPrimitive.Close\n data-slot=\"dialog-close\"\n className={cn(\n \"absolute right-3 top-3 inline-flex size-8 items-center justify-center text-muted-foreground transition-colors hover:bg-muted hover:text-foreground\",\n \"outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-popover\",\n )}\n >\n <IconX aria-hidden=\"true\" className=\"size-4\" />\n <span className=\"sr-only\">Close</span>\n </DialogPrimitive.Close>\n ) : null}\n </DialogPrimitive.Content>\n </DialogPortal>\n );\n}\n\nfunction DialogHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"dialog-header\"\n className={cn(\"flex flex-col gap-1.5 pr-6 text-left\", className)}\n {...props}\n />\n );\n}\n\nfunction DialogFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"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 DialogTitle({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Title>) {\n return (\n <DialogPrimitive.Title\n data-slot=\"dialog-title\"\n className={cn(\"text-lg font-semibold leading-none tracking-tight\", className)}\n {...props}\n />\n );\n}\n\nfunction DialogDescription({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Description>) {\n return (\n <DialogPrimitive.Description\n data-slot=\"dialog-description\"\n className={cn(\"text-sm leading-relaxed text-muted-foreground\", className)}\n {...props}\n />\n );\n}\n\nexport {\n Dialog,\n DialogTrigger,\n DialogClose,\n DialogPortal,\n DialogOverlay,\n DialogContent,\n DialogHeader,\n DialogFooter,\n DialogTitle,\n DialogDescription,\n};\n",
|
|
29
|
+
"type": "registry:ui",
|
|
30
|
+
"target": "components/ui/dialog.tsx"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "dropdown-menu",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Dropdown Menu",
|
|
6
|
+
"description": "A keyboard-navigable action menu with checkbox and radio items.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"radix-ui",
|
|
9
|
+
"@tabler/icons-react"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"https://abackui.com/r/utils.json"
|
|
13
|
+
],
|
|
14
|
+
"categories": [
|
|
15
|
+
"Overlays"
|
|
16
|
+
],
|
|
17
|
+
"docs": "https://abackui.com/docs/components/dropdown-menu",
|
|
18
|
+
"meta": {
|
|
19
|
+
"status": "stable",
|
|
20
|
+
"usedIn": [
|
|
21
|
+
"aback-launch",
|
|
22
|
+
"aback-group"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
{
|
|
27
|
+
"path": "components/ui/dropdown-menu.tsx",
|
|
28
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { DropdownMenu as DropdownMenuPrimitive } from \"radix-ui\";\nimport { IconCheck, IconChevronRight } from \"@tabler/icons-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst DropdownMenu = DropdownMenuPrimitive.Root;\nconst DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;\nconst DropdownMenuGroup = DropdownMenuPrimitive.Group;\nconst DropdownMenuPortal = DropdownMenuPrimitive.Portal;\nconst DropdownMenuSub = DropdownMenuPrimitive.Sub;\nconst DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;\n\nfunction DropdownMenuContent({\n className,\n sideOffset = 4,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {\n return (\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.Content\n data-slot=\"dropdown-menu-content\"\n sideOffset={sideOffset}\n className={cn(\n \"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[12rem] overflow-y-auto\",\n \"border border-border bg-popover p-1 text-popover-foreground shadow-lg\",\n \"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 </DropdownMenuPrimitive.Portal>\n );\n}\n\nfunction DropdownMenuItem({\n className,\n inset,\n variant = \"default\",\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {\n inset?: boolean;\n variant?: \"default\" | \"destructive\";\n}) {\n return (\n <DropdownMenuPrimitive.Item\n data-slot=\"dropdown-menu-item\"\n data-variant={variant}\n className={cn(\n \"relative flex cursor-default select-none items-center gap-2 px-3 py-2 text-sm outline-none\",\n \"transition-colors focus:bg-muted focus:text-foreground\",\n \"data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n \"[&_svg]:size-4 [&_svg]:shrink-0\",\n inset && \"pl-8\",\n variant === \"destructive\" &&\n \"text-destructive focus:bg-danger-soft focus:text-danger-strong\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction DropdownMenuCheckboxItem({\n className,\n children,\n checked,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {\n return (\n <DropdownMenuPrimitive.CheckboxItem\n data-slot=\"dropdown-menu-checkbox-item\"\n checked={checked}\n className={cn(\n \"relative flex cursor-default select-none items-center gap-2 py-2 pl-8 pr-3 text-sm outline-none\",\n \"transition-colors focus:bg-muted focus:text-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n className,\n )}\n {...props}\n >\n <span className=\"absolute left-2 flex size-4 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <IconCheck aria-hidden=\"true\" className=\"size-4\" />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {children}\n </DropdownMenuPrimitive.CheckboxItem>\n );\n}\n\nfunction DropdownMenuRadioItem({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {\n return (\n <DropdownMenuPrimitive.RadioItem\n data-slot=\"dropdown-menu-radio-item\"\n className={cn(\n \"relative flex cursor-default select-none items-center gap-2 py-2 pl-8 pr-3 text-sm outline-none\",\n \"transition-colors focus:bg-muted focus:text-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n className,\n )}\n {...props}\n >\n <span className=\"absolute left-2 flex size-4 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <span className=\"size-2 bg-current\" />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {children}\n </DropdownMenuPrimitive.RadioItem>\n );\n}\n\nfunction DropdownMenuLabel({\n className,\n inset,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {\n inset?: boolean;\n}) {\n return (\n <DropdownMenuPrimitive.Label\n data-slot=\"dropdown-menu-label\"\n className={cn(\n \"px-3 py-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground\",\n inset && \"pl-8\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction DropdownMenuSeparator({\n className,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {\n return (\n <DropdownMenuPrimitive.Separator\n data-slot=\"dropdown-menu-separator\"\n className={cn(\"-mx-1 my-1 h-px bg-separator\", className)}\n {...props}\n />\n );\n}\n\nfunction DropdownMenuShortcut({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"dropdown-menu-shortcut\"\n className={cn(\n \"ml-auto text-[10px] font-semibold uppercase tracking-[0.12em] text-muted-foreground\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction DropdownMenuSubTrigger({\n className,\n inset,\n children,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {\n inset?: boolean;\n}) {\n return (\n <DropdownMenuPrimitive.SubTrigger\n data-slot=\"dropdown-menu-sub-trigger\"\n className={cn(\n \"flex cursor-default select-none items-center gap-2 px-3 py-2 text-sm outline-none\",\n \"focus:bg-muted data-[state=open]:bg-muted\",\n inset && \"pl-8\",\n className,\n )}\n {...props}\n >\n {children}\n <IconChevronRight aria-hidden=\"true\" className=\"ml-auto size-4\" />\n </DropdownMenuPrimitive.SubTrigger>\n );\n}\n\nfunction DropdownMenuSubContent({\n className,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {\n return (\n <DropdownMenuPrimitive.SubContent\n data-slot=\"dropdown-menu-sub-content\"\n className={cn(\n \"z-50 min-w-[10rem] overflow-hidden border border-border bg-popover p-1 text-popover-foreground shadow-lg\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport {\n DropdownMenu,\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuCheckboxItem,\n DropdownMenuRadioItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuShortcut,\n DropdownMenuGroup,\n DropdownMenuPortal,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuRadioGroup,\n};\n",
|
|
29
|
+
"type": "registry:ui",
|
|
30
|
+
"target": "components/ui/dropdown-menu.tsx"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "dropzone",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Dropzone",
|
|
6
|
+
"description": "A keyboard-operable drag-and-drop file area that validates type, size, and count.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@tabler/icons-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://abackui.com/r/utils.json"
|
|
12
|
+
],
|
|
13
|
+
"categories": [
|
|
14
|
+
"Forms"
|
|
15
|
+
],
|
|
16
|
+
"docs": "https://abackui.com/docs/components/dropzone",
|
|
17
|
+
"meta": {
|
|
18
|
+
"status": "beta",
|
|
19
|
+
"usedIn": [
|
|
20
|
+
"aback-tools"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
{
|
|
25
|
+
"path": "components/ui/dropzone.tsx",
|
|
26
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { IconUpload } from \"@tabler/icons-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type FileRejectionReason = \"type\" | \"size\" | \"count\";\n\nexport interface FileRejection {\n file: File;\n reason: FileRejectionReason;\n}\n\n/**\n * True when a file satisfies an `accept` string in the same syntax as the\n * native input: extensions (\".pdf\"), exact MIME types (\"image/png\"), and\n * wildcards (\"image/*\"). The picker filters by `accept`, but dropped files do\n * not, so every file is checked here before it is reported as accepted.\n */\nfunction fileMatchesAccept(file: File, accept?: string) {\n if (!accept) return true;\n const tokens = accept\n .split(\",\")\n .map((token) => token.trim().toLowerCase())\n .filter(Boolean);\n if (tokens.length === 0) return true;\n\n const name = file.name.toLowerCase();\n const type = (file.type || \"\").toLowerCase();\n return tokens.some((token) => {\n if (token.startsWith(\".\")) return name.endsWith(token);\n if (token.endsWith(\"/*\")) return type.startsWith(token.slice(0, -1));\n return type === token;\n });\n}\n\n/** \"980 B\", \"14.2 KB\", \"3.1 MB\". Uses 1024-based units, like file managers. */\nfunction formatFileSize(bytes: number) {\n if (!Number.isFinite(bytes) || bytes < 0) return \"\";\n if (bytes < 1024) return `${bytes} B`;\n const units = [\"KB\", \"MB\", \"GB\", \"TB\"];\n let value = bytes / 1024;\n let unit = 0;\n while (value >= 1024 && unit < units.length - 1) {\n value /= 1024;\n unit += 1;\n }\n return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;\n}\n\nexport interface DropzoneProps\n extends Omit<React.ComponentProps<\"div\">, \"onDrop\" | \"onChange\" | \"children\"> {\n /** Called with the files that passed type, size, and count checks. */\n onFilesAccepted: (files: File[]) => void;\n /** Called with each refused file and the reason, so the page can explain it. */\n onFilesRejected?: (rejections: FileRejection[]) => void;\n /** Accepted types, in native `accept` syntax. Checked for dropped files too. */\n accept?: string;\n multiple?: boolean;\n /** Largest accepted file in bytes. */\n maxSize?: number;\n /** Most files accepted from one selection. Defaults to 1 unless `multiple`. */\n maxFiles?: number;\n disabled?: boolean;\n /** Primary line, also the input's accessible name. */\n label?: React.ReactNode;\n /** Secondary line under the label. */\n description?: React.ReactNode;\n /** Formats and limits, such as \"PNG or JPG, up to 10 MB\". Announced as the description. */\n hint?: React.ReactNode;\n icon?: React.ReactNode;\n /** Props for the underlying file input, such as `id`, `name`, or `aria-describedby`. */\n inputProps?: Omit<\n React.ComponentProps<\"input\">,\n \"type\" | \"accept\" | \"multiple\" | \"disabled\" | \"onChange\"\n >;\n}\n\n/**\n * A drag-and-drop area backed by a real file input. The input is visually\n * hidden but stays focusable, so Tab reaches it, Enter or Space opens the\n * picker, and the frame shows the focus ring. Files are validated the same way\n * whether they were dropped or picked.\n */\nfunction Dropzone({\n onFilesAccepted,\n onFilesRejected,\n accept,\n multiple = false,\n maxSize,\n maxFiles,\n disabled = false,\n label = \"Drop a file here\",\n description = \"or click to browse\",\n hint,\n icon,\n inputProps,\n className,\n ...props\n}: DropzoneProps) {\n const generatedId = React.useId();\n const inputId = inputProps?.id ?? `${generatedId}-input`;\n const labelId = `${generatedId}-label`;\n const descriptionId = `${generatedId}-description`;\n const hintId = `${generatedId}-hint`;\n const [dragging, setDragging] = React.useState(false);\n const depth = React.useRef(0);\n\n const limit = maxFiles ?? (multiple ? Number.POSITIVE_INFINITY : 1);\n\n function handleFiles(list: FileList | File[] | null) {\n if (!list || disabled) return;\n const files = Array.from(list);\n const accepted: File[] = [];\n const rejected: FileRejection[] = [];\n\n for (const file of files) {\n if (!fileMatchesAccept(file, accept)) {\n rejected.push({ file, reason: \"type\" });\n } else if (maxSize !== undefined && file.size > maxSize) {\n rejected.push({ file, reason: \"size\" });\n } else if (accepted.length >= limit) {\n rejected.push({ file, reason: \"count\" });\n } else {\n accepted.push(file);\n }\n }\n\n if (accepted.length > 0) onFilesAccepted(accepted);\n if (rejected.length > 0) onFilesRejected?.(rejected);\n }\n\n function handleDragEnter(event: React.DragEvent<HTMLDivElement>) {\n event.preventDefault();\n if (disabled) return;\n depth.current += 1;\n setDragging(true);\n }\n\n function handleDragOver(event: React.DragEvent<HTMLDivElement>) {\n event.preventDefault();\n if (disabled) return;\n event.dataTransfer.dropEffect = \"copy\";\n }\n\n function handleDragLeave(event: React.DragEvent<HTMLDivElement>) {\n event.preventDefault();\n depth.current = Math.max(0, depth.current - 1);\n if (depth.current === 0) setDragging(false);\n }\n\n function handleDrop(event: React.DragEvent<HTMLDivElement>) {\n event.preventDefault();\n depth.current = 0;\n setDragging(false);\n handleFiles(event.dataTransfer.files);\n }\n\n const describedBy = [\n description ? descriptionId : null,\n hint ? hintId : null,\n inputProps?.[\"aria-describedby\"],\n ]\n .filter(Boolean)\n .join(\" \");\n\n return (\n <div\n data-slot=\"dropzone\"\n data-dragging={dragging ? \"\" : undefined}\n data-disabled={disabled ? \"\" : undefined}\n onDragEnter={handleDragEnter}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n onDrop={handleDrop}\n className={cn(\n \"relative border-2 border-dashed border-muted-foreground/25 bg-background transition-colors duration-200\",\n \"has-[input:focus-visible]:border-solid has-[input:focus-visible]:border-focus-ring\",\n \"has-[input:focus-visible]:ring-2 has-[input:focus-visible]:ring-focus-ring has-[input:focus-visible]:ring-offset-2 has-[input:focus-visible]:ring-offset-background\",\n !disabled && \"hover:border-primary-300 hover:bg-muted/50 dark:hover:border-primary-600\",\n dragging && \"border-primary-500 bg-brand-wash\",\n disabled && \"cursor-not-allowed opacity-50\",\n className,\n )}\n {...props}\n >\n <input\n {...inputProps}\n id={inputId}\n type=\"file\"\n accept={accept}\n multiple={multiple}\n disabled={disabled}\n aria-labelledby={labelId}\n aria-describedby={describedBy || undefined}\n className={cn(\"sr-only\", inputProps?.className)}\n onChange={(event) => {\n handleFiles(event.target.files);\n // Clear the value so choosing the same file again fires a change.\n event.target.value = \"\";\n }}\n />\n <label\n htmlFor={inputId}\n className={cn(\n \"flex flex-col items-center gap-3 p-8 text-center\",\n disabled ? \"cursor-not-allowed\" : \"cursor-pointer\",\n )}\n >\n <span\n aria-hidden=\"true\"\n className=\"flex size-12 items-center justify-center bg-brand-chip text-brand-icon [&_svg]:size-6\"\n >\n {icon ?? <IconUpload />}\n </span>\n <span className=\"flex flex-col gap-1\">\n <span id={labelId} className=\"text-sm font-medium text-foreground\">\n {label}\n </span>\n {description ? (\n <span id={descriptionId} className=\"text-sm text-muted-foreground\">\n {description}\n </span>\n ) : null}\n </span>\n {hint ? (\n <span id={hintId} className=\"text-xs text-muted-foreground\">\n {hint}\n </span>\n ) : null}\n </label>\n </div>\n );\n}\n\nexport { Dropzone, fileMatchesAccept, formatFileSize };\n",
|
|
27
|
+
"type": "registry:ui",
|
|
28
|
+
"target": "components/ui/dropzone.tsx"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "empty-state",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Empty State",
|
|
6
|
+
"description": "A clear zero-data state with an optional action.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@tabler/icons-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://abackui.com/r/utils.json"
|
|
12
|
+
],
|
|
13
|
+
"categories": [
|
|
14
|
+
"Application"
|
|
15
|
+
],
|
|
16
|
+
"docs": "https://abackui.com/docs/components/empty-state",
|
|
17
|
+
"meta": {
|
|
18
|
+
"status": "stable",
|
|
19
|
+
"usedIn": [
|
|
20
|
+
"aback-tools",
|
|
21
|
+
"aback-launch"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
{
|
|
26
|
+
"path": "components/ui/empty-state.tsx",
|
|
27
|
+
"content": "import * as React from \"react\";\nimport { IconInbox } from \"@tabler/icons-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface EmptyStateProps extends Omit<React.ComponentProps<\"div\">, \"title\"> {\n icon?: React.ReactNode;\n title: React.ReactNode;\n description?: React.ReactNode;\n action?: React.ReactNode;\n}\n\nfunction EmptyState({\n icon,\n title,\n description,\n action,\n className,\n ...props\n}: EmptyStateProps) {\n return (\n <div\n data-slot=\"empty-state\"\n className={cn(\n \"flex flex-col items-center justify-center gap-3 border border-border bg-muted/10 p-12 text-center\",\n className,\n )}\n {...props}\n >\n <div className=\"flex size-12 items-center justify-center text-muted-foreground/40 [&_svg]:size-8\">\n {icon ?? <IconInbox aria-hidden=\"true\" />}\n </div>\n <p className=\"text-base font-semibold text-foreground\">{title}</p>\n {description ? (\n <p className=\"max-w-sm text-sm leading-relaxed text-muted-foreground\">\n {description}\n </p>\n ) : null}\n {action ? <div className=\"mt-2\">{action}</div> : null}\n </div>\n );\n}\n\nexport { EmptyState };\n",
|
|
28
|
+
"type": "registry:ui",
|
|
29
|
+
"target": "components/ui/empty-state.tsx"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "faq",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "FAQ",
|
|
6
|
+
"description": "Questions in an accordion with indexable closed answers and optional FAQPage structured data.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://abackui.com/r/accordion.json",
|
|
9
|
+
"https://abackui.com/r/section-heading.json"
|
|
10
|
+
],
|
|
11
|
+
"categories": [
|
|
12
|
+
"Marketing"
|
|
13
|
+
],
|
|
14
|
+
"docs": "https://abackui.com/docs/blocks/faq",
|
|
15
|
+
"meta": {
|
|
16
|
+
"status": "beta",
|
|
17
|
+
"usedIn": [
|
|
18
|
+
"aback-tools",
|
|
19
|
+
"aback-launch",
|
|
20
|
+
"aback-group"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
{
|
|
25
|
+
"path": "components/blocks/faq.tsx",
|
|
26
|
+
"content": "import { useId, type ReactNode } from \"react\";\n\nimport {\n Accordion,\n AccordionContent,\n AccordionItem,\n AccordionTrigger,\n} from \"@/components/ui/accordion\";\nimport { SectionHeading } from \"@/components/ui/section-heading\";\n\nexport interface FaqItem {\n question: string;\n /** Plain text, so it can be mirrored in structured data. Blank lines start new paragraphs. */\n answer: string;\n}\n\nexport interface FaqBlockProps {\n eyebrow?: ReactNode;\n title?: ReactNode;\n description?: ReactNode;\n items: FaqItem[];\n /** Emit FAQPage JSON-LD for search engines. Only for questions you answer on the page. */\n structuredData?: boolean;\n /** A line after the list, such as a contact link. */\n footer?: ReactNode;\n}\n\nfunction faqJsonLd(items: FaqItem[]) {\n const data = {\n \"@context\": \"https://schema.org\",\n \"@type\": \"FAQPage\",\n mainEntity: items.map((item) => ({\n \"@type\": \"Question\",\n name: item.question,\n acceptedAnswer: { \"@type\": \"Answer\", text: item.answer },\n })),\n };\n // Escaping \"<\" keeps an answer containing \"</script>\" inside the tag.\n return JSON.stringify(data).replace(/</g, \"\\\\u003c\");\n}\n\n/**\n * Frequently asked questions in an accordion. The first answer is open and\n * closed answers stay in the HTML, so they are searchable and indexable.\n */\nexport function FaqBlock({\n eyebrow,\n title = \"Frequently asked questions\",\n description,\n items,\n structuredData = false,\n footer,\n}: FaqBlockProps) {\n const headingId = useId();\n return (\n <section aria-labelledby={headingId} className=\"mx-auto w-full max-w-3xl px-4 py-16 md:px-6 lg:py-20\">\n <SectionHeading eyebrow={eyebrow} title={title} description={description} headingId={headingId} />\n\n <Accordion\n type=\"multiple\"\n defaultValue={items.length ? [\"faq-0\"] : []}\n className=\"mt-8 border-t border-border\"\n >\n {items.map((item, index) => (\n <AccordionItem key={item.question} value={`faq-${index}`}>\n <AccordionTrigger className=\"text-base\">{item.question}</AccordionTrigger>\n <AccordionContent forceMount className=\"flex flex-col gap-3 [[data-state=closed]>&]:hidden\">\n {item.answer.split(/\\n\\s*\\n/).map((paragraph, paragraphIndex) => (\n <p key={paragraphIndex}>{paragraph}</p>\n ))}\n </AccordionContent>\n </AccordionItem>\n ))}\n </Accordion>\n\n {footer ? <div className=\"mt-6 text-sm text-muted-foreground\">{footer}</div> : null}\n\n {structuredData && items.length ? (\n <script type=\"application/ld+json\" dangerouslySetInnerHTML={{ __html: faqJsonLd(items) }} />\n ) : null}\n </section>\n );\n}\n",
|
|
27
|
+
"type": "registry:block",
|
|
28
|
+
"target": "components/blocks/faq.tsx"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "feature-grid",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "Feature grid",
|
|
6
|
+
"description": "A titled grid of features with brand icon chips and optional links, joined by hairlines.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@tabler/icons-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://abackui.com/r/utils.json",
|
|
12
|
+
"https://abackui.com/r/section-heading.json"
|
|
13
|
+
],
|
|
14
|
+
"categories": [
|
|
15
|
+
"Marketing"
|
|
16
|
+
],
|
|
17
|
+
"docs": "https://abackui.com/docs/blocks/feature-grid",
|
|
18
|
+
"meta": {
|
|
19
|
+
"status": "beta",
|
|
20
|
+
"usedIn": [
|
|
21
|
+
"aback-tools",
|
|
22
|
+
"aback-launch"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
{
|
|
27
|
+
"path": "components/blocks/feature-grid.tsx",
|
|
28
|
+
"content": "import type { ReactNode } from \"react\";\nimport { IconArrowRight } from \"@tabler/icons-react\";\n\nimport { SectionHeading } from \"@/components/ui/section-heading\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface Feature {\n /** A 20px Tabler icon. It is decorative. */\n icon: ReactNode;\n title: string;\n description: ReactNode;\n /** Makes the feature a link to its detail page. */\n href?: string;\n}\n\nexport interface FeatureGridBlockProps {\n eyebrow?: ReactNode;\n title: ReactNode;\n description?: ReactNode;\n features: Feature[];\n columns?: 2 | 3 | 4;\n /** Id of the section heading, unique per page. */\n headingId?: string;\n}\n\nconst columnClasses = {\n 2: \"sm:grid-cols-2\",\n 3: \"sm:grid-cols-2 lg:grid-cols-3\",\n 4: \"sm:grid-cols-2 lg:grid-cols-4\",\n} as const;\n\n/**\n * A titled grid of features with brand icon chips. Cells share hairline\n * borders instead of floating cards. A feature with an href becomes one\n * link whose name is its title.\n */\nexport function FeatureGridBlock({\n eyebrow,\n title,\n description,\n features,\n columns = 3,\n headingId = \"features-title\",\n}: FeatureGridBlockProps) {\n return (\n <section aria-labelledby={headingId} className=\"mx-auto w-full max-w-6xl px-4 py-16 md:px-6 lg:py-20\">\n <SectionHeading eyebrow={eyebrow} title={title} description={description} headingId={headingId} />\n\n <ul className={cn(\"mt-10 grid gap-px border border-border bg-border\", columnClasses[columns])}>\n {features.map((feature) => {\n const body = (\n <>\n <span\n aria-hidden=\"true\"\n className=\"flex size-10 items-center justify-center bg-brand-chip text-brand-icon [&_svg]:size-5\"\n >\n {feature.icon}\n </span>\n <h3 className=\"mt-4 flex items-center gap-1.5 text-base font-semibold text-foreground\">\n {feature.title}\n {feature.href ? (\n <IconArrowRight\n aria-hidden=\"true\"\n className=\"size-4 text-muted-foreground transition-transform duration-200 group-hover:translate-x-0.5 motion-reduce:transition-none\"\n />\n ) : null}\n </h3>\n <p className=\"mt-1.5 text-sm leading-relaxed text-muted-foreground\">{feature.description}</p>\n </>\n );\n\n return (\n <li key={feature.title} className=\"bg-card\">\n {feature.href ? (\n <a\n href={feature.href}\n className=\"group block h-full p-6 outline-none transition-colors duration-200 hover:bg-muted/40 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-focus-ring\"\n >\n {body}\n </a>\n ) : (\n <div className=\"h-full p-6\">{body}</div>\n )}\n </li>\n );\n })}\n </ul>\n </section>\n );\n}\n",
|
|
29
|
+
"type": "registry:block",
|
|
30
|
+
"target": "components/blocks/feature-grid.tsx"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "field",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Field",
|
|
6
|
+
"description": "A form field frame that wires labels, help text, and errors.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@tabler/icons-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://abackui.com/r/utils.json"
|
|
12
|
+
],
|
|
13
|
+
"categories": [
|
|
14
|
+
"Forms"
|
|
15
|
+
],
|
|
16
|
+
"docs": "https://abackui.com/docs/components/field",
|
|
17
|
+
"meta": {
|
|
18
|
+
"status": "stable",
|
|
19
|
+
"usedIn": []
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
{
|
|
23
|
+
"path": "components/ui/field.tsx",
|
|
24
|
+
"content": "import * as React from \"react\";\nimport { IconAlertCircle } from \"@tabler/icons-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface FieldProps extends React.ComponentProps<\"div\"> {\n /** The control id this field labels. */\n htmlFor: string;\n label: React.ReactNode;\n description?: React.ReactNode;\n error?: React.ReactNode;\n required?: boolean;\n}\n\n/**\n * Layout and labeling for a single form control. It does not inject props into\n * the control; pass `id`, `aria-describedby`, and `aria-invalid` explicitly so\n * the relationship stays visible in the consuming code.\n */\nfunction Field({\n htmlFor,\n label,\n description,\n error,\n required,\n className,\n children,\n ...props\n}: FieldProps) {\n return (\n <div data-slot=\"field\" className={cn(\"flex flex-col gap-1.5\", className)} {...props}>\n <label\n htmlFor={htmlFor}\n className=\"flex items-center gap-1 text-sm font-medium leading-none text-foreground\"\n >\n {label}\n {required ? (\n <span aria-hidden=\"true\" className=\"text-destructive\">\n *\n </span>\n ) : null}\n </label>\n {children}\n {description ? (\n <p id={`${htmlFor}-description`} className=\"text-xs leading-relaxed text-muted-foreground\">\n {description}\n </p>\n ) : null}\n {error ? (\n <p\n id={`${htmlFor}-error`}\n role=\"alert\"\n className=\"flex items-start gap-1.5 text-xs font-medium leading-relaxed text-danger\"\n >\n <IconAlertCircle aria-hidden=\"true\" className=\"mt-0.5 size-3.5 shrink-0\" />\n {error}\n </p>\n ) : null}\n </div>\n );\n}\n\nexport { Field };\n",
|
|
25
|
+
"type": "registry:ui",
|
|
26
|
+
"target": "components/ui/field.tsx"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "hero",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "Hero",
|
|
6
|
+
"description": "A landing-page opening with announcement, h1, highlight line, actions, proof points, and an aside.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@tabler/icons-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://abackui.com/r/button.json"
|
|
12
|
+
],
|
|
13
|
+
"categories": [
|
|
14
|
+
"Marketing"
|
|
15
|
+
],
|
|
16
|
+
"docs": "https://abackui.com/docs/blocks/hero",
|
|
17
|
+
"meta": {
|
|
18
|
+
"status": "beta",
|
|
19
|
+
"usedIn": [
|
|
20
|
+
"aback-launch",
|
|
21
|
+
"aback-group"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
{
|
|
26
|
+
"path": "components/blocks/hero.tsx",
|
|
27
|
+
"content": "import { useId, type ReactNode } from \"react\";\nimport { IconArrowRight, IconCheck } from \"@tabler/icons-react\";\n\nimport { Button } from \"@/components/ui/button\";\n\nexport interface HeroAction {\n label: string;\n href: string;\n}\n\nexport interface HeroBlockProps {\n /** A short announcement linked from a pill above the title. */\n announcement?: { label: ReactNode; href: string };\n /** The first line of the h1. */\n title: ReactNode;\n /** An optional second line of the h1, set in the brand colour. */\n highlight?: ReactNode;\n description: ReactNode;\n primaryAction: HeroAction;\n secondaryAction?: HeroAction;\n /** A line under the actions, such as \"No credit card required.\" */\n note?: ReactNode;\n /** Short proof points with check marks. Keep them true and specific. */\n highlights?: string[];\n /** Content for the right column on wide screens: a product shot, stats, or a demo. */\n aside?: ReactNode;\n}\n\n/**\n * The opening section of a landing page. It renders the page's h1, so use it\n * once. The layout is two columns from lg and stacks on smaller screens, with\n * the aside after the actions.\n */\nexport function HeroBlock({\n announcement,\n title,\n highlight,\n description,\n primaryAction,\n secondaryAction,\n note,\n highlights = [],\n aside,\n}: HeroBlockProps) {\n const headingId = useId();\n return (\n <section aria-labelledby={headingId} className=\"border-b border-separator\">\n <div className=\"mx-auto grid w-full max-w-6xl gap-12 px-4 py-14 md:px-6 lg:grid-cols-[1.05fr_0.95fr] lg:items-center lg:py-20\">\n <div className=\"flex flex-col items-start\">\n {announcement ? (\n <a\n href={announcement.href}\n className=\"group mb-6 inline-flex items-center gap-2 border border-border bg-card px-2.5 py-1 text-xs font-medium text-muted-foreground outline-none transition-colors duration-200 hover:border-primary-300 hover:text-foreground focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\"\n >\n <span aria-hidden=\"true\" className=\"size-1.5 bg-primary-500\" />\n {announcement.label}\n <IconArrowRight\n aria-hidden=\"true\"\n className=\"size-3 transition-transform duration-200 group-hover:translate-x-0.5 motion-reduce:transition-none\"\n />\n </a>\n ) : null}\n\n <h1\n id={headingId}\n className=\"max-w-xl text-4xl font-bold leading-[1.05] tracking-tight text-foreground text-balance sm:text-5xl\"\n >\n {title}\n {highlight ? <span className=\"block text-link\">{highlight}</span> : null}\n </h1>\n\n <p className=\"mt-5 max-w-xl text-base leading-relaxed text-muted-foreground sm:text-lg\">\n {description}\n </p>\n\n <div className=\"mt-8 flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:flex-wrap\">\n <Button size=\"lg\" variant=\"brand\" asChild>\n <a href={primaryAction.href}>\n {primaryAction.label}\n <IconArrowRight aria-hidden=\"true\" />\n </a>\n </Button>\n {secondaryAction ? (\n <Button size=\"lg\" variant=\"outline\" asChild>\n <a href={secondaryAction.href}>{secondaryAction.label}</a>\n </Button>\n ) : null}\n </div>\n\n {note ? <p className=\"mt-3 text-xs text-muted-foreground\">{note}</p> : null}\n\n {highlights.length ? (\n <ul className=\"mt-6 grid gap-x-6 gap-y-2 text-sm text-muted-foreground sm:grid-cols-2\">\n {highlights.map((item) => (\n <li key={item} className=\"flex items-start gap-2\">\n <IconCheck aria-hidden=\"true\" className=\"mt-0.5 size-4 shrink-0 text-brand-icon\" />\n {item}\n </li>\n ))}\n </ul>\n ) : null}\n </div>\n\n {aside ? <div className=\"min-w-0\">{aside}</div> : null}\n </div>\n </section>\n );\n}\n",
|
|
28
|
+
"type": "registry:block",
|
|
29
|
+
"target": "components/blocks/hero.tsx"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|