From d2bb01985b9eeec2dcedc6b359ddbed1310a622b Mon Sep 17 00:00:00 2001 From: Armando Anaya Date: Tue, 1 Sep 2026 11:49:22 -0700 Subject: [PATCH] Paint menu, select and combobox surfaces on the page's own palette `menuColor: inverted` gave every floating surface the literal `dark` class, so a menu was the dark theme's `popover` whatever the page was doing. In a light application the panel arrives as a black rectangle that reads as a different product, and the first consumer to open a dropdown on a light page sees the design system disagree with itself. The preset flag moves to `default` and the three affected primitives are regenerated from the registry, so they stay shadcn-canonical and the additive gate keeps comparing like for like. The tooltip is untouched: Nova paints it `bg-foreground text-background`, which flips by construction. DESIGN.md records the new rule in place of the old one, and the config gate moves with it. --- DESIGN.md | 12 +++++++----- components.json | 2 +- gates/tokens.test.mjs | 2 +- package.json | 2 +- shadcn/combobox.tsx | 2 +- shadcn/dropdown-menu.tsx | 4 ++-- shadcn/select.tsx | 4 +++- src/primitives/combobox.tsx | 2 +- src/primitives/dropdown-menu.tsx | 4 ++-- src/primitives/select.tsx | 4 +++- 10 files changed, 22 insertions(+), 16 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index f4d30c7..80748f1 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -47,7 +47,7 @@ Preset `b2iH`, generated with shadcn CLI **4.19.0**, decodes to: | `font` | `geist` | `styles.css` → `--font-sans: 'Geist Variable', sans-serif` | | `heading` | `inherit` | `styles.css` → `--font-heading: var(--font-sans)` | | `radius` | `medium` | `styles.css` → `--radius: 0.625rem` | -| `menu` | `inverted` / `subtle` | `components.json` → `menuColor`, `menuAccent` | +| `menu` | `default` / `subtle` | `components.json` → `menuColor`, `menuAccent` | | `pointer` | on | `styles.css` base layer → `cursor: pointer` on pressable controls | **What `components.json` can hold** is its whole current content: `style`, `rsc` (`false`), @@ -121,11 +121,13 @@ it correctly, and the gate and this section move together or not at all. This package itself has **zero brand sites**: it declares the `--brand` token and never paints with it, and `gates/tokens.test.mjs` enforces that. -### Menus and the inverted subtree +### Menus and the page's palette -`menuColor: inverted` means every menu, select and combobox surface carries the literal `dark` -class, so a floating surface is the dark theme's `popover` whatever the page is doing. That is -contrast on purpose. The tooltip reaches the same intent without a subtree — Nova gives it +`menuColor: default` means a menu, select or combobox surface paints on the same `popover` +tokens as the page around it, and follows that page from light to dark. Nova's alternative, +`inverted`, gives each of those surfaces the literal `dark` class, so a floating panel is the +dark theme's `popover` whatever the page is doing; in a light application it arrives as a black +rectangle that reads as a different product. The tooltip is not a counterexample: Nova paints it `bg-foreground text-background`, which flips by construction, because one line of text has no palette of its own to keep in step. `menuAccent: subtle` means items highlight with `accent`, the token every other hover state uses. diff --git a/components.json b/components.json index acca9a5..c0a8f41 100644 --- a/components.json +++ b/components.json @@ -7,7 +7,7 @@ "iconLibrary": "lucide", "rtl": false, "aliases": { "components": "@/components", "utils": "@/lib/cn", "ui": "@/primitives", "lib": "@/lib", "hooks": "@/hooks" }, - "menuColor": "inverted", + "menuColor": "default", "menuAccent": "subtle", "registries": {} } diff --git a/gates/tokens.test.mjs b/gates/tokens.test.mjs index 2b572fa..8d20344 100644 --- a/gates/tokens.test.mjs +++ b/gates/tokens.test.mjs @@ -215,7 +215,7 @@ test("components.json holds the schema-supported preset fields, and no others", // The preset's own values, where the schema does have a field for them. assert.equal(config.style, "radix-nova"); assert.equal(config.iconLibrary, "lucide"); - assert.equal(config.menuColor, "inverted"); + assert.equal(config.menuColor, "default"); assert.equal(config.menuAccent, "subtle"); assert.equal(config.tailwind.baseColor, "neutral"); assert.equal(config.tailwind.css, "src/styles.css"); diff --git a/package.json b/package.json index 8971473..d228591 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@robomous/ui-core", - "version": "0.1.0", + "version": "0.1.1", "description": "Robomous design system: shadcn Nova primitives under Radix behaviour, design tokens, and the gates that keep them canonical. Extracted from Robomous/VisionSet.", "license": "Apache-2.0", "repository": { "type": "git", "url": "https://github.com/Robomous/ui-core.git" }, diff --git a/shadcn/combobox.tsx b/shadcn/combobox.tsx index 70a65e0..81afea6 100644 --- a/shadcn/combobox.tsx +++ b/shadcn/combobox.tsx @@ -112,7 +112,7 @@ function ComboboxContent({ diff --git a/shadcn/dropdown-menu.tsx b/shadcn/dropdown-menu.tsx index 98f8a97..006e74f 100644 --- a/shadcn/dropdown-menu.tsx +++ b/shadcn/dropdown-menu.tsx @@ -41,7 +41,7 @@ function DropdownMenuContent({ data-slot="dropdown-menu-content" sideOffset={sideOffset} align={align} - className={cn("dark z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )} + className={cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )} {...props} /> @@ -242,7 +242,7 @@ function DropdownMenuSubContent({ return ( ) diff --git a/shadcn/select.tsx b/shadcn/select.tsx index 40638e7..80755b8 100644 --- a/shadcn/select.tsx +++ b/shadcn/select.tsx @@ -1,3 +1,5 @@ +"use client" + import * as React from "react" import { Select as SelectPrimitive } from "radix-ui" @@ -67,7 +69,7 @@ function SelectContent({ diff --git a/src/primitives/dropdown-menu.tsx b/src/primitives/dropdown-menu.tsx index 270ad0d..1bff3c8 100644 --- a/src/primitives/dropdown-menu.tsx +++ b/src/primitives/dropdown-menu.tsx @@ -41,7 +41,7 @@ function DropdownMenuContent({ data-slot="dropdown-menu-content" sideOffset={sideOffset} align={align} - className={cn("dark z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )} + className={cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )} {...props} /> @@ -242,7 +242,7 @@ function DropdownMenuSubContent({ return ( ) diff --git a/src/primitives/select.tsx b/src/primitives/select.tsx index 78c8784..b0d5c45 100644 --- a/src/primitives/select.tsx +++ b/src/primitives/select.tsx @@ -1,3 +1,5 @@ +"use client" + import * as React from "react" import { Select as SelectPrimitive } from "radix-ui" @@ -67,7 +69,7 @@ function SelectContent({