@lessly/ui 0.2.1 → 0.3.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/dist/index.d.ts CHANGED
@@ -730,6 +730,67 @@ interface UseSidebarResult {
730
730
  */
731
731
  declare function useSidebar(options?: UseSidebarOptions): UseSidebarResult;
732
732
 
733
+ type NavRowVariant = 'rail' | 'menu';
734
+ interface NavRowOwnProps {
735
+ /** Leading glyph. Rendered in a fixed 20px box; SVGs are normalized to 18px.
736
+ * A falsy value (`null`/`false`/`0`/`''`) renders nothing — there is no reserved
737
+ * empty slot, so the row's content shifts left when `icon` is falsy. */
738
+ icon?: React$1.ReactNode;
739
+ label: string;
740
+ active?: boolean;
741
+ variant: NavRowVariant;
742
+ /** Right-aligned adornment: chevron, checkmark, action. Keeps its own size.
743
+ * A falsy value (`null`/`false`/`0`/`''`) renders nothing — e.g. passing
744
+ * `trailing={count}` with `count === 0` produces no adornment, not a "0". */
745
+ trailing?: React$1.ReactNode;
746
+ /** Icon-only row (collapsed rails). `label` becomes the aria-label. */
747
+ hideLabel?: boolean;
748
+ }
749
+ /** Render the single child element instead of a button, e.g. a router <Link>.
750
+ * The row composes its content from props, so the child's own children are replaced —
751
+ * `children` is only accepted when `asChild` is true; otherwise it is rejected at the
752
+ * type level rather than silently dropped at render time. */
753
+ type NavRowProps = NavRowOwnProps & Omit<React$1.ComponentPropsWithoutRef<'button'>, 'children'> & ({
754
+ asChild: true;
755
+ children: React$1.ReactElement;
756
+ } | {
757
+ asChild?: false;
758
+ children?: never;
759
+ });
760
+ /** Nav row primitive (rail and menu variants). Composes an icon, label and trailing slot
761
+ * into a button, or into a single child via `asChild` (e.g. a router `<Link>`).
762
+ *
763
+ * Rail rows are designed for the `bg-surface` canvas: in the light theme `--hov-bg`
764
+ * equals `--color-bg-elevated`/`--menu-bg` (`#ffffff`), so a rail `NavRow` placed on an
765
+ * elevated/white surface has no hover contrast in light mode. This is the approved
766
+ * design and the token is not expected to change — avoid using rail `NavRow` off the
767
+ * `bg-surface` canvas in light theme. */
768
+ declare const NavRow: React$1.ForwardRefExoticComponent<NavRowProps & React$1.RefAttributes<HTMLButtonElement>>;
769
+
770
+ type NavSectionLabelProps = React$1.HTMLAttributes<HTMLDivElement>;
771
+ /** Section header for nav rails and menu popups. Insets match the 0.3.0 prototype
772
+ * (4px 4px 2px 8px), so it hangs slightly left of the rows it labels. */
773
+ declare const NavSectionLabel: React$1.ForwardRefExoticComponent<NavSectionLabelProps & React$1.RefAttributes<HTMLDivElement>>;
774
+
775
+ interface MenuPopupProps {
776
+ /** The element that opens the menu. Rendered via PopoverTrigger asChild. */
777
+ trigger: React$1.ReactNode;
778
+ children: React$1.ReactNode;
779
+ open?: boolean;
780
+ defaultOpen?: boolean;
781
+ onOpenChange?: (open: boolean) => void;
782
+ /** Which way the panel opens from its trigger. Default 'down'. */
783
+ direction?: 'up' | 'down';
784
+ align?: 'start' | 'center' | 'end';
785
+ /** Gap between trigger and panel, in px. Default 6 (prototype). */
786
+ sideOffset?: number;
787
+ className?: string;
788
+ }
789
+ /** Popup menu shell. Wraps the repo's Radix Popover, so outside-click close, focus
790
+ * management and portalling come from the primitive rather than being re-implemented. */
791
+ declare function MenuPopup({ trigger, children, open, defaultOpen, onOpenChange, direction, align, sideOffset, className, }: MenuPopupProps): React$1.JSX.Element;
792
+ declare const MenuDivider: React$1.ForwardRefExoticComponent<React$1.HTMLAttributes<HTMLDivElement> & React$1.RefAttributes<HTMLDivElement>>;
793
+
733
794
  interface NavItem {
734
795
  label: string;
735
796
  href: string;
@@ -788,4 +849,4 @@ declare function AppShell({ sidebar, topBar, children, className }: AppShellProp
788
849
  /** Alias of AppShell — reads better in app router files. */
789
850
  declare const AuthenticatedLayout: typeof AppShell;
790
851
 
791
- export { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, AppShell, type AppShellProps, AppSidebar, type AppSidebarProps, AspectRatio, AuthenticatedLayout, Avatar, AvatarFallback, AvatarImage, Badge, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, Calendar, type CalendarProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Carousel, type CarouselApi, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, Checkbox, Collapsible, CollapsibleContent, CollapsibleTrigger, Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut, ConfirmDialog, type ConfirmDialogProps, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, type CopyButtonProps, DatePicker, type DatePickerProps, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DocsLink, type DocsLinkProps, Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerTitle, DrawerTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, type EmptyStateProps, ExtensionIcon, type ExtensionIconProps, ExtensionLink, type ExtensionLinkProps, type ExtensionLinkUiMode, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, HoverCard, HoverCardContent, HoverCardTrigger, InlineError, type InlineErrorProps, Input, InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, Label, Menubar, MenubarCheckboxItem, MenubarContent, MenubarGroup, MenubarItem, MenubarLabel, MenubarMenu, MenubarPortal, MenubarRadioGroup, MenubarRadioItem, MenubarSeparator, MenubarShortcut, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger, type NavItem, type NavLinkComponent, NavigationMenu, NavigationMenuContent, NavigationMenuIndicator, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, NavigationMenuViewport, Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, Progress, RadioGroup, RadioGroupItem, ScrollArea, ScrollBar, SectionIntro, Select, type SelectOption, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarNav, SidebarNavLink, type SidebarNavLinkProps, type SidebarNavProps, Skeleton, Slider, StatCard, type StatCardProps, StatusDot, type StatusDotProps, Switch, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, type TextareaProps, ThemeToggle, Toaster, Toggle, ToggleGroup, ToggleGroupItem, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, type UseSidebarOptions, type UseSidebarResult, UserMenu, type UserMenuItem, type UserMenuProps, type UserMenuUser, alertVariants, badgeVariants, buttonVariants, cn, isKnownExtensionIcon, navigationMenuTriggerStyle, statusDotVariants, toggleVariants, useCarousel, useFormField, useIsMobile, useSidebar, useTheme };
852
+ export { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, AppShell, type AppShellProps, AppSidebar, type AppSidebarProps, AspectRatio, AuthenticatedLayout, Avatar, AvatarFallback, AvatarImage, Badge, type BadgeProps, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, type ButtonProps, Calendar, type CalendarProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Carousel, type CarouselApi, CarouselContent, type CarouselContextProps, CarouselItem, CarouselNext, type CarouselOptions, type CarouselPlugin, CarouselPrevious, type CarouselProps, Checkbox, Collapsible, CollapsibleContent, CollapsibleTrigger, Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut, ConfirmDialog, type ConfirmDialogProps, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, type CopyButtonProps, DatePicker, type DatePickerProps, type DeltaDirection, type DeltaTone, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DocsLink, type DocsLinkProps, Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerTitle, DrawerTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, type EmptyStateProps, ExtensionIcon, type ExtensionIconProps, ExtensionLink, type ExtensionLinkProps, type ExtensionLinkUiMode, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, HoverCard, HoverCardContent, HoverCardTrigger, InlineError, type InlineErrorProps, Input, InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, type InputProps, Label, MenuDivider, MenuPopup, type MenuPopupProps, Menubar, MenubarCheckboxItem, MenubarContent, MenubarGroup, MenubarItem, MenubarLabel, MenubarMenu, MenubarPortal, MenubarRadioGroup, MenubarRadioItem, MenubarSeparator, MenubarShortcut, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger, type NavItem, type NavLinkComponent, NavRow, type NavRowProps, type NavRowVariant, NavSectionLabel, type NavSectionLabelProps, NavigationMenu, NavigationMenuContent, NavigationMenuIndicator, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, NavigationMenuViewport, Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, type PaginationLinkProps, PaginationNext, PaginationPrevious, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, Progress, RadioGroup, RadioGroupItem, type ResolvedTheme, ScrollArea, ScrollBar, SectionIntro, type SectionIntroProps, Select, type SelectOption, type SelectProps, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarNav, SidebarNavLink, type SidebarNavLinkProps, type SidebarNavProps, Skeleton, Slider, StatCard, type StatCardProps, StatusDot, type StatusDotProps, Switch, type SwitchProps, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, type TextareaProps, type Theme, ThemeToggle, Toaster, Toggle, ToggleGroup, ToggleGroupItem, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, type UseSidebarOptions, type UseSidebarResult, UserMenu, type UserMenuItem, type UserMenuProps, type UserMenuUser, alertVariants, badgeVariants, buttonVariants, cn, isKnownExtensionIcon, navigationMenuTriggerStyle, statusDotVariants, toggleVariants, useCarousel, useFormField, useIsMobile, useSidebar, useTheme };
package/dist/index.js CHANGED
@@ -1878,6 +1878,10 @@ NavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayN
1878
1878
  // src/hooks/useTheme.ts
1879
1879
  import { useEffect, useState, useCallback } from "react";
1880
1880
  var STORAGE_KEY = "lessly_theme";
1881
+ var THEMES = ["light", "dark", "system"];
1882
+ function parseStoredTheme(stored) {
1883
+ return THEMES.includes(stored) ? stored : "system";
1884
+ }
1881
1885
  function getSystemTheme() {
1882
1886
  if (typeof window === "undefined") return "dark";
1883
1887
  return window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
@@ -1896,10 +1900,9 @@ function applyTheme(resolved) {
1896
1900
  }
1897
1901
  }
1898
1902
  function useTheme() {
1899
- const [theme, setThemeState] = useState(() => {
1900
- const stored = localStorage.getItem(STORAGE_KEY);
1901
- return stored || "system";
1902
- });
1903
+ const [theme, setThemeState] = useState(
1904
+ () => parseStoredTheme(localStorage.getItem(STORAGE_KEY))
1905
+ );
1903
1906
  const resolvedTheme = resolveTheme(theme);
1904
1907
  const setTheme = useCallback((newTheme) => {
1905
1908
  setThemeState(newTheme);
@@ -2632,7 +2635,7 @@ function UserMenu({ user, items, collapsed = false, align = "end" }) {
2632
2635
  "button",
2633
2636
  {
2634
2637
  type: "button",
2635
- className: "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors hover:bg-bg-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus",
2638
+ className: "flex w-full items-center gap-2.5 rounded-xl px-2.5 py-1.5 text-left transition-colors hover:bg-[var(--hov-bg)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus",
2636
2639
  children: [
2637
2640
  avatar,
2638
2641
  /* @__PURE__ */ jsxs25("span", { className: "min-w-0 flex-1", children: [
@@ -2649,30 +2652,41 @@ function UserMenu({ user, items, collapsed = false, align = "end" }) {
2649
2652
  /* @__PURE__ */ jsx52(TooltipTrigger, { asChild: true, children: menuTrigger }),
2650
2653
  /* @__PURE__ */ jsx52(TooltipContent, { side: "right", children: user.name })
2651
2654
  ] }) }) : menuTrigger,
2652
- /* @__PURE__ */ jsxs25(DropdownMenuContent, { align, className: "w-56", children: [
2653
- /* @__PURE__ */ jsxs25(DropdownMenuLabel, { children: [
2654
- /* @__PURE__ */ jsx52("span", { className: "block truncate text-sm font-medium text-text-primary", children: user.name }),
2655
- user.email && /* @__PURE__ */ jsx52("span", { className: "block truncate text-xs font-normal text-text-tertiary", children: user.email })
2656
- ] }),
2657
- /* @__PURE__ */ jsx52(DropdownMenuSeparator, {}),
2658
- items.map((item, i) => {
2659
- const Icon = item.icon;
2660
- const content = /* @__PURE__ */ jsxs25(Fragment, { children: [
2661
- Icon && /* @__PURE__ */ jsx52(Icon, { className: "size-4", "aria-hidden": "true" }),
2662
- item.label
2663
- ] });
2664
- return /* @__PURE__ */ jsx52(
2665
- DropdownMenuItem,
2666
- {
2667
- onSelect: item.onSelect,
2668
- className: cn("gap-2", item.destructive && "text-text-danger focus:text-text-danger"),
2669
- asChild: Boolean(item.href),
2670
- children: item.href ? /* @__PURE__ */ jsx52("a", { href: item.href, children: content }) : content
2671
- },
2672
- `${item.label}-${i}`
2673
- );
2674
- })
2675
- ] })
2655
+ /* @__PURE__ */ jsxs25(
2656
+ DropdownMenuContent,
2657
+ {
2658
+ align,
2659
+ sideOffset: 6,
2660
+ className: "flex w-56 flex-col gap-px rounded-xl border-border-subtle bg-[var(--menu-bg)] p-1 shadow-(--menu-shadow)",
2661
+ children: [
2662
+ /* @__PURE__ */ jsxs25(DropdownMenuLabel, { className: "px-2 pt-1.5 pb-1 text-xs font-medium text-text-tertiary", children: [
2663
+ /* @__PURE__ */ jsx52("span", { className: "block truncate text-sm font-medium text-text-primary", children: user.name }),
2664
+ user.email && /* @__PURE__ */ jsx52("span", { className: "block truncate text-xs font-normal text-text-tertiary", children: user.email })
2665
+ ] }),
2666
+ /* @__PURE__ */ jsx52(DropdownMenuSeparator, { className: "mx-0 shrink-0 bg-border-subtle" }),
2667
+ items.map((item, i) => {
2668
+ const Icon = item.icon;
2669
+ const content = /* @__PURE__ */ jsxs25(Fragment, { children: [
2670
+ Icon && /* @__PURE__ */ jsx52(Icon, { className: "size-4", "aria-hidden": "true" }),
2671
+ item.label
2672
+ ] });
2673
+ return /* @__PURE__ */ jsx52(
2674
+ DropdownMenuItem,
2675
+ {
2676
+ onSelect: item.onSelect,
2677
+ className: cn(
2678
+ "gap-2 rounded-md px-2 py-1.5 text-sm font-medium text-text-secondary focus:bg-[var(--menu-hov)] focus:text-text-primary",
2679
+ item.destructive && "text-text-danger focus:text-text-danger"
2680
+ ),
2681
+ asChild: Boolean(item.href),
2682
+ children: item.href ? /* @__PURE__ */ jsx52("a", { href: item.href, children: content }) : content
2683
+ },
2684
+ `${item.label}-${i}`
2685
+ );
2686
+ })
2687
+ ]
2688
+ }
2689
+ )
2676
2690
  ] });
2677
2691
  return menu;
2678
2692
  }
@@ -2770,9 +2784,113 @@ function useSidebar(options = {}) {
2770
2784
  return { collapsed, setCollapsed, toggle };
2771
2785
  }
2772
2786
 
2787
+ // src/components/nav-row.tsx
2788
+ import * as React47 from "react";
2789
+ import { Slot as Slot4 } from "@radix-ui/react-slot";
2790
+ import { Fragment as Fragment2, jsx as jsx55, jsxs as jsxs26 } from "react/jsx-runtime";
2791
+ var base = "flex w-full items-center text-left text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus";
2792
+ var shape = {
2793
+ rail: "gap-[14px] rounded-xl px-2.5 py-2",
2794
+ menu: "gap-2 rounded-md px-2 py-1.5"
2795
+ };
2796
+ var state = {
2797
+ rail: {
2798
+ idle: "font-medium text-text-secondary hover:bg-[var(--hov-bg)] hover:text-text-primary",
2799
+ active: "bg-bg-brand-subtle font-semibold text-text-brand"
2800
+ },
2801
+ menu: {
2802
+ idle: "font-medium text-text-secondary hover:bg-[var(--menu-hov)]",
2803
+ active: "font-semibold text-text-brand hover:bg-[var(--menu-hov)]"
2804
+ }
2805
+ };
2806
+ var NavRow = React47.forwardRef(function NavRow2({ icon, label, active = false, variant, trailing, hideLabel = false, asChild = false, className, children, ...props }, ref) {
2807
+ const content = /* @__PURE__ */ jsxs26(Fragment2, { children: [
2808
+ icon && /* @__PURE__ */ jsx55(
2809
+ "span",
2810
+ {
2811
+ className: "flex size-5 shrink-0 items-center justify-center [&_svg]:size-[18px]",
2812
+ "aria-hidden": "true",
2813
+ children: icon
2814
+ }
2815
+ ),
2816
+ !hideLabel && /* @__PURE__ */ jsx55("span", { className: "min-w-0 flex-1 truncate", children: label }),
2817
+ trailing && /* @__PURE__ */ jsx55("span", { className: "flex shrink-0 items-center", children: trailing })
2818
+ ] });
2819
+ const classes = cn(base, shape[variant], state[variant][active ? "active" : "idle"], className);
2820
+ if (asChild) {
2821
+ const child = React47.Children.only(children);
2822
+ return /* @__PURE__ */ jsx55(Slot4, { ref, className: classes, "aria-label": hideLabel ? label : void 0, ...props, children: React47.cloneElement(child, void 0, content) });
2823
+ }
2824
+ return /* @__PURE__ */ jsx55("button", { ref, type: "button", className: classes, "aria-label": hideLabel ? label : void 0, ...props, children: content });
2825
+ });
2826
+ NavRow.displayName = "NavRow";
2827
+
2828
+ // src/components/nav-section-label.tsx
2829
+ import * as React48 from "react";
2830
+ import { jsx as jsx56 } from "react/jsx-runtime";
2831
+ var NavSectionLabel = React48.forwardRef(
2832
+ function NavSectionLabel2({ className, ...props }, ref) {
2833
+ return /* @__PURE__ */ jsx56(
2834
+ "div",
2835
+ {
2836
+ ref,
2837
+ className: cn("pt-1 pr-1 pb-0.5 pl-2 text-xs font-medium text-text-tertiary", className),
2838
+ ...props
2839
+ }
2840
+ );
2841
+ }
2842
+ );
2843
+ NavSectionLabel.displayName = "NavSectionLabel";
2844
+
2845
+ // src/components/menu-popup.tsx
2846
+ import * as React49 from "react";
2847
+ import { jsx as jsx57, jsxs as jsxs27 } from "react/jsx-runtime";
2848
+ function MenuPopup({
2849
+ trigger,
2850
+ children,
2851
+ open,
2852
+ defaultOpen,
2853
+ onOpenChange,
2854
+ direction = "down",
2855
+ align = "start",
2856
+ sideOffset = 6,
2857
+ className
2858
+ }) {
2859
+ return /* @__PURE__ */ jsxs27(Popover, { open, defaultOpen, onOpenChange, children: [
2860
+ /* @__PURE__ */ jsx57(PopoverTrigger, { asChild: true, children: trigger }),
2861
+ /* @__PURE__ */ jsx57(
2862
+ PopoverContent,
2863
+ {
2864
+ side: direction === "up" ? "top" : "bottom",
2865
+ align,
2866
+ sideOffset,
2867
+ className: cn(
2868
+ "flex w-56 flex-col gap-px rounded-xl border border-border-subtle bg-[var(--menu-bg)] p-1 shadow-(--menu-shadow)",
2869
+ className
2870
+ ),
2871
+ children
2872
+ }
2873
+ )
2874
+ ] });
2875
+ }
2876
+ var MenuDivider = React49.forwardRef(
2877
+ function MenuDivider2({ className, ...props }, ref) {
2878
+ return /* @__PURE__ */ jsx57(
2879
+ "div",
2880
+ {
2881
+ ref,
2882
+ role: "separator",
2883
+ className: cn("my-1 h-px shrink-0 bg-border-subtle", className),
2884
+ ...props
2885
+ }
2886
+ );
2887
+ }
2888
+ );
2889
+ MenuDivider.displayName = "MenuDivider";
2890
+
2773
2891
  // src/components/sidebar-nav.tsx
2774
2892
  import { Link as Link3, useLocation } from "react-router";
2775
- import { jsx as jsx55, jsxs as jsxs26 } from "react/jsx-runtime";
2893
+ import { jsx as jsx58, jsxs as jsxs28 } from "react/jsx-runtime";
2776
2894
  function isActivePath(pathname, full, exact) {
2777
2895
  if (exact) return pathname === full;
2778
2896
  return pathname === full || pathname.startsWith(`${full}/`);
@@ -2788,28 +2906,23 @@ function SidebarNavLink({
2788
2906
  const Comp = LinkComponent ?? Link3;
2789
2907
  const to = `${basePath}${item.href}`;
2790
2908
  const Icon = item.icon;
2791
- const link = /* @__PURE__ */ jsxs26(
2792
- Comp,
2909
+ const link = /* @__PURE__ */ jsx58(
2910
+ NavRow,
2793
2911
  {
2794
- to,
2795
- className: cn(
2796
- "flex items-center rounded-lg text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus",
2797
- collapsed ? "size-9 justify-center" : "w-full gap-2.5 px-2.5 py-[7px]",
2798
- active ? "bg-bg-secondary font-medium text-text-brand" : "text-text-secondary hover:bg-bg-secondary hover:text-text-primary",
2799
- className
2800
- ),
2801
- "aria-current": active ? "page" : void 0,
2802
- "aria-label": collapsed ? item.label : void 0,
2803
- children: [
2804
- Icon && /* @__PURE__ */ jsx55(Icon, { className: "size-[18px] shrink-0", "aria-hidden": "true" }),
2805
- !collapsed && item.label
2806
- ]
2912
+ variant: "rail",
2913
+ active,
2914
+ label: item.label,
2915
+ hideLabel: collapsed,
2916
+ icon: Icon ? /* @__PURE__ */ jsx58(Icon, { "aria-hidden": "true" }) : void 0,
2917
+ className: cn(collapsed && "size-9 justify-center px-0 py-0", className),
2918
+ asChild: true,
2919
+ children: /* @__PURE__ */ jsx58(Comp, { to, "aria-current": active ? "page" : void 0 })
2807
2920
  }
2808
2921
  );
2809
2922
  if (!collapsed) return link;
2810
- return /* @__PURE__ */ jsxs26(Tooltip, { children: [
2811
- /* @__PURE__ */ jsx55(TooltipTrigger, { asChild: true, children: link }),
2812
- /* @__PURE__ */ jsx55(TooltipContent, { side: "right", children: item.label })
2923
+ return /* @__PURE__ */ jsxs28(Tooltip, { children: [
2924
+ /* @__PURE__ */ jsx58(TooltipTrigger, { asChild: true, children: link }),
2925
+ /* @__PURE__ */ jsx58(TooltipContent, { side: "right", children: item.label })
2813
2926
  ] });
2814
2927
  }
2815
2928
  function SidebarNav({
@@ -2821,18 +2934,18 @@ function SidebarNav({
2821
2934
  className
2822
2935
  }) {
2823
2936
  const { pathname } = useLocation();
2824
- const nav = /* @__PURE__ */ jsx55(
2937
+ const nav = /* @__PURE__ */ jsx58(
2825
2938
  "nav",
2826
2939
  {
2827
2940
  className: cn(
2828
2941
  "flex flex-col gap-0.5",
2829
- collapsed ? "items-center px-1" : "px-2",
2942
+ collapsed ? "items-center px-1" : "px-3",
2830
2943
  className
2831
2944
  ),
2832
2945
  children: items.map((item) => {
2833
2946
  const full = `${basePath}${item.href}`;
2834
2947
  const active = isItemActive ? isItemActive(item, pathname) : isActivePath(pathname, full, item.exact);
2835
- return /* @__PURE__ */ jsx55(
2948
+ return /* @__PURE__ */ jsx58(
2836
2949
  SidebarNavLink,
2837
2950
  {
2838
2951
  item,
@@ -2846,12 +2959,12 @@ function SidebarNav({
2846
2959
  })
2847
2960
  }
2848
2961
  );
2849
- return collapsed ? /* @__PURE__ */ jsx55(TooltipProvider, { children: nav }) : nav;
2962
+ return collapsed ? /* @__PURE__ */ jsx58(TooltipProvider, { children: nav }) : nav;
2850
2963
  }
2851
2964
 
2852
2965
  // src/components/app-sidebar.tsx
2853
2966
  import { PanelLeft } from "lucide-react";
2854
- import { jsx as jsx56, jsxs as jsxs27 } from "react/jsx-runtime";
2967
+ import { jsx as jsx59, jsxs as jsxs29 } from "react/jsx-runtime";
2855
2968
  function AppSidebar({
2856
2969
  items,
2857
2970
  logo,
@@ -2871,7 +2984,7 @@ function AppSidebar({
2871
2984
  defaultCollapsed,
2872
2985
  onCollapsedChange
2873
2986
  });
2874
- return /* @__PURE__ */ jsxs27(
2987
+ return /* @__PURE__ */ jsxs29(
2875
2988
  "aside",
2876
2989
  {
2877
2990
  "data-collapsed": collapsed,
@@ -2881,76 +2994,76 @@ function AppSidebar({
2881
2994
  className
2882
2995
  ),
2883
2996
  children: [
2884
- collapsed ? /* @__PURE__ */ jsx56("div", { className: "flex justify-center border-b border-border-subtle p-2", children: header ?? /* @__PURE__ */ jsxs27("div", { className: "group relative flex size-8 items-center justify-center", children: [
2885
- /* @__PURE__ */ jsx56("span", { className: "pointer-events-none transition-opacity group-hover:opacity-0 group-focus-within:opacity-0", children: logoCollapsed ?? logo }),
2886
- /* @__PURE__ */ jsx56(
2997
+ collapsed ? /* @__PURE__ */ jsx59("div", { className: "flex justify-center border-b border-border-subtle p-2", children: header ?? /* @__PURE__ */ jsxs29("div", { className: "group relative flex size-8 items-center justify-center", children: [
2998
+ /* @__PURE__ */ jsx59("span", { className: "pointer-events-none transition-opacity group-hover:opacity-0 group-focus-within:opacity-0", children: logoCollapsed ?? logo }),
2999
+ /* @__PURE__ */ jsx59(
2887
3000
  "button",
2888
3001
  {
2889
3002
  type: "button",
2890
3003
  onClick: toggle,
2891
3004
  "aria-label": "Expand sidebar",
2892
3005
  "aria-expanded": false,
2893
- className: "absolute inset-0 flex items-center justify-center rounded-md text-text-secondary opacity-0 transition-opacity hover:bg-bg-secondary focus-visible:outline-none focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-border-focus group-hover:opacity-100 group-focus-within:opacity-100",
2894
- children: /* @__PURE__ */ jsx56(PanelLeft, { className: "size-4", "aria-hidden": "true" })
3006
+ className: "absolute inset-0 flex items-center justify-center rounded-md text-text-secondary opacity-0 transition-opacity hover:bg-[var(--hov-bg)] focus-visible:outline-none focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-border-focus group-hover:opacity-100 group-focus-within:opacity-100",
3007
+ children: /* @__PURE__ */ jsx59(PanelLeft, { className: "size-4", "aria-hidden": "true" })
2895
3008
  }
2896
3009
  )
2897
- ] }) }) : /* @__PURE__ */ jsxs27("div", { className: "flex items-center justify-between border-b border-border-subtle p-2", children: [
3010
+ ] }) }) : /* @__PURE__ */ jsxs29("div", { className: "flex items-center justify-between border-b border-border-subtle p-3", children: [
2898
3011
  header ?? logo,
2899
- /* @__PURE__ */ jsx56(
3012
+ /* @__PURE__ */ jsx59(
2900
3013
  "button",
2901
3014
  {
2902
3015
  type: "button",
2903
3016
  onClick: toggle,
2904
3017
  "aria-label": "Collapse sidebar",
2905
3018
  "aria-expanded": true,
2906
- className: "flex size-6 items-center justify-center rounded-md text-text-secondary transition-colors hover:bg-bg-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus",
2907
- children: /* @__PURE__ */ jsx56(PanelLeft, { className: "size-4", "aria-hidden": "true" })
3019
+ className: "flex size-6 items-center justify-center rounded-md text-text-secondary transition-colors hover:bg-[var(--hov-bg)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus",
3020
+ children: /* @__PURE__ */ jsx59(PanelLeft, { className: "size-4", "aria-hidden": "true" })
2908
3021
  }
2909
3022
  )
2910
3023
  ] }),
2911
- /* @__PURE__ */ jsx56("div", { className: "flex-1 overflow-y-auto overflow-x-hidden py-2", children: /* @__PURE__ */ jsx56(SidebarNav, { items, collapsed, basePath, LinkComponent, isItemActive }) }),
2912
- footer && /* @__PURE__ */ jsx56("div", { className: cn("border-t border-border-subtle", collapsed ? "px-1 py-2" : "px-2 py-2"), children: footer })
3024
+ /* @__PURE__ */ jsx59("div", { className: "flex-1 overflow-y-auto overflow-x-hidden pt-3 pb-2", children: /* @__PURE__ */ jsx59(SidebarNav, { items, collapsed, basePath, LinkComponent, isItemActive }) }),
3025
+ footer && /* @__PURE__ */ jsx59("div", { className: cn("border-t border-border-subtle", collapsed ? "px-1 py-2" : "px-3 py-3"), children: footer })
2913
3026
  ]
2914
3027
  }
2915
3028
  );
2916
3029
  }
2917
3030
 
2918
3031
  // src/components/app-shell.tsx
2919
- import * as React47 from "react";
3032
+ import * as React50 from "react";
2920
3033
  import { Menu as Menu2 } from "lucide-react";
2921
3034
  import { useLocation as useLocation2 } from "react-router";
2922
- import { jsx as jsx57, jsxs as jsxs28 } from "react/jsx-runtime";
3035
+ import { jsx as jsx60, jsxs as jsxs30 } from "react/jsx-runtime";
2923
3036
  function expandedSidebar(sidebar) {
2924
- return React47.isValidElement(sidebar) ? React47.cloneElement(sidebar, { collapsed: false }) : sidebar;
3037
+ return React50.isValidElement(sidebar) ? React50.cloneElement(sidebar, { collapsed: false }) : sidebar;
2925
3038
  }
2926
3039
  function AppShell({ sidebar, topBar, children, className }) {
2927
3040
  const { pathname } = useLocation2();
2928
- const [mobileOpen, setMobileOpen] = React47.useState(false);
2929
- React47.useEffect(() => {
3041
+ const [mobileOpen, setMobileOpen] = React50.useState(false);
3042
+ React50.useEffect(() => {
2930
3043
  setMobileOpen(false);
2931
3044
  }, [pathname]);
2932
3045
  const isMobile = useIsMobile();
2933
3046
  if (isMobile) {
2934
- return /* @__PURE__ */ jsx57(TooltipProvider, { children: /* @__PURE__ */ jsxs28("div", { className: cn("flex min-h-screen flex-col bg-bg-sunken", className), children: [
2935
- /* @__PURE__ */ jsxs28("header", { className: "flex shrink-0 items-center justify-between border-b border-border-subtle bg-bg-surface px-4 py-3", children: [
2936
- /* @__PURE__ */ jsxs28(Sheet, { open: mobileOpen, onOpenChange: setMobileOpen, children: [
2937
- /* @__PURE__ */ jsx57(SheetTrigger, { asChild: true, children: /* @__PURE__ */ jsx57(Button, { variant: "ghost", size: "icon", className: "size-8 text-text-secondary", "aria-label": "Open navigation", children: /* @__PURE__ */ jsx57(Menu2, { className: "size-5" }) }) }),
2938
- /* @__PURE__ */ jsxs28(SheetContent, { side: "left", className: "w-[240px] bg-bg-surface p-0", children: [
2939
- /* @__PURE__ */ jsx57(SheetTitle, { className: "sr-only", children: "Navigation" }),
2940
- /* @__PURE__ */ jsx57(SheetDescription, { className: "sr-only", children: "Application navigation menu" }),
3047
+ return /* @__PURE__ */ jsx60(TooltipProvider, { children: /* @__PURE__ */ jsxs30("div", { className: cn("flex min-h-screen flex-col bg-bg-sunken", className), children: [
3048
+ /* @__PURE__ */ jsxs30("header", { className: "flex shrink-0 items-center justify-between border-b border-border-subtle bg-bg-surface px-4 py-3", children: [
3049
+ /* @__PURE__ */ jsxs30(Sheet, { open: mobileOpen, onOpenChange: setMobileOpen, children: [
3050
+ /* @__PURE__ */ jsx60(SheetTrigger, { asChild: true, children: /* @__PURE__ */ jsx60(Button, { variant: "ghost", size: "icon", className: "size-8 text-text-secondary", "aria-label": "Open navigation", children: /* @__PURE__ */ jsx60(Menu2, { className: "size-5" }) }) }),
3051
+ /* @__PURE__ */ jsxs30(SheetContent, { side: "left", className: "w-[240px] bg-bg-surface p-0", children: [
3052
+ /* @__PURE__ */ jsx60(SheetTitle, { className: "sr-only", children: "Navigation" }),
3053
+ /* @__PURE__ */ jsx60(SheetDescription, { className: "sr-only", children: "Application navigation menu" }),
2941
3054
  expandedSidebar(sidebar)
2942
3055
  ] })
2943
3056
  ] }),
2944
- /* @__PURE__ */ jsx57("div", { className: "flex items-center gap-2", children: topBar })
3057
+ /* @__PURE__ */ jsx60("div", { className: "flex items-center gap-2", children: topBar })
2945
3058
  ] }),
2946
- /* @__PURE__ */ jsx57("main", { className: "flex-1 overflow-auto p-4", children })
3059
+ /* @__PURE__ */ jsx60("main", { className: "flex-1 overflow-auto p-4", children })
2947
3060
  ] }) });
2948
3061
  }
2949
- return /* @__PURE__ */ jsx57(TooltipProvider, { children: /* @__PURE__ */ jsxs28("div", { className: cn("flex min-h-screen gap-3 bg-bg-sunken p-6", className), children: [
2950
- /* @__PURE__ */ jsx57("div", { className: "sticky top-6 h-[calc(100vh-48px)] shrink-0", children: sidebar }),
2951
- /* @__PURE__ */ jsxs28("div", { className: "flex min-w-0 flex-1 flex-col overflow-hidden rounded-2xl border border-border-subtle bg-bg-surface", children: [
2952
- /* @__PURE__ */ jsx57("div", { className: "flex min-h-[52px] shrink-0 items-center justify-end gap-2 border-b border-border-subtle px-5", children: topBar }),
2953
- /* @__PURE__ */ jsx57("main", { className: "flex-1 overflow-auto px-7 py-8", children })
3062
+ return /* @__PURE__ */ jsx60(TooltipProvider, { children: /* @__PURE__ */ jsxs30("div", { className: cn("flex min-h-screen gap-2 bg-bg-sunken p-4", className), children: [
3063
+ /* @__PURE__ */ jsx60("div", { className: "sticky top-4 h-[calc(100vh-32px)] shrink-0", children: sidebar }),
3064
+ /* @__PURE__ */ jsxs30("div", { className: "flex min-w-0 flex-1 flex-col overflow-hidden rounded-2xl border border-border-subtle bg-bg-surface", children: [
3065
+ /* @__PURE__ */ jsx60("div", { className: "flex min-h-[52px] shrink-0 items-center justify-end gap-2 border-b border-border-subtle px-5", children: topBar }),
3066
+ /* @__PURE__ */ jsx60("main", { className: "flex-1 overflow-auto px-7 py-8", children })
2954
3067
  ] })
2955
3068
  ] }) });
2956
3069
  }
@@ -3089,6 +3202,8 @@ export {
3089
3202
  InputOTPSeparator,
3090
3203
  InputOTPSlot,
3091
3204
  Label2 as Label,
3205
+ MenuDivider,
3206
+ MenuPopup,
3092
3207
  Menubar,
3093
3208
  MenubarCheckboxItem,
3094
3209
  MenubarContent,
@@ -3105,6 +3220,8 @@ export {
3105
3220
  MenubarSubContent,
3106
3221
  MenubarSubTrigger,
3107
3222
  MenubarTrigger,
3223
+ NavRow,
3224
+ NavSectionLabel,
3108
3225
  NavigationMenu,
3109
3226
  NavigationMenuContent,
3110
3227
  NavigationMenuIndicator,
package/dist/styles.css CHANGED
@@ -107,6 +107,14 @@
107
107
  --color-border-success: #1da456;
108
108
  --color-border-warning: #b3780e;
109
109
  --color-border-danger: #e82020;
110
+ /* === Nav / menu surfaces (0.3.0 sidebar) === */
111
+ /* --menu-shadow is a literal: this package ships no --shadow-* variables (shadows live
112
+ in the generated Tailwind preset's boxShadow map), so var(--shadow-md) would resolve
113
+ to nothing. Value mirrors the 0.3.0 prototype DS's own --shadow-md. */
114
+ --hov-bg: color-mix(in srgb, var(--color-text-primary) 7%, transparent);
115
+ --menu-bg: var(--color-bg-elevated);
116
+ --menu-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
117
+ --menu-hov: var(--hov-bg);
110
118
  }
111
119
 
112
120
  .light {
@@ -116,7 +124,7 @@
116
124
  --color-bg-elevated: #ffffff;
117
125
  --color-bg-sunken: #e1e2e4;
118
126
  --color-bg-brand: #06297d;
119
- --color-bg-brand-subtle: #eaeef4;
127
+ --color-bg-brand-subtle: #e4ecfe;
120
128
  --color-bg-brand-bright: #165ff2;
121
129
  --color-bg-brand-hover: #05226a;
122
130
  --color-bg-success: #13703c;
@@ -137,7 +145,7 @@
137
145
  --color-text-on-success: #f6f7f7;
138
146
  --color-text-on-warning: #08090c;
139
147
  --color-text-on-danger: #f6f7f7;
140
- --color-text-brand: #0940b8;
148
+ --color-text-brand: #0b4cd5;
141
149
  --color-text-success: #13703c;
142
150
  --color-text-warning: #7a5409;
143
151
  --color-text-danger: #94120e;
@@ -146,7 +154,7 @@
146
154
  --color-icon-primary: #252936;
147
155
  --color-icon-secondary: #535a6e;
148
156
  --color-icon-disabled: #8a91a7;
149
- --color-icon-brand: #0940b8;
157
+ --color-icon-brand: #165ff2;
150
158
  --color-icon-success: #13703c;
151
159
  --color-icon-warning: #7a5409;
152
160
  --color-icon-danger: #94120e;
@@ -158,6 +166,11 @@
158
166
  --color-border-success: #188a49;
159
167
  --color-border-warning: #b3780e;
160
168
  --color-border-danger: #e82020;
169
+ /* Light keeps opaque plates: a translucent text-mix would vanish on a white menu. */
170
+ --hov-bg: #ffffff;
171
+ --menu-bg: #ffffff;
172
+ --menu-shadow: 0 12px 32px rgba(16, 24, 40, 0.1), 0 2px 8px rgba(16, 24, 40, 0.06);
173
+ --menu-hov: #f2f4f7;
161
174
  }
162
175
 
163
176
  /* === Base === */
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@lessly/ui",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "Lessly design system — shared UI primitives, tokens, and theme",
5
5
  "type": "module",
6
+ "packageManager": "pnpm@10.30.3",
6
7
  "main": "./dist/index.js",
7
8
  "module": "./dist/index.js",
8
9
  "types": "./dist/index.d.ts",
@@ -26,6 +27,20 @@
26
27
  "engines": {
27
28
  "node": ">=20"
28
29
  },
30
+ "scripts": {
31
+ "build": "tsup && node scripts/build-styles.mjs",
32
+ "prepare": "pnpm build",
33
+ "test": "vitest run",
34
+ "type-check": "tsc --noEmit",
35
+ "lint": "eslint .",
36
+ "prepublishOnly": "pnpm build && pnpm test && node scripts/check-package-contract.mjs",
37
+ "storybook": "storybook dev -p 6006 --no-open",
38
+ "build-storybook": "storybook build",
39
+ "changeset": "changeset",
40
+ "release:version": "changeset version && node scripts/stamp-changelog-date.mjs",
41
+ "release:publish": "node scripts/release-publish.mjs",
42
+ "release:tag": "changeset tag"
43
+ },
29
44
  "publishConfig": {
30
45
  "registry": "https://registry.npmjs.org/",
31
46
  "access": "public"
@@ -101,17 +116,5 @@
101
116
  "typescript-eslint": "^8.63.0",
102
117
  "vite": "^5.4.21",
103
118
  "vitest": "^3.0.5"
104
- },
105
- "scripts": {
106
- "build": "tsup && node scripts/build-styles.mjs",
107
- "test": "vitest run",
108
- "type-check": "tsc --noEmit",
109
- "lint": "eslint .",
110
- "storybook": "storybook dev -p 6006 --no-open",
111
- "build-storybook": "storybook build",
112
- "changeset": "changeset",
113
- "release:version": "changeset version && node scripts/stamp-changelog-date.mjs",
114
- "release:publish": "node scripts/release-publish.mjs",
115
- "release:tag": "changeset tag"
116
119
  }
117
- }
120
+ }