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.
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 +66 -0
  36. package/dist/config.d.ts.map +1 -0
  37. package/dist/config.js +297 -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,32 @@
1
+ {
2
+ "$schema": "https://abackui.com/schema/registry-item.json",
3
+ "name": "switch",
4
+ "type": "registry:ui",
5
+ "title": "Switch",
6
+ "description": "A binary toggle with switch semantics.",
7
+ "dependencies": [
8
+ "radix-ui"
9
+ ],
10
+ "registryDependencies": [
11
+ "https://abackui.com/r/utils.json"
12
+ ],
13
+ "categories": [
14
+ "Forms"
15
+ ],
16
+ "docs": "https://abackui.com/docs/components/switch",
17
+ "meta": {
18
+ "status": "stable",
19
+ "usedIn": [
20
+ "aback-tools",
21
+ "aback-launch"
22
+ ]
23
+ },
24
+ "files": [
25
+ {
26
+ "path": "components/ui/switch.tsx",
27
+ "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Switch as SwitchPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Switch({\n className,\n ...props\n}: React.ComponentProps<typeof SwitchPrimitive.Root>) {\n return (\n <SwitchPrimitive.Root\n data-slot=\"switch\"\n className={cn(\n \"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center border border-transparent bg-input p-0.5 outline-none transition-colors duration-200\",\n \"focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n \"data-[state=checked]:bg-primary\",\n className,\n )}\n {...props}\n >\n <SwitchPrimitive.Thumb\n data-slot=\"switch-thumb\"\n className={cn(\n \"pointer-events-none block size-4 bg-background shadow-xs transition-transform duration-200\",\n \"data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0\",\n )}\n />\n </SwitchPrimitive.Root>\n );\n}\n\nexport { Switch };\n",
28
+ "type": "registry:ui",
29
+ "target": "components/ui/switch.tsx"
30
+ }
31
+ ]
32
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "$schema": "https://abackui.com/schema/registry-item.json",
3
+ "name": "table-of-contents",
4
+ "type": "registry:ui",
5
+ "title": "Table of Contents",
6
+ "description": "An \"On this page\" list of anchors that tracks the section in view.",
7
+ "dependencies": [
8
+ "@tabler/icons-react"
9
+ ],
10
+ "registryDependencies": [
11
+ "https://abackui.com/r/utils.json"
12
+ ],
13
+ "categories": [
14
+ "Navigation"
15
+ ],
16
+ "docs": "https://abackui.com/docs/components/table-of-contents",
17
+ "meta": {
18
+ "status": "beta",
19
+ "usedIn": [
20
+ "aback-tools",
21
+ "aback-launch"
22
+ ]
23
+ },
24
+ "files": [
25
+ {
26
+ "path": "components/ui/table-of-contents.tsx",
27
+ "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { IconList } from \"@tabler/icons-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface TableOfContentsItem {\n /** The id of the heading on the page, without the hash. */\n id: string;\n title: React.ReactNode;\n /** Heading depth relative to the first level. 1 is a top-level section. */\n depth?: 1 | 2 | 3;\n}\n\nexport interface TableOfContentsProps extends Omit<React.ComponentProps<\"nav\">, \"children\" | \"title\"> {\n items: TableOfContentsItem[];\n /** Visible heading of the list. Also names the navigation landmark. */\n title?: React.ReactNode;\n /**\n * Distance from the top of the viewport, in pixels, where a heading counts\n * as reached. Set it to the height of a sticky header.\n */\n offset?: number;\n /** Disable scroll tracking, for a static list. */\n trackActive?: boolean;\n}\n\nconst depthIndent = {\n 1: \"pl-3\",\n 2: \"pl-6\",\n 3: \"pl-9\",\n} as const;\n\n/**\n * An \"On this page\" list that follows the reader. Links are real anchors, so\n * they work without JavaScript and update the URL hash; the active section is\n * read from the scroll position, once per frame, and marked with `aria-current`.\n */\nfunction TableOfContents({\n items,\n title = \"On this page\",\n offset = 96,\n trackActive = true,\n className,\n \"aria-label\": ariaLabel,\n ...props\n}: TableOfContentsProps) {\n const headingId = React.useId();\n const [active, setActive] = React.useState<string | null>(null);\n const ids = items.map((item) => item.id).join(\"|\");\n\n React.useEffect(() => {\n if (!trackActive) return;\n const targets = ids\n .split(\"|\")\n .map((id) => document.getElementById(id))\n .filter((element): element is HTMLElement => element !== null);\n if (targets.length === 0) return;\n\n // The current section is the last heading that has scrolled up to the\n // offset line. Reading positions on scroll, rather than waiting for\n // intersection changes, keeps it right when a fast scroll jumps past a\n // section. At the very bottom the last section wins, since short final\n // sections can never reach the line.\n let frame = 0;\n let pending = false;\n function update() {\n pending = false;\n const root = document.documentElement;\n const atBottom =\n window.scrollY > 0 && window.innerHeight + window.scrollY >= root.scrollHeight - 2;\n let current = targets[0]!.id;\n if (atBottom) {\n current = targets[targets.length - 1]!.id;\n } else {\n for (const target of targets) {\n if (target.getBoundingClientRect().top > offset + 8) break;\n current = target.id;\n }\n }\n setActive(current);\n }\n function schedule() {\n if (pending) return;\n pending = true;\n frame = window.requestAnimationFrame(update);\n }\n\n schedule();\n window.addEventListener(\"scroll\", schedule, { passive: true });\n window.addEventListener(\"resize\", schedule);\n return () => {\n if (pending) window.cancelAnimationFrame(frame);\n window.removeEventListener(\"scroll\", schedule);\n window.removeEventListener(\"resize\", schedule);\n };\n }, [ids, offset, trackActive]);\n\n if (items.length === 0) return null;\n\n return (\n <nav\n data-slot=\"table-of-contents\"\n aria-label={ariaLabel}\n aria-labelledby={ariaLabel ? undefined : headingId}\n className={cn(\"flex flex-col gap-2\", className)}\n {...props}\n >\n <p\n id={headingId}\n className=\"flex items-center gap-2 border-b border-border pb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground\"\n >\n <IconList aria-hidden=\"true\" className=\"size-4 text-brand-icon\" />\n {title}\n </p>\n <ul className=\"flex flex-col\">\n {items.map((item) => {\n const current = active === item.id;\n return (\n <li key={item.id}>\n <a\n href={`#${item.id}`}\n aria-current={current ? \"location\" : undefined}\n onClick={() => setActive(item.id)}\n className={cn(\n \"block border-l-2 py-1.5 pr-2 text-xs leading-snug outline-none transition-colors duration-150\",\n \"focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-focus-ring\",\n depthIndent[item.depth ?? 1],\n current\n ? \"border-primary-500 bg-brand-wash font-semibold text-link\"\n : \"border-transparent text-muted-foreground hover:border-border hover:text-foreground\",\n )}\n >\n {item.title}\n </a>\n </li>\n );\n })}\n </ul>\n </nav>\n );\n}\n\nexport { TableOfContents };\n",
28
+ "type": "registry:ui",
29
+ "target": "components/ui/table-of-contents.tsx"
30
+ }
31
+ ]
32
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "https://abackui.com/schema/registry-item.json",
3
+ "name": "table",
4
+ "type": "registry:ui",
5
+ "title": "Table",
6
+ "description": "Semantic table primitives for dense data.",
7
+ "registryDependencies": [
8
+ "https://abackui.com/r/utils.json"
9
+ ],
10
+ "categories": [
11
+ "Data Display"
12
+ ],
13
+ "docs": "https://abackui.com/docs/components/table",
14
+ "meta": {
15
+ "status": "stable",
16
+ "usedIn": [
17
+ "aback-launch"
18
+ ]
19
+ },
20
+ "files": [
21
+ {
22
+ "path": "components/ui/table.tsx",
23
+ "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface TableProps extends React.ComponentProps<\"table\"> {\n /** Props for the horizontally scrollable container, which is keyboard focusable. */\n containerProps?: React.ComponentProps<\"div\">;\n}\n\nfunction Table({ className, containerProps, ...props }: TableProps) {\n const {\n className: containerClassName,\n tabIndex,\n ...containerRest\n } = containerProps ?? {};\n\n return (\n <div\n {...containerRest}\n data-slot=\"table-container\"\n tabIndex={tabIndex ?? 0}\n className={cn(\"relative w-full overflow-x-auto\", containerClassName)}\n >\n <table\n data-slot=\"table\"\n className={cn(\"w-full caption-bottom border-collapse text-sm\", className)}\n {...props}\n />\n </div>\n );\n}\n\nfunction TableHeader({ className, ...props }: React.ComponentProps<\"thead\">) {\n return (\n <thead\n data-slot=\"table-header\"\n className={cn(\"bg-muted/40 [&_tr]:border-b [&_tr]:border-border\", className)}\n {...props}\n />\n );\n}\n\nfunction TableBody({ className, ...props }: React.ComponentProps<\"tbody\">) {\n return (\n <tbody\n data-slot=\"table-body\"\n className={cn(\"[&_tr:last-child]:border-0\", className)}\n {...props}\n />\n );\n}\n\nfunction TableFooter({ className, ...props }: React.ComponentProps<\"tfoot\">) {\n return (\n <tfoot\n data-slot=\"table-footer\"\n className={cn(\"border-t border-border bg-muted/40 font-medium\", className)}\n {...props}\n />\n );\n}\n\nfunction TableRow({ className, ...props }: React.ComponentProps<\"tr\">) {\n return (\n <tr\n data-slot=\"table-row\"\n className={cn(\n \"border-b border-border transition-colors duration-150 hover:bg-muted/40 data-[state=selected]:bg-muted\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction TableHead({ className, ...props }: React.ComponentProps<\"th\">) {\n return (\n <th\n data-slot=\"table-head\"\n scope=\"col\"\n className={cn(\n \"h-9 whitespace-nowrap px-3 text-left align-middle text-xs font-medium uppercase tracking-wide text-muted-foreground\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction TableCell({ className, ...props }: React.ComponentProps<\"td\">) {\n return (\n <td\n data-slot=\"table-cell\"\n className={cn(\"px-3 py-3 align-middle text-foreground\", className)}\n {...props}\n />\n );\n}\n\nfunction TableCaption({ className, ...props }: React.ComponentProps<\"caption\">) {\n return (\n <caption\n data-slot=\"table-caption\"\n className={cn(\"mt-4 text-xs text-muted-foreground\", className)}\n {...props}\n />\n );\n}\n\nexport {\n Table,\n TableHeader,\n TableBody,\n TableFooter,\n TableRow,\n TableHead,\n TableCell,\n TableCaption,\n};\n",
24
+ "type": "registry:ui",
25
+ "target": "components/ui/table.tsx"
26
+ }
27
+ ]
28
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "https://abackui.com/schema/registry-item.json",
3
+ "name": "tabs",
4
+ "type": "registry:ui",
5
+ "title": "Tabs",
6
+ "description": "Switch between related panels with arrow-key navigation.",
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/tabs",
17
+ "meta": {
18
+ "status": "stable",
19
+ "usedIn": []
20
+ },
21
+ "files": [
22
+ {
23
+ "path": "components/ui/tabs.tsx",
24
+ "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Tabs as TabsPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Tabs = TabsPrimitive.Root;\n\nfunction TabsList({\n className,\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.List>) {\n return (\n <TabsPrimitive.List\n data-slot=\"tabs-list\"\n className={cn(\n \"inline-flex h-9 items-center justify-center gap-1 border border-border bg-muted/50 p-0.5 text-muted-foreground\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction TabsTrigger({\n className,\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {\n return (\n <TabsPrimitive.Trigger\n data-slot=\"tabs-trigger\"\n className={cn(\n \"inline-flex h-8 items-center justify-center gap-1.5 whitespace-nowrap px-3 text-sm font-medium\",\n \"outline-none transition-colors duration-200 hover:text-foreground\",\n \"focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n \"disabled:pointer-events-none disabled:opacity-50\",\n \"data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-xs\",\n \"[&_svg]:size-4 [&_svg]:shrink-0\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction TabsContent({\n className,\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.Content>) {\n return (\n <TabsPrimitive.Content\n data-slot=\"tabs-content\"\n className={cn(\n \"mt-4 outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent };\n",
25
+ "type": "registry:ui",
26
+ "target": "components/ui/tabs.tsx"
27
+ }
28
+ ]
29
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "https://abackui.com/schema/registry-item.json",
3
+ "name": "textarea",
4
+ "type": "registry:ui",
5
+ "title": "Textarea",
6
+ "description": "A multi-line text control for longer input.",
7
+ "registryDependencies": [
8
+ "https://abackui.com/r/utils.json"
9
+ ],
10
+ "categories": [
11
+ "Forms"
12
+ ],
13
+ "docs": "https://abackui.com/docs/components/textarea",
14
+ "meta": {
15
+ "status": "stable",
16
+ "usedIn": [
17
+ "aback-tools",
18
+ "aback-launch"
19
+ ]
20
+ },
21
+ "files": [
22
+ {
23
+ "path": "components/ui/textarea.tsx",
24
+ "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst textareaClassName = [\n \"flex min-h-20 w-full resize-y border border-input bg-transparent p-3 text-sm text-foreground shadow-xs\",\n \"outline-none transition-[color,background-color,border-color,box-shadow] duration-200\",\n \"placeholder:text-muted-foreground\",\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 \"dark:bg-input/30\",\n].join(\" \");\n\nconst Textarea = React.forwardRef<\n HTMLTextAreaElement,\n React.ComponentProps<\"textarea\">\n>(function Textarea({ className, ...props }, ref) {\n return (\n <textarea\n ref={ref}\n data-slot=\"textarea\"\n className={cn(textareaClassName, className)}\n {...props}\n />\n );\n});\nTextarea.displayName = \"Textarea\";\n\nexport { Textarea, textareaClassName };\n",
25
+ "type": "registry:ui",
26
+ "target": "components/ui/textarea.tsx"
27
+ }
28
+ ]
29
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "$schema": "https://abackui.com/schema/registry-item.json",
3
+ "name": "theme-toggle",
4
+ "type": "registry:ui",
5
+ "title": "Theme Toggle",
6
+ "description": "A light and dark switch, a useTheme hook, and a no-flash theme script.",
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
+ "Application"
16
+ ],
17
+ "docs": "https://abackui.com/docs/components/theme-toggle",
18
+ "meta": {
19
+ "status": "beta",
20
+ "usedIn": [
21
+ "aback-tools",
22
+ "aback-launch",
23
+ "aback-group"
24
+ ]
25
+ },
26
+ "files": [
27
+ {
28
+ "path": "components/ui/theme-toggle.tsx",
29
+ "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { IconMoon, IconSun } from \"@tabler/icons-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Button, type ButtonProps } from \"@/components/ui/button\";\n\nexport type ThemePreference = \"light\" | \"dark\" | \"system\";\nexport type ResolvedTheme = \"light\" | \"dark\";\n\nconst DEFAULT_STORAGE_KEY = \"theme\";\nconst CHANGE_EVENT = \"aback-ui:themechange\";\n\nfunction readPreference(storageKey: string): ThemePreference {\n try {\n const stored = window.localStorage.getItem(storageKey);\n if (stored === \"light\" || stored === \"dark\" || stored === \"system\") return stored;\n } catch {\n // Storage throws in some privacy modes and sandboxed frames.\n }\n return \"system\";\n}\n\nfunction systemTheme(): ResolvedTheme {\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\n}\n\nfunction resolve(preference: ThemePreference): ResolvedTheme {\n return preference === \"system\" ? systemTheme() : preference;\n}\n\nfunction applyTheme(theme: ResolvedTheme) {\n document.documentElement.classList.toggle(\"dark\", theme === \"dark\");\n}\n\nfunction subscribe(storageKey: string, onChange: () => void) {\n const media = window.matchMedia(\"(prefers-color-scheme: dark)\");\n\n function onSystemChange() {\n if (readPreference(storageKey) !== \"system\") return;\n applyTheme(systemTheme());\n onChange();\n }\n\n function onStorage(event: StorageEvent) {\n if (event.key !== storageKey) return;\n applyTheme(resolve(readPreference(storageKey)));\n onChange();\n }\n\n media.addEventListener(\"change\", onSystemChange);\n window.addEventListener(\"storage\", onStorage);\n window.addEventListener(CHANGE_EVENT, onChange);\n return () => {\n media.removeEventListener(\"change\", onSystemChange);\n window.removeEventListener(\"storage\", onStorage);\n window.removeEventListener(CHANGE_EVENT, onChange);\n };\n}\n\nexport interface UseThemeResult {\n /** What the visitor chose, including \"system\". */\n theme: ThemePreference;\n /** What is on screen. \"light\" during the server render. */\n resolvedTheme: ResolvedTheme;\n setTheme: (theme: ThemePreference) => void;\n}\n\n/**\n * Reads and writes the colour theme. The contract is the one the Aback theme\n * CSS expects: a `dark` class on `<html>`, with the choice stored under\n * `storageKey`. Every component using the hook stays in sync, and so do\n * other tabs.\n */\nfunction useTheme(storageKey: string = DEFAULT_STORAGE_KEY): UseThemeResult {\n const subscribeToTheme = React.useCallback(\n (onChange: () => void) => subscribe(storageKey, onChange),\n [storageKey],\n );\n // A string snapshot keeps the value primitive, so React can compare it.\n const snapshot = React.useSyncExternalStore(\n subscribeToTheme,\n () => {\n const preference = readPreference(storageKey);\n return `${preference}:${document.documentElement.classList.contains(\"dark\") ? \"dark\" : \"light\"}`;\n },\n () => \"system:light\",\n );\n const [theme, resolvedTheme] = snapshot.split(\":\") as [ThemePreference, ResolvedTheme];\n\n const setTheme = React.useCallback(\n (next: ThemePreference) => {\n try {\n window.localStorage.setItem(storageKey, next);\n } catch {\n // Not persisted, but the theme still changes for this page view.\n }\n applyTheme(resolve(next));\n window.dispatchEvent(new Event(CHANGE_EVENT));\n },\n [storageKey],\n );\n\n return { theme, resolvedTheme, setTheme };\n}\n\nexport interface ThemeToggleProps\n extends Omit<ButtonProps, \"onClick\" | \"children\" | \"asChild\"> {\n storageKey?: string;\n /** Accessible name while the light theme is showing. */\n toDarkLabel?: string;\n /** Accessible name while the dark theme is showing. */\n toLightLabel?: string;\n}\n\n/**\n * A light and dark switch. The icon follows the `dark` class through CSS, so\n * it is right before hydration; the accessible name follows the state.\n */\nfunction ThemeToggle({\n storageKey,\n toDarkLabel = \"Switch to dark theme\",\n toLightLabel = \"Switch to light theme\",\n variant = \"ghost\",\n size = \"icon\",\n className,\n ...props\n}: ThemeToggleProps) {\n const { resolvedTheme, setTheme } = useTheme(storageKey);\n const label = resolvedTheme === \"dark\" ? toLightLabel : toDarkLabel;\n\n return (\n <Button\n type=\"button\"\n data-slot=\"theme-toggle\"\n variant={variant}\n size={size}\n aria-label={label}\n title={label}\n onClick={() => setTheme(resolvedTheme === \"dark\" ? \"light\" : \"dark\")}\n className={cn(className)}\n {...props}\n >\n <IconMoon aria-hidden=\"true\" className=\"dark:hidden\" />\n <IconSun aria-hidden=\"true\" className=\"hidden dark:block\" />\n </Button>\n );\n}\n\nexport interface ThemeScriptProps {\n storageKey?: string;\n /** Content Security Policy nonce for the inline script. */\n nonce?: string;\n}\n\n/**\n * Applies the stored theme before the first paint, so a dark-mode visitor\n * never sees a white flash. Render it in `<head>`, or first in `<body>`, and\n * add `suppressHydrationWarning` to `<html>` because the class changes early.\n */\nfunction ThemeScript({ storageKey = DEFAULT_STORAGE_KEY, nonce }: ThemeScriptProps) {\n // JSON.stringify quotes the key, and the escape keeps it inside the script.\n const key = JSON.stringify(storageKey).replace(/</g, \"\\\\u003c\");\n const code = `(function(){try{var t=localStorage.getItem(${key});var d=t===\"dark\"||((!t||t===\"system\")&&window.matchMedia(\"(prefers-color-scheme: dark)\").matches);document.documentElement.classList.toggle(\"dark\",d);}catch(e){}})();`;\n return <script nonce={nonce} dangerouslySetInnerHTML={{ __html: code }} />;\n}\n\nexport { ThemeScript, ThemeToggle, useTheme };\n",
30
+ "type": "registry:ui",
31
+ "target": "components/ui/theme-toggle.tsx"
32
+ }
33
+ ]
34
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "https://abackui.com/schema/registry-item.json",
3
+ "name": "theme",
4
+ "type": "registry:file",
5
+ "title": "Aback theme",
6
+ "description": "Semantic tokens, the Aback Blue ramp, square geometry, and light and dark values.",
7
+ "docs": "https://abackui.com/docs/theming",
8
+ "files": [
9
+ {
10
+ "path": "~/styles/aback.css",
11
+ "content": "@custom-variant dark (&:is(.dark *));\n\n/*\n * Aback UI theme\n * --------------\n * Import after Tailwind:\n *\n * @import \"tailwindcss\";\n * @import \"./styles/aback.css\";\n *\n * Light is the base theme. Dark is the `dark` class on <html>.\n * Every value here is a semantic role, not a one-off swatch. See DESIGN.md.\n */\n\n@font-face {\n font-family: \"GeistSansFallback\";\n src: local(\"Arial\"), local(\"Helvetica Neue\"), local(\"Helvetica\"), local(\"sans-serif\");\n ascent-override: 95%;\n descent-override: 22%;\n line-gap-override: 0%;\n size-adjust: 105%;\n}\n\n@theme inline {\n --font-sans: var(--font-geist-sans), \"GeistSansFallback\", ui-sans-serif, system-ui, sans-serif;\n --font-mono: var(--font-geist-mono), ui-monospace, SFMono-Regular, Menlo, monospace;\n\n /* Surface and text */\n --color-background: var(--background);\n --color-foreground: var(--foreground);\n --color-card: var(--card);\n --color-card-foreground: var(--card-foreground);\n --color-popover: var(--popover);\n --color-popover-foreground: var(--popover-foreground);\n\n /* Action: neutral ink, never brand blue by default */\n --color-primary: var(--primary);\n --color-primary-foreground: var(--primary-foreground);\n --color-secondary: var(--secondary);\n --color-secondary-foreground: var(--secondary-foreground);\n --color-muted: var(--muted);\n --color-muted-foreground: var(--muted-foreground);\n --color-accent: var(--accent);\n --color-accent-foreground: var(--accent-foreground);\n\n /* Structure */\n --color-border: var(--border);\n --color-separator: var(--separator);\n --color-input: var(--input);\n --color-ring: var(--ring);\n --color-focus-ring: var(--focus-ring);\n\n /* Brand roles: read the theme variable so light and dark stay correct */\n --color-link: var(--link);\n --color-link-hover: var(--link-hover);\n --color-brand-icon: var(--brand-icon);\n --color-brand-chip: var(--brand-chip);\n --color-brand-wash: var(--brand-wash);\n --color-brand-fill: var(--brand-fill);\n --color-on-brand-fill: var(--on-brand-fill);\n\n /* Status */\n --color-destructive: var(--destructive);\n --color-destructive-foreground: var(--destructive-foreground);\n --color-success: var(--success);\n --color-success-soft: var(--success-soft);\n --color-success-strong: var(--success-strong);\n --color-warning: var(--warning);\n --color-warning-soft: var(--warning-soft);\n --color-warning-strong: var(--warning-strong);\n --color-danger: var(--danger);\n --color-danger-soft: var(--danger-soft);\n --color-danger-strong: var(--danger-strong);\n --color-info: var(--info);\n --color-info-soft: var(--info-soft);\n\n /* Aback Blue ramp. Static by design: 500 is the brand anchor in both themes. */\n --color-primary-50: #e6f6ff;\n --color-primary-100: #ccedff;\n --color-primary-200: #99dbff;\n --color-primary-300: #66c9ff;\n --color-primary-400: #33b7ff;\n --color-primary-500: #009bf9;\n --color-primary-600: #007cc7;\n --color-primary-700: #005d95;\n --color-primary-800: #003e63;\n --color-primary-900: #001f32;\n --color-primary-950: #000f19;\n\n /* Square geometry: every radius token resolves to zero. */\n --radius: 0px;\n --radius-xs: 0px;\n --radius-sm: 0px;\n --radius-md: 0px;\n --radius-lg: 0px;\n --radius-xl: 0px;\n --radius-2xl: 0px;\n --radius-3xl: 0px;\n --radius-4xl: 0px;\n}\n\n:root {\n color-scheme: light;\n\n --background: #ffffff;\n --foreground: #0a0a0a;\n --card: #ffffff;\n --card-foreground: #0a0a0a;\n --popover: #ffffff;\n --popover-foreground: #0a0a0a;\n\n --primary: #171717;\n --primary-foreground: #fafafa;\n --secondary: #f5f5f5;\n --secondary-foreground: #171717;\n --muted: #f5f5f5;\n --muted-foreground: #737373;\n --accent: #f5f5f5;\n --accent-foreground: #171717;\n\n --destructive: #e7000b;\n --destructive-foreground: #ffffff;\n --border: #e5e5e5;\n --separator: #d7d7d7;\n --input: #e5e5e5;\n --ring: #a1a1a1;\n --focus-ring: #007cc7;\n\n --link: #005d95;\n --link-hover: #003e63;\n --brand-icon: #007cc7;\n --brand-chip: #ccedff;\n --brand-wash: #e6f6ff;\n --brand-fill: #005d95;\n --on-brand-fill: #ffffff;\n\n --success: #008236;\n --success-soft: #dcfce7;\n --success-strong: #016630;\n --warning: #bb4d00;\n --warning-soft: #fffbeb;\n --warning-strong: #973c00;\n --danger: #c10007;\n --danger-soft: #fef2f2;\n --danger-strong: #9f0712;\n --info: #005d95;\n --info-soft: #e6f6ff;\n\n --overlay: rgb(0 0 0 / 0.4);\n --scrollbar-thumb: #b4b4b4;\n --scrollbar-thumb-hover: #949494;\n --scrollbar-track: #f1f1f1;\n\n --radius: 0px;\n}\n\n.dark {\n color-scheme: dark;\n\n --background: #0a0a0a;\n --foreground: #fafafa;\n --card: #171717;\n --card-foreground: #fafafa;\n --popover: #171717;\n --popover-foreground: #fafafa;\n\n --primary: #e5e5e5;\n --primary-foreground: #171717;\n --secondary: #262626;\n --secondary-foreground: #fafafa;\n --muted: #262626;\n --muted-foreground: #a1a1a1;\n --accent: #262626;\n --accent-foreground: #fafafa;\n\n --destructive: #ff6467;\n --destructive-foreground: #170000;\n --border: rgb(255 255 255 / 0.1);\n --separator: rgb(255 255 255 / 0.15);\n --input: rgb(255 255 255 / 0.15);\n --ring: #737373;\n --focus-ring: #33b7ff;\n\n --link: #33b7ff;\n --link-hover: #66c9ff;\n --brand-icon: #33b7ff;\n --brand-chip: #001f32;\n --brand-wash: rgb(0 31 50 / 0.3);\n --brand-fill: #33b7ff;\n --on-brand-fill: #000f19;\n\n --success: #05df72;\n --success-soft: #052e16;\n --success-strong: #7bf1a8;\n --warning: #fbbf24;\n --warning-soft: #2b1a00;\n --warning-strong: #fcd34d;\n --danger: #ff6467;\n --danger-soft: #2b0a0a;\n --danger-strong: #ffa2a2;\n --info: #66c9ff;\n --info-soft: rgb(0 31 50 / 0.3);\n\n --overlay: rgb(0 0 0 / 0.6);\n --scrollbar-thumb: #3d3d3d;\n --scrollbar-thumb-hover: #525252;\n --scrollbar-track: #1a1a1a;\n\n --radius: 0px;\n}\n\n@layer base {\n * {\n border-color: var(--border);\n }\n\n html {\n -webkit-text-size-adjust: 100%;\n scrollbar-width: thin;\n scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);\n }\n\n body {\n background-color: var(--background);\n color: var(--foreground);\n font-family: var(--font-sans);\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n }\n\n ::selection {\n background-color: var(--brand-chip);\n color: var(--foreground);\n }\n\n :-webkit-any(button, a, input, textarea, select, summary, [tabindex]):focus-visible {\n outline: none;\n }\n\n ::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n }\n\n ::-webkit-scrollbar-track {\n background: var(--scrollbar-track);\n }\n\n ::-webkit-scrollbar-thumb {\n background: var(--scrollbar-thumb);\n border-radius: 4px;\n }\n\n ::-webkit-scrollbar-thumb:hover {\n background: var(--scrollbar-thumb-hover);\n }\n}\n\n@keyframes accordion-down {\n from {\n height: 0;\n }\n to {\n height: var(--radix-accordion-content-height);\n }\n}\n\n@keyframes accordion-up {\n from {\n height: var(--radix-accordion-content-height);\n }\n to {\n height: 0;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n *,\n *::before,\n *::after {\n animation-duration: 0.01ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0.01ms !important;\n scroll-behavior: auto !important;\n }\n}\n",
12
+ "type": "registry:file",
13
+ "target": "~/styles/aback.css"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "https://abackui.com/schema/registry-item.json",
3
+ "name": "toast",
4
+ "type": "registry:ui",
5
+ "title": "Toast",
6
+ "description": "Transient status messages with actions, themed by the Aback tokens.",
7
+ "dependencies": [
8
+ "@tabler/icons-react",
9
+ "sonner"
10
+ ],
11
+ "registryDependencies": [
12
+ "https://abackui.com/r/utils.json"
13
+ ],
14
+ "categories": [
15
+ "Feedback"
16
+ ],
17
+ "docs": "https://abackui.com/docs/components/toast",
18
+ "meta": {
19
+ "status": "beta"
20
+ },
21
+ "files": [
22
+ {
23
+ "path": "components/ui/toaster.tsx",
24
+ "content": "\"use client\";\n\nimport {\n IconAlertTriangle,\n IconCircleCheck,\n IconCircleX,\n IconInfoCircle,\n IconX,\n} from \"@tabler/icons-react\";\nimport {\n Toaster as SonnerToaster,\n toast,\n type ToasterProps as SonnerToasterProps,\n} from \"sonner\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * The toast surface.\n *\n * `Toaster` is the Aback presentation of sonner: the same toast API the rest of\n * the ecosystem uses, drawn with the design tokens instead of sonner's own\n * palette. Mount it once near the root of the app, then call `toast()` from\n * anywhere — including event handlers and non-React modules.\n *\n * The status colours are read from the token layer, so a toast, an `Alert`, and\n * a `Badge` in the same viewport are the same green or red. The normal toast is\n * a popover surface, not a signal. The shape is square, like every other Aback\n * surface.\n *\n * Toasts are supplementary: a message that must be seen belongs inline, next to\n * the thing it is about. Use a toast when the failure has no natural home on the\n * page (an optimistic background mutation) or to confirm an action whose result\n * is no longer visible.\n */\nexport interface ToasterProps\n extends Omit<SonnerToasterProps, \"className\" | \"style\" | \"theme\"> {\n /** Force a theme. Defaults to `system`; pass the app's resolved theme when it is class-based. */\n theme?: \"light\" | \"dark\" | \"system\";\n className?: string;\n}\n\nconst iconProps = { size: 16, stroke: 2, \"aria-hidden\": true } as const;\n\nconst tokenVariables = {\n // Neutral toast: a popover surface, so it follows the theme.\n \"--normal-bg\": \"var(--popover)\",\n \"--normal-bg-hover\": \"var(--muted)\",\n \"--normal-border\": \"var(--border)\",\n \"--normal-border-hover\": \"var(--separator)\",\n \"--normal-text\": \"var(--popover-foreground)\",\n\n // Status toasts: the same soft/strong pairs the Alert uses.\n \"--success-bg\": \"var(--success-soft)\",\n \"--success-border\": \"color-mix(in oklab, var(--success) 30%, transparent)\",\n \"--success-text\": \"var(--success-strong)\",\n \"--error-bg\": \"var(--danger-soft)\",\n \"--error-border\": \"color-mix(in oklab, var(--danger) 30%, transparent)\",\n \"--error-text\": \"var(--danger-strong)\",\n \"--warning-bg\": \"var(--warning-soft)\",\n \"--warning-border\": \"color-mix(in oklab, var(--warning) 30%, transparent)\",\n \"--warning-text\": \"var(--warning-strong)\",\n \"--info-bg\": \"var(--info-soft)\",\n \"--info-border\": \"color-mix(in oklab, var(--info) 30%, transparent)\",\n \"--info-text\": \"var(--info)\",\n\n // Square everywhere, like the rest of the system.\n \"--border-radius\": \"0px\",\n \"--width\": \"22.5rem\",\n} as React.CSSProperties;\n\nfunction Toaster({\n theme = \"system\",\n position = \"top-right\",\n closeButton = true,\n richColors = true,\n className,\n toastOptions,\n ...props\n}: ToasterProps) {\n return (\n <SonnerToaster\n theme={theme}\n position={position}\n closeButton={closeButton}\n richColors={richColors}\n className={cn(\"font-sans\", className)}\n style={{ ...tokenVariables, fontFamily: \"var(--font-sans)\" }}\n icons={{\n success: <IconCircleCheck {...iconProps} />,\n error: <IconCircleX {...iconProps} />,\n warning: <IconAlertTriangle {...iconProps} />,\n info: <IconInfoCircle {...iconProps} />,\n close: <IconX size={13} stroke={2} aria-hidden=\"true\" />,\n }}\n toastOptions={{\n ...toastOptions,\n classNames: {\n toast: \"!rounded-none !p-3.5 !gap-2.5\",\n title: \"!text-sm !font-semibold\",\n description: \"!text-xs !leading-relaxed\",\n actionButton:\n \"!rounded-none !bg-primary !text-primary-foreground !text-xs !font-semibold\",\n cancelButton:\n \"!rounded-none !bg-muted !text-foreground !text-xs !font-semibold\",\n closeButton:\n \"!rounded-none !border-border !bg-popover !text-muted-foreground hover:!text-foreground\",\n ...toastOptions?.classNames,\n },\n }}\n {...props}\n />\n );\n}\n\nexport { Toaster, toast };\n",
25
+ "type": "registry:ui",
26
+ "target": "components/ui/toaster.tsx"
27
+ }
28
+ ]
29
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "https://abackui.com/schema/registry-item.json",
3
+ "name": "toggle-group",
4
+ "type": "registry:ui",
5
+ "title": "Toggle Group",
6
+ "description": "A set of related toggles with shared styling and keyboard behavior.",
7
+ "dependencies": [
8
+ "class-variance-authority",
9
+ "radix-ui"
10
+ ],
11
+ "registryDependencies": [
12
+ "https://abackui.com/r/utils.json"
13
+ ],
14
+ "categories": [
15
+ "Forms"
16
+ ],
17
+ "docs": "https://abackui.com/docs/components/toggle-group",
18
+ "meta": {
19
+ "status": "beta"
20
+ },
21
+ "files": [
22
+ {
23
+ "path": "components/ui/toggle-group.tsx",
24
+ "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ToggleGroup as ToggleGroupPrimitive } from \"radix-ui\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst toggleGroupItemVariants = cva(\n [\n \"inline-flex shrink-0 items-center justify-center gap-1.5 text-sm font-medium whitespace-nowrap\",\n \"outline-none transition-colors duration-150\",\n \"focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-inset\",\n \"disabled:pointer-events-none disabled:opacity-50\",\n \"data-[state=on]:bg-primary data-[state=on]:text-primary-foreground\",\n ],\n {\n variants: {\n variant: {\n default: \"bg-transparent text-foreground hover:bg-muted\",\n outline:\n \"border border-input bg-background text-foreground hover:bg-muted data-[state=on]:border-primary\",\n },\n size: {\n sm: \"h-8 min-w-8 px-2.5 text-xs [&_svg:not([class*='size-'])]:size-3.5\",\n default: \"h-9 min-w-9 px-3 [&_svg:not([class*='size-'])]:size-4\",\n lg: \"h-10 min-w-10 px-4 [&_svg:not([class*='size-'])]:size-4\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n },\n);\n\nexport type ToggleGroupProps = React.ComponentProps<typeof ToggleGroupPrimitive.Root> &\n VariantProps<typeof toggleGroupItemVariants>;\n\n// Root is a single/multiple discriminated union; the wrapper keeps the same\n// contract and forwards `type` untouched.\nconst ToggleGroupRoot = ToggleGroupPrimitive.Root as React.ComponentType<ToggleGroupProps>;\n\nfunction ToggleGroup({\n className,\n variant = \"default\",\n size = \"default\",\n ...props\n}: ToggleGroupProps) {\n return (\n <ToggleGroupRoot\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n className={cn(\n \"inline-flex w-fit divide-x divide-border border border-border bg-background\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction ToggleGroupItem({\n className,\n variant = \"default\",\n size = \"default\",\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &\n VariantProps<typeof toggleGroupItemVariants>) {\n return (\n <ToggleGroupPrimitive.Item\n data-slot=\"toggle-group-item\"\n className={cn(toggleGroupItemVariants({ variant, size }), className)}\n {...props}\n />\n );\n}\n\nexport { ToggleGroup, ToggleGroupItem, toggleGroupItemVariants };\n",
25
+ "type": "registry:ui",
26
+ "target": "components/ui/toggle-group.tsx"
27
+ }
28
+ ]
29
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "https://abackui.com/schema/registry-item.json",
3
+ "name": "tooltip",
4
+ "type": "registry:ui",
5
+ "title": "Tooltip",
6
+ "description": "A short label for icon-only or ambiguous controls.",
7
+ "dependencies": [
8
+ "radix-ui"
9
+ ],
10
+ "registryDependencies": [
11
+ "https://abackui.com/r/utils.json"
12
+ ],
13
+ "categories": [
14
+ "Overlays"
15
+ ],
16
+ "docs": "https://abackui.com/docs/components/tooltip",
17
+ "meta": {
18
+ "status": "stable",
19
+ "usedIn": []
20
+ },
21
+ "files": [
22
+ {
23
+ "path": "components/ui/tooltip.tsx",
24
+ "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Tooltip as TooltipPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst TooltipProvider = TooltipPrimitive.Provider;\nconst TooltipTrigger = TooltipPrimitive.Trigger;\n\nfunction Tooltip({\n delayDuration = 200,\n ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Root> & {\n delayDuration?: number;\n}) {\n return (\n <TooltipPrimitive.Provider delayDuration={delayDuration}>\n <TooltipPrimitive.Root data-slot=\"tooltip\" {...props} />\n </TooltipPrimitive.Provider>\n );\n}\n\nfunction TooltipContent({\n className,\n sideOffset = 6,\n ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Content>) {\n return (\n <TooltipPrimitive.Portal>\n <TooltipPrimitive.Content\n data-slot=\"tooltip-content\"\n sideOffset={sideOffset}\n className={cn(\n \"z-[70] max-w-xs border border-border bg-popover px-2.5 py-1.5\",\n \"text-xs leading-relaxed text-popover-foreground shadow-md\",\n \"animate-in fade-in-0 zoom-in-95 duration-150\",\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 </TooltipPrimitive.Portal>\n );\n}\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };\n",
25
+ "type": "registry:ui",
26
+ "target": "components/ui/tooltip.tsx"
27
+ }
28
+ ]
29
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "$schema": "https://abackui.com/schema/registry-item.json",
3
+ "name": "users-table",
4
+ "type": "registry:block",
5
+ "title": "Users table",
6
+ "description": "A searchable member directory with role filters, sorting, row actions, empty state, and pagination.",
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/badge.json",
14
+ "https://abackui.com/r/button.json",
15
+ "https://abackui.com/r/dropdown-menu.json",
16
+ "https://abackui.com/r/empty-state.json",
17
+ "https://abackui.com/r/input.json",
18
+ "https://abackui.com/r/pagination.json",
19
+ "https://abackui.com/r/table.json"
20
+ ],
21
+ "categories": [
22
+ "Data Display"
23
+ ],
24
+ "docs": "https://abackui.com/docs/blocks/users-table",
25
+ "meta": {
26
+ "status": "beta"
27
+ },
28
+ "files": [
29
+ {
30
+ "path": "components/blocks/users-table.tsx",
31
+ "content": "\"use client\";\n\nimport { useMemo, useState } from \"react\";\nimport {\n IconArrowsSort,\n IconChevronDown,\n IconDots,\n IconSearch,\n IconUsers,\n} from \"@tabler/icons-react\";\n\nimport { Avatar, AvatarFallback } from \"@/components/ui/avatar\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { EmptyState } from \"@/components/ui/empty-state\";\nimport { Input } from \"@/components/ui/input\";\nimport { Pagination } from \"@/components/ui/pagination\";\nimport {\n Table,\n TableBody,\n TableCaption,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"@/components/ui/table\";\n\nexport type MemberRole = \"Admin\" | \"Editor\" | \"Viewer\";\nexport type MemberStatus = \"active\" | \"invited\" | \"suspended\";\n\nexport interface WorkspaceMember {\n id: string;\n name: string;\n email: string;\n role: MemberRole;\n status: MemberStatus;\n lastActive: string;\n}\n\nexport interface UsersTableBlockProps {\n members?: WorkspaceMember[];\n /** Renders an invite action in the toolbar when provided. */\n onInvite?: () => void;\n /** Called when a row's manage action is chosen. */\n onManage?: (member: WorkspaceMember) => void;\n}\n\nconst DEMO_MEMBERS: WorkspaceMember[] = [\n { id: \"1\", name: \"Aditi Rao\", email: \"aditi@aback.com\", role: \"Admin\", status: \"active\", lastActive: \"2 minutes ago\" },\n { id: \"2\", name: \"Bilal Khan\", email: \"bilal@aback.com\", role: \"Editor\", status: \"active\", lastActive: \"1 hour ago\" },\n { id: \"3\", name: \"Chen Wei\", email: \"chen@aback.com\", role: \"Viewer\", status: \"invited\", lastActive: \"Invite sent\" },\n { id: \"4\", name: \"Dara Okafor\", email: \"dara@aback.com\", role: \"Editor\", status: \"active\", lastActive: \"Yesterday\" },\n { id: \"5\", name: \"Elena Rossi\", email: \"elena@aback.com\", role: \"Viewer\", status: \"suspended\", lastActive: \"3 weeks ago\" },\n { id: \"6\", name: \"Farid Haddad\", email: \"farid@aback.com\", role: \"Editor\", status: \"active\", lastActive: \"3 days ago\" },\n { id: \"7\", name: \"Grace Lee\", email: \"grace@aback.com\", role: \"Admin\", status: \"active\", lastActive: \"5 minutes ago\" },\n { id: \"8\", name: \"Hassan Ali\", email: \"hassan@aback.com\", role: \"Viewer\", status: \"invited\", lastActive: \"Invite sent\" },\n { id: \"9\", name: \"Imani Brooks\", email: \"imani@aback.com\", role: \"Viewer\", status: \"active\", lastActive: \"1 week ago\" },\n { id: \"10\", name: \"Jonas Weber\", email: \"jonas@aback.com\", role: \"Editor\", status: \"active\", lastActive: \"4 hours ago\" },\n { id: \"11\", name: \"Keiko Tanaka\", email: \"keiko@aback.com\", role: \"Viewer\", status: \"active\", lastActive: \"2 days ago\" },\n];\n\nconst PAGE_SIZE = 5;\nconst ROLES: Array<\"all\" | MemberRole> = [\"all\", \"Admin\", \"Editor\", \"Viewer\"];\n\nconst statusVariant = {\n active: \"success\",\n invited: \"warning\",\n suspended: \"outline\",\n} as const;\n\nfunction initials(name: string) {\n return name\n .split(\" \")\n .map((part) => part[0])\n .filter(Boolean)\n .slice(0, 2)\n .join(\"\");\n}\n\nexport function UsersTableBlock({\n members = DEMO_MEMBERS,\n onInvite,\n onManage,\n}: UsersTableBlockProps) {\n const [query, setQuery] = useState(\"\");\n const [role, setRole] = useState<\"all\" | MemberRole>(\"all\");\n const [sort, setSort] = useState<\"asc\" | \"desc\">(\"asc\");\n const [page, setPage] = useState(1);\n const [copied, setCopied] = useState<string | null>(null);\n\n const filtered = useMemo(() => {\n const term = query.trim().toLowerCase();\n return members\n .filter((member) => {\n const matchesQuery =\n !term ||\n `${member.name} ${member.email}`.toLowerCase().includes(term);\n const matchesRole = role === \"all\" || member.role === role;\n return matchesQuery && matchesRole;\n })\n .sort((a, b) =>\n sort === \"asc\" ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name),\n );\n }, [members, query, role, sort]);\n\n const pageCount = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));\n const current = Math.min(page, pageCount);\n const visible = filtered.slice((current - 1) * PAGE_SIZE, current * PAGE_SIZE);\n\n function resetFilters() {\n setQuery(\"\");\n setRole(\"all\");\n setPage(1);\n }\n\n return (\n <section aria-labelledby=\"members-title\" className=\"flex flex-col gap-4\">\n <div className=\"flex flex-wrap items-end justify-between gap-3\">\n <div>\n <h2 id=\"members-title\" className=\"text-lg font-semibold tracking-tight text-foreground\">\n Members\n </h2>\n <p className=\"mt-0.5 text-sm text-muted-foreground\">\n {members.length} people have access to this workspace.\n </p>\n </div>\n {onInvite ? (\n <Button onClick={onInvite}>\n <IconUsers aria-hidden=\"true\" />\n Invite member\n </Button>\n ) : null}\n </div>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <div className=\"relative min-w-0 flex-1 sm:max-w-xs\">\n <IconSearch\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground\"\n />\n <Input\n aria-label=\"Search members\"\n placeholder=\"Search name or email\"\n value={query}\n onChange={(event) => {\n setQuery(event.target.value);\n setPage(1);\n }}\n className=\"pl-8\"\n />\n </div>\n\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"outline\" size=\"sm\">\n Role: {role === \"all\" ? \"All\" : role}\n <IconChevronDown aria-hidden=\"true\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\">\n {ROLES.map((option) => (\n <DropdownMenuItem\n key={option}\n onSelect={() => {\n setRole(option);\n setPage(1);\n }}\n >\n {option === \"all\" ? \"All roles\" : option}\n </DropdownMenuItem>\n ))}\n </DropdownMenuContent>\n </DropdownMenu>\n\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setSort((currentSort) => (currentSort === \"asc\" ? \"desc\" : \"asc\"))}\n aria-label={`Sort by name, ${sort === \"asc\" ? \"ascending\" : \"descending\"}`}\n >\n <IconArrowsSort aria-hidden=\"true\" />\n Name {sort === \"asc\" ? \"A–Z\" : \"Z–A\"}\n </Button>\n </div>\n\n <p role=\"status\" className=\"sr-only\">\n {copied ? `Copied ${copied} to the clipboard.` : \"\"}\n </p>\n\n {filtered.length === 0 ? (\n <EmptyState\n icon={<IconSearch aria-hidden=\"true\" />}\n title=\"No members match\"\n description=\"Try a different name, email, or role.\"\n action={\n <Button variant=\"outline\" size=\"sm\" onClick={resetFilters}>\n Clear filters\n </Button>\n }\n />\n ) : (\n <>\n <Table containerProps={{ \"aria-label\": \"Workspace members\" }}>\n <TableCaption className=\"sr-only\">\n Workspace members, their role, status, and last activity.\n </TableCaption>\n <TableHeader>\n <TableRow>\n <TableHead scope=\"col\">Member</TableHead>\n <TableHead scope=\"col\">Role</TableHead>\n <TableHead scope=\"col\">Status</TableHead>\n <TableHead scope=\"col\">Last active</TableHead>\n <TableHead scope=\"col\" className=\"w-10\">\n <span className=\"sr-only\">Actions</span>\n </TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {visible.map((member) => (\n <TableRow key={member.id}>\n <TableCell>\n <div className=\"flex items-center gap-3\">\n <Avatar size=\"sm\">\n <AvatarFallback>{initials(member.name)}</AvatarFallback>\n </Avatar>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-foreground\">\n {member.name}\n </p>\n <p className=\"truncate text-xs text-muted-foreground\">{member.email}</p>\n </div>\n </div>\n </TableCell>\n <TableCell className=\"text-sm text-muted-foreground\">{member.role}</TableCell>\n <TableCell>\n <Badge variant={statusVariant[member.status]}>{member.status}</Badge>\n </TableCell>\n <TableCell className=\"whitespace-nowrap text-xs text-muted-foreground\">\n {member.lastActive}\n </TableCell>\n <TableCell>\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"ghost\" size=\"icon-sm\" aria-label={`Actions for ${member.name}`}>\n <IconDots aria-hidden=\"true\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem\n onSelect={() => {\n void navigator.clipboard?.writeText(member.email);\n setCopied(member.email);\n }}\n >\n Copy email\n </DropdownMenuItem>\n {onManage ? (\n <>\n <DropdownMenuSeparator />\n <DropdownMenuItem onSelect={() => onManage(member)}>\n Manage member\n </DropdownMenuItem>\n </>\n ) : null}\n </DropdownMenuContent>\n </DropdownMenu>\n </TableCell>\n </TableRow>\n ))}\n </TableBody>\n </Table>\n\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <p className=\"text-xs text-muted-foreground\">\n Showing {visible.length} of {filtered.length} members\n </p>\n <Pagination\n page={current}\n pageCount={pageCount}\n onPageChange={setPage}\n labels={{ navigation: \"Member pages\" }}\n />\n </div>\n </>\n )}\n </section>\n );\n}\n",
32
+ "type": "registry:block",
33
+ "target": "components/blocks/users-table.tsx"
34
+ }
35
+ ]
36
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://abackui.com/schema/registry-item.json",
3
+ "name": "utils",
4
+ "type": "registry:lib",
5
+ "title": "Class utility",
6
+ "description": "The cn() helper used by every Aback UI component.",
7
+ "dependencies": [
8
+ "clsx",
9
+ "tailwind-merge"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "lib/utils.ts",
14
+ "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n",
15
+ "type": "registry:lib",
16
+ "target": "lib/utils.ts"
17
+ }
18
+ ]
19
+ }