aback-ui 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +85 -2
  3. package/dist/args.d.ts +15 -0
  4. package/dist/args.d.ts.map +1 -0
  5. package/dist/args.js +91 -0
  6. package/dist/args.js.map +1 -0
  7. package/dist/cli.d.ts +13 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +106 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/commands/add.d.ts +29 -0
  12. package/dist/commands/add.d.ts.map +1 -0
  13. package/dist/commands/add.js +82 -0
  14. package/dist/commands/add.js.map +1 -0
  15. package/dist/commands/diff.d.ts +13 -0
  16. package/dist/commands/diff.d.ts.map +1 -0
  17. package/dist/commands/diff.js +85 -0
  18. package/dist/commands/diff.js.map +1 -0
  19. package/dist/commands/doctor.d.ts +8 -0
  20. package/dist/commands/doctor.d.ts.map +1 -0
  21. package/dist/commands/doctor.js +45 -0
  22. package/dist/commands/doctor.js.map +1 -0
  23. package/dist/commands/init.d.ts +19 -0
  24. package/dist/commands/init.d.ts.map +1 -0
  25. package/dist/commands/init.js +97 -0
  26. package/dist/commands/init.js.map +1 -0
  27. package/dist/commands/list.d.ts +11 -0
  28. package/dist/commands/list.d.ts.map +1 -0
  29. package/dist/commands/list.js +42 -0
  30. package/dist/commands/list.js.map +1 -0
  31. package/dist/commands/view.d.ts +11 -0
  32. package/dist/commands/view.d.ts.map +1 -0
  33. package/dist/commands/view.js +43 -0
  34. package/dist/commands/view.js.map +1 -0
  35. package/dist/config.d.ts +62 -0
  36. package/dist/config.d.ts.map +1 -0
  37. package/dist/config.js +265 -0
  38. package/dist/config.js.map +1 -0
  39. package/dist/diff.d.ts +30 -0
  40. package/dist/diff.d.ts.map +1 -0
  41. package/dist/diff.js +121 -0
  42. package/dist/diff.js.map +1 -0
  43. package/dist/doctor.d.ts +35 -0
  44. package/dist/doctor.d.ts.map +1 -0
  45. package/dist/doctor.js +203 -0
  46. package/dist/doctor.js.map +1 -0
  47. package/dist/errors.d.ts +6 -0
  48. package/dist/errors.d.ts.map +1 -0
  49. package/dist/errors.js +12 -0
  50. package/dist/errors.js.map +1 -0
  51. package/dist/help.d.ts +3 -0
  52. package/dist/help.d.ts.map +1 -0
  53. package/dist/help.js +48 -0
  54. package/dist/help.js.map +1 -0
  55. package/dist/index.d.ts +3 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +4 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/install.d.ts +53 -0
  60. package/dist/install.d.ts.map +1 -0
  61. package/dist/install.js +119 -0
  62. package/dist/install.js.map +1 -0
  63. package/dist/logger.d.ts +14 -0
  64. package/dist/logger.d.ts.map +1 -0
  65. package/dist/logger.js +47 -0
  66. package/dist/logger.js.map +1 -0
  67. package/dist/paths.d.ts +18 -0
  68. package/dist/paths.d.ts.map +1 -0
  69. package/dist/paths.js +76 -0
  70. package/dist/paths.js.map +1 -0
  71. package/dist/prompt.d.ts +13 -0
  72. package/dist/prompt.d.ts.map +1 -0
  73. package/dist/prompt.js +25 -0
  74. package/dist/prompt.js.map +1 -0
  75. package/dist/registry.d.ts +49 -0
  76. package/dist/registry.d.ts.map +1 -0
  77. package/dist/registry.js +216 -0
  78. package/dist/registry.js.map +1 -0
  79. package/dist/types.d.ts +88 -0
  80. package/dist/types.d.ts.map +1 -0
  81. package/dist/types.js +2 -0
  82. package/dist/types.js.map +1 -0
  83. package/package.json +59 -4
  84. package/registry/accordion.json +32 -0
  85. package/registry/alert-dialog.json +28 -0
  86. package/registry/alert.json +31 -0
  87. package/registry/article-layout.json +38 -0
  88. package/registry/avatar.json +31 -0
  89. package/registry/badge.json +33 -0
  90. package/registry/breadcrumbs.json +32 -0
  91. package/registry/button.json +34 -0
  92. package/registry/calendar.json +29 -0
  93. package/registry/card.json +30 -0
  94. package/registry/checkbox.json +33 -0
  95. package/registry/code-block.json +29 -0
  96. package/registry/collapsible.json +28 -0
  97. package/registry/combobox.json +30 -0
  98. package/registry/command.json +34 -0
  99. package/registry/contact-form.json +37 -0
  100. package/registry/cookie-consent.json +28 -0
  101. package/registry/copy-button.json +33 -0
  102. package/registry/cta-banner.json +32 -0
  103. package/registry/dashboard-shell.json +32 -0
  104. package/registry/date-picker.json +32 -0
  105. package/registry/description-list.json +28 -0
  106. package/registry/dialog.json +33 -0
  107. package/registry/dropdown-menu.json +33 -0
  108. package/registry/dropzone.json +31 -0
  109. package/registry/empty-state.json +32 -0
  110. package/registry/faq.json +31 -0
  111. package/registry/feature-grid.json +33 -0
  112. package/registry/field.json +29 -0
  113. package/registry/hero.json +32 -0
  114. package/registry/hover-card.json +28 -0
  115. package/registry/index.json +1633 -0
  116. package/registry/input-group.json +30 -0
  117. package/registry/input.json +29 -0
  118. package/registry/kbd.json +29 -0
  119. package/registry/label.json +29 -0
  120. package/registry/layout.json +32 -0
  121. package/registry/newsletter.json +35 -0
  122. package/registry/not-found.json +34 -0
  123. package/registry/page-header.json +29 -0
  124. package/registry/pagination.json +25 -0
  125. package/registry/popover.json +28 -0
  126. package/registry/pricing.json +35 -0
  127. package/registry/progress.json +28 -0
  128. package/registry/radio-group.json +28 -0
  129. package/registry/rating.json +32 -0
  130. package/registry/reading-progress.json +29 -0
  131. package/registry/relative-time.json +29 -0
  132. package/registry/scroll-area.json +28 -0
  133. package/registry/section-heading.json +30 -0
  134. package/registry/select.json +32 -0
  135. package/registry/separator.json +32 -0
  136. package/registry/settings-section.json +32 -0
  137. package/registry/share-buttons.json +34 -0
  138. package/registry/sheet.json +34 -0
  139. package/registry/sign-in.json +31 -0
  140. package/registry/site-footer.json +31 -0
  141. package/registry/site-header.json +37 -0
  142. package/registry/skeleton.json +28 -0
  143. package/registry/skip-link.json +30 -0
  144. package/registry/slider.json +28 -0
  145. package/registry/spinner.json +32 -0
  146. package/registry/stat.json +32 -0
  147. package/registry/steps.json +31 -0
  148. package/registry/switch.json +32 -0
  149. package/registry/table-of-contents.json +32 -0
  150. package/registry/table.json +28 -0
  151. package/registry/tabs.json +29 -0
  152. package/registry/textarea.json +29 -0
  153. package/registry/theme-toggle.json +34 -0
  154. package/registry/theme.json +16 -0
  155. package/registry/toast.json +29 -0
  156. package/registry/toggle-group.json +29 -0
  157. package/registry/tooltip.json +29 -0
  158. package/registry/users-table.json +36 -0
  159. package/registry/utils.json +19 -0
@@ -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
+ }
@@ -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
+ }