@open-mercato/ui 0.4.11-develop.2631.481e9df5b0 → 0.5.0
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 +2 -2
- package/AGENTS.md +28 -4
- package/agentic/standalone-guide.md +97 -0
- package/build.mjs +10 -6
- package/dist/backend/AppShell.js +15 -2
- package/dist/backend/AppShell.js.map +2 -2
- package/dist/backend/DataTable.js +22 -1
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/detail/CustomDataSection.js +1 -5
- package/dist/backend/detail/CustomDataSection.js.map +2 -2
- package/dist/backend/detail/InlineEditors.js +2 -5
- package/dist/backend/detail/InlineEditors.js.map +2 -2
- package/dist/backend/detail/NotesSection.js +2 -6
- package/dist/backend/detail/NotesSection.js.map +2 -2
- package/dist/backend/icons/lucideRegistry.generated.js +93 -3
- package/dist/backend/icons/lucideRegistry.generated.js.map +2 -2
- package/dist/backend/markdown/MarkdownContent.js +47 -4
- package/dist/backend/markdown/MarkdownContent.js.map +2 -2
- package/dist/portal/PortalShell.js +41 -11
- package/dist/portal/PortalShell.js.map +2 -2
- package/dist/portal/hooks/usePortalDashboardWidgets.js +40 -1
- package/dist/portal/hooks/usePortalDashboardWidgets.js.map +2 -2
- package/dist/portal/utils/nav.js +84 -0
- package/dist/portal/utils/nav.js.map +7 -0
- package/package.json +13 -9
- package/src/backend/AppShell.tsx +22 -2
- package/src/backend/DataTable.tsx +28 -5
- package/src/backend/__tests__/AppShell.test.tsx +67 -0
- package/src/backend/__tests__/FormHeader.test.tsx +0 -1
- package/src/backend/detail/CustomDataSection.tsx +1 -10
- package/src/backend/detail/InlineEditors.tsx +3 -15
- package/src/backend/detail/NotesSection.tsx +5 -14
- package/src/backend/icons/lucideRegistry.generated.tsx +93 -3
- package/src/backend/injection/__tests__/resolveInjectedIcon.test.tsx +7 -0
- package/src/backend/markdown/MarkdownContent.tsx +76 -6
- package/src/backend/section-page/types.ts +1 -0
- package/src/portal/PortalShell.tsx +43 -11
- package/src/portal/hooks/__tests__/usePortalDashboardWidgets.test.tsx +117 -0
- package/src/portal/hooks/usePortalDashboardWidgets.ts +55 -1
- package/src/portal/utils/__tests__/nav.test.ts +199 -0
- package/src/portal/utils/nav.ts +150 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/icons/lucideRegistry.generated.tsx"],
|
|
4
|
-
"sourcesContent": ["/* eslint-disable */\n// AUTO-GENERATED by @open-mercato/ui build. Do not edit by hand.\n// Generated from discovered icon string usages across the repo.\n\nimport type * as React from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport {\n AlertCircle,\n AlertTriangle,\n BadgeCheck,\n BarChart2,\n Bell,\n Bolt,\n Box,\n Briefcase,\n BriefcaseBusiness,\n Building,\n Building2,\n Calendar,\n CalendarCheck,\n CalendarClock,\n CalendarOff,\n CalendarX,\n Check,\n CheckCircle,\n CheckSquare,\n ClipboardList,\n Clock,\n Clock3,\n Coins,\n Copy,\n CreditCard,\n Database,\n DollarSign,\n Download,\n ExternalLink,\n FileMinus,\n FileText,\n FilterX,\n FolderTree,\n GitBranch,\n GitCompareArrows,\n GitPullRequestArrow,\n Handshake,\n Heart,\n Inbox,\n Key,\n KeyRound,\n Layers,\n LineChart,\n List,\n Lock,\n Mail,\n MailOpen,\n MapPin,\n Package,\n PackagePlus,\n PackageX,\n Percent,\n PieChart,\n PlusSquare,\n Receipt,\n ReceiptText,\n Reply,\n Ruler,\n Settings,\n Shapes,\n Shield,\n ShieldAlert,\n ShieldCheck,\n ShoppingCart,\n Sliders,\n Smartphone,\n StickyNote,\n Store,\n Tag,\n Ticket,\n Trash2,\n TrendingUp,\n Trophy,\n Truck,\n Unlock,\n User,\n UserMinus,\n UserPlus,\n UserRound,\n Users,\n Webhook,\n X,\n XCircle,\n} from 'lucide-react'\n\nexport const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {\n 'alert-circle': AlertCircle,\n 'alert-triangle': AlertTriangle,\n 'badge-check': BadgeCheck,\n 'bar-chart-2': BarChart2,\n 'bell': Bell,\n 'bolt': Bolt,\n 'box': Box,\n 'briefcase': Briefcase,\n 'briefcase-business': BriefcaseBusiness,\n 'building': Building,\n 'building2': Building2,\n 'calendar': Calendar,\n 'calendar-check': CalendarCheck,\n 'calendar-clock': CalendarClock,\n 'calendar-off': CalendarOff,\n 'calendar-x': CalendarX,\n 'check': Check,\n 'check-circle': CheckCircle,\n 'check-square': CheckSquare,\n 'clipboard-list': ClipboardList,\n 'clock': Clock,\n 'clock-3': Clock3,\n 'coins': Coins,\n 'copy': Copy,\n 'credit-card': CreditCard,\n 'database': Database,\n 'dollar-sign': DollarSign,\n 'download': Download,\n 'external-link': ExternalLink,\n 'file-minus': FileMinus,\n 'file-text': FileText,\n 'filter-x': FilterX,\n 'folder-tree': FolderTree,\n 'git-branch': GitBranch,\n 'git-compare-arrows': GitCompareArrows,\n 'git-pull-request-arrow': GitPullRequestArrow,\n 'handshake': Handshake,\n 'heart': Heart,\n 'inbox': Inbox,\n 'key': Key,\n 'key-round': KeyRound,\n 'layers': Layers,\n 'line-chart': LineChart,\n 'list': List,\n 'lock': Lock,\n 'mail': Mail,\n 'mail-open': MailOpen,\n 'map-pin': MapPin,\n 'package': Package,\n 'package-plus': PackagePlus,\n 'package-x': PackageX,\n 'percent': Percent,\n 'pie-chart': PieChart,\n 'plus-square': PlusSquare,\n 'receipt': Receipt,\n 'receipt-text': ReceiptText,\n 'reply': Reply,\n 'ruler': Ruler,\n 'settings': Settings,\n 'shapes': Shapes,\n 'shield': Shield,\n 'shield-alert': ShieldAlert,\n 'shield-check': ShieldCheck,\n 'shopping-cart': ShoppingCart,\n 'sliders': Sliders,\n 'smartphone': Smartphone,\n 'sticky-note': StickyNote,\n 'store': Store,\n 'tag': Tag,\n 'ticket': Ticket,\n 'trash-2': Trash2,\n 'trending-up': TrendingUp,\n 'trophy': Trophy,\n 'truck': Truck,\n 'unlock': Unlock,\n 'user': User,\n 'user-minus': UserMinus,\n 'user-plus': UserPlus,\n 'user-round': UserRound,\n 'users': Users,\n 'webhook': Webhook,\n 'x': X,\n 'x-circle': XCircle,\n}\n\nfunction normalizeKebabIconName(input: string): string {\n const trimmed = input.trim()\n if (!trimmed) return ''\n if (!
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["/* eslint-disable */\n// AUTO-GENERATED by @open-mercato/ui build. Do not edit by hand.\n// Generated from discovered icon string usages across the repo.\n\nimport type * as React from 'react'\nimport type { LucideIcon } from 'lucide-react'\nimport {\n Activity,\n AlertCircle,\n AlertOctagon,\n AlertTriangle,\n Archive,\n Award,\n BadgeCheck,\n Ban,\n Banknote,\n BarChart2,\n BarChart3,\n Bell,\n Bolt,\n Bookmark,\n Box,\n Briefcase,\n BriefcaseBusiness,\n Building,\n Building2,\n Calendar,\n CalendarCheck,\n CalendarClock,\n CalendarCog,\n CalendarMinus,\n CalendarOff,\n CalendarX,\n Check,\n CheckCircle,\n CheckCircle2,\n CheckSquare,\n Circle,\n ClipboardCheck,\n ClipboardList,\n Clock,\n Clock3,\n Coins,\n Copy,\n CreditCard,\n Database,\n DollarSign,\n Download,\n ExternalLink,\n FileMinus,\n FilePenLine,\n FileText,\n FilterX,\n Flag,\n FolderTree,\n Gauge,\n GitBranch,\n GitCompareArrows,\n GitPullRequestArrow,\n Globe,\n GraduationCap,\n Hand,\n Handshake,\n Heart,\n Hourglass,\n Inbox,\n Key,\n KeyRound,\n Layers,\n Lightbulb,\n LineChart,\n Link,\n List,\n Loader,\n Loader2,\n Lock,\n Mail,\n MailOpen,\n MapPin,\n Megaphone,\n Notebook,\n Package,\n PackageCheck,\n PackagePlus,\n PackageX,\n PauseCircle,\n Percent,\n Phone,\n PhoneCall,\n PieChart,\n PlusSquare,\n Receipt,\n ReceiptText,\n RefreshCcw,\n Reply,\n RotateCcw,\n Ruler,\n Send,\n Settings,\n Shapes,\n Shield,\n ShieldAlert,\n ShieldCheck,\n ShoppingBag,\n ShoppingCart,\n Shuffle,\n Sliders,\n Smartphone,\n Sparkles,\n Star,\n StickyNote,\n Store,\n Tag,\n Target,\n ThumbsUp,\n Ticket,\n Trash2,\n TrendingUp,\n TriangleAlert,\n Trophy,\n Truck,\n Undo2,\n Unlock,\n User,\n UserCheck,\n UserMinus,\n UserPlus,\n UserRound,\n Users,\n Wallet,\n Webhook,\n Wrench,\n X,\n XCircle,\n} from 'lucide-react'\n\nexport const LUCIDE_ICON_REGISTRY: Record<string, LucideIcon> = {\n 'activity': Activity,\n 'alert-circle': AlertCircle,\n 'alert-octagon': AlertOctagon,\n 'alert-triangle': AlertTriangle,\n 'archive': Archive,\n 'award': Award,\n 'badge-check': BadgeCheck,\n 'ban': Ban,\n 'banknote': Banknote,\n 'bar-chart-2': BarChart2,\n 'bar-chart-3': BarChart3,\n 'bell': Bell,\n 'bolt': Bolt,\n 'bookmark': Bookmark,\n 'box': Box,\n 'briefcase': Briefcase,\n 'briefcase-business': BriefcaseBusiness,\n 'building': Building,\n 'building2': Building2,\n 'calendar': Calendar,\n 'calendar-check': CalendarCheck,\n 'calendar-clock': CalendarClock,\n 'calendar-cog': CalendarCog,\n 'calendar-minus': CalendarMinus,\n 'calendar-off': CalendarOff,\n 'calendar-x': CalendarX,\n 'check': Check,\n 'check-circle': CheckCircle,\n 'check-circle-2': CheckCircle2,\n 'check-square': CheckSquare,\n 'circle': Circle,\n 'clipboard-check': ClipboardCheck,\n 'clipboard-list': ClipboardList,\n 'clock': Clock,\n 'clock-3': Clock3,\n 'coins': Coins,\n 'copy': Copy,\n 'credit-card': CreditCard,\n 'database': Database,\n 'dollar-sign': DollarSign,\n 'download': Download,\n 'external-link': ExternalLink,\n 'file-minus': FileMinus,\n 'file-pen-line': FilePenLine,\n 'file-text': FileText,\n 'filter-x': FilterX,\n 'flag': Flag,\n 'folder-tree': FolderTree,\n 'gauge': Gauge,\n 'git-branch': GitBranch,\n 'git-compare-arrows': GitCompareArrows,\n 'git-pull-request-arrow': GitPullRequestArrow,\n 'globe': Globe,\n 'graduation-cap': GraduationCap,\n 'hand': Hand,\n 'handshake': Handshake,\n 'heart': Heart,\n 'hourglass': Hourglass,\n 'inbox': Inbox,\n 'key': Key,\n 'key-round': KeyRound,\n 'layers': Layers,\n 'lightbulb': Lightbulb,\n 'line-chart': LineChart,\n 'link': Link,\n 'list': List,\n 'loader': Loader,\n 'loader-2': Loader2,\n 'lock': Lock,\n 'mail': Mail,\n 'mail-open': MailOpen,\n 'map-pin': MapPin,\n 'megaphone': Megaphone,\n 'notebook': Notebook,\n 'package': Package,\n 'package-check': PackageCheck,\n 'package-plus': PackagePlus,\n 'package-x': PackageX,\n 'pause-circle': PauseCircle,\n 'percent': Percent,\n 'phone': Phone,\n 'phone-call': PhoneCall,\n 'pie-chart': PieChart,\n 'plus-square': PlusSquare,\n 'receipt': Receipt,\n 'receipt-text': ReceiptText,\n 'refresh-ccw': RefreshCcw,\n 'reply': Reply,\n 'rotate-ccw': RotateCcw,\n 'ruler': Ruler,\n 'send': Send,\n 'settings': Settings,\n 'shapes': Shapes,\n 'shield': Shield,\n 'shield-alert': ShieldAlert,\n 'shield-check': ShieldCheck,\n 'shopping-bag': ShoppingBag,\n 'shopping-cart': ShoppingCart,\n 'shuffle': Shuffle,\n 'sliders': Sliders,\n 'smartphone': Smartphone,\n 'sparkles': Sparkles,\n 'star': Star,\n 'sticky-note': StickyNote,\n 'store': Store,\n 'tag': Tag,\n 'target': Target,\n 'thumbs-up': ThumbsUp,\n 'ticket': Ticket,\n 'trash-2': Trash2,\n 'trending-up': TrendingUp,\n 'triangle-alert': TriangleAlert,\n 'trophy': Trophy,\n 'truck': Truck,\n 'undo-2': Undo2,\n 'unlock': Unlock,\n 'user': User,\n 'user-check': UserCheck,\n 'user-minus': UserMinus,\n 'user-plus': UserPlus,\n 'user-round': UserRound,\n 'users': Users,\n 'wallet': Wallet,\n 'webhook': Webhook,\n 'wrench': Wrench,\n 'x': X,\n 'x-circle': XCircle,\n}\n\nfunction normalizeKebabIconName(input: string): string {\n const trimmed = input.trim()\n if (!trimmed) return ''\n const withoutPrefix = trimmed.startsWith('lucide:') ? trimmed.slice('lucide:'.length) : trimmed\n if (!withoutPrefix) return ''\n if (!withoutPrefix.includes('-') && !withoutPrefix.includes('_') && !withoutPrefix.includes(' ') && /[A-Z]/.test(withoutPrefix)) {\n return withoutPrefix\n .replace(/([a-z0-9])([A-Z])/g, '$1-$2')\n .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')\n .toLowerCase()\n }\n return withoutPrefix\n .replace(/[_\\s]+/g, '-')\n .replace(/-+/g, '-')\n .toLowerCase()\n}\n\nexport function resolveRegisteredLucideIcon(name: string | undefined): LucideIcon | null {\n if (!name) return null\n const normalized = normalizeKebabIconName(name)\n if (!normalized) return null\n return (LUCIDE_ICON_REGISTRY as Record<string, LucideIcon | undefined>)[normalized] ?? null\n}\n\nexport function resolveRegisteredLucideIconNode(\n name: string | undefined,\n className: string\n): React.ReactNode | null {\n const Icon = resolveRegisteredLucideIcon(name)\n if (!Icon) return null\n return <Icon className={className} />\n}\n"],
|
|
5
|
+
"mappings": "AAwSS;AAlST;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAC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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,9 +1,51 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { useMarkdownRemarkPlugins } from "./useMarkdownRemarkPlugins.js";
|
|
5
|
-
const
|
|
5
|
+
const isTestEnv = typeof process !== "undefined" && process.env.NODE_ENV === "test";
|
|
6
|
+
const TestMarkdownComponent = ({ children }) => /* @__PURE__ */ jsx(Fragment, { children });
|
|
7
|
+
let loadedReactMarkdownComponent = isTestEnv ? TestMarkdownComponent : null;
|
|
8
|
+
let reactMarkdownComponentPromise = null;
|
|
9
|
+
function loadReactMarkdownComponent() {
|
|
10
|
+
if (loadedReactMarkdownComponent) {
|
|
11
|
+
return Promise.resolve(loadedReactMarkdownComponent);
|
|
12
|
+
}
|
|
13
|
+
if (!reactMarkdownComponentPromise) {
|
|
14
|
+
reactMarkdownComponentPromise = import("react-markdown").then((mod) => {
|
|
15
|
+
const component = mod.default;
|
|
16
|
+
loadedReactMarkdownComponent = component;
|
|
17
|
+
return component;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return reactMarkdownComponentPromise;
|
|
21
|
+
}
|
|
22
|
+
function ReactMarkdownComponent(props) {
|
|
23
|
+
const [Component, setComponent] = React.useState(
|
|
24
|
+
() => loadedReactMarkdownComponent
|
|
25
|
+
);
|
|
26
|
+
React.useEffect(() => {
|
|
27
|
+
if (Component) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
let active = true;
|
|
31
|
+
void loadReactMarkdownComponent().then((resolved) => {
|
|
32
|
+
if (active) {
|
|
33
|
+
setComponent(() => resolved);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
return () => {
|
|
37
|
+
active = false;
|
|
38
|
+
};
|
|
39
|
+
}, [Component]);
|
|
40
|
+
if (!Component) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return /* @__PURE__ */ jsx(Component, { ...props });
|
|
44
|
+
}
|
|
6
45
|
const EMPTY_PLUGINS = [];
|
|
46
|
+
function MarkdownPreview({ children, className, remarkPlugins }) {
|
|
47
|
+
return /* @__PURE__ */ jsx("div", { className, children: /* @__PURE__ */ jsx(ReactMarkdownComponent, { remarkPlugins, children }) });
|
|
48
|
+
}
|
|
7
49
|
function MarkdownContent({
|
|
8
50
|
body,
|
|
9
51
|
format = "text",
|
|
@@ -17,9 +59,10 @@ function MarkdownContent({
|
|
|
17
59
|
if (!shouldRenderMarkdown) {
|
|
18
60
|
return /* @__PURE__ */ jsx("div", { className, children: body });
|
|
19
61
|
}
|
|
20
|
-
return /* @__PURE__ */ jsx(
|
|
62
|
+
return /* @__PURE__ */ jsx(MarkdownPreview, { className, remarkPlugins: plugins, children: body });
|
|
21
63
|
}
|
|
22
64
|
export {
|
|
23
|
-
MarkdownContent
|
|
65
|
+
MarkdownContent,
|
|
66
|
+
MarkdownPreview
|
|
24
67
|
};
|
|
25
68
|
//# sourceMappingURL=MarkdownContent.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/backend/markdown/MarkdownContent.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { PluggableList } from 'unified'\nimport { useMarkdownRemarkPlugins } from './useMarkdownRemarkPlugins'\n\nconst
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { PluggableList } from 'unified'\nimport { useMarkdownRemarkPlugins } from './useMarkdownRemarkPlugins'\n\ntype ReactMarkdownProps = {\n children: React.ReactNode\n remarkPlugins?: PluggableList\n}\n\nconst isTestEnv = typeof process !== 'undefined' && process.env.NODE_ENV === 'test'\n\nconst TestMarkdownComponent: React.ComponentType<ReactMarkdownProps> = ({ children }) => <>{children}</>\n\nlet loadedReactMarkdownComponent: React.ComponentType<ReactMarkdownProps> | null = isTestEnv\n ? TestMarkdownComponent\n : null\nlet reactMarkdownComponentPromise: Promise<React.ComponentType<ReactMarkdownProps>> | null = null\n\nfunction loadReactMarkdownComponent(): Promise<React.ComponentType<ReactMarkdownProps>> {\n if (loadedReactMarkdownComponent) {\n return Promise.resolve(loadedReactMarkdownComponent)\n }\n\n if (!reactMarkdownComponentPromise) {\n reactMarkdownComponentPromise = import('react-markdown').then((mod) => {\n const component = mod.default as React.ComponentType<ReactMarkdownProps>\n loadedReactMarkdownComponent = component\n return component\n })\n }\n\n return reactMarkdownComponentPromise\n}\n\nfunction ReactMarkdownComponent(props: ReactMarkdownProps) {\n const [Component, setComponent] = React.useState<React.ComponentType<ReactMarkdownProps> | null>(\n () => loadedReactMarkdownComponent,\n )\n\n React.useEffect(() => {\n if (Component) {\n return\n }\n\n let active = true\n\n void loadReactMarkdownComponent().then((resolved) => {\n if (active) {\n setComponent(() => resolved)\n }\n })\n\n return () => {\n active = false\n }\n }, [Component])\n\n if (!Component) {\n return null\n }\n\n return <Component {...props} />\n}\n\nexport type MarkdownContentProps = {\n body: string\n format?: 'text' | 'markdown'\n className?: string\n remarkPlugins?: PluggableList\n}\n\nexport type MarkdownPreviewProps = {\n children: string\n className?: string\n remarkPlugins?: PluggableList\n}\n\nconst EMPTY_PLUGINS: PluggableList = []\n\nexport function MarkdownPreview({ children, className, remarkPlugins }: MarkdownPreviewProps) {\n return (\n <div className={className}>\n <ReactMarkdownComponent remarkPlugins={remarkPlugins}>{children}</ReactMarkdownComponent>\n </div>\n )\n}\n\nexport function MarkdownContent({\n body,\n format = 'text',\n className,\n remarkPlugins,\n}: MarkdownContentProps) {\n const shouldRenderMarkdown = format === 'markdown'\n const plugins = useMarkdownRemarkPlugins(\n shouldRenderMarkdown ? remarkPlugins : EMPTY_PLUGINS,\n )\n\n if (!shouldRenderMarkdown) {\n return <div className={className}>{body}</div>\n }\n\n return (\n <MarkdownPreview className={className} remarkPlugins={plugins}>\n {body}\n </MarkdownPreview>\n )\n}\n"],
|
|
5
|
+
"mappings": ";AAayF;AAXzF,YAAY,WAAW;AAEvB,SAAS,gCAAgC;AAOzC,MAAM,YAAY,OAAO,YAAY,eAAe,QAAQ,IAAI,aAAa;AAE7E,MAAM,wBAAiE,CAAC,EAAE,SAAS,MAAM,gCAAG,UAAS;AAErG,IAAI,+BAA+E,YAC/E,wBACA;AACJ,IAAI,gCAAyF;AAE7F,SAAS,6BAA+E;AACtF,MAAI,8BAA8B;AAChC,WAAO,QAAQ,QAAQ,4BAA4B;AAAA,EACrD;AAEA,MAAI,CAAC,+BAA+B;AAClC,oCAAgC,OAAO,gBAAgB,EAAE,KAAK,CAAC,QAAQ;AACrE,YAAM,YAAY,IAAI;AACtB,qCAA+B;AAC/B,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AAEA,SAAS,uBAAuB,OAA2B;AACzD,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM;AAAA,IACtC,MAAM;AAAA,EACR;AAEA,QAAM,UAAU,MAAM;AACpB,QAAI,WAAW;AACb;AAAA,IACF;AAEA,QAAI,SAAS;AAEb,SAAK,2BAA2B,EAAE,KAAK,CAAC,aAAa;AACnD,UAAI,QAAQ;AACV,qBAAa,MAAM,QAAQ;AAAA,MAC7B;AAAA,IACF,CAAC;AAED,WAAO,MAAM;AACX,eAAS;AAAA,IACX;AAAA,EACF,GAAG,CAAC,SAAS,CAAC;AAEd,MAAI,CAAC,WAAW;AACd,WAAO;AAAA,EACT;AAEA,SAAO,oBAAC,aAAW,GAAG,OAAO;AAC/B;AAeA,MAAM,gBAA+B,CAAC;AAE/B,SAAS,gBAAgB,EAAE,UAAU,WAAW,cAAc,GAAyB;AAC5F,SACE,oBAAC,SAAI,WACH,8BAAC,0BAAuB,eAA+B,UAAS,GAClE;AAEJ;AAEO,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA;AACF,GAAyB;AACvB,QAAM,uBAAuB,WAAW;AACxC,QAAM,UAAU;AAAA,IACd,uBAAuB,gBAAgB;AAAA,EACzC;AAEA,MAAI,CAAC,sBAAsB;AACzB,WAAO,oBAAC,SAAI,WAAuB,gBAAK;AAAA,EAC1C;AAEA,SACE,oBAAC,mBAAgB,WAAsB,eAAe,SACnD,gBACH;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useState, useCallback, useMemo } from "react";
|
|
3
|
+
import { useEffect, useState, useCallback, useMemo } from "react";
|
|
4
4
|
import Image from "next/image";
|
|
5
5
|
import Link from "next/link";
|
|
6
6
|
import { usePathname } from "next/navigation";
|
|
@@ -12,6 +12,7 @@ import { usePortalEventBridge } from "./hooks/usePortalEventBridge.js";
|
|
|
12
12
|
import { mergeMenuItems } from "../backend/injection/mergeMenuItems.js";
|
|
13
13
|
import { PortalNotificationBell } from "./components/PortalNotificationBell.js";
|
|
14
14
|
import { usePortalContext } from "./PortalContext.js";
|
|
15
|
+
import { apiCall } from "../backend/utils/apiCall.js";
|
|
15
16
|
const PORTAL_SHELL_HANDLE = "page:portal:layout";
|
|
16
17
|
const PORTAL_HEADER_HANDLE = "section:portal:header";
|
|
17
18
|
const PORTAL_FOOTER_HANDLE = "section:portal:footer";
|
|
@@ -101,24 +102,53 @@ function PortalShell({
|
|
|
101
102
|
const portalHome = orgSlug ? `/${orgSlug}/portal` : "/portal";
|
|
102
103
|
const loginHref = orgSlug ? `/${orgSlug}/portal/login` : "/portal/login";
|
|
103
104
|
const signupHref = orgSlug ? `/${orgSlug}/portal/signup` : "/portal/signup";
|
|
104
|
-
const dashboardHref = orgSlug ? `/${orgSlug}/portal/dashboard` : "/portal/dashboard";
|
|
105
|
-
const profileHref = orgSlug ? `/${orgSlug}/portal/profile` : "/portal/profile";
|
|
106
105
|
const headerTitle = orgName || t("portal.title", "Customer Portal");
|
|
107
106
|
const closeMobile = useCallback(() => setMobileOpen(false), []);
|
|
107
|
+
const [autoNavGroups, setAutoNavGroups] = useState([]);
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (!authenticated) {
|
|
110
|
+
setAutoNavGroups([]);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
let cancelled = false;
|
|
114
|
+
const load = async () => {
|
|
115
|
+
try {
|
|
116
|
+
const { ok, result } = await apiCall(
|
|
117
|
+
"/api/customer_accounts/portal/nav"
|
|
118
|
+
);
|
|
119
|
+
if (cancelled || !ok || !result?.ok) return;
|
|
120
|
+
setAutoNavGroups(Array.isArray(result.groups) ? result.groups : []);
|
|
121
|
+
} catch {
|
|
122
|
+
if (!cancelled) setAutoNavGroups([]);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
void load();
|
|
126
|
+
return () => {
|
|
127
|
+
cancelled = true;
|
|
128
|
+
};
|
|
129
|
+
}, [authenticated]);
|
|
108
130
|
const mergedNavItems = useMemo(() => {
|
|
109
131
|
if (!authenticated) return [];
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
132
|
+
const discovered = autoNavGroups.find((g) => g.id === "main")?.items ?? [];
|
|
133
|
+
const builtIn = discovered.map((item) => ({
|
|
134
|
+
id: item.id,
|
|
135
|
+
labelKey: item.labelKey,
|
|
136
|
+
label: item.label,
|
|
137
|
+
href: item.href
|
|
138
|
+
}));
|
|
113
139
|
return mergeMenuItems(builtIn, injectedMainItems);
|
|
114
|
-
}, [authenticated,
|
|
140
|
+
}, [authenticated, autoNavGroups, injectedMainItems]);
|
|
115
141
|
const mergedAccountItems = useMemo(() => {
|
|
116
142
|
if (!authenticated) return [];
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
143
|
+
const discovered = autoNavGroups.find((g) => g.id === "account")?.items ?? [];
|
|
144
|
+
const builtIn = discovered.map((item) => ({
|
|
145
|
+
id: item.id,
|
|
146
|
+
labelKey: item.labelKey,
|
|
147
|
+
label: item.label,
|
|
148
|
+
href: item.href
|
|
149
|
+
}));
|
|
120
150
|
return mergeMenuItems(builtIn, injectedAccountItems);
|
|
121
|
-
}, [authenticated,
|
|
151
|
+
}, [authenticated, autoNavGroups, injectedAccountItems]);
|
|
122
152
|
if (!authenticated) {
|
|
123
153
|
return /* @__PURE__ */ jsxs("div", { className: "flex min-h-svh flex-col bg-background", "data-portal-handle": PORTAL_SHELL_HANDLE, children: [
|
|
124
154
|
/* @__PURE__ */ jsx("header", { className: "sticky top-0 z-40 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80", "data-portal-handle": PORTAL_HEADER_HANDLE, children: /* @__PURE__ */ jsxs("div", { className: "mx-auto flex h-16 w-full max-w-screen-lg items-center justify-between px-6", children: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/portal/PortalShell.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport { type ReactNode, useState, useCallback, useMemo, useContext } from 'react'\nimport Image from 'next/image'\nimport Link from 'next/link'\nimport { usePathname } from 'next/navigation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '../primitives/button'\nimport { IconButton } from '../primitives/icon-button'\nimport { usePortalInjectedMenuItems } from './hooks/usePortalInjectedMenuItems'\nimport { usePortalEventBridge } from './hooks/usePortalEventBridge'\nimport { mergeMenuItems } from '../backend/injection/mergeMenuItems'\nimport type { MergedMenuItem } from '../backend/injection/mergeMenuItems'\nimport { PortalNotificationBell } from './components/PortalNotificationBell'\nimport { usePortalContext } from './PortalContext'\n\n// Component replacement handle IDs (FROZEN once shipped)\nexport const PORTAL_SHELL_HANDLE = 'page:portal:layout'\nexport const PORTAL_HEADER_HANDLE = 'section:portal:header'\nexport const PORTAL_FOOTER_HANDLE = 'section:portal:footer'\nexport const PORTAL_SIDEBAR_HANDLE = 'section:portal:sidebar'\nexport const PORTAL_USER_MENU_HANDLE = 'section:portal:user-menu'\n\nexport type PortalShellProps = {\n children: ReactNode\n /** Override orgSlug (used on public pages without context) */\n orgSlug?: string\n /** Override organization name (used on public pages without context) */\n organizationName?: string\n /** Whether to show authenticated layout. Auto-detected from context when omitted. */\n authenticated?: boolean\n /** Logout handler. Auto-provided from context when omitted. */\n onLogout?: () => void\n enableEventBridge?: boolean\n /** Override user name. Auto-read from context when omitted. */\n userName?: string\n /** Override user email. Auto-read from context when omitted. */\n userEmail?: string\n}\n\nfunction PortalEventBridgeMount() {\n usePortalEventBridge()\n return null\n}\n\n/* ---- Inline SVG icons ---- */\n\nfunction MenuIcon({ className }: { className?: string }) {\n return (\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className={className}>\n <line x1=\"4\" x2=\"20\" y1=\"12\" y2=\"12\" /><line x1=\"4\" x2=\"20\" y1=\"6\" y2=\"6\" /><line x1=\"4\" x2=\"20\" y1=\"18\" y2=\"18\" />\n </svg>\n )\n}\n\nfunction XIcon({ className }: { className?: string }) {\n return (\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className={className}>\n <path d=\"M18 6 6 18\" /><path d=\"m6 6 12 12\" />\n </svg>\n )\n}\n\nfunction LogOutIcon({ className }: { className?: string }) {\n return (\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className={className}>\n <path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\" /><polyline points=\"16 17 21 12 16 7\" /><line x1=\"21\" x2=\"9\" y1=\"12\" y2=\"12\" />\n </svg>\n )\n}\n\n/* ---- Sidebar nav item ---- */\n\nfunction SidebarNavItem({\n item,\n active,\n t,\n onClick,\n}: {\n item: MergedMenuItem\n active: boolean\n t: (key: string, fallback?: string) => string\n onClick?: () => void\n}) {\n const label = item.labelKey ? t(item.labelKey, item.label) : item.label\n if (!label) return null\n\n const cls = [\n 'flex items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium transition-colors',\n active\n ? 'bg-foreground text-background'\n : 'text-muted-foreground hover:bg-muted hover:text-foreground',\n ].join(' ')\n\n if (item.href) {\n return (\n <Link href={item.href} className={cls} data-menu-item-id={item.id} onClick={onClick}>\n {label}\n </Link>\n )\n }\n if (item.onClick) {\n return (\n <button type=\"button\" className={cls} data-menu-item-id={item.id} onClick={() => { item.onClick?.(); onClick?.() }}>\n {label}\n </button>\n )\n }\n return null\n}\n\n/* ---- User initials avatar ---- */\n\nfunction UserAvatar({ name, className }: { name?: string; className?: string }) {\n const initials = name\n ? name.split(' ').map((w) => w[0]).slice(0, 2).join('').toUpperCase()\n : '?'\n return (\n <div className={`flex items-center justify-center rounded-full bg-foreground text-[11px] font-semibold text-background ${className ?? 'size-8'}`}>\n {initials}\n </div>\n )\n}\n\n/* ---- Try reading from PortalContext ---- */\n\nfunction useOptionalPortalContext() {\n try {\n return usePortalContext()\n } catch {\n return null\n }\n}\n\n/* ================================================================== */\n/* PortalShell */\n/* ================================================================== */\n\n/**\n * Portal layout shell.\n *\n * When a `PortalProvider` is mounted in a parent layout, PortalShell reads\n * auth/tenant state from context \u2014 no re-fetching on navigation. Props are\n * used as overrides or for public pages that don't have a context.\n */\nexport function PortalShell({\n children,\n orgSlug: orgSlugProp,\n organizationName: orgNameProp,\n authenticated: authenticatedProp,\n onLogout: onLogoutProp,\n enableEventBridge = false,\n userName: userNameProp,\n userEmail: userEmailProp,\n}: PortalShellProps) {\n const t = useT()\n const pathname = usePathname()\n const [mobileOpen, setMobileOpen] = useState(false)\n\n // Read from context when available (persists across navigations)\n const portalCtx = useOptionalPortalContext()\n\n // Resolve values: context takes priority, props are fallback/override\n const orgSlug = portalCtx?.orgSlug ?? orgSlugProp\n const orgName = portalCtx?.tenant.organizationName ?? orgNameProp\n const user = portalCtx?.auth.user ?? null\n const authenticated = authenticatedProp ?? !!user\n const onLogout = onLogoutProp ?? portalCtx?.auth.logout\n const userName = userNameProp ?? user?.displayName\n const userEmail = userEmailProp ?? user?.email\n\n const { items: injectedMainItems } = usePortalInjectedMenuItems('menu:portal:sidebar:main')\n const { items: injectedAccountItems } = usePortalInjectedMenuItems('menu:portal:sidebar:account')\n\n const portalHome = orgSlug ? `/${orgSlug}/portal` : '/portal'\n const loginHref = orgSlug ? `/${orgSlug}/portal/login` : '/portal/login'\n const signupHref = orgSlug ? `/${orgSlug}/portal/signup` : '/portal/signup'\n const dashboardHref = orgSlug ? `/${orgSlug}/portal/dashboard` : '/portal/dashboard'\n const profileHref = orgSlug ? `/${orgSlug}/portal/profile` : '/portal/profile'\n // Always use the resolved organization name from the database.\n // Fall back to the generic portal title \u2014 never display the raw slug.\n const headerTitle = orgName || t('portal.title', 'Customer Portal')\n\n const closeMobile = useCallback(() => setMobileOpen(false), [])\n\n const mergedNavItems = useMemo(() => {\n if (!authenticated) return []\n const builtIn = [\n { id: 'portal-dashboard', labelKey: 'portal.nav.dashboard', href: dashboardHref },\n ]\n return mergeMenuItems(builtIn, injectedMainItems)\n }, [authenticated, dashboardHref, injectedMainItems])\n\n const mergedAccountItems = useMemo(() => {\n if (!authenticated) return []\n const builtIn = [\n { id: 'portal-profile', labelKey: 'portal.nav.profile', href: profileHref },\n ]\n return mergeMenuItems(builtIn, injectedAccountItems)\n }, [authenticated, profileHref, injectedAccountItems])\n\n /* ---- PUBLIC LAYOUT ---- */\n if (!authenticated) {\n return (\n <div className=\"flex min-h-svh flex-col bg-background\" data-portal-handle={PORTAL_SHELL_HANDLE}>\n <header className=\"sticky top-0 z-40 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80\" data-portal-handle={PORTAL_HEADER_HANDLE}>\n <div className=\"mx-auto flex h-16 w-full max-w-screen-lg items-center justify-between px-6\">\n <Link href={portalHome} className=\"flex items-center gap-2.5 text-foreground transition hover:opacity-80\" aria-label={headerTitle}>\n <Image src=\"/open-mercato.svg\" alt=\"\" width={28} height={28} className=\"\" priority />\n <span className=\"text-[15px] font-semibold tracking-tight\">{headerTitle}</span>\n </Link>\n <nav aria-label=\"Primary\" className=\"flex items-center gap-1\">\n <Button asChild variant=\"ghost\" size=\"sm\" className=\"text-[13px]\">\n <Link href={loginHref}>{t('portal.nav.login', 'Log In')}</Link>\n </Button>\n <Button asChild size=\"sm\" className=\"rounded-lg text-[13px]\">\n <Link href={signupHref}>{t('portal.nav.signup', 'Sign Up')}</Link>\n </Button>\n </nav>\n </div>\n </header>\n\n <main className=\"flex-1\">\n <div className=\"mx-auto flex w-full max-w-screen-lg flex-col gap-8 px-6 py-12 sm:py-20\">\n {children}\n </div>\n </main>\n\n <footer className=\"border-t\" data-portal-handle={PORTAL_FOOTER_HANDLE}>\n <div className=\"mx-auto flex w-full max-w-screen-lg items-center justify-between px-6 py-6\">\n <Link href={portalHome} className=\"flex items-center gap-2 text-muted-foreground transition hover:text-foreground\">\n <Image src=\"/open-mercato.svg\" alt=\"\" width={20} height={20} className=\"\" />\n <span className=\"text-sm font-medium text-foreground\">{headerTitle}</span>\n </Link>\n <p className=\"text-xs text-muted-foreground/60\">\n {t('portal.footer.copyright', '\\u00A9 {year} All rights reserved.', { year: new Date().getFullYear() })}\n </p>\n </div>\n </footer>\n </div>\n )\n }\n\n /* ---- AUTHENTICATED LAYOUT ---- */\n\n const sidebarContent = (\n <div className=\"flex h-full flex-col\" data-portal-handle={PORTAL_SIDEBAR_HANDLE}>\n <div className=\"flex h-16 items-center gap-2.5 border-b px-5\">\n <Link href={portalHome} className=\"flex items-center gap-2.5 text-foreground transition hover:opacity-80\" aria-label={headerTitle}>\n <Image src=\"/open-mercato.svg\" alt=\"\" width={22} height={22} className=\"\" />\n <span className=\"text-[14px] font-semibold tracking-tight truncate\">{headerTitle}</span>\n </Link>\n </div>\n\n <nav aria-label=\"Portal navigation\" className=\"flex-1 overflow-y-auto px-3 py-5\">\n <p className=\"mb-2 px-3 text-[10px] font-semibold uppercase tracking-[0.12em] text-muted-foreground/50\">\n {t('portal.nav.home', 'Portal')}\n </p>\n <div className=\"flex flex-col gap-0.5\">\n {mergedNavItems.map((item) => (\n <SidebarNavItem\n key={item.id}\n item={item}\n active={!!item.href && pathname.startsWith(item.href)}\n t={t}\n onClick={closeMobile}\n />\n ))}\n </div>\n\n {mergedAccountItems.length > 0 ? (\n <div className=\"mt-8\">\n <p className=\"mb-2 px-3 text-[10px] font-semibold uppercase tracking-[0.12em] text-muted-foreground/50\">\n {t('portal.nav.account', 'Account')}\n </p>\n <div className=\"flex flex-col gap-0.5\">\n {mergedAccountItems.map((item) => (\n <SidebarNavItem\n key={item.id}\n item={item}\n active={!!item.href && pathname.startsWith(item.href)}\n t={t}\n onClick={closeMobile}\n />\n ))}\n </div>\n </div>\n ) : null}\n </nav>\n\n <div className=\"border-t px-3 py-3\">\n <div className=\"flex items-center gap-2.5 rounded-lg px-3 py-2\">\n <UserAvatar name={userName} className=\"size-8\" />\n <div className=\"min-w-0 flex-1\">\n {userName ? (\n <p className=\"truncate text-[13px] font-medium leading-tight\">{userName}</p>\n ) : (\n <div className=\"h-4 w-24 animate-pulse rounded bg-muted\" />\n )}\n {userEmail ? (\n <p className=\"truncate text-[11px] text-muted-foreground\">{userEmail}</p>\n ) : (\n <div className=\"mt-1 h-3 w-32 animate-pulse rounded bg-muted\" />\n )}\n </div>\n </div>\n <button\n type=\"button\"\n onClick={onLogout}\n className=\"mt-0.5 flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] text-muted-foreground transition-colors hover:bg-muted hover:text-foreground\"\n data-portal-handle={PORTAL_USER_MENU_HANDLE}\n data-menu-item-id=\"portal-logout\"\n >\n <LogOutIcon className=\"size-4\" />\n {t('portal.nav.logout', 'Log Out')}\n </button>\n </div>\n </div>\n )\n\n return (\n <div className=\"flex min-h-svh bg-background\" data-portal-handle={PORTAL_SHELL_HANDLE}>\n {enableEventBridge ? <PortalEventBridgeMount /> : null}\n\n <aside className=\"hidden w-[240px] shrink-0 border-r lg:block\">\n {sidebarContent}\n </aside>\n\n {mobileOpen ? (\n <div className=\"fixed inset-0 z-50 lg:hidden\">\n <div className=\"absolute inset-0 bg-black/30 backdrop-blur-sm\" onClick={closeMobile} />\n <aside className=\"relative z-10 h-full w-[280px] bg-background shadow-2xl\">\n <div className=\"absolute right-3 top-4 z-20\">\n <IconButton variant=\"ghost\" size=\"sm\" type=\"button\" onClick={closeMobile} aria-label=\"Close menu\">\n <XIcon className=\"size-4\" />\n </IconButton>\n </div>\n {sidebarContent}\n </aside>\n </div>\n ) : null}\n\n <div className=\"flex min-w-0 flex-1 flex-col\">\n <header className=\"flex h-16 items-center justify-between border-b px-4 lg:px-8\" data-portal-handle={PORTAL_HEADER_HANDLE}>\n <div className=\"flex items-center gap-3\">\n <IconButton variant=\"ghost\" size=\"sm\" type=\"button\" onClick={() => setMobileOpen(true)} className=\"lg:hidden\" aria-label=\"Open menu\">\n <MenuIcon className=\"size-5\" />\n </IconButton>\n </div>\n <div className=\"flex items-center gap-3\">\n <PortalNotificationBell t={t} />\n </div>\n </header>\n\n <main className=\"flex-1 overflow-y-auto\">\n <div className=\"w-full px-4 py-6 lg:px-8 lg:py-8\">\n {children}\n </div>\n </main>\n\n <footer className=\"border-t px-4 py-4 lg:px-8\" data-portal-handle={PORTAL_FOOTER_HANDLE}>\n <p className=\"text-[11px] text-muted-foreground/50\">\n {t('portal.footer.copyright', '\\u00A9 {year} All rights reserved.', { year: new Date().getFullYear() })}\n </p>\n </footer>\n </div>\n </div>\n )\n}\n\nexport default PortalShell\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport { type ReactNode, useEffect, useState, useCallback, useMemo, useContext } from 'react'\nimport Image from 'next/image'\nimport Link from 'next/link'\nimport { usePathname } from 'next/navigation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '../primitives/button'\nimport { IconButton } from '../primitives/icon-button'\nimport { usePortalInjectedMenuItems } from './hooks/usePortalInjectedMenuItems'\nimport { usePortalEventBridge } from './hooks/usePortalEventBridge'\nimport { mergeMenuItems } from '../backend/injection/mergeMenuItems'\nimport type { MergedMenuItem } from '../backend/injection/mergeMenuItems'\nimport { PortalNotificationBell } from './components/PortalNotificationBell'\nimport { usePortalContext } from './PortalContext'\nimport { apiCall } from '../backend/utils/apiCall'\nimport type { PortalNavGroup } from './utils/nav'\n\n// Component replacement handle IDs (FROZEN once shipped)\nexport const PORTAL_SHELL_HANDLE = 'page:portal:layout'\nexport const PORTAL_HEADER_HANDLE = 'section:portal:header'\nexport const PORTAL_FOOTER_HANDLE = 'section:portal:footer'\nexport const PORTAL_SIDEBAR_HANDLE = 'section:portal:sidebar'\nexport const PORTAL_USER_MENU_HANDLE = 'section:portal:user-menu'\n\nexport type PortalShellProps = {\n children: ReactNode\n /** Override orgSlug (used on public pages without context) */\n orgSlug?: string\n /** Override organization name (used on public pages without context) */\n organizationName?: string\n /** Whether to show authenticated layout. Auto-detected from context when omitted. */\n authenticated?: boolean\n /** Logout handler. Auto-provided from context when omitted. */\n onLogout?: () => void\n enableEventBridge?: boolean\n /** Override user name. Auto-read from context when omitted. */\n userName?: string\n /** Override user email. Auto-read from context when omitted. */\n userEmail?: string\n}\n\nfunction PortalEventBridgeMount() {\n usePortalEventBridge()\n return null\n}\n\n/* ---- Inline SVG icons ---- */\n\nfunction MenuIcon({ className }: { className?: string }) {\n return (\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className={className}>\n <line x1=\"4\" x2=\"20\" y1=\"12\" y2=\"12\" /><line x1=\"4\" x2=\"20\" y1=\"6\" y2=\"6\" /><line x1=\"4\" x2=\"20\" y1=\"18\" y2=\"18\" />\n </svg>\n )\n}\n\nfunction XIcon({ className }: { className?: string }) {\n return (\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className={className}>\n <path d=\"M18 6 6 18\" /><path d=\"m6 6 12 12\" />\n </svg>\n )\n}\n\nfunction LogOutIcon({ className }: { className?: string }) {\n return (\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className={className}>\n <path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\" /><polyline points=\"16 17 21 12 16 7\" /><line x1=\"21\" x2=\"9\" y1=\"12\" y2=\"12\" />\n </svg>\n )\n}\n\n/* ---- Sidebar nav item ---- */\n\nfunction SidebarNavItem({\n item,\n active,\n t,\n onClick,\n}: {\n item: MergedMenuItem\n active: boolean\n t: (key: string, fallback?: string) => string\n onClick?: () => void\n}) {\n const label = item.labelKey ? t(item.labelKey, item.label) : item.label\n if (!label) return null\n\n const cls = [\n 'flex items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium transition-colors',\n active\n ? 'bg-foreground text-background'\n : 'text-muted-foreground hover:bg-muted hover:text-foreground',\n ].join(' ')\n\n if (item.href) {\n return (\n <Link href={item.href} className={cls} data-menu-item-id={item.id} onClick={onClick}>\n {label}\n </Link>\n )\n }\n if (item.onClick) {\n return (\n <button type=\"button\" className={cls} data-menu-item-id={item.id} onClick={() => { item.onClick?.(); onClick?.() }}>\n {label}\n </button>\n )\n }\n return null\n}\n\n/* ---- User initials avatar ---- */\n\nfunction UserAvatar({ name, className }: { name?: string; className?: string }) {\n const initials = name\n ? name.split(' ').map((w) => w[0]).slice(0, 2).join('').toUpperCase()\n : '?'\n return (\n <div className={`flex items-center justify-center rounded-full bg-foreground text-[11px] font-semibold text-background ${className ?? 'size-8'}`}>\n {initials}\n </div>\n )\n}\n\n/* ---- Try reading from PortalContext ---- */\n\nfunction useOptionalPortalContext() {\n try {\n return usePortalContext()\n } catch {\n return null\n }\n}\n\n/* ================================================================== */\n/* PortalShell */\n/* ================================================================== */\n\n/**\n * Portal layout shell.\n *\n * When a `PortalProvider` is mounted in a parent layout, PortalShell reads\n * auth/tenant state from context \u2014 no re-fetching on navigation. Props are\n * used as overrides or for public pages that don't have a context.\n */\nexport function PortalShell({\n children,\n orgSlug: orgSlugProp,\n organizationName: orgNameProp,\n authenticated: authenticatedProp,\n onLogout: onLogoutProp,\n enableEventBridge = false,\n userName: userNameProp,\n userEmail: userEmailProp,\n}: PortalShellProps) {\n const t = useT()\n const pathname = usePathname()\n const [mobileOpen, setMobileOpen] = useState(false)\n\n // Read from context when available (persists across navigations)\n const portalCtx = useOptionalPortalContext()\n\n // Resolve values: context takes priority, props are fallback/override\n const orgSlug = portalCtx?.orgSlug ?? orgSlugProp\n const orgName = portalCtx?.tenant.organizationName ?? orgNameProp\n const user = portalCtx?.auth.user ?? null\n const authenticated = authenticatedProp ?? !!user\n const onLogout = onLogoutProp ?? portalCtx?.auth.logout\n const userName = userNameProp ?? user?.displayName\n const userEmail = userEmailProp ?? user?.email\n\n const { items: injectedMainItems } = usePortalInjectedMenuItems('menu:portal:sidebar:main')\n const { items: injectedAccountItems } = usePortalInjectedMenuItems('menu:portal:sidebar:account')\n\n const portalHome = orgSlug ? `/${orgSlug}/portal` : '/portal'\n const loginHref = orgSlug ? `/${orgSlug}/portal/login` : '/portal/login'\n const signupHref = orgSlug ? `/${orgSlug}/portal/signup` : '/portal/signup'\n // Always use the resolved organization name from the database.\n // Fall back to the generic portal title \u2014 never display the raw slug.\n const headerTitle = orgName || t('portal.title', 'Customer Portal')\n\n const closeMobile = useCallback(() => setMobileOpen(false), [])\n\n const [autoNavGroups, setAutoNavGroups] = useState<PortalNavGroup[]>([])\n useEffect(() => {\n if (!authenticated) {\n setAutoNavGroups([])\n return\n }\n let cancelled = false\n const load = async () => {\n try {\n const { ok, result } = await apiCall<{ ok: boolean; groups?: PortalNavGroup[] }>(\n '/api/customer_accounts/portal/nav',\n )\n if (cancelled || !ok || !result?.ok) return\n setAutoNavGroups(Array.isArray(result.groups) ? result.groups : [])\n } catch {\n if (!cancelled) setAutoNavGroups([])\n }\n }\n void load()\n return () => {\n cancelled = true\n }\n }, [authenticated])\n\n const mergedNavItems = useMemo(() => {\n if (!authenticated) return []\n const discovered = autoNavGroups.find((g) => g.id === 'main')?.items ?? []\n const builtIn = discovered.map((item) => ({\n id: item.id,\n labelKey: item.labelKey,\n label: item.label,\n href: item.href,\n }))\n return mergeMenuItems(builtIn, injectedMainItems)\n }, [authenticated, autoNavGroups, injectedMainItems])\n\n const mergedAccountItems = useMemo(() => {\n if (!authenticated) return []\n const discovered = autoNavGroups.find((g) => g.id === 'account')?.items ?? []\n const builtIn = discovered.map((item) => ({\n id: item.id,\n labelKey: item.labelKey,\n label: item.label,\n href: item.href,\n }))\n return mergeMenuItems(builtIn, injectedAccountItems)\n }, [authenticated, autoNavGroups, injectedAccountItems])\n\n /* ---- PUBLIC LAYOUT ---- */\n if (!authenticated) {\n return (\n <div className=\"flex min-h-svh flex-col bg-background\" data-portal-handle={PORTAL_SHELL_HANDLE}>\n <header className=\"sticky top-0 z-40 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80\" data-portal-handle={PORTAL_HEADER_HANDLE}>\n <div className=\"mx-auto flex h-16 w-full max-w-screen-lg items-center justify-between px-6\">\n <Link href={portalHome} className=\"flex items-center gap-2.5 text-foreground transition hover:opacity-80\" aria-label={headerTitle}>\n <Image src=\"/open-mercato.svg\" alt=\"\" width={28} height={28} className=\"\" priority />\n <span className=\"text-[15px] font-semibold tracking-tight\">{headerTitle}</span>\n </Link>\n <nav aria-label=\"Primary\" className=\"flex items-center gap-1\">\n <Button asChild variant=\"ghost\" size=\"sm\" className=\"text-[13px]\">\n <Link href={loginHref}>{t('portal.nav.login', 'Log In')}</Link>\n </Button>\n <Button asChild size=\"sm\" className=\"rounded-lg text-[13px]\">\n <Link href={signupHref}>{t('portal.nav.signup', 'Sign Up')}</Link>\n </Button>\n </nav>\n </div>\n </header>\n\n <main className=\"flex-1\">\n <div className=\"mx-auto flex w-full max-w-screen-lg flex-col gap-8 px-6 py-12 sm:py-20\">\n {children}\n </div>\n </main>\n\n <footer className=\"border-t\" data-portal-handle={PORTAL_FOOTER_HANDLE}>\n <div className=\"mx-auto flex w-full max-w-screen-lg items-center justify-between px-6 py-6\">\n <Link href={portalHome} className=\"flex items-center gap-2 text-muted-foreground transition hover:text-foreground\">\n <Image src=\"/open-mercato.svg\" alt=\"\" width={20} height={20} className=\"\" />\n <span className=\"text-sm font-medium text-foreground\">{headerTitle}</span>\n </Link>\n <p className=\"text-xs text-muted-foreground/60\">\n {t('portal.footer.copyright', '\\u00A9 {year} All rights reserved.', { year: new Date().getFullYear() })}\n </p>\n </div>\n </footer>\n </div>\n )\n }\n\n /* ---- AUTHENTICATED LAYOUT ---- */\n\n const sidebarContent = (\n <div className=\"flex h-full flex-col\" data-portal-handle={PORTAL_SIDEBAR_HANDLE}>\n <div className=\"flex h-16 items-center gap-2.5 border-b px-5\">\n <Link href={portalHome} className=\"flex items-center gap-2.5 text-foreground transition hover:opacity-80\" aria-label={headerTitle}>\n <Image src=\"/open-mercato.svg\" alt=\"\" width={22} height={22} className=\"\" />\n <span className=\"text-[14px] font-semibold tracking-tight truncate\">{headerTitle}</span>\n </Link>\n </div>\n\n <nav aria-label=\"Portal navigation\" className=\"flex-1 overflow-y-auto px-3 py-5\">\n <p className=\"mb-2 px-3 text-[10px] font-semibold uppercase tracking-[0.12em] text-muted-foreground/50\">\n {t('portal.nav.home', 'Portal')}\n </p>\n <div className=\"flex flex-col gap-0.5\">\n {mergedNavItems.map((item) => (\n <SidebarNavItem\n key={item.id}\n item={item}\n active={!!item.href && pathname.startsWith(item.href)}\n t={t}\n onClick={closeMobile}\n />\n ))}\n </div>\n\n {mergedAccountItems.length > 0 ? (\n <div className=\"mt-8\">\n <p className=\"mb-2 px-3 text-[10px] font-semibold uppercase tracking-[0.12em] text-muted-foreground/50\">\n {t('portal.nav.account', 'Account')}\n </p>\n <div className=\"flex flex-col gap-0.5\">\n {mergedAccountItems.map((item) => (\n <SidebarNavItem\n key={item.id}\n item={item}\n active={!!item.href && pathname.startsWith(item.href)}\n t={t}\n onClick={closeMobile}\n />\n ))}\n </div>\n </div>\n ) : null}\n </nav>\n\n <div className=\"border-t px-3 py-3\">\n <div className=\"flex items-center gap-2.5 rounded-lg px-3 py-2\">\n <UserAvatar name={userName} className=\"size-8\" />\n <div className=\"min-w-0 flex-1\">\n {userName ? (\n <p className=\"truncate text-[13px] font-medium leading-tight\">{userName}</p>\n ) : (\n <div className=\"h-4 w-24 animate-pulse rounded bg-muted\" />\n )}\n {userEmail ? (\n <p className=\"truncate text-[11px] text-muted-foreground\">{userEmail}</p>\n ) : (\n <div className=\"mt-1 h-3 w-32 animate-pulse rounded bg-muted\" />\n )}\n </div>\n </div>\n <button\n type=\"button\"\n onClick={onLogout}\n className=\"mt-0.5 flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] text-muted-foreground transition-colors hover:bg-muted hover:text-foreground\"\n data-portal-handle={PORTAL_USER_MENU_HANDLE}\n data-menu-item-id=\"portal-logout\"\n >\n <LogOutIcon className=\"size-4\" />\n {t('portal.nav.logout', 'Log Out')}\n </button>\n </div>\n </div>\n )\n\n return (\n <div className=\"flex min-h-svh bg-background\" data-portal-handle={PORTAL_SHELL_HANDLE}>\n {enableEventBridge ? <PortalEventBridgeMount /> : null}\n\n <aside className=\"hidden w-[240px] shrink-0 border-r lg:block\">\n {sidebarContent}\n </aside>\n\n {mobileOpen ? (\n <div className=\"fixed inset-0 z-50 lg:hidden\">\n <div className=\"absolute inset-0 bg-black/30 backdrop-blur-sm\" onClick={closeMobile} />\n <aside className=\"relative z-10 h-full w-[280px] bg-background shadow-2xl\">\n <div className=\"absolute right-3 top-4 z-20\">\n <IconButton variant=\"ghost\" size=\"sm\" type=\"button\" onClick={closeMobile} aria-label=\"Close menu\">\n <XIcon className=\"size-4\" />\n </IconButton>\n </div>\n {sidebarContent}\n </aside>\n </div>\n ) : null}\n\n <div className=\"flex min-w-0 flex-1 flex-col\">\n <header className=\"flex h-16 items-center justify-between border-b px-4 lg:px-8\" data-portal-handle={PORTAL_HEADER_HANDLE}>\n <div className=\"flex items-center gap-3\">\n <IconButton variant=\"ghost\" size=\"sm\" type=\"button\" onClick={() => setMobileOpen(true)} className=\"lg:hidden\" aria-label=\"Open menu\">\n <MenuIcon className=\"size-5\" />\n </IconButton>\n </div>\n <div className=\"flex items-center gap-3\">\n <PortalNotificationBell t={t} />\n </div>\n </header>\n\n <main className=\"flex-1 overflow-y-auto\">\n <div className=\"w-full px-4 py-6 lg:px-8 lg:py-8\">\n {children}\n </div>\n </main>\n\n <footer className=\"border-t px-4 py-4 lg:px-8\" data-portal-handle={PORTAL_FOOTER_HANDLE}>\n <p className=\"text-[11px] text-muted-foreground/50\">\n {t('portal.footer.copyright', '\\u00A9 {year} All rights reserved.', { year: new Date().getFullYear() })}\n </p>\n </footer>\n </div>\n </div>\n )\n}\n\nexport default PortalShell\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,10 +1,37 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { loadInjectionWidgetsForSpot } from "@open-mercato/shared/modules/widgets/injection-loader";
|
|
4
|
+
import { hasAllFeatures } from "@open-mercato/shared/security/features";
|
|
5
|
+
import { apiCall } from "../../backend/utils/apiCall.js";
|
|
6
|
+
function collectRequiredFeatures(widgets) {
|
|
7
|
+
const set = /* @__PURE__ */ new Set();
|
|
8
|
+
for (const widget of widgets) {
|
|
9
|
+
for (const feature of widget.metadata.features ?? []) {
|
|
10
|
+
if (!feature || feature.trim().length === 0) continue;
|
|
11
|
+
set.add(feature);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return Array.from(set);
|
|
15
|
+
}
|
|
16
|
+
async function readPortalGrantedFeatures(features) {
|
|
17
|
+
if (features.length === 0) return /* @__PURE__ */ new Set();
|
|
18
|
+
try {
|
|
19
|
+
const { ok, result: data } = await apiCall("/api/customer_accounts/portal/feature-check", {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: { "content-type": "application/json" },
|
|
22
|
+
body: JSON.stringify({ features })
|
|
23
|
+
});
|
|
24
|
+
if (!ok || !data?.ok) return /* @__PURE__ */ new Set();
|
|
25
|
+
return new Set(data.granted ?? []);
|
|
26
|
+
} catch {
|
|
27
|
+
return /* @__PURE__ */ new Set();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
4
30
|
function usePortalDashboardWidgets(spotId) {
|
|
5
31
|
const [widgets, setWidgets] = React.useState([]);
|
|
6
32
|
const [isLoading, setIsLoading] = React.useState(true);
|
|
7
33
|
const [error, setError] = React.useState(null);
|
|
34
|
+
const [grantedFeatures, setGrantedFeatures] = React.useState(/* @__PURE__ */ new Set());
|
|
8
35
|
React.useEffect(() => {
|
|
9
36
|
let mounted = true;
|
|
10
37
|
const load = async () => {
|
|
@@ -15,6 +42,10 @@ function usePortalDashboardWidgets(spotId) {
|
|
|
15
42
|
if (!mounted) return;
|
|
16
43
|
const uiWidgets = loaded.filter((w) => typeof w.Widget === "function");
|
|
17
44
|
setWidgets(uiWidgets);
|
|
45
|
+
const required = collectRequiredFeatures(uiWidgets);
|
|
46
|
+
const granted = await readPortalGrantedFeatures(required);
|
|
47
|
+
if (!mounted) return;
|
|
48
|
+
setGrantedFeatures(granted);
|
|
18
49
|
} catch (loadError) {
|
|
19
50
|
if (!mounted) return;
|
|
20
51
|
console.error(`[usePortalDashboardWidgets] Failed to load widgets for spot ${spotId}:`, loadError);
|
|
@@ -29,7 +60,15 @@ function usePortalDashboardWidgets(spotId) {
|
|
|
29
60
|
mounted = false;
|
|
30
61
|
};
|
|
31
62
|
}, [spotId]);
|
|
32
|
-
|
|
63
|
+
const grantedFeatureList = React.useMemo(() => Array.from(grantedFeatures), [grantedFeatures]);
|
|
64
|
+
const visibleWidgets = React.useMemo(
|
|
65
|
+
() => widgets.filter((widget) => {
|
|
66
|
+
const required = widget.metadata.features ?? [];
|
|
67
|
+
return required.length === 0 || hasAllFeatures(grantedFeatureList, required);
|
|
68
|
+
}),
|
|
69
|
+
[widgets, grantedFeatureList]
|
|
70
|
+
);
|
|
71
|
+
return { widgets: visibleWidgets, isLoading, error };
|
|
33
72
|
}
|
|
34
73
|
export {
|
|
35
74
|
usePortalDashboardWidgets
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/portal/hooks/usePortalDashboardWidgets.ts"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport type { InjectionSpotId } from '@open-mercato/shared/modules/widgets/injection'\nimport { loadInjectionWidgetsForSpot, type LoadedInjectionWidget } from '@open-mercato/shared/modules/widgets/injection-loader'\n\n/**\n * Loads UI injection widgets (with Widget component) for a portal spot.\n *\n * Unlike `useInjectionDataWidgets` which loads data-only widgets (columns, fields, menuItems),\n * this hook loads widgets that export a `Widget` React component \u2014 suitable for\n * portal dashboard sections and other UI injection spots.\n */\nexport function usePortalDashboardWidgets(spotId: InjectionSpotId): {\n widgets: LoadedInjectionWidget[]\n isLoading: boolean\n error: string | null\n} {\n const [widgets, setWidgets] = React.useState<LoadedInjectionWidget[]>([])\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n\n React.useEffect(() => {\n let mounted = true\n const load = async () => {\n try {\n setIsLoading(true)\n setError(null)\n const loaded = await loadInjectionWidgetsForSpot(spotId)\n if (!mounted) return\n // Only keep widgets that have a Widget component\n const uiWidgets = loaded.filter((w) => typeof w.Widget === 'function')\n setWidgets(uiWidgets)\n } catch (loadError) {\n if (!mounted) return\n console.error(`[usePortalDashboardWidgets] Failed to load widgets for spot ${spotId}:`, loadError)\n setError(loadError instanceof Error ? loadError.message : String(loadError))\n setWidgets([])\n } finally {\n if (mounted) setIsLoading(false)\n }\n }\n void load()\n return () => {\n mounted = false\n }\n }, [spotId])\n\n return { widgets, isLoading, error }\n}\n"],
|
|
5
|
-
"mappings": ";AAEA,YAAY,WAAW;AAEvB,SAAS,mCAA+D;
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport type { InjectionSpotId } from '@open-mercato/shared/modules/widgets/injection'\nimport { loadInjectionWidgetsForSpot, type LoadedInjectionWidget } from '@open-mercato/shared/modules/widgets/injection-loader'\nimport { hasAllFeatures } from '@open-mercato/shared/security/features'\nimport { apiCall } from '../../backend/utils/apiCall'\n\ntype PortalFeatureCheckResponse = {\n ok: boolean\n granted?: string[]\n}\n\nfunction collectRequiredFeatures(widgets: LoadedInjectionWidget[]): string[] {\n const set = new Set<string>()\n for (const widget of widgets) {\n for (const feature of widget.metadata.features ?? []) {\n if (!feature || feature.trim().length === 0) continue\n set.add(feature)\n }\n }\n return Array.from(set)\n}\n\nasync function readPortalGrantedFeatures(features: string[]): Promise<Set<string>> {\n if (features.length === 0) return new Set()\n try {\n const { ok, result: data } = await apiCall<PortalFeatureCheckResponse>('/api/customer_accounts/portal/feature-check', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ features }),\n })\n if (!ok || !data?.ok) return new Set()\n return new Set(data.granted ?? [])\n } catch {\n return new Set()\n }\n}\n\n/**\n * Loads UI injection widgets (with Widget component) for a portal spot.\n *\n * Unlike `useInjectionDataWidgets` which loads data-only widgets (columns, fields, menuItems),\n * this hook loads widgets that export a `Widget` React component \u2014 suitable for\n * portal dashboard sections and other UI injection spots.\n *\n * Feature gating: widgets declaring `metadata.features` are filtered against the\n * authenticated customer's grants resolved via\n * `/api/customer_accounts/portal/feature-check`. Wildcard grants (`portal.*`) resolve\n * through the shared matcher.\n */\nexport function usePortalDashboardWidgets(spotId: InjectionSpotId): {\n widgets: LoadedInjectionWidget[]\n isLoading: boolean\n error: string | null\n} {\n const [widgets, setWidgets] = React.useState<LoadedInjectionWidget[]>([])\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [grantedFeatures, setGrantedFeatures] = React.useState<Set<string>>(new Set())\n\n React.useEffect(() => {\n let mounted = true\n const load = async () => {\n try {\n setIsLoading(true)\n setError(null)\n const loaded = await loadInjectionWidgetsForSpot(spotId)\n if (!mounted) return\n // Only keep widgets that have a Widget component\n const uiWidgets = loaded.filter((w) => typeof w.Widget === 'function')\n setWidgets(uiWidgets)\n const required = collectRequiredFeatures(uiWidgets)\n const granted = await readPortalGrantedFeatures(required)\n if (!mounted) return\n setGrantedFeatures(granted)\n } catch (loadError) {\n if (!mounted) return\n console.error(`[usePortalDashboardWidgets] Failed to load widgets for spot ${spotId}:`, loadError)\n setError(loadError instanceof Error ? loadError.message : String(loadError))\n setWidgets([])\n } finally {\n if (mounted) setIsLoading(false)\n }\n }\n void load()\n return () => {\n mounted = false\n }\n }, [spotId])\n\n const grantedFeatureList = React.useMemo(() => Array.from(grantedFeatures), [grantedFeatures])\n\n const visibleWidgets = React.useMemo(\n () =>\n widgets.filter((widget) => {\n const required = widget.metadata.features ?? []\n return required.length === 0 || hasAllFeatures(grantedFeatureList, required)\n }),\n [widgets, grantedFeatureList],\n )\n\n return { widgets: visibleWidgets, isLoading, error }\n}\n"],
|
|
5
|
+
"mappings": ";AAEA,YAAY,WAAW;AAEvB,SAAS,mCAA+D;AACxE,SAAS,sBAAsB;AAC/B,SAAS,eAAe;AAOxB,SAAS,wBAAwB,SAA4C;AAC3E,QAAM,MAAM,oBAAI,IAAY;AAC5B,aAAW,UAAU,SAAS;AAC5B,eAAW,WAAW,OAAO,SAAS,YAAY,CAAC,GAAG;AACpD,UAAI,CAAC,WAAW,QAAQ,KAAK,EAAE,WAAW,EAAG;AAC7C,UAAI,IAAI,OAAO;AAAA,IACjB;AAAA,EACF;AACA,SAAO,MAAM,KAAK,GAAG;AACvB;AAEA,eAAe,0BAA0B,UAA0C;AACjF,MAAI,SAAS,WAAW,EAAG,QAAO,oBAAI,IAAI;AAC1C,MAAI;AACF,UAAM,EAAE,IAAI,QAAQ,KAAK,IAAI,MAAM,QAAoC,+CAA+C;AAAA,MACpH,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,KAAK,UAAU,EAAE,SAAS,CAAC;AAAA,IACnC,CAAC;AACD,QAAI,CAAC,MAAM,CAAC,MAAM,GAAI,QAAO,oBAAI,IAAI;AACrC,WAAO,IAAI,IAAI,KAAK,WAAW,CAAC,CAAC;AAAA,EACnC,QAAQ;AACN,WAAO,oBAAI,IAAI;AAAA,EACjB;AACF;AAcO,SAAS,0BAA0B,QAIxC;AACA,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAkC,CAAC,CAAC;AACxE,QAAM,CAAC,WAAW,YAAY,IAAI,MAAM,SAAS,IAAI;AACrD,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,IAAI;AAC5D,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,MAAM,SAAsB,oBAAI,IAAI,CAAC;AAEnF,QAAM,UAAU,MAAM;AACpB,QAAI,UAAU;AACd,UAAM,OAAO,YAAY;AACvB,UAAI;AACF,qBAAa,IAAI;AACjB,iBAAS,IAAI;AACb,cAAM,SAAS,MAAM,4BAA4B,MAAM;AACvD,YAAI,CAAC,QAAS;AAEd,cAAM,YAAY,OAAO,OAAO,CAAC,MAAM,OAAO,EAAE,WAAW,UAAU;AACrE,mBAAW,SAAS;AACpB,cAAM,WAAW,wBAAwB,SAAS;AAClD,cAAM,UAAU,MAAM,0BAA0B,QAAQ;AACxD,YAAI,CAAC,QAAS;AACd,2BAAmB,OAAO;AAAA,MAC5B,SAAS,WAAW;AAClB,YAAI,CAAC,QAAS;AACd,gBAAQ,MAAM,+DAA+D,MAAM,KAAK,SAAS;AACjG,iBAAS,qBAAqB,QAAQ,UAAU,UAAU,OAAO,SAAS,CAAC;AAC3E,mBAAW,CAAC,CAAC;AAAA,MACf,UAAE;AACA,YAAI,QAAS,cAAa,KAAK;AAAA,MACjC;AAAA,IACF;AACA,SAAK,KAAK;AACV,WAAO,MAAM;AACX,gBAAU;AAAA,IACZ;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,qBAAqB,MAAM,QAAQ,MAAM,MAAM,KAAK,eAAe,GAAG,CAAC,eAAe,CAAC;AAE7F,QAAM,iBAAiB,MAAM;AAAA,IAC3B,MACE,QAAQ,OAAO,CAAC,WAAW;AACzB,YAAM,WAAW,OAAO,SAAS,YAAY,CAAC;AAC9C,aAAO,SAAS,WAAW,KAAK,eAAe,oBAAoB,QAAQ;AAAA,IAC7E,CAAC;AAAA,IACH,CAAC,SAAS,kBAAkB;AAAA,EAC9B;AAEA,SAAO,EAAE,SAAS,gBAAgB,WAAW,MAAM;AACrD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { hasAllFeatures } from "@open-mercato/shared/security/features";
|
|
2
|
+
function isPortalPattern(pattern) {
|
|
3
|
+
if (!pattern) return false;
|
|
4
|
+
return pattern.startsWith("/[orgSlug]/portal/") || pattern === "/[orgSlug]/portal";
|
|
5
|
+
}
|
|
6
|
+
function hasNoUnresolvedParams(href) {
|
|
7
|
+
return !href.includes("[");
|
|
8
|
+
}
|
|
9
|
+
function resolveHref(pattern, orgSlug) {
|
|
10
|
+
return pattern.replace("[orgSlug]", orgSlug);
|
|
11
|
+
}
|
|
12
|
+
function pickGroup(group) {
|
|
13
|
+
if (group === "main" || group === "account") return group;
|
|
14
|
+
return "main";
|
|
15
|
+
}
|
|
16
|
+
function buildPortalNav({
|
|
17
|
+
routes,
|
|
18
|
+
orgSlug,
|
|
19
|
+
grantedFeatures,
|
|
20
|
+
isPortalAdmin = false
|
|
21
|
+
}) {
|
|
22
|
+
const mainItems = [];
|
|
23
|
+
const accountItems = [];
|
|
24
|
+
for (const route of routes) {
|
|
25
|
+
const pattern = route.pattern ?? route.path;
|
|
26
|
+
if (!isPortalPattern(pattern)) continue;
|
|
27
|
+
if (route.navHidden) continue;
|
|
28
|
+
const nav = route.nav;
|
|
29
|
+
if (!nav || typeof nav.label !== "string" || nav.label.length === 0) continue;
|
|
30
|
+
const requireFeatures = route.requireCustomerFeatures ?? [];
|
|
31
|
+
if (!isPortalAdmin && requireFeatures.length) {
|
|
32
|
+
if (!hasAllFeatures(grantedFeatures, requireFeatures)) continue;
|
|
33
|
+
}
|
|
34
|
+
const href = resolveHref(pattern, orgSlug);
|
|
35
|
+
if (!hasNoUnresolvedParams(href)) continue;
|
|
36
|
+
const group = pickGroup(nav.group);
|
|
37
|
+
const item = {
|
|
38
|
+
id: `portal-nav:${pattern}`,
|
|
39
|
+
label: nav.label,
|
|
40
|
+
labelKey: nav.labelKey,
|
|
41
|
+
href,
|
|
42
|
+
icon: nav.icon,
|
|
43
|
+
order: typeof nav.order === "number" ? nav.order : 100
|
|
44
|
+
};
|
|
45
|
+
if (group === "account") accountItems.push(item);
|
|
46
|
+
else mainItems.push(item);
|
|
47
|
+
}
|
|
48
|
+
const sortItems = (items) => items.sort((a, b) => {
|
|
49
|
+
if (a.order !== b.order) return a.order - b.order;
|
|
50
|
+
return a.label.localeCompare(b.label);
|
|
51
|
+
});
|
|
52
|
+
sortItems(mainItems);
|
|
53
|
+
sortItems(accountItems);
|
|
54
|
+
const groups = [];
|
|
55
|
+
if (mainItems.length) groups.push({ id: "main", items: mainItems });
|
|
56
|
+
if (accountItems.length) groups.push({ id: "account", items: accountItems });
|
|
57
|
+
return groups;
|
|
58
|
+
}
|
|
59
|
+
function mergePortalSidebarGroupsWithInjected(discovered, injected) {
|
|
60
|
+
const mergeGroup = (base, extra) => {
|
|
61
|
+
const knownIds = new Set(base.map((item) => item.id));
|
|
62
|
+
const knownHrefs = new Set(base.map((item) => item.href).filter((href) => Boolean(href)));
|
|
63
|
+
const merged = [...base];
|
|
64
|
+
for (const item of extra) {
|
|
65
|
+
if (knownIds.has(item.id)) continue;
|
|
66
|
+
if (item.href && knownHrefs.has(item.href)) continue;
|
|
67
|
+
merged.push(item);
|
|
68
|
+
knownIds.add(item.id);
|
|
69
|
+
if (item.href) knownHrefs.add(item.href);
|
|
70
|
+
}
|
|
71
|
+
return merged;
|
|
72
|
+
};
|
|
73
|
+
const mainBase = discovered.find((g) => g.id === "main")?.items ?? [];
|
|
74
|
+
const accountBase = discovered.find((g) => g.id === "account")?.items ?? [];
|
|
75
|
+
return {
|
|
76
|
+
main: mergeGroup(mainBase, injected.main),
|
|
77
|
+
account: mergeGroup(accountBase, injected.account)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export {
|
|
81
|
+
buildPortalNav,
|
|
82
|
+
mergePortalSidebarGroupsWithInjected
|
|
83
|
+
};
|
|
84
|
+
//# sourceMappingURL=nav.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/portal/utils/nav.ts"],
|
|
4
|
+
"sourcesContent": ["import type { FrontendRouteManifestEntry } from '@open-mercato/shared/modules/registry'\nimport { hasAllFeatures } from '@open-mercato/shared/security/features'\n\nexport type PortalNavGroupId = 'main' | 'account'\n\nexport type PortalNavItem = {\n id: string\n label: string\n labelKey?: string\n href: string\n icon?: string\n order: number\n}\n\nexport type PortalNavGroup = {\n id: PortalNavGroupId\n items: PortalNavItem[]\n}\n\nexport type BuildPortalNavOptions = {\n /** Route manifest to inspect (typically `getFrontendRouteManifests()`). */\n routes: readonly FrontendRouteManifestEntry[]\n /** Current customer org slug \u2014 substituted into `[orgSlug]` patterns. */\n orgSlug: string\n /** Feature strings granted to the current customer (may include wildcards). */\n grantedFeatures: readonly string[]\n /** If true, bypass feature checks (portal admin). Defaults to false. */\n isPortalAdmin?: boolean\n}\n\nfunction isPortalPattern(pattern: string | undefined): pattern is string {\n if (!pattern) return false\n return pattern.startsWith('/[orgSlug]/portal/') || pattern === '/[orgSlug]/portal'\n}\n\nfunction hasNoUnresolvedParams(href: string): boolean {\n return !href.includes('[')\n}\n\nfunction resolveHref(pattern: string, orgSlug: string): string {\n return pattern.replace('[orgSlug]', orgSlug)\n}\n\nfunction pickGroup(group: unknown): PortalNavGroupId {\n if (group === 'main' || group === 'account') return group\n return 'main'\n}\n\n/**\n * Build the portal sidebar from the frontend route manifest.\n *\n * Mirrors `buildAdminNav()` for the portal surface: selects routes under\n * `/[orgSlug]/portal/*` that declare a `nav` block, applies\n * `requireCustomerFeatures` against the caller's grants (wildcards honored),\n * and returns ordered sidebar groups.\n *\n * Absence of `nav` on a metadata file means the page is routable but not\n * auto-listed \u2014 useful for detail/create pages.\n */\nexport function buildPortalNav({\n routes,\n orgSlug,\n grantedFeatures,\n isPortalAdmin = false,\n}: BuildPortalNavOptions): PortalNavGroup[] {\n const mainItems: PortalNavItem[] = []\n const accountItems: PortalNavItem[] = []\n\n for (const route of routes) {\n const pattern = route.pattern ?? route.path\n if (!isPortalPattern(pattern)) continue\n if (route.navHidden) continue\n const nav = route.nav\n if (!nav || typeof nav.label !== 'string' || nav.label.length === 0) continue\n\n const requireFeatures = route.requireCustomerFeatures ?? []\n if (!isPortalAdmin && requireFeatures.length) {\n if (!hasAllFeatures(grantedFeatures as string[], requireFeatures as string[])) continue\n }\n\n const href = resolveHref(pattern as string, orgSlug)\n if (!hasNoUnresolvedParams(href)) continue\n\n const group = pickGroup(nav.group)\n const item: PortalNavItem = {\n id: `portal-nav:${pattern}`,\n label: nav.label,\n labelKey: nav.labelKey,\n href,\n icon: nav.icon,\n order: typeof nav.order === 'number' ? nav.order : 100,\n }\n if (group === 'account') accountItems.push(item)\n else mainItems.push(item)\n }\n\n const sortItems = (items: PortalNavItem[]) =>\n items.sort((a, b) => {\n if (a.order !== b.order) return a.order - b.order\n return a.label.localeCompare(b.label)\n })\n\n sortItems(mainItems)\n sortItems(accountItems)\n\n const groups: PortalNavGroup[] = []\n if (mainItems.length) groups.push({ id: 'main', items: mainItems })\n if (accountItems.length) groups.push({ id: 'account', items: accountItems })\n return groups\n}\n\n/**\n * Merge sidebar groups from the portal nav endpoint with items contributed via\n * `usePortalInjectedMenuItems`. Auto-discovered entries take precedence \u2014\n * injected items with matching `id` or `href` are dropped as duplicates.\n */\nexport function mergePortalSidebarGroupsWithInjected<TInjected extends { id: string; href?: string }>(\n discovered: readonly PortalNavGroup[],\n injected: {\n main: readonly TInjected[]\n account: readonly TInjected[]\n },\n): {\n main: Array<PortalNavItem | TInjected>\n account: Array<PortalNavItem | TInjected>\n} {\n const mergeGroup = <T extends PortalNavItem | TInjected>(\n base: readonly PortalNavItem[],\n extra: readonly TInjected[],\n ): Array<PortalNavItem | TInjected> => {\n const knownIds = new Set(base.map((item) => item.id))\n const knownHrefs = new Set(base.map((item) => item.href).filter((href): href is string => Boolean(href)))\n const merged: Array<PortalNavItem | TInjected> = [...base]\n for (const item of extra) {\n if (knownIds.has(item.id)) continue\n if (item.href && knownHrefs.has(item.href)) continue\n merged.push(item)\n knownIds.add(item.id)\n if (item.href) knownHrefs.add(item.href)\n }\n return merged\n }\n\n const mainBase = discovered.find((g) => g.id === 'main')?.items ?? []\n const accountBase = discovered.find((g) => g.id === 'account')?.items ?? []\n return {\n main: mergeGroup(mainBase, injected.main),\n account: mergeGroup(accountBase, injected.account),\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -128,26 +128,30 @@
|
|
|
128
128
|
"@tanstack/react-virtual": "^3.13.23",
|
|
129
129
|
"date-fns": "^4.1.0",
|
|
130
130
|
"react-big-calendar": "^1.19.4",
|
|
131
|
-
"react-day-picker": "^9.
|
|
132
|
-
"recharts": "^
|
|
131
|
+
"react-day-picker": "^9.14.0",
|
|
132
|
+
"recharts": "^3.8.1"
|
|
133
133
|
},
|
|
134
134
|
"peerDependencies": {
|
|
135
|
-
"@open-mercato/shared": "0.
|
|
135
|
+
"@open-mercato/shared": "0.5.0",
|
|
136
136
|
"react": ">=18.0.0",
|
|
137
137
|
"react-dom": ">=18.0.0"
|
|
138
138
|
},
|
|
139
139
|
"devDependencies": {
|
|
140
|
-
"@open-mercato/shared": "0.
|
|
140
|
+
"@open-mercato/shared": "0.5.0",
|
|
141
141
|
"@testing-library/dom": "^10.4.1",
|
|
142
142
|
"@testing-library/jest-dom": "^6.9.1",
|
|
143
143
|
"@testing-library/react": "^16.3.1",
|
|
144
144
|
"@types/jest": "^30.0.0",
|
|
145
|
-
"jest": "^30.
|
|
146
|
-
"jest-environment-jsdom": "^30.
|
|
147
|
-
"ts-jest": "^29.4.
|
|
145
|
+
"jest": "^30.3.0",
|
|
146
|
+
"jest-environment-jsdom": "^30.3.0",
|
|
147
|
+
"ts-jest": "^29.4.9"
|
|
148
148
|
},
|
|
149
149
|
"publishConfig": {
|
|
150
150
|
"access": "public"
|
|
151
151
|
},
|
|
152
|
-
"
|
|
152
|
+
"repository": {
|
|
153
|
+
"type": "git",
|
|
154
|
+
"url": "https://github.com/open-mercato/open-mercato",
|
|
155
|
+
"directory": "packages/ui"
|
|
156
|
+
}
|
|
153
157
|
}
|