@eintrek/erp-shell 0.1.45 → 0.1.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +3 -0
- package/dist/index.esm.js +132 -39
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +133 -36
- package/dist/index.js.map +1 -1
- package/dist/navigation/app-layout-shell.d.ts +14 -1
- package/dist/navigation/app-sidebar.d.ts +10 -1
- package/dist/navigation/chrome.d.ts +24 -0
- package/dist/navigation/nav-main.d.ts +4 -1
- package/dist/navigation/nav-user.d.ts +6 -1
- package/dist/navigation/navigation-types.d.ts +8 -0
- package/dist/navigation/organization-switcher.d.ts +7 -1
- package/dist/navigation/sidebar-help-card.d.ts +22 -0
- package/dist/navigation/workspace-brand.d.ts +19 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -795,14 +795,14 @@ function AppBreadcrumb({ labels = {}, homeLabel = "แดชบอร์ด", ro
|
|
|
795
795
|
if (segments.length === 0) {
|
|
796
796
|
return jsxRuntime.jsx("h1", { className: "text-xl font-semibold", children: homeLabel });
|
|
797
797
|
}
|
|
798
|
-
return (jsxRuntime.jsx(erpTheme.Breadcrumb, { children: jsxRuntime.jsxs(erpTheme.BreadcrumbList, { children: [jsxRuntime.jsx(erpTheme.BreadcrumbItem, { children: jsxRuntime.jsx(erpTheme.BreadcrumbLink, { asChild: true, children: jsxRuntime.jsx(Link, { href: "/", className: "font-thai text-[14px] font-normal text-
|
|
798
|
+
return (jsxRuntime.jsx(erpTheme.Breadcrumb, { children: jsxRuntime.jsxs(erpTheme.BreadcrumbList, { children: [jsxRuntime.jsx(erpTheme.BreadcrumbItem, { children: jsxRuntime.jsx(erpTheme.BreadcrumbLink, { asChild: true, children: jsxRuntime.jsx(Link, { href: "/", className: "font-thai text-[14px] font-normal text-[var(--header-fg-muted,rgb(255_255_255/0.8))] hover:text-[var(--header-fg,white)] transition-colors", children: rootLabel }) }) }), segments.map((segment, index) => {
|
|
799
799
|
const href = "/" + segments.slice(0, index + 1).join("/");
|
|
800
800
|
const isLast = index === segments.length - 1;
|
|
801
801
|
// A record id is not a place. Falling back to the raw segment put a
|
|
802
802
|
// 36-character uuid in the trail, which pushed everything before it
|
|
803
803
|
// off a phone and told the reader nothing they could use.
|
|
804
804
|
const label = labels[segment] ?? (isRecordID(segment) ? "รายละเอียด" : segment);
|
|
805
|
-
return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(erpTheme.BreadcrumbSeparator, { className: "text-
|
|
805
|
+
return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(erpTheme.BreadcrumbSeparator, { className: "text-[var(--header-fg-muted,rgb(255_255_255/0.8))]", children: jsxRuntime.jsx(lucideReact.ChevronRight, { strokeWidth: 2, className: "h-4 w-4" }) }), jsxRuntime.jsx(erpTheme.BreadcrumbItem, { children: isLast ? (jsxRuntime.jsx(erpTheme.BreadcrumbPage, { className: "font-thai text-[14px] font-normal text-[var(--header-fg,rgb(255_255_255/0.95))] leading-none", children: decodeURIComponent(label) })) : (jsxRuntime.jsx(erpTheme.BreadcrumbLink, { asChild: true, children: jsxRuntime.jsx(Link, { href: href, className: "font-thai text-[14px] font-normal text-[var(--header-fg-muted,rgb(255_255_255/0.8))] hover:text-[var(--header-fg,white)] transition-colors leading-none", children: decodeURIComponent(label) }) })) })] }, `${segment}-${index}`));
|
|
806
806
|
})] }) }));
|
|
807
807
|
}
|
|
808
808
|
/**
|
|
@@ -820,7 +820,7 @@ function isRecordID(segment) {
|
|
|
820
820
|
function isExternalHref(href) {
|
|
821
821
|
return /^https?:\/\//i.test(href) || href.startsWith("//");
|
|
822
822
|
}
|
|
823
|
-
function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref = "/select-organization", signInRedirectHref = "/auth/signin", systemLinks, showSelectOrganization = true, showLanguageSwitch = true, } = {}) {
|
|
823
|
+
function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref = "/select-organization", signInRedirectHref = "/auth/signin", systemLinks, showSelectOrganization = true, showLanguageSwitch = true, variant = "brand", } = {}) {
|
|
824
824
|
const { data: session, status } = react.useSession();
|
|
825
825
|
const router = navigation.useRouter();
|
|
826
826
|
const { theme, setTheme } = nextThemes.useTheme();
|
|
@@ -943,6 +943,10 @@ function NavUser({ organization, avatarSrc, profileHref, selectOrganizationHref
|
|
|
943
943
|
setShowThemeMenu(false);
|
|
944
944
|
setShowLanguageMenu(false);
|
|
945
945
|
}, className: "flex w-full items-center gap-3 px-3 py-2.5 text-sm text-sidebar-foreground hover:bg-sidebar-accent rounded-md transition-colors", children: [jsxRuntime.jsx(lucideReact.LayoutGrid, { className: "h-4 w-4" }), jsxRuntime.jsx("span", { className: "flex-1 text-left", children: "\u0E41\u0E2D\u0E1B\u0E1E\u0E25\u0E34\u0E40\u0E04\u0E0A\u0E31\u0E19" }), jsxRuntime.jsx(lucideReact.ChevronUp, { className: `h-4 w-4 transition-transform ${showAppsMenu ? "rotate-180" : ""}` })] }), showAppsMenu && (jsxRuntime.jsx("div", { className: "mt-1 ml-4 space-y-1", children: visibleSystemLinks.map(link => (jsxRuntime.jsxs("button", { type: "button", onClick: () => handleSystemLink(link.href), className: "flex w-full items-center gap-3 px-3 py-2 text-sm text-sidebar-foreground hover:bg-sidebar-accent rounded-md transition-colors", children: [link.icon ?? jsxRuntime.jsx(lucideReact.ExternalLink, { className: "h-4 w-4" }), jsxRuntime.jsx("span", { className: "flex-1 text-left", children: link.label }), link.active && (jsxRuntime.jsx("div", { className: "h-2 w-2 rounded-full bg-primary" }))] }, `${link.label}:${link.href}`))) }))] })), jsxRuntime.jsx("button", { onClick: handleSignOut, disabled: isSigningOut, className: "flex w-full items-center gap-3 px-3 py-2.5 text-sm text-sidebar-foreground hover:bg-sidebar-accent rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed mt-1", children: isSigningOut ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(lucideReact.Loader2, { className: "h-4 w-4 animate-spin" }), jsxRuntime.jsx("span", { className: "flex-1 text-left", children: "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E2D\u0E2D\u0E01\u0E08\u0E32\u0E01\u0E23\u0E30\u0E1A\u0E1A..." })] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(lucideReact.LogOut, { className: "h-4 w-4" }), jsxRuntime.jsx("span", { className: "flex-1 text-left", children: "\u0E2D\u0E2D\u0E01\u0E08\u0E32\u0E01\u0E23\u0E30\u0E1A\u0E1A" })] })) })] })] })) : null;
|
|
946
|
+
if (variant === "workspace") {
|
|
947
|
+
return (jsxRuntime.jsx("div", { className: "mt-3.5 border-t border-sidebar-border pt-3", children: jsxRuntime.jsxs("div", { className: "relative", children: [jsxRuntime.jsxs("button", { ref: buttonRef, onClick: () => setIsOpen(!isOpen), "aria-expanded": isOpen, className: "flex w-full items-center gap-[11px] rounded-lg px-3 py-1.5 text-left transition-colors hover:bg-sidebar-accent/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring", children: [jsxRuntime.jsxs(erpTheme.Avatar, { className: "size-[34px] shrink-0 rounded-full border-[3px] border-sidebar-accent", children: [user.avatar ? (jsxRuntime.jsx(erpTheme.AvatarImage, { src: user.avatar, alt: user.name })) : null, jsxRuntime.jsx(erpTheme.AvatarFallback, { className: "rounded-full bg-sidebar-accent text-sm font-medium text-sidebar-accent-foreground", children: getInitials(user.name) })] }), jsxRuntime.jsxs("span", { className: "grid min-w-0 flex-1 leading-tight", children: [jsxRuntime.jsx("strong", { className: "truncate text-[13px] font-semibold", title: user.name, children: user.name }), jsxRuntime.jsx("small", { className: "truncate text-[11px] text-muted-foreground", title: user.email, children: user.email })] }), jsxRuntime.jsx(lucideReact.ChevronsUpDown, { className: "size-3.5 shrink-0 text-muted-foreground" })] }), typeof window !== "undefined" &&
|
|
948
|
+
reactDom.createPortal(dropdownContent, document.body)] }) }));
|
|
949
|
+
}
|
|
946
950
|
return (jsxRuntime.jsx("div", { className: "p-2", children: jsxRuntime.jsxs("div", { className: "relative", children: [jsxRuntime.jsxs("button", { ref: buttonRef, onClick: () => setIsOpen(!isOpen), className: "flex w-full items-center gap-2 p-2 rounded-md transition-colors", children: [jsxRuntime.jsxs(erpTheme.Avatar, { className: "h-8 w-8 rounded-lg", children: [user.avatar ? (jsxRuntime.jsx(erpTheme.AvatarImage, { src: user.avatar, alt: user.name })) : null, jsxRuntime.jsx(erpTheme.AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground w-8 h-8", children: getInitials(user.name) })] }), jsxRuntime.jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [jsxRuntime.jsx("span", { className: "truncate font-medium", title: user.name, children: user.name }), jsxRuntime.jsx("span", { className: "truncate text-xs text-sidebar-foreground/70", title: user.email, children: user.email })] }), jsxRuntime.jsx(lucideReact.ChevronUp, { className: `ml-auto size-4 transition-transform ${isOpen ? "rotate-180" : ""}` })] }), typeof window !== "undefined" &&
|
|
947
951
|
reactDom.createPortal(dropdownContent, document.body)] }) }));
|
|
948
952
|
}
|
|
@@ -1074,7 +1078,15 @@ function isPathActive(pathname, url) {
|
|
|
1074
1078
|
return false;
|
|
1075
1079
|
}
|
|
1076
1080
|
|
|
1077
|
-
|
|
1081
|
+
/** "/O1/" and "/O1" are the same page; a query string never is. */
|
|
1082
|
+
function samePage(pathname, url) {
|
|
1083
|
+
if (url.includes("?"))
|
|
1084
|
+
return false;
|
|
1085
|
+
const strip = (p) => (p.length > 1 ? p.replace(/\/+$/, "") : p);
|
|
1086
|
+
return strip(pathname) === strip(url);
|
|
1087
|
+
}
|
|
1088
|
+
function NavMain({ items, orgCode, closeOnNavigation = false, approvalCounts, chrome = "brand", }) {
|
|
1089
|
+
const workspace = chrome === "workspace";
|
|
1078
1090
|
const params = navigation.useParams();
|
|
1079
1091
|
const pathname = navigation.usePathname();
|
|
1080
1092
|
const { setOpen } = useSidebar();
|
|
@@ -1083,7 +1095,11 @@ function NavMain({ items, orgCode, closeOnNavigation = false, approvalCounts, })
|
|
|
1083
1095
|
setOpen(false);
|
|
1084
1096
|
};
|
|
1085
1097
|
const code = orgCode ?? params?.code;
|
|
1086
|
-
return (jsxRuntime.jsx(SidebarGroup, { "data-sidebar": true, children: jsxRuntime.jsx(SidebarMenu, { children: items.map(item => {
|
|
1098
|
+
return (jsxRuntime.jsx(SidebarGroup, { "data-sidebar": true, className: workspace ? "p-0" : undefined, children: jsxRuntime.jsx(SidebarMenu, { className: workspace ? "gap-[3px]" : undefined, children: items.map((item, index) => {
|
|
1099
|
+
const groupHeading = workspace &&
|
|
1100
|
+
item.group &&
|
|
1101
|
+
item.group !== items[index - 1]?.group ? (jsxRuntime.jsx("li", { className: "list-none px-[15px] pb-2.5 text-xs text-muted-foreground" +
|
|
1102
|
+
(index > 0 ? " pt-8" : ""), children: item.group }, `group-${item.id}`)) : null;
|
|
1087
1103
|
const itemUrl = buildUrlWithCode(code, item.url);
|
|
1088
1104
|
// Most specific (longest) matching sub-item URL so only one link is active.
|
|
1089
1105
|
const activeSubItemUrl = item.subModules?.reduce((best, subItem) => {
|
|
@@ -1104,30 +1120,43 @@ function NavMain({ items, orgCode, closeOnNavigation = false, approvalCounts, })
|
|
|
1104
1120
|
isPathActive(pathname, subPage.url)));
|
|
1105
1121
|
// Parent only active when path matches its URL exactly AND no child
|
|
1106
1122
|
// is active (so parent+child don't both light up).
|
|
1107
|
-
const isItemActive = pathname === itemUrl &&
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1123
|
+
const isItemActive = (workspace ? samePage(pathname, itemUrl) : pathname === itemUrl) &&
|
|
1124
|
+
!hasActiveSubItem;
|
|
1125
|
+
const IconComp = item.icon;
|
|
1126
|
+
const workspaceButton = workspace
|
|
1127
|
+
? "h-auto gap-3 rounded-[10px] px-3.5 py-[11px] text-sm " +
|
|
1128
|
+
(isItemActive
|
|
1129
|
+
? "bg-sidebar-accent font-bold text-sidebar-accent-foreground"
|
|
1130
|
+
: "font-medium text-sidebar-foreground/70")
|
|
1131
|
+
: "";
|
|
1132
|
+
const icon = workspace && IconComp ? (jsxRuntime.jsx(IconComp, { className: "size-[18px] shrink-0" })) : null;
|
|
1133
|
+
const badge = workspace && item.badge !== undefined && item.badge !== "" ? (jsxRuntime.jsx("span", { className: "ml-auto min-w-5 rounded-[5px] bg-primary/15 px-1 text-center text-[10px] leading-5 font-bold tabular-nums text-primary", children: item.badge })) : null;
|
|
1134
|
+
return (jsxRuntime.jsxs(React__namespace.Fragment, { children: [groupHeading, jsxRuntime.jsx(erpTheme.Collapsible, { asChild: true, defaultOpen: isItemActive || hasActiveSubItem, className: "group/collapsible", children: jsxRuntime.jsxs(SidebarMenuItem, { children: [jsxRuntime.jsx(erpTheme.CollapsibleTrigger, { asChild: true, children: jsxRuntime.jsx(SidebarMenuButton, { asChild: !item.subModules || item.subModules.length === 0, isActive: isItemActive, className: "data-[state=open]:text-sidebar-accent-foreground " +
|
|
1135
|
+
workspaceButton, children: item.subModules && item.subModules.length > 0 ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [icon, jsxRuntime.jsx("span", { id: item.id, children: item.titleTh }), jsxRuntime.jsx(lucideReact.ChevronRight, { className: "ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" })] })) : (jsxRuntime.jsxs(Link, { href: itemUrl, prefetch: false, className: "flex items-center w-full min-w-0 flex-1 " +
|
|
1136
|
+
(workspace ? "gap-3" : "gap-2"), onClick: handleNavLinkClick, "aria-current": isItemActive ? "page" : undefined, children: [icon, jsxRuntime.jsx("span", { id: item.id, className: "flex-1 truncate", title: item.titleTh, children: item.titleTh }), badge] })) }) }), item.subModules && item.subModules.length > 0 && (jsxRuntime.jsx(erpTheme.CollapsibleContent, { children: jsxRuntime.jsx(SidebarMenuSub, { className: workspace
|
|
1137
|
+
? "mx-0 ml-[23px] mt-1 border-l border-sidebar-border pl-2.5 [&_a]:text-[13px] [&_svg]:!size-4"
|
|
1138
|
+
: undefined, children: item.subModules.map(subItem => {
|
|
1139
|
+
const subItemUrl = buildUrlWithCode(code, subItem.url);
|
|
1140
|
+
const isSubItemActive = subItemUrl === activeSubItemUrl;
|
|
1141
|
+
const SubIcon = subItem.icon;
|
|
1142
|
+
const approvalCount = approvalCounts && subItem.approvalCountKey
|
|
1143
|
+
? approvalCounts[subItem.approvalCountKey] ?? 0
|
|
1144
|
+
: 0;
|
|
1145
|
+
if (subItem.isCollapsible &&
|
|
1146
|
+
subItem.subPages &&
|
|
1147
|
+
subItem.subPages.length > 0) {
|
|
1148
|
+
const hasActiveSubPage = subItem.subPages.some(subPage => isPathActive(pathname, buildUrlWithCode(code, subPage.url)));
|
|
1149
|
+
return (jsxRuntime.jsx(erpTheme.Collapsible, { asChild: true, defaultOpen: isSubItemActive || hasActiveSubPage, className: "group/sub-collapsible", children: jsxRuntime.jsxs(SidebarMenuSubItem, { children: [jsxRuntime.jsx(erpTheme.CollapsibleTrigger, { asChild: true, children: jsxRuntime.jsxs(SidebarMenuSubButton, { className: "w-full", isActive: isSubItemActive || hasActiveSubPage, children: [SubIcon && (jsxRuntime.jsx(SubIcon, { className: "sidebar-submodule-icon flex-shrink-0" })), jsxRuntime.jsx("span", { className: "flex-1 truncate", title: subItem.titleTh, children: subItem.titleTh }), jsxRuntime.jsx(lucideReact.ChevronDown, { className: "ml-auto size-4 transition-transform duration-200 group-data-[state=open]/sub-collapsible:rotate-180" })] }) }), jsxRuntime.jsx(erpTheme.CollapsibleContent, { children: jsxRuntime.jsx(SidebarMenuSub, { className: "ml-4", children: subItem.subPages.map(subPage => {
|
|
1150
|
+
const subPageUrl = buildUrlWithCode(code, subPage.url);
|
|
1151
|
+
const isSubPageActive = isPathActive(pathname, subPageUrl);
|
|
1152
|
+
const SubPageIcon = subPage.icon;
|
|
1153
|
+
return (jsxRuntime.jsx(SidebarMenuSubItem, { children: jsxRuntime.jsx(SidebarMenuSubButton, { asChild: true, className: `font-thai w-full text-sm ${isSubPageActive
|
|
1154
|
+
? "text-sidebar-accent-foreground font-medium"
|
|
1155
|
+
: "text-sidebar-foreground"}`, isActive: false, children: jsxRuntime.jsxs(Link, { href: subPageUrl, prefetch: false, className: "w-full flex items-center", onClick: handleNavLinkClick, children: [SubPageIcon && (jsxRuntime.jsx(SubPageIcon, { className: "sidebar-subpage-icon flex-shrink-0" })), jsxRuntime.jsx("span", { className: "flex-1 truncate", title: subPage.titleTh, children: subPage.titleTh })] }) }) }, subPage.id));
|
|
1156
|
+
}) }) })] }) }, subItem.id));
|
|
1157
|
+
}
|
|
1158
|
+
return (jsxRuntime.jsx(SidebarMenuSubItem, { children: jsxRuntime.jsx(SidebarMenuSubButton, { asChild: true, className: "w-full", isActive: isSubItemActive, children: jsxRuntime.jsxs(Link, { href: subItemUrl, prefetch: false, className: "w-full flex items-center", onClick: handleNavLinkClick, children: [SubIcon && (jsxRuntime.jsx(SubIcon, { className: "sidebar-submodule-icon flex-shrink-0" })), jsxRuntime.jsx("span", { className: "flex-1 truncate", title: subItem.titleTh, children: subItem.titleTh }), approvalCount > 0 && (jsxRuntime.jsx(erpTheme.Badge, { variant: "destructive", className: "ml-auto h-5 min-w-5 px-1.5 text-xs font-medium tabular-nums", children: approvalCount > 99 ? "99+" : approvalCount }))] }) }) }, subItem.id));
|
|
1159
|
+
}) }) }))] }) })] }, item.id));
|
|
1131
1160
|
}) }) }));
|
|
1132
1161
|
}
|
|
1133
1162
|
|
|
@@ -1135,14 +1164,16 @@ function OrgMark({ logoUrl, size = "md", }) {
|
|
|
1135
1164
|
const [failed, setFailed] = React.useState(false);
|
|
1136
1165
|
const box = size === "sm"
|
|
1137
1166
|
? "flex size-6 items-center justify-center rounded-md border overflow-hidden bg-background"
|
|
1138
|
-
:
|
|
1167
|
+
: size === "header"
|
|
1168
|
+
? "flex aspect-square size-8 shrink-0 items-center justify-center rounded-[9px] border border-[color:color-mix(in_oklab,var(--header-fg,currentColor)_28%,transparent)] bg-[color-mix(in_oklab,var(--header-fg,currentColor)_10%,transparent)] text-[var(--header-fg,currentColor)] overflow-hidden max-sm:size-7"
|
|
1169
|
+
: "flex aspect-square size-8 shrink-0 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground overflow-hidden";
|
|
1139
1170
|
const icon = size === "sm" ? "size-3.5 shrink-0" : "size-4";
|
|
1140
1171
|
if (logoUrl && !failed) {
|
|
1141
1172
|
return (jsxRuntime.jsx("div", { className: erpTheme.cn(box, size === "md" && "bg-white"), children: jsxRuntime.jsx("img", { src: logoUrl, alt: "", className: "size-full object-contain p-0.5", onError: () => setFailed(true) }) }));
|
|
1142
1173
|
}
|
|
1143
1174
|
return (jsxRuntime.jsx("div", { className: box, children: jsxRuntime.jsx(lucideReact.Building2, { className: icon }) }));
|
|
1144
1175
|
}
|
|
1145
|
-
function OrganizationSwitcher({ activeOrganization, isContextLoading = false, organizations, isLoadingOrgs = false, setOrganizationCookie, registerOrganizationHref = "/register-organization", className, }) {
|
|
1176
|
+
function OrganizationSwitcher({ activeOrganization, isContextLoading = false, organizations, isLoadingOrgs = false, setOrganizationCookie, registerOrganizationHref = "/register-organization", placement = "sidebar", className, }) {
|
|
1146
1177
|
const { isMobile } = useSidebarToggle();
|
|
1147
1178
|
const router = navigation.useRouter();
|
|
1148
1179
|
const params = navigation.useParams();
|
|
@@ -1183,6 +1214,14 @@ function OrganizationSwitcher({ activeOrganization, isContextLoading = false, or
|
|
|
1183
1214
|
});
|
|
1184
1215
|
};
|
|
1185
1216
|
const isLoading = isLoadingOrgs || isContextLoading;
|
|
1217
|
+
if (placement === "header") {
|
|
1218
|
+
const face = (interactive) => (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(OrgMark, { logoUrl: activeOrganization?.logoUrl, size: "header" }), jsxRuntime.jsxs("span", { className: "min-w-0 text-left", children: [jsxRuntime.jsx("small", { className: "block text-[10px] leading-tight text-[var(--header-fg-muted,currentColor)] max-sm:text-[9px]", children: "\u0E2D\u0E07\u0E04\u0E4C\u0E01\u0E23\u0E17\u0E35\u0E48\u0E01\u0E33\u0E25\u0E31\u0E07\u0E43\u0E0A\u0E49\u0E07\u0E32\u0E19" }), isLoading ? (jsxRuntime.jsx(erpTheme.Skeleton, { className: "mt-1 h-3.5 w-28" })) : (jsxRuntime.jsx("strong", { className: "block max-w-[220px] truncate text-xs font-semibold text-[var(--header-fg,currentColor)] max-sm:max-w-[140px] max-sm:text-[11px]", children: activeOrganization?.name || "เลือกองค์กร" }))] }), interactive ? (isPending ? (jsxRuntime.jsx(lucideReact.Loader2, { className: "ml-1 size-3.5 shrink-0 animate-spin text-[var(--header-fg-muted,currentColor)]" })) : (jsxRuntime.jsx(lucideReact.ChevronsUpDown, { className: "ml-1 size-3.5 shrink-0 text-[var(--header-fg-muted,currentColor)]" }))) : null] }));
|
|
1219
|
+
const faceClass = erpTheme.cn("flex items-center gap-2.5 rounded-lg py-1.5 max-sm:gap-2", className);
|
|
1220
|
+
if (isLoading || organizations.length <= 1) {
|
|
1221
|
+
return jsxRuntime.jsx("div", { className: faceClass, children: face(false) });
|
|
1222
|
+
}
|
|
1223
|
+
return (jsxRuntime.jsxs(erpTheme.DropdownMenu, { children: [jsxRuntime.jsx(erpTheme.DropdownMenuTrigger, { className: erpTheme.cn(faceClass, "px-1.5 outline-none transition-colors hover:bg-[var(--header-hover,var(--accent))] focus-visible:ring-2 focus-visible:ring-[var(--header-fg-muted,var(--ring))] data-[state=open]:bg-[var(--header-hover,var(--accent))]"), "aria-label": "\u0E40\u0E1B\u0E25\u0E35\u0E48\u0E22\u0E19\u0E2D\u0E07\u0E04\u0E4C\u0E01\u0E23", children: face(true) }), jsxRuntime.jsxs(erpTheme.DropdownMenuContent, { className: "min-w-64 rounded-lg", align: "end", side: "bottom", sideOffset: 6, children: [jsxRuntime.jsx(erpTheme.DropdownMenuLabel, { className: "text-xs text-muted-foreground", children: "\u0E2D\u0E07\u0E04\u0E4C\u0E01\u0E23" }), organizations.map(org => (jsxRuntime.jsxs(erpTheme.DropdownMenuItem, { onClick: () => handleOrganizationChange(org), disabled: isPending, className: erpTheme.cn("gap-2 p-2 cursor-pointer", String(activeOrganization?.id) === String(org.id) && "bg-accent"), children: [jsxRuntime.jsx(OrgMark, { logoUrl: org.logoUrl, size: "sm" }), jsxRuntime.jsx("span", { className: "truncate", children: org.name })] }, org.id))), jsxRuntime.jsx(erpTheme.DropdownMenuSeparator, {}), jsxRuntime.jsxs(erpTheme.DropdownMenuItem, { className: "gap-2 p-2 cursor-pointer", onClick: () => router.push(registerOrganizationHref), children: [jsxRuntime.jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-transparent", children: jsxRuntime.jsx(lucideReact.Plus, { className: "size-4" }) }), jsxRuntime.jsx("span", { className: "text-muted-foreground font-medium", children: "\u0E40\u0E1E\u0E34\u0E48\u0E21\u0E2D\u0E07\u0E04\u0E4C\u0E01\u0E23" })] })] })] }));
|
|
1224
|
+
}
|
|
1186
1225
|
if (isLoading) {
|
|
1187
1226
|
return (jsxRuntime.jsx(SidebarMenu, { className: className, children: jsxRuntime.jsx(SidebarMenuItem, { children: jsxRuntime.jsxs("div", { className: "flex items-center gap-2 p-2", children: [jsxRuntime.jsx(erpTheme.Skeleton, { className: "size-8 rounded-lg" }), jsxRuntime.jsx(erpTheme.Skeleton, { className: "h-4 w-24" })] }) }) }));
|
|
1188
1227
|
}
|
|
@@ -1197,7 +1236,7 @@ function SidebarHeader({ children, className }) {
|
|
|
1197
1236
|
return jsxRuntime.jsx("div", { className: erpTheme.cn("px-2 py-3", className), children: children });
|
|
1198
1237
|
}
|
|
1199
1238
|
|
|
1200
|
-
function AppSidebar({ modules, tools, activeModule, orgCode, approvalCounts, closeOnNavigation = false, header, footer, className, }) {
|
|
1239
|
+
function AppSidebar({ modules, tools, activeModule, orgCode, approvalCounts, closeOnNavigation = false, header, footer, bottom, chrome = "brand", className, }) {
|
|
1201
1240
|
const { open } = useSidebar();
|
|
1202
1241
|
const modulesWithActiveState = React__namespace.useMemo(() => modules.map(m => ({
|
|
1203
1242
|
...m,
|
|
@@ -1205,9 +1244,27 @@ function AppSidebar({ modules, tools, activeModule, orgCode, approvalCounts, clo
|
|
|
1205
1244
|
})), [modules, activeModule]);
|
|
1206
1245
|
if (!open)
|
|
1207
1246
|
return null;
|
|
1247
|
+
if (chrome === "workspace") {
|
|
1248
|
+
return (jsxRuntime.jsxs("div", { className: erpTheme.cn("flex h-screen w-[244px] shrink-0 flex-col overflow-hidden border-r border-sidebar-border bg-sidebar text-sidebar-foreground",
|
|
1249
|
+
// As a drawer (≤1024px) it floats over the page and needs the lift.
|
|
1250
|
+
"max-[1024px]:shadow-2xl", className), children: [header ? jsxRuntime.jsx("div", { className: "shrink-0 px-[18px] pt-8 pb-6", children: header }) : null, jsxRuntime.jsxs(SidebarContent, { className: "flex-1 overflow-y-auto px-[18px]", children: [jsxRuntime.jsx(NavMain, { items: modulesWithActiveState, orgCode: orgCode, approvalCounts: approvalCounts, closeOnNavigation: closeOnNavigation, chrome: "workspace" }), jsxRuntime.jsx(NavTools, { tools: tools, closeOnNavigation: closeOnNavigation })] }), bottom ? jsxRuntime.jsx("div", { className: "shrink-0 px-[18px] pt-6", children: bottom }) : null, jsxRuntime.jsx("div", { className: "shrink-0 px-[18px] pb-3", children: footer })] }));
|
|
1251
|
+
}
|
|
1208
1252
|
return (jsxRuntime.jsxs("div", { className: erpTheme.cn("bg-sidebar shrink-0 flex flex-col overflow-hidden shadow-2xl h-screen w-[var(--sidebar-width-mobile)]", "!w-76 xl:rounded-b-4xl xl:rounded-tl-4xl xl:m-5 xl:h-[calc(100vh-2.5rem)]", className), children: [jsxRuntime.jsx(SidebarHeader, { children: header }), jsxRuntime.jsxs(SidebarContent, { className: "pl-2 pr-2 flex-1 overflow-y-auto", children: [jsxRuntime.jsx(NavMain, { items: modulesWithActiveState, orgCode: orgCode, approvalCounts: approvalCounts, closeOnNavigation: closeOnNavigation }), jsxRuntime.jsx(NavTools, { tools: tools, closeOnNavigation: closeOnNavigation })] }), jsxRuntime.jsx(SidebarFooter, { className: "pl-2 pr-2", children: footer })] }));
|
|
1209
1253
|
}
|
|
1210
1254
|
|
|
1255
|
+
/**
|
|
1256
|
+
* Header ink as CSS variables. Header-borne pieces — breadcrumb, sidebar
|
|
1257
|
+
* trigger, the org switcher, the app's own page title — read these, with
|
|
1258
|
+
* white as the fallback, so the brand chrome looks exactly as before.
|
|
1259
|
+
*/
|
|
1260
|
+
const WORKSPACE_HEADER_VARS = {
|
|
1261
|
+
"--header-fg": "#fff",
|
|
1262
|
+
"--header-fg-muted": "rgb(255 255 255 / 0.78)",
|
|
1263
|
+
"--header-hover": "rgb(255 255 255 / 0.12)",
|
|
1264
|
+
"--header-title-px": "0px",
|
|
1265
|
+
"--header-title-py": "0px",
|
|
1266
|
+
};
|
|
1267
|
+
|
|
1211
1268
|
/**
|
|
1212
1269
|
* Top-level app chrome: sticky header, sidebar slot, content gutter and the
|
|
1213
1270
|
* session/org-context plumbing that both Accounting and HR need. Apps wrap
|
|
@@ -1220,7 +1277,7 @@ function AppSidebar({ modules, tools, activeModule, orgCode, approvalCounts, clo
|
|
|
1220
1277
|
* an erp-shell bump — domain components must NOT add their own root padding
|
|
1221
1278
|
* or content gets indented twice.
|
|
1222
1279
|
*/
|
|
1223
|
-
function AppLayoutShell({ children, sidebar, loadingComponent, breadcrumb, permissionProvider, organizationId, setOrganizationId, organizations, signInUrl, selectOrganizationUrl, }) {
|
|
1280
|
+
function AppLayoutShell({ children, sidebar, loadingComponent, breadcrumb, permissionProvider, organizationId, setOrganizationId, organizations, signInUrl, selectOrganizationUrl, chrome = "brand", headerEnd, mobileBrand, }) {
|
|
1224
1281
|
const { data: session, status } = react.useSession();
|
|
1225
1282
|
const { open, setOpen } = useSidebar();
|
|
1226
1283
|
const router = navigation.useRouter();
|
|
@@ -1343,10 +1400,15 @@ function AppLayoutShell({ children, sidebar, loadingComponent, breadcrumb, permi
|
|
|
1343
1400
|
}
|
|
1344
1401
|
// When the sidebar is docked-open it consumes 21.5rem on the left; otherwise
|
|
1345
1402
|
// the content takes the full width and we add a left gutter inside.
|
|
1346
|
-
const
|
|
1347
|
-
const
|
|
1403
|
+
const workspace = chrome === "workspace";
|
|
1404
|
+
const contentMargin = open && !isSidebarModalLayout
|
|
1405
|
+
? workspace
|
|
1406
|
+
? "ml-[244px]"
|
|
1407
|
+
: "ml-[21.5rem]"
|
|
1408
|
+
: "";
|
|
1409
|
+
const needsLeftGutter = workspace || !(open && !isSidebarModalLayout);
|
|
1348
1410
|
const wrap = permissionProvider ?? ((c) => c);
|
|
1349
|
-
return (jsxRuntime.jsxs("div", { className: "flex min-h-screen w-full bg-background", children: [open && (jsxRuntime.jsx("div", { className: "fixed left-0 top-0 z-50 h-screen", children: sidebar })), open && isSidebarModalLayout && (jsxRuntime.jsx("button", { type: "button", "aria-label": "\u0E1B\u0E34\u0E14\u0E40\u0E21\u0E19\u0E39", className: "fixed inset-0 z-40 bg-black/50", onClick: () => setOpen(false) })), jsxRuntime.jsx("div", { className: cx("relative flex min-w-0 flex-1 flex-col transition-all", contentMargin), children: jsxRuntime.jsxs("main", { className: "flex flex-1 flex-col min-h-screen gap-4", children: [jsxRuntime.jsxs("div", { className: "sticky top-0 z-30 shrink-0 w-full", children: [jsxRuntime.jsx(HeaderBackground, {}), jsxRuntime.jsxs("header", { className: "relative shrink-0 gap-2 py-4 h-auto min-h-[123.5px] w-full z-10", children: [jsxRuntime.jsxs("div", { className: "flex w-full items-center gap-2 px-3 sm:px-4 lg:px-6", children: [jsxRuntime.jsx(SidebarTrigger, { className: "shrink-0 text-white hover:bg-white/10" }), jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: breadcrumb })] }), jsxRuntime.jsx("div", { id: "header-title" })] })] }), jsxRuntime.jsx(React.Suspense, { fallback: jsxRuntime.jsx("div", { className: "relative z-10 min-h-[calc(100vh-8rem)]", children: jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center", children: jsxRuntime.jsxs("div", { className: "space-y-4 text-center", children: [jsxRuntime.jsx("div", { className: "mx-auto h-8 w-8 animate-spin rounded-full border-b-2 border-primary" }), jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E42\u0E2B\u0E25\u0E14..." })] }) }) }), children: jsxRuntime.jsx("div", { className: cx("relative z-10 pr-6", needsLeftGutter && "pl-6"), children: orgContextIsStale ? loadingComponent : wrap(children) }, `org-${contentOrgKey}`) })] }) })] }));
|
|
1411
|
+
return (jsxRuntime.jsxs("div", { className: "flex min-h-screen w-full bg-background", children: [open && (jsxRuntime.jsx("div", { className: "fixed left-0 top-0 z-50 h-screen", children: sidebar })), open && isSidebarModalLayout && (jsxRuntime.jsx("button", { type: "button", "aria-label": "\u0E1B\u0E34\u0E14\u0E40\u0E21\u0E19\u0E39", className: "fixed inset-0 z-40 bg-black/50", onClick: () => setOpen(false) })), jsxRuntime.jsx("div", { className: cx("relative flex min-w-0 flex-1 flex-col transition-all", contentMargin), children: jsxRuntime.jsxs("main", { className: cx("flex flex-1 flex-col min-h-screen", workspace ? "gap-0" : "gap-4"), children: [workspace ? (jsxRuntime.jsxs("div", { className: "sticky top-0 z-30 shrink-0 w-full", style: WORKSPACE_HEADER_VARS, children: [jsxRuntime.jsx(HeaderBackground, { className: "rounded-none" }), jsxRuntime.jsxs("header", { className: "relative z-10 flex min-h-[72px] w-full items-center gap-3 px-4 py-3 text-[var(--header-fg)] sm:px-6 lg:min-h-[88px] lg:px-10", children: [jsxRuntime.jsx(SidebarTrigger, { className: "shrink-0 text-[var(--header-fg-muted)] hover:bg-[var(--header-hover)] hover:text-[var(--header-fg)] [&_svg]:!text-current" }), isSidebarModalLayout && mobileBrand ? (jsxRuntime.jsx("div", { className: "shrink-0", children: mobileBrand })) : null, jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-1 flex-col justify-center gap-0.5", children: [jsxRuntime.jsx("div", { className: "hidden min-w-0 sm:block", children: breadcrumb }), jsxRuntime.jsx("div", { id: "header-title", className: "min-w-0" })] }), headerEnd ? (jsxRuntime.jsx("div", { className: "flex shrink-0 items-center gap-3 sm:gap-5", children: headerEnd })) : null] })] })) : (jsxRuntime.jsxs("div", { className: "sticky top-0 z-30 shrink-0 w-full", children: [jsxRuntime.jsx(HeaderBackground, {}), jsxRuntime.jsxs("header", { className: "relative shrink-0 gap-2 py-4 h-auto min-h-[123.5px] w-full z-10", children: [jsxRuntime.jsxs("div", { className: "flex w-full items-center gap-2 px-3 sm:px-4 lg:px-6", children: [jsxRuntime.jsx(SidebarTrigger, { className: "shrink-0 text-white hover:bg-white/10" }), jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: breadcrumb }), headerEnd ? (jsxRuntime.jsx("div", { className: "flex shrink-0 items-center gap-3", children: headerEnd })) : null] }), jsxRuntime.jsx("div", { id: "header-title" })] })] })), jsxRuntime.jsx(React.Suspense, { fallback: jsxRuntime.jsx("div", { className: "relative z-10 min-h-[calc(100vh-8rem)]", children: jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center", children: jsxRuntime.jsxs("div", { className: "space-y-4 text-center", children: [jsxRuntime.jsx("div", { className: "mx-auto h-8 w-8 animate-spin rounded-full border-b-2 border-primary" }), jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E42\u0E2B\u0E25\u0E14..." })] }) }) }), children: jsxRuntime.jsx("div", { className: cx("relative z-10 pr-6", needsLeftGutter && "pl-6", workspace && "pt-4 sm:pr-6 lg:pt-6 lg:pr-10 lg:pl-10"), children: orgContextIsStale ? loadingComponent : wrap(children) }, `org-${contentOrgKey}`) })] }) })] }));
|
|
1350
1412
|
}
|
|
1351
1413
|
/** Local tiny classnames helper — avoids pulling clsx/tailwind-merge into
|
|
1352
1414
|
* the shell just for the layout file. Falsy values are skipped. */
|
|
@@ -1354,6 +1416,37 @@ function cx(...parts) {
|
|
|
1354
1416
|
return parts.filter(Boolean).join(" ");
|
|
1355
1417
|
}
|
|
1356
1418
|
|
|
1419
|
+
/**
|
|
1420
|
+
* The product mark at the top of the workspace sidebar: the app icon, the
|
|
1421
|
+
* name, and a spaced caption beneath it.
|
|
1422
|
+
*/
|
|
1423
|
+
function WorkspaceBrand({ name, caption, mark, href, compact = false, className, }) {
|
|
1424
|
+
const body = (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { className: erpTheme.cn("grid shrink-0 place-items-center overflow-hidden rounded-[14px] [&>img]:size-full [&>img]:object-cover", compact ? "size-9 rounded-[11px]" : "size-11"), children: mark }), compact ? (jsxRuntime.jsx("span", { className: "sr-only", children: name })) : (jsxRuntime.jsxs("span", { className: "min-w-0", children: [jsxRuntime.jsx("span", { className: "block truncate text-[25px] leading-tight font-extrabold tracking-[-0.05em]", children: name }), caption ? (jsxRuntime.jsx("small", { className: "-mt-0.5 block truncate text-[9px] font-semibold tracking-[0.29em] text-muted-foreground", children: caption })) : null] }))] }));
|
|
1425
|
+
const cls = erpTheme.cn("flex items-center gap-[11px] rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", !compact && "px-4", className);
|
|
1426
|
+
return href ? (jsxRuntime.jsx(Link, { href: href, className: cls, "aria-label": compact ? name : undefined, children: body })) : (jsxRuntime.jsx("div", { className: cls, children: body }));
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function isExternal(href) {
|
|
1430
|
+
return /^https?:\/\//i.test(href);
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* The card at the foot of the workspace sidebar that points someone who
|
|
1434
|
+
* does not know where to start at the place that will tell them.
|
|
1435
|
+
*/
|
|
1436
|
+
function SidebarHelpCard({ title, description, actionLabel, href, icon, className, }) {
|
|
1437
|
+
const external = isExternal(href);
|
|
1438
|
+
const action = (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [actionLabel, jsxRuntime.jsx(lucideReact.ArrowRight, { className: "size-[15px]" })] }));
|
|
1439
|
+
const actionClass = "mt-3 flex w-full items-center justify-between text-xs font-semibold text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded";
|
|
1440
|
+
return (jsxRuntime.jsxs("div", { className: erpTheme.cn("mx-1.5 mb-5 rounded-[13px] border border-primary/10 bg-gradient-to-br from-primary/[0.06] to-primary/[0.02] px-3.5 py-4", className), children: [jsxRuntime.jsx("span", { className: "mb-[7px] block text-primary/70", children: icon ?? jsxRuntime.jsx(lucideReact.Compass, { className: "size-6", strokeWidth: 1.6 }) }), jsxRuntime.jsx("strong", { className: "block text-xs font-semibold", children: title }), description ? (jsxRuntime.jsx("p", { className: "mt-[7px] text-xs leading-[1.7] text-muted-foreground", children: description })) : null, external ? (jsxRuntime.jsx("a", { href: href, target: "_blank", rel: "noopener noreferrer", className: actionClass, children: action })) : (jsxRuntime.jsx(Link, { href: href, className: actionClass, children: action }))] }));
|
|
1441
|
+
}
|
|
1442
|
+
/** A quiet row under the help card — the manual, the support chat. */
|
|
1443
|
+
function SidebarHelpLink({ label, href, icon, className }) {
|
|
1444
|
+
const external = isExternal(href);
|
|
1445
|
+
const cls = erpTheme.cn("flex w-full items-center gap-3 rounded-[10px] px-3.5 py-[11px] text-xs font-medium text-sidebar-foreground/70 transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", className);
|
|
1446
|
+
const body = (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [icon ? jsxRuntime.jsx("span", { className: "shrink-0 [&>svg]:size-[18px]", children: icon }) : null, jsxRuntime.jsx("span", { className: "flex-1 truncate", children: label }), external ? jsxRuntime.jsx(lucideReact.ArrowUpRight, { className: "size-4 shrink-0" }) : null] }));
|
|
1447
|
+
return external ? (jsxRuntime.jsx("a", { href: href, target: "_blank", rel: "noopener noreferrer", className: cls, children: body })) : (jsxRuntime.jsx(Link, { href: href, className: cls, children: body }));
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1357
1450
|
/**
|
|
1358
1451
|
* Central "no access" screen shared by every policy check (route-level
|
|
1359
1452
|
* PermissionProvider and page-level PermissionGateShell). One look everywhere:
|
|
@@ -2193,10 +2286,14 @@ exports.SessionRefreshTrigger = SessionRefreshTrigger;
|
|
|
2193
2286
|
exports.SidebarContent = SidebarContent;
|
|
2194
2287
|
exports.SidebarFooter = SidebarFooter;
|
|
2195
2288
|
exports.SidebarHeader = SidebarHeader;
|
|
2289
|
+
exports.SidebarHelpCard = SidebarHelpCard;
|
|
2290
|
+
exports.SidebarHelpLink = SidebarHelpLink;
|
|
2196
2291
|
exports.SidebarProvider = SidebarProvider;
|
|
2197
2292
|
exports.SidebarTrigger = SidebarTrigger;
|
|
2198
2293
|
exports.TapMenu = TapMenu;
|
|
2199
2294
|
exports.ThemeProvider = ThemeProvider;
|
|
2295
|
+
exports.WORKSPACE_HEADER_VARS = WORKSPACE_HEADER_VARS;
|
|
2296
|
+
exports.WorkspaceBrand = WorkspaceBrand;
|
|
2200
2297
|
exports.axiosInstance = instance;
|
|
2201
2298
|
exports.buildSystemLinks = buildSystemLinks;
|
|
2202
2299
|
exports.buildUrlWithCode = buildUrlWithCode;
|