@vvedantb/ui 0.6.1 → 0.7.1
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 +143 -3
- package/package.json +1 -1
- package/shell/src/Sidebar.tsx +3 -1
- package/shell/src/SidebarPanel.tsx +38 -2
- package/shell/src/app-shell-classes.ts +38 -6
- package/shell/src/index.ts +3 -0
- package/src/index.ts +4 -0
- package/src/ui/_menu-classes.alt.ts +3 -3
- package/src/ui/_menu-classes.ts +3 -3
- package/src/ui/badge.tsx +2 -0
- package/src/ui/empty-state.tsx +119 -0
- package/src/ui/kbd.tsx +16 -6
- package/src/ui/spinner.tsx +22 -6
- package/src/ui/tabs.tsx +28 -17
- package/src/utils/cn.ts +5 -3
- package/tokens/README.md +3 -3
- package/tokens/src/css/compat-shadcn-rgb.css +14 -0
- package/tokens/src/css/utilities.css +0 -5
- package/tokens/src/tokens.css +16 -0
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 })`, `sidebarNavIconClass(active)`, `sidebarSectionLabelClass`. See [Menu item corners and sidebar nav rows (0.7.1)](#menu-item-corners-and-sidebar-nav-rows-071).
|
|
303
303
|
|
|
304
304
|
### Header actions and account badge (0.5.2)
|
|
305
305
|
|
|
@@ -385,7 +385,7 @@ function PanelWidth() {
|
|
|
385
385
|
|
|
386
386
|
Shadcn-style building blocks. The prop-driven components above are thin compositions of them and render the same DOM.
|
|
387
387
|
|
|
388
|
-
Convention: compound parts are static members of a namespace object, `<Namespace>.<Part>`. There are
|
|
388
|
+
Convention: compound parts are static members of a namespace object, `<Namespace>.<Part>`. There are four namespaces: `ConfirmDialog.*`, `AppShell.*`, `Sidebar.*` and (0.7.0) `EmptyState.*` (see [Spinner, empty state, tabs, badge and kbd](#spinner-empty-state-tabs-badge-and-kbd-070)). `ConfirmDialog` and `AppShell` stay callable with their legacy props. `Sidebar` is a plain object. Prop types are exported flat, for example `ConfirmDialogRootProps`, `AppShellMainProps` and `SidebarRailItemProps`. Every part that renders an element merges `className` last and forwards native props.
|
|
389
389
|
|
|
390
390
|
### `ConfirmDialog.*`
|
|
391
391
|
|
|
@@ -434,6 +434,7 @@ 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`. |
|
|
437
438
|
| `AccountMenu` | Dropdown root. `badge` adds the unread dot (`AccountBadge`) in a `relative` wrapper. |
|
|
438
439
|
| `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
440
|
| `AccountAvatar` | Avatar image or initials. `userName`, `imageUrl`. |
|
|
@@ -559,6 +560,145 @@ A full shell:
|
|
|
559
560
|
</AppShell.Root>
|
|
560
561
|
```
|
|
561
562
|
|
|
563
|
+
## Spinner, empty state, tabs, badge and kbd (0.7.0)
|
|
564
|
+
|
|
565
|
+
All of these are opt-in. Without the new props, components render as in 0.6.1.
|
|
566
|
+
|
|
567
|
+
```tsx
|
|
568
|
+
import {
|
|
569
|
+
Badge, Button, EmptyState, Kbd, Spinner, Tabs, TabsList, TabsTrigger,
|
|
570
|
+
} from "@vvedantb/ui";
|
|
571
|
+
|
|
572
|
+
<Button disabled={saving}>
|
|
573
|
+
{saving ? <Spinner size={14} tone="inherit" decorative className="mr-1.5" /> : null}
|
|
574
|
+
Save
|
|
575
|
+
</Button>
|
|
576
|
+
|
|
577
|
+
<EmptyState.Root className="h-full">
|
|
578
|
+
<EmptyState.Icon icon={IconFolder} />
|
|
579
|
+
<EmptyState.Description>No files yet.</EmptyState.Description>
|
|
580
|
+
</EmptyState.Root>
|
|
581
|
+
|
|
582
|
+
<EmptyState.Root variant="panel">
|
|
583
|
+
<EmptyState.Icon icon={IconLink} />
|
|
584
|
+
<EmptyState.Title>No linked items</EmptyState.Title>
|
|
585
|
+
<EmptyState.Description>Link one to see it here.</EmptyState.Description>
|
|
586
|
+
<EmptyState.Actions><Button size="sm">Link</Button></EmptyState.Actions>
|
|
587
|
+
</EmptyState.Root>
|
|
588
|
+
|
|
589
|
+
<Tabs value={active}>
|
|
590
|
+
<TabsList>
|
|
591
|
+
<TabsTrigger value="keys" asChild>
|
|
592
|
+
<Link to="/settings/keys">Keys</Link>
|
|
593
|
+
</TabsTrigger>
|
|
594
|
+
</TabsList>
|
|
595
|
+
</Tabs>
|
|
596
|
+
|
|
597
|
+
<Tabs value={feature} onValueChange={setFeature}>
|
|
598
|
+
<TabsList variant="soft" aria-label="Features">
|
|
599
|
+
<TabsTrigger value="notes"><IconNotes size={15} />Notes</TabsTrigger>
|
|
600
|
+
</TabsList>
|
|
601
|
+
</Tabs>
|
|
602
|
+
|
|
603
|
+
<Badge variant="destructive-soft">Revoked</Badge>
|
|
604
|
+
<Kbd variant="subtle">Esc</Kbd>
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
- `Spinner` `size`: `"sm"`, `"md"` (default) and `"lg"` add a `size-*` class; a number is passed to the icon as its pixel size and adds no size class. `stroke` sets the stroke width. `tone`: `"muted"` (default) adds `text-muted-foreground`; `"inherit"` adds no colour class, so the spinner takes the current text colour. A colour class in `className` also replaces the muted default. `decorative` renders `aria-hidden="true"` with no `role="status"` or `aria-label`; use it where the parent already has a label, such as a busy button. Without it, `aria-label` in props replaces `"Loading"`.
|
|
608
|
+
- `EmptyState.*`: composable "nothing here yet" block. Parts read the Root `variant`.
|
|
609
|
+
|
|
610
|
+
| Part | Purpose |
|
|
611
|
+
| --- | --- |
|
|
612
|
+
| `Root` | `<div>`, centred column. `variant`: `"default"` (`px-4 py-20`, open) or `"panel"` (`rounded-lg bg-surface-secondary px-4 py-10`). |
|
|
613
|
+
| `Icon` | `icon` component (for example a Tabler icon). `default`: the bare icon, `size` 40, `mb-3 text-muted-foreground`. `panel`: the icon (`size` 22, `stroke` 1.5, `text-muted-foreground`) in an `h-11 w-11 rounded-lg bg-surface-tertiary` tile. `size` and `stroke` override; `className` goes on the outer element (icon or tile). |
|
|
614
|
+
| `Title` | `<p>`, `text-sm font-medium text-foreground`; `default` adds `mb-1`. `asChild` renders your element (for example an `h3`) instead. |
|
|
615
|
+
| `Description` | `<p>`, `text-sm text-muted-foreground`; `panel` adds `mt-1 max-w-[240px]`. |
|
|
616
|
+
| `Actions` | `<div>`, `mt-4`. |
|
|
617
|
+
|
|
618
|
+
- `TabsList` `variant="soft"`: borderless pills that wrap and centre (`flex flex-wrap items-center justify-center gap-1.5`). Triggers inside it are `rounded-full px-4 py-2 text-sm`, muted until active, filled with `bg-surface-secondary` when active or hovered, and `relative overflow-hidden` so a child such as a progress bar can sit on the bottom edge. It uses no `t-tabs` CSS and has no focus ring of its own. `pills` and `segmented` are unchanged. Link tabs work with any variant: `TabsTrigger asChild` around the router link (Radix sets `role="tab"`, `data-state` and roving focus on it). The package does not depend on a router.
|
|
619
|
+
- `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"`.
|
|
620
|
+
- `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.
|
|
621
|
+
|
|
622
|
+
## Menu item corners and sidebar nav rows (0.7.1)
|
|
623
|
+
|
|
624
|
+
Visual change. Values in brackets are at the default `--radius: 1rem` and `--spacing: 0.25rem`.
|
|
625
|
+
|
|
626
|
+
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:
|
|
627
|
+
|
|
628
|
+
| Utility | Value | Used by |
|
|
629
|
+
| --- | --- | --- |
|
|
630
|
+
| `rounded-surface` | `clamp(0.75rem, var(--radius), 1rem)` (16px) | Opt-in surfaces (Toaster `toastRadius="surface"`, `SessionsHoverCard`). Was in `utilities.css`. |
|
|
631
|
+
| `rounded-menu-item` | `min(var(--radius), 0.5rem)` (8px) | Menu items and sidebar nav rows. |
|
|
632
|
+
| `rounded-control` | `min(var(--radius), 0.625rem)` (10px) | Apps. |
|
|
633
|
+
|
|
634
|
+
The root `cn` knows these are radius classes, so `cn("rounded-lg", "rounded-menu-item")` keeps only the last one.
|
|
635
|
+
|
|
636
|
+
### Menu items
|
|
637
|
+
|
|
638
|
+
| Piece | Before | From 0.7.1 |
|
|
639
|
+
| --- | --- | --- |
|
|
640
|
+
| `DropdownMenu` and `ContextMenu` item, sub-trigger, checkbox item and radio item | `rounded-md`, `calc(var(--radius) - 2px)` (14px) | `rounded-menu-item` (8px) |
|
|
641
|
+
| Menu surface | `rounded-lg` (16px) | unchanged |
|
|
642
|
+
|
|
643
|
+
### Sidebar panel nav rows
|
|
644
|
+
|
|
645
|
+
Rows in the panel beside the rail. Rail tiles are unchanged.
|
|
646
|
+
|
|
647
|
+
| Piece | Before | From 0.7.1 |
|
|
648
|
+
| --- | --- | --- |
|
|
649
|
+
| Radius | none in the helper (apps added `rounded-lg`, 16px) | `rounded-menu-item` (8px) |
|
|
650
|
+
| Padding | `px-3 py-1.5` (12px / 6px) | `px-4 py-1.5` (16px / 6px) |
|
|
651
|
+
| Icon gap | `gap-3` (12px) | `gap-2` (8px) |
|
|
652
|
+
| Text | none in the helper (apps added `text-sm`, 14px / 20px) | `text-[0.8125rem] leading-[18px] tracking-[0.005em]` (13px / 18px) |
|
|
653
|
+
| Row height | 32px with `text-sm` | 30px |
|
|
654
|
+
| Width | none in the helper | `flex w-full items-center` |
|
|
655
|
+
| Idle text | `text-muted`, `hover:text-foreground` | `font-normal text-foreground/80`, `hover:text-foreground` |
|
|
656
|
+
| Active text | `text-foreground` | `font-medium text-foreground` |
|
|
657
|
+
| Hover and active fill | none in the helper | `hover:bg-surface-tertiary` (idle rows), `bg-surface-tertiary` (active row), same radius |
|
|
658
|
+
| Press | none | `active:scale-[0.99]`, `transition-[scale,background-color,color] duration-150 ease-smooth` |
|
|
659
|
+
| Focus ring | none | `focus-visible:ring-2 focus-visible:ring-foreground/35`, no outline |
|
|
660
|
+
| Icon-only row | `justify-center px-0 py-1.5` | same, plus the radius, text, fill and focus classes above |
|
|
661
|
+
| Icon (`sidebarNavIconClass`) | none (apps used 18px in a 20px box) | `size-[19px] shrink-0`; `text-muted-foreground` idle, `text-foreground` active |
|
|
662
|
+
| 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. |
|
|
663
|
+
|
|
664
|
+
The hover fill is static: it does not slide between rows. While another row is hovered, the active row keeps its fill.
|
|
665
|
+
|
|
666
|
+
One call per row:
|
|
667
|
+
|
|
668
|
+
```tsx
|
|
669
|
+
<nav className="px-2">
|
|
670
|
+
<p className={sidebarSectionLabelClass}>Library</p>
|
|
671
|
+
<div className="space-y-1">
|
|
672
|
+
<Link to="/home" aria-current={active ? "page" : undefined} className={sidebarNavItemClass({ active, iconOnly })}>
|
|
673
|
+
<IconHome size={19} className={sidebarNavIconClass(active)} />
|
|
674
|
+
{iconOnly ? null : <span className="truncate">Home</span>}
|
|
675
|
+
</Link>
|
|
676
|
+
</div>
|
|
677
|
+
</nav>
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
Or the part, with the router link as the child:
|
|
681
|
+
|
|
682
|
+
```tsx
|
|
683
|
+
<Sidebar.NavItem asChild active={active} iconOnly={iconOnly} icon={<IconHome />}>
|
|
684
|
+
<Link to="/home">{iconOnly ? null : <span className="truncate">Home</span>}</Link>
|
|
685
|
+
</Sidebar.NavItem>
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
- `px-2` on the list is the panel inset: rows are full width inside it. `space-y-1` (4px) separates rows.
|
|
689
|
+
- `sidebarNavRowClass(isIconOnly)` and `sidebarNavLinkTextClass(isActive)` now return the classes above, so existing calls change look. `sidebarNavItemClass` is the two merged.
|
|
690
|
+
- 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`.
|
|
691
|
+
|
|
692
|
+
Colour mapping. The package has no sidebar colour tokens, so the rows use the nearest general token:
|
|
693
|
+
|
|
694
|
+
| Role | Reference look (light / dark, sRGB) | Package token (light / dark, sRGB) |
|
|
695
|
+
| --- | --- | --- |
|
|
696
|
+
| 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. |
|
|
697
|
+
| Active text and icon | 9 9 11 / 250 250 250 | `foreground`: 24 24 24 / 252 252 252 |
|
|
698
|
+
| Idle text (80%) | 24 24 25 / 251 252 253 | `foreground` at 80% |
|
|
699
|
+
| Idle icon | 100 101 103 / 159 160 162 | `muted-foreground` (`--muted`): 114 114 114 / 160 160 160 |
|
|
700
|
+
| Focus ring (35%) | 9 9 11 / 250 250 250 | `foreground` at 35% |
|
|
701
|
+
|
|
562
702
|
## Toaster
|
|
563
703
|
|
|
564
704
|
```tsx
|
|
@@ -584,7 +724,7 @@ Opt-in props (all default to the current look):
|
|
|
584
724
|
|
|
585
725
|
| Prop | Values | Effect |
|
|
586
726
|
| --- | --- | --- |
|
|
587
|
-
| `toastRadius` | `"lg"` (default), `"surface"`, `"control"` | `rounded-lg`, `rounded-surface` (from `tokens
|
|
727
|
+
| `toastRadius` | `"lg"` (default), `"surface"`, `"control"` | `rounded-lg`, `rounded-surface` (from `tokens.css`, or the compat file) or `rounded-field` (the input radius). |
|
|
588
728
|
| `density` | `"default"`, `"compact"` | Current padding and text, or `gap-2 px-3 py-2 text-xs` with `text-xs` descriptions. |
|
|
589
729
|
| `toastIcons` | `"default"`, `"filled24"` | Sonner icons or 24px filled success, info, warning and error icons, a matching loader and a 14px close cross. |
|
|
590
730
|
| `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. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vvedantb/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
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",
|
package/shell/src/Sidebar.tsx
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
AccountSeparator,
|
|
10
10
|
AccountTrigger,
|
|
11
11
|
} from "./SidebarAccountMenu";
|
|
12
|
-
import { Panel, PanelActions, PanelClose, PanelHeader, PanelTitle } from "./SidebarPanel";
|
|
12
|
+
import { NavItem, Panel, PanelActions, PanelClose, PanelHeader, PanelTitle } from "./SidebarPanel";
|
|
13
13
|
import { Rail, RailBody, RailFooter, RailHeader, RailItem, RailSeparator } from "./SidebarRail";
|
|
14
14
|
|
|
15
15
|
export type {
|
|
@@ -36,6 +36,7 @@ export type {
|
|
|
36
36
|
SidebarPanelHeaderProps,
|
|
37
37
|
SidebarPanelRootProps,
|
|
38
38
|
SidebarPanelTitleProps,
|
|
39
|
+
SidebarNavItemProps,
|
|
39
40
|
} from "./SidebarPanel";
|
|
40
41
|
export type {
|
|
41
42
|
SidebarAccountAvatarProps,
|
|
@@ -68,6 +69,7 @@ export const Sidebar = {
|
|
|
68
69
|
PanelTitle,
|
|
69
70
|
PanelActions,
|
|
70
71
|
PanelClose,
|
|
72
|
+
NavItem,
|
|
71
73
|
AccountMenu,
|
|
72
74
|
AccountTrigger,
|
|
73
75
|
AccountAvatar,
|
|
@@ -1,8 +1,9 @@
|
|
|
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 { panelCloseButtonClass, sidebarHeaderRowClass, sidebarNavIconClass, sidebarNavItemClass } from "./app-shell-classes";
|
|
6
7
|
import { useOptionalSidebar } from "./SidebarChrome";
|
|
7
8
|
|
|
8
9
|
export type SidebarPanelProps = {
|
|
@@ -131,3 +132,38 @@ export function PanelClose({ label = "Close navigation", onClick, className, chi
|
|
|
131
132
|
</Button>
|
|
132
133
|
);
|
|
133
134
|
}
|
|
135
|
+
|
|
136
|
+
export type SidebarNavItemProps = ComponentPropsWithoutRef<"button"> & {
|
|
137
|
+
/** Current route: static fill, `font-medium`, `aria-current="page"`. */
|
|
138
|
+
active?: boolean;
|
|
139
|
+
/** Collapsed panel: centred icon, no horizontal padding. */
|
|
140
|
+
iconOnly?: boolean;
|
|
141
|
+
/** Leading icon element; gets `sidebarNavIconClass(active)` merged onto it. */
|
|
142
|
+
icon?: ReactElement;
|
|
143
|
+
/** Render the single child element (for example a router link) as the row. */
|
|
144
|
+
asChild?: boolean;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/** Panel nav row (`sidebarNavItemClass`). Renders a native `<button>`, or its child with `asChild`. */
|
|
148
|
+
export function NavItem({
|
|
149
|
+
active = false,
|
|
150
|
+
iconOnly = false,
|
|
151
|
+
icon,
|
|
152
|
+
asChild = false,
|
|
153
|
+
className,
|
|
154
|
+
children,
|
|
155
|
+
...props
|
|
156
|
+
}: SidebarNavItemProps) {
|
|
157
|
+
const Row = asChild ? Slot : "button";
|
|
158
|
+
return (
|
|
159
|
+
<Row
|
|
160
|
+
type={asChild ? undefined : "button"}
|
|
161
|
+
{...props}
|
|
162
|
+
aria-current={active ? "page" : undefined}
|
|
163
|
+
className={cn(sidebarNavItemClass({ active, iconOnly }), className)}
|
|
164
|
+
>
|
|
165
|
+
{icon ? <Slot className={sidebarNavIconClass(active)}>{icon}</Slot> : null}
|
|
166
|
+
<Slottable>{children}</Slottable>
|
|
167
|
+
</Row>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
@@ -19,18 +19,50 @@ 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
|
-
/**
|
|
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-
|
|
28
|
+
"sidebar-section-label px-4 py-1 text-[11px] leading-4 font-medium tracking-[-0.01em]";
|
|
25
29
|
|
|
26
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Nav row state: idle rows are `foreground/80` and get the fill on hover;
|
|
32
|
+
* the active row is `font-medium text-foreground` on a static fill.
|
|
33
|
+
*/
|
|
27
34
|
export function sidebarNavLinkTextClass(isActive: boolean): string {
|
|
28
|
-
return
|
|
35
|
+
return isActive
|
|
36
|
+
? "bg-surface-tertiary font-medium text-foreground"
|
|
37
|
+
: "font-normal text-foreground/80 hover:bg-surface-tertiary hover:text-foreground";
|
|
29
38
|
}
|
|
30
39
|
|
|
31
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Panel nav row layout: full width, `rounded-menu-item` (8px), 13px / 18px
|
|
42
|
+
* text, focus ring and press scale. Icon-only rows centre the icon.
|
|
43
|
+
*/
|
|
32
44
|
export function sidebarNavRowClass(isIconOnly: boolean): string {
|
|
33
|
-
return cn(
|
|
45
|
+
return cn(
|
|
46
|
+
"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",
|
|
47
|
+
isIconOnly ? "justify-center px-0" : "gap-2 px-4",
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type SidebarNavItemClassOptions = {
|
|
52
|
+
/** Current route. */
|
|
53
|
+
active?: boolean;
|
|
54
|
+
/** Collapsed panel: centred icon, no horizontal padding. */
|
|
55
|
+
iconOnly?: boolean;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Whole panel nav row: `sidebarNavRowClass` plus `sidebarNavLinkTextClass`. */
|
|
59
|
+
export function sidebarNavItemClass({ active = false, iconOnly = false }: SidebarNavItemClassOptions = {}): string {
|
|
60
|
+
return cn(sidebarNavRowClass(iconOnly), sidebarNavLinkTextClass(active));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Nav row icon (put it on the `<svg>`): 19px, muted until active. */
|
|
64
|
+
export function sidebarNavIconClass(active: boolean): string {
|
|
65
|
+
return cn("size-[19px] shrink-0", active ? "text-foreground" : "text-muted-foreground");
|
|
34
66
|
}
|
|
35
67
|
|
|
36
68
|
/** Sidebar header row: the panel title and its trailing actions. */
|
package/shell/src/index.ts
CHANGED
|
@@ -45,7 +45,10 @@ export {
|
|
|
45
45
|
railBadgeClass,
|
|
46
46
|
railTileClass,
|
|
47
47
|
railTileStateClass,
|
|
48
|
+
sidebarNavIconClass,
|
|
49
|
+
sidebarNavItemClass,
|
|
48
50
|
sidebarNavLinkTextClass,
|
|
49
51
|
sidebarNavRowClass,
|
|
50
52
|
sidebarSectionLabelClass,
|
|
51
53
|
} from "./app-shell-classes.js";
|
|
54
|
+
export type { SidebarNavItemClassOptions } from "./app-shell-classes.js";
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ export * from "./ui/dialog";
|
|
|
21
21
|
export * from "./ui/alert-dialog";
|
|
22
22
|
export * from "./ui/confirm-dialog";
|
|
23
23
|
export * from "./ui/dropdown-menu";
|
|
24
|
+
export * from "./ui/empty-state";
|
|
24
25
|
export * from "./ui/hover-card";
|
|
25
26
|
export * from "./ui/input";
|
|
26
27
|
export * from "./ui/label";
|
|
@@ -157,10 +158,13 @@ export {
|
|
|
157
158
|
railBadgeClass,
|
|
158
159
|
railTileClass,
|
|
159
160
|
railTileStateClass,
|
|
161
|
+
sidebarNavIconClass,
|
|
162
|
+
sidebarNavItemClass,
|
|
160
163
|
sidebarNavLinkTextClass,
|
|
161
164
|
sidebarNavRowClass,
|
|
162
165
|
sidebarSectionLabelClass,
|
|
163
166
|
} from "../shell/src/app-shell-classes";
|
|
167
|
+
export type { SidebarNavItemClassOptions } from "../shell/src/app-shell-classes";
|
|
164
168
|
|
|
165
169
|
// App helpers: confirm skip, update watcher, settings chrome, stable auth.
|
|
166
170
|
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-
|
|
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-
|
|
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-
|
|
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";
|
package/src/ui/_menu-classes.ts
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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/ui/badge.tsx
CHANGED
|
@@ -9,6 +9,8 @@ const badgeVariants = cva(
|
|
|
9
9
|
default: "border-border bg-default text-foreground",
|
|
10
10
|
secondary: "border-separator bg-default text-default-foreground",
|
|
11
11
|
destructive: "border-danger/25 bg-danger/12 text-danger",
|
|
12
|
+
// Neutral border with a faint danger tint, e.g. a revoked or expired status.
|
|
13
|
+
"destructive-soft": "border-border bg-danger/10 text-danger",
|
|
12
14
|
outline: "border-separator bg-transparent text-muted-foreground",
|
|
13
15
|
success: "border-success/25 bg-success/12 text-success",
|
|
14
16
|
warning: "border-warning/25 bg-warning/12 text-warning",
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
3
|
+
import { cn } from "../utils/cn";
|
|
4
|
+
|
|
5
|
+
// `default`: open centred block, large bare icon, one muted line (a page or list body).
|
|
6
|
+
// `panel`: filled rounded block with the icon in a tile (a detail panel or modal section).
|
|
7
|
+
export type EmptyStateVariant = "default" | "panel";
|
|
8
|
+
|
|
9
|
+
const EmptyStateContext = React.createContext<EmptyStateVariant>("default");
|
|
10
|
+
|
|
11
|
+
export type EmptyStateRootProps = React.ComponentProps<"div"> & {
|
|
12
|
+
/** Layout for the other parts. Default `"default"`. */
|
|
13
|
+
variant?: EmptyStateVariant;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function Root({ variant = "default", className, ...props }: EmptyStateRootProps) {
|
|
17
|
+
return (
|
|
18
|
+
<EmptyStateContext value={variant}>
|
|
19
|
+
<div
|
|
20
|
+
{...props}
|
|
21
|
+
className={cn(
|
|
22
|
+
"flex flex-col items-center justify-center text-center",
|
|
23
|
+
variant === "panel" ? "rounded-lg bg-surface-secondary px-4 py-10" : "px-4 py-20",
|
|
24
|
+
className,
|
|
25
|
+
)}
|
|
26
|
+
/>
|
|
27
|
+
</EmptyStateContext>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
Root.displayName = "EmptyState.Root";
|
|
31
|
+
|
|
32
|
+
export type EmptyStateIconProps = {
|
|
33
|
+
/** Icon component, e.g. a Tabler icon. */
|
|
34
|
+
icon: React.ComponentType<{ size?: number | string; stroke?: number | string; className?: string }>;
|
|
35
|
+
/** Pixel size. Default 40 (`default`) or 22 (`panel`). */
|
|
36
|
+
size?: number;
|
|
37
|
+
/** Stroke width. Default the icon's own (`default`) or 1.5 (`panel`). */
|
|
38
|
+
stroke?: number;
|
|
39
|
+
/** Classes for the outer element: the icon (`default`) or its tile (`panel`). */
|
|
40
|
+
className?: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function Icon({ icon: IconComponent, size, stroke, className }: EmptyStateIconProps) {
|
|
44
|
+
if (React.use(EmptyStateContext) === "panel") {
|
|
45
|
+
return (
|
|
46
|
+
<div
|
|
47
|
+
className={cn(
|
|
48
|
+
"mb-3 flex h-11 w-11 items-center justify-center rounded-lg bg-surface-tertiary",
|
|
49
|
+
className,
|
|
50
|
+
)}
|
|
51
|
+
>
|
|
52
|
+
<IconComponent size={size ?? 22} className="text-muted-foreground" stroke={stroke ?? 1.5} />
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return (
|
|
57
|
+
<IconComponent
|
|
58
|
+
size={size ?? 40}
|
|
59
|
+
stroke={stroke}
|
|
60
|
+
className={cn("mb-3 text-muted-foreground", className)}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
Icon.displayName = "EmptyState.Icon";
|
|
65
|
+
|
|
66
|
+
export type EmptyStateTitleProps = React.ComponentProps<"p"> & {
|
|
67
|
+
/** Render the child element (e.g. an `h3`) with the title classes. */
|
|
68
|
+
asChild?: boolean;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function Title({ asChild = false, className, ...props }: EmptyStateTitleProps) {
|
|
72
|
+
const Comp = asChild ? Slot : "p";
|
|
73
|
+
return (
|
|
74
|
+
<Comp
|
|
75
|
+
{...props}
|
|
76
|
+
className={cn(
|
|
77
|
+
"text-sm font-medium text-foreground",
|
|
78
|
+
React.use(EmptyStateContext) === "default" && "mb-1",
|
|
79
|
+
className,
|
|
80
|
+
)}
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
Title.displayName = "EmptyState.Title";
|
|
85
|
+
|
|
86
|
+
export type EmptyStateDescriptionProps = React.ComponentProps<"p">;
|
|
87
|
+
|
|
88
|
+
function Description({ className, ...props }: EmptyStateDescriptionProps) {
|
|
89
|
+
return (
|
|
90
|
+
<p
|
|
91
|
+
{...props}
|
|
92
|
+
className={cn(
|
|
93
|
+
React.use(EmptyStateContext) === "panel" && "mt-1 max-w-[240px]",
|
|
94
|
+
"text-sm text-muted-foreground",
|
|
95
|
+
className,
|
|
96
|
+
)}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
Description.displayName = "EmptyState.Description";
|
|
101
|
+
|
|
102
|
+
export type EmptyStateActionsProps = React.ComponentProps<"div">;
|
|
103
|
+
|
|
104
|
+
function Actions({ className, ...props }: EmptyStateActionsProps) {
|
|
105
|
+
return <div {...props} className={cn("mt-4", className)} />;
|
|
106
|
+
}
|
|
107
|
+
Actions.displayName = "EmptyState.Actions";
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Composable "nothing here yet" block: `EmptyState.Root` sets the variant, the
|
|
111
|
+
* other parts read it. Render only the parts you need.
|
|
112
|
+
*/
|
|
113
|
+
export const EmptyState = {
|
|
114
|
+
Root,
|
|
115
|
+
Icon,
|
|
116
|
+
Title,
|
|
117
|
+
Description,
|
|
118
|
+
Actions,
|
|
119
|
+
};
|
package/src/ui/kbd.tsx
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { cn } from "../utils/cn";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
// `subtle`: plain filled key for inline hints; size and font come from the
|
|
5
|
+
// surrounding text.
|
|
6
|
+
const kbdVariantClasses = {
|
|
7
|
+
default:
|
|
8
|
+
"pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded-md bg-surface-secondary px-1.5 font-mono text-[10px] font-medium text-muted-foreground",
|
|
9
|
+
subtle: "rounded bg-surface-secondary px-1.5 py-0.5 text-foreground",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type KbdProps = React.HTMLAttributes<HTMLElement> & {
|
|
13
|
+
/** Default `"default"`. */
|
|
14
|
+
variant?: keyof typeof kbdVariantClasses;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const Kbd = React.forwardRef<HTMLElement, KbdProps>(
|
|
18
|
+
({ className, variant = "default", ...props }, ref) => (
|
|
6
19
|
<kbd
|
|
7
20
|
ref={ref}
|
|
8
|
-
className={cn(
|
|
9
|
-
"pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded-md bg-surface-secondary px-1.5 font-mono text-[10px] font-medium text-muted-foreground",
|
|
10
|
-
className,
|
|
11
|
-
)}
|
|
21
|
+
className={cn(kbdVariantClasses[variant], className)}
|
|
12
22
|
{...props}
|
|
13
23
|
/>
|
|
14
24
|
),
|
package/src/ui/spinner.tsx
CHANGED
|
@@ -7,18 +7,34 @@ const sizeClasses = {
|
|
|
7
7
|
lg: "size-8",
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
+
export type SpinnerProps = Omit<React.ComponentPropsWithoutRef<"svg">, "size" | "stroke"> & {
|
|
11
|
+
/** Named size (a `size-*` class) or a pixel size passed to the icon (no size class). Default `"md"`. */
|
|
12
|
+
size?: "sm" | "md" | "lg" | number;
|
|
13
|
+
/** Icon stroke width. Default the icon's own (2). */
|
|
14
|
+
stroke?: number | string;
|
|
15
|
+
/** `"muted"` adds `text-muted-foreground`; `"inherit"` adds no colour class, so the icon uses `currentColor`. Default `"muted"`. */
|
|
16
|
+
tone?: "muted" | "inherit";
|
|
17
|
+
/** Hide from assistive tech (`aria-hidden`, no `role="status"` or label), e.g. inside a labelled button. Default `false`. */
|
|
18
|
+
decorative?: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
10
21
|
function Spinner({
|
|
11
22
|
size = "md",
|
|
23
|
+
tone = "muted",
|
|
24
|
+
decorative = false,
|
|
12
25
|
className,
|
|
13
26
|
...props
|
|
14
|
-
}:
|
|
15
|
-
size?: "sm" | "md" | "lg";
|
|
16
|
-
}) {
|
|
27
|
+
}: SpinnerProps) {
|
|
17
28
|
return (
|
|
18
29
|
<IconLoader2
|
|
19
|
-
role
|
|
20
|
-
|
|
21
|
-
className={cn(
|
|
30
|
+
{...(decorative ? { "aria-hidden": true } : { role: "status", "aria-label": "Loading" })}
|
|
31
|
+
size={typeof size === "number" ? size : undefined}
|
|
32
|
+
className={cn(
|
|
33
|
+
"animate-spin",
|
|
34
|
+
tone === "muted" && "text-muted-foreground",
|
|
35
|
+
typeof size === "number" ? undefined : sizeClasses[size],
|
|
36
|
+
className,
|
|
37
|
+
)}
|
|
22
38
|
{...props}
|
|
23
39
|
/>
|
|
24
40
|
);
|
package/src/ui/tabs.tsx
CHANGED
|
@@ -8,7 +8,11 @@ const Tabs = TabsPrimitive.Root;
|
|
|
8
8
|
|
|
9
9
|
// `pills` (default): isolated rounded pills with a gap, no shared track.
|
|
10
10
|
// `segmented`: connected bar with a sliding pill, kept for marketing surfaces.
|
|
11
|
-
|
|
11
|
+
// `soft`: borderless wrapping pills, the active one filled. Uses no `t-tabs`
|
|
12
|
+
// CSS, so triggers can hold an absolutely placed child (e.g. a progress bar).
|
|
13
|
+
type TabsListVariant = "pills" | "segmented" | "soft";
|
|
14
|
+
|
|
15
|
+
const TabsListVariantContext = React.createContext<TabsListVariant>("pills");
|
|
12
16
|
|
|
13
17
|
const TabsList = React.forwardRef<
|
|
14
18
|
React.ComponentRef<typeof TabsPrimitive.List>,
|
|
@@ -82,21 +86,26 @@ const TabsList = React.forwardRef<
|
|
|
82
86
|
}, [variant]);
|
|
83
87
|
|
|
84
88
|
return (
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
89
|
+
<TabsListVariantContext value={variant}>
|
|
90
|
+
<TabsPrimitive.List
|
|
91
|
+
ref={mergedRef}
|
|
92
|
+
className={cn(
|
|
93
|
+
variant === "soft"
|
|
94
|
+
? "flex flex-wrap items-center justify-center gap-1.5"
|
|
95
|
+
: "t-tabs max-sm:max-w-full max-sm:justify-center-safe max-sm:overflow-x-auto max-sm:scrollbar-none",
|
|
96
|
+
variant === "segmented" && "t-tabs--segmented",
|
|
97
|
+
variant === "pills" && "t-tabs--pills",
|
|
98
|
+
className,
|
|
99
|
+
)}
|
|
100
|
+
data-variant={variant}
|
|
101
|
+
{...props}
|
|
102
|
+
>
|
|
103
|
+
{variant === "segmented" ? (
|
|
104
|
+
<span ref={pillRef} className="t-tabs-pill" aria-hidden="true" />
|
|
105
|
+
) : null}
|
|
106
|
+
{children}
|
|
107
|
+
</TabsPrimitive.List>
|
|
108
|
+
</TabsListVariantContext>
|
|
100
109
|
);
|
|
101
110
|
});
|
|
102
111
|
TabsList.displayName = TabsPrimitive.List.displayName;
|
|
@@ -108,7 +117,9 @@ const TabsTrigger = React.forwardRef<
|
|
|
108
117
|
<TabsPrimitive.Trigger
|
|
109
118
|
ref={ref}
|
|
110
119
|
className={cn(
|
|
111
|
-
|
|
120
|
+
React.use(TabsListVariantContext) === "soft"
|
|
121
|
+
? "relative flex items-center gap-2 overflow-hidden rounded-full px-4 py-2 text-sm text-muted-foreground transition-[transform,background-color] hover:bg-surface-secondary active:scale-[0.97] data-[state=active]:bg-surface-secondary data-[state=active]:text-foreground"
|
|
122
|
+
: "t-tab inline-flex items-center justify-center gap-1.5 whitespace-nowrap text-sm font-medium ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
112
123
|
className,
|
|
113
124
|
)}
|
|
114
125
|
{...props}
|
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`
|
|
5
|
-
// `rounded-
|
|
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. 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
|
|
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
|
|
|
@@ -65,6 +65,20 @@
|
|
|
65
65
|
--radius-field: min(calc(var(--radius) + 0.25rem), 1.25rem);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/* The radius utilities from tokens.css, which this file replaces. They read
|
|
69
|
+
the app's `--radius`. */
|
|
70
|
+
@utility rounded-surface {
|
|
71
|
+
border-radius: clamp(0.75rem, var(--radius), 1rem);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@utility rounded-menu-item {
|
|
75
|
+
border-radius: min(var(--radius), 0.5rem);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@utility rounded-control {
|
|
79
|
+
border-radius: min(var(--radius), 0.625rem);
|
|
80
|
+
}
|
|
81
|
+
|
|
68
82
|
:root {
|
|
69
83
|
--scrollbar: rgb(var(--muted-foreground) / 0.35);
|
|
70
84
|
--field-radius: min(calc(var(--radius) + 0.25rem), 1.25rem);
|
package/tokens/src/tokens.css
CHANGED
|
@@ -94,6 +94,22 @@
|
|
|
94
94
|
--font-instrumentSans: "Instrument Sans", system-ui, sans-serif;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/* Radius utilities that follow the theme `--radius` (values at the default
|
|
98
|
+
1rem in brackets). Surfaces: kept between 0.75rem and 1rem (16px). Menu
|
|
99
|
+
items and sidebar nav rows: capped at 0.5rem (8px) so they sit inside the
|
|
100
|
+
surface corner. Controls: capped at 0.625rem (10px). */
|
|
101
|
+
@utility rounded-surface {
|
|
102
|
+
border-radius: clamp(0.75rem, var(--radius), 1rem);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@utility rounded-menu-item {
|
|
106
|
+
border-radius: min(var(--radius), 0.5rem);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@utility rounded-control {
|
|
110
|
+
border-radius: min(var(--radius), 0.625rem);
|
|
111
|
+
}
|
|
112
|
+
|
|
97
113
|
@layer base {
|
|
98
114
|
:root,
|
|
99
115
|
.light,
|