@object-ui/app-shell 3.3.1 → 3.3.2

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,33 @@
1
1
  # @object-ui/app-shell — Changelog
2
2
 
3
+ ## 3.3.2
4
+
5
+ ### Patch Changes
6
+
7
+ - @object-ui/types@3.3.2
8
+ - @object-ui/core@3.3.2
9
+ - @object-ui/i18n@3.3.2
10
+ - @object-ui/react@3.3.2
11
+ - @object-ui/components@3.3.2
12
+ - @object-ui/fields@3.3.2
13
+ - @object-ui/layout@3.3.2
14
+ - @object-ui/data-objectstack@3.3.2
15
+ - @object-ui/auth@3.3.2
16
+ - @object-ui/permissions@3.3.2
17
+ - @object-ui/plugin-calendar@3.3.2
18
+ - @object-ui/plugin-charts@3.3.2
19
+ - @object-ui/plugin-chatbot@3.3.2
20
+ - @object-ui/plugin-dashboard@3.3.2
21
+ - @object-ui/plugin-designer@3.3.2
22
+ - @object-ui/plugin-detail@3.3.2
23
+ - @object-ui/plugin-form@3.3.2
24
+ - @object-ui/plugin-grid@3.3.2
25
+ - @object-ui/plugin-kanban@3.3.2
26
+ - @object-ui/plugin-list@3.3.2
27
+ - @object-ui/plugin-report@3.3.2
28
+ - @object-ui/plugin-view@3.3.2
29
+ - @object-ui/collaboration@3.3.2
30
+
3
31
  ## 3.3.1
4
32
 
5
33
  ### 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();
@@ -178,5 +178,5 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
178
178
  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
179
  const isLast = i === extraSegments.length - 1;
180
180
  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' })] })] })] })] })] })] }));
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, 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
182
  }
@@ -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,30 +85,6 @@ 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();
@@ -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;
@@ -2,23 +2,86 @@
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
  */
6
- import * as LucideIcons from 'lucide-react';
13
+ import React from 'react';
7
14
  import { Database } from 'lucide-react';
15
+ // @ts-expect-error - lucide-react has no `exports` field; subpath types live alongside dynamic.mjs
16
+ import { DynamicIcon } from 'lucide-react/dynamic.mjs';
17
+ /** Convert PascalCase / camelCase / mixed names to kebab-case for DynamicIcon. */
18
+ function toKebab(name) {
19
+ if (name.includes('-'))
20
+ return name.toLowerCase();
21
+ return name
22
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
23
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
24
+ .toLowerCase();
25
+ }
8
26
  /**
9
- * Resolve a Lucide icon by name (kebab-case or PascalCase)
10
- * Falls back to Database icon if not found
27
+ * Aliases for icon names that exist in other icon sets (FontAwesome, Material,
28
+ * Bootstrap…) but use a different name in Lucide. Mapped to the closest
29
+ * Lucide equivalent so existing metadata authored against FA stays portable.
30
+ */
31
+ const ICON_ALIASES = {
32
+ bullseye: 'target',
33
+ bullhorn: 'megaphone',
34
+ tachometer: 'gauge',
35
+ 'tachometer-alt': 'gauge',
36
+ dashboard: 'layout-dashboard',
37
+ 'life-ring': 'life-buoy',
38
+ tasks: 'list-checks',
39
+ 'file-invoice': 'receipt',
40
+ 'file-signature': 'file-pen-line',
41
+ 'file-pdf': 'file-text',
42
+ 'check-square': 'square-check',
43
+ 'arrow-up-right-from-square': 'external-link',
44
+ 'sign-out-alt': 'log-out',
45
+ 'sign-in-alt': 'log-in',
46
+ 'cog': 'settings',
47
+ 'gears': 'settings',
48
+ 'envelope': 'mail',
49
+ 'phone-alt': 'phone',
50
+ 'map-marker-alt': 'map-pin',
51
+ 'edit': 'pencil',
52
+ 'trash-alt': 'trash-2',
53
+ 'plus-circle': 'circle-plus',
54
+ 'minus-circle': 'circle-minus',
55
+ 'times-circle': 'circle-x',
56
+ 'check-circle': 'circle-check',
57
+ 'info-circle': 'info',
58
+ 'question-circle': 'circle-help',
59
+ 'exclamation-triangle': 'triangle-alert',
60
+ };
61
+ const cache = new Map();
62
+ /**
63
+ * Resolve a Lucide icon by name (kebab-case or PascalCase).
64
+ *
65
+ * Returns a React component that lazy-loads the underlying SVG icon on
66
+ * mount. Falls back to the `Database` icon (statically imported) when no
67
+ * `name` is given.
68
+ *
69
+ * The returned component is memoised per `name` so repeated calls with the
70
+ * same name yield the same component reference (stable for React.memo).
11
71
  */
12
72
  export function getIcon(name) {
13
73
  if (!name)
14
74
  return Database;
15
- if (LucideIcons[name])
16
- return LucideIcons[name];
17
- const pascal = name
18
- .split('-')
19
- .map(p => p.charAt(0).toUpperCase() + p.slice(1))
20
- .join('');
21
- if (LucideIcons[pascal])
22
- return LucideIcons[pascal];
23
- return Database;
75
+ const cached = cache.get(name);
76
+ if (cached)
77
+ return cached;
78
+ const kebab = ICON_ALIASES[toKebab(name)] ?? toKebab(name);
79
+ const Wrapped = (props) => React.createElement(DynamicIcon, {
80
+ name: kebab,
81
+ fallback: Database,
82
+ ...props,
83
+ });
84
+ Wrapped.displayName = `LucideIcon(${name})`;
85
+ cache.set(name, Wrapped);
86
+ return Wrapped;
24
87
  }
@@ -8,7 +8,4 @@
8
8
  * - useObjectActions for toolbar create button
9
9
  * - ListView delegation for non-grid view types (kanban, calendar, chart, etc.)
10
10
  */
11
- import '@object-ui/plugin-grid';
12
- import '@object-ui/plugin-kanban';
13
- import '@object-ui/plugin-calendar';
14
11
  export declare function ObjectView({ dataSource, objects, onEdit }: any): import("react/jsx-runtime").JSX.Element;
@@ -9,16 +9,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
9
  * - useObjectActions for toolbar create button
10
10
  * - ListView delegation for non-grid view types (kanban, calendar, chart, etc.)
11
11
  */
12
- import { useMemo, useState, useCallback, useEffect } from 'react';
12
+ import { useMemo, useState, useCallback, useEffect, lazy, Suspense } from 'react';
13
13
  import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
14
- import { ObjectChart } from '@object-ui/plugin-charts';
14
+ const ObjectChart = lazy(() => import('@object-ui/plugin-charts').then((m) => ({ default: m.ObjectChart })));
15
15
  import { ListView } from '@object-ui/plugin-list';
16
16
  import { DetailView, RecordChatterPanel } from '@object-ui/plugin-detail';
17
17
  import { ObjectView as PluginObjectView, ViewTabBar } from '@object-ui/plugin-view';
18
- // Import plugins for side-effects (registration)
19
- import '@object-ui/plugin-grid';
20
- import '@object-ui/plugin-kanban';
21
- import '@object-ui/plugin-calendar';
18
+ // Plugin registration is handled by the host app (e.g. apps/console/src/main.tsx
19
+ // uses ComponentRegistry.registerLazy so heavy plugins stay code-split).
20
+ // Do NOT add eager `import '@object-ui/plugin-*'` side-effect imports here.
22
21
  import { Button, Empty, EmptyTitle, EmptyDescription, NavigationOverlay, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from '@object-ui/components';
23
22
  import { Plus, Table as TableIcon, Settings2, Wrench, KanbanSquare, Calendar, LayoutGrid, Activity, GanttChart, MapPin, BarChart3 } from 'lucide-react';
24
23
  import { getIcon } from '../utils/getIcon';
@@ -575,17 +574,17 @@ export function ObjectView({ dataSource, objects, onEdit }) {
575
574
  }
576
575
  if (viewDef.type === 'chart') {
577
576
  const chartConfig = viewDef.chart || {};
578
- return (_jsx(ObjectChart, { dataSource: ds, schema: {
579
- type: 'object-chart',
580
- objectName: objectDef.name,
581
- chartType: chartConfig.chartType,
582
- xAxisField: chartConfig.xAxisField,
583
- yAxisFields: chartConfig.yAxisFields,
584
- aggregation: chartConfig.aggregation,
585
- series: chartConfig.series,
586
- config: chartConfig.config,
587
- filter: chartConfig.filter,
588
- } }, key));
577
+ return (_jsx(Suspense, { fallback: _jsx("div", { className: "p-4 text-sm text-muted-foreground", children: "Loading chart\u2026" }), children: _jsx(ObjectChart, { dataSource: ds, schema: {
578
+ type: 'object-chart',
579
+ objectName: objectDef.name,
580
+ chartType: chartConfig.chartType,
581
+ xAxisField: chartConfig.xAxisField,
582
+ yAxisFields: chartConfig.yAxisFields,
583
+ aggregation: chartConfig.aggregation,
584
+ series: chartConfig.series,
585
+ config: chartConfig.config,
586
+ filter: chartConfig.filter,
587
+ } }) }, key));
589
588
  }
590
589
  const fullSchema = {
591
590
  ...listSchema,
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useEffect, useCallback, useMemo } from 'react';
2
+ import { useState, useEffect, useCallback, useMemo, lazy, Suspense } from 'react';
3
3
  import { useParams } from 'react-router-dom';
4
- import { ReportViewer, ReportConfigPanel } from '@object-ui/plugin-report';
4
+ const ReportViewer = lazy(() => import('@object-ui/plugin-report').then((m) => ({ default: m.ReportViewer })));
5
+ const ReportConfigPanel = lazy(() => import('@object-ui/plugin-report').then((m) => ({ default: m.ReportConfigPanel })));
5
6
  import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
6
7
  import { Pencil, BarChart3, Loader2 } from 'lucide-react';
7
8
  import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
@@ -280,5 +281,5 @@ export function ReportView({ dataSource }) {
280
281
  allowExport: true,
281
282
  loading: dataLoading, // Loading state for data fetching
282
283
  };
283
- return (_jsxs("div", { className: "flex flex-col h-full overflow-hidden bg-background", children: [_jsxs("div", { className: "flex flex-col sm:flex-row justify-between sm:items-center gap-3 sm:gap-4 p-4 sm:p-6 border-b shrink-0", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h1", { className: "text-lg sm:text-xl md:text-2xl font-bold tracking-tight truncate", children: previewReport.title || previewReport.label || 'Report Viewer' }), previewReport.description && (_jsx("p", { className: "text-sm text-muted-foreground mt-1 line-clamp-2", children: previewReport.description }))] }), _jsx("div", { className: "shrink-0 flex items-center gap-1.5", children: _jsxs("button", { type: "button", onClick: handleOpenConfigPanel, className: "inline-flex items-center gap-1.5 rounded-md border border-input bg-background px-2.5 py-1.5 text-xs font-medium text-muted-foreground shadow-sm hover:bg-accent hover:text-accent-foreground", "data-testid": "report-edit-button", children: [_jsx(Pencil, { className: "h-3.5 w-3.5" }), "Edit"] }) })] }), _jsxs("div", { className: "flex-1 overflow-hidden flex flex-col sm:flex-row relative", children: [_jsx("div", { className: "flex-1 min-w-0 overflow-auto p-4 sm:p-6 lg:p-8 bg-muted/5", children: _jsx("div", { className: "max-w-5xl mx-auto shadow-sm border rounded-lg sm:rounded-xl bg-background overflow-hidden min-h-150", children: _jsx(ReportViewer, { schema: viewerSchema }) }) }), _jsx(ReportConfigPanel, { open: configPanelOpen, onClose: handleCloseConfigPanel, config: reportConfig, onSave: handleReportConfigSave, onFieldChange: handleReportFieldChange, availableFields: availableFields }), _jsx(MetadataPanel, { open: showDebug, sections: [{ title: 'Report Configuration', data: previewReport }] })] })] }));
284
+ return (_jsxs("div", { className: "flex flex-col h-full overflow-hidden bg-background", children: [_jsxs("div", { className: "flex flex-col sm:flex-row justify-between sm:items-center gap-3 sm:gap-4 p-4 sm:p-6 border-b shrink-0", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h1", { className: "text-lg sm:text-xl md:text-2xl font-bold tracking-tight truncate", children: previewReport.title || previewReport.label || 'Report Viewer' }), previewReport.description && (_jsx("p", { className: "text-sm text-muted-foreground mt-1 line-clamp-2", children: previewReport.description }))] }), _jsx("div", { className: "shrink-0 flex items-center gap-1.5", children: _jsxs("button", { type: "button", onClick: handleOpenConfigPanel, className: "inline-flex items-center gap-1.5 rounded-md border border-input bg-background px-2.5 py-1.5 text-xs font-medium text-muted-foreground shadow-sm hover:bg-accent hover:text-accent-foreground", "data-testid": "report-edit-button", children: [_jsx(Pencil, { className: "h-3.5 w-3.5" }), "Edit"] }) })] }), _jsxs("div", { className: "flex-1 overflow-hidden flex flex-col sm:flex-row relative", children: [_jsx("div", { className: "flex-1 min-w-0 overflow-auto p-4 sm:p-6 lg:p-8 bg-muted/5", children: _jsx("div", { className: "max-w-5xl mx-auto shadow-sm border rounded-lg sm:rounded-xl bg-background overflow-hidden min-h-150", children: _jsx(Suspense, { fallback: _jsx("div", { className: "p-8 text-sm text-muted-foreground", children: "Loading report\u2026" }), children: _jsx(ReportViewer, { schema: viewerSchema }) }) }) }), _jsx(Suspense, { fallback: null, children: _jsx(ReportConfigPanel, { open: configPanelOpen, onClose: handleCloseConfigPanel, config: reportConfig, onSave: handleReportConfigSave, onFieldChange: handleReportFieldChange, availableFields: availableFields }) }), _jsx(MetadataPanel, { open: showDebug, sections: [{ title: 'Report Configuration', data: previewReport }] })] })] }));
284
285
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/app-shell",
3
- "version": "3.3.1",
3
+ "version": "3.3.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Minimal application shell for ObjectUI - framework-agnostic rendering engine",
@@ -26,34 +26,34 @@
26
26
  "dependencies": {
27
27
  "lucide-react": "^0.544.0",
28
28
  "sonner": "^2.0.7",
29
- "@object-ui/auth": "3.3.1",
30
- "@object-ui/collaboration": "3.3.1",
31
- "@object-ui/components": "3.3.1",
32
- "@object-ui/core": "3.3.1",
33
- "@object-ui/data-objectstack": "3.3.1",
34
- "@object-ui/fields": "3.3.1",
35
- "@object-ui/i18n": "3.3.1",
36
- "@object-ui/layout": "3.3.1",
37
- "@object-ui/permissions": "3.3.1",
38
- "@object-ui/react": "3.3.1",
39
- "@object-ui/types": "3.3.1"
29
+ "@object-ui/auth": "3.3.2",
30
+ "@object-ui/collaboration": "3.3.2",
31
+ "@object-ui/components": "3.3.2",
32
+ "@object-ui/core": "3.3.2",
33
+ "@object-ui/data-objectstack": "3.3.2",
34
+ "@object-ui/fields": "3.3.2",
35
+ "@object-ui/i18n": "3.3.2",
36
+ "@object-ui/layout": "3.3.2",
37
+ "@object-ui/permissions": "3.3.2",
38
+ "@object-ui/react": "3.3.2",
39
+ "@object-ui/types": "3.3.2"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "react": "^18.0.0 || ^19.0.0",
43
43
  "react-dom": "^18.0.0 || ^19.0.0",
44
44
  "react-router-dom": "^6.0.0 || ^7.0.0",
45
- "@object-ui/plugin-calendar": "3.3.1",
46
- "@object-ui/plugin-charts": "3.3.1",
47
- "@object-ui/plugin-chatbot": "3.3.1",
48
- "@object-ui/plugin-dashboard": "3.3.1",
49
- "@object-ui/plugin-designer": "3.3.1",
50
- "@object-ui/plugin-detail": "3.3.1",
51
- "@object-ui/plugin-form": "3.3.1",
52
- "@object-ui/plugin-grid": "3.3.1",
53
- "@object-ui/plugin-kanban": "3.3.1",
54
- "@object-ui/plugin-list": "3.3.1",
55
- "@object-ui/plugin-report": "3.3.1",
56
- "@object-ui/plugin-view": "3.3.1"
45
+ "@object-ui/plugin-calendar": "3.3.2",
46
+ "@object-ui/plugin-charts": "3.3.2",
47
+ "@object-ui/plugin-chatbot": "3.3.2",
48
+ "@object-ui/plugin-dashboard": "3.3.2",
49
+ "@object-ui/plugin-designer": "3.3.2",
50
+ "@object-ui/plugin-detail": "3.3.2",
51
+ "@object-ui/plugin-form": "3.3.2",
52
+ "@object-ui/plugin-grid": "3.3.2",
53
+ "@object-ui/plugin-kanban": "3.3.2",
54
+ "@object-ui/plugin-list": "3.3.2",
55
+ "@object-ui/plugin-report": "3.3.2",
56
+ "@object-ui/plugin-view": "3.3.2"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@types/node": "^25.6.0",
@@ -64,7 +64,7 @@
64
64
  "react-router-dom": "^7.14.2",
65
65
  "sonner": "^2.0.7",
66
66
  "typescript": "^6.0.3",
67
- "vite": "^6.2.2"
67
+ "vite": "^8.0.10"
68
68
  },
69
69
  "keywords": [
70
70
  "objectui",