@object-ui/app-shell 3.3.1 → 4.0.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/CHANGELOG.md CHANGED
@@ -1,5 +1,65 @@
1
1
  # @object-ui/app-shell — Changelog
2
2
 
3
+ ## 4.0.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [a2d7023]
8
+ - Updated dependencies [f1ca238]
9
+ - Updated dependencies [de881ef]
10
+ - Updated dependencies [b2be122]
11
+ - @object-ui/components@3.4.0
12
+ - @object-ui/fields@3.4.0
13
+ - @object-ui/plugin-designer@3.4.0
14
+ - @object-ui/plugin-grid@3.4.0
15
+ - @object-ui/plugin-kanban@3.4.0
16
+ - @object-ui/types@3.4.0
17
+ - @object-ui/plugin-form@3.4.0
18
+ - @object-ui/plugin-calendar@3.4.0
19
+ - @object-ui/layout@3.4.0
20
+ - @object-ui/plugin-charts@3.4.0
21
+ - @object-ui/plugin-chatbot@3.4.0
22
+ - @object-ui/plugin-dashboard@3.4.0
23
+ - @object-ui/plugin-detail@3.4.0
24
+ - @object-ui/plugin-list@3.4.0
25
+ - @object-ui/plugin-report@3.4.0
26
+ - @object-ui/plugin-view@3.4.0
27
+ - @object-ui/auth@3.4.0
28
+ - @object-ui/collaboration@3.4.0
29
+ - @object-ui/core@3.4.0
30
+ - @object-ui/data-objectstack@3.4.0
31
+ - @object-ui/permissions@3.4.0
32
+ - @object-ui/react@3.4.0
33
+ - @object-ui/i18n@3.4.0
34
+
35
+ ## 3.3.2
36
+
37
+ ### Patch Changes
38
+
39
+ - @object-ui/types@3.3.2
40
+ - @object-ui/core@3.3.2
41
+ - @object-ui/i18n@3.3.2
42
+ - @object-ui/react@3.3.2
43
+ - @object-ui/components@3.3.2
44
+ - @object-ui/fields@3.3.2
45
+ - @object-ui/layout@3.3.2
46
+ - @object-ui/data-objectstack@3.3.2
47
+ - @object-ui/auth@3.3.2
48
+ - @object-ui/permissions@3.3.2
49
+ - @object-ui/plugin-calendar@3.3.2
50
+ - @object-ui/plugin-charts@3.3.2
51
+ - @object-ui/plugin-chatbot@3.3.2
52
+ - @object-ui/plugin-dashboard@3.3.2
53
+ - @object-ui/plugin-designer@3.3.2
54
+ - @object-ui/plugin-detail@3.3.2
55
+ - @object-ui/plugin-form@3.3.2
56
+ - @object-ui/plugin-grid@3.3.2
57
+ - @object-ui/plugin-kanban@3.3.2
58
+ - @object-ui/plugin-list@3.3.2
59
+ - @object-ui/plugin-report@3.3.2
60
+ - @object-ui/plugin-view@3.3.2
61
+ - @object-ui/collaboration@3.3.2
62
+
3
63
  ## 3.3.1
4
64
 
5
65
  ### Patch Changes
@@ -9,27 +9,13 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
9
9
  */
10
10
  import { useEffect, useState, useCallback } from 'react';
11
11
  import { useNavigate, useParams } from 'react-router-dom';
12
- import * as LucideIcons from 'lucide-react';
13
12
  import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from '@object-ui/components';
14
- import { LayoutDashboard, FileText, BarChart3, Database, Moon, Sun, Monitor, Search, Plus, } from 'lucide-react';
13
+ import { LayoutDashboard, FileText, BarChart3, Moon, Sun, Monitor, Search, Plus, } from 'lucide-react';
15
14
  import { useTheme } from './ThemeProvider';
16
15
  import { useExpressionContext, evaluateVisibility } from '../providers/ExpressionProvider';
17
16
  import { useObjectTranslation } from '@object-ui/i18n';
18
17
  import { resolveI18nLabel } from '../utils';
19
- /** Resolve a Lucide icon by name (kebab-case or PascalCase) */
20
- function getIcon(name) {
21
- if (!name)
22
- return Database;
23
- if (LucideIcons[name])
24
- return LucideIcons[name];
25
- const pascal = name
26
- .split('-')
27
- .map(p => p.charAt(0).toUpperCase() + p.slice(1))
28
- .join('');
29
- if (LucideIcons[pascal])
30
- return LucideIcons[pascal];
31
- return Database;
32
- }
18
+ import { getIcon } from '../utils/getIcon';
33
19
  export function CommandPalette({ apps, activeApp, objects: _objects, onAppChange }) {
34
20
  const [open, setOpen] = useState(false);
35
21
  const navigate = useNavigate();
@@ -53,8 +53,8 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
53
53
  const { user, signOut, organizations, activeOrganization, isOrganizationsLoading, } = useAuth();
54
54
  const dataSource = useAdapter();
55
55
  const { t } = useObjectTranslation();
56
- const { objectLabel } = useObjectLabel();
57
- const { apps: metadataApps } = useMetadata();
56
+ const { objectLabel, dashboardLabel, pageLabel, reportLabel } = useObjectLabel();
57
+ const { apps: metadataApps, dashboards: metadataDashboards, pages: metadataPages, reports: metadataReports } = useMetadata();
58
58
  const { currentAppName, recordTitle } = useNavigationContext();
59
59
  const [apiPresenceUsers, setApiPresenceUsers] = useState(null);
60
60
  const [apiActivities, setApiActivities] = useState(null);
@@ -134,18 +134,30 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
134
134
  if (isApp) {
135
135
  if (routeType === 'dashboard') {
136
136
  extraSegments.push({ label: t('console.breadcrumb.dashboards'), href: baseHref });
137
- if (pathParts[3])
138
- extraSegments.push({ label: humanizeSlug(pathParts[3]) });
137
+ if (pathParts[3]) {
138
+ const dashboardName = pathParts[3];
139
+ const dashboardDef = (metadataDashboards || []).find((d) => d.name === dashboardName);
140
+ const fallback = dashboardDef?.label || humanizeSlug(dashboardName);
141
+ extraSegments.push({ label: dashboardLabel({ name: dashboardName, label: fallback }) });
142
+ }
139
143
  }
140
144
  else if (routeType === 'page') {
141
145
  extraSegments.push({ label: t('console.breadcrumb.pages'), href: baseHref });
142
- if (pathParts[3])
143
- extraSegments.push({ label: humanizeSlug(pathParts[3]) });
146
+ if (pathParts[3]) {
147
+ const pageName = pathParts[3];
148
+ const pageDef = (metadataPages || []).find((p) => p.name === pageName);
149
+ const fallback = pageDef?.label || humanizeSlug(pageName);
150
+ extraSegments.push({ label: pageLabel({ name: pageName, label: fallback }) });
151
+ }
144
152
  }
145
153
  else if (routeType === 'report') {
146
154
  extraSegments.push({ label: t('console.breadcrumb.reports'), href: baseHref });
147
- if (pathParts[3])
148
- extraSegments.push({ label: humanizeSlug(pathParts[3]) });
155
+ if (pathParts[3]) {
156
+ const reportName = pathParts[3];
157
+ const reportDef = (metadataReports || []).find((r) => r.name === reportName);
158
+ const fallback = reportDef?.label || humanizeSlug(reportName);
159
+ extraSegments.push({ label: reportLabel({ name: reportName, label: fallback }) });
160
+ }
149
161
  }
150
162
  else if (routeType === 'system') {
151
163
  extraSegments.push({ label: t('console.breadcrumb.system') });
@@ -178,5 +190,5 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
178
190
  return (_jsxs("div", { className: "flex items-center justify-between w-full h-full", children: [_jsxs("div", { className: "flex items-center min-w-0 flex-1", children: [_jsx(Link, { to: "/home", className: "flex items-center justify-center h-7 w-7 shrink-0 rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors", title: "ObjectStack", children: _jsx(Boxes, { className: "h-4 w-4" }) }), resolvedVariant === 'home' && (_jsx("span", { className: "hidden sm:inline ml-2 text-sm font-semibold tracking-tight", children: "ObjectStack" })), resolvedVariant === 'orgs' && (_jsxs(_Fragment, { children: [_jsx(PathSep, {}), _jsx("span", { className: "text-sm font-medium text-foreground/80 px-1.5", children: t('organizations.title', { defaultValue: 'Organizations' }) })] })), isApp && (_jsxs(_Fragment, { children: [_jsx(SidebarTrigger, { className: "md:hidden shrink-0 ml-1" }), activeAppName && onAppChange ? (_jsxs(_Fragment, { children: [_jsx(PathSep, {}), _jsx(AppSwitcher, { activeAppName: activeAppName, onAppChange: onAppChange })] })) : appName ? (_jsxs(_Fragment, { children: [_jsx(PathSep, {}), _jsx("span", { className: "text-sm font-medium text-foreground/80 px-1.5", children: appName })] })) : null, extraSegments.map((seg, i) => {
179
191
  const isLast = i === extraSegments.length - 1;
180
192
  return (_jsxs("span", { className: "hidden sm:flex items-center min-w-0", children: [_jsx(PathSep, {}), seg.siblings && seg.siblings.length > 1 ? (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { className: `flex items-center gap-1 rounded-md px-1.5 py-1 text-sm font-medium transition-colors outline-none hover:bg-accent hover:text-foreground ${!isLast ? 'text-foreground/60' : 'text-foreground/80'}`, children: [seg.label, _jsx(ChevronDown, { className: "h-3.5 w-3.5 text-muted-foreground" })] }), _jsxs(DropdownMenuContent, { align: "start", sideOffset: 8, className: "w-56 max-h-72 overflow-y-auto", children: [_jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground font-normal", children: "Switch Object" }), _jsx(DropdownMenuSeparator, {}), seg.siblings.map((sibling) => (_jsx(DropdownMenuItem, { asChild: true, children: _jsx(Link, { to: sibling.href, className: "w-full", children: sibling.label }) }, sibling.href)))] })] })) : seg.href ? (_jsx(Link, { to: seg.href, className: `rounded-md px-1.5 py-1 text-sm font-medium transition-colors hover:bg-accent hover:text-foreground truncate max-w-[160px] ${isLast ? 'text-foreground/80' : 'text-foreground/60'}`, children: seg.label })) : (_jsx("span", { className: `px-1.5 py-1 text-sm font-medium truncate max-w-[160px] ${isLast ? 'text-foreground/80' : 'text-foreground/60'}`, children: seg.label }))] }, i));
181
- }), _jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: lastSegmentLabel })] }))] }), _jsxs("div", { className: "flex items-center gap-0.5 sm:gap-1 shrink-0 [&>*+*[data-topbar-group]]:ml-1 [&>[data-topbar-group]+[data-topbar-group]]:border-l [&>[data-topbar-group]+[data-topbar-group]]:border-border/60 [&>[data-topbar-group]+[data-topbar-group]]:pl-1 sm:[&>[data-topbar-group]+[data-topbar-group]]:pl-2 sm:[&>[data-topbar-group]+[data-topbar-group]]:ml-2", children: [!isOnline && (_jsxs("div", { className: "flex items-center gap-1 px-2 py-1 rounded-full bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200 text-xs font-medium", children: [_jsx("span", { className: "h-2 w-2 rounded-full bg-yellow-500 animate-pulse" }), "Offline"] })), isApp && connectionState && _jsx(ConnectionStatus, { state: connectionState }), isApp && activeUsers.length > 0 && (_jsx("div", { className: "hidden md:flex items-center shrink-0", title: "Users currently online", children: _jsx(PresenceAvatars, { users: activeUsers, size: "sm", maxVisible: 3, showStatus: true }) })), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 sm:gap-1 shrink-0", children: [_jsxs("button", { onClick: () => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true })), className: "hidden lg:flex relative items-center gap-2 w-48 xl:w-64 h-8 px-3 text-sm rounded-md border bg-muted/50 text-muted-foreground hover:bg-muted transition-colors", children: [_jsx(Search, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "flex-1 text-left text-xs", children: t('console.search', { defaultValue: 'Search...' }) }), _jsxs("kbd", { className: "pointer-events-none inline-flex h-5 items-center gap-0.5 rounded border bg-background px-1.5 text-[10px] font-medium text-muted-foreground", children: [_jsx("span", { className: "text-xs", children: "\u2318" }), "K"] })] }), _jsx(Button, { variant: "ghost", size: "icon", className: "lg:hidden h-8 w-8 shrink-0", onClick: () => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true })), "aria-label": t('console.search', { defaultValue: 'Search...' }), children: _jsx(Search, { className: "h-4 w-4" }) })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [_jsx("div", { className: "hidden sm:flex shrink-0", children: _jsx(ActivityFeed, { activities: activeActivities }) }), _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 hidden md:flex shrink-0", asChild: true, "aria-label": t('sidebar.helpTooltip', { defaultValue: 'Help & Documentation' }), children: _jsx("a", { href: "https://docs.objectstack.ai", target: "_blank", rel: "noopener noreferrer", children: _jsx(HelpCircle, { className: "h-4 w-4" }) }) })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [_jsx("div", { className: "hidden sm:flex shrink-0", children: _jsx(ModeToggle, {}) }), _jsx("div", { className: "hidden sm:flex shrink-0", children: _jsx(LocaleSwitcher, {}) }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0 rounded-full", children: _jsxs(Avatar, { className: "h-7 w-7 rounded-full", children: [_jsx(AvatarImage, { src: user?.image ?? '/avatars/user.jpg', alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-full bg-primary text-primary-foreground text-xs", children: getUserInitials(user) })] }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "min-w-64 rounded-lg", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "p-0 font-normal", children: _jsxs("div", { className: "flex items-center gap-2 px-2 py-2", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image ?? '/avatars/user.jpg', alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] })] }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuGroup, { children: [hasOrgSection && (_jsxs(DropdownMenuItem, { onClick: () => navigate('/organizations'), className: "cursor-pointer", children: [_jsx(Boxes, { className: "mr-2 h-4 w-4" }), t('organizations.mine', { defaultValue: 'My Organizations' })] })), _jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/setup/system/profile'), children: [_jsx(UserIcon, { className: "mr-2 h-4 w-4" }), t('user.profile', { defaultValue: 'Profile' })] }), _jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/setup'), children: [_jsx(Settings, { className: "mr-2 h-4 w-4" }), t('sidebar.settings', { defaultValue: 'Settings' })] })] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "text-destructive focus:text-destructive", onClick: () => signOut(), children: [_jsx(LogOut, { className: "mr-2 h-4 w-4" }), t('user.logout', { defaultValue: 'Log out' })] })] })] })] })] })] }));
193
+ }), _jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: lastSegmentLabel })] }))] }), _jsxs("div", { className: "flex items-center gap-0.5 sm:gap-1 shrink-0 [&>*+*[data-topbar-group]]:ml-1 [&>[data-topbar-group]+[data-topbar-group]]:border-l [&>[data-topbar-group]+[data-topbar-group]]:border-border/60 [&>[data-topbar-group]+[data-topbar-group]]:pl-1 sm:[&>[data-topbar-group]+[data-topbar-group]]:pl-2 sm:[&>[data-topbar-group]+[data-topbar-group]]:ml-2", children: [!isOnline && (_jsxs("div", { className: "flex items-center gap-1 px-2 py-1 rounded-full bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200 text-xs font-medium", children: [_jsx("span", { className: "h-2 w-2 rounded-full bg-yellow-500 animate-pulse" }), "Offline"] })), isApp && connectionState && _jsx(ConnectionStatus, { state: connectionState }), isApp && activeUsers.length > 0 && (_jsx("div", { className: "hidden md:flex items-center shrink-0", title: "Users currently online", children: _jsx(PresenceAvatars, { users: activeUsers, size: "sm", maxVisible: 3, showStatus: true }) })), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 sm:gap-1 shrink-0", children: [_jsxs("button", { onClick: () => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true })), className: "hidden lg:flex relative items-center gap-2 w-48 xl:w-64 h-8 px-3 text-sm rounded-md border bg-muted/50 text-muted-foreground hover:bg-muted transition-colors", children: [_jsx(Search, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "flex-1 text-left text-xs", children: t('console.search', { defaultValue: 'Search...' }) }), _jsxs("kbd", { className: "pointer-events-none inline-flex h-5 items-center gap-0.5 rounded border bg-background px-1.5 text-[10px] font-medium text-muted-foreground", children: [_jsx("span", { className: "text-xs", children: "\u2318" }), "K"] })] }), _jsx(Button, { variant: "ghost", size: "icon", className: "lg:hidden h-8 w-8 shrink-0", onClick: () => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true })), "aria-label": t('console.search', { defaultValue: 'Search...' }), children: _jsx(Search, { className: "h-4 w-4" }) })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [_jsx("div", { className: "hidden sm:flex shrink-0", children: _jsx(ActivityFeed, { activities: activeActivities }) }), _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 hidden md:flex shrink-0", asChild: true, "aria-label": t('sidebar.helpTooltip', { defaultValue: 'Help & Documentation' }), children: _jsx("a", { href: "https://docs.objectstack.ai", target: "_blank", rel: "noopener noreferrer", children: _jsx(HelpCircle, { className: "h-4 w-4" }) }) })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [_jsx("div", { className: "hidden sm:flex shrink-0", children: _jsx(ModeToggle, {}) }), _jsx("div", { className: "hidden sm:flex shrink-0", children: _jsx(LocaleSwitcher, {}) }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0 rounded-full", children: _jsxs(Avatar, { className: "h-7 w-7 rounded-full", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-full bg-primary text-primary-foreground text-xs", children: getUserInitials(user) })] }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "min-w-64 rounded-lg", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "p-0 font-normal", children: _jsxs("div", { className: "flex items-center gap-2 px-2 py-2", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] })] }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuGroup, { children: [hasOrgSection && (_jsxs(DropdownMenuItem, { onClick: () => navigate('/organizations'), className: "cursor-pointer", children: [_jsx(Boxes, { className: "mr-2 h-4 w-4" }), t('organizations.mine', { defaultValue: 'My Organizations' })] })), _jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/setup/system/profile'), children: [_jsx(UserIcon, { className: "mr-2 h-4 w-4" }), t('user.profile', { defaultValue: 'Profile' })] }), _jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/setup'), children: [_jsx(Settings, { className: "mr-2 h-4 w-4" }), t('sidebar.settings', { defaultValue: 'Settings' })] })] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "text-destructive focus:text-destructive", onClick: () => signOut(), children: [_jsx(LogOut, { className: "mr-2 h-4 w-4" }), t('user.logout', { defaultValue: 'Log out' })] })] })] })] })] })] }));
182
194
  }
@@ -11,7 +11,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
11
11
  */
12
12
  import * as React from 'react';
13
13
  import { useNavigate, Link } from 'react-router-dom';
14
- import * as LucideIcons from 'lucide-react';
14
+ import { Layers } from 'lucide-react';
15
+ import { getIcon as resolveIcon } from '../utils/getIcon';
15
16
  import { Sidebar, SidebarHeader, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarGroupContent, SidebarMenu, SidebarMenuItem, SidebarMenuButton, SidebarMenuAction, SidebarInput, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuGroup, Avatar, AvatarImage, AvatarFallback, useSidebar, } from '@object-ui/components';
16
17
  import { ChevronsUpDown, Plus, Settings, LogOut, Database, Clock, Star, StarOff, Search, Pencil, ChevronRight, Home, } from 'lucide-react';
17
18
  import { NavigationRenderer } from '@object-ui/layout';
@@ -84,26 +85,10 @@ function useNavOrder(appName) {
84
85
  }
85
86
  /**
86
87
  * Resolve a Lucide icon component by name string.
87
- * Supports camelCase, PascalCase, and kebab-case icon names.
88
+ * Delegates to the shared lazy `getIcon` helper which returns a wrapper
89
+ * around lucide-react `DynamicIcon` so individual icons are code-split.
88
90
  */
89
- function getIcon(name) {
90
- if (!name)
91
- return LucideIcons.Database;
92
- // 1. Direct match (PascalCase or camelCase)
93
- if (LucideIcons[name]) {
94
- return LucideIcons[name];
95
- }
96
- // 2. Try converting kebab-case to PascalCase (e.g. "shopping-cart" -> "ShoppingCart")
97
- const pascalName = name
98
- .split('-')
99
- .map(part => part.charAt(0).toUpperCase() + part.slice(1))
100
- .join('');
101
- if (LucideIcons[pascalName]) {
102
- return LucideIcons[pascalName];
103
- }
104
- // 3. Fallback
105
- return LucideIcons.Database;
106
- }
91
+ const getIcon = resolveIcon;
107
92
  export function AppSidebar({ activeAppName, onAppChange }) {
108
93
  const { isMobile } = useSidebar();
109
94
  const { user, signOut } = useAuth();
@@ -192,14 +177,14 @@ export function AppSidebar({ activeAppName, onAppChange }) {
192
177
  { id: 'sys-roles', label: 'Roles', type: 'url', url: '/apps/setup/system/roles', icon: 'shield' },
193
178
  { id: 'sys-create-app', label: 'Create App', type: 'url', url: '/create-app', icon: 'plus' },
194
179
  ], []);
195
- return (_jsxs(_Fragment, { children: [_jsxs(Sidebar, { collapsible: "icon", children: [_jsx(SidebarHeader, { children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: activeApp ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(SidebarMenuButton, { size: "lg", className: "data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", children: [_jsx("div", { className: "flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground", style: primaryColor ? { backgroundColor: primaryColor } : undefined, children: logo ? (_jsx("img", { src: logo, alt: resolveI18nLabel(activeApp.label, t), className: "size-6 object-contain" })) : (React.createElement(getIcon(activeApp.icon), { className: "size-4" })) }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: resolveI18nLabel(activeApp.label, t) }), _jsx("span", { className: "truncate text-xs", children: resolveI18nLabel(activeApp.description, t) || `${activeApps.length} Apps Available` })] }), _jsx(ChevronsUpDown, { className: "ml-auto" })] }) }), _jsxs(DropdownMenuContent, { className: "w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg", align: "start", side: isMobile ? "bottom" : "right", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground", children: "Switch Application" }), activeApps.map((app) => (_jsxs(DropdownMenuItem, { onClick: () => onAppChange(app.name), className: "gap-2 p-2", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-sm border", children: app.icon ? React.createElement(getIcon(app.icon), { className: "size-3" }) : _jsx(Database, { className: "size-3" }) }), resolveI18nLabel(app.label, t), activeApp.name === app.name && _jsx("span", { className: "ml-auto text-xs", children: "\u2713" })] }, app.name))), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate('/home'), "data-testid": "home-link-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Home, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: "Home" })] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate(`/apps/${activeAppName}/create-app`), "data-testid": "add-app-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Plus, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: "Add App" })] }), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate(`/apps/${activeAppName}/edit-app/${activeAppName}`), "data-testid": "edit-app-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Pencil, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: "Edit App" })] }), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate('/apps/setup/system/apps'), "data-testid": "manage-all-apps-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Settings, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: "Manage All Apps" })] })] })] })) : (_jsxs(SidebarMenuButton, { size: "lg", onClick: () => navigate('/apps/setup'), "data-testid": "system-sidebar-header", children: [_jsx("div", { className: "flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground", children: _jsx(Settings, { className: "size-4" }) }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: "System Console" }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: "No apps configured" })] })] })) }) }) }), _jsx(SidebarContent, { children: activeApp ? (_jsxs(_Fragment, { children: [areas.length > 1 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(LucideIcons.Layers, { className: "h-3.5 w-3.5" }), "Area"] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: areas.map((area) => {
180
+ return (_jsxs(_Fragment, { children: [_jsxs(Sidebar, { collapsible: "icon", children: [_jsx(SidebarHeader, { children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: activeApp ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(SidebarMenuButton, { size: "lg", className: "data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", children: [_jsx("div", { className: "flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground", style: primaryColor ? { backgroundColor: primaryColor } : undefined, children: logo ? (_jsx("img", { src: logo, alt: resolveI18nLabel(activeApp.label, t), className: "size-6 object-contain" })) : (React.createElement(getIcon(activeApp.icon), { className: "size-4" })) }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: resolveI18nLabel(activeApp.label, t) }), _jsx("span", { className: "truncate text-xs", children: resolveI18nLabel(activeApp.description, t) || `${activeApps.length} Apps Available` })] }), _jsx(ChevronsUpDown, { className: "ml-auto" })] }) }), _jsxs(DropdownMenuContent, { className: "w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg", align: "start", side: isMobile ? "bottom" : "right", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground", children: "Switch Application" }), activeApps.map((app) => (_jsxs(DropdownMenuItem, { onClick: () => onAppChange(app.name), className: "gap-2 p-2", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-sm border", children: app.icon ? React.createElement(getIcon(app.icon), { className: "size-3" }) : _jsx(Database, { className: "size-3" }) }), resolveI18nLabel(app.label, t), activeApp.name === app.name && _jsx("span", { className: "ml-auto text-xs", children: "\u2713" })] }, app.name))), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate('/home'), "data-testid": "home-link-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Home, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: "Home" })] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate(`/apps/${activeAppName}/create-app`), "data-testid": "add-app-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Plus, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: "Add App" })] }), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate(`/apps/${activeAppName}/edit-app/${activeAppName}`), "data-testid": "edit-app-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Pencil, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: "Edit App" })] }), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate('/apps/setup/system/apps'), "data-testid": "manage-all-apps-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Settings, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: "Manage All Apps" })] })] })] })) : (_jsxs(SidebarMenuButton, { size: "lg", onClick: () => navigate('/apps/setup'), "data-testid": "system-sidebar-header", children: [_jsx("div", { className: "flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground", children: _jsx(Settings, { className: "size-4" }) }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: "System Console" }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: "No apps configured" })] })] })) }) }) }), _jsx(SidebarContent, { children: activeApp ? (_jsxs(_Fragment, { children: [areas.length > 1 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Layers, { className: "h-3.5 w-3.5" }), "Area"] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: areas.map((area) => {
196
181
  const AreaIcon = getIcon(area.icon);
197
182
  const isActiveArea = area.id === activeAreaId;
198
183
  return (_jsx(SidebarMenuItem, { children: _jsxs(SidebarMenuButton, { isActive: isActiveArea, tooltip: area.label, onClick: () => setActiveAreaId(area.id), children: [_jsx(AreaIcon, { className: "h-4 w-4" }), _jsx("span", { children: area.label })] }) }, area.id));
199
184
  }) }) })] })), _jsx(SidebarGroup, { className: "py-0", children: _jsxs(SidebarGroupContent, { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-2 top-1/2 size-4 -translate-y-1/2 select-none opacity-70" }), _jsx(SidebarInput, { placeholder: "Search navigation...", value: navSearchQuery, onChange: (e) => setNavSearchQuery(e.target.value), className: "pl-8" })] }) }), _jsx(NavigationRenderer, { items: processedNavigation, basePath: basePath, evaluateVisibility: evalVis, checkPermission: checkPerm, searchQuery: navSearchQuery, enablePinning: true, onPinToggle: togglePin, enableReorder: true, onReorder: handleReorder, resolveObjectLabel: (objectName, fallback) => resolveNavObjectLabel({ name: objectName, label: fallback }), t: t }), recentItems.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5 cursor-pointer select-none", onClick: () => setRecentExpanded(prev => !prev), children: [_jsx(ChevronRight, { className: `h-3 w-3 transition-transform duration-150 ${recentExpanded ? 'rotate-90' : ''}` }), _jsx(Clock, { className: "h-3.5 w-3.5" }), "Recent"] }), recentExpanded && (_jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: recentItems.slice(0, 5).map(item => (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : '📄' }), _jsx("span", { className: "truncate", children: item.label })] }) }) }, item.id))) }) }))] })), favorites.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Star, { className: "h-3.5 w-3.5" }), "Favorites"] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: favorites.slice(0, 8).map(item => (_jsxs(SidebarMenuItem, { children: [_jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : item.type === 'page' ? '📄' : '📋' }), _jsx("span", { className: "truncate", children: item.label })] }) }), _jsx(SidebarMenuAction, { showOnHover: true, onClick: (e) => { e.stopPropagation(); removeFavorite(item.id); }, "aria-label": `Remove ${item.label} from favorites`, children: _jsx(StarOff, { className: "h-3 w-3" }) })] }, item.id))) }) })] }))] })) : (_jsxs(SidebarGroup, { "data-testid": "system-fallback-nav", children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Settings, { className: "h-3.5 w-3.5" }), "System"] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: systemFallbackNavigation.map((item) => {
200
185
  const NavIcon = getIcon(item.icon);
201
186
  return (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.url || '/system', children: [_jsx(NavIcon, { className: "h-4 w-4" }), _jsx("span", { children: item.label })] }) }) }, item.id));
202
- }) }) })] })) }), _jsx(SidebarFooter, { children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(SidebarMenuButton, { size: "lg", className: "data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image ?? '/avatars/user.jpg', alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] }), _jsx(ChevronsUpDown, { className: "ml-auto size-4" })] }) }), _jsxs(DropdownMenuContent, { className: "w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg", side: isMobile ? "bottom" : "right", align: "end", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "p-0 font-normal", children: _jsxs("div", { className: "flex items-center gap-2 px-1 py-1.5 text-left text-sm", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image ?? '/avatars/user.jpg', alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] })] }) }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuGroup, { children: _jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/setup'), children: [_jsx(Settings, { className: "mr-2 h-4 w-4" }), "Settings"] }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "text-destructive focus:text-destructive", onClick: () => signOut(), children: [_jsx(LogOut, { className: "mr-2 h-4 w-4" }), "Log out"] })] })] }) }) }) })] }), isMobile && (_jsx("div", { className: "fixed bottom-0 left-0 right-0 z-50 flex items-center justify-around border-t bg-background/95 backdrop-blur-sm px-2 py-1 sm:hidden safe-area-bottom", children: (activeApp ? resolvedNavigation : systemFallbackNavigation).filter((n) => n.type !== 'group').slice(0, 5).map((item) => {
187
+ }) }) })] })) }), _jsx(SidebarFooter, { children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(SidebarMenuButton, { size: "lg", className: "data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] }), _jsx(ChevronsUpDown, { className: "ml-auto size-4" })] }) }), _jsxs(DropdownMenuContent, { className: "w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg", side: isMobile ? "bottom" : "right", align: "end", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "p-0 font-normal", children: _jsxs("div", { className: "flex items-center gap-2 px-1 py-1.5 text-left text-sm", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] })] }) }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuGroup, { children: _jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/setup'), children: [_jsx(Settings, { className: "mr-2 h-4 w-4" }), "Settings"] }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "text-destructive focus:text-destructive", onClick: () => signOut(), children: [_jsx(LogOut, { className: "mr-2 h-4 w-4" }), "Log out"] })] })] }) }) }) })] }), isMobile && (_jsx("div", { className: "fixed bottom-0 left-0 right-0 z-50 flex items-center justify-around border-t bg-background/95 backdrop-blur-sm px-2 py-1 sm:hidden safe-area-bottom", children: (activeApp ? resolvedNavigation : systemFallbackNavigation).filter((n) => n.type !== 'group').slice(0, 5).map((item) => {
203
188
  const NavIcon = getIcon(item.icon);
204
189
  const baseUrl = activeApp ? `/apps/${activeAppName}` : '';
205
190
  let href = item.url || '#';
@@ -1,25 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import * as LucideIcons from 'lucide-react';
3
2
  import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, } from '@object-ui/components';
4
3
  import { ChevronDown, Check } from 'lucide-react';
5
4
  import { useMetadata } from '../providers/MetadataProvider';
6
5
  import { resolveI18nLabel } from '../utils';
7
6
  import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n';
8
- function getIcon(name) {
9
- if (!name)
10
- return LucideIcons.Database;
11
- const lookup = (key) => {
12
- try {
13
- const icon = LucideIcons[key];
14
- return typeof icon === 'function' ? icon : undefined;
15
- }
16
- catch {
17
- return undefined;
18
- }
19
- };
20
- const pascalName = name.split(/[-_]/).map(p => p.charAt(0).toUpperCase() + p.slice(1)).join('');
21
- return lookup(name) ?? lookup(pascalName) ?? LucideIcons.Database;
22
- }
7
+ import { getIcon } from '../utils/getIcon';
23
8
  export function AppSwitcher({ activeAppName, onAppChange }) {
24
9
  const { apps: metadataApps } = useMetadata();
25
10
  const { t } = useObjectTranslation();
@@ -0,0 +1,10 @@
1
+ interface ConsoleObject {
2
+ name: string;
3
+ label?: string;
4
+ }
5
+ export interface ConsoleFloatingChatbotProps {
6
+ appLabel: string;
7
+ objects: ConsoleObject[];
8
+ }
9
+ export default function ConsoleFloatingChatbot({ appLabel, objects }: ConsoleFloatingChatbotProps): import("react/jsx-runtime").JSX.Element;
10
+ export {};
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { FloatingChatbot, useObjectChat } from '@object-ui/plugin-chatbot';
3
+ export default function ConsoleFloatingChatbot({ appLabel, objects }) {
4
+ const objectNames = objects.map((o) => o.label || o.name).join(', ');
5
+ const { messages, isLoading, error, sendMessage, stop, reload, clear, } = useObjectChat({
6
+ initialMessages: [
7
+ {
8
+ id: 'welcome',
9
+ role: 'assistant',
10
+ content: `Hello! I'm your **${appLabel}** assistant. How can I help you today?`,
11
+ },
12
+ ],
13
+ autoResponse: true,
14
+ autoResponseText: objectNames
15
+ ? `I can help you work with ${objectNames}. What would you like to do?`
16
+ : 'Thanks for your message! I\'m here to help you navigate and manage your data.',
17
+ autoResponseDelay: 800,
18
+ });
19
+ return (_jsx(FloatingChatbot, { floatingConfig: {
20
+ position: 'bottom-right',
21
+ defaultOpen: false,
22
+ panelWidth: 400,
23
+ panelHeight: 520,
24
+ title: `${appLabel} Assistant`,
25
+ triggerSize: 56,
26
+ }, messages: messages, placeholder: "Ask anything...", onSendMessage: (content) => sendMessage(content), onClear: clear, onStop: isLoading ? stop : undefined, onReload: reload, isLoading: isLoading, error: error, enableMarkdown: true }));
27
+ }
@@ -17,5 +17,6 @@ interface ConsoleLayoutProps {
17
17
  objects: any[];
18
18
  connectionState?: ConnectionState;
19
19
  }
20
+ /** Floating chatbot wired with useObjectChat for demo auto-response */
20
21
  export declare function ConsoleLayout({ children, activeAppName, activeApp, onAppChange, objects, connectionState }: ConsoleLayoutProps): import("react/jsx-runtime").JSX.Element;
21
22
  export {};
@@ -8,10 +8,12 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
8
8
  * Sets navigation context to 'app' for app-specific routes.
9
9
  * @module
10
10
  */
11
- import { useEffect } from 'react';
11
+ import { useEffect, Suspense, lazy } from 'react';
12
12
  import { AppShell } from '@object-ui/layout';
13
- import { FloatingChatbot, useObjectChat } from '@object-ui/plugin-chatbot';
14
13
  import { useDiscovery } from '@object-ui/react';
14
+ // Lazy-load the chatbot so its heavy markdown deps (~150 KB) stay out of
15
+ // the initial paint until the AI assistant is actually enabled.
16
+ const ConsoleFloatingChatbot = lazy(() => import('./ConsoleFloatingChatbot'));
15
17
  import { UnifiedSidebar } from './UnifiedSidebar';
16
18
  import { AppHeader } from './AppHeader';
17
19
  import { useResponsiveSidebar } from '../hooks/useResponsiveSidebar';
@@ -23,31 +25,7 @@ function ConsoleLayoutInner({ children }) {
23
25
  return _jsx(_Fragment, { children: children });
24
26
  }
25
27
  /** Floating chatbot wired with useObjectChat for demo auto-response */
26
- function ConsoleFloatingChatbot({ appLabel, objects }) {
27
- const objectNames = objects.map((o) => o.label || o.name).join(', ');
28
- const { messages, isLoading, error, sendMessage, stop, reload, clear, } = useObjectChat({
29
- initialMessages: [
30
- {
31
- id: 'welcome',
32
- role: 'assistant',
33
- content: `Hello! I'm your **${appLabel}** assistant. How can I help you today?`,
34
- },
35
- ],
36
- autoResponse: true,
37
- autoResponseText: objectNames
38
- ? `I can help you work with ${objectNames}. What would you like to do?`
39
- : 'Thanks for your message! I\'m here to help you navigate and manage your data.',
40
- autoResponseDelay: 800,
41
- });
42
- return (_jsx(FloatingChatbot, { floatingConfig: {
43
- position: 'bottom-right',
44
- defaultOpen: false,
45
- panelWidth: 400,
46
- panelHeight: 520,
47
- title: `${appLabel} Assistant`,
48
- triggerSize: 56,
49
- }, messages: messages, placeholder: "Ask anything...", onSendMessage: (content) => sendMessage(content), onClear: clear, onStop: isLoading ? stop : undefined, onReload: reload, isLoading: isLoading, error: error, enableMarkdown: true }));
50
- }
28
+ // (moved to ./ConsoleFloatingChatbot.tsx for code-splitting)
51
29
  export function ConsoleLayout({ children, activeAppName, activeApp, onAppChange, objects, connectionState }) {
52
30
  const appLabel = resolveI18nLabel(activeApp?.label) || activeAppName;
53
31
  const { isAiEnabled } = useDiscovery();
@@ -67,5 +45,5 @@ export function ConsoleLayout({ children, activeAppName, activeApp, onAppChange,
67
45
  ? `${resolveI18nLabel(activeApp.label)} — ObjectStack Console`
68
46
  : undefined,
69
47
  }
70
- : undefined, children: [_jsx(ConsoleLayoutInner, { children: children }), isAiEnabled && _jsx(ConsoleFloatingChatbot, { appLabel: appLabel, objects: objects })] }));
48
+ : undefined, children: [_jsx(ConsoleLayoutInner, { children: children }), isAiEnabled && (_jsx(Suspense, { fallback: null, children: _jsx(ConsoleFloatingChatbot, { appLabel: appLabel, objects: objects }) }))] }));
71
49
  }
@@ -12,6 +12,11 @@
12
12
  *
13
13
  * @module
14
14
  */
15
+ /**
16
+ * Lazy-resolved Lucide icon — see ../utils/getIcon for impl.
17
+ * The local symbol is kept for backwards compat with existing call sites
18
+ * within this file.
19
+ */
15
20
  interface UnifiedSidebarProps {
16
21
  /** When in app context, the active app name */
17
22
  activeAppName?: string;
@@ -15,7 +15,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
15
15
  */
16
16
  import * as React from 'react';
17
17
  import { Link, useLocation } from 'react-router-dom';
18
- import * as LucideIcons from 'lucide-react';
18
+ import { getIcon } from '../utils/getIcon';
19
19
  import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarGroupContent, SidebarMenu, SidebarMenuItem, SidebarMenuButton, SidebarMenuAction, SidebarTrigger, useSidebar, } from '@object-ui/components';
20
20
  import { Clock, Star, StarOff, ChevronRight, Layers, } from 'lucide-react';
21
21
  import { NavigationRenderer } from '@object-ui/layout';
@@ -85,35 +85,11 @@ function useNavOrder(appName) {
85
85
  }, [orderMap, persist]);
86
86
  return { applyOrder, handleReorder };
87
87
  }
88
- /**
89
- * Resolve a Lucide icon component by name string.
90
- * Safely handles both exact names and kebab-case → PascalCase conversion.
91
- * The try/catch guards against strict module proxy environments (e.g. vitest mocks).
92
- */
93
- function getIcon(name) {
94
- if (!name)
95
- return LucideIcons.Database;
96
- const lookup = (key) => {
97
- try {
98
- const icon = LucideIcons[key];
99
- return typeof icon === 'function' ? icon : undefined;
100
- }
101
- catch {
102
- return undefined;
103
- }
104
- };
105
- // Try exact match first, then convert kebab-case / lowercase to PascalCase
106
- const pascalName = name
107
- .split(/[-_]/)
108
- .map(part => part.charAt(0).toUpperCase() + part.slice(1))
109
- .join('');
110
- return lookup(name) ?? lookup(pascalName) ?? LucideIcons.Database;
111
- }
112
88
  export function UnifiedSidebar({ activeAppName }) {
113
89
  const { isMobile } = useSidebar();
114
90
  const location = useLocation();
115
91
  const { t } = useObjectTranslation();
116
- const { objectLabel: resolveNavObjectLabel } = useObjectLabel();
92
+ const { objectLabel: resolveNavObjectLabel, dashboardLabel: resolveNavDashboardLabel, navGroupLabel: resolveNavGroupLabel } = useObjectLabel();
117
93
  const { context, currentAppName } = useNavigationContext();
118
94
  // Swipe-from-left-edge gesture to open sidebar on mobile
119
95
  React.useEffect(() => {
@@ -185,15 +161,15 @@ export function UnifiedSidebar({ activeAppName }) {
185
161
  return can(object, action);
186
162
  }), [can]);
187
163
  const basePath = context === 'app' && activeApp ? `/apps/${activeApp.name}` : '';
188
- return (_jsxs(_Fragment, { children: [_jsxs(Sidebar, { collapsible: "icon", className: "!top-14 !h-[calc(100svh-3.5rem)]", children: [_jsx(SidebarContent, { className: "pt-2", children: _jsx("div", { className: "transition-opacity duration-200 ease-in-out", children: context === 'app' && activeApp ? (_jsxs(_Fragment, { children: [areas.length > 1 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Layers, { className: "h-3.5 w-3.5" }), "Area"] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: areas.map((area) => {
164
+ return (_jsxs(_Fragment, { children: [_jsxs(Sidebar, { collapsible: "icon", className: "!top-14 !h-[calc(100svh-3.5rem)]", children: [_jsx(SidebarContent, { className: "pt-2", children: _jsx("div", { className: "transition-opacity duration-200 ease-in-out", children: context === 'app' && activeApp ? (_jsxs(_Fragment, { children: [areas.length > 1 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Layers, { className: "h-3.5 w-3.5" }), t('sidebar.area', { defaultValue: 'Area' })] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: areas.map((area) => {
189
165
  const AreaIcon = getIcon(area.icon);
190
166
  const isActiveArea = area.id === activeAreaId;
191
167
  return (_jsx(SidebarMenuItem, { children: _jsxs(SidebarMenuButton, { isActive: isActiveArea, tooltip: area.label, onClick: () => setActiveAreaId(area.id), children: [_jsx(AreaIcon, { className: "h-4 w-4" }), _jsx("span", { children: area.label })] }) }, area.id));
192
- }) }) })] })), _jsx(NavigationRenderer, { items: processedNavigation, basePath: basePath, evaluateVisibility: evalVis, checkPermission: checkPerm, enablePinning: true, onPinToggle: togglePin, enableReorder: true, onReorder: handleReorder, resolveObjectLabel: (objectName, fallback) => resolveNavObjectLabel({ name: objectName, label: fallback }), t: t }), recentItems.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5 cursor-pointer select-none", onClick: () => setRecentExpanded(prev => !prev), children: [_jsx(ChevronRight, { className: `h-3 w-3 transition-transform duration-150 ${recentExpanded ? 'rotate-90' : ''}` }), _jsx(Clock, { className: "h-3.5 w-3.5" }), "Recent"] }), recentExpanded && (_jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: recentItems.slice(0, 5).map(item => (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : '📄' }), _jsx("span", { className: "truncate", children: item.label })] }) }) }, item.id))) }) }))] })), favorites.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Star, { className: "h-3.5 w-3.5" }), "Favorites"] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: favorites.slice(0, 8).map(item => (_jsxs(SidebarMenuItem, { children: [_jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : item.type === 'page' ? '📄' : '📋' }), _jsx("span", { className: "truncate", children: item.label })] }) }), _jsx(SidebarMenuAction, { showOnHover: true, onClick: (e) => { e.stopPropagation(); removeFavorite(item.id); }, "aria-label": `Remove ${item.label} from favorites`, children: _jsx(StarOff, { className: "h-3 w-3" }) })] }, item.id))) }) })] }))] })) : (_jsxs(_Fragment, { children: [_jsx(SidebarGroup, { children: _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: homeNavigation.map((item) => {
168
+ }) }) })] })), _jsx(NavigationRenderer, { items: processedNavigation, basePath: basePath, evaluateVisibility: evalVis, checkPermission: checkPerm, enablePinning: !isMobile, onPinToggle: togglePin, enableReorder: !isMobile, onReorder: handleReorder, resolveObjectLabel: (objectName, fallback) => resolveNavObjectLabel({ name: objectName, label: fallback }), resolveDashboardLabel: (dashboardName, fallback) => resolveNavDashboardLabel({ name: dashboardName, label: fallback }), resolveGroupLabel: activeApp ? (groupId, fallback) => resolveNavGroupLabel(activeApp.name, groupId, fallback) : undefined, t: t }), recentItems.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5 cursor-pointer select-none", onClick: () => setRecentExpanded(prev => !prev), children: [_jsx(ChevronRight, { className: `h-3 w-3 transition-transform duration-150 ${recentExpanded ? 'rotate-90' : ''}` }), _jsx(Clock, { className: "h-3.5 w-3.5" }), t('sidebar.recent', { defaultValue: 'Recent' })] }), recentExpanded && (_jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: recentItems.slice(0, 5).map(item => (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : '📄' }), _jsx("span", { className: "truncate", children: item.label })] }) }) }, item.id))) }) }))] })), favorites.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Star, { className: "h-3.5 w-3.5" }), t('sidebar.favorites', { defaultValue: 'Favorites' })] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: favorites.slice(0, 8).map(item => (_jsxs(SidebarMenuItem, { children: [_jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : item.type === 'page' ? '📄' : '📋' }), _jsx("span", { className: "truncate", children: item.label })] }) }), _jsx(SidebarMenuAction, { showOnHover: true, onClick: (e) => { e.stopPropagation(); removeFavorite(item.id); }, "aria-label": t('sidebar.removeFromFavorites', { defaultValue: 'Remove {{name}} from favorites', name: item.label }), children: _jsx(StarOff, { className: "h-3 w-3" }) })] }, item.id))) }) })] }))] })) : (_jsxs(_Fragment, { children: [_jsx(SidebarGroup, { children: _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: homeNavigation.map((item) => {
193
169
  const NavIcon = getIcon(item.icon);
194
170
  const isActive = location.pathname === item.url;
195
171
  return (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, isActive: isActive, children: _jsxs(Link, { to: item.url || '/home', children: [_jsx(NavIcon, { className: "h-4 w-4" }), _jsx("span", { children: item.label })] }) }) }, item.id));
196
- }) }) }) }), favorites.filter(f => f.type === 'object' || f.type === 'dashboard' || f.type === 'page').length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Star, { className: "h-3.5 w-3.5" }), "Starred"] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: favorites.filter(f => f.type === 'object' || f.type === 'dashboard' || f.type === 'page').slice(0, 8).map(item => (_jsxs(SidebarMenuItem, { children: [_jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'page' ? '📄' : '📋' }), _jsx("span", { className: "truncate", children: item.label })] }) }), _jsx(SidebarMenuAction, { showOnHover: true, onClick: (e) => { e.stopPropagation(); removeFavorite(item.id); }, "aria-label": `Remove ${item.label} from favorites`, children: _jsx(StarOff, { className: "h-3 w-3" }) })] }, item.id))) }) })] }))] })) }) }), _jsx(SidebarFooter, { className: "border-t p-1", children: _jsx(SidebarTrigger, { className: "w-full justify-start pl-2 group-data-[state=collapsed]:justify-center group-data-[state=collapsed]:pl-0" }) })] }), isMobile && context === 'app' && (_jsx("div", { className: "fixed bottom-0 left-0 right-0 z-50 flex items-center justify-around border-t bg-background/95 backdrop-blur-sm px-2 py-1 sm:hidden safe-area-bottom", children: processedNavigation.filter((n) => n.type !== 'group').slice(0, 5).map((item) => {
172
+ }) }) }) }), favorites.filter(f => f.type === 'object' || f.type === 'dashboard' || f.type === 'page').length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Star, { className: "h-3.5 w-3.5" }), t('sidebar.starred', { defaultValue: 'Starred' })] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: favorites.filter(f => f.type === 'object' || f.type === 'dashboard' || f.type === 'page').slice(0, 8).map(item => (_jsxs(SidebarMenuItem, { children: [_jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'page' ? '📄' : '📋' }), _jsx("span", { className: "truncate", children: item.label })] }) }), _jsx(SidebarMenuAction, { showOnHover: true, onClick: (e) => { e.stopPropagation(); removeFavorite(item.id); }, "aria-label": t('sidebar.removeFromFavorites', { defaultValue: 'Remove {{name}} from favorites', name: item.label }), children: _jsx(StarOff, { className: "h-3 w-3" }) })] }, item.id))) }) })] }))] })) }) }), _jsx(SidebarFooter, { className: "border-t p-1", children: _jsx(SidebarTrigger, { className: "w-full justify-start pl-2 group-data-[state=collapsed]:justify-center group-data-[state=collapsed]:pl-0" }) })] }), isMobile && context === 'app' && (_jsx("div", { className: "fixed bottom-0 left-0 right-0 z-50 flex items-center justify-around border-t bg-background/95 backdrop-blur-sm px-2 py-1 sm:hidden safe-area-bottom", children: processedNavigation.filter((n) => n.type !== 'group').slice(0, 5).map((item) => {
197
173
  const NavIcon = getIcon(item.icon);
198
174
  let href = item.url || '#';
199
175
  if (item.type === 'object') {
@@ -7,7 +7,7 @@ export { useMetadata };
7
7
  // Constants
8
8
  // ---------------------------------------------------------------------------
9
9
  const DEFAULT_TTL_MS = 5 * 60 * 1000;
10
- const EAGER_TYPES = ['app'];
10
+ const EAGER_TYPES = ['app', 'view'];
11
11
  const TYPE_BY_STATE_KEY = {
12
12
  apps: 'app',
13
13
  objects: 'object',
@@ -61,6 +61,44 @@ function isNamedItem(item) {
61
61
  'name' in item &&
62
62
  typeof item.name === 'string');
63
63
  }
64
+ /**
65
+ * Merge `view` metadata (the @objectstack/spec View container, keyed by
66
+ * target object name) into object definitions so that `objectDef.listViews`
67
+ * is populated for the renderer (`@object-ui/plugin-view`) which expects it.
68
+ *
69
+ * Each View has shape `{ list?, form?, listViews?, formViews? }`. We collapse:
70
+ * - `view.list` → `listViews[view.list.name || 'default']`
71
+ * - `view.listViews` → spread into `listViews`
72
+ *
73
+ * Existing `obj.listViews` / `obj.list_views` win to preserve overrides.
74
+ */
75
+ function mergeViewsIntoObjects(objects, views) {
76
+ if (!objects.length || !views.length)
77
+ return objects;
78
+ const byObject = {};
79
+ for (const view of views) {
80
+ const objName = view?.name || view?.list?.data?.object || view?.form?.data?.object;
81
+ if (!objName)
82
+ continue;
83
+ const bucket = (byObject[objName] || (byObject[objName] = {}));
84
+ if (view.list) {
85
+ const k = view.list.name || 'default';
86
+ bucket[k] = view.list;
87
+ }
88
+ if (view.listViews && typeof view.listViews === 'object') {
89
+ for (const [k, v] of Object.entries(view.listViews)) {
90
+ bucket[k] = v;
91
+ }
92
+ }
93
+ }
94
+ return objects.map(obj => {
95
+ const extra = byObject[obj.name];
96
+ if (!extra)
97
+ return obj;
98
+ const existing = obj.listViews || obj.list_views || {};
99
+ return { ...obj, listViews: { ...extra, ...existing } };
100
+ });
101
+ }
64
102
  function emptyEntry() {
65
103
  return {
66
104
  status: 'idle',
@@ -294,7 +332,9 @@ export function MetadataProvider({ children, adapter, ttlMs = DEFAULT_TTL_MS })
294
332
  const base = {
295
333
  apps: getEntry('app').items,
296
334
  get objects() {
297
- return readType(TYPE_BY_STATE_KEY.objects);
335
+ const objs = readType(TYPE_BY_STATE_KEY.objects);
336
+ const views = readType('view');
337
+ return views.length ? mergeViewsIntoObjects(objs, views) : objs;
298
338
  },
299
339
  get dashboards() {
300
340
  return readType(TYPE_BY_STATE_KEY.dashboards);
@@ -2,9 +2,23 @@
2
2
  * Icon utilities
3
3
  *
4
4
  * Helpers for resolving Lucide icons by name.
5
+ *
6
+ * Implementation: instead of statically importing every icon (~1500
7
+ * components, ~568 KB raw / 140 KB gz), we wrap lucide-react's built-in
8
+ * `DynamicIcon` so each icon is fetched as its own tiny chunk on first use.
9
+ *
10
+ * The exported `getIcon(name)` API stays synchronous and returns a React
11
+ * component, preserving call sites that do `const Icon = getIcon(name); <Icon />`.
5
12
  */
13
+ import React from 'react';
6
14
  /**
7
- * Resolve a Lucide icon by name (kebab-case or PascalCase)
8
- * Falls back to Database icon if not found
15
+ * Resolve a Lucide icon by name (kebab-case or PascalCase).
16
+ *
17
+ * Returns a React component that lazy-loads the underlying SVG icon on
18
+ * mount. Falls back to the `Database` icon (statically imported) when no
19
+ * `name` is given.
20
+ *
21
+ * The returned component is memoised per `name` so repeated calls with the
22
+ * same name yield the same component reference (stable for React.memo).
9
23
  */
10
24
  export declare function getIcon(name?: string): React.ElementType;