@goldencomm/baseline-registry 0.0.0-stage → 0.1.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/README.md +26 -2
- package/package.json +15 -4
- package/r/accordion-contained.json +44 -0
- package/r/accordion-split.json +44 -0
- package/r/accordion.json +43 -0
- package/r/alert.json +27 -0
- package/r/aspect-ratio.json +24 -0
- package/r/avatar.json +27 -0
- package/r/badge.json +28 -0
- package/r/base.json +1058 -0
- package/r/blog-card-grid.json +30 -0
- package/r/breadcrumb.json +28 -0
- package/r/button-group.json +28 -0
- package/r/button.json +28 -0
- package/r/card.json +24 -0
- package/r/carousel.json +29 -0
- package/r/checkbox.json +25 -0
- package/r/columns.json +24 -0
- package/r/contact-centered.json +32 -0
- package/r/container.json +27 -0
- package/r/cta-left.json +28 -0
- package/r/dialog.json +31 -0
- package/r/drawer.json +27 -0
- package/r/dropdown-menu.json +28 -0
- package/r/faq-centered.json +28 -0
- package/r/field.json +29 -0
- package/r/header-inline.json +33 -0
- package/r/header-stacked.json +34 -0
- package/r/hero-centered.json +28 -0
- package/r/hero-split.json +29 -0
- package/r/hover-card.json +29 -0
- package/r/icon.json +30 -0
- package/r/input-group.json +30 -0
- package/r/input-otp.json +28 -0
- package/r/input.json +27 -0
- package/r/interest.json +15 -0
- package/r/item.json +29 -0
- package/r/kbd.json +24 -0
- package/r/label.json +24 -0
- package/r/media.json +33 -0
- package/r/meter.json +24 -0
- package/r/native-select.json +25 -0
- package/r/navigation-menu.json +30 -0
- package/r/pagination-bordered.json +27 -0
- package/r/pagination-grouped.json +27 -0
- package/r/pagination.json +26 -0
- package/r/panel.json +27 -0
- package/r/popover.json +28 -0
- package/r/progress.json +34 -0
- package/r/radio.json +24 -0
- package/r/registry.json +2770 -0
- package/r/section.json +27 -0
- package/r/select.json +28 -0
- package/r/separator.json +27 -0
- package/r/sheet.json +27 -0
- package/r/sidebar.json +34 -0
- package/r/spinner.json +25 -0
- package/r/switch.json +27 -0
- package/r/table.json +24 -0
- package/r/tabs-line.json +28 -0
- package/r/tabs.json +27 -0
- package/r/text-styles.json +16 -0
- package/r/textarea.json +24 -0
- package/r/toggle-group.json +52 -0
- package/r/toggle.json +27 -0
- package/r/tooltip.json +62 -0
- package/r/typeset.json +16 -0
- package/r/utils.json +18 -0
package/r/section.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "section",
|
|
4
|
+
"title": "Section",
|
|
5
|
+
"description": "Baseline section (draft). Spec: components/section/spec.md",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"class-variance-authority"
|
|
8
|
+
],
|
|
9
|
+
"registryDependencies": [
|
|
10
|
+
"@baseline/utils"
|
|
11
|
+
],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "src/ui/section/section.tsx",
|
|
15
|
+
"content": "// baseline:section@0.2.0\n// Baseline layout component (no shadcn counterpart). Spec: components/section/spec.md.\n// A full-width band: the colour scheme, the surface (Figma's Surface mode on the Section) and the block padding.\n// Padding and gap are steps of the Responsive spacing scale (spacing/responsive/*), whose values already change at md,\n// lg and xl, so no breakpoint classes are needed.\n//\n// Ladder step 1: a plain element. No state: server-safe.\nimport * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/utils\";\n\nconst sectionVariants = cva(\"flex flex-col\", {\n variants: {\n // Figma's Surface modes a band can take: the fill and text pair each mode resolves to (Surface is Figma-only).\n surface: {\n background: \"bg-background text-foreground\",\n muted: \"bg-muted text-muted-foreground\",\n card: \"bg-card text-card-foreground\",\n primary: \"bg-primary text-primary-foreground\",\n secondary: \"bg-secondary text-secondary-foreground\",\n accent: \"bg-accent text-accent-foreground\",\n inverse: \"bg-foreground text-background\",\n },\n paddingTop: {\n none: \"pt-0\",\n xs: \"pt-(--bl-spacing-responsive-xs)\",\n sm: \"pt-(--bl-spacing-responsive-sm)\",\n md: \"pt-(--bl-spacing-responsive-md)\",\n lg: \"pt-(--bl-spacing-responsive-lg)\",\n xl: \"pt-(--bl-spacing-responsive-xl)\",\n \"2xl\": \"pt-(--bl-spacing-responsive-2xl)\",\n },\n paddingBottom: {\n none: \"pb-0\",\n xs: \"pb-(--bl-spacing-responsive-xs)\",\n sm: \"pb-(--bl-spacing-responsive-sm)\",\n md: \"pb-(--bl-spacing-responsive-md)\",\n lg: \"pb-(--bl-spacing-responsive-lg)\",\n xl: \"pb-(--bl-spacing-responsive-xl)\",\n \"2xl\": \"pb-(--bl-spacing-responsive-2xl)\",\n },\n gap: {\n none: \"gap-0\",\n xs: \"gap-(--bl-spacing-responsive-xs)\",\n sm: \"gap-(--bl-spacing-responsive-sm)\",\n md: \"gap-(--bl-spacing-responsive-md)\",\n lg: \"gap-(--bl-spacing-responsive-lg)\",\n xl: \"gap-(--bl-spacing-responsive-xl)\",\n \"2xl\": \"gap-(--bl-spacing-responsive-2xl)\",\n },\n },\n defaultVariants: { surface: \"background\", paddingTop: \"lg\", paddingBottom: \"lg\", gap: \"sm\" },\n});\n\ntype Scheme = \"scheme-1\" | \"scheme-2\" | \"error\" | \"success\" | \"warning\";\n\n/**\n * A full-width band of a page holding one or more Containers. `scheme` sets the colour scheme for the band (and\n * everything in it); `surface` picks the role pair it paints within that scheme (`muted` for an announcement bar);\n * `paddingTop` / `paddingBottom` / `gap` take a step of the responsive spacing scale or `none`.\n * An unnamed <section> is not a landmark: give it `aria-labelledby` its heading when it should be one.\n */\nfunction Section({\n as = \"section\",\n scheme,\n surface,\n paddingTop,\n paddingBottom,\n gap,\n className,\n ...props\n}: React.ComponentProps<\"section\"> &\n VariantProps<typeof sectionVariants> & {\n as?: \"section\" | \"article\" | \"header\" | \"footer\" | \"div\";\n scheme?: Scheme;\n }) {\n // every allowed tag is an HTMLElement; the cast drops the div-vs-section ref mismatch TypeScript sees in the union\n const Comp = as as React.ElementType;\n return (\n <Comp\n data-slot=\"section\"\n data-scheme={scheme}\n className={cn(sectionVariants({ surface, paddingTop, paddingBottom, gap }), className)}\n {...props}\n />\n );\n}\n\nexport { Section, sectionVariants };\n",
|
|
16
|
+
"type": "registry:ui",
|
|
17
|
+
"target": "components/ui/section.tsx"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"meta": {
|
|
21
|
+
"spec": "components/section/spec.md",
|
|
22
|
+
"figma": "2253:17828",
|
|
23
|
+
"upstream": "none (a Baseline layout component; shadcn has no counterpart)",
|
|
24
|
+
"status": "draft"
|
|
25
|
+
},
|
|
26
|
+
"type": "registry:ui"
|
|
27
|
+
}
|
package/r/select.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "select",
|
|
4
|
+
"title": "Select",
|
|
5
|
+
"description": "Baseline select (draft). Spec: components/select/spec.md",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"@base-ui/react"
|
|
8
|
+
],
|
|
9
|
+
"registryDependencies": [
|
|
10
|
+
"@baseline/utils",
|
|
11
|
+
"@baseline/icon"
|
|
12
|
+
],
|
|
13
|
+
"files": [
|
|
14
|
+
{
|
|
15
|
+
"path": "src/ui/select/select.tsx",
|
|
16
|
+
"content": "\"use client\";\n// baseline:select@0.1.0\n// Fork of shadcn/ui Select (base-ui flavour, source read 2026-09-30). Spec: components/select/spec.md.\n// Geometry comes from Figma through the select/* tokens (ADR-0017).\n//\n// THIS IS THE ONE ADR-0030 EXCEPTION. That ADR allows Base UI only where the primitive renders the element a\n// native implementation would, and Base UI Select does not: it is a <button> plus a portalled listbox, not a\n// <select>. It is allowed here because the browser's option list cannot be styled at all, and the designed\n// panel (Select Panel 2026:24802 — grouped rows, group labels, separators, a check on the selected row,\n// scroll arrows) is not expressible on a native control at any price.\n//\n// The exception is scoped, not open-ended: the trigger is the same box either way, off the same select/*\n// variables, and NativeSelect (components/native-select/spec.md) remains the component on WordPress, Shopify\n// and HTML, and the one to reach for first in React. When customizable <select> lands in browsers this is a\n// different conversation -- we reassess dropping the Base UI version and bringing the panel's styling to\n// Shopify and WordPress.\n//\n// 'use client' sits here rather than at a smaller leaf because every part of the composition reads Base UI's\n// Select context; there is no server-safe subset to carve out.\nimport * as React from \"react\";\nimport { Select as SelectPrimitive } from \"@base-ui/react/select\";\nimport { cn } from \"@/lib/utils\";\nimport { Icon } from \"@/registry/ui/icon/icon\";\n\nconst Select = SelectPrimitive.Root;\n\nfunction SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {\n return (\n <SelectPrimitive.Group\n data-slot=\"select-group\"\n // No padding here: it is on the Popup instead, so an UNgrouped panel is inset the same as a grouped one.\n // See SelectContent for why that matters to alignItemWithTrigger.\n className={cn(\"scroll-my-1\", className)}\n {...props}\n />\n );\n}\n\nfunction SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {\n return (\n <SelectPrimitive.Value\n data-slot=\"select-value\"\n className={cn(\"flex flex-1 text-left\", className)}\n {...props}\n />\n );\n}\n\n/**\n * The trigger. Deliberately no `size` prop: upstream offers `sm | default`, the Figma set has no Size axis and\n * only `select/md/*` exists, so Baseline ships one size — the same call Input and Textarea made.\n */\nfunction SelectTrigger({ className, children, ...props }: SelectPrimitive.Trigger.Props) {\n return (\n <SelectPrimitive.Trigger\n data-slot=\"select-trigger\"\n className={cn(\n [\n \"flex w-full items-center justify-between whitespace-nowrap select-none\",\n \"gap-(--bl-select-md-gap) h-(--bl-select-md-height)\",\n \"rounded-(--bl-select-md-radius) border-(length:--bl-select-md-border-width) border-input\",\n \"px-(--bl-select-md-padding-x) py-(--bl-select-md-padding-y)\",\n \"bg-transparent text-foreground outline-none transition-colors\",\n \"text-base md:text-(length:--bl-select-md-font-size) md:leading-(--bl-select-md-line-height)\",\n \"data-placeholder:text-muted-foreground\",\n \"focus-visible:border-primary focus-visible:ring-(length:--bl-control-ring-width) focus-visible:ring-ring\",\n // invalid is set here, never inherited from an invalid Field, so the trigger looks the same\n // standalone as it does inside one — the call Input made and NativeSelect repeats\n \"aria-invalid:border-destructive aria-invalid:text-destructive\",\n \"aria-invalid:ring-(length:--bl-control-ring-width) aria-invalid:ring-(color:--ring-invalid)\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n \"*:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex\",\n \"*:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-(--bl-select-md-gap)\",\n ].join(\" \"),\n className,\n )}\n {...props}\n >\n {children}\n {/* Baseline's Icon, not lucide: the glyph has to follow the active icon set (PLAN §4.9). */}\n <SelectPrimitive.Icon\n render={\n <Icon\n name=\"chevron-down\"\n className=\"pointer-events-none shrink-0 size-(--bl-select-md-icon-size) text-muted-foreground\"\n />\n }\n />\n </SelectPrimitive.Trigger>\n );\n}\n\nfunction SelectContent({\n className,\n children,\n side = \"bottom\",\n sideOffset = 4,\n align = \"center\",\n alignOffset = 0,\n alignItemWithTrigger = true,\n ...props\n}: SelectPrimitive.Popup.Props &\n Pick<\n SelectPrimitive.Positioner.Props,\n \"align\" | \"alignOffset\" | \"side\" | \"sideOffset\" | \"alignItemWithTrigger\"\n >) {\n return (\n <SelectPrimitive.Portal>\n <SelectPrimitive.Positioner\n side={side}\n sideOffset={sideOffset}\n align={align}\n alignOffset={alignOffset}\n alignItemWithTrigger={alignItemWithTrigger}\n className=\"isolate z-50\"\n >\n <SelectPrimitive.Popup\n data-slot=\"select-content\"\n data-align-trigger={alignItemWithTrigger}\n className={cn(\n [\n \"relative isolate z-50 origin-(--transform-origin) overflow-x-hidden overflow-y-auto\",\n \"max-h-(--available-height) w-(--anchor-width) min-w-36\",\n // The panel carries the inset, and it has its own variable now rather than borrowing the\n // group's. alignItemWithTrigger positions the panel so the SELECTED ITEM'S TEXT sits over the\n // trigger's text, so the two insets must match:\n // trigger = border 1 + select/md/padding-x 10 = 11px\n // panel = border 1 + select/panel/padding 4 + item-x 6 = 11px\n // tokens/build.mjs asserts that equality, so changing any of the three fails the build rather\n // than drifting a few pixels that only show up as a panel sitting slightly off its trigger.\n \"p-(--bl-select-panel-padding)\",\n // the panel binds color/popover, not the page background (Core batch, 2026-09-28)\n \"bg-popover text-popover-foreground shadow-md\",\n \"rounded-(--bl-select-panel-radius)\",\n \"border-(length:--bl-select-panel-border-width) border-border\",\n \"duration-100 data-[align-trigger=true]:animate-none\",\n \"data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2\",\n \"data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2\",\n \"data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2\",\n \"data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95\",\n \"data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95\",\n ].join(\" \"),\n className,\n )}\n {...props}\n >\n <SelectScrollUpButton />\n <SelectPrimitive.List>{children}</SelectPrimitive.List>\n <SelectScrollDownButton />\n </SelectPrimitive.Popup>\n </SelectPrimitive.Positioner>\n </SelectPrimitive.Portal>\n );\n}\n\nfunction SelectLabel({ className, ...props }: SelectPrimitive.GroupLabel.Props) {\n return (\n <SelectPrimitive.GroupLabel\n data-slot=\"select-label\"\n className={cn(\n \"text-muted-foreground\",\n \"px-(--bl-select-label-padding-x) py-(--bl-select-label-padding-y)\",\n \"text-(length:--bl-select-label-font-size) leading-(--bl-select-label-line-height)\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction SelectItem({ className, children, ...props }: SelectPrimitive.Item.Props) {\n return (\n <SelectPrimitive.Item\n data-slot=\"select-item\"\n className={cn(\n [\n \"relative flex w-full cursor-default items-center outline-hidden select-none\",\n \"gap-(--bl-select-item-gap) rounded-(--bl-select-item-radius)\",\n \"ps-(--bl-select-item-padding-x) py-(--bl-select-item-padding-y)\",\n // room on the inline end for the check, which is absolutely positioned\n \"pe-[calc(var(--bl-select-item-padding-x)+var(--bl-select-item-icon-size)+var(--bl-select-item-gap))]\",\n \"text-(length:--bl-select-item-font-size) leading-(--bl-select-item-line-height)\",\n \"focus:bg-accent focus:text-accent-foreground\",\n \"data-disabled:pointer-events-none data-disabled:opacity-50\",\n \"*:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-(--bl-select-item-gap)\",\n ].join(\" \"),\n className,\n )}\n {...props}\n >\n <SelectPrimitive.ItemText className=\"flex flex-1 shrink-0 gap-(--bl-select-item-gap) whitespace-nowrap\">\n {children}\n </SelectPrimitive.ItemText>\n <SelectPrimitive.ItemIndicator\n render={\n <span className=\"pointer-events-none absolute end-(--bl-select-item-padding-x) flex size-(--bl-select-item-icon-size) items-center justify-center\">\n <Icon name=\"check\" className=\"pointer-events-none size-full\" />\n </span>\n }\n />\n </SelectPrimitive.Item>\n );\n}\n\nfunction SelectSeparator({ className, ...props }: SelectPrimitive.Separator.Props) {\n return (\n <SelectPrimitive.Separator\n data-slot=\"select-separator\"\n className={cn(\n // full-bleed against the panel's own padding\n \"pointer-events-none -mx-(--bl-select-panel-padding) my-1 h-(--bl-separator-thickness) bg-border\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction SelectScrollUpButton({\n className,\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) {\n return (\n <SelectPrimitive.ScrollUpArrow\n data-slot=\"select-scroll-up-button\"\n className={cn(\n \"top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover\",\n \"py-(--bl-select-item-padding-y)\",\n className,\n )}\n {...props}\n >\n <Icon name=\"chevron-up\" className=\"size-(--bl-select-item-icon-size)\" />\n </SelectPrimitive.ScrollUpArrow>\n );\n}\n\nfunction SelectScrollDownButton({\n className,\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) {\n return (\n <SelectPrimitive.ScrollDownArrow\n data-slot=\"select-scroll-down-button\"\n className={cn(\n \"bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover\",\n \"py-(--bl-select-item-padding-y)\",\n className,\n )}\n {...props}\n >\n <Icon name=\"chevron-down\" className=\"size-(--bl-select-item-icon-size)\" />\n </SelectPrimitive.ScrollDownArrow>\n );\n}\n\nexport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectLabel,\n SelectScrollDownButton,\n SelectScrollUpButton,\n SelectSeparator,\n SelectTrigger,\n SelectValue,\n};\n",
|
|
17
|
+
"type": "registry:ui",
|
|
18
|
+
"target": "components/ui/select.tsx"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"meta": {
|
|
22
|
+
"spec": "components/select/spec.md",
|
|
23
|
+
"figma": "2026:24802",
|
|
24
|
+
"upstream": "shadcn/ui select (base-ui flavour, source read from ui.shadcn.com 2026-09-25)",
|
|
25
|
+
"status": "draft"
|
|
26
|
+
},
|
|
27
|
+
"type": "registry:ui"
|
|
28
|
+
}
|
package/r/separator.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "separator",
|
|
4
|
+
"title": "Separator",
|
|
5
|
+
"description": "Baseline separator (ready). Spec: components/separator/spec.md",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"@base-ui/react"
|
|
8
|
+
],
|
|
9
|
+
"registryDependencies": [
|
|
10
|
+
"@baseline/utils"
|
|
11
|
+
],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "src/ui/separator/separator.tsx",
|
|
15
|
+
"content": "// baseline:separator@0.1.0\n// Fork of shadcn/ui Separator (base-ui flavour, source read 2026-09-25). Spec: components/separator/spec.md.\n// A separator is a filled hairline rather than a border, which is how Figma draws it too: the Field Separator\n// rectangle binds its height to separator/thickness and its fill to color/border.\nimport { Separator as SeparatorPrimitive } from \"@base-ui/react/separator\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * A rule between sections. Base UI gives it the right role and aria-orientation; decorative separators inside\n * a labelled group should be hidden from assistive tech by the parent instead.\n */\nfunction Separator({ className, orientation = \"horizontal\", ...props }: SeparatorPrimitive.Props) {\n return (\n <SeparatorPrimitive\n data-slot=\"separator\"\n orientation={orientation}\n className={cn(\n \"shrink-0 bg-border\",\n // `data-[orientation=…]`, not `data-horizontal`. Base UI renders data-orientation=\"horizontal\" with a\n // VALUE; Tailwind's bare `data-horizontal:` compiles to the attribute selector `[data-horizontal]`,\n // which matches nothing -- so every separator had height 0 and was invisible (found 2026-09-30).\n \"data-[orientation=horizontal]:h-(--bl-separator-thickness) data-[orientation=horizontal]:w-full\",\n \"data-[orientation=vertical]:w-(--bl-separator-thickness) data-[orientation=vertical]:self-stretch\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport { Separator };\n",
|
|
16
|
+
"type": "registry:ui",
|
|
17
|
+
"target": "components/ui/separator.tsx"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"meta": {
|
|
21
|
+
"spec": "components/separator/spec.md",
|
|
22
|
+
"figma": "4237:74",
|
|
23
|
+
"upstream": "shadcn/ui separator (base-ui flavour, source read from ui.shadcn.com 2026-09-25)",
|
|
24
|
+
"status": "ready"
|
|
25
|
+
},
|
|
26
|
+
"type": "registry:ui"
|
|
27
|
+
}
|
package/r/sheet.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "sheet",
|
|
4
|
+
"title": "Sheet",
|
|
5
|
+
"description": "Baseline sheet (ready). Spec: components/sheet/spec.md. Not shadcn's API (ADR-0033): no Sheet, SheetTrigger or SheetClose; give SheetContent an id and open it with <Button command=\"show-modal\" commandfor=\"<id>\">",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"@baseline/utils",
|
|
8
|
+
"@baseline/button",
|
|
9
|
+
"@baseline/dialog",
|
|
10
|
+
"@baseline/icon"
|
|
11
|
+
],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "src/ui/sheet/sheet.tsx",
|
|
15
|
+
"content": "// baseline:sheet@0.2.0\n// Fork of shadcn/ui Sheet (base-ui flavour, source read 2026-10-02). Spec: components/sheet/spec.md.\n// Geometry comes from Figma through the sheet/* tokens (ADR-0017); the backdrop is the color/overlay role (ADR-0032).\n//\n// Ladder step 1, as Dialog: upstream's Sheet is Base UI's Dialog restyled as an edge panel; this one is the native\n// <dialog> and invoker commands (ADR-0030), drawn as a panel on one side. A SERVER COMPONENT with explicit ids, as Dialog\n// (ADR-0033): no Sheet root, SheetTrigger or SheetClose; a Button with command=\"show-modal\" commandfor=\"<id>\" opens it\n// from anywhere on the page, one with command=\"close\" closes it.\n// <Button command=\"show-modal\" commandfor=\"cart\">Cart</Button>\n// <SheetContent id=\"cart\" side=\"right\">\n// <SheetHeader><SheetTitle id=\"cart-title\">Cart</SheetTitle></SheetHeader>\n// …\n// <SheetFooter><Button>Check out</Button></SheetFooter>\n// </SheetContent>\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/registry/ui/button/button\";\nimport { DialogDescription, DialogTitle } from \"@/registry/ui/dialog/dialog\";\nimport { Icon } from \"@/registry/ui/icon/icon\";\n\ntype SheetSide = \"top\" | \"right\" | \"bottom\" | \"left\";\n\n// The panel. The <dialog>'s UA box (centred, max-width/height, margin) is reset; each side pins it to its edge with a\n// border on the open side, and slides it 2.5rem in with the fade (upstream's 200ms ease-in-out, from @starting-style).\nconst contentBase = [\n \"group/sheet-content fixed m-0 max-h-none max-w-none p-0 hidden open:flex flex-col gap-(--bl-sheet-gap)\",\n \"border-0 border-border bg-popover bg-clip-padding text-sm text-popover-foreground shadow-lg outline-none\",\n \"opacity-0 open:opacity-100 open:translate-x-0 open:translate-y-0 starting:open:opacity-0\",\n \"transition-[opacity,translate,display,overlay] transition-discrete duration-200 ease-in-out motion-reduce:transition-none\",\n \"backdrop:bg-(--overlay) supports-backdrop-filter:backdrop:backdrop-blur-xs\",\n \"backdrop:opacity-0 open:backdrop:opacity-100 starting:open:backdrop:opacity-0\",\n \"backdrop:transition-[opacity,display,overlay] backdrop:transition-discrete backdrop:duration-150 motion-reduce:backdrop:transition-none\",\n].join(\" \");\n\nconst SIDES: Record<SheetSide, string> = {\n right:\n \"inset-y-0 right-0 left-auto h-full w-3/4 sm:max-w-(--bl-sheet-max-width) border-l-(length:--bl-sheet-border-width) translate-x-10 starting:open:translate-x-10\",\n left:\n \"inset-y-0 left-0 right-auto h-full w-3/4 sm:max-w-(--bl-sheet-max-width) border-r-(length:--bl-sheet-border-width) -translate-x-10 starting:open:-translate-x-10\",\n top: \"inset-x-0 top-0 bottom-auto h-auto w-full border-b-(length:--bl-sheet-border-width) -translate-y-10 starting:open:-translate-y-10\",\n bottom:\n \"inset-x-0 bottom-0 top-auto h-auto w-full border-t-(length:--bl-sheet-border-width) translate-y-10 starting:open:translate-y-10\",\n};\n\ntype SheetContentProps = Omit<React.ComponentProps<\"dialog\">, \"id\"> & {\n /** Required: the id invokers point at (commandfor). The title and description take `<id>-title` / `<id>-description`. */\n id: string;\n /** Upstream's physical sides; right by default. */\n side?: SheetSide;\n /** The corner ghost icon button (default true), a native invoker with command=\"close\". */\n showCloseButton?: boolean;\n};\n\n/** The panel, a modal <dialog>. `aria-labelledby` / `aria-describedby` default to `<id>-title` / `<id>-description`. */\nfunction SheetContent({ id, className, children, side = \"right\", showCloseButton = true, ...props }: SheetContentProps) {\n return (\n <dialog\n id={id}\n data-slot=\"sheet-content\"\n data-side={side}\n closedby=\"any\"\n aria-labelledby={props[\"aria-label\"] ? undefined : `${id}-title`}\n aria-describedby={`${id}-description`}\n className={cn(contentBase, SIDES[side], className)}\n {...props}\n >\n {children}\n {showCloseButton ? (\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n command=\"close\"\n commandfor={id}\n data-slot=\"sheet-close\"\n className=\"absolute top-(--bl-sheet-close-inset) right-(--bl-sheet-close-inset)\"\n >\n <Icon name=\"close\" />\n <span className=\"sr-only\">Close</span>\n </Button>\n ) : null}\n </dialog>\n );\n}\n\nfunction SheetHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"sheet-header\"\n className={cn(\"flex flex-col gap-(--bl-sheet-header-gap) p-(--bl-sheet-padding)\", className)}\n {...props}\n />\n );\n}\n\nfunction SheetFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"sheet-footer\"\n className={cn(\"mt-auto flex flex-col gap-(--bl-sheet-footer-gap) p-(--bl-sheet-padding)\", className)}\n {...props}\n />\n );\n}\n\n/** Give it `<sheet id>-title`: the sheet's accessible name points there. */\nfunction SheetTitle({ className, ...props }: React.ComponentProps<\"h2\">) {\n return (\n <DialogTitle\n data-slot=\"sheet-title\"\n className={cn(\n \"text-(length:--bl-sheet-title-font-size) leading-(--bl-sheet-title-line-height) font-(--bl-sheet-title-font-weight) text-foreground\",\n className,\n )}\n {...props}\n />\n );\n}\n\n/** Give it `<sheet id>-description`. */\nfunction SheetDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n return (\n <DialogDescription\n data-slot=\"sheet-description\"\n className={cn(\"text-(length:--bl-sheet-description-font-size) leading-(--bl-sheet-description-line-height)\", className)}\n {...props}\n />\n );\n}\n\nexport { SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle };\nexport type { SheetContentProps, SheetSide };\n",
|
|
16
|
+
"type": "registry:ui",
|
|
17
|
+
"target": "components/ui/sheet.tsx"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"meta": {
|
|
21
|
+
"spec": "components/sheet/spec.md",
|
|
22
|
+
"figma": "4467:82",
|
|
23
|
+
"upstream": "shadcn/ui sheet (base-ui flavour, source read from ui.shadcn.com 2026-10-02)",
|
|
24
|
+
"status": "ready"
|
|
25
|
+
},
|
|
26
|
+
"type": "registry:ui"
|
|
27
|
+
}
|
package/r/sidebar.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "sidebar",
|
|
4
|
+
"title": "Sidebar",
|
|
5
|
+
"description": "Baseline sidebar (ready). Spec: components/sidebar/spec.md. Upstream's parts: SidebarProvider (open state, a cookie, Ctrl/Cmd+B), Sidebar (collapsible offcanvas | icon | none; variant sidebar | floating | inset; side), the menu parts; Baseline's native Sheet on mobile and Tooltip on collapsed buttons",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"@base-ui/react",
|
|
8
|
+
"class-variance-authority"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"@baseline/utils",
|
|
12
|
+
"@baseline/button",
|
|
13
|
+
"@baseline/icon",
|
|
14
|
+
"@baseline/input",
|
|
15
|
+
"@baseline/separator",
|
|
16
|
+
"@baseline/sheet",
|
|
17
|
+
"@baseline/tooltip"
|
|
18
|
+
],
|
|
19
|
+
"files": [
|
|
20
|
+
{
|
|
21
|
+
"path": "src/ui/sidebar/sidebar.tsx",
|
|
22
|
+
"content": "\"use client\";\n// baseline:sidebar@0.1.0\n// Fork of shadcn/ui Sidebar (base-ui flavour, source read 2026-10-02, re-read 2026-10-06). Spec: components/sidebar/spec.md.\n// Upstream's parts and behaviour (SidebarProvider: open state, the sidebar_state cookie, Ctrl/Cmd+B; collapsible\n// offcanvas | icon | none; variant sidebar | floating | inset) on Baseline's natives: on mobile the sidebar is\n// Baseline's Sheet (a <dialog>, opened with showModal from the provider's state), and a collapsed menu button's label is\n// Baseline's Tooltip (interestfor). Geometry comes from Figma through the sidebar/* tokens (ADR-0017), where upstream\n// derives a size from another (the action's offsets from the button height) the calc says so; colours are upstream's\n// sidebar roles. --sidebar-width / --sidebar-width-icon stay upstream's override points, set from the tokens.\nimport * as React from \"react\";\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/registry/ui/button/button\";\nimport { Icon } from \"@/registry/ui/icon/icon\";\nimport { Input } from \"@/registry/ui/input/input\";\nimport { Separator } from \"@/registry/ui/separator/separator\";\nimport { SheetContent, SheetDescription, SheetHeader, SheetTitle } from \"@/registry/ui/sheet/sheet\";\nimport { TooltipContent, TooltipTrigger } from \"@/registry/ui/tooltip/tooltip\";\n\nconst SIDEBAR_COOKIE_NAME = \"sidebar_state\";\nconst SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;\nconst SIDEBAR_KEYBOARD_SHORTCUT = \"b\";\nconst MOBILE_QUERY = \"(max-width: 767px)\"; // upstream's useIsMobile: below md\n\n// upstream's use-mobile hook, inline (the registry has no hooks folder)\nfunction useIsMobile() {\n return React.useSyncExternalStore(\n (onChange) => {\n const mq = window.matchMedia(MOBILE_QUERY);\n mq.addEventListener(\"change\", onChange);\n return () => mq.removeEventListener(\"change\", onChange);\n },\n () => window.matchMedia(MOBILE_QUERY).matches,\n () => false,\n );\n}\n\ntype SidebarContextProps = {\n state: \"expanded\" | \"collapsed\";\n open: boolean;\n setOpen: (open: boolean) => void;\n openMobile: boolean;\n setOpenMobile: (open: boolean) => void;\n isMobile: boolean;\n toggleSidebar: () => void;\n};\n\nconst SidebarContext = React.createContext<SidebarContextProps | null>(null);\n\nfunction useSidebar() {\n const context = React.useContext(SidebarContext);\n if (!context) throw new Error(\"useSidebar must be used within a SidebarProvider.\");\n return context;\n}\n\nfunction SidebarProvider({\n defaultOpen = true,\n open: openProp,\n onOpenChange: setOpenProp,\n className,\n style,\n children,\n ...props\n}: React.ComponentProps<\"div\"> & { defaultOpen?: boolean; open?: boolean; onOpenChange?: (open: boolean) => void }) {\n const isMobile = useIsMobile();\n const [openMobile, setOpenMobile] = React.useState(false);\n const [_open, _setOpen] = React.useState(defaultOpen);\n const open = openProp ?? _open;\n const setOpen = React.useCallback(\n (value: boolean | ((value: boolean) => boolean)) => {\n const openState = typeof value === \"function\" ? value(open) : value;\n if (setOpenProp) setOpenProp(openState);\n else _setOpen(openState);\n document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;\n },\n [setOpenProp, open],\n );\n const toggleSidebar = React.useCallback(\n () => (isMobile ? setOpenMobile((o) => !o) : setOpen((o) => !o)),\n [isMobile, setOpen, setOpenMobile],\n );\n\n React.useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {\n event.preventDefault();\n toggleSidebar();\n }\n };\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [toggleSidebar]);\n\n const state = open ? \"expanded\" : \"collapsed\";\n const contextValue = React.useMemo<SidebarContextProps>(\n () => ({ state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar }),\n [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar],\n );\n\n return (\n <SidebarContext.Provider value={contextValue}>\n <div\n data-slot=\"sidebar-wrapper\"\n style={\n { \"--sidebar-width\": \"var(--bl-sidebar-width)\", \"--sidebar-width-icon\": \"var(--bl-sidebar-width-icon)\", ...style } as React.CSSProperties\n }\n className={cn(\"group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar\", className)}\n {...props}\n >\n {children}\n </div>\n </SidebarContext.Provider>\n );\n}\n\nfunction Sidebar({\n side = \"left\",\n variant = \"sidebar\",\n collapsible = \"offcanvas\",\n className,\n children,\n dir,\n ...props\n}: React.ComponentProps<\"div\"> & {\n side?: \"left\" | \"right\";\n variant?: \"sidebar\" | \"floating\" | \"inset\";\n collapsible?: \"offcanvas\" | \"icon\" | \"none\";\n}) {\n const { isMobile, state, openMobile, setOpenMobile } = useSidebar();\n const sheetId = React.useId();\n const sheetRef = React.useRef<HTMLDialogElement>(null);\n\n // the mobile sheet is a native <dialog>: the provider's state opens and closes it, and its own closing (Escape, a\n // click outside, its close button) updates the state\n React.useEffect(() => {\n const d = sheetRef.current;\n if (!d) return;\n if (openMobile && !d.open) d.showModal();\n else if (!openMobile && d.open) d.close();\n }, [openMobile, isMobile]);\n React.useEffect(() => {\n const d = sheetRef.current;\n if (!d) return;\n const onClose = () => setOpenMobile(false);\n d.addEventListener(\"close\", onClose);\n return () => d.removeEventListener(\"close\", onClose);\n }, [isMobile, setOpenMobile]);\n\n if (collapsible === \"none\") {\n return (\n <div\n data-slot=\"sidebar\"\n className={cn(\"flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground\", className)}\n {...props}\n >\n {children}\n </div>\n );\n }\n\n if (isMobile) {\n return (\n <SheetContent\n ref={sheetRef}\n id={sheetId}\n dir={dir}\n side={side}\n showCloseButton={false}\n data-sidebar=\"sidebar\"\n data-mobile=\"true\"\n className=\"w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground\"\n style={{ \"--sidebar-width\": \"var(--bl-sidebar-width-mobile)\" } as React.CSSProperties}\n >\n <SheetHeader className=\"sr-only\">\n <SheetTitle>Sidebar</SheetTitle>\n <SheetDescription>Displays the mobile sidebar.</SheetDescription>\n </SheetHeader>\n <div data-slot=\"sidebar\" className=\"flex h-full w-full flex-col\">\n {children}\n </div>\n </SheetContent>\n );\n }\n\n return (\n <div\n className=\"group peer hidden text-sidebar-foreground md:block\"\n data-state={state}\n data-collapsible={state === \"collapsed\" ? collapsible : \"\"}\n data-variant={variant}\n data-side={side}\n data-slot=\"sidebar\"\n >\n {/* the sidebar's gap in the page */}\n <div\n data-slot=\"sidebar-gap\"\n className={cn(\n \"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear\",\n \"group-data-[collapsible=offcanvas]:w-0 group-data-[side=right]:rotate-180\",\n variant === \"floating\" || variant === \"inset\"\n ? // floating / inset: the icon width plus the padding on both sides\n \"group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+var(--bl-sidebar-padding)*2)]\"\n : \"group-data-[collapsible=icon]:w-(--sidebar-width-icon)\",\n )}\n />\n <div\n data-slot=\"sidebar-container\"\n data-side={side}\n className={cn(\n \"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex\",\n \"data-[side=left]:left-0 data-[side=left]:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]\",\n \"data-[side=right]:right-0 data-[side=right]:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]\",\n variant === \"floating\" || variant === \"inset\"\n ? // the padded icon width plus the floating ring on both sides (upstream's + 2px)\n \"p-(--bl-sidebar-padding) group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+var(--bl-sidebar-padding)*2+var(--bl-sidebar-border-width)*2)]\"\n : \"group-data-[collapsible=icon]:w-(--sidebar-width-icon) border-sidebar-border group-data-[side=left]:border-r-(length:--bl-sidebar-border-width) group-data-[side=right]:border-l-(length:--bl-sidebar-border-width)\",\n className,\n )}\n {...props}\n >\n <div\n data-sidebar=\"sidebar\"\n data-slot=\"sidebar-inner\"\n className=\"flex size-full flex-col bg-sidebar group-data-[variant=floating]:rounded-(--bl-sidebar-floating-radius) group-data-[variant=floating]:shadow-sm group-data-[variant=floating]:ring-(length:--bl-sidebar-border-width) group-data-[variant=floating]:ring-sidebar-border\"\n >\n {children}\n </div>\n </div>\n </div>\n );\n}\n\nfunction SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<typeof Button>) {\n const { toggleSidebar } = useSidebar();\n return (\n <Button\n data-sidebar=\"trigger\"\n data-slot=\"sidebar-trigger\"\n variant=\"ghost\"\n size=\"icon-sm\"\n className={cn(className)}\n onClick={(event) => {\n onClick?.(event);\n toggleSidebar();\n }}\n {...props}\n >\n <Icon name=\"sidebar\" className=\"rtl:-scale-x-100\" />\n <span className=\"sr-only\">Toggle Sidebar</span>\n </Button>\n );\n}\n\nfunction SidebarRail({ className, ...props }: React.ComponentProps<\"button\">) {\n const { toggleSidebar } = useSidebar();\n return (\n <button\n data-sidebar=\"rail\"\n data-slot=\"sidebar-rail\"\n aria-label=\"Toggle Sidebar\"\n tabIndex={-1}\n onClick={toggleSidebar}\n title=\"Toggle Sidebar\"\n className={cn(\n \"absolute inset-y-0 z-20 hidden w-(--bl-sidebar-rail-width) transition-all ease-linear sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2\",\n \"group-data-[side=left]:-right-(--bl-sidebar-rail-width) group-data-[side=right]:left-0\",\n \"after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-sidebar-border\",\n \"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize\",\n \"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize\",\n \"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar\",\n \"[[data-side=left][data-collapsible=offcanvas]_&]:-right-[calc(var(--bl-sidebar-rail-width)/2)]\",\n \"[[data-side=right][data-collapsible=offcanvas]_&]:-left-[calc(var(--bl-sidebar-rail-width)/2)]\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction SidebarInset({ className, ...props }: React.ComponentProps<\"main\">) {\n return (\n <main\n data-slot=\"sidebar-inset\"\n className={cn(\n \"relative flex w-full flex-1 flex-col bg-background\",\n \"md:peer-data-[variant=inset]:m-(--bl-sidebar-inset-margin) md:peer-data-[variant=inset]:ms-0 md:peer-data-[variant=inset]:rounded-(--bl-sidebar-inset-radius) md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ms-(--bl-sidebar-inset-margin)\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction SidebarInput({ className, ...props }: React.ComponentProps<typeof Input>) {\n return (\n <Input\n data-slot=\"sidebar-input\"\n data-sidebar=\"input\"\n className={cn(\"h-(--bl-sidebar-menu-button-md-height) w-full bg-background shadow-none\", className)}\n {...props}\n />\n );\n}\n\nfunction SidebarHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"sidebar-header\"\n data-sidebar=\"header\"\n className={cn(\"flex flex-col gap-(--bl-sidebar-gap) p-(--bl-sidebar-padding)\", className)}\n {...props}\n />\n );\n}\n\nfunction SidebarFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"sidebar-footer\"\n data-sidebar=\"footer\"\n className={cn(\"flex flex-col gap-(--bl-sidebar-gap) p-(--bl-sidebar-padding)\", className)}\n {...props}\n />\n );\n}\n\nfunction SidebarSeparator({ className, ...props }: React.ComponentProps<typeof Separator>) {\n return (\n <Separator\n data-slot=\"sidebar-separator\"\n data-sidebar=\"separator\"\n className={cn(\"mx-(--bl-sidebar-padding) w-auto bg-sidebar-border\", className)}\n {...props}\n />\n );\n}\n\nfunction SidebarContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"sidebar-content\"\n data-sidebar=\"content\"\n className={cn(\n \"flex min-h-0 flex-1 flex-col gap-0 overflow-auto [scrollbar-width:none] group-data-[collapsible=icon]:overflow-hidden [&::-webkit-scrollbar]:hidden\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction SidebarGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"sidebar-group\"\n data-sidebar=\"group\"\n className={cn(\"relative flex w-full min-w-0 flex-col p-(--bl-sidebar-padding)\", className)}\n {...props}\n />\n );\n}\n\nfunction SidebarGroupLabel({ className, render, ...props }: useRender.ComponentProps<\"div\"> & React.ComponentProps<\"div\">) {\n return useRender({\n defaultTagName: \"div\",\n props: mergeProps<\"div\">(\n {\n className: cn(\n \"flex h-(--bl-sidebar-group-label-height) shrink-0 items-center rounded-(--bl-sidebar-menu-button-radius) px-(--bl-sidebar-group-label-padding-x)\",\n \"text-(length:--bl-sidebar-group-label-font-size) leading-(--bl-sidebar-group-label-line-height) font-(--bl-sidebar-group-label-font-weight) text-(color:--bl-sidebar-group-label-foreground)\",\n \"ring-sidebar-ring outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-(length:--bl-control-ring-width)\",\n \"group-data-[collapsible=icon]:-mt-(--bl-sidebar-group-label-height) group-data-[collapsible=icon]:opacity-0\",\n \"[&>svg]:size-(--bl-sidebar-menu-button-icon-size) [&>svg]:shrink-0 [&_[data-slot=icon]]:size-(--bl-sidebar-menu-button-icon-size)\",\n className,\n ),\n },\n props,\n ),\n render,\n state: { slot: \"sidebar-group-label\", sidebar: \"group-label\" },\n });\n}\n\n// the 20px square of an action, centred on a group label / menu button (upstream's top-3.5 right-3 / top-1.5 right-1)\nconst actionBox =\n \"flex aspect-square w-(--bl-sidebar-menu-badge-height) items-center justify-center rounded-(--bl-sidebar-menu-button-radius) p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-(length:--bl-control-ring-width) md:after:hidden [&>svg]:size-(--bl-sidebar-menu-button-icon-size) [&>svg]:shrink-0 [&_[data-slot=icon]]:size-(--bl-sidebar-menu-button-icon-size)\";\n\nfunction SidebarGroupAction({ className, render, ...props }: useRender.ComponentProps<\"button\"> & React.ComponentProps<\"button\">) {\n return useRender({\n defaultTagName: \"button\",\n props: mergeProps<\"button\">(\n {\n className: cn(\n \"absolute end-[calc(var(--bl-sidebar-padding)+var(--bl-sidebar-menu-badge-padding-x))] top-[calc(var(--bl-sidebar-padding)+(var(--bl-sidebar-group-label-height)-var(--bl-sidebar-menu-badge-height))/2)] group-data-[collapsible=icon]:hidden\",\n actionBox,\n className,\n ),\n },\n props,\n ),\n render,\n state: { slot: \"sidebar-group-action\", sidebar: \"group-action\" },\n });\n}\n\nfunction SidebarGroupContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"sidebar-group-content\"\n data-sidebar=\"group-content\"\n className={cn(\"w-full text-(length:--bl-sidebar-menu-button-md-font-size)\", className)}\n {...props}\n />\n );\n}\n\nfunction SidebarMenu({ className, ...props }: React.ComponentProps<\"ul\">) {\n return (\n <ul data-slot=\"sidebar-menu\" data-sidebar=\"menu\" className={cn(\"flex w-full min-w-0 flex-col gap-0\", className)} {...props} />\n );\n}\n\nfunction SidebarMenuItem({ className, ...props }: React.ComponentProps<\"li\">) {\n return <li data-slot=\"sidebar-menu-item\" data-sidebar=\"menu-item\" className={cn(\"group/menu-item relative\", className)} {...props} />;\n}\n\n// upstream's sizes default | sm | lg are Baseline's md | sm | lg (28 / 32 / 48)\nconst sidebarMenuButtonVariants = cva(\n [\n \"peer/menu-button group/menu-button flex w-full items-center gap-(--bl-sidebar-menu-button-gap) overflow-hidden rounded-(--bl-sidebar-menu-button-radius) p-(--bl-sidebar-menu-button-padding) text-left\",\n \"ring-sidebar-ring outline-hidden transition-[width,height,padding] focus-visible:ring-(length:--bl-control-ring-width)\",\n \"group-has-data-[sidebar=menu-action]/menu-item:pe-[calc(var(--bl-sidebar-menu-badge-height)+var(--bl-sidebar-menu-button-padding)+var(--bl-sidebar-menu-badge-padding-x))]\",\n \"group-data-[collapsible=icon]:size-(--bl-sidebar-menu-button-md-height)! group-data-[collapsible=icon]:p-(--bl-sidebar-menu-button-padding)!\",\n \"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground\",\n \"disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50\",\n \"data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground\",\n \"data-active:bg-sidebar-accent data-active:font-(--bl-sidebar-menu-button-font-weight-active) data-active:text-sidebar-accent-foreground\",\n \"[&_svg]:size-(--bl-sidebar-menu-button-icon-size) [&_svg]:shrink-0 [&_[data-slot=icon]]:size-(--bl-sidebar-menu-button-icon-size) [&>span:last-child]:truncate\",\n ],\n {\n variants: {\n variant: {\n default: \"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground\",\n outline:\n \"bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_var(--sidebar-accent)]\",\n },\n size: {\n sm: \"h-(--bl-sidebar-menu-button-sm-height) text-(length:--bl-sidebar-menu-button-sm-font-size) leading-(--bl-sidebar-menu-button-sm-line-height)\",\n md: \"h-(--bl-sidebar-menu-button-md-height) text-(length:--bl-sidebar-menu-button-md-font-size) leading-(--bl-sidebar-menu-button-md-line-height)\",\n lg: \"h-(--bl-sidebar-menu-button-lg-height) text-(length:--bl-sidebar-menu-button-lg-font-size) leading-(--bl-sidebar-menu-button-lg-line-height) group-data-[collapsible=icon]:p-0!\",\n },\n },\n defaultVariants: { variant: \"default\", size: \"md\" },\n },\n);\n\nfunction SidebarMenuButton({\n render,\n isActive = false,\n variant = \"default\",\n size = \"md\",\n tooltip,\n className,\n ...props\n}: useRender.ComponentProps<\"button\"> &\n React.ComponentProps<\"button\"> & {\n isActive?: boolean;\n /** shown beside the button while the sidebar is collapsed to icons (Baseline's Tooltip) */\n tooltip?: string | Omit<React.ComponentProps<typeof TooltipContent>, \"id\">;\n } & VariantProps<typeof sidebarMenuButtonVariants>) {\n // Baseline: the menu parts also serve a site's mobile menu (WordPress, Shopify and the header blocks), with no\n // SidebarProvider; there is nothing to collapse, so no tooltip. Upstream's useSidebar() would throw there.\n const sidebar = React.useContext(SidebarContext);\n const tooltipId = React.useId();\n const showTooltip = Boolean(tooltip) && sidebar?.state === \"collapsed\" && !sidebar?.isMobile;\n const merged = mergeProps<\"button\">({ className: cn(sidebarMenuButtonVariants({ variant, size }), className) }, props);\n const dataState = { \"data-slot\": \"sidebar-menu-button\", \"data-sidebar\": \"menu-button\", \"data-size\": size, \"data-active\": isActive ? \"\" : undefined };\n\n const comp = useRender({\n defaultTagName: \"button\",\n props: { ...merged, ...dataState },\n render,\n });\n if (!showTooltip) return comp;\n\n const content = typeof tooltip === \"string\" ? { children: tooltip } : tooltip!;\n return (\n <>\n <TooltipTrigger interestfor={tooltipId} render={render ?? <button type=\"button\" />} {...merged} {...dataState} />\n <TooltipContent id={tooltipId} side=\"right\" align=\"center\" {...content} />\n </>\n );\n}\n\n// an action / badge's top: centred on its button, whatever the button's size (upstream's top-1 / top-1.5 / top-2.5)\nconst onButton = [\n \"peer-data-[size=sm]/menu-button:top-[calc((var(--bl-sidebar-menu-button-sm-height)-var(--bl-sidebar-menu-badge-height))/2)]\",\n \"peer-data-[size=md]/menu-button:top-[calc((var(--bl-sidebar-menu-button-md-height)-var(--bl-sidebar-menu-badge-height))/2)]\",\n \"peer-data-[size=lg]/menu-button:top-[calc((var(--bl-sidebar-menu-button-lg-height)-var(--bl-sidebar-menu-badge-height))/2)]\",\n];\n\nfunction SidebarMenuAction({\n className,\n render,\n showOnHover = false,\n ...props\n}: useRender.ComponentProps<\"button\"> & React.ComponentProps<\"button\"> & { showOnHover?: boolean }) {\n return useRender({\n defaultTagName: \"button\",\n props: mergeProps<\"button\">(\n {\n className: cn(\n \"absolute end-(--bl-sidebar-menu-badge-padding-x) group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground\",\n onButton,\n actionBox,\n showOnHover &&\n \"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-active/menu-button:text-sidebar-accent-foreground aria-expanded:opacity-100 md:opacity-0\",\n className,\n ),\n },\n props,\n ),\n render,\n state: { slot: \"sidebar-menu-action\", sidebar: \"menu-action\" },\n });\n}\n\nfunction SidebarMenuBadge({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"sidebar-menu-badge\"\n data-sidebar=\"menu-badge\"\n className={cn(\n \"pointer-events-none absolute end-(--bl-sidebar-menu-badge-padding-x) flex h-(--bl-sidebar-menu-badge-height) min-w-(--bl-sidebar-menu-badge-height) items-center justify-center rounded-(--bl-sidebar-menu-badge-radius) px-(--bl-sidebar-menu-badge-padding-x)\",\n // text/xs medium, as the group label\n \"text-(length:--bl-sidebar-group-label-font-size) font-(--bl-sidebar-group-label-font-weight) text-sidebar-foreground tabular-nums select-none\",\n \"group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-active/menu-button:text-sidebar-accent-foreground\",\n onButton,\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction SidebarMenuSkeleton({ className, showIcon = false, ...props }: React.ComponentProps<\"div\"> & { showIcon?: boolean }) {\n // random width between 50 and 90%, as upstream's (fixed after the first render)\n const [width] = React.useState(() => `${Math.floor(Math.random() * 40) + 50}%`);\n const bone = \"animate-pulse rounded-(--bl-sidebar-menu-button-radius) bg-sidebar-accent\"; // upstream's Skeleton\n return (\n <div\n data-slot=\"sidebar-menu-skeleton\"\n data-sidebar=\"menu-skeleton\"\n className={cn(\n \"flex h-(--bl-sidebar-menu-button-md-height) items-center gap-(--bl-sidebar-menu-button-gap) rounded-(--bl-sidebar-menu-button-radius) px-(--bl-sidebar-menu-button-padding)\",\n className,\n )}\n {...props}\n >\n {showIcon && <div className={cn(bone, \"size-(--bl-sidebar-menu-button-icon-size)\")} data-sidebar=\"menu-skeleton-icon\" />}\n <div\n className={cn(bone, \"h-(--bl-sidebar-menu-button-icon-size) max-w-(--skeleton-width) flex-1\")}\n data-sidebar=\"menu-skeleton-text\"\n style={{ \"--skeleton-width\": width } as React.CSSProperties}\n />\n </div>\n );\n}\n\nfunction SidebarMenuSub({ className, ...props }: React.ComponentProps<\"ul\">) {\n return (\n <ul\n data-slot=\"sidebar-menu-sub\"\n data-sidebar=\"menu-sub\"\n className={cn(\n \"mx-(--bl-sidebar-menu-sub-margin-x) flex min-w-0 translate-x-px flex-col gap-(--bl-sidebar-menu-sub-gap) border-s-(length:--bl-sidebar-border-width) border-sidebar-border px-(--bl-sidebar-menu-sub-padding-x) py-(--bl-sidebar-menu-sub-padding-y) group-data-[collapsible=icon]:hidden\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction SidebarMenuSubItem({ className, ...props }: React.ComponentProps<\"li\">) {\n return (\n <li data-slot=\"sidebar-menu-sub-item\" data-sidebar=\"menu-sub-item\" className={cn(\"group/menu-sub-item relative\", className)} {...props} />\n );\n}\n\nfunction SidebarMenuSubButton({\n render,\n size = \"md\",\n isActive = false,\n className,\n ...props\n}: useRender.ComponentProps<\"a\"> & React.ComponentProps<\"a\"> & { size?: \"sm\" | \"md\"; isActive?: boolean }) {\n return useRender({\n defaultTagName: \"a\",\n props: mergeProps<\"a\">(\n {\n className: cn(\n \"flex h-(--bl-sidebar-menu-sub-button-height) min-w-0 -translate-x-px items-center gap-(--bl-sidebar-menu-button-gap) overflow-hidden rounded-(--bl-sidebar-menu-button-radius) px-(--bl-sidebar-menu-sub-button-padding-x) text-sidebar-foreground\",\n \"ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:hidden focus-visible:ring-(length:--bl-control-ring-width)\",\n \"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground\",\n \"disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50\",\n \"data-[size=md]:text-(length:--bl-sidebar-menu-button-md-font-size) data-[size=sm]:text-(length:--bl-sidebar-menu-button-sm-font-size)\",\n \"data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground [&>span:last-child]:truncate\",\n \"[&>svg]:size-(--bl-sidebar-menu-button-icon-size) [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground\",\n className,\n ),\n },\n props,\n ),\n render,\n state: { slot: \"sidebar-menu-sub-button\", sidebar: \"menu-sub-button\", size, active: isActive },\n });\n}\n\nexport {\n Sidebar,\n SidebarContent,\n SidebarFooter,\n SidebarGroup,\n SidebarGroupAction,\n SidebarGroupContent,\n SidebarGroupLabel,\n SidebarHeader,\n SidebarInput,\n SidebarInset,\n SidebarMenu,\n SidebarMenuAction,\n SidebarMenuBadge,\n SidebarMenuButton,\n SidebarMenuItem,\n SidebarMenuSkeleton,\n SidebarMenuSub,\n SidebarMenuSubButton,\n SidebarMenuSubItem,\n SidebarProvider,\n SidebarRail,\n SidebarSeparator,\n SidebarTrigger,\n useSidebar,\n};\n",
|
|
23
|
+
"type": "registry:ui",
|
|
24
|
+
"target": "components/ui/sidebar.tsx"
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"meta": {
|
|
28
|
+
"spec": "components/sidebar/spec.md",
|
|
29
|
+
"figma": "4559:1289",
|
|
30
|
+
"upstream": "shadcn/ui sidebar (base-ui flavour, source read from ui.shadcn.com 2026-10-02, re-read 2026-10-06, unchanged)",
|
|
31
|
+
"status": "ready"
|
|
32
|
+
},
|
|
33
|
+
"type": "registry:ui"
|
|
34
|
+
}
|
package/r/spinner.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "spinner",
|
|
4
|
+
"title": "Spinner",
|
|
5
|
+
"description": "Baseline spinner (ready). Spec: components/spinner/spec.md",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"@baseline/utils",
|
|
8
|
+
"@baseline/icon"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "src/ui/spinner/spinner.tsx",
|
|
13
|
+
"content": "// baseline:spinner@0.2.0\n// Fork of shadcn/ui Spinner (base-ui flavour, source read 2026-09-30). Spec: components/spinner/spec.md.\n//\n// Ladder step 1: the Icon component's CSS mask plus a CSS rotation. No JS, no SVG in the DOM, server-safe.\n// The glyph is the system icon `loader`, resolved to the ACTIVE icon set at build time, so a Material client gets\n// Material's progress_activity and a Lucide client Lucide's loader-circle, exactly as Figma's Icon Placeholder draws\n// it. Upstream hard-codes Lucide's Loader2Icon and B1 hard-coded its path; both are what this replaces.\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Icon } from \"@/registry/ui/icon/icon\";\n\n/**\n * An indeterminate loading indicator. For progress whose amount is known, use Progress.\n *\n * Standalone it is `spinner/size` (16px). Inside a Button it takes the icon's place, marked with data-icon as an\n * icon would be (`<Spinner data-icon=\"inline-start\" />`), and the button's icon size; the button is disabled.\n */\nfunction Spinner({ className, ...props }: Omit<React.ComponentProps<\"span\">, \"children\">) {\n return (\n <Icon\n name=\"loader\"\n data-slot=\"spinner\"\n role=\"status\"\n aria-label=\"Loading\"\n className={cn(\n \"size-(--bl-spinner-size) animate-spin\",\n // Reduced motion slows the turn rather than stopping it: a frozen spinner reads as \"stuck\".\n \"motion-reduce:[animation-duration:3s]\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport { Spinner };\n",
|
|
14
|
+
"type": "registry:ui",
|
|
15
|
+
"target": "components/ui/spinner.tsx"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"meta": {
|
|
19
|
+
"spec": "components/spinner/spec.md",
|
|
20
|
+
"figma": "478:10",
|
|
21
|
+
"upstream": "shadcn/ui spinner (base-ui flavour, source read from ui.shadcn.com 2026-09-30)",
|
|
22
|
+
"status": "ready"
|
|
23
|
+
},
|
|
24
|
+
"type": "registry:ui"
|
|
25
|
+
}
|
package/r/switch.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "switch",
|
|
4
|
+
"title": "Switch",
|
|
5
|
+
"description": "Baseline switch (draft). Spec: components/switch/spec.md",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"class-variance-authority"
|
|
8
|
+
],
|
|
9
|
+
"registryDependencies": [
|
|
10
|
+
"@baseline/utils"
|
|
11
|
+
],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "src/ui/switch/switch.tsx",
|
|
15
|
+
"content": "// baseline:switch@0.1.0\n// Fork of shadcn/ui Switch (base-ui flavour, source read 2026-09-29). Spec: components/switch/spec.md.\n// Geometry comes from Figma through the switch/* tokens (ADR-0017).\n//\n// ADR-0030: this is a real <input type=\"checkbox\" role=\"switch\">, not Base UI's <button role=\"switch\">.\n// role=\"switch\" is what makes a screen reader say on/off instead of checked/unchecked, and it is a plain\n// attribute, so the native route loses nothing.\nimport * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/utils\";\n\n// The track height is deliberately NOT a variable: the track hugs its thumb with switch/padding on each side,\n// in Figma and here, so binding a height would let the two disagree (spec: Sizes). Upstream fixes both, which\n// is where its 18.4px comes from; Figma draws 36x20 and 28x16 on whole spacing steps and wins.\nconst switchVariants = cva(\n [\"group/switch relative inline-flex shrink-0 items-center align-middle\",\n // the whole control fades together, thumb included\n \"has-disabled:cursor-not-allowed has-disabled:opacity-50\"].join(\" \"),\n {\n variants: {\n size: {\n md: \"w-(--bl-switch-md-width) h-[calc(var(--bl-switch-md-thumb-size)+2*var(--bl-switch-padding))]\",\n sm: \"w-(--bl-switch-sm-width) h-[calc(var(--bl-switch-sm-thumb-size)+2*var(--bl-switch-padding))]\",\n },\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\n// Travel is the track minus its padding minus the thumb, so the thumb lands exactly switch/padding from the\n// far end. Static class maps, never interpolation (CLAUDE.md).\n// The thumb is color/primary-foreground, not color/background. Baseline has no light/dark: Scheme 2 IS the\n// dark scheme, and its --background is #020618, so bg-background drew a near-black thumb on an indigo track.\n// A hardcoded white would be wrong in the other direction -- Warning's --primary is #ffb900 and its\n// --primary-foreground is #461901, because white on amber is unreadable. primary-foreground is *defined* as\n// the colour that reads on primary, so it is correct in all five schemes by construction, and it is what\n// Radio's dot already uses. Unchecked the track is --input, where it also reads. (Rebound in Figma on the 12\n// Switch variants, 2026-09-29.)\nconst switchThumbVariants = cva(\n [\"pointer-events-none absolute top-1/2 -translate-y-1/2\",\n \"left-(--bl-switch-padding) bg-primary-foreground transition-transform\"].join(\" \"),\n {\n variants: {\n size: {\n md: \"size-(--bl-switch-md-thumb-size) rounded-(--bl-switch-md-thumb-radius) peer-checked:translate-x-[calc(var(--bl-switch-md-width)-2*var(--bl-switch-padding)-var(--bl-switch-md-thumb-size))]\",\n sm: \"size-(--bl-switch-sm-thumb-size) rounded-(--bl-switch-sm-thumb-radius) peer-checked:translate-x-[calc(var(--bl-switch-sm-width)-2*var(--bl-switch-padding)-var(--bl-switch-sm-thumb-size))]\",\n },\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\ntype SwitchProps = Omit<React.ComponentProps<\"input\">, \"type\" | \"size\"> & VariantProps<typeof switchVariants>;\n\n/**\n * Switch turns one setting on or off and applies the change immediately. That is the whole difference from\n * Checkbox: a checkbox states a value that some later Submit commits, a switch commits as it is flipped. If a\n * control needs a Save button beside it, it is a Checkbox.\n *\n * It owns its track and its thumb and nothing else: no label, no description, no error. Those belong to Field,\n * which swaps Switch as its `Control` — almost always `orientation=\"horizontal\"` with the control after the\n * text. The label must say what turning it ON does, never \"toggle\".\n *\n * Server-safe: no hooks, no directive.\n */\nfunction Switch({ className, size, ...props }: SwitchProps) {\n return (\n <span\n data-slot=\"switch\"\n data-size={size ?? \"md\"}\n className={cn(switchVariants({ size }), className)}\n >\n <input\n type=\"checkbox\"\n role=\"switch\"\n data-slot=\"switch-control\"\n className={cn(\n [\n \"peer size-full appearance-none outline-none transition-colors\",\n \"rounded-(--bl-switch-radius)\",\n // The border is a real colour in every state, not transparent: Figma binds color/input when\n // unchecked and color/primary when checked, on all twelve variants. It was written as\n // border-transparent while switch/border-width was still 0 and nothing could show; the designer\n // set that token to 1px on 2026-09-29, which made the difference visible.\n \"border-(length:--bl-switch-border-width) border-input\",\n \"bg-input checked:border-primary checked:bg-primary\",\n \"focus-visible:ring-(length:--bl-control-ring-width) focus-visible:ring-ring\",\n // invalid is a destructive border plus the --ring-invalid ring, the same contract Input,\n // Checkbox and Radio use -- and, as on those three, invalid does NOT override checked: a\n // switched-on control still has to read as on, so the primary border wins and only the ring\n // carries the error. Figma draws exactly this (State=invalid, Checked=true binds color/primary).\n \"aria-invalid:ring-(length:--bl-control-ring-width) aria-invalid:ring-(color:--ring-invalid)\",\n \"aria-invalid:border-destructive\",\n \"aria-invalid:checked:border-primary\",\n \"disabled:cursor-not-allowed\",\n // a 20px-tall track fails WCAG 2.5.8 on touch, so the target is grown with a pseudo-element\n // rather than with padding, which would move the track.\n \"after:absolute after:-inset-x-3 after:-inset-y-2 after:content-['']\",\n ].join(\" \"),\n )}\n {...props}\n />\n {/* The thumb is a sibling, not a pseudo-element: a replaced element cannot have children. It never\n takes the click. */}\n <span\n aria-hidden=\"true\"\n data-slot=\"switch-thumb\"\n className={switchThumbVariants({ size })}\n />\n </span>\n );\n}\n\nexport { Switch };\nexport type { SwitchProps };\n",
|
|
16
|
+
"type": "registry:ui",
|
|
17
|
+
"target": "components/ui/switch.tsx"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"meta": {
|
|
21
|
+
"spec": "components/switch/spec.md",
|
|
22
|
+
"figma": "472:100",
|
|
23
|
+
"upstream": "shadcn/ui switch (base-ui flavour, source read from ui.shadcn.com 2026-09-25)",
|
|
24
|
+
"status": "draft"
|
|
25
|
+
},
|
|
26
|
+
"type": "registry:ui"
|
|
27
|
+
}
|
package/r/table.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "table",
|
|
4
|
+
"title": "Table",
|
|
5
|
+
"description": "Baseline table (ready). Spec: components/table/spec.md",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"@baseline/utils"
|
|
8
|
+
],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "src/ui/table/table.tsx",
|
|
12
|
+
"content": "// baseline:table@0.1.0\n// Fork of shadcn/ui Table (base-ui flavour, source read 2026-10-02). Spec: components/table/spec.md.\n// Geometry comes from Figma through the table/* tokens (ADR-0017); the row hover and footer fill are the\n// table/row/hover-background and table/footer/background tints (muted at 50 %, ADR-0017 §2).\n//\n// Ladder step 1: a real <table> in an overflow-x container, as upstream. Upstream's \"use client\" is dropped: nothing in\n// the file needs the client, so it is server-safe.\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * The container scrolls sideways when the table is wider than its parent. When it can overflow, give the container a\n * label and tabindex={0} through `containerProps` so keyboard users can scroll it.\n */\nfunction Table({ className, containerProps, ...props }: React.ComponentProps<\"table\"> & { containerProps?: React.ComponentProps<\"div\"> }) {\n return (\n <div data-slot=\"table-container\" {...containerProps} className={cn(\"relative w-full overflow-x-auto\", containerProps?.className)}>\n <table\n data-slot=\"table\"\n className={cn(\"w-full caption-bottom text-(length:--bl-table-font-size) leading-(--bl-table-line-height)\", className)}\n {...props}\n />\n </div>\n );\n}\n\n// upstream relies on shadcn's global `* { border-color: var(--border) }`, which Baseline does not ship, so every border\n// here names border-border (as Card does)\nfunction TableHeader({ className, ...props }: React.ComponentProps<\"thead\">) {\n return (\n <thead\n data-slot=\"table-header\"\n className={cn(\"[&_tr]:border-b-(length:--bl-table-border-width) [&_tr]:border-border\", className)}\n {...props}\n />\n );\n}\n\nfunction TableBody({ className, ...props }: React.ComponentProps<\"tbody\">) {\n return <tbody data-slot=\"table-body\" className={cn(\"[&_tr:last-child]:border-0\", className)} {...props} />;\n}\n\nfunction TableFooter({ className, ...props }: React.ComponentProps<\"tfoot\">) {\n return (\n <tfoot\n data-slot=\"table-footer\"\n className={cn(\n \"border-t-(length:--bl-table-border-width) border-border bg-(--bl-table-footer-background) font-(--bl-table-head-font-weight) [&>tr]:last:border-b-0\",\n className,\n )}\n {...props}\n />\n );\n}\n\n/** Hover fills the row; data-state=\"selected\" marks a selected row. */\nfunction TableRow({ className, ...props }: React.ComponentProps<\"tr\">) {\n return (\n <tr\n data-slot=\"table-row\"\n className={cn(\n \"border-b-(length:--bl-table-border-width) border-border transition-colors\",\n \"hover:bg-(--bl-table-row-hover-background) has-aria-expanded:bg-(--bl-table-row-hover-background) data-[state=selected]:bg-muted\",\n className,\n )}\n {...props}\n />\n );\n}\n\n/** A header cell; pass scope=\"col\" (or \"row\") so assistive tech pairs it with its cells. */\nfunction TableHead({ className, ...props }: React.ComponentProps<\"th\">) {\n return (\n <th\n data-slot=\"table-head\"\n className={cn(\n \"h-(--bl-table-head-height) px-(--bl-table-cell-padding-x) text-start align-middle font-(--bl-table-head-font-weight) whitespace-nowrap text-foreground\",\n \"[&:has([role=checkbox])]:pe-0\",\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(\n \"px-(--bl-table-cell-padding-x) py-(--bl-table-cell-padding-y) align-middle whitespace-nowrap\",\n \"[&:has([role=checkbox])]:pe-0\",\n className,\n )}\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-(--bl-table-caption-gap) text-(length:--bl-table-font-size) leading-(--bl-table-line-height) text-muted-foreground\", className)}\n {...props}\n />\n );\n}\n\nexport { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };\n",
|
|
13
|
+
"type": "registry:ui",
|
|
14
|
+
"target": "components/ui/table.tsx"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"meta": {
|
|
18
|
+
"spec": "components/table/spec.md",
|
|
19
|
+
"figma": "4361:15",
|
|
20
|
+
"upstream": "shadcn/ui table (base-ui flavour, source read from ui.shadcn.com 2026-09-30)",
|
|
21
|
+
"status": "ready"
|
|
22
|
+
},
|
|
23
|
+
"type": "registry:ui"
|
|
24
|
+
}
|
package/r/tabs-line.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "tabs-line",
|
|
4
|
+
"title": "Tabs (line)",
|
|
5
|
+
"description": "Baseline tabs, the line look (ADR-0031: install one look per project; it replaces @baseline/tabs). Spec: components/tabs/spec.md",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"@base-ui/react"
|
|
8
|
+
],
|
|
9
|
+
"registryDependencies": [
|
|
10
|
+
"@baseline/utils"
|
|
11
|
+
],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "src/ui/tabs/tabs.line.tsx",
|
|
15
|
+
"content": "// baseline:tabs@0.1.0\n// Fork of shadcn/ui Tabs (base-ui flavour, source read 2026-10-02, re-read 2026-10-06). Spec: components/tabs/spec.md.\n// THE LINE LOOK (ADR-0031), drawn as bordered Pagination's (user, 2026-10-06, replacing upstream's variant=\"line\"): a\n// tabs/line/border-width rule under the list (on its end side when vertical), the tabs tabs/list/gap-line apart, and a\n// tabs/line/indicator-width indicator sitting on the rule: primary under the active tab, whose text is primary too, and\n// tabs/line/indicator-hover under a hovered one. tabs.tsx (the default) is the other look; a project installs one, as\n// components/ui/tabs.tsx.\n//\n// Ladder step 3, Base UI Tabs (see tabs.tsx). The indicator reaches past the list's padding and its rule, so its outer\n// edge is the rule's and it covers it, as bordered Pagination's current page does.\nimport * as React from \"react\";\nimport { Tabs as TabsPrimitive } from \"@base-ui/react/tabs\";\nimport { cn } from \"@/lib/utils\";\n\nfunction Tabs({ className, orientation = \"horizontal\", ...props }: TabsPrimitive.Root.Props) {\n return (\n <TabsPrimitive.Root\n data-slot=\"tabs\"\n orientation={orientation}\n className={cn(\"group/tabs flex gap-(--bl-tabs-gap) data-[orientation=horizontal]:flex-col\", className)}\n {...props}\n />\n );\n}\n\nfunction TabsList({ className, ...props }: TabsPrimitive.List.Props) {\n return (\n <TabsPrimitive.List\n data-slot=\"tabs-list\"\n data-variant=\"line\"\n className={cn(\n \"group/tabs-list inline-flex w-fit items-center justify-center gap-(--bl-tabs-list-gap-line) border-border bg-transparent p-(--bl-tabs-list-padding) text-muted-foreground\",\n // the rule\n \"group-data-[orientation=horizontal]/tabs:border-b-(length:--bl-tabs-line-border-width) group-data-[orientation=vertical]/tabs:border-e-(length:--bl-tabs-line-border-width)\",\n \"group-data-[orientation=horizontal]/tabs:h-(--bl-tabs-list-height) group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {\n return (\n <TabsPrimitive.Tab\n data-slot=\"tabs-trigger\"\n className={cn(\n \"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-(--bl-tabs-trigger-gap) whitespace-nowrap\",\n \"rounded-(--bl-tabs-trigger-radius) border-(length:--bl-tabs-trigger-border-width) border-transparent bg-transparent px-(--bl-tabs-trigger-padding-x) py-(--bl-tabs-trigger-padding-y)\",\n \"text-(length:--bl-tabs-trigger-font-size) leading-(--bl-tabs-trigger-line-height) font-(--bl-tabs-trigger-font-weight) text-(color:--bl-tabs-trigger-foreground) transition-all\",\n \"group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground\",\n \"outline-none focus-visible:border-ring focus-visible:ring-(length:--bl-control-ring-width) focus-visible:ring-ring\",\n \"disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50\",\n \"has-data-[icon=inline-start]:ps-(--bl-tabs-trigger-padding-x-icon) has-data-[icon=inline-end]:pe-(--bl-tabs-trigger-padding-x-icon)\",\n \"[&_[data-slot=icon]]:pointer-events-none [&_[data-slot=icon]]:shrink-0 [&_[data-slot=icon]]:size-(--bl-tabs-trigger-icon-size)\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-(--bl-tabs-trigger-icon-size)\",\n \"data-active:text-primary\",\n // the indicator: primary when active, the hover tint over an inactive tab\n \"after:absolute after:bg-primary after:opacity-0 after:transition-opacity data-active:after:opacity-100\",\n \"hover:not-data-active:after:bg-(--bl-tabs-line-indicator-hover) hover:not-data-active:after:opacity-100\",\n \"group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:h-(--bl-tabs-line-indicator-width)\",\n \"group-data-[orientation=horizontal]/tabs:after:bottom-[calc(-1*(var(--bl-tabs-list-padding)+var(--bl-tabs-line-border-width)))]\",\n \"group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:w-(--bl-tabs-line-indicator-width)\",\n \"group-data-[orientation=vertical]/tabs:after:-end-[calc(var(--bl-tabs-list-padding)+var(--bl-tabs-line-border-width))]\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {\n return <TabsPrimitive.Panel data-slot=\"tabs-content\" className={cn(\"flex-1 text-sm outline-none\", className)} {...props} />;\n}\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent };\n",
|
|
16
|
+
"type": "registry:ui",
|
|
17
|
+
"target": "components/ui/tabs.tsx"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"meta": {
|
|
21
|
+
"spec": "components/tabs/spec.md",
|
|
22
|
+
"figma": "4538:15627",
|
|
23
|
+
"upstream": "shadcn/ui tabs (base-ui flavour, source read from ui.shadcn.com 2026-10-02, re-read 2026-10-06, unchanged)",
|
|
24
|
+
"status": "ready",
|
|
25
|
+
"look": "line"
|
|
26
|
+
},
|
|
27
|
+
"type": "registry:ui"
|
|
28
|
+
}
|
package/r/tabs.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "tabs",
|
|
4
|
+
"title": "Tabs",
|
|
5
|
+
"description": "Baseline tabs (ready). Spec: components/tabs/spec.md. Base UI Tabs (ADR-0030: there is no native tabs element, so the primitive renders the markup a native build would). One file per look (ADR-0031): tabs (a muted pill list) or tabs-line (a rule under the list with a primary indicator on the active tab, as bordered Pagination)",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"@base-ui/react"
|
|
8
|
+
],
|
|
9
|
+
"registryDependencies": [
|
|
10
|
+
"@baseline/utils"
|
|
11
|
+
],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "src/ui/tabs/tabs.tsx",
|
|
15
|
+
"content": "// baseline:tabs@0.1.0\n// Fork of shadcn/ui Tabs (base-ui flavour, source read 2026-10-02, re-read 2026-10-06). Spec: components/tabs/spec.md.\n// THE DEFAULT LOOK (ADR-0031): upstream's muted pill list, the active tab a background pill with shadow-sm.\n// tabs.line.tsx is the other look; a project installs one, as components/ui/tabs.tsx. Geometry comes from the tabs/*\n// tokens (ADR-0017); the inactive text is tabs/trigger/foreground (foreground at 60%, upstream's text-foreground/60).\n//\n// Ladder step 3, Base UI Tabs: there is no native tabs element, so the primitive renders the WAI-ARIA markup a native\n// build would (role=tablist / tab / tabpanel, roving tabindex), and ADR-0030's test passes. The Base UI parts carry\n// 'use client' themselves, so this file stays a server component. Base UI writes data-orientation and data-active;\n// upstream's data-horizontal / data-vertical variants are shadcn's own, so the selectors here spell the attribute out.\nimport * as React from \"react\";\nimport { Tabs as TabsPrimitive } from \"@base-ui/react/tabs\";\nimport { cn } from \"@/lib/utils\";\n\nfunction Tabs({ className, orientation = \"horizontal\", ...props }: TabsPrimitive.Root.Props) {\n return (\n <TabsPrimitive.Root\n data-slot=\"tabs\"\n orientation={orientation}\n className={cn(\"group/tabs flex gap-(--bl-tabs-gap) data-[orientation=horizontal]:flex-col\", className)}\n {...props}\n />\n );\n}\n\nfunction TabsList({ className, ...props }: TabsPrimitive.List.Props) {\n return (\n <TabsPrimitive.List\n data-slot=\"tabs-list\"\n data-variant=\"default\"\n className={cn(\n \"group/tabs-list inline-flex w-fit items-center justify-center rounded-(--bl-tabs-list-radius) bg-muted p-(--bl-tabs-list-padding) text-muted-foreground\",\n \"group-data-[orientation=horizontal]/tabs:h-(--bl-tabs-list-height) group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {\n return (\n <TabsPrimitive.Tab\n data-slot=\"tabs-trigger\"\n className={cn(\n \"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-(--bl-tabs-trigger-gap) whitespace-nowrap\",\n \"rounded-(--bl-tabs-trigger-radius) border-(length:--bl-tabs-trigger-border-width) border-transparent px-(--bl-tabs-trigger-padding-x) py-(--bl-tabs-trigger-padding-y)\",\n \"text-(length:--bl-tabs-trigger-font-size) leading-(--bl-tabs-trigger-line-height) font-(--bl-tabs-trigger-font-weight) text-(color:--bl-tabs-trigger-foreground) transition-all\",\n \"group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground\",\n // focus keeps Baseline's ring role (already translucent), as Button's\n \"outline-none focus-visible:border-ring focus-visible:ring-(length:--bl-control-ring-width) focus-visible:ring-ring\",\n \"disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50\",\n \"has-data-[icon=inline-start]:ps-(--bl-tabs-trigger-padding-x-icon) has-data-[icon=inline-end]:pe-(--bl-tabs-trigger-padding-x-icon)\",\n // Icon always carries size-4, so its size is set outright (as Button); a raw svg keeps upstream's not-size- guard\n \"[&_[data-slot=icon]]:pointer-events-none [&_[data-slot=icon]]:shrink-0 [&_[data-slot=icon]]:size-(--bl-tabs-trigger-icon-size)\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-(--bl-tabs-trigger-icon-size)\",\n \"data-active:bg-background data-active:text-foreground data-active:shadow-sm\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {\n return <TabsPrimitive.Panel data-slot=\"tabs-content\" className={cn(\"flex-1 text-sm outline-none\", className)} {...props} />;\n}\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent };\n",
|
|
16
|
+
"type": "registry:ui",
|
|
17
|
+
"target": "components/ui/tabs.tsx"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"meta": {
|
|
21
|
+
"spec": "components/tabs/spec.md",
|
|
22
|
+
"figma": "4538:15627",
|
|
23
|
+
"upstream": "shadcn/ui tabs (base-ui flavour, source read from ui.shadcn.com 2026-10-02, re-read 2026-10-06, unchanged)",
|
|
24
|
+
"status": "ready"
|
|
25
|
+
},
|
|
26
|
+
"type": "registry:ui"
|
|
27
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "text-styles",
|
|
4
|
+
"title": "Text styles",
|
|
5
|
+
"description": "The content ramp: one Tailwind utility per Figma display/* and heading/* text style (display-lg … heading-sm), on the responsive text scale. CSS only. ADR-0014.",
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"path": "src/styles/text-styles.css",
|
|
9
|
+
"content": "/* Generated by tokens/build.mjs from the Figma text styles (src/figma/text-styles.json). Do not edit. Content ramp\n (ADR-0014): one Tailwind utility per style, e.g. className=\"display-lg\". */\n@utility display-lg {\n font-family: var(--bl-font-sans);\n font-size: var(--bl-text-responsive-5xl);\n line-height: var(--bl-text-responsive-5xl--line-height);\n font-weight: 600;\n}\n@utility display-md {\n font-family: var(--bl-font-sans);\n font-size: var(--bl-text-responsive-4xl);\n line-height: var(--bl-text-responsive-4xl--line-height);\n font-weight: 600;\n}\n@utility display-sm {\n font-family: var(--bl-font-sans);\n font-size: var(--bl-text-responsive-3xl);\n line-height: var(--bl-text-responsive-3xl--line-height);\n font-weight: 600;\n}\n@utility heading-lg {\n font-family: var(--bl-font-sans);\n font-size: var(--bl-text-responsive-xl);\n line-height: var(--bl-text-responsive-xl--line-height);\n font-weight: 600;\n}\n@utility heading-md {\n font-family: var(--bl-font-sans);\n font-size: var(--bl-text-responsive-lg);\n line-height: var(--bl-text-responsive-lg--line-height);\n font-weight: 600;\n}\n@utility heading-sm {\n font-family: var(--bl-font-sans);\n font-size: var(--bl-text-responsive-md);\n line-height: var(--bl-text-responsive-md--line-height);\n font-weight: 600;\n}\n@utility heading-xl {\n font-family: var(--bl-font-sans);\n font-size: var(--bl-text-responsive-2xl);\n line-height: var(--bl-text-responsive-2xl--line-height);\n font-weight: 600;\n}\n",
|
|
10
|
+
"type": "registry:file",
|
|
11
|
+
"target": "app/text-styles.css"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"docs": "Add @import \"./text-styles.css\"; to app/globals.css after @import \"tailwindcss\"; then className=\"display-lg\" (or heading-xl, …) on designed headings. Prose keeps typeset.",
|
|
15
|
+
"type": "registry:item"
|
|
16
|
+
}
|
package/r/textarea.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "textarea",
|
|
4
|
+
"title": "Textarea",
|
|
5
|
+
"description": "Baseline textarea (draft). Spec: components/textarea/spec.md",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"@baseline/utils"
|
|
8
|
+
],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "src/ui/textarea/textarea.tsx",
|
|
12
|
+
"content": "// baseline:textarea@0.1.0\n// Fork of shadcn/ui Textarea (source read 2026-09-29). Spec: components/textarea/spec.md.\n// Geometry comes from Figma through the textarea/* tokens (ADR-0017).\n//\n// Upstream's Textarea imports no primitive at all -- it is a bare <textarea> typed as\n// React.ComponentProps<\"textarea\"> -- so the base-ui and Radix flavours of this one component are identical\n// and ADR-0030 has nothing to decide. Ladder step 1: the native element IS the component.\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Textarea — the multi-line text control. Like Input it owns its box and nothing else: no label, no\n * description, no error. Those belong to Field, which swaps Textarea as its `Control`. A textarea without a\n * label is a bug.\n *\n * Server-safe: no hooks, no directive.\n */\nfunction Textarea({ className, ...props }: React.ComponentProps<\"textarea\">) {\n return (\n <textarea\n data-slot=\"textarea\"\n className={cn(\n [\n // field-sizing:content grows the box with its content in CSS alone, which is why upstream dropped\n // its JS resize hook and why Baseline has none on any platform. Safari and Firefox ignore it today\n // and simply keep the `rows` height and scroll — a plain textarea, so the degradation is invisible\n // rather than broken. min-height is the floor either way.\n // Vertical only. A textarea is as wide as its container -- Field, a form column, a grid cell -- so dragging it\n // wider breaks the layout around it and nothing puts it back. Height is the dimension the user actually needs,\n // and field-sizing already grows it; the handle is for overriding that.\n \"resize-y\",\n \"field-sizing-content w-full min-w-0 bg-transparent text-foreground outline-none transition-colors\",\n // box: every value is a textarea/* variable, never a Tailwind step and never --bl-control-* directly\n // (the shared layer moves every control at once; a component must be able to leave it alone)\n \"min-h-(--bl-textarea-md-min-height) rounded-(--bl-textarea-md-radius)\",\n // padding-y is the one number that differs from the shared control box: upstream uses py-1 on Input\n // and py-2 here, because a paragraph starting at the top of a 64px box wants a looser pad than a\n // single line centred in a 32px one (user decision, 2026-09-28).\n \"px-(--bl-textarea-md-padding-x) py-(--bl-textarea-md-padding-y)\",\n \"border-(length:--bl-textarea-md-border-width) border-input\",\n // 16px on phones so iOS does not zoom on focus, the token size from md up (shadcn's rule)\n \"text-base md:text-(length:--bl-textarea-md-font-size) md:leading-(--bl-textarea-md-line-height)\",\n \"placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground\",\n // focus: solid primary border + the shared 3px ring (color/ring is primary at 25%)\n \"focus-visible:border-primary focus-visible:ring-(length:--bl-control-ring-width) focus-visible:ring-ring\",\n // invalid is set here, never inherited from an invalid Field, so it looks the same on every\n // platform and on a standalone textarea — the same call Input made (spec: States)\n \"aria-invalid:border-destructive aria-invalid:text-destructive\",\n \"aria-invalid:ring-(length:--bl-control-ring-width) aria-invalid:ring-(color:--ring-invalid)\",\n // upstream additionally tints the disabled box; Figma only drops opacity, and opacity is what every\n // other Baseline control does, so one disabled treatment across the system wins (spec: States)\n \"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50\",\n ].join(\" \"),\n className,\n )}\n {...props}\n />\n );\n}\n\nexport { Textarea };\n",
|
|
13
|
+
"type": "registry:ui",
|
|
14
|
+
"target": "components/ui/textarea.tsx"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"meta": {
|
|
18
|
+
"spec": "components/textarea/spec.md",
|
|
19
|
+
"figma": "1780:10193",
|
|
20
|
+
"upstream": "shadcn/ui textarea (base-ui flavour, source read from ui.shadcn.com 2026-09-25)",
|
|
21
|
+
"status": "draft"
|
|
22
|
+
},
|
|
23
|
+
"type": "registry:ui"
|
|
24
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "toggle-group",
|
|
4
|
+
"title": "Toggle-group",
|
|
5
|
+
"description": "Baseline toggle-group (ready). Spec: components/toggle-group/spec.md. Not Base UI: native radios (single, the default) or checkboxes (multiple). value / defaultValue are string arrays as upstream's; single choice keeps one selected once chosen",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"@baseline/utils",
|
|
8
|
+
"@baseline/toggle"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "src/ui/toggle-group/toggle-group.tsx",
|
|
13
|
+
"content": "// baseline:toggle-group@0.1.0\n// Fork of shadcn/ui Toggle Group (base-ui flavour, source read 2026-10-02). Spec: components/toggle-group/spec.md.\n//\n// Ladder step 1 (ADR-0030, user 2026-10-06): NOT Base UI. Single choice (the default) is a radio group — items share a\n// name, the browser keeps one on, moves and selects with the arrow keys — and `multiple` is a set of checkboxes. Each\n// item is a Toggle. The group passes name, type, checked state, variant and size to its direct items with cloneElement\n// (as Accordion), so there is no context and no client code; onValueChange reads the checked inputs on a change event.\n// <ToggleGroup variant=\"outline\" multiple>\n// <ToggleGroupItem value=\"bold\" aria-label=\"Toggle bold\"><Icon className=\"icon-[material-symbols--format-bold]\" /></ToggleGroupItem>\n// </ToggleGroup>\n// Accepted trade-off (user, 2026-10-06): once a single-choice group has a value it cannot return to none.\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Toggle, type ToggleProps } from \"@/registry/ui/toggle/toggle\";\n\ntype ToggleGroupProps = Omit<React.ComponentProps<\"div\">, \"defaultValue\" | \"onChange\"> & {\n /** false (default): radios, one on; true: checkboxes */\n multiple?: boolean;\n defaultValue?: string[];\n value?: string[];\n onValueChange?: (value: string[]) => void;\n /** the inputs' name; generated when left out */\n name?: string;\n variant?: ToggleProps[\"variant\"];\n size?: ToggleProps[\"size\"];\n /** 0 joins the items; any other value spaces them by toggle-group/gap (only 0 vs not-0 matters) */\n spacing?: number;\n orientation?: \"horizontal\" | \"vertical\";\n disabled?: boolean;\n};\n\ntype ToggleGroupItemProps = Omit<ToggleProps, \"value\"> & { value: string };\n\nfunction ToggleGroupItem({ className, ...props }: ToggleGroupItemProps) {\n return <Toggle data-slot=\"toggle-group-item\" className={cn(\"shrink-0\", className)} {...props} />;\n}\n\nfunction ToggleGroup({\n multiple = false,\n defaultValue,\n value,\n onValueChange,\n name,\n variant,\n size,\n spacing = 2,\n orientation = \"horizontal\",\n disabled,\n className,\n children,\n ...props\n}: ToggleGroupProps) {\n const id = React.useId();\n const groupName = name ?? `toggle-group${id}`;\n const type = multiple ? \"checkbox\" : \"radio\";\n const items = React.Children.map(children, (child) => {\n if (!React.isValidElement<ToggleGroupItemProps>(child) || child.type !== ToggleGroupItem) return child;\n const itemValue = child.props.value;\n return React.cloneElement(child, {\n name: groupName,\n type,\n variant: child.props.variant ?? variant,\n size: child.props.size ?? size,\n disabled: child.props.disabled ?? disabled,\n // controlled: the group's value decides, and the change is reported from the group (below)\n ...(value !== undefined\n ? { pressed: value.includes(itemValue), onChange: child.props.onChange ?? (() => {}) }\n : defaultValue !== undefined\n ? { defaultPressed: defaultValue.includes(itemValue) }\n : {}),\n });\n });\n return (\n <div\n role=\"group\"\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n data-spacing={spacing}\n data-orientation={orientation}\n className={cn(\n \"group/toggle-group flex w-fit flex-row items-center gap-(--bl-toggle-group-gap) data-[spacing=0]:gap-0\",\n \"data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch\",\n className,\n )}\n onChange={\n onValueChange\n ? (e) => {\n const inputs = e.currentTarget.querySelectorAll<HTMLInputElement>(`input[name=\"${CSS.escape(groupName)}\"]`);\n onValueChange([...inputs].filter((i) => i.checked).map((i) => i.value));\n }\n : undefined\n }\n {...props}\n >\n {items}\n </div>\n );\n}\n\nexport { ToggleGroup, ToggleGroupItem };\nexport type { ToggleGroupItemProps, ToggleGroupProps };\n",
|
|
14
|
+
"type": "registry:ui",
|
|
15
|
+
"target": "components/ui/toggle-group.tsx"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"css": {
|
|
19
|
+
"[data-slot=toggle-group][data-spacing=\"0\"] > [data-slot=toggle-group-item]": {
|
|
20
|
+
"border-radius": "0"
|
|
21
|
+
},
|
|
22
|
+
"[data-slot=toggle-group][data-spacing=\"0\"][data-orientation=horizontal] > [data-slot=toggle-group-item]:first-child": {
|
|
23
|
+
"border-start-start-radius": "var(--toggle-radius)",
|
|
24
|
+
"border-end-start-radius": "var(--toggle-radius)"
|
|
25
|
+
},
|
|
26
|
+
"[data-slot=toggle-group][data-spacing=\"0\"][data-orientation=horizontal] > [data-slot=toggle-group-item]:last-child": {
|
|
27
|
+
"border-start-end-radius": "var(--toggle-radius)",
|
|
28
|
+
"border-end-end-radius": "var(--toggle-radius)"
|
|
29
|
+
},
|
|
30
|
+
"[data-slot=toggle-group][data-spacing=\"0\"][data-orientation=vertical] > [data-slot=toggle-group-item]:first-child": {
|
|
31
|
+
"border-start-start-radius": "var(--toggle-radius)",
|
|
32
|
+
"border-start-end-radius": "var(--toggle-radius)"
|
|
33
|
+
},
|
|
34
|
+
"[data-slot=toggle-group][data-spacing=\"0\"][data-orientation=vertical] > [data-slot=toggle-group-item]:last-child": {
|
|
35
|
+
"border-end-start-radius": "var(--toggle-radius)",
|
|
36
|
+
"border-end-end-radius": "var(--toggle-radius)"
|
|
37
|
+
},
|
|
38
|
+
"[data-slot=toggle-group][data-spacing=\"0\"][data-orientation=horizontal] > [data-slot=toggle-group-item][data-variant=outline]:not(:first-child)": {
|
|
39
|
+
"border-inline-start-width": "0"
|
|
40
|
+
},
|
|
41
|
+
"[data-slot=toggle-group][data-spacing=\"0\"][data-orientation=vertical] > [data-slot=toggle-group-item][data-variant=outline]:not(:first-child)": {
|
|
42
|
+
"border-block-start-width": "0"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"meta": {
|
|
46
|
+
"spec": "components/toggle-group/spec.md",
|
|
47
|
+
"figma": "4524:424",
|
|
48
|
+
"upstream": "shadcn/ui toggle-group (base-ui flavour, source read from ui.shadcn.com 2026-10-02)",
|
|
49
|
+
"status": "ready"
|
|
50
|
+
},
|
|
51
|
+
"type": "registry:ui"
|
|
52
|
+
}
|