@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.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +1 -0
- package/dist/ai/AiIcon.js +1 -1
- package/dist/ai/AiIcon.js.map +2 -2
- package/dist/backend/AppShell.js +223 -153
- package/dist/backend/AppShell.js.map +2 -2
- package/dist/backend/ProfileDropdown.js +101 -79
- package/dist/backend/ProfileDropdown.js.map +2 -2
- package/dist/backend/notifications/NotificationCountBadge.js +1 -1
- package/dist/backend/notifications/NotificationCountBadge.js.map +2 -2
- package/dist/backend/notifications/NotificationItem.js +67 -50
- package/dist/backend/notifications/NotificationItem.js.map +2 -2
- package/dist/backend/notifications/NotificationPanel.js +108 -85
- package/dist/backend/notifications/NotificationPanel.js.map +2 -2
- package/dist/primitives/breadcrumb.js +161 -0
- package/dist/primitives/breadcrumb.js.map +7 -0
- package/dist/primitives/sheet.js +114 -0
- package/dist/primitives/sheet.js.map +7 -0
- package/package.json +3 -3
- package/src/ai/AiIcon.tsx +1 -1
- package/src/backend/AppShell.tsx +151 -49
- package/src/backend/ProfileDropdown.tsx +115 -88
- package/src/backend/__tests__/AppShell.test.tsx +8 -0
- package/src/backend/notifications/NotificationCountBadge.tsx +1 -1
- package/src/backend/notifications/NotificationItem.tsx +97 -104
- package/src/backend/notifications/NotificationPanel.tsx +126 -102
- package/src/primitives/__tests__/breadcrumb.test.tsx +298 -0
- package/src/primitives/__tests__/sheet.test.tsx +152 -0
- package/src/primitives/breadcrumb.tsx +185 -0
- 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.
|
|
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.
|
|
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.
|
|
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
package/src/backend/AppShell.tsx
CHANGED
|
@@ -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
|
|
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
|
|
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="
|
|
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
|
-
|
|
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
|
-
<
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
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
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
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
|
|
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/
|
|
1324
|
-
<aside className="absolute left-0 top-0 flex h-full w-[
|
|
1325
|
-
<div className="shrink-0
|
|
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="
|
|
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
|
-
|
|
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>
|