@open-mercato/ui 0.6.2-develop.3372.1.de9a169973 → 0.6.2-develop.3380.1.bb14509041

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.
Files changed (30) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +1 -0
  3. package/dist/ai/AiIcon.js +1 -1
  4. package/dist/ai/AiIcon.js.map +2 -2
  5. package/dist/backend/AppShell.js +223 -153
  6. package/dist/backend/AppShell.js.map +2 -2
  7. package/dist/backend/ProfileDropdown.js +101 -79
  8. package/dist/backend/ProfileDropdown.js.map +2 -2
  9. package/dist/backend/notifications/NotificationCountBadge.js +1 -1
  10. package/dist/backend/notifications/NotificationCountBadge.js.map +2 -2
  11. package/dist/backend/notifications/NotificationItem.js +67 -50
  12. package/dist/backend/notifications/NotificationItem.js.map +2 -2
  13. package/dist/backend/notifications/NotificationPanel.js +108 -85
  14. package/dist/backend/notifications/NotificationPanel.js.map +2 -2
  15. package/dist/primitives/breadcrumb.js +161 -0
  16. package/dist/primitives/breadcrumb.js.map +7 -0
  17. package/dist/primitives/sheet.js +114 -0
  18. package/dist/primitives/sheet.js.map +7 -0
  19. package/package.json +3 -3
  20. package/src/ai/AiIcon.tsx +1 -1
  21. package/src/backend/AppShell.tsx +151 -49
  22. package/src/backend/ProfileDropdown.tsx +115 -88
  23. package/src/backend/__tests__/AppShell.test.tsx +8 -0
  24. package/src/backend/notifications/NotificationCountBadge.tsx +1 -1
  25. package/src/backend/notifications/NotificationItem.tsx +97 -104
  26. package/src/backend/notifications/NotificationPanel.tsx +126 -102
  27. package/src/primitives/__tests__/breadcrumb.test.tsx +298 -0
  28. package/src/primitives/__tests__/sheet.test.tsx +152 -0
  29. package/src/primitives/breadcrumb.tsx +185 -0
  30. package/src/primitives/sheet.tsx +165 -0
@@ -0,0 +1,161 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { Slot } from "@radix-ui/react-slot";
5
+ import { ChevronRight, MoreHorizontal } from "lucide-react";
6
+ import { cn } from "@open-mercato/shared/lib/utils";
7
+ const BreadcrumbContext = React.createContext({
8
+ divider: "slash"
9
+ });
10
+ function Breadcrumb({
11
+ divider = "slash",
12
+ className,
13
+ children,
14
+ ...props
15
+ }) {
16
+ const contextValue = React.useMemo(
17
+ () => ({ divider }),
18
+ [divider]
19
+ );
20
+ return /* @__PURE__ */ jsx(BreadcrumbContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
21
+ "nav",
22
+ {
23
+ "aria-label": "Breadcrumb",
24
+ "data-slot": "breadcrumb",
25
+ "data-divider": divider,
26
+ className: cn("min-w-0", className),
27
+ ...props,
28
+ children
29
+ }
30
+ ) });
31
+ }
32
+ function BreadcrumbList({ className, children, ...props }) {
33
+ if (React.Children.count(children) === 0) return null;
34
+ return /* @__PURE__ */ jsx(
35
+ "ol",
36
+ {
37
+ "data-slot": "breadcrumb-list",
38
+ className: cn(
39
+ "flex items-center gap-1.5 text-sm font-medium leading-5 tracking-tight min-w-0",
40
+ className
41
+ ),
42
+ ...props,
43
+ children
44
+ }
45
+ );
46
+ }
47
+ function BreadcrumbItem({ className, ...props }) {
48
+ return /* @__PURE__ */ jsx(
49
+ "li",
50
+ {
51
+ "data-slot": "breadcrumb-item",
52
+ className: cn("inline-flex items-center gap-1.5 min-w-0", className),
53
+ ...props
54
+ }
55
+ );
56
+ }
57
+ function BreadcrumbLink({
58
+ asChild = false,
59
+ className,
60
+ children,
61
+ ...props
62
+ }) {
63
+ const Comp = asChild ? Slot : "a";
64
+ return /* @__PURE__ */ jsx(
65
+ Comp,
66
+ {
67
+ "data-slot": "breadcrumb-link",
68
+ className: cn(
69
+ "inline-flex items-center gap-1.5 max-w-[40vw] md:max-w-[28vw] truncate text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:text-foreground rounded-sm focus-visible:shadow-focus [&_svg:not([class*='size-'])]:size-5 [&_svg]:shrink-0",
70
+ className
71
+ ),
72
+ ...props,
73
+ children
74
+ }
75
+ );
76
+ }
77
+ function BreadcrumbPage({ className, ...props }) {
78
+ return /* @__PURE__ */ jsx(
79
+ "span",
80
+ {
81
+ "aria-current": "page",
82
+ "data-slot": "breadcrumb-page",
83
+ className: cn(
84
+ "inline-block text-foreground truncate min-w-0 max-w-[45vw] md:max-w-[60vw] [&_svg:not([class*='size-'])]:size-5 [&_svg]:shrink-0 [&_svg]:inline-block [&_svg]:align-middle",
85
+ className
86
+ ),
87
+ ...props
88
+ }
89
+ );
90
+ }
91
+ function BreadcrumbStatic({ className, ...props }) {
92
+ return /* @__PURE__ */ jsx(
93
+ "span",
94
+ {
95
+ "data-slot": "breadcrumb-static",
96
+ className: cn(
97
+ "inline-flex items-center gap-1.5 max-w-[40vw] md:max-w-[28vw] truncate text-muted-foreground select-none [&_svg:not([class*='size-'])]:size-5 [&_svg]:shrink-0",
98
+ className
99
+ ),
100
+ ...props
101
+ }
102
+ );
103
+ }
104
+ function BreadcrumbSeparator({
105
+ divider: dividerProp,
106
+ className,
107
+ children,
108
+ ...props
109
+ }) {
110
+ const { divider: contextDivider } = React.useContext(BreadcrumbContext);
111
+ const divider = dividerProp ?? contextDivider;
112
+ return /* @__PURE__ */ jsx(
113
+ "li",
114
+ {
115
+ role: "presentation",
116
+ "aria-hidden": "true",
117
+ "data-slot": "breadcrumb-separator",
118
+ "data-divider": divider,
119
+ className: cn(
120
+ "inline-flex items-center text-text-disabled select-none [&_svg:not([class*='size-'])]:size-5",
121
+ className
122
+ ),
123
+ ...props,
124
+ children: children ?? /* @__PURE__ */ jsx(DefaultSeparator, { divider })
125
+ }
126
+ );
127
+ }
128
+ function DefaultSeparator({ divider }) {
129
+ if (divider === "arrow") return /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true" });
130
+ if (divider === "dot") return /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" });
131
+ return /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "/" });
132
+ }
133
+ function BreadcrumbEllipsis({ className, children, ...props }) {
134
+ return /* @__PURE__ */ jsxs(
135
+ "span",
136
+ {
137
+ role: "presentation",
138
+ "data-slot": "breadcrumb-ellipsis",
139
+ className: cn(
140
+ "inline-flex size-5 items-center justify-center text-muted-foreground",
141
+ className
142
+ ),
143
+ ...props,
144
+ children: [
145
+ children ?? /* @__PURE__ */ jsx(MoreHorizontal, { "aria-hidden": "true", className: "size-4" }),
146
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: "More" })
147
+ ]
148
+ }
149
+ );
150
+ }
151
+ export {
152
+ Breadcrumb,
153
+ BreadcrumbEllipsis,
154
+ BreadcrumbItem,
155
+ BreadcrumbLink,
156
+ BreadcrumbList,
157
+ BreadcrumbPage,
158
+ BreadcrumbSeparator,
159
+ BreadcrumbStatic
160
+ };
161
+ //# sourceMappingURL=breadcrumb.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/primitives/breadcrumb.tsx"],
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { Slot } from '@radix-ui/react-slot'\nimport { ChevronRight, MoreHorizontal } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nexport type BreadcrumbDivider = 'slash' | 'arrow' | 'dot'\n\ntype BreadcrumbContextValue = {\n divider: BreadcrumbDivider\n}\n\nconst BreadcrumbContext = React.createContext<BreadcrumbContextValue>({\n divider: 'slash',\n})\n\nexport type BreadcrumbProps = React.ComponentProps<'nav'> & {\n divider?: BreadcrumbDivider\n}\n\nexport function Breadcrumb({\n divider = 'slash',\n className,\n children,\n ...props\n}: BreadcrumbProps) {\n const contextValue = React.useMemo<BreadcrumbContextValue>(\n () => ({ divider }),\n [divider],\n )\n return (\n <BreadcrumbContext.Provider value={contextValue}>\n <nav\n aria-label=\"Breadcrumb\"\n data-slot=\"breadcrumb\"\n data-divider={divider}\n className={cn('min-w-0', className)}\n {...props}\n >\n {children}\n </nav>\n </BreadcrumbContext.Provider>\n )\n}\n\nexport type BreadcrumbListProps = React.ComponentProps<'ol'>\n\nexport function BreadcrumbList({ className, children, ...props }: BreadcrumbListProps) {\n if (React.Children.count(children) === 0) return null\n return (\n <ol\n data-slot=\"breadcrumb-list\"\n className={cn(\n 'flex items-center gap-1.5 text-sm font-medium leading-5 tracking-tight min-w-0',\n className,\n )}\n {...props}\n >\n {children}\n </ol>\n )\n}\n\nexport type BreadcrumbItemProps = React.ComponentProps<'li'>\n\nexport function BreadcrumbItem({ className, ...props }: BreadcrumbItemProps) {\n return (\n <li\n data-slot=\"breadcrumb-item\"\n className={cn('inline-flex items-center gap-1.5 min-w-0', className)}\n {...props}\n />\n )\n}\n\nexport type BreadcrumbLinkProps = React.ComponentProps<'a'> & {\n asChild?: boolean\n}\n\nexport function BreadcrumbLink({\n asChild = false,\n className,\n children,\n ...props\n}: BreadcrumbLinkProps) {\n const Comp = asChild ? Slot : 'a'\n return (\n <Comp\n data-slot=\"breadcrumb-link\"\n className={cn(\n \"inline-flex items-center gap-1.5 max-w-[40vw] md:max-w-[28vw] truncate text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:text-foreground rounded-sm focus-visible:shadow-focus [&_svg:not([class*='size-'])]:size-5 [&_svg]:shrink-0\",\n className,\n )}\n {...props}\n >\n {children}\n </Comp>\n )\n}\n\nexport type BreadcrumbPageProps = React.ComponentProps<'span'>\n\nexport function BreadcrumbPage({ className, ...props }: BreadcrumbPageProps) {\n return (\n <span\n aria-current=\"page\"\n data-slot=\"breadcrumb-page\"\n className={cn(\n \"inline-block text-foreground truncate min-w-0 max-w-[45vw] md:max-w-[60vw] [&_svg:not([class*='size-'])]:size-5 [&_svg]:shrink-0 [&_svg]:inline-block [&_svg]:align-middle\",\n className,\n )}\n {...props}\n />\n )\n}\n\nexport type BreadcrumbStaticProps = React.ComponentProps<'span'>\n\nexport function BreadcrumbStatic({ className, ...props }: BreadcrumbStaticProps) {\n return (\n <span\n data-slot=\"breadcrumb-static\"\n className={cn(\n \"inline-flex items-center gap-1.5 max-w-[40vw] md:max-w-[28vw] truncate text-muted-foreground select-none [&_svg:not([class*='size-'])]:size-5 [&_svg]:shrink-0\",\n className,\n )}\n {...props}\n />\n )\n}\n\nexport type BreadcrumbSeparatorProps = React.ComponentProps<'li'> & {\n divider?: BreadcrumbDivider\n}\n\nexport function BreadcrumbSeparator({\n divider: dividerProp,\n className,\n children,\n ...props\n}: BreadcrumbSeparatorProps) {\n const { divider: contextDivider } = React.useContext(BreadcrumbContext)\n const divider = dividerProp ?? contextDivider\n return (\n <li\n role=\"presentation\"\n aria-hidden=\"true\"\n data-slot=\"breadcrumb-separator\"\n data-divider={divider}\n className={cn(\n \"inline-flex items-center text-text-disabled select-none [&_svg:not([class*='size-'])]:size-5\",\n className,\n )}\n {...props}\n >\n {children ?? <DefaultSeparator divider={divider} />}\n </li>\n )\n}\n\nfunction DefaultSeparator({ divider }: { divider: BreadcrumbDivider }) {\n if (divider === 'arrow') return <ChevronRight aria-hidden=\"true\" />\n if (divider === 'dot') return <span aria-hidden=\"true\">\u00B7</span>\n return <span aria-hidden=\"true\">/</span>\n}\n\nexport type BreadcrumbEllipsisProps = React.ComponentProps<'span'>\n\nexport function BreadcrumbEllipsis({ className, children, ...props }: BreadcrumbEllipsisProps) {\n return (\n <span\n role=\"presentation\"\n data-slot=\"breadcrumb-ellipsis\"\n className={cn(\n 'inline-flex size-5 items-center justify-center text-muted-foreground',\n className,\n )}\n {...props}\n >\n {children ?? <MoreHorizontal aria-hidden=\"true\" className=\"size-4\" />}\n <span className=\"sr-only\">More</span>\n </span>\n )\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,114 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
5
+ import { cva } from "class-variance-authority";
6
+ import { X } from "lucide-react";
7
+ import { cn } from "@open-mercato/shared/lib/utils";
8
+ const Sheet = DialogPrimitive.Root;
9
+ const SheetTrigger = DialogPrimitive.Trigger;
10
+ const SheetClose = DialogPrimitive.Close;
11
+ const SheetPortal = DialogPrimitive.Portal;
12
+ const SheetOverlay = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
13
+ DialogPrimitive.Overlay,
14
+ {
15
+ className: cn(
16
+ "fixed inset-x-0 bottom-0 top-[var(--topbar-height,0px)] z-overlay bg-black/40 backdrop-blur-sm",
17
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
18
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
19
+ className
20
+ ),
21
+ ...props,
22
+ ref
23
+ }
24
+ ));
25
+ SheetOverlay.displayName = DialogPrimitive.Overlay.displayName;
26
+ const sheetVariants = cva(
27
+ cn(
28
+ "fixed z-modal flex flex-col gap-4 bg-background shadow-lg transition ease-in-out",
29
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
30
+ "data-[state=closed]:duration-200 data-[state=open]:duration-300"
31
+ ),
32
+ {
33
+ variants: {
34
+ side: {
35
+ top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
36
+ bottom: "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
37
+ left: "top-[var(--topbar-height,0px)] bottom-0 left-0 w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-md",
38
+ right: "top-[var(--topbar-height,0px)] bottom-0 right-0 w-full sm:max-w-md border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right"
39
+ }
40
+ },
41
+ defaultVariants: {
42
+ side: "right"
43
+ }
44
+ }
45
+ );
46
+ const SheetContent = React.forwardRef(({ side = "right", className, children, hideClose = false, closeLabel = "Close", ...props }, ref) => /* @__PURE__ */ jsxs(SheetPortal, { children: [
47
+ /* @__PURE__ */ jsx(SheetOverlay, {}),
48
+ /* @__PURE__ */ jsxs(DialogPrimitive.Content, { ref, className: cn(sheetVariants({ side }), className), ...props, children: [
49
+ children,
50
+ !hideClose ? /* @__PURE__ */ jsx(
51
+ DialogPrimitive.Close,
52
+ {
53
+ className: "absolute right-4 top-4 inline-flex size-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus-visible:shadow-focus",
54
+ "aria-label": closeLabel,
55
+ children: /* @__PURE__ */ jsx(X, { className: "size-4", "aria-hidden": "true" })
56
+ }
57
+ ) : null
58
+ ] })
59
+ ] }));
60
+ SheetContent.displayName = DialogPrimitive.Content.displayName;
61
+ const SheetHeader = React.forwardRef(
62
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx(
63
+ "div",
64
+ {
65
+ ref,
66
+ className: cn("flex flex-col gap-1.5 border-b px-4 py-3 pr-12 text-left", className),
67
+ ...props
68
+ }
69
+ )
70
+ );
71
+ SheetHeader.displayName = "SheetHeader";
72
+ const SheetFooter = React.forwardRef(
73
+ ({ className, ...props }, ref) => /* @__PURE__ */ jsx(
74
+ "div",
75
+ {
76
+ ref,
77
+ className: cn("flex flex-col-reverse gap-2 border-t px-4 py-3 sm:flex-row sm:justify-end", className),
78
+ ...props
79
+ }
80
+ )
81
+ );
82
+ SheetFooter.displayName = "SheetFooter";
83
+ const SheetTitle = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
84
+ DialogPrimitive.Title,
85
+ {
86
+ ref,
87
+ className: cn("text-base font-semibold text-foreground", className),
88
+ ...props
89
+ }
90
+ ));
91
+ SheetTitle.displayName = DialogPrimitive.Title.displayName;
92
+ const SheetDescription = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
93
+ DialogPrimitive.Description,
94
+ {
95
+ ref,
96
+ className: cn("text-sm text-muted-foreground", className),
97
+ ...props
98
+ }
99
+ ));
100
+ SheetDescription.displayName = DialogPrimitive.Description.displayName;
101
+ export {
102
+ Sheet,
103
+ SheetClose,
104
+ SheetContent,
105
+ SheetDescription,
106
+ SheetFooter,
107
+ SheetHeader,
108
+ SheetOverlay,
109
+ SheetPortal,
110
+ SheetTitle,
111
+ SheetTrigger,
112
+ sheetVariants
113
+ };
114
+ //# sourceMappingURL=sheet.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/primitives/sheet.tsx"],
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport * as DialogPrimitive from '@radix-ui/react-dialog'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { X } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\n\nconst Sheet = DialogPrimitive.Root\n\nconst SheetTrigger = DialogPrimitive.Trigger\n\nconst SheetClose = DialogPrimitive.Close\n\nconst SheetPortal = DialogPrimitive.Portal\n\nconst SheetOverlay = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Overlay>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Overlay\n className={cn(\n 'fixed inset-x-0 bottom-0 top-[var(--topbar-height,0px)] z-overlay bg-black/40 backdrop-blur-sm',\n 'data-[state=open]:animate-in data-[state=closed]:animate-out',\n 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',\n className,\n )}\n {...props}\n ref={ref}\n />\n))\nSheetOverlay.displayName = DialogPrimitive.Overlay.displayName\n\nconst sheetVariants = cva(\n cn(\n 'fixed z-modal flex flex-col gap-4 bg-background shadow-lg transition ease-in-out',\n 'data-[state=open]:animate-in data-[state=closed]:animate-out',\n 'data-[state=closed]:duration-200 data-[state=open]:duration-300',\n ),\n {\n variants: {\n side: {\n top:\n 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',\n bottom:\n 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',\n left:\n 'top-[var(--topbar-height,0px)] bottom-0 left-0 w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-md',\n right:\n 'top-[var(--topbar-height,0px)] bottom-0 right-0 w-full sm:max-w-md border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right',\n },\n },\n defaultVariants: {\n side: 'right',\n },\n }\n)\n\nexport type SheetContentProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> &\n VariantProps<typeof sheetVariants> & {\n /** Hide the built-in close (X) button \u2014 useful when the consumer renders its own close affordance. */\n hideClose?: boolean\n /** Override the aria-label for the built-in close button. */\n closeLabel?: string\n }\n\n/**\n * SheetContent \u2014 side-anchored Radix Dialog wrapper.\n *\n * Topbar integration: the `left` and `right` variants anchor at\n * `top: var(--topbar-height, 0px)` so a sticky app topbar stays visible above\n * the panel. Consumers that render inside `AppShell` get the correct offset\n * because the shell sets `--topbar-height` on the outer container. Surfaces\n * outside the shell (portal modals, marketing pages, standalone embeds) keep\n * the default `0px` fallback and align to the viewport top \u2014 that is the\n * correct behavior when there is no topbar to clear.\n *\n * To opt into the offset from your own layout, set the CSS variable on any\n * ancestor of `<SheetContent>`:\n *\n * ```css\n * .my-shell { --topbar-height: 60px; }\n * ```\n */\n\nconst SheetContent = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Content>,\n SheetContentProps\n>(({ side = 'right', className, children, hideClose = false, closeLabel = 'Close', ...props }, ref) => (\n <SheetPortal>\n <SheetOverlay />\n <DialogPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>\n {children}\n {!hideClose ? (\n <DialogPrimitive.Close\n className=\"absolute right-4 top-4 inline-flex size-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus-visible:shadow-focus\"\n aria-label={closeLabel}\n >\n <X className=\"size-4\" aria-hidden=\"true\" />\n </DialogPrimitive.Close>\n ) : null}\n </DialogPrimitive.Content>\n </SheetPortal>\n))\nSheetContent.displayName = DialogPrimitive.Content.displayName\n\nconst SheetHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn('flex flex-col gap-1.5 border-b px-4 py-3 pr-12 text-left', className)}\n {...props}\n />\n ),\n)\nSheetHeader.displayName = 'SheetHeader'\n\nconst SheetFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n className={cn('flex flex-col-reverse gap-2 border-t px-4 py-3 sm:flex-row sm:justify-end', className)}\n {...props}\n />\n ),\n)\nSheetFooter.displayName = 'SheetFooter'\n\nconst SheetTitle = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Title>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Title\n ref={ref}\n className={cn('text-base font-semibold text-foreground', className)}\n {...props}\n />\n))\nSheetTitle.displayName = DialogPrimitive.Title.displayName\n\nconst SheetDescription = React.forwardRef<\n React.ElementRef<typeof DialogPrimitive.Description>,\n React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n <DialogPrimitive.Description\n ref={ref}\n className={cn('text-sm text-muted-foreground', className)}\n {...props}\n />\n))\nSheetDescription.displayName = DialogPrimitive.Description.displayName\n\nexport {\n Sheet,\n SheetClose,\n SheetContent,\n SheetDescription,\n SheetFooter,\n SheetHeader,\n SheetOverlay,\n SheetPortal,\n SheetTitle,\n SheetTrigger,\n sheetVariants,\n}\n"],
5
+ "mappings": ";AAoBE,cAuEE,YAvEF;AAlBF,YAAY,WAAW;AACvB,YAAY,qBAAqB;AACjC,SAAS,WAA8B;AACvC,SAAS,SAAS;AAClB,SAAS,UAAU;AAEnB,MAAM,QAAQ,gBAAgB;AAE9B,MAAM,eAAe,gBAAgB;AAErC,MAAM,aAAa,gBAAgB;AAEnC,MAAM,cAAc,gBAAgB;AAEpC,MAAM,eAAe,MAAM,WAGzB,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,QAC1B;AAAA,EAAC,gBAAgB;AAAA,EAAhB;AAAA,IACC,WAAW;AAAA,MACT;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACC,GAAG;AAAA,IACJ;AAAA;AACF,CACD;AACD,aAAa,cAAc,gBAAgB,QAAQ;AAEnD,MAAM,gBAAgB;AAAA,EACpB;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE,UAAU;AAAA,MACR,MAAM;AAAA,QACJ,KACE;AAAA,QACF,QACE;AAAA,QACF,MACE;AAAA,QACF,OACE;AAAA,MACJ;AAAA,IACF;AAAA,IACA,iBAAiB;AAAA,MACf,MAAM;AAAA,IACR;AAAA,EACF;AACF;AA6BA,MAAM,eAAe,MAAM,WAGzB,CAAC,EAAE,OAAO,SAAS,WAAW,UAAU,YAAY,OAAO,aAAa,SAAS,GAAG,MAAM,GAAG,QAC7F,qBAAC,eACC;AAAA,sBAAC,gBAAa;AAAA,EACd,qBAAC,gBAAgB,SAAhB,EAAwB,KAAU,WAAW,GAAG,cAAc,EAAE,KAAK,CAAC,GAAG,SAAS,GAAI,GAAG,OACvF;AAAA;AAAA,IACA,CAAC,YACA;AAAA,MAAC,gBAAgB;AAAA,MAAhB;AAAA,QACC,WAAU;AAAA,QACV,cAAY;AAAA,QAEZ,8BAAC,KAAE,WAAU,UAAS,eAAY,QAAO;AAAA;AAAA,IAC3C,IACE;AAAA,KACN;AAAA,GACF,CACD;AACD,aAAa,cAAc,gBAAgB,QAAQ;AAEnD,MAAM,cAAc,MAAM;AAAA,EACxB,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,QACxB;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,WAAW,GAAG,4DAA4D,SAAS;AAAA,MAClF,GAAG;AAAA;AAAA,EACN;AAEJ;AACA,YAAY,cAAc;AAE1B,MAAM,cAAc,MAAM;AAAA,EACxB,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,QACxB;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,WAAW,GAAG,6EAA6E,SAAS;AAAA,MACnG,GAAG;AAAA;AAAA,EACN;AAEJ;AACA,YAAY,cAAc;AAE1B,MAAM,aAAa,MAAM,WAGvB,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,QAC1B;AAAA,EAAC,gBAAgB;AAAA,EAAhB;AAAA,IACC;AAAA,IACA,WAAW,GAAG,2CAA2C,SAAS;AAAA,IACjE,GAAG;AAAA;AACN,CACD;AACD,WAAW,cAAc,gBAAgB,MAAM;AAE/C,MAAM,mBAAmB,MAAM,WAG7B,CAAC,EAAE,WAAW,GAAG,MAAM,GAAG,QAC1B;AAAA,EAAC,gBAAgB;AAAA,EAAhB;AAAA,IACC;AAAA,IACA,WAAW,GAAG,iCAAiC,SAAS;AAAA,IACvD,GAAG;AAAA;AACN,CACD;AACD,iBAAiB,cAAc,gBAAgB,YAAY;",
6
+ "names": []
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ui",
3
- "version": "0.6.2-develop.3372.1.de9a169973",
3
+ "version": "0.6.2-develop.3380.1.bb14509041",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -141,13 +141,13 @@
141
141
  "remark-gfm": "^4.0.1"
142
142
  },
143
143
  "peerDependencies": {
144
- "@open-mercato/shared": "0.6.2-develop.3372.1.de9a169973",
144
+ "@open-mercato/shared": "0.6.2-develop.3380.1.bb14509041",
145
145
  "react": ">=18.0.0",
146
146
  "react-dom": ">=18.0.0",
147
147
  "react-is": ">=18.0.0"
148
148
  },
149
149
  "devDependencies": {
150
- "@open-mercato/shared": "0.6.2-develop.3372.1.de9a169973",
150
+ "@open-mercato/shared": "0.6.2-develop.3380.1.bb14509041",
151
151
  "@testing-library/dom": "^10.4.1",
152
152
  "@testing-library/jest-dom": "^6.9.1",
153
153
  "@testing-library/react": "^16.3.1",
package/src/ai/AiIcon.tsx CHANGED
@@ -6,7 +6,7 @@ export function AiIcon({ className, ...props }: LucideProps) {
6
6
  <Sparkles
7
7
  aria-hidden
8
8
  {...props}
9
- className={cn(className, 'text-brand-violet')}
9
+ className={cn(className, 'text-accent-indigo')}
10
10
  />
11
11
  )
12
12
  }
@@ -3,8 +3,17 @@ import * as React from 'react'
3
3
  import { createContext, useContext } from 'react'
4
4
  import Link from 'next/link'
5
5
  import Image from 'next/image'
6
- import { ChevronDown, ChevronLeft, Search, X } from 'lucide-react'
6
+ import { ChevronDown, ChevronLeft, Home, PanelLeftClose, PanelLeftOpen, Search, X } from 'lucide-react'
7
7
  import { Button } from '../primitives/button'
8
+ import {
9
+ Breadcrumb as BreadcrumbNav,
10
+ BreadcrumbEllipsis,
11
+ BreadcrumbItem,
12
+ BreadcrumbLink,
13
+ BreadcrumbList,
14
+ BreadcrumbPage,
15
+ BreadcrumbSeparator,
16
+ } from '../primitives/breadcrumb'
8
17
  import { IconButton } from '../primitives/icon-button'
9
18
  import { Input } from '../primitives/input'
10
19
  import { SearchInput } from '../primitives/search-input'
@@ -428,6 +437,13 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
428
437
  useEventBridge() // SSE DOM Event Bridge — singleton SSE connection for real-time server events
429
438
  const resolvedProductName = productName ?? t('appShell.productName')
430
439
  const [mobileOpen, setMobileOpen] = React.useState(false)
440
+ // When the mobile drawer opens on a settings/profile route, it follows the
441
+ // section sidebar by default. Set to 'main' to force-show the main nav even
442
+ // when the route is in a section context. Reset on close.
443
+ const [mobileDrawerView, setMobileDrawerView] = React.useState<'auto' | 'main'>('auto')
444
+ React.useEffect(() => {
445
+ if (!mobileOpen) setMobileDrawerView('auto')
446
+ }, [mobileOpen])
431
447
  // Initialize from server-provided prop only to avoid hydration flicker
432
448
  const [collapsed, setCollapsed] = React.useState(sidebarCollapsedDefault)
433
449
  // Maintain internal nav state so we can augment it client-side
@@ -670,7 +686,7 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
670
686
  aria-label={t('appShell.goToDashboard')}
671
687
  >
672
688
  <Image src={logo?.src ?? "/open-mercato.svg"} alt={logo?.alt ?? resolvedProductName} width={40} height={40} className="rounded-full shrink-0" />
673
- {!compact && <span className="text-sm font-medium text-foreground">{resolvedProductName}</span>}
689
+ {!compact && <span className="truncate text-sm font-medium text-foreground">{resolvedProductName}</span>}
674
690
  </Link>
675
691
  </div>
676
692
  )}
@@ -803,7 +819,7 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
803
819
  aria-label={t('appShell.goToDashboard')}
804
820
  >
805
821
  <Image src={logo?.src ?? "/open-mercato.svg"} alt={logo?.alt ?? resolvedProductName} width={40} height={40} className="rounded-full shrink-0" />
806
- {!compact && <span className="text-sm font-medium text-foreground">{resolvedProductName}</span>}
822
+ {!compact && <span className="truncate text-sm font-medium text-foreground">{resolvedProductName}</span>}
807
823
  </Link>
808
824
  </div>
809
825
  ) : null}
@@ -869,7 +885,7 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
869
885
  aria-label={t('appShell.goToDashboard')}
870
886
  >
871
887
  <Image src={logo?.src ?? "/open-mercato.svg"} alt={logo?.alt ?? resolvedProductName} width={40} height={40} className="rounded-full shrink-0" />
872
- {!compact && <span className="text-sm font-medium text-foreground">{resolvedProductName}</span>}
888
+ {!compact && <span className="truncate text-sm font-medium text-foreground">{resolvedProductName}</span>}
873
889
  </Link>
874
890
  </div>
875
891
  )}
@@ -1130,7 +1146,6 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
1130
1146
  type="button"
1131
1147
  variant="outline"
1132
1148
  size="sm"
1133
- className="h-7 text-xs"
1134
1149
  data-menu-item-id={item.id}
1135
1150
  onClick={() => item.onClick?.()}
1136
1151
  >
@@ -1143,7 +1158,22 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
1143
1158
 
1144
1159
  return (
1145
1160
  <HeaderContext.Provider value={headerCtxValue}>
1146
- <div className={`min-h-svh lg:grid transition-[grid-template-columns] duration-200 ease-out ${gridColsClass}`}>
1161
+ <div
1162
+ className={`relative min-h-svh lg:grid transition-[grid-template-columns] duration-200 ease-out ${gridColsClass}`}
1163
+ style={{ '--topbar-height': '61px' } as React.CSSProperties}
1164
+ >
1165
+ {/* Desktop sidebar collapse/expand toggle — sits on the divider line between
1166
+ sidebar and content, like Notion/Vercel. Hidden on mobile (hamburger in
1167
+ topbar handles the drawer). */}
1168
+ <button
1169
+ type="button"
1170
+ onClick={() => setCollapsed((c) => !c)}
1171
+ aria-label={t('appShell.toggleSidebar')}
1172
+ className="hidden lg:flex fixed top-4 z-dropdown size-7 items-center justify-center rounded-md border bg-background text-muted-foreground shadow-sm transition-all hover:text-foreground hover:bg-muted focus:outline-none focus-visible:shadow-focus"
1173
+ style={{ left: `calc(${asideWidth} - 14px)` }}
1174
+ >
1175
+ {effectiveCollapsed ? <PanelLeftOpen className="size-4" /> : <PanelLeftClose className="size-4" />}
1176
+ </button>
1147
1177
  {/* Desktop main sidebar */}
1148
1178
  <aside ref={sidebarAsideRef} className={`${asideClassesBase} ${effectiveCollapsed ? 'px-2' : 'px-3'} hidden lg:block lg:sticky lg:top-0 lg:h-svh lg:self-start lg:overflow-hidden lg:relative transition-[width,padding] duration-200 ease-out`} style={{ width: asideWidth }}>
1149
1179
  {renderSidebar(effectiveCollapsed, false, isSectionView)}
@@ -1206,7 +1236,7 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
1206
1236
  ) : null}
1207
1237
 
1208
1238
  <div className="flex min-h-svh flex-col min-w-0">
1209
- <header className="border-b bg-background/80 px-3 lg:px-4 py-2 lg:py-3 flex items-center justify-between gap-2">
1239
+ <header className="sticky top-0 z-sticky border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 px-3 sm:px-4 lg:px-6 py-3 flex items-center justify-between gap-2 sm:gap-3">
1210
1240
  <div
1211
1241
  data-testid="backend-chrome-ready"
1212
1242
  data-ready={isChromeReady ? 'true' : 'false'}
@@ -1214,22 +1244,9 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
1214
1244
  />
1215
1245
  <div className="flex items-center gap-2 min-w-0">
1216
1246
  {/* Mobile menu button */}
1217
- <IconButton variant="outline" size="sm" className="lg:hidden" aria-label={t('appShell.openMenu')} onClick={() => setMobileOpen(true)}>
1247
+ <IconButton variant="ghost" size="sm" className="lg:hidden" aria-label={t('appShell.openMenu')} onClick={() => setMobileOpen(true)}>
1218
1248
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
1219
1249
  </IconButton>
1220
- {/* Desktop collapse toggle */}
1221
- <IconButton
1222
- variant="outline"
1223
- size="sm"
1224
- className="hidden lg:inline-flex"
1225
- aria-label={t('appShell.toggleSidebar')}
1226
- onClick={() => setCollapsed((c) => !c)}
1227
- >
1228
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
1229
- <rect x="3" y="4" width="18" height="16" rx="2"/>
1230
- <path d="M9 4v16"/>
1231
- </svg>
1232
- </IconButton>
1233
1250
  {/* Header breadcrumb: always starts with Dashboard */}
1234
1251
  {(() => {
1235
1252
  const dashboardLabel = t('dashboard.title')
@@ -1243,30 +1260,65 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
1243
1260
  rest = [{ label: headerTitle }]
1244
1261
  }
1245
1262
  const items = [...root, ...rest]
1246
- const lastIndex = items.length - 1
1263
+ if (items.length === 0) return null
1264
+ const home = items[0]
1265
+ const current = items.length > 1 ? items[items.length - 1] : null
1266
+ const mid = items.slice(1, -1)
1267
+ const hasMid = mid.length > 0
1247
1268
  return (
1248
- <nav className="flex items-center gap-2 text-sm min-w-0">
1249
- {items.map((b, i) => {
1250
- const isLast = i === lastIndex
1251
- const hiddenOnMobile = !isLast ? 'hidden md:inline' : ''
1252
- return (
1253
- <React.Fragment key={i}>
1254
- {i > 0 && <span className={`text-muted-foreground hidden md:inline`}>/</span>}
1255
- {b.href ? (
1256
- <Link href={b.href} className={`text-muted-foreground hover:text-foreground ${hiddenOnMobile}`}>
1257
- {b.label}
1269
+ <BreadcrumbNav divider="arrow" className="ml-2 lg:ml-3 text-sm">
1270
+ <BreadcrumbList className="[&_[data-slot=breadcrumb-separator]_svg]:size-4">
1271
+ <BreadcrumbItem>
1272
+ {home.href && current ? (
1273
+ <BreadcrumbLink asChild aria-label={home.label}>
1274
+ <Link href={home.href}>
1275
+ <Home className="size-4" aria-hidden="true" />
1258
1276
  </Link>
1259
- ) : (
1260
- <span className={`font-medium truncate max-w-[45vw] md:max-w-[60vw]`}>{b.label}</span>
1261
- )}
1262
- </React.Fragment>
1263
- )
1264
- })}
1265
- </nav>
1277
+ </BreadcrumbLink>
1278
+ ) : (
1279
+ <BreadcrumbPage aria-label={home.label}>
1280
+ <Home className="size-4" aria-hidden="true" />
1281
+ </BreadcrumbPage>
1282
+ )}
1283
+ </BreadcrumbItem>
1284
+ {current ? (
1285
+ <>
1286
+ {hasMid ? (
1287
+ <>
1288
+ <BreadcrumbSeparator className="md:hidden" />
1289
+ <BreadcrumbItem className="md:hidden">
1290
+ <BreadcrumbEllipsis aria-label={t('appShell.breadcrumb.collapsed', { count: mid.length })} />
1291
+ </BreadcrumbItem>
1292
+ {mid.map((b, i) => (
1293
+ <React.Fragment key={`mid-${i}`}>
1294
+ <BreadcrumbSeparator className="hidden md:inline-flex" />
1295
+ <BreadcrumbItem className="hidden md:inline-flex">
1296
+ {b.href ? (
1297
+ <BreadcrumbLink asChild title={b.label}>
1298
+ <Link href={b.href}>{b.label}</Link>
1299
+ </BreadcrumbLink>
1300
+ ) : (
1301
+ <BreadcrumbLink title={b.label} aria-disabled="true" tabIndex={-1}>
1302
+ {b.label}
1303
+ </BreadcrumbLink>
1304
+ )}
1305
+ </BreadcrumbItem>
1306
+ </React.Fragment>
1307
+ ))}
1308
+ </>
1309
+ ) : null}
1310
+ <BreadcrumbSeparator />
1311
+ <BreadcrumbItem>
1312
+ <BreadcrumbPage title={current.label}>{current.label}</BreadcrumbPage>
1313
+ </BreadcrumbItem>
1314
+ </>
1315
+ ) : null}
1316
+ </BreadcrumbList>
1317
+ </BreadcrumbNav>
1266
1318
  )
1267
1319
  })()}
1268
1320
  </div>
1269
- <div className="flex items-center gap-1 md:gap-2 text-sm shrink-0">
1321
+ <div className="flex items-center gap-1.5 sm:gap-2 md:gap-3 text-sm shrink-0">
1270
1322
  <StatusBadgeInjectionSpot
1271
1323
  spotId={GLOBAL_HEADER_STATUS_INDICATORS_INJECTION_SPOT_ID}
1272
1324
  context={injectionContext}
@@ -1320,23 +1372,73 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
1320
1372
  {/* Mobile drawer */}
1321
1373
  {mobileOpen && (
1322
1374
  <div className="lg:hidden fixed inset-0 z-modal">
1323
- <div className="absolute inset-0 bg-black/20" onClick={() => setMobileOpen(false)} aria-hidden="true" />
1324
- <aside className="absolute left-0 top-0 flex h-full w-[260px] flex-col bg-background border-r overflow-hidden">
1325
- <div className="shrink-0 p-3 pb-2 flex items-center justify-between border-b">
1326
- <Link href="/backend" className="flex items-center gap-2 text-sm font-semibold" onClick={() => setMobileOpen(false)} aria-label={t('appShell.goToDashboard')}>
1327
- <Image src={logo?.src ?? "/open-mercato.svg"} alt={logo?.alt ?? resolvedProductName} width={28} height={28} className="rounded" />
1328
- {resolvedProductName}
1375
+ <div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={() => setMobileOpen(false)} aria-hidden="true" />
1376
+ <aside className="absolute left-0 top-0 flex h-full w-[280px] max-w-[85vw] flex-col bg-background border-r shadow-lg overflow-hidden">
1377
+ <div className="shrink-0 flex items-center justify-between gap-2 border-b px-4 py-3">
1378
+ <Link href="/backend" className="flex items-center gap-2 min-w-0 text-sm font-semibold" onClick={() => setMobileOpen(false)} aria-label={t('appShell.goToDashboard')}>
1379
+ <Image src={logo?.src ?? "/open-mercato.svg"} alt={logo?.alt ?? resolvedProductName} width={28} height={28} className="rounded shrink-0" />
1380
+ <span className="truncate">{resolvedProductName}</span>
1329
1381
  </Link>
1330
- <IconButton variant="outline" size="sm" onClick={() => setMobileOpen(false)} aria-label={t('appShell.closeMenu')}>✕</IconButton>
1382
+ <IconButton variant="ghost" size="sm" onClick={() => setMobileOpen(false)} aria-label={t('appShell.closeMenu')}>
1383
+ <X className="size-4" />
1384
+ </IconButton>
1331
1385
  </div>
1332
1386
  {mobileSidebarSlot && (
1333
1387
  <div className="shrink-0 border-b px-3 py-2">
1334
1388
  {mobileSidebarSlot}
1335
1389
  </div>
1336
1390
  )}
1337
- <div className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden p-3">
1391
+ {sidebarMode !== 'main' ? (
1392
+ <div className="shrink-0 flex items-center gap-5 border-b px-4 pt-3 pb-0" role="tablist">
1393
+ {([
1394
+ { id: 'main' as const, label: t('backend.nav.main', 'Main') },
1395
+ {
1396
+ id: 'section' as const,
1397
+ label:
1398
+ sidebarMode === 'settings'
1399
+ ? settingsSectionTitle ?? t('backend.nav.settings', 'Settings')
1400
+ : profileSectionTitle ?? t('backend.nav.profile', 'Profile'),
1401
+ },
1402
+ ]).map((tab) => {
1403
+ const isActive =
1404
+ tab.id === 'main' ? mobileDrawerView === 'main' : mobileDrawerView === 'auto'
1405
+ const tabId = `mobile-drawer-tab-${tab.id}`
1406
+ return (
1407
+ <button
1408
+ key={tab.id}
1409
+ id={tabId}
1410
+ type="button"
1411
+ role="tab"
1412
+ aria-selected={isActive}
1413
+ aria-controls="mobile-drawer-tabpanel"
1414
+ onClick={() => setMobileDrawerView(tab.id === 'main' ? 'main' : 'auto')}
1415
+ className="relative inline-flex items-center pb-2 text-sm font-medium leading-5 tracking-tight transition-colors focus:outline-none data-[active=true]:text-foreground data-[active=false]:text-muted-foreground hover:text-foreground"
1416
+ data-active={isActive}
1417
+ >
1418
+ <span>{tab.label}</span>
1419
+ {isActive ? (
1420
+ <span
1421
+ className="absolute -bottom-px left-0 right-0 h-0.5 bg-foreground"
1422
+ aria-hidden="true"
1423
+ />
1424
+ ) : null}
1425
+ </button>
1426
+ )
1427
+ })}
1428
+ </div>
1429
+ ) : null}
1430
+ <div
1431
+ id="mobile-drawer-tabpanel"
1432
+ role={sidebarMode !== 'main' ? 'tabpanel' : undefined}
1433
+ aria-labelledby={
1434
+ sidebarMode !== 'main'
1435
+ ? `mobile-drawer-tab-${mobileDrawerView === 'main' ? 'main' : 'section'}`
1436
+ : undefined
1437
+ }
1438
+ className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden p-3"
1439
+ >
1338
1440
  {/* Force expanded sidebar in mobile drawer, hide its header and collapse toggle */}
1339
- {renderSidebar(false, true)}
1441
+ {renderSidebar(false, true, mobileDrawerView === 'main')}
1340
1442
  </div>
1341
1443
  </aside>
1342
1444
  </div>