@vvedantb/ui 0.7.0 → 0.7.2

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 CHANGED
@@ -299,7 +299,7 @@ import {
299
299
  - `SidebarPanel`: legacy (still supported) `headerTrailing`, `onClose`, `closeLabel` and `closeButtonClassName`; compose `Sidebar.Panel` parts instead. `title`, `headerTrailing`, `onClose` (adds the close button; pass it on the mobile drawer only), `closeLabel`, `closeButtonClassName`, `collapsed` (`md:hidden`), `className`, `children`.
300
300
  - `SidebarAccountMenu`: legacy (still supported) `badge`, `triggerProps`, `contentProps` and `stackedLabel`; compose `Sidebar.Account*` parts instead. `name`, `email` (two-line label when set), `stackedLabel` (two-line layout even without `email`, default `false`), `imageUrl` (initials without it), `align` (`"center"`), `side` (`"right"`), `sideOffset` (8), `collisionPadding`, `contentClassName`, `children` (menu items). 0.5.2 adds `badge` (unread dot beside the trigger, in a `relative` wrapper around the menu, so the trigger's dimming and press scale skip it: `true` shows it, `false` fades it out, omitted renders no wrapper and no dot), `triggerProps` (spread onto the trigger `Button` after the defaults; `className` merges last) and `contentProps` (spread onto `DropdownMenuContent`, for example `fitViewport` or `aria-label`; no `className`, use `contentClassName`). `SidebarAccountMenuPlaceholder` is the tile-sized loading box.
301
301
  - `SignOutDialog`: legacy (still supported) `question` and `title`; compose `ConfirmDialog` parts with `alert={false}` for other copy. `open`, `onOpenChange` (not called with `false` while `pending`), `pending`, `onConfirm`, `description`, `contentClassName`, `title` (`"Sign out"`), `question` (`"Sign out of your account?"`).
302
- - Class helpers: `railTileClass`, `railTileStateClass(active)`, `railBadgeClass`, `sidebarNavRowClass(isIconOnly)`, `sidebarNavLinkTextClass(isActive)`, `sidebarSectionLabelClass`.
302
+ - Class helpers: `railTileClass`, `railTileStateClass(active)`, `railBadgeClass`, `sidebarNavRowClass(isIconOnly)`, `sidebarNavLinkTextClass(isActive)`, `sidebarNavItemClass({ active, iconOnly, selected, dragging, dropTarget })`, `sidebarNavIconClass(active)`, `sidebarSectionLabelClass`, and the row parts `sidebarNavItemLeadingClass`, `sidebarNavItemLabelClass`, `sidebarNavItemSubtitleClass`, `sidebarNavItemTrailingClass`, `sidebarNavItemActionsClass` and `sidebarNavItemIndentStyle(indent)`. See [Menu item corners and sidebar nav rows (0.7.1)](#menu-item-corners-and-sidebar-nav-rows-071) and [Sidebar nav row parts (0.7.2)](#sidebar-nav-row-parts-072).
303
303
 
304
304
  ### Header actions and account badge (0.5.2)
305
305
 
@@ -434,6 +434,8 @@ Convention: compound parts are static members of a namespace object, `<Namespace
434
434
  | `PanelHeader` | Padded header row for `PanelTitle` and `PanelActions`. |
435
435
  | `PanelTitle` / `PanelActions` | The `<h2>` and the trailing actions. |
436
436
  | `PanelClose` | Ghost close button. `label` (`"Close navigation"`). Without `onClick` it closes the drawer. |
437
+ | `NavItem` | Panel nav row (`sidebarNavItemClass`). `active` (fill, `font-medium`, `aria-current="page"`), `iconOnly`, `icon` (element; gets `sidebarNavIconClass`), `asChild` for a router link. A `<button type="button">` without `asChild`. 0.7.2: `selected`, `indent`, `dragging`, `dropTarget`; see [Sidebar nav row parts (0.7.2)](#sidebar-nav-row-parts-072). |
438
+ | `NavItemLeading` / `NavItemLabel` / `NavItemTrailing` / `NavItemActions` | Row parts (0.7.2): leading slot, truncated label, always-visible trailing content and hover-revealed actions. `<span>`, or the child with `asChild`. |
437
439
  | `AccountMenu` | Dropdown root. `badge` adds the unread dot (`AccountBadge`) in a `relative` wrapper. |
438
440
  | `AccountTrigger` | Rail-tile-sized avatar button. `userName` (default `title` and `aria-label`, and the initials), `imageUrl`, plus `Button` props; native `name` goes to the button. |
439
441
  | `AccountAvatar` | Avatar image or initials. `userName`, `imageUrl`. |
@@ -618,6 +620,178 @@ import {
618
620
  - `Badge` `variant="destructive-soft"`: neutral border with a faint danger tint (`border-border bg-danger/10 text-danger`). Resolves to the same classes as a default badge with `className="bg-danger/10 text-danger"`. `variant="success"` already resolves to the same classes as a default badge with `className="border-success/25 bg-success/12 text-success"`.
619
621
  - `Kbd` `variant="subtle"`: a plain filled key for inline hints, `rounded bg-surface-secondary px-1.5 py-0.5 text-foreground`. No fixed height, font or size, so it follows the surrounding text.
620
622
 
623
+ ## Menu item corners and sidebar nav rows (0.7.1)
624
+
625
+ Visual change. Values in brackets are at the default `--radius: 1rem` and `--spacing: 0.25rem`.
626
+
627
+ The radius utilities now come with `tokens.css` (and `compat-shadcn-rgb.css`), so an app that imports `@vvedantb/ui/tokens` has them without another import:
628
+
629
+ | Utility | Value | Used by |
630
+ | --- | --- | --- |
631
+ | `rounded-surface` | `clamp(0.75rem, var(--radius), 1rem)` (16px) | Opt-in surfaces (Toaster `toastRadius="surface"`, `SessionsHoverCard`). Was in `utilities.css`. |
632
+ | `rounded-menu-item` | `min(var(--radius), 0.5rem)` (8px) | Menu items and sidebar nav rows. |
633
+ | `rounded-control` | `min(var(--radius), 0.625rem)` (10px) | Apps. |
634
+
635
+ The root `cn` knows these are radius classes, so `cn("rounded-lg", "rounded-menu-item")` keeps only the last one.
636
+
637
+ ### Menu items
638
+
639
+ | Piece | Before | From 0.7.1 |
640
+ | --- | --- | --- |
641
+ | `DropdownMenu` and `ContextMenu` item, sub-trigger, checkbox item and radio item | `rounded-md`, `calc(var(--radius) - 2px)` (14px) | `rounded-menu-item` (8px) |
642
+ | Menu surface | `rounded-lg` (16px) | unchanged |
643
+
644
+ ### Sidebar panel nav rows
645
+
646
+ Rows in the panel beside the rail. Rail tiles are unchanged.
647
+
648
+ | Piece | Before | From 0.7.1 |
649
+ | --- | --- | --- |
650
+ | Radius | none in the helper (apps added `rounded-lg`, 16px) | `rounded-menu-item` (8px) |
651
+ | Padding | `px-3 py-1.5` (12px / 6px) | `px-4 py-1.5` (16px / 6px) |
652
+ | Icon gap | `gap-3` (12px) | `gap-2` (8px) |
653
+ | Text | none in the helper (apps added `text-sm`, 14px / 20px) | `text-[0.8125rem] leading-[18px] tracking-[0.005em]` (13px / 18px) |
654
+ | Row height | 32px with `text-sm` | 30px |
655
+ | Width | none in the helper | `flex w-full items-center` |
656
+ | Idle text | `text-muted`, `hover:text-foreground` | `font-normal text-foreground/80`, `hover:text-foreground` |
657
+ | Active text | `text-foreground` | `font-medium text-foreground` |
658
+ | Hover and active fill | none in the helper | `hover:bg-surface-tertiary` (idle rows), `bg-surface-tertiary` (active row), same radius |
659
+ | Press | none | `active:scale-[0.99]`, `transition-[scale,background-color,color] duration-150 ease-smooth` |
660
+ | Focus ring | none | `focus-visible:ring-2 focus-visible:ring-foreground/35`, no outline |
661
+ | Icon-only row | `justify-center px-0 py-1.5` | same, plus the radius, text, fill and focus classes above |
662
+ | Icon (`sidebarNavIconClass`) | none (apps used 18px in a 20px box) | `size-[19px] shrink-0`; `text-muted-foreground` idle, `text-foreground` active |
663
+ | Section label | `px-3 py-1 text-[11px]` | `px-4 py-1 text-[11px] leading-4` (16px line). Colour still comes from the app's `.sidebar-section-label` rule. |
664
+
665
+ The hover fill is static: it does not slide between rows. While another row is hovered, the active row keeps its fill.
666
+
667
+ One call per row:
668
+
669
+ ```tsx
670
+ <nav className="px-2">
671
+ <p className={sidebarSectionLabelClass}>Library</p>
672
+ <div className="space-y-1">
673
+ <Link to="/home" aria-current={active ? "page" : undefined} className={sidebarNavItemClass({ active, iconOnly })}>
674
+ <IconHome size={19} className={sidebarNavIconClass(active)} />
675
+ {iconOnly ? null : <span className="truncate">Home</span>}
676
+ </Link>
677
+ </div>
678
+ </nav>
679
+ ```
680
+
681
+ Or the part, with the router link as the child:
682
+
683
+ ```tsx
684
+ <Sidebar.NavItem asChild active={active} iconOnly={iconOnly} icon={<IconHome />}>
685
+ <Link to="/home">{iconOnly ? null : <span className="truncate">Home</span>}</Link>
686
+ </Sidebar.NavItem>
687
+ ```
688
+
689
+ - `px-2` on the list is the panel inset: rows are full width inside it. `space-y-1` (4px) separates rows.
690
+ - `sidebarNavRowClass(isIconOnly)` and `sidebarNavLinkTextClass(isActive)` now return the classes above, so existing calls change look. `sidebarNavItemClass` is the two merged.
691
+ - Classes an app added beside the helpers, such as `rounded-lg text-sm font-medium tracking-normal transition-colors duration-200`, are now redundant. Through the root `cn` they lose to the helper when passed before it. Passed after it, or joined without `cn`, they override the new radius, size and weight. Remove them, and remove app-side active fills such as `bg-surface-tertiary`.
692
+
693
+ Colour mapping. The package has no sidebar colour tokens, so the rows use the nearest general token:
694
+
695
+ | Role | Reference look (light / dark, sRGB) | Package token (light / dark, sRGB) |
696
+ | --- | --- | --- |
697
+ | Hover and active fill | 233 234 236 / 28 28 28 | `surface-tertiary`: 234 234 234 / about 39 39 39. Light matches; dark is a step lighter. It is the rail tile's active fill too, so a rail tile and a nav row stay the same. From 0.7.2 the rows use `sidebar-accent` (234 234 234 / 28 28 28); see below. |
698
+ | Active text and icon | 9 9 11 / 250 250 250 | `foreground`: 24 24 24 / 252 252 252 |
699
+ | Idle text (80%) | 24 24 25 / 251 252 253 | `foreground` at 80% |
700
+ | Idle icon | 100 101 103 / 159 160 162 | `muted-foreground` (`--muted`): 114 114 114 / 160 160 160 |
701
+ | Focus ring (35%) | 9 9 11 / 250 250 250 | `foreground` at 35% |
702
+
703
+ ## Sidebar nav row parts (0.7.2)
704
+
705
+ One row style for every sidebar list: plain nav links and rich rows (checkbox, avatar, two-line label, nested folders, `...` menus, drag and drop, rename inputs). `Sidebar.NavItem` is the row; the parts go inside it.
706
+
707
+ Rich row (the link is the row; the parts are its children):
708
+
709
+ ```tsx
710
+ <Sidebar.NavItem asChild active={isActive} selected={selected} indent={1} dragging={isDragged} dropTarget={isOver}>
711
+ <Link to="/chat/$id" params={{ id }} draggable onDragStart={onDragStart}>
712
+ <Sidebar.NavItemLeading>
713
+ <Checkbox checked={selected} onClick={(event) => event.stopPropagation()} aria-label={`Select ${title}`} />
714
+ </Sidebar.NavItemLeading>
715
+ <Sidebar.NavItemLabel>{title}</Sidebar.NavItemLabel>
716
+ <Sidebar.NavItemTrailing>{generating ? <Spinner size={14} decorative /> : count}</Sidebar.NavItemTrailing>
717
+ <Sidebar.NavItemActions>
718
+ <DropdownMenu>
719
+ <DropdownMenuTrigger asChild>
720
+ <Button
721
+ variant="ghost"
722
+ size="icon-xs"
723
+ aria-label="Row actions"
724
+ onClick={(event) => {
725
+ event.stopPropagation();
726
+ event.preventDefault();
727
+ }}
728
+ >
729
+ <IconDots size={16} />
730
+ </Button>
731
+ </DropdownMenuTrigger>
732
+ <DropdownMenuContent>...</DropdownMenuContent>
733
+ </DropdownMenu>
734
+ </Sidebar.NavItemActions>
735
+ </Link>
736
+ </Sidebar.NavItem>
737
+ ```
738
+
739
+ Simple row (unchanged from 0.7.1):
740
+
741
+ ```tsx
742
+ <Sidebar.NavItem asChild active={active} icon={<IconHome />}>
743
+ <Link to="/home"><span className="truncate">Home</span></Link>
744
+ </Sidebar.NavItem>
745
+ ```
746
+
747
+ `Sidebar.NavItem` props added in 0.7.2:
748
+
749
+ | Prop | Effect |
750
+ | --- | --- |
751
+ | `selected` | The active fill (`bg-sidebar-accent text-foreground`) without `font-medium` or `aria-current`. `data-selected="true"`. |
752
+ | `indent` | Nesting level from 0. Left padding is `calc(1rem + indent * 12px)`: 16px, 28px, 40px, ... Set as inline `paddingLeft`, so any depth works and a `style.paddingLeft` you pass wins. Ignored with `iconOnly`. |
753
+ | `dragging` | `opacity-40 cursor-grabbing`. `data-dragging="true"`. |
754
+ | `dropTarget` | Fill plus `ring-2 ring-inset ring-focus`. `data-drop-target="true"`. |
755
+
756
+ `active` also sets `data-active="true"`. Each attribute is left off when its prop is false. The styles key off the attributes (`data-[selected=true]:`, `data-[dragging=true]:`, `data-[drop-target=true]:`), so a row that sets them itself (for example from a tree library's drag state) gets the same look. `sidebarNavItemClass` takes the same `selected`, `dragging` and `dropTarget` options for rows that use the class only.
757
+
758
+ Parts. Each renders a `<span>` with its class (`className` merges last), or merges onto its single child with `asChild`:
759
+
760
+ | Part | Class | Use |
761
+ | --- | --- | --- |
762
+ | `NavItemLeading` | `sidebarNavItemLeadingClass`: `flex shrink-0 items-center` | Checkbox, icon, avatar, status dot, folder chevron. Put `sidebarNavIconClass(active)` on an icon. A 40px avatar makes the row taller; add `py-2` to the row if you want more room. |
763
+ | `NavItemLabel` | `sidebarNavItemLabelClass`: `min-w-0 flex-1 truncate` | The title. For two lines, put `<span className="block truncate">` and `<span className={sidebarNavItemSubtitleClass}>` (`block truncate text-xs text-muted-foreground`) inside. For rename, `asChild` on the input. |
764
+ | `NavItemTrailing` | `sidebarNavItemTrailingClass`: `ml-auto flex shrink-0 items-center gap-1.5 text-xs text-muted-foreground` | Always visible: counts, unread dots, spinners, badges. |
765
+ | `NavItemActions` | `sidebarNavItemActionsClass` (below) | Hover-revealed `...` button. |
766
+
767
+ Actions sit in the row flow at the end (`ml-auto`), so an `asChild` link row needs no `relative`. The part is `h-6` with `-my-1`, so a 24px or 28px icon button does not change the 30px row height. It reserves its width while hidden, so the label truncates a little earlier. For the older overlay look, pass `className="absolute right-3"` to the part and `relative` to the row. Visibility:
768
+
769
+ - `opacity-0` by default, `transition-opacity duration-75`.
770
+ - `group-hover:opacity-100` and `group-focus-within:opacity-100`: the row is a `group`.
771
+ - `has-[[data-state=open]]:opacity-100`: a Radix menu trigger inside it is open.
772
+ - `data-[state=open]:opacity-100`: the part itself is the trigger (`asChild`).
773
+
774
+ The part adds no handlers. Call `stopPropagation()` (and `preventDefault()` inside a link) on the button so it does not run the row click.
775
+
776
+ Other rich-row needs:
777
+
778
+ - Folder rows and drop zones: render the row as a `<div>` or `<button>` (`asChild` on a `<div>` when the row holds its own buttons) and wrap it in your drop zone. Set `dropTarget` while a drag is over it.
779
+ - Indent guide lines: absolutely positioned spans in the row; add `relative` to the row.
780
+ - Context menus and hover cards: wrap `Sidebar.NavItem` as before; `ContextMenuTrigger asChild` can wrap it directly.
781
+
782
+ Classes a rich row no longer needs beside the parts: `rounded-lg`, `rounded-md`, `text-sm` (and `text-sm` on the title span), `px-3 py-1.5`, `gap-3`, `py-2`/`gap-3` for padding alone, `pl-7`/`pl-8` (use `indent`), `bg-surface-tertiary` for active, hover or selected, `ring-2 ring-inset ring-focus` and `opacity-40 cursor-grabbing` for drag states, `opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity` on the menu button, `mr-2` on a checkbox (the row `gap-2` spaces it), `truncate flex-1 min-w-0` on the title, `text-xs text-muted` on counts, and app row helpers that set any of these (a local list-item class, an outer `group py-px` wrapper, a `ui-sidebar-row` class).
783
+
784
+ ### Fill colour: `sidebar-accent`
785
+
786
+ Rows (hover, active, selected, drop target) use `bg-sidebar-accent` instead of `bg-surface-tertiary`.
787
+
788
+ | | Light | Dark |
789
+ | --- | --- | --- |
790
+ | `tokens.css` `--sidebar-accent` | `var(--surface-tertiary)` (about 234 234 234; unchanged) | `oklch(22.64% 0 0)` (28 28 28; was about 39 39 39) |
791
+ | `compat-shadcn-rgb.css` `--color-sidebar-accent` | `rgb(var(--sidebar-accent, var(--secondary)))`: the app's `--sidebar-accent` channels if defined, else `--secondary` as before | same |
792
+
793
+ Rail tiles keep `surface-tertiary`, so in dark mode the active rail tile is a step lighter than the active row. An app that imports `tokens.css` and defines its own `--sidebar-accent` overrides the fill; it must be a full colour value there (use `compat-shadcn-rgb.css` for `R G B` channels).
794
+
621
795
  ## Toaster
622
796
 
623
797
  ```tsx
@@ -643,7 +817,7 @@ Opt-in props (all default to the current look):
643
817
 
644
818
  | Prop | Values | Effect |
645
819
  | --- | --- | --- |
646
- | `toastRadius` | `"lg"` (default), `"surface"`, `"control"` | `rounded-lg`, `rounded-surface` (from `tokens/css/utilities.css`) or `rounded-field` (the input radius). |
820
+ | `toastRadius` | `"lg"` (default), `"surface"`, `"control"` | `rounded-lg`, `rounded-surface` (from `tokens.css`, or the compat file) or `rounded-field` (the input radius). |
647
821
  | `density` | `"default"`, `"compact"` | Current padding and text, or `gap-2 px-3 py-2 text-xs` with `text-xs` descriptions. |
648
822
  | `toastIcons` | `"default"`, `"filled24"` | Sonner icons or 24px filled success, info, warning and error icons, a matching loader and a 14px close cross. |
649
823
  | `closeButtonStyle` | `"default"`, `"hoverChip"` | Text-style close button or a round chip at the top-right, shown on hover and always on small screens. It turns on `closeButton` unless you set it. |
@@ -666,7 +840,7 @@ Components use `text-muted-foreground`, `primary` and `--color-*` theme variable
666
840
  Use this when the app defines shadcn-style variables as space-separated channels (`--background: 244 245 246`) and reads them as `rgb(var(--background) / 0.5)`.
667
841
 
668
842
  1. Import `compat-shadcn-rgb.css` **instead of** `tokens.css`. Never import both.
669
- 2. The app keeps `--background`, `--foreground`, `--card`, `--popover`, `--primary`, `--secondary`, `--muted`, `--accent`, `--destructive`, `--border`, `--input`, `--ring` (each with its `-foreground` where shadcn has one) and `--radius`. The compat file does not define them. It only references them, so the app can change `--primary`, `--ring`, `--radius` or `--font-sans` at runtime.
843
+ 2. The app keeps `--background`, `--foreground`, `--card`, `--popover`, `--primary`, `--secondary`, `--muted`, `--accent`, `--destructive`, `--border`, `--input`, `--ring` (each with its `-foreground` where shadcn has one) and `--radius`. The compat file does not define them. It only references them, so the app can change `--primary`, `--ring`, `--radius` or `--font-sans` at runtime. `--sidebar-accent` is optional: sidebar nav rows use it when set, else `--secondary` (0.7.2).
670
844
  3. Add the `@source` line for the package sources.
671
845
 
672
846
  ```css
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vvedantb/ui",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "license": "MIT",
5
5
  "description": "Shared React 19 UI for web apps: one JavaScript entry (primitives, shell pieces, app helpers, native tokens), design-token CSS, shared Tailwind v4 CSS and a Tailwind preset.",
6
6
  "type": "module",
@@ -9,7 +9,18 @@ import {
9
9
  AccountSeparator,
10
10
  AccountTrigger,
11
11
  } from "./SidebarAccountMenu";
12
- import { Panel, PanelActions, PanelClose, PanelHeader, PanelTitle } from "./SidebarPanel";
12
+ import {
13
+ NavItem,
14
+ NavItemActions,
15
+ NavItemLabel,
16
+ NavItemLeading,
17
+ NavItemTrailing,
18
+ Panel,
19
+ PanelActions,
20
+ PanelClose,
21
+ PanelHeader,
22
+ PanelTitle,
23
+ } from "./SidebarPanel";
13
24
  import { Rail, RailBody, RailFooter, RailHeader, RailItem, RailSeparator } from "./SidebarRail";
14
25
 
15
26
  export type {
@@ -36,6 +47,8 @@ export type {
36
47
  SidebarPanelHeaderProps,
37
48
  SidebarPanelRootProps,
38
49
  SidebarPanelTitleProps,
50
+ SidebarNavItemPartProps,
51
+ SidebarNavItemProps,
39
52
  } from "./SidebarPanel";
40
53
  export type {
41
54
  SidebarAccountAvatarProps,
@@ -68,6 +81,11 @@ export const Sidebar = {
68
81
  PanelTitle,
69
82
  PanelActions,
70
83
  PanelClose,
84
+ NavItem,
85
+ NavItemLeading,
86
+ NavItemLabel,
87
+ NavItemTrailing,
88
+ NavItemActions,
71
89
  AccountMenu,
72
90
  AccountTrigger,
73
91
  AccountAvatar,
@@ -1,8 +1,19 @@
1
- import type { ComponentPropsWithoutRef, ReactNode } from "react";
1
+ import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
2
+ import { Slot, Slottable } from "@radix-ui/react-slot";
2
3
  import { IconX } from "@tabler/icons-react";
3
4
  import { Button, type ButtonProps } from "../../src/ui/button";
4
5
  import { cn } from "../../src/utils/cn";
5
- import { panelCloseButtonClass, sidebarHeaderRowClass } from "./app-shell-classes";
6
+ import {
7
+ panelCloseButtonClass,
8
+ sidebarHeaderRowClass,
9
+ sidebarNavIconClass,
10
+ sidebarNavItemActionsClass,
11
+ sidebarNavItemClass,
12
+ sidebarNavItemIndentStyle,
13
+ sidebarNavItemLabelClass,
14
+ sidebarNavItemLeadingClass,
15
+ sidebarNavItemTrailingClass,
16
+ } from "./app-shell-classes";
6
17
  import { useOptionalSidebar } from "./SidebarChrome";
7
18
 
8
19
  export type SidebarPanelProps = {
@@ -131,3 +142,89 @@ export function PanelClose({ label = "Close navigation", onClick, className, chi
131
142
  </Button>
132
143
  );
133
144
  }
145
+
146
+ export type SidebarNavItemProps = ComponentPropsWithoutRef<"button"> & {
147
+ /** Current route: static fill, `font-medium`, `aria-current="page"`, `data-active="true"`. */
148
+ active?: boolean;
149
+ /** Collapsed panel: centred icon, no horizontal padding. */
150
+ iconOnly?: boolean;
151
+ /** Leading icon element; gets `sidebarNavIconClass(active)` merged onto it. For other leading content use `Sidebar.NavItemLeading`. */
152
+ icon?: ReactElement;
153
+ /** Render the single child element (for example a router link) as the row. */
154
+ asChild?: boolean;
155
+ /** Picked (for example in a multi-select list): the active fill without `font-medium`. Sets `data-selected="true"`. */
156
+ selected?: boolean;
157
+ /** Nesting level from 0. Each level adds 12px to the 16px left padding. Ignored with `iconOnly`. */
158
+ indent?: number;
159
+ /** Row being dragged. Sets `data-dragging="true"`. */
160
+ dragging?: boolean;
161
+ /** Row under a drag. Sets `data-drop-target="true"`. */
162
+ dropTarget?: boolean;
163
+ };
164
+
165
+ /** Panel nav row (`sidebarNavItemClass`). Renders a native `<button>`, or its child with `asChild`. Compose `NavItemLeading`, `NavItemLabel`, `NavItemTrailing` and `NavItemActions` inside. */
166
+ export function NavItem({
167
+ active = false,
168
+ iconOnly = false,
169
+ icon,
170
+ asChild = false,
171
+ selected = false,
172
+ indent = 0,
173
+ dragging = false,
174
+ dropTarget = false,
175
+ className,
176
+ style,
177
+ children,
178
+ ...props
179
+ }: SidebarNavItemProps) {
180
+ const Row = asChild ? Slot : "button";
181
+ return (
182
+ <Row
183
+ type={asChild ? undefined : "button"}
184
+ {...props}
185
+ aria-current={active ? "page" : undefined}
186
+ data-active={active || undefined}
187
+ data-selected={selected || undefined}
188
+ data-dragging={dragging || undefined}
189
+ data-drop-target={dropTarget || undefined}
190
+ style={indent > 0 && !iconOnly ? { ...sidebarNavItemIndentStyle(indent), ...style } : style}
191
+ className={cn(sidebarNavItemClass({ active, iconOnly }), className)}
192
+ >
193
+ {icon ? <Slot className={sidebarNavIconClass(active)}>{icon}</Slot> : null}
194
+ <Slottable>{children}</Slottable>
195
+ </Row>
196
+ );
197
+ }
198
+
199
+ export type SidebarNavItemPartProps = ComponentPropsWithoutRef<"span"> & {
200
+ /** Merge the part classes onto the single child element instead of a `<span>`. */
201
+ asChild?: boolean;
202
+ };
203
+
204
+ function NavItemPart({ partClass, asChild = false, className, ...props }: SidebarNavItemPartProps & { partClass: string }) {
205
+ const Part = asChild ? Slot : "span";
206
+ return <Part {...props} className={cn(partClass, className)} />;
207
+ }
208
+
209
+ /** Leading slot of a nav row: checkbox, icon, avatar or status dot (`sidebarNavItemLeadingClass`). */
210
+ export function NavItemLeading(props: SidebarNavItemPartProps) {
211
+ return <NavItemPart {...props} partClass={sidebarNavItemLeadingClass} />;
212
+ }
213
+
214
+ /** Truncated label of a nav row (`sidebarNavItemLabelClass`). `asChild` on a rename input keeps the layout. */
215
+ export function NavItemLabel(props: SidebarNavItemPartProps) {
216
+ return <NavItemPart {...props} partClass={sidebarNavItemLabelClass} />;
217
+ }
218
+
219
+ /** Always-visible trailing content of a nav row: counts, badges, spinners (`sidebarNavItemTrailingClass`). */
220
+ export function NavItemTrailing(props: SidebarNavItemPartProps) {
221
+ return <NavItemPart {...props} partClass={sidebarNavItemTrailingClass} />;
222
+ }
223
+
224
+ /**
225
+ * Hover-revealed actions of a nav row, such as a `...` menu (`sidebarNavItemActionsClass`).
226
+ * Adds no handlers: stop propagation (and prevent default inside a link) on the button.
227
+ */
228
+ export function NavItemActions(props: SidebarNavItemPartProps) {
229
+ return <NavItemPart {...props} partClass={sidebarNavItemActionsClass} />;
230
+ }
@@ -19,18 +19,111 @@ export function railTileStateClass(active: boolean): string {
19
19
  /** Count badge pinned to the bottom-right of a rail tile. */
20
20
  export const railBadgeClass = "absolute -bottom-0.5 -right-0.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-accent px-1 text-[10px] font-semibold leading-none text-accent-foreground"; // check-tokens-allow: accent
21
21
 
22
- /** Static section label, sentence case, padded to match nav rows. */
22
+ /**
23
+ * Static section label above a group of panel nav rows. `px-4` lines the text
24
+ * up with the nav row icons; the colour comes from the app's
25
+ * `.sidebar-section-label` rule.
26
+ */
23
27
  export const sidebarSectionLabelClass =
24
- "sidebar-section-label px-3 py-1 text-[11px] font-medium tracking-[-0.01em]";
28
+ "sidebar-section-label px-4 py-1 text-[11px] leading-4 font-medium tracking-[-0.01em]";
25
29
 
26
- /** Nav link text: muted until active. */
30
+ /**
31
+ * Nav row state: idle rows are `foreground/80` and get the `sidebar-accent`
32
+ * fill on hover; the active row is `font-medium text-foreground` on a static
33
+ * fill.
34
+ */
27
35
  export function sidebarNavLinkTextClass(isActive: boolean): string {
28
- return cn(isActive ? "text-foreground" : "text-muted hover:text-foreground"); // check-tokens-allow: text-muted
36
+ return isActive
37
+ ? "bg-sidebar-accent font-medium text-foreground"
38
+ : "font-normal text-foreground/80 hover:bg-sidebar-accent hover:text-foreground";
29
39
  }
30
40
 
31
- /** Padding for main sidebar nav rows. */
41
+ /**
42
+ * Panel nav row layout: full width, `rounded-menu-item` (8px), 13px / 18px
43
+ * text, focus ring and press scale. Icon-only rows centre the icon.
44
+ */
32
45
  export function sidebarNavRowClass(isIconOnly: boolean): string {
33
- return cn(isIconOnly ? "justify-center px-0 py-1.5" : "gap-3 px-3 py-1.5");
46
+ return cn(
47
+ "group flex w-full items-center rounded-menu-item py-1.5 text-[0.8125rem] leading-[18px] tracking-[0.005em] transition-[scale,background-color,color] duration-150 ease-smooth active:scale-[0.99] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/35",
48
+ isIconOnly ? "justify-center px-0" : "gap-2 px-4",
49
+ );
50
+ }
51
+
52
+ export type SidebarNavItemClassOptions = {
53
+ /** Current route. */
54
+ active?: boolean;
55
+ /** Collapsed panel: centred icon, no horizontal padding. */
56
+ iconOnly?: boolean;
57
+ /** Picked (for example in a multi-select list): the active fill, without `font-medium`. Same as `data-selected="true"`. */
58
+ selected?: boolean;
59
+ /** Row being dragged: `opacity-40 cursor-grabbing`. Same as `data-dragging="true"`. */
60
+ dragging?: boolean;
61
+ /** Row under a drag: fill plus an inset `ring-focus` ring. Same as `data-drop-target="true"`. */
62
+ dropTarget?: boolean;
63
+ };
64
+
65
+ // Row states an app can also set as data attributes on the row itself.
66
+ const sidebarNavItemDataStateClass =
67
+ "data-[selected=true]:bg-sidebar-accent data-[selected=true]:text-foreground data-[dragging=true]:cursor-grabbing data-[dragging=true]:opacity-40 data-[drop-target=true]:bg-sidebar-accent data-[drop-target=true]:text-foreground data-[drop-target=true]:ring-2 data-[drop-target=true]:ring-inset data-[drop-target=true]:ring-focus";
68
+
69
+ /**
70
+ * Whole panel nav row: `sidebarNavRowClass` plus `sidebarNavLinkTextClass`,
71
+ * plus `data-selected`, `data-dragging` and `data-drop-target` (`="true"`)
72
+ * styles. The `selected`, `dragging` and `dropTarget` options apply the same
73
+ * styles without the attributes.
74
+ */
75
+ export function sidebarNavItemClass({
76
+ active = false,
77
+ iconOnly = false,
78
+ selected = false,
79
+ dragging = false,
80
+ dropTarget = false,
81
+ }: SidebarNavItemClassOptions = {}): string {
82
+ return cn(
83
+ sidebarNavRowClass(iconOnly),
84
+ sidebarNavLinkTextClass(active),
85
+ sidebarNavItemDataStateClass,
86
+ selected && "bg-sidebar-accent text-foreground",
87
+ dragging && "cursor-grabbing opacity-40",
88
+ dropTarget && "bg-sidebar-accent text-foreground ring-2 ring-inset ring-focus",
89
+ );
90
+ }
91
+
92
+ /**
93
+ * Nested row padding: each `indent` level adds 12px to the row's 16px
94
+ * (`px-4`) left padding. Put it on the row's `style`.
95
+ */
96
+ export function sidebarNavItemIndentStyle(indent: number): { paddingLeft: string } {
97
+ return { paddingLeft: `calc(1rem + ${indent * 12}px)` };
98
+ }
99
+
100
+ /** Row part: leading checkbox, icon, avatar or status dot. */
101
+ export const sidebarNavItemLeadingClass = "flex shrink-0 items-center";
102
+
103
+ /**
104
+ * Row part: the label, which takes the free width and truncates. For two
105
+ * lines, put `block truncate` spans inside (subtitle: `sidebarNavItemSubtitleClass`).
106
+ */
107
+ export const sidebarNavItemLabelClass = "min-w-0 flex-1 truncate";
108
+
109
+ /** Second, muted line inside the label. */
110
+ export const sidebarNavItemSubtitleClass = "block truncate text-xs text-muted-foreground";
111
+
112
+ /** Row part: always-visible trailing content (counts, badges, spinners). */
113
+ export const sidebarNavItemTrailingClass = "ml-auto flex shrink-0 items-center gap-1.5 text-xs text-muted-foreground";
114
+
115
+ /**
116
+ * Row part: actions such as a `...` menu button. In the row flow at the end,
117
+ * 24px tall without growing the row. Hidden until the row is hovered or has
118
+ * focus, or a menu trigger inside it (or the part itself) is
119
+ * `data-state="open"`.
120
+ */
121
+ export const sidebarNavItemActionsClass =
122
+ "-my-1 ml-auto flex h-6 shrink-0 items-center opacity-0 transition-opacity duration-75 group-hover:opacity-100 group-focus-within:opacity-100 has-[[data-state=open]]:opacity-100 data-[state=open]:opacity-100";
123
+
124
+ /** Nav row icon (put it on the `<svg>`): 19px, muted until active. */
125
+ export function sidebarNavIconClass(active: boolean): string {
126
+ return cn("size-[19px] shrink-0", active ? "text-foreground" : "text-muted-foreground");
34
127
  }
35
128
 
36
129
  /** Sidebar header row: the panel title and its trailing actions. */
@@ -45,7 +45,16 @@ export {
45
45
  railBadgeClass,
46
46
  railTileClass,
47
47
  railTileStateClass,
48
+ sidebarNavIconClass,
49
+ sidebarNavItemActionsClass,
50
+ sidebarNavItemClass,
51
+ sidebarNavItemIndentStyle,
52
+ sidebarNavItemLabelClass,
53
+ sidebarNavItemLeadingClass,
54
+ sidebarNavItemSubtitleClass,
55
+ sidebarNavItemTrailingClass,
48
56
  sidebarNavLinkTextClass,
49
57
  sidebarNavRowClass,
50
58
  sidebarSectionLabelClass,
51
59
  } from "./app-shell-classes.js";
60
+ export type { SidebarNavItemClassOptions } from "./app-shell-classes.js";
package/src/index.ts CHANGED
@@ -158,10 +158,19 @@ export {
158
158
  railBadgeClass,
159
159
  railTileClass,
160
160
  railTileStateClass,
161
+ sidebarNavIconClass,
162
+ sidebarNavItemActionsClass,
163
+ sidebarNavItemClass,
164
+ sidebarNavItemIndentStyle,
165
+ sidebarNavItemLabelClass,
166
+ sidebarNavItemLeadingClass,
167
+ sidebarNavItemSubtitleClass,
168
+ sidebarNavItemTrailingClass,
161
169
  sidebarNavLinkTextClass,
162
170
  sidebarNavRowClass,
163
171
  sidebarSectionLabelClass,
164
172
  } from "../shell/src/app-shell-classes";
173
+ export type { SidebarNavItemClassOptions } from "../shell/src/app-shell-classes";
165
174
 
166
175
  // App helpers: confirm skip, update watcher, settings chrome, stable auth.
167
176
  export * from "../app/src/index";
@@ -17,13 +17,13 @@ export const menuContentClass = [
17
17
  ].join(" ");
18
18
 
19
19
  export const menuSubTriggerClass =
20
- "flex cursor-pointer select-none items-center gap-2 rounded-md px-2.5 py-2 text-sm text-foreground outline-hidden transition-[background-color,color,transform] duration-150 ease-out active:scale-[0.99] focus:bg-default focus:text-foreground data-highlighted:bg-default data-highlighted:text-foreground data-[state=open]:bg-default data-disabled:cursor-not-allowed [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0";
20
+ "flex cursor-pointer select-none items-center gap-2 rounded-menu-item px-2.5 py-2 text-sm text-foreground outline-hidden transition-[background-color,color,transform] duration-150 ease-out active:scale-[0.99] focus:bg-default focus:text-foreground data-highlighted:bg-default data-highlighted:text-foreground data-[state=open]:bg-default data-disabled:cursor-not-allowed [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0";
21
21
 
22
22
  export const menuItemClass =
23
- "relative flex cursor-pointer select-none items-center gap-2 rounded-md px-2.5 py-2 text-sm outline-hidden transition-[background-color,color,transform] duration-150 ease-out active:scale-[0.99] focus:bg-default focus:text-foreground data-highlighted:bg-default data-highlighted:text-foreground data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0 [&_svg]:pointer-events-none";
23
+ "relative flex cursor-pointer select-none items-center gap-2 rounded-menu-item px-2.5 py-2 text-sm outline-hidden transition-[background-color,color,transform] duration-150 ease-out active:scale-[0.99] focus:bg-default focus:text-foreground data-highlighted:bg-default data-highlighted:text-foreground data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0 [&_svg]:pointer-events-none";
24
24
 
25
25
  export const menuCheckboxRadioItemClass =
26
- "relative flex cursor-pointer select-none items-center rounded-md py-2 pl-8 pr-2.5 text-sm outline-hidden transition-[background-color,color,transform] duration-150 ease-out active:scale-[0.99] focus:bg-default focus:text-foreground data-highlighted:bg-default data-highlighted:text-foreground data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50";
26
+ "relative flex cursor-pointer select-none items-center rounded-menu-item py-2 pl-8 pr-2.5 text-sm outline-hidden transition-[background-color,color,transform] duration-150 ease-out active:scale-[0.99] focus:bg-default focus:text-foreground data-highlighted:bg-default data-highlighted:text-foreground data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50";
27
27
 
28
28
  export const menuLabelClass =
29
29
  "px-2.5 py-1.5 text-xs font-semibold uppercase tracking-widest text-muted-foreground";
@@ -14,13 +14,13 @@ export const menuContentClass = [
14
14
  ].join(" ");
15
15
 
16
16
  export const menuSubTriggerClass =
17
- "flex cursor-pointer select-none items-center gap-2 rounded-md px-2.5 py-2 max-sm:min-h-10 text-sm text-foreground outline-none transition-[background-color,color] focus:bg-default focus:text-foreground data-[highlighted]:bg-default data-[highlighted]:text-foreground data-[state=open]:bg-default data-[disabled]:cursor-not-allowed [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0";
17
+ "flex cursor-pointer select-none items-center gap-2 rounded-menu-item px-2.5 py-2 max-sm:min-h-10 text-sm text-foreground outline-none transition-[background-color,color] focus:bg-default focus:text-foreground data-[highlighted]:bg-default data-[highlighted]:text-foreground data-[state=open]:bg-default data-[disabled]:cursor-not-allowed [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0";
18
18
 
19
19
  export const menuItemClass =
20
- "relative flex cursor-pointer select-none items-center gap-2 rounded-md px-2.5 py-2 max-sm:min-h-10 text-sm outline-none transition-[background-color,color] focus:bg-default focus:text-foreground data-[highlighted]:bg-default data-[highlighted]:text-foreground data-[disabled]:pointer-events-none data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0 [&_svg]:pointer-events-none";
20
+ "relative flex cursor-pointer select-none items-center gap-2 rounded-menu-item px-2.5 py-2 max-sm:min-h-10 text-sm outline-none transition-[background-color,color] focus:bg-default focus:text-foreground data-[highlighted]:bg-default data-[highlighted]:text-foreground data-[disabled]:pointer-events-none data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0 [&_svg]:pointer-events-none";
21
21
 
22
22
  export const menuCheckboxRadioItemClass =
23
- "relative flex cursor-pointer select-none items-center rounded-md py-2 max-sm:min-h-10 pl-8 pr-2.5 text-sm outline-none transition-[background-color,color] focus:bg-default focus:text-foreground data-[highlighted]:bg-default data-[highlighted]:text-foreground data-[disabled]:pointer-events-none data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50";
23
+ "relative flex cursor-pointer select-none items-center rounded-menu-item py-2 max-sm:min-h-10 pl-8 pr-2.5 text-sm outline-none transition-[background-color,color] focus:bg-default focus:text-foreground data-[highlighted]:bg-default data-[highlighted]:text-foreground data-[disabled]:pointer-events-none data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50";
24
24
 
25
25
  export const menuLabelClass =
26
26
  "px-2.5 py-1.5 text-xs font-semibold uppercase tracking-widest text-muted-foreground";
package/src/utils/cn.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  import { type ClassValue, clsx } from "clsx";
2
2
  import { extendTailwindMerge } from "tailwind-merge";
3
3
 
4
- // `rounded-field` is a custom radius token — teach twMerge it conflicts with
5
- // `rounded-none` / `rounded-md` / etc. so Input overrides actually win.
4
+ // `rounded-field` and the tokens.css radii (`rounded-surface`,
5
+ // `rounded-menu-item`, `rounded-control`) are custom radius tokens — teach
6
+ // twMerge they conflict with `rounded-none` / `rounded-md` / etc. so the last
7
+ // radius class wins.
6
8
  const twMerge = extendTailwindMerge({
7
9
  extend: {
8
10
  theme: {
9
- borderRadius: ["field"],
11
+ borderRadius: ["field", "surface", "menu-item", "control"],
10
12
  },
11
13
  },
12
14
  });
package/tokens/README.md CHANGED
@@ -6,17 +6,17 @@ Design tokens and shared Tailwind v4 CSS for the vvedantb web apps. The CSS and
6
6
 
7
7
  | Export | File | What it is |
8
8
  | --- | --- | --- |
9
- | `@vvedantb/ui/tokens`, `@vvedantb/ui/tokens/tokens.css` | `src/tokens.css` | Colour, radius, shadow, ease and font tokens (`@theme`), light/dark variables, `dark` variant, `*` border and `body` defaults. In 0.2.1 it also defines shadcn-style aliases (`primary`, `muted-foreground`, `secondary`, `destructive`, `card`, `popover`, `input`, `ring`) that point at the same tokens. |
9
+ | `@vvedantb/ui/tokens`, `@vvedantb/ui/tokens/tokens.css` | `src/tokens.css` | Colour, radius, shadow, ease and font tokens (`@theme`), light/dark variables, `dark` variant, the radius utilities `rounded-surface` (`clamp(0.75rem, var(--radius), 1rem)`), `rounded-menu-item` (`min(var(--radius), 0.5rem)`) and `rounded-control` (`min(var(--radius), 0.625rem)`) (0.7.1; `rounded-surface` was in `utilities.css` before), `*` border and `body` defaults. 0.7.2 adds `--sidebar-accent` / `sidebar-accent` (sidebar nav row fill: light `var(--surface-tertiary)`, dark `oklch(22.64% 0 0)`). In 0.2.1 it also defines shadcn-style aliases (`primary`, `muted-foreground`, `secondary`, `destructive`, `card`, `popover`, `input`, `ring`) that point at the same tokens. |
10
10
  | `tokens` and `Tokens` from `@vvedantb/ui` | `src/native.ts` | JS colours, radii and spacing for React Native. `@vvedantb/native-ui` keeps its own copy in `src/tokens.ts`; a test keeps the two equal. |
11
11
  | `@vvedantb/ui/tokens/css/base.css` | `src/css/base.css` | `html` touch/accent, `::selection`, control transitions, pointer cursor, range accent (`@layer base`). |
12
12
  | `@vvedantb/ui/tokens/css/dialog.css` | `src/css/dialog.css` | Unlayered `max-width: none` for `@vvedantb/ui` compact (bottom-sheet) dialogs. |
13
13
  | `@vvedantb/ui/tokens/css/modal.css` | `src/css/modal.css` | `.t-modal`, `.is-open`, `.is-closing` and `t-modal-presence-wait` (`@layer components`). |
14
14
  | `@vvedantb/ui/tokens/css/tabs.css` | `src/css/tabs.css` | `.t-tab`, active colour, `.t-tabs-pill`, small-screen `.t-tabs` scrolling. |
15
15
  | `@vvedantb/ui/tokens/css/tabs-pills.css` | `src/css/tabs-pills.css` | `--tabs-*` colour variables, `.t-tabs--segmented`, `.t-tabs--pills` and pill hover / active fills. |
16
- | `@vvedantb/ui/tokens/css/utilities.css` | `src/css/utilities.css` | `@utility hit-target`, `@utility reveal-on-hover` and `@utility rounded-surface` (`clamp(0.75rem, var(--radius), 1rem)`). |
16
+ | `@vvedantb/ui/tokens/css/utilities.css` | `src/css/utilities.css` | `@utility hit-target` and `@utility reveal-on-hover`. `rounded-surface` moved to `tokens.css` in 0.7.1. |
17
17
  | `@vvedantb/ui/tokens/css/scrollbar-thin.css` | `src/css/scrollbar-thin.css` | `.scrollbar-thin` (`@layer utilities`, always emitted). |
18
18
  | `@vvedantb/ui/tokens/css/animations.css` | `src/css/animations.css` | `animate-aurora`, `animate-fade-in-up`, `animate-indeterminate` as `@theme` (CSS-first apps). |
19
- | `@vvedantb/ui/tokens/css/compat-shadcn-rgb.css`, `@vvedantb/ui/tokens/compat/shadcn-rgb.css` | `src/css/compat-shadcn-rgb.css` | Replaces `tokens.css` for apps with shadcn-style `R G B` channel variables. Defines only the UI-only `--color-*` names as `rgb(var(--x) / alpha)` references, `--radius-field`, `--scrollbar`, `--field-radius` and a `glass-panel-strong` fallback. Import it **or** `tokens.css`, never both. |
19
+ | `@vvedantb/ui/tokens/css/compat-shadcn-rgb.css`, `@vvedantb/ui/tokens/compat/shadcn-rgb.css` | `src/css/compat-shadcn-rgb.css` | Replaces `tokens.css` for apps with shadcn-style `R G B` channel variables. Defines only the UI-only `--color-*` names as `rgb(var(--x) / alpha)` references, `--radius-field`, `--scrollbar`, `--field-radius`, the same three radius utilities as `tokens.css` and a `glass-panel-strong` fallback. Import it **or** `tokens.css`, never both. |
20
20
  | `@vvedantb/ui/tokens/css/index.css` | `src/css/index.css` | base + dialog + utilities + scrollbar-thin + animations, for new CSS-first apps. |
21
21
  | `@vvedantb/ui/tokens/tailwind-preset` | `src/tailwind-preset.mjs` (+ `.d.mts`) | Tailwind preset for apps that load a JS config with `@config`. |
22
22
 
@@ -23,6 +23,9 @@
23
23
  * --accent, --accent-foreground, --destructive, --destructive-foreground,
24
24
  * --border, --input, --ring and --radius (all colours as "R G B").
25
25
  *
26
+ * `--color-sidebar-accent` (sidebar nav row fill) reads the app's
27
+ * `--sidebar-accent` when it is defined, else `--secondary`.
28
+ *
26
29
  * `--color-card` and `--color-muted` are references to `--card` and `--muted`.
27
30
  * `SonnerToaster sonnerVars` reads them, along with `--color-border` and
28
31
  * `--color-foreground`. An app that already maps those names keeps its own
@@ -44,6 +47,7 @@
44
47
  --color-surface-secondary-foreground: rgb(var(--foreground));
45
48
  --color-surface-tertiary: rgb(var(--secondary));
46
49
  --color-surface-tertiary-foreground: rgb(var(--secondary-foreground));
50
+ --color-sidebar-accent: rgb(var(--sidebar-accent, var(--secondary)));
47
51
  --color-overlay: rgb(var(--popover));
48
52
  --color-overlay-foreground: rgb(var(--popover-foreground));
49
53
  --color-field-background: rgb(var(--card));
@@ -65,6 +69,20 @@
65
69
  --radius-field: min(calc(var(--radius) + 0.25rem), 1.25rem);
66
70
  }
67
71
 
72
+ /* The radius utilities from tokens.css, which this file replaces. They read
73
+ the app's `--radius`. */
74
+ @utility rounded-surface {
75
+ border-radius: clamp(0.75rem, var(--radius), 1rem);
76
+ }
77
+
78
+ @utility rounded-menu-item {
79
+ border-radius: min(var(--radius), 0.5rem);
80
+ }
81
+
82
+ @utility rounded-control {
83
+ border-radius: min(var(--radius), 0.625rem);
84
+ }
85
+
68
86
  :root {
69
87
  --scrollbar: rgb(var(--muted-foreground) / 0.35);
70
88
  --field-radius: min(calc(var(--radius) + 0.25rem), 1.25rem);
@@ -32,8 +32,3 @@
32
32
  }
33
33
  }
34
34
  }
35
-
36
- /* Surface radius: follows the theme `--radius`, kept between 0.75rem and 1rem. */
37
- @utility rounded-surface {
38
- border-radius: clamp(0.75rem, var(--radius), 1rem);
39
- }
@@ -56,6 +56,7 @@
56
56
  --color-surface-tertiary: var(--surface-tertiary);
57
57
  --color-surface-tertiary-foreground: var(--surface-tertiary-foreground);
58
58
  --color-surface-card: var(--surface-card);
59
+ --color-sidebar-accent: var(--sidebar-accent);
59
60
  --color-warning: var(--warning);
60
61
  --color-warning-foreground: var(--warning-foreground);
61
62
 
@@ -94,6 +95,22 @@
94
95
  --font-instrumentSans: "Instrument Sans", system-ui, sans-serif;
95
96
  }
96
97
 
98
+ /* Radius utilities that follow the theme `--radius` (values at the default
99
+ 1rem in brackets). Surfaces: kept between 0.75rem and 1rem (16px). Menu
100
+ items and sidebar nav rows: capped at 0.5rem (8px) so they sit inside the
101
+ surface corner. Controls: capped at 0.625rem (10px). */
102
+ @utility rounded-surface {
103
+ border-radius: clamp(0.75rem, var(--radius), 1rem);
104
+ }
105
+
106
+ @utility rounded-menu-item {
107
+ border-radius: min(var(--radius), 0.5rem);
108
+ }
109
+
110
+ @utility rounded-control {
111
+ border-radius: min(var(--radius), 0.625rem);
112
+ }
113
+
97
114
  @layer base {
98
115
  :root,
99
116
  .light,
@@ -139,6 +156,8 @@
139
156
  --surface-tertiary: oklch(93.73% 0 0);
140
157
  --surface-tertiary-foreground: oklch(21.03% 0 0);
141
158
  --surface-card: oklch(98% 0 0);
159
+ /* Sidebar nav row hover, active and selected fill. */
160
+ --sidebar-accent: var(--surface-tertiary);
142
161
  --warning: oklch(78.19% 0.159 72.33);
143
162
  --warning-foreground: oklch(21.03% 0.0059 72.33);
144
163
  --radius: 1rem;
@@ -186,6 +205,8 @@
186
205
  --surface-tertiary: oklch(27.21% 0.0023 0);
187
206
  --surface-tertiary-foreground: oklch(99.11% 0 0);
188
207
  --surface-card: oklch(25.7% 0.0023 0);
208
+ /* sRGB 28 28 28: a step darker than --surface-tertiary. */
209
+ --sidebar-accent: oklch(22.64% 0 0);
189
210
  --warning: oklch(82.03% 0.1392 76.34);
190
211
  --warning-foreground: oklch(21.03% 0.0059 76.34);
191
212
  --radius: 1rem;