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.
- package/LICENSE +21 -0
- package/README.md +85 -2
- package/dist/args.d.ts +15 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +91 -0
- package/dist/args.js.map +1 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +106 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/add.d.ts +29 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +82 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/diff.d.ts +13 -0
- package/dist/commands/diff.d.ts.map +1 -0
- package/dist/commands/diff.js +85 -0
- package/dist/commands/diff.js.map +1 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +45 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/init.d.ts +19 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +97 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/list.d.ts +11 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +42 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/view.d.ts +11 -0
- package/dist/commands/view.d.ts.map +1 -0
- package/dist/commands/view.js +43 -0
- package/dist/commands/view.js.map +1 -0
- package/dist/config.d.ts +62 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +265 -0
- package/dist/config.js.map +1 -0
- package/dist/diff.d.ts +30 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +121 -0
- package/dist/diff.js.map +1 -0
- package/dist/doctor.d.ts +35 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +203 -0
- package/dist/doctor.js.map +1 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +12 -0
- package/dist/errors.js.map +1 -0
- package/dist/help.d.ts +3 -0
- package/dist/help.d.ts.map +1 -0
- package/dist/help.js +48 -0
- package/dist/help.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/install.d.ts +53 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +119 -0
- package/dist/install.js.map +1 -0
- package/dist/logger.d.ts +14 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +47 -0
- package/dist/logger.js.map +1 -0
- package/dist/paths.d.ts +18 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +76 -0
- package/dist/paths.js.map +1 -0
- package/dist/prompt.d.ts +13 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +25 -0
- package/dist/prompt.js.map +1 -0
- package/dist/registry.d.ts +49 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +216 -0
- package/dist/registry.js.map +1 -0
- package/dist/types.d.ts +88 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +59 -4
- package/registry/accordion.json +32 -0
- package/registry/alert-dialog.json +28 -0
- package/registry/alert.json +31 -0
- package/registry/article-layout.json +38 -0
- package/registry/avatar.json +31 -0
- package/registry/badge.json +33 -0
- package/registry/breadcrumbs.json +32 -0
- package/registry/button.json +34 -0
- package/registry/calendar.json +29 -0
- package/registry/card.json +30 -0
- package/registry/checkbox.json +33 -0
- package/registry/code-block.json +29 -0
- package/registry/collapsible.json +28 -0
- package/registry/combobox.json +30 -0
- package/registry/command.json +34 -0
- package/registry/contact-form.json +37 -0
- package/registry/cookie-consent.json +28 -0
- package/registry/copy-button.json +33 -0
- package/registry/cta-banner.json +32 -0
- package/registry/dashboard-shell.json +32 -0
- package/registry/date-picker.json +32 -0
- package/registry/description-list.json +28 -0
- package/registry/dialog.json +33 -0
- package/registry/dropdown-menu.json +33 -0
- package/registry/dropzone.json +31 -0
- package/registry/empty-state.json +32 -0
- package/registry/faq.json +31 -0
- package/registry/feature-grid.json +33 -0
- package/registry/field.json +29 -0
- package/registry/hero.json +32 -0
- package/registry/hover-card.json +28 -0
- package/registry/index.json +1633 -0
- package/registry/input-group.json +30 -0
- package/registry/input.json +29 -0
- package/registry/kbd.json +29 -0
- package/registry/label.json +29 -0
- package/registry/layout.json +32 -0
- package/registry/newsletter.json +35 -0
- package/registry/not-found.json +34 -0
- package/registry/page-header.json +29 -0
- package/registry/pagination.json +25 -0
- package/registry/popover.json +28 -0
- package/registry/pricing.json +35 -0
- package/registry/progress.json +28 -0
- package/registry/radio-group.json +28 -0
- package/registry/rating.json +32 -0
- package/registry/reading-progress.json +29 -0
- package/registry/relative-time.json +29 -0
- package/registry/scroll-area.json +28 -0
- package/registry/section-heading.json +30 -0
- package/registry/select.json +32 -0
- package/registry/separator.json +32 -0
- package/registry/settings-section.json +32 -0
- package/registry/share-buttons.json +34 -0
- package/registry/sheet.json +34 -0
- package/registry/sign-in.json +31 -0
- package/registry/site-footer.json +31 -0
- package/registry/site-header.json +37 -0
- package/registry/skeleton.json +28 -0
- package/registry/skip-link.json +30 -0
- package/registry/slider.json +28 -0
- package/registry/spinner.json +32 -0
- package/registry/stat.json +32 -0
- package/registry/steps.json +31 -0
- package/registry/switch.json +32 -0
- package/registry/table-of-contents.json +32 -0
- package/registry/table.json +28 -0
- package/registry/tabs.json +29 -0
- package/registry/textarea.json +29 -0
- package/registry/theme-toggle.json +34 -0
- package/registry/theme.json +16 -0
- package/registry/toast.json +29 -0
- package/registry/toggle-group.json +29 -0
- package/registry/tooltip.json +29 -0
- package/registry/users-table.json +36 -0
- package/registry/utils.json +19 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "input-group",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Input Group",
|
|
6
|
+
"description": "Joins an input with units, prefixes, icons, or a compact button inside one 36px frame.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://abackui.com/r/utils.json",
|
|
9
|
+
"https://abackui.com/r/input.json"
|
|
10
|
+
],
|
|
11
|
+
"categories": [
|
|
12
|
+
"Forms"
|
|
13
|
+
],
|
|
14
|
+
"docs": "https://abackui.com/docs/components/input-group",
|
|
15
|
+
"meta": {
|
|
16
|
+
"status": "beta",
|
|
17
|
+
"usedIn": [
|
|
18
|
+
"aback-tools",
|
|
19
|
+
"aback-launch"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
{
|
|
24
|
+
"path": "components/ui/input-group.tsx",
|
|
25
|
+
"content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Input } from \"@/components/ui/input\";\n\nconst CONTROL_SELECTOR = \"[data-slot='input-group-control']\";\n\n/**\n * A 36px input frame that joins a control with prefixes and suffixes: units,\n * currency, a search icon, or a small button. The frame owns the border and\n * the focus ring, so the affixes and the field read as one control.\n */\nfunction InputGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"input-group\"\n className={cn(\n \"flex h-9 w-full min-w-0 items-stretch border border-input bg-transparent shadow-xs dark:bg-input/30\",\n \"transition-[border-color,box-shadow] duration-200\",\n \"has-[[data-slot=input-group-control]:focus-visible]:border-ring\",\n \"has-[[data-slot=input-group-control]:focus-visible]:ring-2 has-[[data-slot=input-group-control]:focus-visible]:ring-focus-ring\",\n \"has-[[data-slot=input-group-control]:focus-visible]:ring-offset-2 has-[[data-slot=input-group-control]:focus-visible]:ring-offset-background\",\n \"has-[[data-slot=input-group-control][aria-invalid=true]]:border-destructive\",\n \"has-[[data-slot=input-group-control]:disabled]:cursor-not-allowed has-[[data-slot=input-group-control]:disabled]:opacity-50\",\n className,\n )}\n {...props}\n />\n );\n}\n\n/** The text control inside an InputGroup. Accepts every native input prop. */\nconst InputGroupInput = React.forwardRef<HTMLInputElement, React.ComponentProps<\"input\">>(\n function InputGroupInput({ className, ...props }, ref) {\n return (\n <Input\n ref={ref}\n data-slot=\"input-group-control\"\n className={cn(\n \"h-full min-w-0 flex-1 border-0 bg-transparent shadow-none\",\n \"focus-visible:ring-0 focus-visible:ring-offset-0 dark:bg-transparent\",\n className,\n )}\n {...props}\n />\n );\n },\n);\nInputGroupInput.displayName = \"InputGroupInput\";\n\nexport interface InputGroupAddonProps extends React.ComponentProps<\"div\"> {\n /** Which side of the control the addon sits on. Sets the dividing border. */\n align?: \"start\" | \"end\";\n /**\n * `text` is a boxed segment for units and prefixes (\"$\", \"%\", \"https://\").\n * `inline` sits inside the field for an icon or a compact button.\n */\n variant?: \"text\" | \"inline\";\n}\n\n/**\n * A prefix or suffix. Clicking a non-interactive addon focuses the control,\n * the way a native label would. Give the addon an id and add it to the\n * control's `aria-describedby` when it carries meaning, such as a unit.\n */\nfunction InputGroupAddon({\n align = \"start\",\n variant = \"text\",\n className,\n onMouseDown,\n ...props\n}: InputGroupAddonProps) {\n function handleMouseDown(event: React.MouseEvent<HTMLDivElement>) {\n onMouseDown?.(event);\n if (event.defaultPrevented) return;\n const target = event.target as HTMLElement;\n if (target.closest(\"button, a, input, select, textarea\")) return;\n const control = event.currentTarget.parentElement?.querySelector<HTMLElement>(CONTROL_SELECTOR);\n if (!control) return;\n event.preventDefault();\n control.focus();\n }\n\n return (\n <div\n data-slot=\"input-group-addon\"\n data-align={align}\n data-variant={variant}\n onMouseDown={handleMouseDown}\n className={cn(\n \"flex shrink-0 select-none items-center whitespace-nowrap text-sm text-muted-foreground\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n variant === \"text\" && \"border-input bg-muted/40 px-3\",\n variant === \"text\" && (align === \"start\" ? \"border-r\" : \"border-l\"),\n variant === \"inline\" && (align === \"start\" ? \"pl-3\" : \"gap-1 pr-2\"),\n align === \"end\" && \"order-last\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport { InputGroup, InputGroupAddon, InputGroupInput };\n",
|
|
26
|
+
"type": "registry:ui",
|
|
27
|
+
"target": "components/ui/input-group.tsx"
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "input",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Input",
|
|
6
|
+
"description": "A single-line text control aligned to the 36px control grid.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://abackui.com/r/utils.json"
|
|
9
|
+
],
|
|
10
|
+
"categories": [
|
|
11
|
+
"Forms"
|
|
12
|
+
],
|
|
13
|
+
"docs": "https://abackui.com/docs/components/input",
|
|
14
|
+
"meta": {
|
|
15
|
+
"status": "stable",
|
|
16
|
+
"usedIn": [
|
|
17
|
+
"aback-tools",
|
|
18
|
+
"aback-launch"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
{
|
|
23
|
+
"path": "components/ui/input.tsx",
|
|
24
|
+
"content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst inputClassName = [\n \"flex h-9 w-full min-w-0 border border-input bg-transparent px-3 py-1 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 \"file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-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 Input = React.forwardRef<HTMLInputElement, React.ComponentProps<\"input\">>(\n function Input({ className, type, ...props }, ref) {\n return (\n <input\n ref={ref}\n type={type}\n data-slot=\"input\"\n className={cn(inputClassName, className)}\n {...props}\n />\n );\n },\n);\nInput.displayName = \"Input\";\n\nexport { Input, inputClassName };\n",
|
|
25
|
+
"type": "registry:ui",
|
|
26
|
+
"target": "components/ui/input.tsx"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "kbd",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Kbd",
|
|
6
|
+
"description": "A keycap for documenting shortcuts.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://abackui.com/r/utils.json"
|
|
9
|
+
],
|
|
10
|
+
"categories": [
|
|
11
|
+
"Foundation"
|
|
12
|
+
],
|
|
13
|
+
"docs": "https://abackui.com/docs/components/kbd",
|
|
14
|
+
"meta": {
|
|
15
|
+
"status": "stable",
|
|
16
|
+
"usedIn": [
|
|
17
|
+
"aback-tools",
|
|
18
|
+
"aback-launch"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
{
|
|
23
|
+
"path": "components/ui/kbd.tsx",
|
|
24
|
+
"content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Kbd({ className, ...props }: React.ComponentProps<\"kbd\">) {\n return (\n <kbd\n data-slot=\"kbd\"\n className={cn(\n \"inline-flex h-5 min-w-5 items-center justify-center border border-border/60 bg-background px-1\",\n \"font-sans text-[10px] font-bold uppercase tracking-[0.12em] text-muted-foreground\",\n \"shadow-[0_1px_0_0_rgb(0_0_0/0.04)] dark:shadow-[0_1px_0_0_rgb(255_255_255/0.04)]\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport { Kbd };\n",
|
|
25
|
+
"type": "registry:ui",
|
|
26
|
+
"target": "components/ui/kbd.tsx"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "label",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Label",
|
|
6
|
+
"description": "An accessible form label with peer-disabled styling.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://abackui.com/r/utils.json"
|
|
9
|
+
],
|
|
10
|
+
"categories": [
|
|
11
|
+
"Forms"
|
|
12
|
+
],
|
|
13
|
+
"docs": "https://abackui.com/docs/components/label",
|
|
14
|
+
"meta": {
|
|
15
|
+
"status": "stable",
|
|
16
|
+
"usedIn": [
|
|
17
|
+
"aback-tools",
|
|
18
|
+
"aback-launch"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
{
|
|
23
|
+
"path": "components/ui/label.tsx",
|
|
24
|
+
"content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Label = React.forwardRef<\n HTMLLabelElement,\n React.ComponentProps<\"label\">\n>(function Label({ className, ...props }, ref) {\n return (\n <label\n ref={ref}\n data-slot=\"label\"\n className={cn(\n \"flex items-center gap-1 text-sm font-medium leading-none text-foreground\",\n \"peer-disabled:cursor-not-allowed peer-disabled:opacity-70\",\n className,\n )}\n {...props}\n />\n );\n});\nLabel.displayName = \"Label\";\n\nexport { Label };\n",
|
|
25
|
+
"type": "registry:ui",
|
|
26
|
+
"target": "components/ui/label.tsx"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "layout",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Layout",
|
|
6
|
+
"description": "Container, Stack, Group, and Grid primitives that map to the Aback spacing scale.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"radix-ui"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://abackui.com/r/utils.json"
|
|
12
|
+
],
|
|
13
|
+
"categories": [
|
|
14
|
+
"Layout"
|
|
15
|
+
],
|
|
16
|
+
"docs": "https://abackui.com/docs/components/layout",
|
|
17
|
+
"meta": {
|
|
18
|
+
"status": "beta",
|
|
19
|
+
"usedIn": [
|
|
20
|
+
"aback-tools",
|
|
21
|
+
"aback-launch"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
{
|
|
26
|
+
"path": "components/ui/layout.tsx",
|
|
27
|
+
"content": "import * as React from \"react\";\nimport { Slot } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\n// Literal class names, so Tailwind can find every one of them in the source.\nconst gapClasses = {\n none: \"gap-0\",\n xs: \"gap-1\",\n sm: \"gap-2\",\n md: \"gap-4\",\n lg: \"gap-6\",\n xl: \"gap-8\",\n} as const;\n\nexport type LayoutGap = keyof typeof gapClasses;\n\ninterface LayoutProps extends React.ComponentProps<\"div\"> {\n /** Render the child element instead of a div, such as a `<main>` or `<section>`. */\n asChild?: boolean;\n}\n\nconst containerSizes = {\n sm: \"max-w-2xl\",\n md: \"max-w-4xl\",\n lg: \"max-w-6xl\",\n xl: \"max-w-7xl\",\n full: \"max-w-full\",\n} as const;\n\nexport interface ContainerProps extends LayoutProps {\n size?: keyof typeof containerSizes;\n}\n\n/** A centred page column with the standard gutter: 16px on phones, 24px from md. */\nfunction Container({ size = \"lg\", asChild = false, className, ...props }: ContainerProps) {\n const Comp = asChild ? Slot.Root : \"div\";\n return (\n <Comp\n data-slot=\"container\"\n className={cn(\"mx-auto w-full px-4 md:px-6\", containerSizes[size], className)}\n {...props}\n />\n );\n}\n\nexport interface StackProps extends LayoutProps {\n gap?: LayoutGap;\n}\n\n/** A vertical flow with one gap between children. */\nfunction Stack({ gap = \"md\", asChild = false, className, ...props }: StackProps) {\n const Comp = asChild ? Slot.Root : \"div\";\n return (\n <Comp\n data-slot=\"stack\"\n className={cn(\"flex flex-col\", gapClasses[gap], className)}\n {...props}\n />\n );\n}\n\nconst alignClasses = {\n start: \"items-start\",\n center: \"items-center\",\n end: \"items-end\",\n baseline: \"items-baseline\",\n stretch: \"items-stretch\",\n} as const;\n\nconst justifyClasses = {\n start: \"justify-start\",\n center: \"justify-center\",\n end: \"justify-end\",\n between: \"justify-between\",\n} as const;\n\nexport interface GroupProps extends LayoutProps {\n gap?: LayoutGap;\n align?: keyof typeof alignClasses;\n justify?: keyof typeof justifyClasses;\n /** Wrap onto new lines on narrow screens. On by default, so rows never overflow. */\n wrap?: boolean;\n}\n\n/** A horizontal row for toolbars and action groups. */\nfunction Group({\n gap = \"sm\",\n align = \"center\",\n justify = \"start\",\n wrap = true,\n asChild = false,\n className,\n ...props\n}: GroupProps) {\n const Comp = asChild ? Slot.Root : \"div\";\n return (\n <Comp\n data-slot=\"group\"\n className={cn(\n \"flex flex-row\",\n wrap && \"flex-wrap\",\n gapClasses[gap],\n alignClasses[align],\n justifyClasses[justify],\n className,\n )}\n {...props}\n />\n );\n}\n\nconst gridColumns = {\n 1: \"grid-cols-1\",\n 2: \"grid-cols-1 sm:grid-cols-2\",\n 3: \"grid-cols-1 sm:grid-cols-2 lg:grid-cols-3\",\n 4: \"grid-cols-1 sm:grid-cols-2 lg:grid-cols-4\",\n 6: \"grid-cols-2 sm:grid-cols-3 lg:grid-cols-6\",\n} as const;\n\nexport interface GridProps extends LayoutProps {\n /** Columns at the widest breakpoint. Narrower screens step down to one or two. */\n columns?: keyof typeof gridColumns;\n gap?: LayoutGap;\n}\n\n/** A responsive card grid that starts at one column on phones. */\nfunction Grid({ columns = 3, gap = \"md\", asChild = false, className, ...props }: GridProps) {\n const Comp = asChild ? Slot.Root : \"div\";\n return (\n <Comp\n data-slot=\"grid\"\n className={cn(\"grid\", gridColumns[columns], gapClasses[gap], className)}\n {...props}\n />\n );\n}\n\nexport { Container, Grid, Group, Stack };\n",
|
|
28
|
+
"type": "registry:ui",
|
|
29
|
+
"target": "components/ui/layout.tsx"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "newsletter",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "Newsletter signup",
|
|
6
|
+
"description": "An email signup with real submitting, success, and error states driven by your subscribe call.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@tabler/icons-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://abackui.com/r/utils.json",
|
|
12
|
+
"https://abackui.com/r/button.json",
|
|
13
|
+
"https://abackui.com/r/input-group.json",
|
|
14
|
+
"https://abackui.com/r/spinner.json"
|
|
15
|
+
],
|
|
16
|
+
"categories": [
|
|
17
|
+
"Marketing"
|
|
18
|
+
],
|
|
19
|
+
"docs": "https://abackui.com/docs/blocks/newsletter",
|
|
20
|
+
"meta": {
|
|
21
|
+
"status": "beta",
|
|
22
|
+
"usedIn": [
|
|
23
|
+
"aback-tools",
|
|
24
|
+
"aback-launch"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
{
|
|
29
|
+
"path": "components/blocks/newsletter.tsx",
|
|
30
|
+
"content": "\"use client\";\n\nimport { useId, useState, type FormEvent, type ReactNode } from \"react\";\nimport { IconAlertCircle, IconCircleCheck, IconMail } from \"@tabler/icons-react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { InputGroup, InputGroupAddon, InputGroupInput } from \"@/components/ui/input-group\";\nimport { Spinner } from \"@/components/ui/spinner\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface NewsletterResult {\n ok: boolean;\n /** Shown to the visitor. Say what happens next, such as \"Check your inbox to confirm.\" */\n message: string;\n}\n\nexport interface NewsletterBlockProps {\n title?: ReactNode;\n description?: ReactNode;\n /** Your subscribe call. The block shows its result and never claims success on its own. */\n onSubscribe: (email: string) => Promise<NewsletterResult>;\n buttonLabel?: string;\n /** What you send and how to leave, shown under the form. */\n consent?: ReactNode;\n /** \"card\" is a bordered panel; \"inline\" sits in a sidebar or footer. */\n variant?: \"card\" | \"inline\";\n}\n\ntype Status = { state: \"idle\" } | { state: \"submitting\" } | { state: \"done\"; result: NewsletterResult };\n\n/**\n * An email signup with real submission states. It validates with the\n * browser's email type, disables itself while submitting, and announces the\n * result your `onSubscribe` returns.\n */\nexport function NewsletterBlock({\n title = \"Get product updates\",\n description = \"One email when something ships. No tracking pixels.\",\n onSubscribe,\n buttonLabel = \"Subscribe\",\n consent,\n variant = \"card\",\n}: NewsletterBlockProps) {\n const id = useId();\n const [email, setEmail] = useState(\"\");\n const [status, setStatus] = useState<Status>({ state: \"idle\" });\n const submitting = status.state === \"submitting\";\n const result = status.state === \"done\" ? status.result : null;\n\n async function handleSubmit(event: FormEvent<HTMLFormElement>) {\n event.preventDefault();\n setStatus({ state: \"submitting\" });\n try {\n const outcome = await onSubscribe(email.trim());\n setStatus({ state: \"done\", result: outcome });\n if (outcome.ok) setEmail(\"\");\n } catch {\n setStatus({\n state: \"done\",\n result: { ok: false, message: \"Something went wrong. Try again in a moment.\" },\n });\n }\n }\n\n return (\n <section\n aria-labelledby={`${id}-title`}\n className={cn(\n \"flex flex-col gap-4\",\n variant === \"card\" && \"border border-border bg-card p-6 sm:p-8\",\n )}\n >\n <div className=\"flex flex-col gap-1.5\">\n <h2 id={`${id}-title`} className={cn(\"font-semibold text-foreground\", variant === \"card\" ? \"text-xl\" : \"text-sm\")}>\n {title}\n </h2>\n {description ? (\n <p className={cn(\"text-muted-foreground\", variant === \"card\" ? \"text-sm\" : \"text-xs\")}>{description}</p>\n ) : null}\n </div>\n\n <form onSubmit={handleSubmit} className=\"flex flex-col gap-2 sm:flex-row\">\n <label htmlFor={`${id}-email`} className=\"sr-only\">\n Email address\n </label>\n <InputGroup className=\"flex-1\">\n <InputGroupAddon variant=\"inline\">\n <IconMail aria-hidden=\"true\" />\n </InputGroupAddon>\n <InputGroupInput\n id={`${id}-email`}\n name=\"email\"\n type=\"email\"\n autoComplete=\"email\"\n inputMode=\"email\"\n required\n placeholder=\"you@company.com\"\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n disabled={submitting}\n aria-invalid={result && !result.ok ? true : undefined}\n aria-describedby={result ? `${id}-result` : undefined}\n />\n </InputGroup>\n <Button type=\"submit\" disabled={submitting} className=\"sm:w-auto\">\n {submitting ? <Spinner aria-hidden=\"true\" size=\"sm\" className=\"text-current\" /> : null}\n {submitting ? \"Subscribing\" : buttonLabel}\n </Button>\n </form>\n\n <div role=\"status\" id={`${id}-result`}>\n {result ? (\n <p\n className={cn(\n \"flex items-start gap-2 border p-3 text-sm\",\n result.ok\n ? \"border-success/30 bg-success-soft text-success-strong\"\n : \"border-danger/30 bg-danger-soft text-danger-strong\",\n )}\n >\n {result.ok ? (\n <IconCircleCheck aria-hidden=\"true\" className=\"mt-0.5 size-4 shrink-0\" />\n ) : (\n <IconAlertCircle aria-hidden=\"true\" className=\"mt-0.5 size-4 shrink-0\" />\n )}\n {result.message}\n </p>\n ) : null}\n </div>\n\n {consent ? <p className=\"text-xs leading-relaxed text-muted-foreground\">{consent}</p> : null}\n </section>\n );\n}\n",
|
|
31
|
+
"type": "registry:block",
|
|
32
|
+
"target": "components/blocks/newsletter.tsx"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "not-found",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "Not found",
|
|
6
|
+
"description": "A 404 page with a way forward: home, an alternative, and the destinations people usually want.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@tabler/icons-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://abackui.com/r/badge.json",
|
|
12
|
+
"https://abackui.com/r/button.json"
|
|
13
|
+
],
|
|
14
|
+
"categories": [
|
|
15
|
+
"Application"
|
|
16
|
+
],
|
|
17
|
+
"docs": "https://abackui.com/docs/blocks/not-found",
|
|
18
|
+
"meta": {
|
|
19
|
+
"status": "beta",
|
|
20
|
+
"usedIn": [
|
|
21
|
+
"aback-tools",
|
|
22
|
+
"aback-launch",
|
|
23
|
+
"aback-group"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
{
|
|
28
|
+
"path": "components/blocks/not-found.tsx",
|
|
29
|
+
"content": "import { useId, type ReactNode } from \"react\";\nimport { IconArrowRight, IconHome, IconSearch } from \"@tabler/icons-react\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\n\nexport interface NotFoundLink {\n title: string;\n description: string;\n href: string;\n}\n\nexport interface NotFoundBlockProps {\n title?: ReactNode;\n description?: ReactNode;\n homeHref?: string;\n /** A second real destination, such as the catalogue or the docs. */\n secondaryAction?: { label: string; href: string };\n /** Where most lost visitors were trying to go. */\n links?: NotFoundLink[];\n /** A hint that search exists, such as \"Press Ctrl K to search.\" Only if search is real. */\n searchHint?: ReactNode;\n}\n\n/**\n * A 404 page that offers a way forward: home, one alternative, and the pages\n * people usually want. It renders the page's h1. Return a real 404 status from\n * your framework's not-found route; this block is only the content.\n */\nexport function NotFoundBlock({\n title = \"Page not found\",\n description = \"The page you followed does not exist. It may have moved or been renamed.\",\n homeHref = \"/\",\n secondaryAction,\n links = [],\n searchHint,\n}: NotFoundBlockProps) {\n const headingId = useId();\n return (\n <section aria-labelledby={headingId} className=\"mx-auto w-full max-w-3xl px-4 py-16 md:px-6 lg:py-24\">\n <div className=\"border border-border bg-card p-6 md:p-10\">\n <Badge variant=\"brand\">Error 404</Badge>\n <h1 id={headingId} className=\"mt-4 text-3xl font-bold tracking-tight text-foreground\">\n {title}\n </h1>\n <p className=\"mt-2 max-w-xl text-sm leading-relaxed text-muted-foreground\">{description}</p>\n\n <div className=\"mt-6 flex flex-wrap gap-2\">\n <Button asChild>\n <a href={homeHref}>\n <IconHome aria-hidden=\"true\" />\n Go home\n </a>\n </Button>\n {secondaryAction ? (\n <Button variant=\"outline\" asChild>\n <a href={secondaryAction.href}>{secondaryAction.label}</a>\n </Button>\n ) : null}\n </div>\n\n {links.length ? (\n <div className=\"mt-10 border-t border-border pt-6\">\n <h2 className=\"text-[11px] font-semibold uppercase tracking-wider text-muted-foreground\">\n Popular destinations\n </h2>\n <ul className=\"mt-3 grid gap-3 sm:grid-cols-2\">\n {links.map((link) => (\n <li key={link.href}>\n <a\n href={link.href}\n className=\"group flex h-full items-start justify-between gap-3 border border-border p-3 outline-none transition-colors duration-200 hover:border-primary-300 hover:bg-muted/40 focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\"\n >\n <span className=\"flex flex-col gap-0.5\">\n <span className=\"text-sm font-medium text-foreground group-hover:text-link\">{link.title}</span>\n <span className=\"text-xs text-muted-foreground\">{link.description}</span>\n </span>\n <IconArrowRight\n aria-hidden=\"true\"\n className=\"mt-0.5 size-4 shrink-0 text-muted-foreground transition-transform duration-200 group-hover:translate-x-0.5 motion-reduce:transition-none\"\n />\n </a>\n </li>\n ))}\n </ul>\n </div>\n ) : null}\n\n {searchHint ? (\n <p className=\"mt-6 flex items-center gap-2 text-xs text-muted-foreground\">\n <IconSearch aria-hidden=\"true\" className=\"size-3.5\" />\n {searchHint}\n </p>\n ) : null}\n </div>\n </section>\n );\n}\n",
|
|
30
|
+
"type": "registry:block",
|
|
31
|
+
"target": "components/blocks/not-found.tsx"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "page-header",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Page Header",
|
|
6
|
+
"description": "The page title block with one H1, a description, and actions.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://abackui.com/r/utils.json"
|
|
9
|
+
],
|
|
10
|
+
"categories": [
|
|
11
|
+
"Application"
|
|
12
|
+
],
|
|
13
|
+
"docs": "https://abackui.com/docs/components/page-header",
|
|
14
|
+
"meta": {
|
|
15
|
+
"status": "stable",
|
|
16
|
+
"usedIn": [
|
|
17
|
+
"aback-tools",
|
|
18
|
+
"aback-launch"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
{
|
|
23
|
+
"path": "components/ui/page-header.tsx",
|
|
24
|
+
"content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface PageHeaderProps extends Omit<React.ComponentProps<\"header\">, \"title\"> {\n title: React.ReactNode;\n description?: React.ReactNode;\n eyebrow?: React.ReactNode;\n actions?: React.ReactNode;\n /**\n * Heading element for the title. Defaults to `h1`, which is the correct\n * choice for a page. Pass `h2` or `h3` only when the header introduces a\n * section below an existing page heading, such as a documentation preview.\n */\n as?: \"h1\" | \"h2\" | \"h3\";\n}\n\n/**\n * The page title block. It renders the page's single <h1> by default.\n * Breadcrumbs belong directly below this block, not inside it.\n */\nfunction PageHeader({\n title,\n description,\n eyebrow,\n actions,\n as: Heading = \"h1\",\n className,\n ...props\n}: PageHeaderProps) {\n return (\n <header\n data-slot=\"page-header\"\n className={cn(\"flex flex-col gap-2\", className)}\n {...props}\n >\n {eyebrow ? (\n <p className=\"text-xs font-medium uppercase tracking-wide text-muted-foreground\">\n {eyebrow}\n </p>\n ) : null}\n <Heading className=\"text-3xl font-bold leading-tight tracking-tight text-foreground text-balance\">\n {title}\n </Heading>\n {description ? (\n <p className=\"max-w-3xl text-base leading-relaxed text-muted-foreground\">\n {description}\n </p>\n ) : null}\n {actions ? <div className=\"mt-2 flex flex-wrap items-center gap-2\">{actions}</div> : null}\n </header>\n );\n}\n\nexport { PageHeader };\n",
|
|
25
|
+
"type": "registry:ui",
|
|
26
|
+
"target": "components/ui/page-header.tsx"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "pagination",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Pagination",
|
|
6
|
+
"description": "Page controls with a current-page marker, collapsing ranges, and link mode.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://abackui.com/r/utils.json"
|
|
9
|
+
],
|
|
10
|
+
"categories": [
|
|
11
|
+
"Navigation"
|
|
12
|
+
],
|
|
13
|
+
"docs": "https://abackui.com/docs/components/pagination",
|
|
14
|
+
"meta": {
|
|
15
|
+
"status": "beta"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
{
|
|
19
|
+
"path": "components/ui/pagination.tsx",
|
|
20
|
+
"content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface PaginationLabels {\n previous?: string;\n next?: string;\n navigation?: string;\n}\n\nexport interface PaginationProps extends Omit<React.ComponentProps<\"nav\">, \"onChange\"> {\n page: number;\n pageCount: number;\n onPageChange?: (page: number) => void;\n /** Render links instead of buttons. Use this for server-rendered pages. */\n hrefFor?: (page: number) => string;\n siblingCount?: number;\n labels?: PaginationLabels;\n}\n\ntype PageItem = number | \"ellipsis\";\n\nfunction range(start: number, end: number): number[] {\n return Array.from({ length: Math.max(0, end - start + 1) }, (_, index) => start + index);\n}\n\nexport function getPageItems(\n page: number,\n pageCount: number,\n siblingCount = 1,\n): PageItem[] {\n const total = Math.max(1, pageCount);\n const current = Math.min(Math.max(1, page), total);\n const visible = siblingCount * 2 + 5;\n\n if (total <= visible) return range(1, total);\n\n const left = Math.max(2, current - siblingCount);\n const right = Math.min(total - 1, current + siblingCount);\n const showLeftEllipsis = left > 2;\n const showRightEllipsis = right < total - 1;\n\n if (!showLeftEllipsis && showRightEllipsis) {\n const count = 3 + siblingCount * 2;\n return [...range(1, count), \"ellipsis\", total];\n }\n\n if (showLeftEllipsis && !showRightEllipsis) {\n const count = 3 + siblingCount * 2;\n return [1, \"ellipsis\", ...range(total - count + 1, total)];\n }\n\n return [1, \"ellipsis\", ...range(left, right), \"ellipsis\", total];\n}\n\nconst base =\n \"inline-flex h-8 min-w-8 items-center justify-center border px-2 text-sm font-medium tabular-nums transition-colors outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\";\n\nfunction Pagination({\n page,\n pageCount,\n onPageChange,\n hrefFor,\n siblingCount = 1,\n labels,\n className,\n ...props\n}: PaginationProps) {\n const total = Math.max(1, pageCount);\n const current = Math.min(Math.max(1, page), total);\n const items = getPageItems(current, total, siblingCount);\n\n function renderItem(target: number, label: React.ReactNode, key: string, disabled = false) {\n const isCurrent = target === current;\n\n if (isCurrent) {\n return (\n <span\n key={key}\n aria-current=\"page\"\n className={cn(base, \"border-primary bg-primary text-primary-foreground\")}\n >\n {label}\n </span>\n );\n }\n\n if (disabled) {\n return (\n <span\n key={key}\n aria-disabled=\"true\"\n className={cn(base, \"border-border text-muted-foreground opacity-50\")}\n >\n {label}\n </span>\n );\n }\n\n const common = {\n \"aria-label\": typeof label === \"string\" ? label : `Go to page ${label}`,\n className: cn(base, \"border-border bg-background text-foreground hover:bg-muted\"),\n };\n\n if (hrefFor) {\n return (\n <a key={key} href={hrefFor(target)} {...common}>\n {label}\n </a>\n );\n }\n\n return (\n <button key={key} type=\"button\" onClick={() => onPageChange?.(target)} {...common}>\n {label}\n </button>\n );\n }\n\n return (\n <nav\n aria-label={labels?.navigation ?? \"Pagination\"}\n className={cn(\"flex flex-wrap items-center gap-1\", className)}\n {...props}\n >\n {renderItem(current - 1, labels?.previous ?? \"Previous\", \"previous\", current <= 1)}\n {items.map((item, index) =>\n item === \"ellipsis\" ? (\n <span\n key={`ellipsis-${index}`}\n aria-hidden=\"true\"\n className=\"inline-flex size-8 items-center justify-center text-sm text-muted-foreground\"\n >\n …\n </span>\n ) : (\n renderItem(item, item, `page-${item}`)\n ),\n )}\n {renderItem(current + 1, labels?.next ?? \"Next\", \"next\", current >= total)}\n </nav>\n );\n}\n\nexport { Pagination };\n",
|
|
21
|
+
"type": "registry:ui",
|
|
22
|
+
"target": "components/ui/pagination.tsx"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "popover",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Popover",
|
|
6
|
+
"description": "A floating panel anchored to a trigger, for filters and compact detail.",
|
|
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/popover",
|
|
17
|
+
"meta": {
|
|
18
|
+
"status": "beta"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
{
|
|
22
|
+
"path": "components/ui/popover.tsx",
|
|
23
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Popover as PopoverPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Popover = PopoverPrimitive.Root;\nconst PopoverTrigger = PopoverPrimitive.Trigger;\nconst PopoverAnchor = PopoverPrimitive.Anchor;\n\nfunction PopoverContent({\n className,\n align = \"center\",\n sideOffset = 6,\n ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Content>) {\n return (\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n data-slot=\"popover-content\"\n align={align}\n sideOffset={sideOffset}\n className={cn(\n \"z-50 w-72 border border-border bg-popover p-4 text-popover-foreground shadow-lg outline-none\",\n \"max-h-[var(--radix-popover-content-available-height)] overflow-y-auto\",\n \"duration-150 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 </PopoverPrimitive.Portal>\n );\n}\n\nexport { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };\n",
|
|
24
|
+
"type": "registry:ui",
|
|
25
|
+
"target": "components/ui/popover.tsx"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "pricing",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "Pricing",
|
|
6
|
+
"description": "Plan cards with a monthly or yearly switch, Intl prices, included and excluded features, and aligned actions.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@tabler/icons-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://abackui.com/r/utils.json",
|
|
12
|
+
"https://abackui.com/r/badge.json",
|
|
13
|
+
"https://abackui.com/r/button.json",
|
|
14
|
+
"https://abackui.com/r/section-heading.json",
|
|
15
|
+
"https://abackui.com/r/toggle-group.json"
|
|
16
|
+
],
|
|
17
|
+
"categories": [
|
|
18
|
+
"Marketing"
|
|
19
|
+
],
|
|
20
|
+
"docs": "https://abackui.com/docs/blocks/pricing",
|
|
21
|
+
"meta": {
|
|
22
|
+
"status": "beta",
|
|
23
|
+
"usedIn": [
|
|
24
|
+
"aback-launch"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
{
|
|
29
|
+
"path": "components/blocks/pricing.tsx",
|
|
30
|
+
"content": "\"use client\";\n\nimport { type ReactNode, useId, useState } from \"react\";\nimport { IconCheck, IconMinus } from \"@tabler/icons-react\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { SectionHeading } from \"@/components/ui/section-heading\";\nimport { ToggleGroup, ToggleGroupItem } from \"@/components/ui/toggle-group\";\nimport { cn } from \"@/lib/utils\";\n\nexport type BillingPeriod = \"monthly\" | \"yearly\";\n\nexport interface PricingFeature {\n text: string;\n included: boolean;\n}\n\nexport interface PricingPlan {\n name: string;\n description: string;\n /** Price per month for each period, in the currency's main unit. 0 renders as Free. */\n price: Record<BillingPeriod, number>;\n features: PricingFeature[];\n cta: { label: string; href: string };\n /** Highlights the plan and shows the badge. */\n highlighted?: boolean;\n badge?: string;\n}\n\nexport interface PricingBlockProps {\n eyebrow?: ReactNode;\n title: ReactNode;\n description?: ReactNode;\n plans: PricingPlan[];\n /** ISO 4217 code for Intl.NumberFormat. */\n currency?: string;\n locale?: string;\n defaultPeriod?: BillingPeriod;\n /** Label for the yearly option, such as \"Yearly, save 20%\". Keep the saving accurate. */\n yearlyLabel?: string;\n /** A sentence under the plans: taxes, trials, or refunds. */\n footnote?: ReactNode;\n}\n\n/**\n * Plan cards with a monthly or yearly switch. Prices are formatted with Intl,\n * features list what is and is not included in words, and every card's call\n * to action lines up on one baseline.\n */\nexport function PricingBlock({\n eyebrow,\n title,\n description,\n plans,\n currency = \"USD\",\n locale = \"en-US\",\n defaultPeriod = \"monthly\",\n yearlyLabel = \"Yearly\",\n footnote,\n}: PricingBlockProps) {\n const headingId = useId();\n const [period, setPeriod] = useState<BillingPeriod>(defaultPeriod);\n const format = new Intl.NumberFormat(locale, {\n style: \"currency\",\n currency,\n maximumFractionDigits: 0,\n });\n\n return (\n <section aria-labelledby={headingId} className=\"mx-auto w-full max-w-6xl px-4 py-16 md:px-6 lg:py-20\">\n <SectionHeading\n align=\"center\"\n eyebrow={eyebrow}\n title={title}\n description={description}\n headingId={headingId}\n />\n\n <div className=\"mt-8 flex justify-center\">\n <ToggleGroup\n type=\"single\"\n variant=\"outline\"\n aria-label=\"Billing period\"\n value={period}\n onValueChange={(value) => {\n if (value === \"monthly\" || value === \"yearly\") setPeriod(value);\n }}\n >\n <ToggleGroupItem value=\"monthly\">Monthly</ToggleGroupItem>\n <ToggleGroupItem value=\"yearly\">{yearlyLabel}</ToggleGroupItem>\n </ToggleGroup>\n </div>\n\n <ul className={cn(\"mt-10 grid gap-6\", plans.length >= 3 ? \"lg:grid-cols-3\" : \"md:grid-cols-2\")}>\n {plans.map((plan) => {\n const amount = plan.price[period];\n return (\n <li\n key={plan.name}\n className={cn(\n \"relative flex flex-col border p-6 sm:p-8\",\n plan.highlighted ? \"border-primary-500 bg-brand-wash\" : \"border-border bg-card\",\n )}\n >\n {plan.badge ? (\n <Badge variant={plan.highlighted ? \"brand\" : \"outline\"} className=\"absolute -top-3 left-6\">\n {plan.badge}\n </Badge>\n ) : null}\n <h3 className=\"text-lg font-semibold text-foreground\">{plan.name}</h3>\n <p className=\"mt-2 min-h-10 text-sm text-muted-foreground\">{plan.description}</p>\n\n <p className=\"mt-6 flex items-baseline gap-1\">\n <span className=\"text-4xl font-extrabold tracking-tight tabular-nums text-foreground\">\n {amount === 0 ? \"Free\" : format.format(amount)}\n </span>\n {amount === 0 ? null : (\n <span className=\"text-sm text-muted-foreground\">\n per month{period === \"yearly\" ? \", billed yearly\" : \"\"}\n </span>\n )}\n </p>\n\n <ul className=\"mt-8 flex flex-1 flex-col gap-3\" aria-label={`${plan.name} features`}>\n {plan.features.map((feature) => (\n <li key={feature.text} className=\"flex items-start gap-3 text-sm\">\n {feature.included ? (\n <IconCheck aria-hidden=\"true\" className=\"mt-0.5 size-4 shrink-0 text-brand-icon\" />\n ) : (\n <IconMinus aria-hidden=\"true\" className=\"mt-0.5 size-4 shrink-0 text-muted-foreground\" />\n )}\n <span className={feature.included ? \"text-foreground\" : \"text-muted-foreground\"}>\n {feature.included ? null : <span className=\"sr-only\">Not included: </span>}\n {feature.text}\n </span>\n </li>\n ))}\n </ul>\n\n <Button asChild size=\"lg\" variant={plan.highlighted ? \"default\" : \"outline\"} className=\"mt-8 w-full\">\n <a href={plan.cta.href}>\n {plan.cta.label}\n <span className=\"sr-only\">, {plan.name} plan</span>\n </a>\n </Button>\n </li>\n );\n })}\n </ul>\n\n {footnote ? <p className=\"mt-6 text-center text-xs text-muted-foreground\">{footnote}</p> : null}\n </section>\n );\n}\n",
|
|
31
|
+
"type": "registry:block",
|
|
32
|
+
"target": "components/blocks/pricing.tsx"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "progress",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Progress",
|
|
6
|
+
"description": "A determinate or indeterminate bar for work that takes time.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"radix-ui"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://abackui.com/r/utils.json"
|
|
12
|
+
],
|
|
13
|
+
"categories": [
|
|
14
|
+
"Feedback"
|
|
15
|
+
],
|
|
16
|
+
"docs": "https://abackui.com/docs/components/progress",
|
|
17
|
+
"meta": {
|
|
18
|
+
"status": "beta"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
{
|
|
22
|
+
"path": "components/ui/progress.tsx",
|
|
23
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Progress as ProgressPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface ProgressProps extends React.ComponentProps<typeof ProgressPrimitive.Root> {\n /** Accessible name. A progress bar without a name is not usable. */\n label?: string;\n /** Optional visible value, for example `64%`. Enable when the number matters. */\n showValue?: boolean;\n indicatorClassName?: string;\n}\n\nfunction Progress({\n className,\n value,\n max = 100,\n label,\n showValue = false,\n indicatorClassName,\n ...props\n}: ProgressProps) {\n const bounded = value === null || value === undefined ? null : Math.min(Math.max(value, 0), max);\n const percent = bounded === null ? 0 : max > 0 ? (bounded / max) * 100 : 0;\n\n return (\n <div data-slot=\"progress\" className=\"flex w-full flex-col gap-1.5\">\n {label || showValue ? (\n <div className=\"flex items-baseline justify-between gap-3\">\n {label ? <span className=\"text-sm font-medium text-foreground\">{label}</span> : <span />}\n {showValue ? (\n <span className=\"text-xs tabular-nums text-muted-foreground\">\n {bounded === null ? \"Working\" : `${Math.round(percent)}%`}\n </span>\n ) : null}\n </div>\n ) : null}\n <ProgressPrimitive.Root\n data-slot=\"progress-root\"\n {...props}\n value={bounded}\n max={max}\n aria-label={label ?? props[\"aria-label\"]}\n className={cn(\"relative h-1.5 w-full overflow-hidden bg-muted\", className)}\n >\n <ProgressPrimitive.Indicator\n data-slot=\"progress-indicator\"\n className={cn(\n \"h-full w-full flex-1 bg-primary-500 transition-transform duration-300 motion-reduce:transition-none\",\n bounded === null ? \"animate-pulse\" : \"\",\n indicatorClassName,\n )}\n style={{ transform: `translateX(-${100 - percent}%)` }}\n />\n </ProgressPrimitive.Root>\n </div>\n );\n}\n\nexport { Progress };\n",
|
|
24
|
+
"type": "registry:ui",
|
|
25
|
+
"target": "components/ui/progress.tsx"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "radio-group",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Radio Group",
|
|
6
|
+
"description": "Mutually exclusive choices with arrow-key selection and a visible label.",
|
|
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/radio-group",
|
|
17
|
+
"meta": {
|
|
18
|
+
"status": "beta"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
{
|
|
22
|
+
"path": "components/ui/radio-group.tsx",
|
|
23
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { RadioGroup as RadioGroupPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction RadioGroup({\n className,\n ...props\n}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {\n return (\n <RadioGroupPrimitive.Root\n data-slot=\"radio-group\"\n className={cn(\"grid gap-2\", className)}\n {...props}\n />\n );\n}\n\nfunction RadioGroupItem({\n className,\n ...props\n}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {\n return (\n <RadioGroupPrimitive.Item\n data-slot=\"radio-group-item\"\n className={cn(\n \"peer size-4 shrink-0 border border-input bg-background outline-none transition-colors\",\n \"hover:border-foreground/40\",\n \"focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n \"data-[state=checked]:border-primary data-[state=checked]:bg-primary\",\n \"aria-invalid:border-destructive\",\n className,\n )}\n {...props}\n >\n <RadioGroupPrimitive.Indicator\n data-slot=\"radio-group-indicator\"\n className=\"flex items-center justify-center\"\n >\n <span aria-hidden=\"true\" className=\"size-1.5 bg-primary-foreground\" />\n </RadioGroupPrimitive.Indicator>\n </RadioGroupPrimitive.Item>\n );\n}\n\nexport { RadioGroup, RadioGroupItem };\n",
|
|
24
|
+
"type": "registry:ui",
|
|
25
|
+
"target": "components/ui/radio-group.tsx"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "rating",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Rating",
|
|
6
|
+
"description": "A star rating built on native radios, with a read-only display for averages.",
|
|
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/rating",
|
|
17
|
+
"meta": {
|
|
18
|
+
"status": "beta",
|
|
19
|
+
"usedIn": [
|
|
20
|
+
"aback-tools",
|
|
21
|
+
"aback-launch"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
{
|
|
26
|
+
"path": "components/ui/rating.tsx",
|
|
27
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { IconStar, IconStarFilled } from \"@tabler/icons-react\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype RatingSize = \"sm\" | \"default\" | \"lg\";\n\nconst iconSize: Record<RatingSize, string> = {\n sm: \"size-4\",\n default: \"size-5\",\n lg: \"size-6\",\n};\n\nfunction defaultItemLabel(value: number) {\n return `${value} ${value === 1 ? \"star\" : \"stars\"}`;\n}\n\nfunction Star({ fill, size }: { fill: number; size: RatingSize }) {\n // A partial fill clips the filled glyph over the outline, so read-only\n // averages such as 4.5 render without a second icon set.\n const percent = Math.round(Math.min(Math.max(fill, 0), 1) * 100);\n return (\n <span aria-hidden=\"true\" className={cn(\"relative inline-flex shrink-0\", iconSize[size])}>\n <IconStar className=\"size-full text-muted-foreground\" />\n {percent > 0 ? (\n <span className=\"absolute inset-y-0 left-0 overflow-hidden\" style={{ width: `${percent}%` }}>\n <IconStarFilled className={cn(\"text-warning\", iconSize[size])} />\n </span>\n ) : null}\n </span>\n );\n}\n\nexport interface RatingProps\n extends Omit<React.ComponentProps<\"div\">, \"onChange\" | \"defaultValue\" | \"dir\"> {\n /** Controlled value. 0 means no rating. */\n value?: number;\n defaultValue?: number;\n onValueChange?: (value: number) => void;\n /** Number of stars. */\n max?: number;\n /** Form field name. The checked radio submits its value with the form. */\n name?: string;\n /**\n * Display a value instead of collecting one. Accepts fractions such as 4.5\n * and renders a single image with a text alternative.\n */\n readOnly?: boolean;\n disabled?: boolean;\n /** Let a second press on the selected star clear the rating. */\n clearable?: boolean;\n size?: RatingSize;\n /** Accessible name for each option. Defaults to \"3 stars\". */\n getItemLabel?: (value: number, max: number) => string;\n /** Text alternative in read-only mode. Defaults to \"Rated 4.5 out of 5\". */\n getValueLabel?: (value: number, max: number) => string;\n}\n\n/**\n * A star rating. Interactive ratings are a native radio group, so arrow keys,\n * Tab, and form submission work without custom key handling. Name the group\n * with `aria-label` or `aria-labelledby`.\n */\nfunction Rating({\n value: valueProp,\n defaultValue = 0,\n onValueChange,\n max = 5,\n name,\n readOnly = false,\n disabled = false,\n clearable = false,\n size = \"default\",\n getItemLabel = defaultItemLabel,\n getValueLabel = (value, total) => `Rated ${value} out of ${total}`,\n className,\n onMouseLeave,\n ...props\n}: RatingProps) {\n const generatedName = React.useId();\n const [uncontrolled, setUncontrolled] = React.useState(defaultValue);\n const [hovered, setHovered] = React.useState<number | null>(null);\n const value = valueProp ?? uncontrolled;\n const stars = Array.from({ length: max }, (_, index) => index + 1);\n\n if (readOnly) {\n return (\n <div\n role=\"img\"\n aria-label={getValueLabel(value, max)}\n data-slot=\"rating\"\n data-readonly=\"\"\n className={cn(\"inline-flex items-center gap-0.5\", className)}\n {...props}\n >\n {stars.map((star) => (\n <Star key={star} fill={value - (star - 1)} size={size} />\n ))}\n </div>\n );\n }\n\n function commit(next: number) {\n if (valueProp === undefined) setUncontrolled(next);\n onValueChange?.(next);\n }\n\n const shown = hovered ?? value;\n\n return (\n <div\n role=\"radiogroup\"\n data-slot=\"rating\"\n data-disabled={disabled ? \"\" : undefined}\n aria-disabled={disabled || undefined}\n className={cn(\"inline-flex items-center gap-0.5\", disabled && \"opacity-50\", className)}\n onMouseLeave={(event) => {\n onMouseLeave?.(event);\n setHovered(null);\n }}\n {...props}\n >\n {stars.map((star) => (\n <label\n key={star}\n data-slot=\"rating-item\"\n data-state={star <= value ? \"on\" : \"off\"}\n onMouseEnter={() => {\n if (!disabled) setHovered(star);\n }}\n className={cn(\n \"relative inline-flex p-0.5 transition-transform duration-150\",\n \"has-[input:focus-visible]:ring-2 has-[input:focus-visible]:ring-focus-ring\",\n disabled ? \"cursor-not-allowed\" : \"cursor-pointer hover:scale-110 motion-reduce:hover:scale-100\",\n )}\n >\n <input\n type=\"radio\"\n className=\"sr-only\"\n name={name ?? generatedName}\n value={star}\n checked={value === star}\n disabled={disabled}\n onChange={() => commit(star)}\n onClick={() => {\n if (clearable && value === star) commit(0);\n }}\n />\n <Star fill={star <= shown ? 1 : 0} size={size} />\n <span className=\"sr-only\">{getItemLabel(star, max)}</span>\n </label>\n ))}\n </div>\n );\n}\n\nexport { Rating };\n",
|
|
28
|
+
"type": "registry:ui",
|
|
29
|
+
"target": "components/ui/rating.tsx"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "reading-progress",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Reading Progress",
|
|
6
|
+
"description": "A decorative bar that fills as the reader moves through an article.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://abackui.com/r/utils.json"
|
|
9
|
+
],
|
|
10
|
+
"categories": [
|
|
11
|
+
"Feedback"
|
|
12
|
+
],
|
|
13
|
+
"docs": "https://abackui.com/docs/components/reading-progress",
|
|
14
|
+
"meta": {
|
|
15
|
+
"status": "beta",
|
|
16
|
+
"usedIn": [
|
|
17
|
+
"aback-tools",
|
|
18
|
+
"aback-launch"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
{
|
|
23
|
+
"path": "components/ui/reading-progress.tsx",
|
|
24
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface ReadingProgressProps extends Omit<React.ComponentProps<\"div\">, \"children\"> {\n /**\n * The element whose length is being read, such as the article. Without it,\n * progress follows the whole document.\n */\n target?: React.RefObject<HTMLElement | null>;\n}\n\nfunction measure(target?: HTMLElement | null) {\n if (!target) {\n const root = document.documentElement;\n const scrollable = root.scrollHeight - root.clientHeight;\n return scrollable > 0 ? root.scrollTop / scrollable : 0;\n }\n const rect = target.getBoundingClientRect();\n const scrollable = rect.height - window.innerHeight;\n if (scrollable <= 0) return rect.top <= 0 ? 1 : 0;\n return -rect.top / scrollable;\n}\n\n/**\n * A thin bar along the top of the viewport that fills as the reader scrolls.\n * It is decorative and hidden from assistive technology. Updates run once per\n * frame and write a transform directly, so scrolling never re-renders React.\n */\nfunction ReadingProgress({ target, className, ...props }: ReadingProgressProps) {\n const bar = React.useRef<HTMLDivElement>(null);\n\n React.useEffect(() => {\n let frame = 0;\n let pending = false;\n function update() {\n const progress = Math.min(Math.max(measure(target?.current), 0), 1);\n if (bar.current) bar.current.style.transform = `scaleX(${progress})`;\n }\n function schedule() {\n if (pending) return;\n pending = true;\n frame = window.requestAnimationFrame(() => {\n pending = false;\n update();\n });\n }\n\n update();\n window.addEventListener(\"scroll\", schedule, { passive: true });\n window.addEventListener(\"resize\", schedule);\n return () => {\n window.removeEventListener(\"scroll\", schedule);\n window.removeEventListener(\"resize\", schedule);\n if (pending) window.cancelAnimationFrame(frame);\n };\n }, [target]);\n\n return (\n <div\n aria-hidden=\"true\"\n data-slot=\"reading-progress\"\n className={cn(\"pointer-events-none fixed inset-x-0 top-0 z-[100] h-[3px] print:hidden\", className)}\n {...props}\n >\n <div\n ref={bar}\n data-slot=\"reading-progress-bar\"\n className=\"h-full origin-left bg-linear-to-r from-primary-400 to-primary-600\"\n style={{ transform: \"scaleX(0)\" }}\n />\n </div>\n );\n}\n\nexport { ReadingProgress };\n",
|
|
25
|
+
"type": "registry:ui",
|
|
26
|
+
"target": "components/ui/reading-progress.tsx"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "relative-time",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Relative Time",
|
|
6
|
+
"description": "A hydration-safe \"3 hours ago\" timestamp, localized by Intl and refreshed on one shared clock.",
|
|
7
|
+
"registryDependencies": [
|
|
8
|
+
"https://abackui.com/r/utils.json"
|
|
9
|
+
],
|
|
10
|
+
"categories": [
|
|
11
|
+
"Data Display"
|
|
12
|
+
],
|
|
13
|
+
"docs": "https://abackui.com/docs/components/relative-time",
|
|
14
|
+
"meta": {
|
|
15
|
+
"status": "beta",
|
|
16
|
+
"usedIn": [
|
|
17
|
+
"aback-tools",
|
|
18
|
+
"aback-launch"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
{
|
|
23
|
+
"path": "components/ui/relative-time.tsx",
|
|
24
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\n\nconst UNITS: ReadonlyArray<[Intl.RelativeTimeFormatUnit, number]> = [\n [\"year\", 31_536_000],\n [\"month\", 2_592_000],\n [\"week\", 604_800],\n [\"day\", 86_400],\n [\"hour\", 3_600],\n [\"minute\", 60],\n];\n\nfunction toDate(value: Date | string | number) {\n return value instanceof Date ? value : new Date(value);\n}\n\n/**\n * \"3 hours ago\", \"yesterday\", \"in 2 days\", localized by Intl. Differences\n * under 45 seconds read as \"now\". Counts are truncated, so 2 h 50 min is\n * \"2 hours ago\", never a rounded-up time that has not happened yet.\n */\nfunction formatRelativeTime(\n value: Date | string | number,\n now: number = Date.now(),\n locale?: string,\n) {\n const date = toDate(value);\n const seconds = (date.getTime() - now) / 1000;\n const format = new Intl.RelativeTimeFormat(locale, { numeric: \"auto\" });\n if (Math.abs(seconds) < 45) return format.format(0, \"second\");\n for (const [unit, size] of UNITS) {\n if (Math.abs(seconds) >= size) return format.format(Math.trunc(seconds / size), unit);\n }\n return format.format(Math.trunc(seconds / 60) || (seconds < 0 ? -1 : 1), \"minute\");\n}\n\ninterface Clock {\n now: number;\n listeners: Set<() => void>;\n timer?: ReturnType<typeof setInterval>;\n}\n\n// One shared clock per refresh interval, so a list of a hundred timestamps\n// re-renders on one timer instead of a hundred. Nothing runs until a\n// component subscribes, and the timer stops with the last subscriber.\nconst clocks = new Map<number, Clock>();\n\nfunction clockFor(interval: number) {\n let clock = clocks.get(interval);\n if (!clock) {\n clock = { now: Date.now(), listeners: new Set() };\n clocks.set(interval, clock);\n }\n return clock;\n}\n\nfunction subscribeClock(interval: number, listener: () => void) {\n const clock = clockFor(interval);\n clock.listeners.add(listener);\n if (!clock.timer && interval > 0) {\n clock.timer = setInterval(() => {\n clock.now = Date.now();\n clock.listeners.forEach((notify) => notify());\n }, interval);\n }\n return () => {\n clock.listeners.delete(listener);\n if (clock.listeners.size === 0) {\n if (clock.timer) clearInterval(clock.timer);\n clocks.delete(interval);\n }\n };\n}\n\nexport interface RelativeTimeProps\n extends Omit<React.ComponentProps<\"time\">, \"dateTime\" | \"children\"> {\n /** The moment to describe. Accepts a Date, an ISO string, or a timestamp. */\n date: Date | string | number;\n /** BCP 47 locale. Defaults to the browser's language. */\n locale?: string;\n /** How often the text updates, in milliseconds. 0 renders once. */\n refreshInterval?: number;\n /** Format of the server render and the hover title. */\n absoluteFormat?: Intl.DateTimeFormatOptions;\n}\n\nconst DEFAULT_ABSOLUTE: Intl.DateTimeFormatOptions = {\n dateStyle: \"medium\",\n timeStyle: \"short\",\n timeZone: \"UTC\",\n};\n\n/**\n * A relative timestamp that is safe on cached and server-rendered pages. The\n * server and the first client render show the absolute date, so hydration\n * matches; the relative phrase replaces it after mount. The absolute date\n * stays available as the title and in the machine-readable `dateTime`.\n */\nfunction RelativeTime({\n date,\n locale,\n refreshInterval = 60_000,\n absoluteFormat = DEFAULT_ABSOLUTE,\n title,\n ...props\n}: RelativeTimeProps) {\n const subscribe = React.useCallback(\n (listener: () => void) => subscribeClock(refreshInterval, listener),\n [refreshInterval],\n );\n const now = React.useSyncExternalStore<number | null>(\n subscribe,\n () => clockFor(refreshInterval).now,\n () => null,\n );\n\n const value = toDate(date);\n if (Number.isNaN(value.getTime())) return null;\n\n const absolute = new Intl.DateTimeFormat(locale ?? \"en-US\", absoluteFormat).format(value);\n\n return (\n <time\n data-slot=\"relative-time\"\n dateTime={value.toISOString()}\n title={title ?? absolute}\n {...props}\n >\n {now === null ? absolute : formatRelativeTime(value, now, locale)}\n </time>\n );\n}\n\nexport { RelativeTime, formatRelativeTime };\n",
|
|
25
|
+
"type": "registry:ui",
|
|
26
|
+
"target": "components/ui/relative-time.tsx"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://abackui.com/schema/registry-item.json",
|
|
3
|
+
"name": "scroll-area",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Scroll Area",
|
|
6
|
+
"description": "A focusable, themed scroll container for long lists and panels.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"radix-ui"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://abackui.com/r/utils.json"
|
|
12
|
+
],
|
|
13
|
+
"categories": [
|
|
14
|
+
"Layout"
|
|
15
|
+
],
|
|
16
|
+
"docs": "https://abackui.com/docs/components/scroll-area",
|
|
17
|
+
"meta": {
|
|
18
|
+
"status": "beta"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
{
|
|
22
|
+
"path": "components/ui/scroll-area.tsx",
|
|
23
|
+
"content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ScrollArea as ScrollAreaPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype ScrollbarVisibility = NonNullable<\n React.ComponentProps<typeof ScrollAreaPrimitive.Root>[\"type\"]\n>;\n\nexport interface ScrollAreaProps\n extends React.ComponentProps<typeof ScrollAreaPrimitive.Root> {\n /** When the custom scrollbar is visible. Defaults to Radix's hover behavior. */\n scrollbarVisibility?: ScrollbarVisibility;\n}\n\nfunction ScrollArea({\n className,\n children,\n scrollbarVisibility = \"hover\",\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledBy,\n ...props\n}: ScrollAreaProps) {\n return (\n <ScrollAreaPrimitive.Root\n data-slot=\"scroll-area\"\n type={scrollbarVisibility}\n className={cn(\"relative overflow-hidden\", className)}\n {...props}\n >\n <ScrollAreaPrimitive.Viewport\n data-slot=\"scroll-area-viewport\"\n tabIndex={0}\n role={ariaLabel || ariaLabelledBy ? \"region\" : undefined}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n className=\"size-full outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-focus-ring\"\n >\n {children}\n </ScrollAreaPrimitive.Viewport>\n <ScrollAreaPrimitive.Scrollbar\n data-slot=\"scroll-area-scrollbar\"\n orientation=\"vertical\"\n className=\"flex touch-none select-none border-l border-transparent p-px transition-colors\"\n >\n <ScrollAreaPrimitive.Thumb\n data-slot=\"scroll-area-thumb\"\n className=\"relative flex-1 bg-scrollbar-thumb hover:bg-scrollbar-thumb-hover\"\n />\n </ScrollAreaPrimitive.Scrollbar>\n <ScrollAreaPrimitive.Corner />\n </ScrollAreaPrimitive.Root>\n );\n}\n\nexport { ScrollArea };\n",
|
|
24
|
+
"type": "registry:ui",
|
|
25
|
+
"target": "components/ui/scroll-area.tsx"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|