@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
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "blog-card-grid",
|
|
4
|
+
"title": "Blog Card Grid",
|
|
5
|
+
"description": "Baseline block blog-card-grid (draft): a composed section to copy and own. Spec: components/sections/spec.md",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"@baseline/utils",
|
|
8
|
+
"@baseline/section",
|
|
9
|
+
"@baseline/container",
|
|
10
|
+
"@baseline/columns",
|
|
11
|
+
"@baseline/badge",
|
|
12
|
+
"@baseline/card",
|
|
13
|
+
"@baseline/media"
|
|
14
|
+
],
|
|
15
|
+
"files": [
|
|
16
|
+
{
|
|
17
|
+
"path": "src/blocks/blog-card-grid/blog-card-grid.tsx",
|
|
18
|
+
"content": "// baseline:blog-card-grid@0.1.0\n// Baseline block (spec: components/sections/spec.md; Figma Blog/Card Grid 2443:879). A composed section to copy and\n// own: Section > Container (wide) with an eyebrow, heading and lead, then a second Container of Columns/3 Cards: a\n// 3:2 image full bleed, the title as a link, the date and author beside a category Badge, and an excerpt. Feed `posts`\n// from the CMS.\nimport { Section } from \"@/registry/ui/section/section\";\nimport { Container } from \"@/registry/ui/container/container\";\nimport { Columns } from \"@/registry/ui/columns/columns\";\nimport { Badge } from \"@/registry/ui/badge/badge\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/registry/ui/card/card\";\nimport { Media, type MediaImage } from \"@/registry/ui/media/media\";\n\ntype BlogPost = { title: string; href: string; image: MediaImage; date: string; author?: string; category?: string; excerpt?: string };\n\ntype BlogCardGridProps = {\n id?: string;\n eyebrow?: string;\n title: string;\n description?: string;\n posts: BlogPost[];\n};\n\nfunction BlogCardGrid({ id = \"blog\", eyebrow, title, description, posts }: BlogCardGridProps) {\n return (\n <Section aria-labelledby={`${id}-title`}>\n <Container width=\"wide\">\n <Columns columns={1}>\n <div>\n <hgroup className=\"flex flex-col gap-2\">\n {eyebrow ? <p className=\"text-sm text-primary\">{eyebrow}</p> : null}\n <h2 id={`${id}-title`} className=\"heading-xl text-balance\">\n {title}\n </h2>\n </hgroup>\n {description ? <p className=\"mt-5 text-base leading-7 text-pretty text-muted-foreground\">{description}</p> : null}\n </div>\n </Columns>\n </Container>\n <Container width=\"wide\">\n <Columns columns={3}>\n {posts.map((post) => (\n <Card key={post.href}>\n <Media image={post.image} ratio={3 / 2} />\n <CardHeader>\n <CardTitle>\n <h3>\n <a href={post.href} className=\"hover:underline hover:underline-offset-4\">\n {post.title}\n </a>\n </h3>\n </CardTitle>\n </CardHeader>\n <CardContent className=\"flex flex-col gap-(--bl-card-md-gap)\">\n <div className=\"flex flex-wrap items-center gap-2 text-muted-foreground\">\n <span>\n <time>{post.date}</time>\n {post.author ? <> · {post.author}</> : null}\n </span>\n {post.category ? <Badge variant=\"secondary\">{post.category}</Badge> : null}\n </div>\n {post.excerpt ? <p className=\"text-foreground\">{post.excerpt}</p> : null}\n </CardContent>\n </Card>\n ))}\n </Columns>\n </Container>\n </Section>\n );\n}\n\nexport { BlogCardGrid };\nexport type { BlogCardGridProps, BlogPost };\n",
|
|
19
|
+
"type": "registry:component",
|
|
20
|
+
"target": "components/blog-card-grid.tsx"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"meta": {
|
|
24
|
+
"spec": "components/sections/spec.md",
|
|
25
|
+
"figma": "2398:23021",
|
|
26
|
+
"status": "draft",
|
|
27
|
+
"block": true
|
|
28
|
+
},
|
|
29
|
+
"type": "registry:block"
|
|
30
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "breadcrumb",
|
|
4
|
+
"title": "Breadcrumb",
|
|
5
|
+
"description": "Baseline breadcrumb (ready). Spec: components/breadcrumb/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/breadcrumb/breadcrumb.tsx",
|
|
16
|
+
"content": "// baseline:breadcrumb@0.1.0\n// Fork of shadcn/ui Breadcrumb (base-ui flavour, source read 2026-10-01). Spec: components/breadcrumb/spec.md.\n// Geometry comes from Figma through the breadcrumb/* tokens (ADR-0017); the separator and ellipsis glyphs are Baseline\n// system icons (chevron-right, more-horizontal), so they follow the active icon set.\n//\n// Ladder step 1: nav > ol > li with real <a> links. BreadcrumbLink uses Base UI useRender for render={<Link />}, which\n// keeps the <a> (ADR-0030). No state, no directive: server-safe.\nimport * as React from \"react\";\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport { cn } from \"@/lib/utils\";\nimport { Icon } from \"@/registry/ui/icon/icon\";\n\nfunction Breadcrumb({ className, ...props }: React.ComponentProps<\"nav\">) {\n return <nav aria-label=\"breadcrumb\" data-slot=\"breadcrumb\" className={cn(className)} {...props} />;\n}\n\nfunction BreadcrumbList({ className, ...props }: React.ComponentProps<\"ol\">) {\n return (\n <ol\n data-slot=\"breadcrumb-list\"\n className={cn(\n \"flex flex-wrap items-center gap-(--bl-breadcrumb-gap) wrap-break-word text-muted-foreground\",\n \"text-(length:--bl-breadcrumb-font-size) leading-(--bl-breadcrumb-line-height)\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction BreadcrumbItem({ className, ...props }: React.ComponentProps<\"li\">) {\n return <li data-slot=\"breadcrumb-item\" className={cn(\"inline-flex items-center gap-(--bl-breadcrumb-item-gap)\", className)} {...props} />;\n}\n\n/** A link in the trail: muted, foreground on hover. `render={<Link href=\"…\" />}` keeps the router's link. */\nfunction BreadcrumbLink({ className, render, ...props }: useRender.ComponentProps<\"a\">) {\n return useRender({\n defaultTagName: \"a\",\n props: mergeProps<\"a\">({ className: cn(\"transition-colors hover:text-foreground\", className) }, props),\n render,\n state: { slot: \"breadcrumb-link\" },\n });\n}\n\n/** The current page: not a link, aria-current=\"page\", in the foreground colour. */\nfunction BreadcrumbPage({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"breadcrumb-page\"\n role=\"link\"\n aria-disabled=\"true\"\n aria-current=\"page\"\n className={cn(\"font-normal text-foreground\", className)}\n {...props}\n />\n );\n}\n\n/** A chevron between items (the system icon, mirrored in right-to-left text). Pass children for another separator. */\nfunction BreadcrumbSeparator({ children, className, ...props }: React.ComponentProps<\"li\">) {\n return (\n <li\n data-slot=\"breadcrumb-separator\"\n role=\"presentation\"\n aria-hidden=\"true\"\n // Icon is an inline-block span; upstream's svg is a block through preflight, which centres it in the row. As inline it\n // sat on the text baseline 1.5px low (found 2026-10-01).\n className={cn(\"[&>svg]:size-(--bl-breadcrumb-separator-icon-size) [&>[data-slot=icon]]:block [&>[data-slot=icon]]:size-(--bl-breadcrumb-separator-icon-size)\", className)}\n {...props}\n >\n {children ?? <Icon name=\"chevron-right\" className=\"rtl:-scale-x-100\" />}\n </li>\n );\n}\n\n/**\n * Collapsed items. Only the glyph is hidden from assistive tech, so the sr-only \"More\" is announced (upstream hides the\n * whole span, which silences it). Usually the trigger of a Dropdown Menu.\n */\nfunction BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"breadcrumb-ellipsis\"\n className={cn(\n \"flex size-(--bl-breadcrumb-ellipsis-size) items-center justify-center\",\n \"[&>svg]:size-(--bl-breadcrumb-ellipsis-icon-size) [&>[data-slot=icon]]:size-(--bl-breadcrumb-ellipsis-icon-size)\",\n className,\n )}\n {...props}\n >\n <Icon name=\"more-horizontal\" />\n <span className=\"sr-only\">More</span>\n </span>\n );\n}\n\nexport { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator, BreadcrumbEllipsis };\n",
|
|
17
|
+
"type": "registry:ui",
|
|
18
|
+
"target": "components/ui/breadcrumb.tsx"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"meta": {
|
|
22
|
+
"spec": "components/breadcrumb/spec.md",
|
|
23
|
+
"figma": "475:121",
|
|
24
|
+
"upstream": "shadcn/ui breadcrumb (base-ui flavour, source read from ui.shadcn.com 2026-09-30)",
|
|
25
|
+
"status": "ready"
|
|
26
|
+
},
|
|
27
|
+
"type": "registry:ui"
|
|
28
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "button-group",
|
|
4
|
+
"title": "Button-group",
|
|
5
|
+
"description": "Baseline button-group (draft). Spec: components/button-group/spec.md",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"class-variance-authority"
|
|
8
|
+
],
|
|
9
|
+
"registryDependencies": [
|
|
10
|
+
"@baseline/utils",
|
|
11
|
+
"@baseline/separator"
|
|
12
|
+
],
|
|
13
|
+
"files": [
|
|
14
|
+
{
|
|
15
|
+
"path": "src/ui/button-group/button-group.tsx",
|
|
16
|
+
"content": "// baseline:button-group@0.1.0\n// Fork of shadcn/ui ButtonGroup (source read 2026-09-30). Spec: components/button-group/spec.md.\n//\n// Button Group owns NO geometry. Every number in it comes from the Buttons inside it: their heights, their\n// padding, their radius. The component is a flex container plus a handful of sibling selectors that collapse\n// the shared borders and square the inner corners. That is why its token list names button/<size>/* and\n// separator/thickness and nothing of its own (ADR-0017).\n//\n// Server-safe: no hooks, no directive. Upstream's ButtonGroupText takes a render prop via Base UI's useRender;\n// Baseline ships a plain <div>, because that convenience is not worth a client boundary on a component whose\n// entire job is layout. A caller who needs another element can wrap or restyle it.\nimport * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/utils\";\nimport { Separator } from \"@/registry/ui/separator/separator\";\n\n// Logical properties throughout (`rounded-s-none`, `border-s-0`), not upstream's left/right. A joined group is\n// exactly the thing that breaks in RTL if you square the wrong end, and upstream ships an RTL demo of it.\nconst buttonGroupVariants = cva(\n [\n \"flex w-fit items-stretch\",\n // a nested group is a group of groups: it gets a real gap rather than joined edges\n \"has-[>[data-slot=button-group]]:gap-(--bl-button-md-gap)\",\n // a focused child lifts above its neighbours so its ring is not clipped by the next button's border\n \"[&>*]:focus-visible:relative [&>*]:focus-visible:z-10\",\n \"[&>input]:flex-1\",\n ].join(\" \"),\n {\n variants: {\n orientation: {\n horizontal:\n \"[&>*:not(:first-child)]:rounded-s-none [&>*:not(:first-child)]:border-s-0 [&>*:not(:last-child)]:rounded-e-none\",\n vertical:\n \"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none\",\n },\n },\n defaultVariants: { orientation: \"horizontal\" },\n },\n);\n\n/**\n * Joins buttons into one unit: shared borders, square inner corners, rounded outer ones.\n *\n * It is a `role=\"group\"`, not a toolbar — no arrow-key navigation, and every button stays in the tab order.\n * If the buttons are a single-choice set, that is Toggle Group, not this.\n */\nfunction ButtonGroup({\n className,\n orientation = \"horizontal\",\n ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof buttonGroupVariants>) {\n return (\n <div\n role=\"group\"\n data-slot=\"button-group\"\n data-orientation={orientation}\n className={cn(buttonGroupVariants({ orientation }), className)}\n {...props}\n />\n );\n}\n\n// `size` exists here and not upstream, so an addon matches the height of the buttons beside it. Every value is\n// a button/<size>/* variable: the text is not a Button, but it has to line up with one exactly.\nconst buttonGroupTextVariants = cva(\n [\n \"flex items-center border border-border bg-muted font-medium text-foreground\",\n \"[&_svg]:pointer-events-none\",\n ].join(\" \"),\n {\n variants: {\n size: {\n xs: \"h-(--bl-button-xs-height) gap-(--bl-button-xs-gap) px-(--bl-button-xs-padding-x) rounded-(--bl-button-xs-radius) text-(length:--bl-button-xs-font-size) leading-(--bl-button-xs-line-height) [&_svg]:size-(--bl-button-xs-icon-size) [&_[data-slot=icon]]:size-(--bl-button-xs-icon-size)\",\n sm: \"h-(--bl-button-sm-height) gap-(--bl-button-sm-gap) px-(--bl-button-sm-padding-x) rounded-(--bl-button-sm-radius) text-(length:--bl-button-sm-font-size) leading-(--bl-button-sm-line-height) [&_svg]:size-(--bl-button-sm-icon-size) [&_[data-slot=icon]]:size-(--bl-button-sm-icon-size)\",\n md: \"h-(--bl-button-md-height) gap-(--bl-button-md-gap) px-(--bl-button-md-padding-x) rounded-(--bl-button-md-radius) text-(length:--bl-button-md-font-size) leading-(--bl-button-md-line-height) [&_svg]:size-(--bl-button-md-icon-size) [&_[data-slot=icon]]:size-(--bl-button-md-icon-size)\",\n lg: \"h-(--bl-button-lg-height) gap-(--bl-button-lg-gap) px-(--bl-button-lg-padding-x) rounded-(--bl-button-lg-radius) text-(length:--bl-button-lg-font-size) leading-(--bl-button-lg-line-height) [&_svg]:size-(--bl-button-lg-icon-size) [&_[data-slot=icon]]:size-(--bl-button-lg-icon-size)\",\n },\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\n/** A non-interactive segment inside the group — a unit, a label, a count. */\nfunction ButtonGroupText({\n className,\n size,\n ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof buttonGroupTextVariants>) {\n return (\n <div\n data-slot=\"button-group-text\"\n className={cn(buttonGroupTextVariants({ size }), className)}\n {...props}\n />\n );\n}\n\n/**\n * A rule between segments, for when the buttons share a fill and the collapsed border is not enough to read\n * as a division. It defaults to the axis that cuts ACROSS the group, so a horizontal group gets a vertical\n * rule — which is why the default here is `vertical` rather than Separator's own `horizontal`.\n */\nfunction ButtonGroupSeparator({\n className,\n orientation = \"vertical\",\n ...props\n}: React.ComponentProps<typeof Separator>) {\n return (\n <Separator\n data-slot=\"button-group-separator\"\n orientation={orientation}\n className={cn(\n \"relative m-0 self-stretch bg-input\",\n \"data-[orientation=vertical]:h-auto\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport { ButtonGroup, ButtonGroupText, ButtonGroupSeparator, buttonGroupVariants };\n",
|
|
17
|
+
"type": "registry:ui",
|
|
18
|
+
"target": "components/ui/button-group.tsx"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"meta": {
|
|
22
|
+
"spec": "components/button-group/spec.md",
|
|
23
|
+
"figma": "1145:201",
|
|
24
|
+
"upstream": "shadcn/ui button-group (base-ui flavour, source read from ui.shadcn.com 2026-09-25)",
|
|
25
|
+
"status": "draft"
|
|
26
|
+
},
|
|
27
|
+
"type": "registry:ui"
|
|
28
|
+
}
|
package/r/button.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "button",
|
|
4
|
+
"title": "Button",
|
|
5
|
+
"description": "Baseline button (ready). Spec: components/button/spec.md",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"@base-ui/react",
|
|
8
|
+
"class-variance-authority"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"@baseline/utils"
|
|
12
|
+
],
|
|
13
|
+
"files": [
|
|
14
|
+
{
|
|
15
|
+
"path": "src/ui/button/button.tsx",
|
|
16
|
+
"content": "// baseline:button@0.4.0\n// Fork of shadcn/ui Button (base-ui flavour, ADR-0005). Spec: components/button/spec.md. Vocabulary: ADR-0013.\n// Geometry comes from Figma through the button/* component tokens (ADR-0017): a designer's change to a size's\n// height, padding, gap, font size, icon size, radius, font family or weight reaches this file with `npm run tokens`.\nimport * as React from \"react\";\nimport { Button as ButtonPrimitive } from \"@base-ui/react/button\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/utils\";\n\nconst buttonVariantsRaw = cva(\n [\n // `relative` so a count Badge child can float on the corner (see the spec's \"Count badge\"); bg-clip-padding keeps\n // a translucent fill from showing under the transparent border, as upstream\n \"group/button relative inline-flex shrink-0 items-center justify-center whitespace-nowrap select-none\",\n // 1px as upstream; Figma's button/border-width is 1 and Figma-only (no codeSyntax), so the class stays `border`\n \"border border-transparent bg-clip-padding\",\n \"font-(family-name:--bl-button-font-family) font-(--bl-button-font-weight)\",\n \"transition-all outline-none\",\n // focus keeps Baseline's ring role (already primary at 25%), so upstream's ring-ring/50 is not applied on top\n \"focus-visible:ring-(length:--bl-control-ring-width) focus-visible:ring-ring\",\n \"active:not-aria-[haspopup]:translate-y-px\",\n \"disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50\",\n \"aria-invalid:border-destructive aria-invalid:ring-(length:--bl-control-ring-width) aria-invalid:ring-destructive/20\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_[data-slot=icon]]:pointer-events-none [&_[data-slot=kbd]]:ms-1\",\n // A busy button composes <Spinner data-icon=\"inline-start\"> (an Icon mask with data-slot=spinner) in place of its\n // icon, so each size below sizes [data-slot=spinner] beside [data-slot=icon].\n ].join(\" \"),\n {\n variants: {\n // upstream's variants (re-aligned 2026-10-01), except `outline`, which stays transparent (user decision)\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-primary/80\",\n secondary:\n \"bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground\",\n outline:\n \"border-border bg-transparent text-foreground hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground\",\n ghost: \"text-foreground hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground\",\n link: \"text-primary underline-offset-4 hover:underline\",\n destructive:\n \"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20\",\n },\n // One line per Figma Size; every value is a button/<size>/* variable, never a Tailwind step. The icon side takes\n // button/<size>/padding-x-icon (upstream's has-data-[icon=…] padding), its own variable since 2026-10-02 (Figma's\n // Icon axis), as Badge's.\n // --button-padding-x is the size's padding, exposed for a count Badge's position.\n size: {\n xs: \"[--button-padding-x:var(--bl-button-xs-padding-x)] h-(--bl-button-xs-height) gap-(--bl-button-xs-gap) px-(--bl-button-xs-padding-x) rounded-(--bl-button-xs-radius) text-(length:--bl-button-xs-font-size) leading-(--bl-button-xs-line-height) has-data-[icon=inline-start]:ps-(--bl-button-xs-padding-x-icon) has-data-[icon=inline-end]:pe-(--bl-button-xs-padding-x-icon) [&_svg]:size-(--bl-button-xs-icon-size) [&_[data-slot=icon]]:size-(--bl-button-xs-icon-size) [&_[data-slot=spinner]]:size-(--bl-button-xs-icon-size)\",\n sm: \"[--button-padding-x:var(--bl-button-sm-padding-x)] h-(--bl-button-sm-height) gap-(--bl-button-sm-gap) px-(--bl-button-sm-padding-x) rounded-(--bl-button-sm-radius) text-(length:--bl-button-sm-font-size) leading-(--bl-button-sm-line-height) has-data-[icon=inline-start]:ps-(--bl-button-sm-padding-x-icon) has-data-[icon=inline-end]:pe-(--bl-button-sm-padding-x-icon) [&_svg]:size-(--bl-button-sm-icon-size) [&_[data-slot=icon]]:size-(--bl-button-sm-icon-size) [&_[data-slot=spinner]]:size-(--bl-button-sm-icon-size)\",\n md: \"[--button-padding-x:var(--bl-button-md-padding-x)] h-(--bl-button-md-height) gap-(--bl-button-md-gap) px-(--bl-button-md-padding-x) rounded-(--bl-button-md-radius) text-(length:--bl-button-md-font-size) leading-(--bl-button-md-line-height) has-data-[icon=inline-start]:ps-(--bl-button-md-padding-x-icon) has-data-[icon=inline-end]:pe-(--bl-button-md-padding-x-icon) [&_svg]:size-(--bl-button-md-icon-size) [&_[data-slot=icon]]:size-(--bl-button-md-icon-size) [&_[data-slot=spinner]]:size-(--bl-button-md-icon-size)\",\n lg: \"[--button-padding-x:var(--bl-button-lg-padding-x)] h-(--bl-button-lg-height) gap-(--bl-button-lg-gap) px-(--bl-button-lg-padding-x) rounded-(--bl-button-lg-radius) text-(length:--bl-button-lg-font-size) leading-(--bl-button-lg-line-height) has-data-[icon=inline-start]:ps-(--bl-button-lg-padding-x-icon) has-data-[icon=inline-end]:pe-(--bl-button-lg-padding-x-icon) [&_svg]:size-(--bl-button-lg-icon-size) [&_[data-slot=icon]]:size-(--bl-button-lg-icon-size) [&_[data-slot=spinner]]:size-(--bl-button-lg-icon-size)\",\n \"icon-xs\": \"[--button-padding-x:var(--bl-button-xs-padding-x)] size-(--bl-button-xs-height) rounded-(--bl-button-xs-radius) [&_svg]:size-(--bl-button-xs-icon-size) [&_[data-slot=icon]]:size-(--bl-button-xs-icon-size) [&_[data-slot=spinner]]:size-(--bl-button-xs-icon-size)\",\n \"icon-sm\": \"[--button-padding-x:var(--bl-button-sm-padding-x)] size-(--bl-button-sm-height) rounded-(--bl-button-sm-radius) [&_svg]:size-(--bl-button-sm-icon-size) [&_[data-slot=icon]]:size-(--bl-button-sm-icon-size) [&_[data-slot=spinner]]:size-(--bl-button-sm-icon-size)\",\n \"icon-md\": \"[--button-padding-x:var(--bl-button-md-padding-x)] size-(--bl-button-md-height) rounded-(--bl-button-md-radius) [&_svg]:size-(--bl-button-md-icon-size) [&_[data-slot=icon]]:size-(--bl-button-md-icon-size) [&_[data-slot=spinner]]:size-(--bl-button-md-icon-size)\",\n \"icon-lg\": \"[--button-padding-x:var(--bl-button-lg-padding-x)] size-(--bl-button-lg-height) rounded-(--bl-button-lg-radius) [&_svg]:size-(--bl-button-lg-icon-size) [&_[data-slot=icon]]:size-(--bl-button-lg-icon-size) [&_[data-slot=spinner]]:size-(--bl-button-lg-icon-size)\",\n },\n },\n defaultVariants: { variant: \"default\", size: \"md\" },\n },\n);\n\n// buttonVariants returns MERGED classes. cva alone returns the base's `border-transparent` beside a variant's\n// `border-border` (and the like); Button resolves that through cn(), but a link that wears buttonVariants directly\n// (<a className={buttonVariants({ variant: \"outline\" })}>) got whichever the stylesheet orders last, so the outline link\n// had no border (found 2026-10-02). Same signature and VariantProps as the cva function.\nconst buttonVariants = (props?: Parameters<typeof buttonVariantsRaw>[0]) => cn(buttonVariantsRaw(props));\n\ntype ButtonProps = ButtonPrimitive.Props & VariantProps<typeof buttonVariants>;\n\n/**\n * Button. Owns its box and how children of a kind sit inside it; never dictates what they are.\n * Icons, <Kbd> and <Badge> compose as children and inherit currentColor.\n *\n * There is no loading prop (upstream has none; removed 2026-10-02). A busy button is composed: the Spinner takes the\n * icon's place, on the icon's side, and the button is disabled:\n * <Button disabled><Spinner data-icon=\"inline-start\" />Saving</Button>\n *\n * A count (cart items, unread messages) is a Badge child floated on the corner. Button is `relative` and exposes its\n * size's padding as --button-padding-x, so the badge starts where the padding does and never covers the label or icon:\n * <Button variant=\"ghost\" size=\"icon-md\" aria-label=\"Cart, 3 items\">\n * <Icon name=\"cart\" />\n * <Badge size=\"sm\" className=\"absolute top-0 start-[calc(100%_-_var(--button-padding-x))] -translate-y-1/2 ring-(length:--bl-overlap-ring-width) ring-background\">3</Badge>\n * </Button>\n *\n * A LINK that looks like a button is an <a> (or a router <Link>) with buttonVariants, not this component:\n * <a href=\"/pricing\" className={buttonVariants({ variant: \"outline\" })}>Pricing</a>\n * <Link href=\"/login\" className={buttonVariants()}>Log in</Link>\n * <a role=\"link\" aria-disabled=\"true\" className={buttonVariants()}>Unavailable</a> (disabled: no href)\n * Base UI's Button enforces button semantics: with nativeButton={false} it gives the rendered element role=\"button\"\n * and tabindex, so <Button render={<a href />} nativeButton={false}> is announced as a button, and its disabled state\n * keeps the href (checked 2026-10-02, @base-ui/react 1.8.0; Base UI's docs: \"Links (<a>) have their own semantics and\n * should not be rendered as buttons through the render prop\"). shadcn's base-ui Pagination does it; Baseline does not.\n * `render` with nativeButton={false} is for a BUTTON drawn on another tag, e.g. <Button render={<div />} nativeButton={false}>.\n * Base UI's Button is a client component; this wrapper carries no directive and can be used from server components.\n */\nfunction Button({ className, variant = \"default\", size = \"md\", ...props }: ButtonProps) {\n return (\n <ButtonPrimitive\n data-slot=\"button\"\n data-variant={variant ?? \"default\"}\n data-size={size ?? \"md\"}\n className={cn(buttonVariants({ variant, size }), className)}\n {...props}\n />\n );\n}\n\nexport { Button, buttonVariants };\nexport type { ButtonProps };\n",
|
|
17
|
+
"type": "registry:ui",
|
|
18
|
+
"target": "components/ui/button.tsx"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"meta": {
|
|
22
|
+
"spec": "components/button/spec.md",
|
|
23
|
+
"figma": "332:47",
|
|
24
|
+
"upstream": "shadcn/ui button (base-ui)",
|
|
25
|
+
"status": "ready"
|
|
26
|
+
},
|
|
27
|
+
"type": "registry:ui"
|
|
28
|
+
}
|
package/r/card.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "card",
|
|
4
|
+
"title": "Card",
|
|
5
|
+
"description": "Baseline card (ready). Spec: components/card/spec.md",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"@baseline/utils"
|
|
8
|
+
],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "src/ui/card/card.tsx",
|
|
12
|
+
"content": "// baseline:card@0.1.0\n// Fork of shadcn/ui Card (base-ui flavour, source read 2026-10-01). Spec: components/card/spec.md.\n// Geometry comes from Figma through the card/* tokens (ADR-0017). The outline is card/border, a tint (foreground at\n// 10 %, ADR-0017 §2), and the footer fill card/footer/background (muted at 50 %), both emitted as color-mix().\n//\n// Ladder step 1: plain elements, as upstream. No primitive, no state: server-safe.\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ntype CardSize = \"sm\" | \"md\";\n\n// One static class per size (ADR-0008). Upstream feeds one --card-spacing; Figma splits padding from the gap between\n// regions (equal today), so the root carries both, plus the radius the first and last image take.\nconst CARD_SIZES: Record<CardSize, string> = {\n sm: \"[--card-padding:var(--bl-card-sm-padding)] [--card-gap:var(--bl-card-sm-gap)] [--card-radius:var(--bl-card-sm-radius)]\",\n md: \"[--card-padding:var(--bl-card-md-padding)] [--card-gap:var(--bl-card-md-gap)] [--card-radius:var(--bl-card-md-radius)]\",\n};\n\n/**\n * A container for one subject. Compose only the regions you need. Upstream's three structural rules are kept: an image\n * as the first child goes full bleed (no top padding, the top corners); a footer removes the bottom padding; a header\n * with an action becomes two columns.\n */\nfunction Card({ className, size = \"md\", ...props }: React.ComponentProps<\"div\"> & { size?: CardSize }) {\n return (\n <div\n data-slot=\"card\"\n data-size={size}\n className={cn(\n \"group/card flex flex-col gap-(--card-gap) overflow-hidden rounded-(--card-radius) bg-card py-(--card-padding) text-card-foreground\",\n \"text-(length:--bl-card-description-font-size) leading-(--bl-card-description-line-height)\",\n \"ring-(length:--bl-card-border-width) ring-(--bl-card-border)\",\n \"has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0\",\n // Baseline Media (a figure) as the first child goes full bleed too; overflow-hidden already clips its corners\n \"has-[>[data-slot=media]:first-child]:pt-0\",\n \"*:[img:first-child]:rounded-t-(--card-radius) *:[img:last-child]:rounded-b-(--card-radius)\",\n CARD_SIZES[size],\n className,\n )}\n {...props}\n />\n );\n}\n\n// Upstream also makes the header an inline-size container (@container/card-header). Left out (2026-10-01): with it,\n// Chrome 154 rendered the CardAction button with no author styles at all, and nothing in Baseline queries that container.\n/** Title, description and an optional corner action. Add `border-b` for a bordered header (Figma's Bordered). */\nfunction CardHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-header\"\n className={cn(\n \"group/card-header grid auto-rows-min items-start rounded-t-(--card-radius) px-(--card-padding)\",\n \"gap-y-(--bl-card-header-gap) gap-x-(--bl-card-header-action-gap)\",\n \"has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]\",\n // upstream relies on shadcn's global `* { border-border }`; Baseline has none, so the colour is named here\n \"[.border-b]:border-border [.border-b]:pb-(--card-padding)\",\n className,\n )}\n {...props}\n />\n );\n}\n\n/** A div, as upstream. Where the card is a section of the document, render a heading instead (`<h3 data-slot=...>`). */\nfunction CardTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-title\"\n className={cn(\n \"font-(--bl-card-title-font-weight)\",\n \"text-(length:--bl-card-title-md-font-size) leading-(--bl-card-title-md-line-height)\",\n \"group-data-[size=sm]/card:text-(length:--bl-card-title-sm-font-size) group-data-[size=sm]/card:leading-(--bl-card-title-sm-line-height)\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction CardDescription({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-description\"\n className={cn(\n \"text-(length:--bl-card-description-font-size) leading-(--bl-card-description-line-height) text-muted-foreground\",\n className,\n )}\n {...props}\n />\n );\n}\n\n/** Top-right of the header, spanning the title and description rows. */\nfunction CardAction({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-action\"\n className={cn(\"col-start-2 row-span-2 row-start-1 self-start justify-self-end\", className)}\n {...props}\n />\n );\n}\n\nfunction CardContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div data-slot=\"card-content\" className={cn(\"px-(--card-padding)\", className)} {...props} />;\n}\n\n/** Always bordered and muted, as upstream. Add `flex-col` for stacked buttons (Figma's Orientation). */\nfunction CardFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-footer\"\n className={cn(\n \"flex items-center gap-(--bl-card-footer-gap) rounded-b-(--card-radius) border-t border-border bg-(--bl-card-footer-background) p-(--card-padding)\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };\nexport type { CardSize };\n",
|
|
13
|
+
"type": "registry:ui",
|
|
14
|
+
"target": "components/ui/card.tsx"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"meta": {
|
|
18
|
+
"spec": "components/card/spec.md",
|
|
19
|
+
"figma": "4358:14597",
|
|
20
|
+
"upstream": "shadcn/ui card (base-ui flavour, source read from ui.shadcn.com 2026-09-30)",
|
|
21
|
+
"status": "ready"
|
|
22
|
+
},
|
|
23
|
+
"type": "registry:ui"
|
|
24
|
+
}
|
package/r/carousel.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "carousel",
|
|
4
|
+
"title": "Carousel",
|
|
5
|
+
"description": "Baseline carousel (ready). Spec: components/carousel/spec.md. Embla, as upstream (embla-carousel-react): opts, plugins (autoplay among them), setApi, the arrow keys; CarouselDots is a Baseline addition",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"embla-carousel-react"
|
|
8
|
+
],
|
|
9
|
+
"registryDependencies": [
|
|
10
|
+
"@baseline/utils",
|
|
11
|
+
"@baseline/button",
|
|
12
|
+
"@baseline/icon"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "src/ui/carousel/carousel.tsx",
|
|
17
|
+
"content": "\"use client\";\n// baseline:carousel@0.1.0\n// Fork of shadcn/ui Carousel (base-ui flavour, source read 2026-10-02, re-read 2026-10-06). Spec: components/carousel/spec.md.\n// Embla, as upstream (embla-carousel-react): opts, plugins (embla-carousel-autoplay among them), setApi, the arrow keys.\n// Geometry comes from Figma through the carousel/* tokens (ADR-0017); the controls are outline icon-sm Buttons made\n// round by carousel/control-radius, carousel/control-offset outside the slides, at the logical start / end (so RTL\n// mirrors them, and the chevrons flip). CarouselDots is a Baseline addition (user, 2026-10-06): one button per scroll\n// snap, the selected one primary; beside the slides when vertical.\n// HTML, WordPress and Shopify render the same markup and run the same engine through platforms/html/src/carousel.js.\nimport * as React from \"react\";\nimport useEmblaCarousel, { type UseEmblaCarouselType } from \"embla-carousel-react\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/registry/ui/button/button\";\nimport { Icon } from \"@/registry/ui/icon/icon\";\n\ntype CarouselApi = UseEmblaCarouselType[1];\ntype UseCarouselParameters = Parameters<typeof useEmblaCarousel>;\ntype CarouselOptions = UseCarouselParameters[0];\ntype CarouselPlugin = UseCarouselParameters[1];\n\ntype CarouselProps = {\n opts?: CarouselOptions;\n plugins?: CarouselPlugin;\n orientation?: \"horizontal\" | \"vertical\";\n setApi?: (api: CarouselApi) => void;\n};\n\ntype CarouselContextProps = {\n carouselRef: ReturnType<typeof useEmblaCarousel>[0];\n api: ReturnType<typeof useEmblaCarousel>[1];\n scrollPrev: () => void;\n scrollNext: () => void;\n canScrollPrev: boolean;\n canScrollNext: boolean;\n} & CarouselProps;\n\nconst CarouselContext = React.createContext<CarouselContextProps | null>(null);\n\nfunction useCarousel() {\n const context = React.useContext(CarouselContext);\n if (!context) throw new Error(\"useCarousel must be used within a <Carousel />\");\n return context;\n}\n\nfunction Carousel({\n orientation = \"horizontal\",\n opts,\n setApi,\n plugins,\n className,\n children,\n ...props\n}: React.ComponentProps<\"div\"> & CarouselProps) {\n const [carouselRef, api] = useEmblaCarousel({ ...opts, axis: orientation === \"horizontal\" ? \"x\" : \"y\" }, plugins);\n const [canScrollPrev, setCanScrollPrev] = React.useState(false);\n const [canScrollNext, setCanScrollNext] = React.useState(false);\n\n const onSelect = React.useCallback((api: CarouselApi) => {\n if (!api) return;\n setCanScrollPrev(api.canScrollPrev());\n setCanScrollNext(api.canScrollNext());\n }, []);\n const scrollPrev = React.useCallback(() => api?.scrollPrev(), [api]);\n const scrollNext = React.useCallback(() => api?.scrollNext(), [api]);\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key === \"ArrowLeft\") {\n event.preventDefault();\n scrollPrev();\n } else if (event.key === \"ArrowRight\") {\n event.preventDefault();\n scrollNext();\n }\n },\n [scrollPrev, scrollNext],\n );\n\n React.useEffect(() => {\n if (!api || !setApi) return;\n setApi(api);\n }, [api, setApi]);\n\n React.useEffect(() => {\n if (!api) return;\n onSelect(api);\n api.on(\"reInit\", onSelect);\n api.on(\"select\", onSelect);\n return () => {\n api?.off(\"select\", onSelect);\n };\n }, [api, onSelect]);\n\n return (\n <CarouselContext.Provider\n value={{\n carouselRef,\n api,\n opts,\n orientation: orientation || (opts?.axis === \"y\" ? \"vertical\" : \"horizontal\"),\n scrollPrev,\n scrollNext,\n canScrollPrev,\n canScrollNext,\n }}\n >\n <div\n onKeyDownCapture={handleKeyDown}\n // with dots below, padding keeps their room (a margin would collapse into the next element's) and the controls stop\n // above it, so they centre on the slides\n className={cn(\n \"group/carousel relative data-[orientation=horizontal]:has-data-[slot=carousel-dots]:pb-[calc(var(--bl-carousel-dots-offset)+var(--bl-carousel-dot-size))]\",\n className,\n )}\n role=\"region\"\n aria-roledescription=\"carousel\"\n data-slot=\"carousel\"\n data-orientation={orientation}\n // for the shared script (platforms/html/src/carousel.js) that runs this markup without React: Embla's options\n data-options={opts ? JSON.stringify(opts) : undefined}\n {...props}\n >\n {children}\n </div>\n </CarouselContext.Provider>\n );\n}\n\nfunction CarouselContent({ className, ...props }: React.ComponentProps<\"div\">) {\n const { carouselRef, orientation } = useCarousel();\n return (\n <div ref={carouselRef} className=\"overflow-hidden\" data-slot=\"carousel-content\">\n <div\n className={cn(\n \"flex\",\n orientation === \"horizontal\" ? \"-ms-(--bl-carousel-gap)\" : \"-mt-(--bl-carousel-gap) flex-col\",\n className,\n )}\n {...props}\n />\n </div>\n );\n}\n\nfunction CarouselItem({ className, ...props }: React.ComponentProps<\"div\">) {\n const { orientation } = useCarousel();\n return (\n <div\n role=\"group\"\n aria-roledescription=\"slide\"\n data-slot=\"carousel-item\"\n className={cn(\n \"min-w-0 shrink-0 grow-0 basis-full\",\n orientation === \"horizontal\" ? \"ps-(--bl-carousel-gap)\" : \"pt-(--bl-carousel-gap)\",\n className,\n )}\n {...props}\n />\n );\n}\n\n// the controls: round outline icon-sm Buttons, carousel/control-offset outside the slides\nconst controlClasses = \"absolute touch-manipulation rounded-(--bl-carousel-control-radius)\";\n\nfunction CarouselPrevious({ className, variant = \"outline\", size = \"icon-sm\", ...props }: React.ComponentProps<typeof Button>) {\n const { orientation, scrollPrev, canScrollPrev } = useCarousel();\n return (\n <Button\n data-slot=\"carousel-previous\"\n variant={variant}\n size={size}\n className={cn(\n controlClasses,\n orientation === \"horizontal\"\n ? \"inset-y-0 -start-(--bl-carousel-control-offset) my-auto group-has-data-[slot=carousel-dots]/carousel:bottom-[calc(var(--bl-carousel-dots-offset)+var(--bl-carousel-dot-size))]\"\n : \"-top-(--bl-carousel-control-offset) left-1/2 -translate-x-1/2 rotate-90\",\n className,\n )}\n disabled={!canScrollPrev}\n onClick={scrollPrev}\n {...props}\n >\n <Icon name=\"chevron-left\" className=\"rtl:-scale-x-100\" />\n <span className=\"sr-only\">Previous slide</span>\n </Button>\n );\n}\n\nfunction CarouselNext({ className, variant = \"outline\", size = \"icon-sm\", ...props }: React.ComponentProps<typeof Button>) {\n const { orientation, scrollNext, canScrollNext } = useCarousel();\n return (\n <Button\n data-slot=\"carousel-next\"\n variant={variant}\n size={size}\n className={cn(\n controlClasses,\n orientation === \"horizontal\"\n ? \"inset-y-0 -end-(--bl-carousel-control-offset) my-auto group-has-data-[slot=carousel-dots]/carousel:bottom-[calc(var(--bl-carousel-dots-offset)+var(--bl-carousel-dot-size))]\"\n : \"-bottom-(--bl-carousel-control-offset) left-1/2 -translate-x-1/2 rotate-90\",\n className,\n )}\n disabled={!canScrollNext}\n onClick={scrollNext}\n {...props}\n >\n <Icon name=\"chevron-right\" className=\"rtl:-scale-x-100\" />\n <span className=\"sr-only\">Next slide</span>\n </Button>\n );\n}\n\nconst dotClasses = cn(\n \"size-(--bl-carousel-dot-size) shrink-0 rounded-(--bl-carousel-dot-radius) bg-(--bl-carousel-dot-background) transition-colors outline-none\",\n \"aria-current:bg-primary focus-visible:ring-(length:--bl-control-ring-width) focus-visible:ring-ring\",\n);\n\n/** Baseline addition: one dot per scroll snap; the selected one is primary and aria-current. */\nfunction CarouselDots({ className, ...props }: React.ComponentProps<\"div\">) {\n const { api, orientation } = useCarousel();\n const [snaps, setSnaps] = React.useState<number[]>([]);\n const [selected, setSelected] = React.useState(0);\n\n React.useEffect(() => {\n if (!api) return;\n const update = () => {\n setSnaps(api.scrollSnapList());\n setSelected(api.selectedScrollSnap());\n };\n update();\n api.on(\"select\", update);\n api.on(\"reInit\", update);\n return () => {\n api.off(\"select\", update);\n api.off(\"reInit\", update);\n };\n }, [api]);\n\n return (\n <div\n data-slot=\"carousel-dots\"\n // the shared script builds the dots where React has not (a static render has none): it gives them these classes\n data-dot-class={dotClasses}\n className={cn(\n \"flex items-center justify-center gap-(--bl-carousel-dots-gap)\",\n orientation === \"horizontal\"\n ? \"absolute inset-x-0 bottom-0\"\n : \"absolute inset-y-0 start-[calc(100%+var(--bl-carousel-dots-offset))] flex-col\",\n className,\n )}\n {...props}\n >\n {snaps.map((_, i) => (\n <button\n key={i}\n type=\"button\"\n data-slot=\"carousel-dot\"\n aria-label={`Go to slide ${i + 1}`}\n aria-current={i === selected ? \"true\" : undefined}\n onClick={() => api?.scrollTo(i)}\n className={dotClasses}\n />\n ))}\n </div>\n );\n}\n\nexport { type CarouselApi, Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext, CarouselDots, useCarousel };\n",
|
|
18
|
+
"type": "registry:ui",
|
|
19
|
+
"target": "components/ui/carousel.tsx"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"meta": {
|
|
23
|
+
"spec": "components/carousel/spec.md",
|
|
24
|
+
"figma": "4557:180",
|
|
25
|
+
"upstream": "shadcn/ui carousel (base-ui flavour, source read from ui.shadcn.com 2026-10-02, re-read 2026-10-06, unchanged)",
|
|
26
|
+
"status": "ready"
|
|
27
|
+
},
|
|
28
|
+
"type": "registry:ui"
|
|
29
|
+
}
|
package/r/checkbox.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "checkbox",
|
|
4
|
+
"title": "Checkbox",
|
|
5
|
+
"description": "Baseline checkbox (draft). Spec: components/checkbox/spec.md",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"@baseline/utils",
|
|
8
|
+
"@baseline/icon"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "src/ui/checkbox/checkbox.tsx",
|
|
13
|
+
"content": "// baseline:checkbox@0.1.0\n// Fork of shadcn/ui Checkbox (base-ui flavour, source read 2026-09-25). Spec: components/checkbox/spec.md.\n// Geometry comes from Figma through the checkbox/* tokens (ADR-0017).\n//\n// ADR-0030: this is a real <input type=\"checkbox\">, not Base UI's <button role=\"checkbox\">. Base UI replaces\n// the element; native does not, and three of the four platforms have no React, so native is the only way this\n// is the same component everywhere.\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Icon } from \"@/registry/ui/icon/icon\";\n\ntype CheckboxProps = Omit<React.ComponentProps<\"input\">, \"type\"> & {\n /**\n * Third state, for a parent checkbox over a partially-selected list. Author-driven: a user can never reach\n * it by clicking. It renders `aria-checked=\"mixed\"` and swaps the glyph for a dash.\n *\n * It deliberately does NOT set the `indeterminate` DOM property. That property is IDL-only, so setting it\n * would need a ref effect and a `'use client'` directive here, and it would have no equivalent in the\n * WordPress, Shopify or HTML templates. Since Baseline draws its own glyph, the property's only remaining\n * job would be matching `:indeterminate`, which nothing here uses. `aria-checked=\"mixed\"` is what actually\n * reaches assistive tech, and it is one attribute on every platform.\n */\n indeterminate?: boolean;\n};\n\n/**\n * Checkbox — the square multi-select control. It owns its box and its glyph and nothing else: no label, no\n * description, no error. Those belong to Field, which swaps Checkbox as its `Control`. A checkbox without a\n * label is a bug.\n *\n * Server-safe: no hooks, no directive.\n */\nfunction Checkbox({ className, indeterminate, checked, defaultChecked, ...props }: CheckboxProps) {\n return (\n <span\n data-slot=\"checkbox\"\n data-indeterminate={indeterminate ? \"true\" : undefined}\n className={cn(\n \"group/checkbox relative inline-flex shrink-0 items-center justify-center align-middle\",\n \"size-(--bl-checkbox-size)\",\n // the whole control fades together, glyph included; opacity on the input alone would leave the\n // check at full strength over a faded box\n \"has-disabled:cursor-not-allowed has-disabled:opacity-50\",\n className,\n )}\n >\n <input\n type=\"checkbox\"\n data-slot=\"checkbox-control\"\n checked={checked}\n defaultChecked={defaultChecked}\n aria-checked={indeterminate ? \"mixed\" : undefined}\n className={cn([\n \"peer size-full appearance-none bg-transparent outline-none transition-colors\",\n \"rounded-(--bl-checkbox-radius) border border-input\",\n // checked and indeterminate share one filled look\n \"checked:border-primary checked:bg-primary\",\n \"group-data-[indeterminate=true]/checkbox:border-primary group-data-[indeterminate=true]/checkbox:bg-primary\",\n \"focus-visible:border-primary focus-visible:ring-(length:--bl-control-ring-width) focus-visible:ring-ring\",\n // invalid does not override checked: a ticked-but-invalid box still has to read as ticked,\n // which is upstream's aria-invalid:aria-checked:border-primary\n \"aria-invalid:border-destructive\",\n \"aria-invalid:ring-(length:--bl-control-ring-width) aria-invalid:ring-(color:--ring-invalid)\",\n \"aria-invalid:checked:border-primary\",\n \"disabled:cursor-not-allowed\",\n // 16px fails WCAG 2.5.8 on touch, so the target is grown with a pseudo-element rather than with\n // padding, which would move the box. Inside a Field the label is clickable too, so this is the\n // floor rather than the whole story.\n \"after:absolute after:-inset-x-3 after:-inset-y-2 after:content-['']\",\n ].join(\" \"))}\n {...props}\n />\n {/* The glyphs are siblings, not pseudo-elements: a replaced element cannot have children, and the\n Iconify class for the active set lives in the generated icon map, so it has to be rendered by\n <Icon> rather than written as a literal here. They never take the click. */}\n <Icon\n name=\"check\"\n className={cn(\n \"pointer-events-none absolute inset-0 size-full text-primary-foreground\",\n \"hidden peer-checked:block\",\n \"group-data-[indeterminate=true]/checkbox:hidden\",\n )}\n />\n <Icon\n name=\"minus\"\n className={cn(\n \"pointer-events-none absolute inset-0 size-full text-primary-foreground\",\n \"hidden group-data-[indeterminate=true]/checkbox:block\",\n )}\n />\n </span>\n );\n}\n\nexport { Checkbox };\nexport type { CheckboxProps };\n",
|
|
14
|
+
"type": "registry:ui",
|
|
15
|
+
"target": "components/ui/checkbox.tsx"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"meta": {
|
|
19
|
+
"spec": "components/checkbox/spec.md",
|
|
20
|
+
"figma": "472:96",
|
|
21
|
+
"upstream": "shadcn/ui checkbox (base-ui flavour, source read from ui.shadcn.com 2026-09-25)",
|
|
22
|
+
"status": "draft"
|
|
23
|
+
},
|
|
24
|
+
"type": "registry:ui"
|
|
25
|
+
}
|
package/r/columns.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "columns",
|
|
4
|
+
"title": "Columns",
|
|
5
|
+
"description": "Baseline columns (draft). Spec: components/columns/spec.md",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"@baseline/utils"
|
|
8
|
+
],
|
|
9
|
+
"files": [
|
|
10
|
+
{
|
|
11
|
+
"path": "src/ui/columns/columns.tsx",
|
|
12
|
+
"content": "// baseline:columns@0.2.0\n// Baseline layout component (no shadcn counterpart). Spec: components/columns/spec.md.\n// A CSS grid of `columns` tracks (1-5) whose cells span tracks, the model Figma's Columns/2 ratios are built on\n// (1:2 = 3 tracks, spans 1 and 2). It collapses like Figma's Device variants: one column below md (Mobile); the tracks\n// from md (Tablet), except four, which are 2 x 2 until xl; spans are capped at the tracks shown, so a span-3 cell in four\n// tracks is full width on Tablet. Cells that do not fill a row wrap. Any child is a cell (span 1); Column sets a span.\n//\n// Ladder step 1: plain elements. No state: server-safe.\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ntype Tracks = 1 | 2 | 3 | 4 | 5;\ntype Step = \"none\" | \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\";\n\n// Static class maps (ADR-0008). --cols is the number of tracks shown at each breakpoint; Column caps its span with it.\nconst TRACKS: Record<Tracks, string> = {\n 1: \"\",\n 2: \"md:grid-cols-2 md:[--cols:2]\",\n 3: \"md:grid-cols-3 md:[--cols:3]\",\n 4: \"md:grid-cols-2 md:[--cols:2] xl:grid-cols-4 xl:[--cols:4]\",\n 5: \"md:grid-cols-5 md:[--cols:5]\",\n};\nconst GAP: Record<Step, string> = {\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// Figma's Invert Order: the source keeps the desktop order; once stacked (below md) the last cell comes first.\nconst INVERT = \"max-md:[&>:last-child]:order-first\";\n\n/**\n * Lays its children out on `columns` grid tracks (1-5) that collapse with the viewport. A child spans one track; wrap\n * it in Column to span more (`<Columns columns={3}><Column>…</Column><Column span={2}>…</Column></Columns>` is 1:2).\n * `invertOrder` puts the last cell first once they stack (an image on the right shows above the text on mobile).\n */\nfunction Columns({\n columns = 2,\n gap = \"sm\",\n invertOrder = false,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & { columns?: Tracks; gap?: Step; invertOrder?: boolean }) {\n return (\n <div\n data-slot=\"columns\"\n data-columns={columns}\n className={cn(\"grid grid-cols-1 *:min-w-0\", TRACKS[columns], GAP[gap], invertOrder && INVERT, className)}\n {...props}\n />\n );\n}\n\n/** A cell of Columns spanning `span` tracks (1-5) from md, capped at the tracks shown; one column below md. */\nfunction Column({\n span = 1,\n className,\n style,\n ...props\n}: React.ComponentProps<\"div\"> & { span?: Tracks }) {\n return (\n <div\n data-slot=\"column\"\n data-span={span}\n // The span is a value, not a class (a class per span x tracks would be 25 combinations): one static class reads it.\n style={{ ...style, [\"--span\" as string]: span }}\n className={cn(\"md:[grid-column:span_min(var(--span),var(--cols,1))]\", className)}\n {...props}\n />\n );\n}\n\nexport { Columns, Column };\n",
|
|
13
|
+
"type": "registry:ui",
|
|
14
|
+
"target": "components/ui/columns.tsx"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"meta": {
|
|
18
|
+
"spec": "components/columns/spec.md",
|
|
19
|
+
"figma": "2282:9253",
|
|
20
|
+
"upstream": "none (a Baseline layout component; shadcn has no counterpart)",
|
|
21
|
+
"status": "draft"
|
|
22
|
+
},
|
|
23
|
+
"type": "registry:ui"
|
|
24
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "contact-centered",
|
|
4
|
+
"title": "Contact Centered",
|
|
5
|
+
"description": "Baseline block contact-centered (draft): a composed section to copy and own. Spec: components/sections/spec.md",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"@baseline/utils",
|
|
8
|
+
"@baseline/section",
|
|
9
|
+
"@baseline/container",
|
|
10
|
+
"@baseline/columns",
|
|
11
|
+
"@baseline/button",
|
|
12
|
+
"@baseline/checkbox",
|
|
13
|
+
"@baseline/field",
|
|
14
|
+
"@baseline/input",
|
|
15
|
+
"@baseline/textarea"
|
|
16
|
+
],
|
|
17
|
+
"files": [
|
|
18
|
+
{
|
|
19
|
+
"path": "src/blocks/contact-centered/contact-centered.tsx",
|
|
20
|
+
"content": "// baseline:contact-centered@0.1.0\n// Baseline block (spec: components/sections/spec.md; Figma Contact/Centered 2504:29251). A composed section to copy and\n// own: Section > Container (content) > a heading and lead, then a contact form of Fields (first and last name, company,\n// email, phone, message, a consent checkbox) and a submit Button 28px below (spacing/7). The form posts to `action`;\n// wire it to the project's form handler.\nimport { Section } from \"@/registry/ui/section/section\";\nimport { Container } from \"@/registry/ui/container/container\";\nimport { Columns } from \"@/registry/ui/columns/columns\";\nimport { Button } from \"@/registry/ui/button/button\";\nimport { Checkbox } from \"@/registry/ui/checkbox/checkbox\";\nimport { Field, FieldContent, FieldDescription, FieldGroup, FieldLabel } from \"@/registry/ui/field/field\";\nimport { Input } from \"@/registry/ui/input/input\";\nimport { Textarea } from \"@/registry/ui/textarea/textarea\";\n\ntype ContactCenteredProps = {\n id?: string;\n title: string;\n description?: string;\n /** where the form posts */\n action?: string;\n privacyHref?: string;\n termsHref?: string;\n submitLabel?: string;\n};\n\nfunction ContactCentered({\n id = \"contact\",\n title,\n description,\n action = \"/contact\",\n privacyHref = \"/privacy\",\n termsHref = \"/terms\",\n submitLabel = \"Submit\",\n}: ContactCenteredProps) {\n const f = (name: string) => `${id}-${name}`;\n return (\n <Section aria-labelledby={`${id}-title`}>\n <Container width=\"content\">\n <Columns columns={1}>\n <div>\n <h2 id={`${id}-title`} className=\"heading-xl text-balance\">\n {title}\n </h2>\n {description ? <p className=\"mt-5 text-base leading-7 text-pretty text-muted-foreground\">{description}</p> : null}\n </div>\n </Columns>\n <Columns columns={1}>\n <form action={action} method=\"post\">\n <FieldGroup>\n <FieldGroup>\n <Field>\n <FieldLabel htmlFor={f(\"first-name\")}>First name</FieldLabel>\n <Input id={f(\"first-name\")} name=\"first_name\" autoComplete=\"given-name\" required />\n </Field>\n <Field>\n <FieldLabel htmlFor={f(\"last-name\")}>Last name</FieldLabel>\n <Input id={f(\"last-name\")} name=\"last_name\" autoComplete=\"family-name\" required />\n </Field>\n </FieldGroup>\n <Field>\n <FieldLabel htmlFor={f(\"company\")}>Company</FieldLabel>\n <Input id={f(\"company\")} name=\"company\" autoComplete=\"organization\" />\n </Field>\n <Field>\n <FieldLabel htmlFor={f(\"email\")}>Email</FieldLabel>\n <Input id={f(\"email\")} name=\"email\" type=\"email\" autoComplete=\"email\" required />\n </Field>\n <Field>\n <FieldLabel htmlFor={f(\"phone\")}>Phone number</FieldLabel>\n <Input id={f(\"phone\")} name=\"phone\" type=\"tel\" autoComplete=\"tel\" />\n </Field>\n <Field>\n <FieldLabel htmlFor={f(\"message\")}>Message</FieldLabel>\n <Textarea id={f(\"message\")} name=\"message\" required />\n </Field>\n <Field orientation=\"horizontal\">\n <Checkbox id={f(\"consent\")} name=\"consent\" required />\n <FieldContent>\n <FieldLabel htmlFor={f(\"consent\")}>I agree to the privacy policy and terms and conditions.</FieldLabel>\n <FieldDescription>\n Review our <a href={privacyHref}>privacy policy</a> and <a href={termsHref}>terms and conditions</a>.\n </FieldDescription>\n </FieldContent>\n </Field>\n </FieldGroup>\n <div className=\"mt-7\">\n <Button type=\"submit\">{submitLabel}</Button>\n </div>\n </form>\n </Columns>\n </Container>\n </Section>\n );\n}\n\nexport { ContactCentered };\nexport type { ContactCenteredProps };\n",
|
|
21
|
+
"type": "registry:component",
|
|
22
|
+
"target": "components/contact-centered.tsx"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"meta": {
|
|
26
|
+
"spec": "components/sections/spec.md",
|
|
27
|
+
"figma": "2398:23021",
|
|
28
|
+
"status": "draft",
|
|
29
|
+
"block": true
|
|
30
|
+
},
|
|
31
|
+
"type": "registry:block"
|
|
32
|
+
}
|
package/r/container.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "container",
|
|
4
|
+
"title": "Container",
|
|
5
|
+
"description": "Baseline container (draft). Spec: components/container/spec.md",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"class-variance-authority"
|
|
8
|
+
],
|
|
9
|
+
"registryDependencies": [
|
|
10
|
+
"@baseline/utils"
|
|
11
|
+
],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "src/ui/container/container.tsx",
|
|
15
|
+
"content": "// baseline:container@0.1.0\n// Baseline layout component (no shadcn counterpart). Spec: components/container/spec.md.\n// WordPress's three widths (content, wide, full) from the shared layout/* variables, plus inline padding on the\n// Responsive spacing scale. The max width includes the padding, as Figma draws it.\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 containerVariants = cva(\"mx-auto flex w-full flex-col\", {\n variants: {\n width: {\n content: \"max-w-(--bl-layout-content-width)\",\n wide: \"max-w-(--bl-layout-wide-width)\",\n full: \"max-w-none\",\n },\n paddingX: {\n none: \"px-0\",\n xs: \"px-(--bl-spacing-responsive-xs)\",\n sm: \"px-(--bl-spacing-responsive-sm)\",\n md: \"px-(--bl-spacing-responsive-md)\",\n lg: \"px-(--bl-spacing-responsive-lg)\",\n xl: \"px-(--bl-spacing-responsive-xl)\",\n \"2xl\": \"px-(--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: { width: \"content\", paddingX: \"xs\", gap: \"sm\" },\n});\n\n/**\n * Sets how wide content runs inside a Section: `content` (layout/content-width), `wide` (layout/wide-width) or `full`,\n * with inline padding (`paddingX`) and the gap between the Columns it holds, each a responsive spacing step or `none`.\n */\nfunction Container({\n width,\n paddingX,\n gap,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof containerVariants>) {\n return (\n <div\n data-slot=\"container\"\n data-width={width ?? \"content\"}\n className={cn(containerVariants({ width, paddingX, gap }), className)}\n {...props}\n />\n );\n}\n\nexport { Container, containerVariants };\n",
|
|
16
|
+
"type": "registry:ui",
|
|
17
|
+
"target": "components/ui/container.tsx"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"meta": {
|
|
21
|
+
"spec": "components/container/spec.md",
|
|
22
|
+
"figma": "1794:10438",
|
|
23
|
+
"upstream": "none (a Baseline layout component; shadcn has no counterpart)",
|
|
24
|
+
"status": "draft"
|
|
25
|
+
},
|
|
26
|
+
"type": "registry:ui"
|
|
27
|
+
}
|
package/r/cta-left.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "cta-left",
|
|
4
|
+
"title": "Cta Left",
|
|
5
|
+
"description": "Baseline block cta-left (draft): a composed section to copy and own. Spec: components/sections/spec.md",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"@baseline/utils",
|
|
8
|
+
"@baseline/section",
|
|
9
|
+
"@baseline/container",
|
|
10
|
+
"@baseline/columns",
|
|
11
|
+
"@baseline/button"
|
|
12
|
+
],
|
|
13
|
+
"files": [
|
|
14
|
+
{
|
|
15
|
+
"path": "src/blocks/cta-left/cta-left.tsx",
|
|
16
|
+
"content": "// baseline:cta-left@0.1.0\n// Baseline block (spec: components/sections/spec.md; Figma CTA/Left 2411:1148). A composed section to copy and own:\n// Section > Container (wide) > Columns/1 with a heading and two actions, start-aligned (20px apart, spacing/5).\nimport type * as React from \"react\";\nimport { Section } from \"@/registry/ui/section/section\";\nimport { Container } from \"@/registry/ui/container/container\";\nimport { Columns } from \"@/registry/ui/columns/columns\";\nimport { buttonVariants } from \"@/registry/ui/button/button\";\n\ntype SectionAction = { label: string; href: string };\n\ntype CtaLeftProps = {\n id?: string;\n title: string;\n /** the first is the primary action, the rest outline */\n actions?: SectionAction[];\n scheme?: React.ComponentProps<typeof Section>[\"scheme\"];\n};\n\nfunction CtaLeft({ id = \"cta\", title, actions = [], scheme }: CtaLeftProps) {\n return (\n <Section aria-labelledby={`${id}-title`} scheme={scheme}>\n <Container width=\"wide\">\n <Columns columns={1}>\n <div className=\"flex flex-col items-start\">\n <h2 id={`${id}-title`} className=\"heading-xl text-balance\">\n {title}\n </h2>\n {actions.length ? (\n <div className=\"mt-5 flex flex-wrap gap-2\">\n {actions.map((a, i) => (\n <a key={a.href} href={a.href} className={buttonVariants({ variant: i === 0 ? \"default\" : \"outline\" })}>\n {a.label}\n </a>\n ))}\n </div>\n ) : null}\n </div>\n </Columns>\n </Container>\n </Section>\n );\n}\n\nexport { CtaLeft };\nexport type { CtaLeftProps, SectionAction };\n",
|
|
17
|
+
"type": "registry:component",
|
|
18
|
+
"target": "components/cta-left.tsx"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"meta": {
|
|
22
|
+
"spec": "components/sections/spec.md",
|
|
23
|
+
"figma": "2398:23021",
|
|
24
|
+
"status": "draft",
|
|
25
|
+
"block": true
|
|
26
|
+
},
|
|
27
|
+
"type": "registry:block"
|
|
28
|
+
}
|
package/r/dialog.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "dialog",
|
|
4
|
+
"title": "Dialog",
|
|
5
|
+
"description": "Baseline dialog (ready). Spec: components/dialog/spec.md. Not shadcn's API (ADR-0033): no Dialog, DialogTrigger or DialogClose; give DialogContent an id and open it with <Button command=\"show-modal\" commandfor=\"<id>\">",
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"@baseline/utils",
|
|
8
|
+
"@baseline/button",
|
|
9
|
+
"@baseline/icon"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "src/ui/dialog/dialog.tsx",
|
|
14
|
+
"content": "// baseline:dialog@0.3.0\n// Fork of shadcn/ui Dialog (base-ui flavour, source read 2026-10-02). Spec: components/dialog/spec.md.\n// Geometry and the two tints come from Figma through the dialog/* tokens (ADR-0017); the backdrop is color/overlay\n// (ADR-0032).\n//\n// Ladder step 1 (ADR-0006, ADR-0030): the native <dialog>, opened and closed by invoker commands, not Base UI's Dialog,\n// which portals a <div> in its place. The browser provides the modal behaviour: top layer (no portal, no z-index), the\n// rest of the page inert, focus moved in and restored to the invoker, Escape. `closedby=\"any\"` closes on an outside\n// click where supported.\n//\n// A SERVER COMPONENT with explicit ids (ADR-0033, a deliberate divergence from upstream's API): no Dialog root, no\n// DialogTrigger or DialogClose, no context. Whatever opens or closes the dialog is a native invoker you write:\n// <Button command=\"show-modal\" commandfor=\"edit-profile\">Edit profile</Button>\n// <DialogContent id=\"edit-profile\">\n// <DialogHeader>\n// <DialogTitle id=\"edit-profile-title\">Edit profile</DialogTitle>\n// <DialogDescription id=\"edit-profile-description\">…</DialogDescription>\n// </DialogHeader>\n// …\n// <DialogFooter><Button variant=\"outline\" command=\"close\" commandfor=\"edit-profile\">Cancel</Button></DialogFooter>\n// </DialogContent>\n// For programmatic control pass a ref and call showModal() / close(); onClose reports every close.\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/registry/ui/button/button\";\nimport { Icon } from \"@/registry/ui/icon/icon\";\n\n// Invoker commands (Baseline: Chrome 135, Firefox 144, Safari 26.2) are not in @types/react 19 yet. React passes the\n// lowercase attributes through to the DOM unchanged.\ndeclare module \"react\" {\n interface ButtonHTMLAttributes<T> {\n command?: \"show-modal\" | \"close\" | \"request-close\" | \"toggle-popover\" | \"show-popover\" | \"hide-popover\" | (string & {});\n commandfor?: string;\n }\n}\n\n// The dialog is `hidden` until [open], so its grid display can transition with it (transition-discrete). The ::backdrop\n// is upstream's overlay, coloured by the color/overlay role (ADR-0032).\nconst contentClasses = [\n \"group/dialog-content fixed inset-0 m-auto h-fit max-h-[calc(100%-2rem)] w-full max-w-[calc(100%-2rem)] sm:max-w-(--bl-dialog-max-width)\",\n \"hidden open:grid gap-(--bl-dialog-gap) overflow-y-auto overscroll-contain\",\n \"rounded-(--bl-dialog-radius) border-0 bg-popover p-(--bl-dialog-padding) text-sm text-popover-foreground outline-none\",\n \"ring-(length:--bl-dialog-border-width) ring-(--bl-dialog-border)\",\n // upstream's fade + zoom-95 at 100ms, as transitions from @starting-style; none under reduced motion\n \"scale-95 opacity-0 open:scale-100 open:opacity-100 starting:open:scale-95 starting:open:opacity-0\",\n \"transition-[opacity,scale,display,overlay] transition-discrete duration-100 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-100 motion-reduce:backdrop:transition-none\",\n // a Media, aspect ratio or image as the first child goes full bleed at the top, as in Card\n \"[&>:is(img,[data-slot=media],[data-slot=aspect-ratio]):first-child]:-mx-(--bl-dialog-padding) [&>:is(img,[data-slot=media],[data-slot=aspect-ratio]):first-child]:-mt-(--bl-dialog-padding)\",\n \"[&>:is(img,[data-slot=media],[data-slot=aspect-ratio]):first-child]:w-[calc(100%+2*var(--bl-dialog-padding))] [&>:is(img,[data-slot=media],[data-slot=aspect-ratio]):first-child]:max-w-none\",\n \"[&>:is(img,[data-slot=media],[data-slot=aspect-ratio]):first-child]:rounded-t-(--bl-dialog-radius) [&>:is(img,[data-slot=media],[data-slot=aspect-ratio]):first-child]:rounded-b-none\",\n].join(\" \");\n\ntype DialogContentProps = 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 /** The corner ghost icon button (default true), a native invoker with command=\"close\". */\n showCloseButton?: boolean;\n};\n\n/**\n * The modal <dialog>. `aria-labelledby` / `aria-describedby` default to `<id>-title` / `<id>-description`; pass your own\n * (or `aria-label`) to override.\n */\nfunction DialogContent({ id, className, children, showCloseButton = true, ...props }: DialogContentProps) {\n return (\n <dialog\n id={id}\n data-slot=\"dialog-content\"\n closedby=\"any\"\n aria-labelledby={props[\"aria-label\"] ? undefined : `${id}-title`}\n aria-describedby={`${id}-description`}\n className={cn(contentClasses, 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=\"dialog-close\"\n className=\"absolute top-(--bl-dialog-close-inset) end-(--bl-dialog-close-inset)\"\n >\n <Icon name=\"close\" />\n <span className=\"sr-only\">Close</span>\n </Button>\n ) : null}\n </dialog>\n );\n}\n\nfunction DialogHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return <div data-slot=\"dialog-header\" className={cn(\"flex flex-col gap-(--bl-dialog-header-gap)\", className)} {...props} />;\n}\n\n/**\n * Always bordered and muted, bleeding to the dialog's edges, as upstream. Stacked (primary on top) below sm, a row\n * aligned to the end from sm. A close or cancel is a Button with command=\"close\" commandfor=\"<dialog id>\"; upstream's\n * `showCloseButton` on the footer is gone because the footer does not know the dialog's id (ADR-0033).\n */\nfunction DialogFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"dialog-footer\"\n className={cn(\n \"-mx-(--bl-dialog-padding) -mb-(--bl-dialog-padding) flex flex-col-reverse gap-(--bl-dialog-footer-gap) sm:flex-row sm:justify-end\",\n // upstream relies on shadcn's global `* { border-border }`; Baseline has none, so the colour is named here\n \"rounded-b-(--bl-dialog-radius) border-t-(length:--bl-dialog-border-width) border-border bg-(--bl-dialog-footer-background) p-(--bl-dialog-padding)\",\n className,\n )}\n {...props}\n />\n );\n}\n\n/** Give it `<dialog id>-title`: the dialog's accessible name points there. */\nfunction DialogTitle({ className, ...props }: React.ComponentProps<\"h2\">) {\n if (process.env.NODE_ENV !== \"production\" && !props.id) {\n console.warn(\"DialogTitle/SheetTitle has no id: give it `<dialog id>-title` so the dialog has an accessible name (ADR-0033).\");\n }\n return (\n <h2\n data-slot=\"dialog-title\"\n className={cn(\n \"font-heading text-(length:--bl-dialog-title-font-size) leading-(--bl-dialog-title-line-height) font-(--bl-dialog-title-font-weight)\",\n className,\n )}\n {...props}\n />\n );\n}\n\n/** Give it `<dialog id>-description`: the dialog's accessible description points there. */\nfunction DialogDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n return (\n <p\n data-slot=\"dialog-description\"\n className={cn(\n \"text-(length:--bl-dialog-description-font-size) leading-(--bl-dialog-description-line-height) text-muted-foreground\",\n \"*:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle };\nexport type { DialogContentProps };\n",
|
|
15
|
+
"type": "registry:ui",
|
|
16
|
+
"target": "components/ui/dialog.tsx"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"css": {
|
|
20
|
+
":root:has(dialog[open]:modal)": {
|
|
21
|
+
"overflow": "hidden"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"meta": {
|
|
25
|
+
"spec": "components/dialog/spec.md",
|
|
26
|
+
"figma": "4461:13577",
|
|
27
|
+
"upstream": "shadcn/ui dialog (base-ui flavour, source read from ui.shadcn.com 2026-10-02)",
|
|
28
|
+
"status": "ready"
|
|
29
|
+
},
|
|
30
|
+
"type": "registry:ui"
|
|
31
|
+
}
|
package/r/drawer.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "drawer",
|
|
4
|
+
"title": "Drawer",
|
|
5
|
+
"description": "Baseline drawer (ready). Spec: components/drawer/spec.md",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"@base-ui/react"
|
|
8
|
+
],
|
|
9
|
+
"registryDependencies": [
|
|
10
|
+
"@baseline/utils"
|
|
11
|
+
],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "src/ui/drawer/drawer.tsx",
|
|
15
|
+
"content": "\"use client\";\n// baseline:drawer@0.1.0\n// Fork of shadcn/ui Drawer (base-ui flavour, source read 2026-10-02; upstream moved from vaul to Base UI's Drawer).\n// Spec: components/drawer/spec.md.\n//\n// THE SECOND ADR-0030 EXCEPTION (after Select). Base UI's Drawer renders a portalled <div>, not a <dialog>, which that\n// ADR forbids, but native has no answer for what a Drawer is: swipe-to-dismiss, snap points, a backdrop that follows the\n// drag, nested drawers. Sheet (sheet.tsx) is its native counterpart and the component WordPress, Shopify and HTML use.\n// The drawn parts are shared: the header, footer, title and description read Sheet's variables, so a page mixing the two\n// does not look mixed. Upstream's literals became variables (ADR-0017): the radius, the inset, the 24rem side width\n// (Sheet's max-width) and the handle; the backdrop is the color/overlay role (ADR-0032).\nimport * as React from \"react\";\nimport { Drawer as DrawerPrimitive } from \"@base-ui/react/drawer\";\nimport { cn } from \"@/lib/utils\";\n\ntype DrawerContextProps = {\n hasSnapPoints: boolean;\n modal: DrawerPrimitive.Root.Props[\"modal\"];\n showSwipeHandle: boolean;\n swipeDirection: NonNullable<DrawerPrimitive.Root.Props[\"swipeDirection\"]>;\n};\n\nconst DrawerContext = React.createContext<DrawerContextProps | null>(null);\n\nfunction useDrawer() {\n const context = React.useContext(DrawerContext);\n if (!context) throw new Error(\"useDrawer must be used within a Drawer.\");\n return context;\n}\n\nfunction Drawer({\n modal = true,\n showSwipeHandle = false,\n snapPoints,\n swipeDirection = \"down\",\n ...props\n}: DrawerPrimitive.Root.Props & { showSwipeHandle?: boolean }) {\n const hasSnapPoints = snapPoints != null && snapPoints.length > 0;\n const contextValue = React.useMemo(\n () => ({ hasSnapPoints, modal, showSwipeHandle, swipeDirection }),\n [hasSnapPoints, modal, showSwipeHandle, swipeDirection],\n );\n return (\n <DrawerContext.Provider value={contextValue}>\n <DrawerPrimitive.Root data-slot=\"drawer\" modal={modal} snapPoints={snapPoints} swipeDirection={swipeDirection} {...props} />\n </DrawerContext.Provider>\n );\n}\n\nfunction DrawerTrigger({ ...props }: DrawerPrimitive.Trigger.Props) {\n return <DrawerPrimitive.Trigger data-slot=\"drawer-trigger\" {...props} />;\n}\n\nfunction DrawerPortal({ ...props }: DrawerPrimitive.Portal.Props) {\n return <DrawerPrimitive.Portal data-slot=\"drawer-portal\" {...props} />;\n}\n\nfunction DrawerClose({ ...props }: DrawerPrimitive.Close.Props) {\n return <DrawerPrimitive.Close data-slot=\"drawer-close\" {...props} />;\n}\n\n// upstream's bg-black/30 is the color/overlay role here (ADR-0032); the opacity still follows the swipe\nfunction DrawerOverlay({ className, ...props }: DrawerPrimitive.Backdrop.Props) {\n return (\n <DrawerPrimitive.Backdrop\n data-slot=\"drawer-overlay\"\n className={cn(\n \"fixed inset-0 z-50 min-h-dvh bg-(--overlay) opacity-[max(var(--drawer-overlay-min-opacity,0),calc(1-var(--drawer-swipe-progress)))] transition-opacity duration-450 ease-[cubic-bezier(0.32,0.72,0,1)] select-none data-ending-style:pointer-events-none data-ending-style:opacity-0 data-ending-style:duration-[calc(var(--drawer-swipe-strength)*400ms)] data-snap-points:[--drawer-overlay-min-opacity:0.5] data-starting-style:opacity-0 data-swiping:duration-0 supports-backdrop-filter:backdrop-blur-sm supports-[-webkit-touch-callout:none]:absolute\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction DrawerSwipeHandle({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"drawer-swipe-handle\"\n aria-hidden=\"true\"\n className={cn(\n \"relative z-10 flex shrink-0 cursor-grab transition-opacity duration-200 group-data-nested-drawer-open/drawer-popup:opacity-0 group-data-nested-drawer-swiping/drawer-popup:opacity-100 group-data-[swipe-axis=x]/drawer-popup:h-full group-data-[swipe-axis=x]/drawer-popup:w-3 group-data-[swipe-axis=x]/drawer-popup:items-center group-data-[swipe-axis=y]/drawer-popup:h-3 group-data-[swipe-axis=y]/drawer-popup:w-full group-data-[swipe-axis=y]/drawer-popup:justify-center group-data-[swipe-direction=down]/drawer-popup:items-end group-data-[swipe-direction=left]/drawer-popup:order-last group-data-[swipe-direction=left]/drawer-popup:justify-start group-data-[swipe-direction=right]/drawer-popup:justify-end group-data-[swipe-direction=up]/drawer-popup:order-last group-data-[swipe-direction=up]/drawer-popup:items-start after:block after:shrink-0 after:rounded-full after:bg-muted group-data-[swipe-axis=x]/drawer-popup:after:h-(--bl-drawer-handle-length) group-data-[swipe-axis=x]/drawer-popup:after:w-(--bl-drawer-handle-thickness) group-data-[swipe-axis=y]/drawer-popup:after:h-(--bl-drawer-handle-thickness) group-data-[swipe-axis=y]/drawer-popup:after:w-(--bl-drawer-handle-length) active:cursor-grabbing\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction DrawerContent({ className, children, ...props }: DrawerPrimitive.Popup.Props) {\n const { hasSnapPoints, modal, showSwipeHandle, swipeDirection } = useDrawer();\n const swipeAxis = swipeDirection === \"down\" || swipeDirection === \"up\" ? \"y\" : \"x\";\n return (\n <DrawerPortal data-slot=\"drawer-portal\">\n {modal === true && <DrawerOverlay data-snap-points={hasSnapPoints ? \"\" : undefined} />}\n <DrawerPrimitive.Viewport\n data-slot=\"drawer-viewport\"\n data-modal={modal}\n className=\"pointer-events-none fixed inset-0 z-50 select-none data-[modal=true]:pointer-events-auto\"\n >\n <DrawerPrimitive.Popup\n data-slot=\"drawer-popup\"\n data-swipe-axis={swipeAxis}\n data-snap-points={hasSnapPoints ? \"\" : undefined}\n className={cn(\n // Base. Upstream's rounded-[min(var(--radius-4xl),24px)] is drawer/radius, its --spacing(2) inset drawer/inset.\n \"group/drawer-popup pointer-events-auto fixed z-50 m-(--drawer-inset,0px) flex h-(--drawer-content-height) max-h-(--drawer-content-max-height,none) min-h-0 w-(--drawer-content-width,auto) transform-[translate3d(var(--translate-x,0px),var(--translate-y,0px),0)_scale(var(--stack-scale))] flex-col rounded-(--bl-drawer-radius) border border-popover bg-popover text-sm text-popover-foreground shadow-xl transition-[transform,height,opacity,filter] duration-450 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform outline-none select-none [--drawer-bleed-background:transparent] [--drawer-inset:var(--bl-drawer-inset)] [--drawer-stacked-shadow:0_-20px_25px_-5px_rgb(0_0_0/0.1),0_-8px_10px_-6px_rgb(0_0_0/0.1)] [interpolate-size:allow-keywords] data-[swipe-direction=down]:data-nested-drawer-open:shadow-(--drawer-stacked-shadow)\",\n // Nested.\n \"data-nested-drawer-open:overflow-hidden data-nested-drawer-open:brightness-95\",\n // Bleed.\n \"after:pointer-events-none after:absolute after:bg-(--drawer-bleed-background,var(--color-popover)) data-[swipe-axis=x]:after:inset-y-0 data-[swipe-axis=x]:after:w-(--bleed) data-[swipe-axis=y]:after:inset-x-0 data-[swipe-axis=y]:after:h-(--bleed) data-[swipe-direction=down]:after:top-full data-[swipe-direction=left]:after:right-full data-[swipe-direction=right]:after:left-full data-[swipe-direction=up]:after:bottom-full\",\n // Sizing. Left and right drawers are 75 % wide up to Sheet's max-width (upstream's 24rem).\n \"[--drawer-content-height:var(--drawer-height,auto)] data-[swipe-axis=x]:[--drawer-content-width:75%] data-[swipe-axis=y]:[--drawer-content-max-height:calc(100dvh-6rem)] data-[swipe-axis=y]:data-snap-points:[--drawer-content-height:100dvh] data-[swipe-axis=x]:sm:[--drawer-content-width:var(--bl-sheet-max-width)]\",\n // Stack.\n \"[--bleed:3rem] [--peek:1rem] [--stack-height:var(--drawer-frontmost-height,var(--drawer-height,0px))] [--stack-peek-offset:max(0px,calc((var(--nested-drawers)-var(--stack-progress))*var(--peek)))] [--stack-progress:clamp(0,var(--drawer-swipe-progress),1)] [--stack-scale-base:max(0,calc(1-(var(--nested-drawers)*var(--stack-step))))] [--stack-scale:clamp(0,calc(var(--stack-scale-base)+(var(--stack-step)*var(--stack-progress))),1)] [--stack-shrink:calc(1-var(--stack-scale))] [--stack-step:0.05]\",\n // Transitions.\n \"data-ending-style:transform-(--closed-transform) data-ending-style:opacity-[0.9999] data-ending-style:duration-[calc(var(--drawer-swipe-strength)*400ms)] data-nested-drawer-swiping:duration-0 data-ending-style:data-nested-drawer-swiping:duration-[calc(var(--drawer-swipe-strength)*400ms)] data-starting-style:transform-(--closed-transform) data-swiping:duration-0 data-ending-style:data-swiping:duration-[calc(var(--drawer-swipe-strength)*400ms)]\",\n // Axis: y.\n \"data-[swipe-axis=y]:inset-x-0 data-[swipe-axis=y]:data-nested-drawer-open:h-(--stack-height)\",\n // Axis: x.\n \"data-[swipe-axis=x]:inset-y-0 data-[swipe-axis=x]:flex-row\",\n // Direction: down.\n \"data-[swipe-direction=down]:bottom-0 data-[swipe-direction=down]:origin-bottom data-[swipe-direction=down]:[--closed-transform:translate3d(0,calc(100%+var(--drawer-inset,0px)+2px),0)] data-[swipe-direction=down]:[--translate-y:calc(var(--drawer-snap-point-offset,0px)+var(--drawer-swipe-movement-y)-var(--stack-peek-offset)-(var(--stack-shrink)*var(--stack-height)))]\",\n // Direction: up.\n \"data-[swipe-direction=up]:top-0 data-[swipe-direction=up]:origin-top data-[swipe-direction=up]:[--closed-transform:translate3d(0,calc(-100%-var(--drawer-inset,0px)-2px),0)] data-[swipe-direction=up]:[--translate-y:calc(var(--drawer-snap-point-offset,0px)+var(--drawer-swipe-movement-y)+var(--stack-peek-offset)+(var(--stack-shrink)*var(--stack-height)))]\",\n // Direction: left.\n \"data-[swipe-direction=left]:left-0 data-[swipe-direction=left]:origin-left data-[swipe-direction=left]:[--closed-transform:translate3d(calc(-100%-var(--drawer-inset,0px)-2px),0,0)] data-[swipe-direction=left]:[--translate-x:calc(var(--drawer-swipe-movement-x)+var(--stack-peek-offset)+(var(--stack-shrink)*100%))]\",\n // Direction: right.\n \"data-[swipe-direction=right]:right-0 data-[swipe-direction=right]:origin-right data-[swipe-direction=right]:[--closed-transform:translate3d(calc(100%+var(--drawer-inset,0px)+2px),0,0)] data-[swipe-direction=right]:[--translate-x:calc(var(--drawer-swipe-movement-x)-var(--stack-peek-offset)-(var(--stack-shrink)*100%))]\",\n className,\n )}\n {...props}\n >\n {showSwipeHandle && <DrawerSwipeHandle />}\n <DrawerPrimitive.Content\n data-slot=\"drawer-content\"\n className=\"flex min-h-0 flex-1 flex-col overflow-hidden overscroll-contain rounded-[inherit] transition-opacity duration-300 ease-[cubic-bezier(0.45,1.005,0,1.005)] select-text group-data-nested-drawer-open/drawer-popup:opacity-0 group-data-nested-drawer-swiping/drawer-popup:opacity-100 group-data-swiping/drawer-popup:select-none\"\n >\n {children}\n </DrawerPrimitive.Content>\n </DrawerPrimitive.Popup>\n </DrawerPrimitive.Viewport>\n </DrawerPortal>\n );\n}\n\n// Header, footer, title and description read Sheet's variables (ADR-0030: the drawn parts are shared). Upstream's\n// md:gap-1.5 on the header is left out: a breakpoint step on a Tailwind literal has no variable; Sheet's header gap holds.\nfunction DrawerHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"drawer-header\"\n className={cn(\n \"flex shrink-0 flex-col gap-(--bl-sheet-header-gap) p-(--bl-sheet-padding) pb-0 group-data-[swipe-axis=y]/drawer-popup:text-center md:text-left\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction DrawerFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"drawer-footer\"\n className={cn(\"mt-auto flex shrink-0 flex-col gap-(--bl-sheet-footer-gap) p-(--bl-sheet-padding) pt-0\", className)}\n {...props}\n />\n );\n}\n\nfunction DrawerTitle({ className, ...props }: DrawerPrimitive.Title.Props) {\n return (\n <DrawerPrimitive.Title\n data-slot=\"drawer-title\"\n className={cn(\n \"font-heading 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\nfunction DrawerDescription({ className, ...props }: DrawerPrimitive.Description.Props) {\n return (\n <DrawerPrimitive.Description\n data-slot=\"drawer-description\"\n className={cn(\n \"text-(length:--bl-sheet-description-font-size) leading-(--bl-sheet-description-line-height) text-balance text-muted-foreground\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport {\n Drawer,\n DrawerClose,\n DrawerContent,\n DrawerDescription,\n DrawerFooter,\n DrawerHeader,\n DrawerOverlay,\n DrawerPortal,\n DrawerSwipeHandle,\n DrawerTitle,\n DrawerTrigger,\n};\n",
|
|
16
|
+
"type": "registry:ui",
|
|
17
|
+
"target": "components/ui/drawer.tsx"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"meta": {
|
|
21
|
+
"spec": "components/drawer/spec.md",
|
|
22
|
+
"figma": "4468:100",
|
|
23
|
+
"upstream": "shadcn/ui drawer (base-ui flavour, @base-ui/react/drawer; upstream moved from vaul; source read 2026-10-02)",
|
|
24
|
+
"status": "ready"
|
|
25
|
+
},
|
|
26
|
+
"type": "registry:ui"
|
|
27
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "dropdown-menu",
|
|
4
|
+
"title": "Dropdown-menu",
|
|
5
|
+
"description": "Baseline dropdown-menu (ready). Spec: components/dropdown-menu/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/dropdown-menu/dropdown-menu.tsx",
|
|
16
|
+
"content": "\"use client\";\n// baseline:dropdown-menu@0.1.0\n// Fork of shadcn/ui Dropdown Menu (base-ui flavour, source read 2026-10-02). Spec: components/dropdown-menu/spec.md.\n// Geometry and the two tints come from Figma through the dropdown-menu/* tokens (ADR-0017); the panel is Popover's look.\n//\n// THE THIRD ADR-0030 EXCEPTION (user, 2026-10-06). Base UI's Menu renders a portalled positioner and a\n// <div role=\"menu\">, not the <div popover> a native menu would be. It is allowed because the menu pattern itself has\n// no native counterpart: focus moved in and back, arrow keys, Home / End, typeahead, checkbox and radio state,\n// submenus with a safe triangle, RTL. The native counterpart is Popover with a list of buttons or links.\n//\n// Motion is upstream's (100ms fade, zoom from 95%, slide from the trigger's side), written as transitions on Base UI's\n// data-starting-style / data-ending-style, which need no plugin (tw-animate-css, which upstream's classes use, is in\n// the base item since 2026-10-06; the transitions stay because they reverse cleanly when closed mid-open).\nimport * as React from \"react\";\nimport { Menu as MenuPrimitive } from \"@base-ui/react/menu\";\nimport { cn } from \"@/lib/utils\";\nimport { Icon } from \"@/registry/ui/icon/icon\";\n\nfunction DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {\n return <MenuPrimitive.Root data-slot=\"dropdown-menu\" {...props} />;\n}\n\nfunction DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) {\n return <MenuPrimitive.Portal data-slot=\"dropdown-menu-portal\" {...props} />;\n}\n\n/** Unstyled, as upstream's: pass `render={<Button variant=\"outline\" />}` for a styled trigger. */\nfunction DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {\n return <MenuPrimitive.Trigger data-slot=\"dropdown-menu-trigger\" {...props} />;\n}\n\nconst contentClasses = [\n \"z-50 max-h-(--available-height) w-(--anchor-width) min-w-(--bl-dropdown-menu-min-width) origin-(--transform-origin)\",\n \"overflow-x-hidden overflow-y-auto rounded-(--bl-dropdown-menu-radius) bg-popover p-(--bl-dropdown-menu-padding) text-popover-foreground\",\n \"shadow-md ring-(length:--bl-dropdown-menu-border-width) ring-(--bl-dropdown-menu-border) outline-none\",\n // upstream's motion as transitions on Base UI's starting / ending style\n \"transition-[opacity,scale,translate] duration-100 motion-reduce:transition-none\",\n \"data-starting-style:scale-95 data-starting-style:opacity-0 data-ending-style:scale-95 data-ending-style:opacity-0\",\n \"data-[side=bottom]:data-starting-style:-translate-y-2 data-[side=top]:data-starting-style:translate-y-2\",\n \"data-[side=right]:data-starting-style:-translate-x-2 data-[side=left]:data-starting-style:translate-x-2\",\n \"data-[side=inline-end]:data-starting-style:-translate-x-2 data-[side=inline-start]:data-starting-style:translate-x-2\",\n].join(\" \");\n\nfunction DropdownMenuContent({\n align = \"start\",\n alignOffset = 0,\n side = \"bottom\",\n sideOffset = 4,\n className,\n ...props\n}: MenuPrimitive.Popup.Props & Pick<MenuPrimitive.Positioner.Props, \"align\" | \"alignOffset\" | \"side\" | \"sideOffset\">) {\n return (\n <MenuPrimitive.Portal>\n <MenuPrimitive.Positioner\n className=\"isolate z-50 outline-none\"\n align={align}\n alignOffset={alignOffset}\n side={side}\n sideOffset={sideOffset}\n >\n <MenuPrimitive.Popup data-slot=\"dropdown-menu-content\" className={cn(contentClasses, className)} {...props} />\n </MenuPrimitive.Positioner>\n </MenuPrimitive.Portal>\n );\n}\n\nfunction DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) {\n return <MenuPrimitive.Group data-slot=\"dropdown-menu-group\" {...props} />;\n}\n\nfunction DropdownMenuLabel({ className, inset, ...props }: MenuPrimitive.GroupLabel.Props & { inset?: boolean }) {\n return (\n <MenuPrimitive.GroupLabel\n data-slot=\"dropdown-menu-label\"\n data-inset={inset}\n className={cn(\n \"px-(--bl-dropdown-menu-item-padding-x) py-(--bl-dropdown-menu-item-padding-y) text-(length:--bl-dropdown-menu-label-font-size)\",\n \"leading-(--bl-dropdown-menu-label-line-height) font-(--bl-dropdown-menu-label-font-weight) text-muted-foreground\",\n \"data-inset:ps-(--bl-dropdown-menu-item-padding-inset)\",\n className,\n )}\n {...props}\n />\n );\n}\n\n// What every row shares: the box, the type, the accent highlight, disabled, and icons at the item icon size.\nconst rowClasses = [\n \"relative flex cursor-default items-center gap-(--bl-dropdown-menu-item-gap) rounded-(--bl-dropdown-menu-item-radius)\",\n \"py-(--bl-dropdown-menu-item-padding-y) text-(length:--bl-dropdown-menu-item-font-size) leading-(--bl-dropdown-menu-item-line-height)\",\n \"outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-inset:ps-(--bl-dropdown-menu-item-padding-inset)\",\n \"data-disabled:pointer-events-none data-disabled:opacity-50\",\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-dropdown-menu-item-icon-size)\",\n \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-(--bl-dropdown-menu-item-icon-size)\",\n].join(\" \");\n\nfunction DropdownMenuItem({\n className,\n inset,\n variant = \"default\",\n ...props\n}: MenuPrimitive.Item.Props & { inset?: boolean; variant?: \"default\" | \"destructive\" }) {\n return (\n <MenuPrimitive.Item\n data-slot=\"dropdown-menu-item\"\n data-inset={inset}\n data-variant={variant}\n className={cn(\n \"group/dropdown-menu-item px-(--bl-dropdown-menu-item-padding-x)\",\n rowClasses,\n \"not-data-[variant=destructive]:focus:**:text-accent-foreground\",\n \"data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-(--bl-dropdown-menu-item-destructive-focus-background) data-[variant=destructive]:focus:text-destructive\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {\n return <MenuPrimitive.SubmenuRoot data-slot=\"dropdown-menu-sub\" {...props} />;\n}\n\nfunction DropdownMenuSubTrigger({\n className,\n inset,\n children,\n ...props\n}: MenuPrimitive.SubmenuTrigger.Props & { inset?: boolean }) {\n return (\n <MenuPrimitive.SubmenuTrigger\n data-slot=\"dropdown-menu-sub-trigger\"\n data-inset={inset}\n className={cn(\n \"px-(--bl-dropdown-menu-item-padding-x)\",\n rowClasses,\n \"focus:**:text-accent-foreground data-popup-open:bg-accent data-popup-open:text-accent-foreground\",\n className,\n )}\n {...props}\n >\n {children}\n <Icon name=\"chevron-right\" className=\"ms-auto rtl:rotate-180\" />\n </MenuPrimitive.SubmenuTrigger>\n );\n}\n\nfunction DropdownMenuSubContent({\n align = \"start\",\n alignOffset = -3,\n side = \"right\",\n sideOffset = 0,\n className,\n ...props\n}: React.ComponentProps<typeof DropdownMenuContent>) {\n return (\n <DropdownMenuContent\n data-slot=\"dropdown-menu-sub-content\"\n className={cn(\"w-auto min-w-(--bl-dropdown-menu-sub-min-width) shadow-lg\", className)}\n align={align}\n alignOffset={alignOffset}\n side={side}\n sideOffset={sideOffset}\n {...props}\n />\n );\n}\n\n// checkbox and radio rows: the start padding of a row, room at the end for the check\nconst checkableClasses = cn(\n rowClasses,\n \"ps-(--bl-dropdown-menu-item-padding-x) pe-(--bl-dropdown-menu-item-padding-end-indicator) focus:**:text-accent-foreground\",\n);\nconst indicatorClasses =\n \"pointer-events-none absolute end-(--bl-dropdown-menu-item-indicator-inset) flex items-center justify-center\";\n\nfunction DropdownMenuCheckboxItem({\n className,\n children,\n checked,\n inset,\n ...props\n}: MenuPrimitive.CheckboxItem.Props & { inset?: boolean }) {\n return (\n <MenuPrimitive.CheckboxItem\n data-slot=\"dropdown-menu-checkbox-item\"\n data-inset={inset}\n className={cn(checkableClasses, className)}\n checked={checked}\n {...props}\n >\n <span className={indicatorClasses} data-slot=\"dropdown-menu-checkbox-item-indicator\">\n <MenuPrimitive.CheckboxItemIndicator>\n <Icon name=\"check\" />\n </MenuPrimitive.CheckboxItemIndicator>\n </span>\n {children}\n </MenuPrimitive.CheckboxItem>\n );\n}\n\nfunction DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {\n return <MenuPrimitive.RadioGroup data-slot=\"dropdown-menu-radio-group\" {...props} />;\n}\n\nfunction DropdownMenuRadioItem({\n className,\n children,\n inset,\n ...props\n}: MenuPrimitive.RadioItem.Props & { inset?: boolean }) {\n return (\n <MenuPrimitive.RadioItem\n data-slot=\"dropdown-menu-radio-item\"\n data-inset={inset}\n className={cn(checkableClasses, className)}\n {...props}\n >\n <span className={indicatorClasses} data-slot=\"dropdown-menu-radio-item-indicator\">\n <MenuPrimitive.RadioItemIndicator>\n <Icon name=\"check\" />\n </MenuPrimitive.RadioItemIndicator>\n </span>\n {children}\n </MenuPrimitive.RadioItem>\n );\n}\n\nfunction DropdownMenuSeparator({ className, ...props }: MenuPrimitive.Separator.Props) {\n return (\n <MenuPrimitive.Separator\n data-slot=\"dropdown-menu-separator\"\n className={cn(\n \"-mx-(--bl-dropdown-menu-padding) my-(--bl-dropdown-menu-separator-margin-y) h-(--bl-dropdown-menu-separator-thickness) bg-border\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction DropdownMenuShortcut({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"dropdown-menu-shortcut\"\n className={cn(\n \"ms-auto text-(length:--bl-dropdown-menu-shortcut-font-size) leading-(--bl-dropdown-menu-shortcut-line-height) tracking-widest\",\n \"text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport {\n DropdownMenu,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuPortal,\n DropdownMenuRadioGroup,\n DropdownMenuRadioItem,\n DropdownMenuSeparator,\n DropdownMenuShortcut,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuTrigger,\n};\n",
|
|
17
|
+
"type": "registry:ui",
|
|
18
|
+
"target": "components/ui/dropdown-menu.tsx"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"meta": {
|
|
22
|
+
"spec": "components/dropdown-menu/spec.md",
|
|
23
|
+
"figma": "484:16",
|
|
24
|
+
"upstream": "shadcn/ui dropdown-menu (base-ui flavour, source read from ui.shadcn.com 2026-10-02)",
|
|
25
|
+
"status": "ready"
|
|
26
|
+
},
|
|
27
|
+
"type": "registry:ui"
|
|
28
|
+
}
|