From 9226becb2aed1f4c9c141e5f7fad4521cca2d77a Mon Sep 17 00:00:00 2001 From: Maciej Pyrc Date: Thu, 1 Oct 2026 13:45:09 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20button=20group?= =?UTF-8?q?,=20empty,=20input=20group,=20item,=20kbd,=20native=20select=20?= =?UTF-8?q?and=20spinner=20primitives,=20size=20the=20select=20trigger=20l?= =?UTF-8?q?ike=20inputs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- apps/admin/src/components/ui/button-group.tsx | 81 +++++++++ apps/admin/src/components/ui/empty.tsx | 86 ++++++++++ apps/admin/src/components/ui/input-group.tsx | 156 +++++++++++++++++ apps/admin/src/components/ui/item.tsx | 158 ++++++++++++++++++ apps/admin/src/components/ui/kbd.tsx | 24 +++ .../admin/src/components/ui/native-select.tsx | 92 ++++++++++ apps/admin/src/components/ui/select.tsx | 26 ++- apps/admin/src/components/ui/spinner.tsx | 10 ++ .../src/components/ui/button-group.tsx | 81 +++++++++ apps/extension/src/components/ui/empty.tsx | 86 ++++++++++ .../src/components/ui/input-group.tsx | 156 +++++++++++++++++ apps/extension/src/components/ui/item.tsx | 158 ++++++++++++++++++ apps/extension/src/components/ui/kbd.tsx | 24 +++ .../src/components/ui/native-select.tsx | 92 ++++++++++ apps/extension/src/components/ui/select.tsx | 26 ++- apps/extension/src/components/ui/spinner.tsx | 10 ++ .../src/components/ui/button-group.tsx | 81 +++++++++ apps/frontend/src/components/ui/empty.tsx | 86 ++++++++++ .../src/components/ui/input-group.tsx | 156 +++++++++++++++++ apps/frontend/src/components/ui/item.tsx | 158 ++++++++++++++++++ apps/frontend/src/components/ui/kbd.tsx | 24 +++ .../src/components/ui/native-select.tsx | 92 ++++++++++ apps/frontend/src/components/ui/select.tsx | 26 ++- apps/frontend/src/components/ui/spinner.tsx | 10 ++ 24 files changed, 1887 insertions(+), 12 deletions(-) create mode 100644 apps/admin/src/components/ui/button-group.tsx create mode 100644 apps/admin/src/components/ui/empty.tsx create mode 100644 apps/admin/src/components/ui/input-group.tsx create mode 100644 apps/admin/src/components/ui/item.tsx create mode 100644 apps/admin/src/components/ui/kbd.tsx create mode 100644 apps/admin/src/components/ui/native-select.tsx create mode 100644 apps/admin/src/components/ui/spinner.tsx create mode 100644 apps/extension/src/components/ui/button-group.tsx create mode 100644 apps/extension/src/components/ui/empty.tsx create mode 100644 apps/extension/src/components/ui/input-group.tsx create mode 100644 apps/extension/src/components/ui/item.tsx create mode 100644 apps/extension/src/components/ui/kbd.tsx create mode 100644 apps/extension/src/components/ui/native-select.tsx create mode 100644 apps/extension/src/components/ui/spinner.tsx create mode 100644 apps/frontend/src/components/ui/button-group.tsx create mode 100644 apps/frontend/src/components/ui/empty.tsx create mode 100644 apps/frontend/src/components/ui/input-group.tsx create mode 100644 apps/frontend/src/components/ui/item.tsx create mode 100644 apps/frontend/src/components/ui/kbd.tsx create mode 100644 apps/frontend/src/components/ui/native-select.tsx create mode 100644 apps/frontend/src/components/ui/spinner.tsx diff --git a/apps/admin/src/components/ui/button-group.tsx b/apps/admin/src/components/ui/button-group.tsx new file mode 100644 index 0000000..03b3380 --- /dev/null +++ b/apps/admin/src/components/ui/button-group.tsx @@ -0,0 +1,81 @@ +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; + +import { Separator } from "@/components/ui/separator"; +import { cn } from "@/lib/utils"; + +/** + * The template's default SelectTrigger is h-10 to match Input, while the default Button + * is h-9. Inside a group they must share a height, so default-size triggers are pulled + * down to the button height here. + */ +const buttonGroupVariants = cva( + "flex w-fit items-stretch [&>[data-slot=select-trigger][data-size=default]]:h-9 has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1", + { + variants: { + orientation: { + horizontal: + "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none", + vertical: + "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none", + }, + }, + defaultVariants: { + orientation: "horizontal", + }, + }, +); + +function ButtonGroup({ + className, + orientation, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ); +} + +function ButtonGroupText({ + className, + asChild = false, + ...props +}: React.ComponentProps<"div"> & { + asChild?: boolean; +}) { + const Comp = asChild ? Slot : "div"; + + return ( + + ); +} + +function ButtonGroupSeparator({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { ButtonGroup, ButtonGroupSeparator, ButtonGroupText, buttonGroupVariants }; diff --git a/apps/admin/src/components/ui/empty.tsx b/apps/admin/src/components/ui/empty.tsx new file mode 100644 index 0000000..0a8b73c --- /dev/null +++ b/apps/admin/src/components/ui/empty.tsx @@ -0,0 +1,86 @@ +import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Empty({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +const emptyMediaVariants = cva( + "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-transparent", + icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +function EmptyMedia({ + className, + variant = "default", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ); +} + +function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) { + return
; +} + +function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) { + return ( +

a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary", + className, + )} + {...props} + /> + ); +} + +function EmptyContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +

+ ); +} + +export { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle }; diff --git a/apps/admin/src/components/ui/input-group.tsx b/apps/admin/src/components/ui/input-group.tsx new file mode 100644 index 0000000..51b5f47 --- /dev/null +++ b/apps/admin/src/components/ui/input-group.tsx @@ -0,0 +1,156 @@ +import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { cn } from "@/lib/utils"; + +/** + * Ported from shadcn. The template's Input wraps its `` in a div, so the + * selectors below target descendant inputs (`[&_input]`) instead of direct children, + * and InputGroupInput styles the control through `inputClassName`. + */ +function InputGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
textarea]:h-auto", + + // Variants based on alignment. + "has-[>[data-align=inline-start]]:[&_input]:pl-2", + "has-[>[data-align=inline-end]]:[&_input]:pr-2", + "has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&_input]:pb-3", + "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&_input]:pt-3", + + // Focus state. + "has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]", + + // Error state. + "has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40", + + className, + )} + {...props} + /> + ); +} + +const inputGroupAddonVariants = cva( + "text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4", + { + variants: { + align: { + "inline-start": "order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]", + "inline-end": "order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]", + "block-start": + "order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-3", + "block-end": "order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-3", + }, + }, + defaultVariants: { + align: "inline-start", + }, + }, +); + +function InputGroupAddon({ + className, + align = "inline-start", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + // The addon behaves like a label: clicking it moves focus into the control. Keyboard + // users reach the control directly with Tab, so a key handler here would add nothing. + return ( + // oxlint-disable-next-line jsx-a11y/click-events-have-key-events +
{ + if ((e.target as HTMLElement).closest("button")) { + return; + } + e.currentTarget.parentElement?.querySelector("input")?.focus(); + }} + {...props} + /> + ); +} + +const inputGroupButtonVariants = cva("flex items-center gap-2 text-sm shadow-none", { + variants: { + size: { + xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5", + sm: "h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5", + "icon-xs": "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0", + "icon-sm": "size-8 p-0 has-[>svg]:p-0", + }, + }, + defaultVariants: { + size: "xs", + }, +}); + +function InputGroupButton({ + className, + type = "button", + variant = "ghost", + size = "xs", + ...props +}: Omit, "size"> & VariantProps) { + return ( +