@object-ui/app-shell 4.7.0 → 4.8.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,120 @@
1
1
  # @object-ui/app-shell — Changelog
2
2
 
3
+ ## 4.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3a17c8d: Mobile UI: aggressive chrome reduction to match real mobile-app conventions.
8
+
9
+ Real mobile CRMs (Salesforce, HubSpot, Notion, Linear) keep one row of
10
+ chrome on phones: title + 1 primary action, plus content. We were
11
+ shipping ~5 rows of toolbars + chips + tabs above the data. This commit
12
+ hides the desktop-only chrome at the `<sm` breakpoint:
13
+ - **ListView**: TabBar (view switcher), UserFilters chip row, quick-filters
14
+ chip row, Sort button, list-scoped Search popover, and the
15
+ (newly-added) mobile-only ViewSettingsPopover gear are all hidden on
16
+ phones. Only the **Filter** icon survives on mobile — paired with the
17
+ global ⌘K top-bar search, that is the entire mobile control surface.
18
+ - **Kanban**: previous commit replaced verbose swipe text with a dot
19
+ indicator; that stands.
20
+ - **ObjectView page header**: the Import (CSV upload) button is hidden
21
+ on mobile — CSV import is a desktop workflow.
22
+
23
+ Net effect on a 390px viewport: ListView toolbar collapses from
24
+ ~10 controls (5 chips + 5 icons) to a single Filter icon next to the
25
+ title; the body of the page is reachable without scrolling past 3 rows
26
+ of chrome.
27
+
28
+ Desktop and tablet behavior is unchanged.
29
+
30
+ - 51e274a: feat(app-shell,plugin-list): mobile Airtable-style topbar + filter chip row
31
+
32
+ Refactor mobile object-view layout to match the Airtable Interface
33
+ pattern:
34
+ - **AppHeader**: the mobile topbar's static page label is now a
35
+ view-switcher dropdown (`<viewName> ▾`). Tapping opens a list of
36
+ available views with icons + active-state checkmark. Falls back to
37
+ plain text when only one view exists, or when the current page has
38
+ no view-switching surface (Home, Settings, …).
39
+ - **ObjectView**: drops the standalone mobile `sm:hidden` view-select
40
+ row that previously lived between the desktop tab bar and the
41
+ content area. View switching is now exposed exclusively via the
42
+ topbar dropdown on mobile, eliminating the duplicated `object name`
43
+ vs `view name` rows.
44
+ - **ListView**: un-hides the `UserFilters` chip row on mobile.
45
+ Single-line, horizontally scrollable, matches the Airtable Interface
46
+ filter chip strip.
47
+ - New lightweight `MobileViewSwitcherContext` provides a
48
+ page → header data channel (no zustand dependency added).
49
+
50
+ Net effect on mobile (390×844):
51
+
52
+ ```
53
+ ☰ 客户卡片 ▾ 🔍 🔔 M ← topbar
54
+ 类型 ▾ 行业 ▾ 是否活跃 ▾ 更多 3 ▾ ⛛ ← chip row
55
+ [content cards] ← content
56
+ (+) ← FAB
57
+ [Leads | Accounts | Contacts | …] ← bottom nav
58
+ ```
59
+
60
+ - 7feed12: Mobile UX: Home affordance + chrome reduction
61
+
62
+ Two fixes that match what users actually need on a 390px viewport:
63
+ - **Add Home link to mobile sidebar.** When inside an app, the sidebar
64
+ drawer previously listed only the current app's nav groups, with no
65
+ way back to the home page (the desktop topbar's logo and AppSwitcher
66
+ pill are hidden on phones). Now the mobile sidebar opens with a
67
+ prominent "Home" row (`/home`) at the top, gated to mobile + app
68
+ context so the desktop layout is untouched.
69
+ - **Cut a row of top chrome.** The list/object PageHeader (icon + title
70
+ - create / import / more actions) duplicated the page title already
71
+ shown in the topbar. On mobile it's hidden entirely; the primary
72
+ create action moves to a floating "+" button anchored above the
73
+ bottom nav. Desktop still renders the full PageHeader.
74
+
75
+ - 00363fd: feat(app-shell): remove mobile bottom-tab navigation
76
+
77
+ The mobile bottom-tab strip was rendering the first 5 leaf items of
78
+ the app's navigation tree — exactly the same items that the drawer
79
+ (`☰`) surfaces, just without grouping, favourites, or recents.
80
+
81
+ Per the Notion / Linear mobile convention, we now rely on the drawer
82
+ alone. Bottom-tab strips work when they expose **orthogonal**
83
+ top-level sections (Airtable's Home / Bases / Notifications / Account)
84
+ — but ours was a duplicate of the drawer, so it was pure visual
85
+ weight: ~52px of vertical real estate, redundant taps, and clashes
86
+ with the FAB and chat-bubble stack at the bottom-right corner.
87
+
88
+ Net effect:
89
+ - Drawer remains the single source of in-app navigation.
90
+ - ~52px reclaimed for list/kanban content on every mobile screen.
91
+ - FAB and chat-bubble keep their existing offsets (no overlap;
92
+ bottom-nav was already accounted for above them).
93
+
94
+ - faba0e3: Mobile UX cleanup:
95
+ - `app-shell/AppHeader`: hide the platform-logo, app-switcher pill, and
96
+ intermediate path separators on mobile when inside an app route. The
97
+ sidebar already exposes those affordances; the topbar now reads
98
+ `☰ + page title + Search + Inbox + Avatar`.
99
+ - `plugin-list`: replace the hidden mobile TabBar with a new compact
100
+ `TabBarSelect` dropdown (current view name + chevron → menu of every
101
+ view). Phone users keep view-switching without burning a row on chip
102
+ pills. Desktop continues to render the inline TabBar.
103
+
104
+ ### Patch Changes
105
+
106
+ - @object-ui/types@4.8.0
107
+ - @object-ui/core@4.8.0
108
+ - @object-ui/i18n@4.8.0
109
+ - @object-ui/react@4.8.0
110
+ - @object-ui/components@4.8.0
111
+ - @object-ui/fields@4.8.0
112
+ - @object-ui/layout@4.8.0
113
+ - @object-ui/data-objectstack@4.8.0
114
+ - @object-ui/auth@4.8.0
115
+ - @object-ui/permissions@4.8.0
116
+ - @object-ui/collaboration@4.8.0
117
+
3
118
  ## 4.7.0
4
119
 
5
120
  ### Patch Changes
@@ -18,8 +18,8 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
18
18
  * @module
19
19
  */
20
20
  import { useLocation, useParams, Link, useNavigate } from 'react-router-dom';
21
- import { SidebarTrigger, Button, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuGroup, Avatar, AvatarImage, AvatarFallback, } from '@object-ui/components';
22
- import { Search, HelpCircle, ChevronDown, Settings, LogOut, User as UserIcon, Boxes, } from 'lucide-react';
21
+ import { SidebarTrigger, Button, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuGroup, Avatar, AvatarImage, AvatarFallback, cn, } from '@object-ui/components';
22
+ import { Search, HelpCircle, ChevronDown, Check, Lock, Settings, LogOut, User as UserIcon, Boxes, } from 'lucide-react';
23
23
  import { useState, useEffect, useCallback, useRef } from 'react';
24
24
  import { useOffline } from '@object-ui/react';
25
25
  import { PresenceAvatars } from '@object-ui/collaboration';
@@ -32,6 +32,7 @@ import { useAdapter } from '../providers/AdapterProvider';
32
32
  import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n';
33
33
  import { useAuth, getUserInitials } from '@object-ui/auth';
34
34
  import { useMetadata } from '../providers/MetadataProvider';
35
+ import { useMobileViewSwitcher } from './MobileViewSwitcherContext';
35
36
  import { useNavigationContext } from '../context/NavigationContext';
36
37
  function humanizeSlug(slug) {
37
38
  return slug.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
@@ -56,6 +57,7 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
56
57
  const { objectLabel, dashboardLabel, pageLabel, reportLabel, viewLabel } = useObjectLabel();
57
58
  const { apps: metadataApps, dashboards: metadataDashboards, pages: metadataPages, reports: metadataReports } = useMetadata();
58
59
  const { currentAppName, recordTitle } = useNavigationContext();
60
+ const mobileSwitcher = useMobileViewSwitcher();
59
61
  const [apiPresenceUsers, setApiPresenceUsers] = useState(null);
60
62
  const [apiActivities, setApiActivities] = useState(null);
61
63
  /** M10.8: in-header notifications. Polled from sys_notification scoped to current user. */
@@ -372,8 +374,16 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
372
374
  }
373
375
  }
374
376
  const lastSegmentLabel = extraSegments[extraSegments.length - 1]?.label || appName || '';
375
- 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", "aria-label": t('common.toggleSidebar') || 'Toggle sidebar' }), 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) => {
377
+ 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: cn("flex items-center justify-center h-7 w-7 shrink-0 rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors", isApp && "hidden sm:flex"), 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", "aria-label": t('common.toggleSidebar') || 'Toggle sidebar' }), activeAppName && onAppChange ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "hidden sm:flex items-center", children: _jsx(PathSep, {}) }), _jsx("div", { className: "hidden sm:flex items-center", children: _jsx(AppSwitcher, { activeAppName: activeAppName, onAppChange: onAppChange }) })] })) : appName ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "hidden sm:flex items-center", children: _jsx(PathSep, {}) }), _jsx("span", { className: "hidden sm:inline text-sm font-medium text-foreground/80 px-1.5", children: appName })] })) : null, extraSegments.map((seg, i) => {
376
378
  const isLast = i === extraSegments.length - 1;
377
379
  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));
378
- }), _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(InboxPopover, { notifications: notifications, unreadCount: unreadCount, pendingApprovalsCount: pendingApprovalsCount, activities: activeActivities, onMarkAllRead: markAllRead, onMarkRead: markNotificationRead }), _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: [" ", _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, {}), _jsx(DropdownMenuLabel, { className: "text-[11px] font-normal text-muted-foreground uppercase tracking-wide px-2", children: t('user.preferences', { defaultValue: 'Preferences' }) }), _jsxs("div", { className: "flex items-center justify-between px-2 py-1.5 text-sm", children: [_jsx("span", { className: "text-foreground/80", children: t('user.theme', { defaultValue: 'Theme' }) }), _jsx(ModeToggle, {})] }), _jsxs("div", { className: "flex items-center justify-between px-2 py-1.5 text-sm", children: [_jsx("span", { className: "text-foreground/80", children: t('user.language', { defaultValue: 'Language' }) }), _jsx(LocaleSwitcher, {})] }), isAuthEnabled && (_jsxs(_Fragment, { children: [_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' })] })] }))] })] })] })] })] }));
380
+ }), mobileSwitcher && mobileSwitcher.views.length > 0 ? (mobileSwitcher.views.length > 1 ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: "sm:hidden flex items-center gap-0.5 min-w-0 ml-1 rounded-md px-1.5 py-1 text-sm font-medium hover:bg-accent active:bg-accent/80 transition-colors", "aria-label": "Switch view", children: [_jsx("span", { className: "truncate max-w-[180px]", children: mobileSwitcher.triggerLabel ??
381
+ mobileSwitcher.views.find((v) => v.id === mobileSwitcher.activeViewId)?.label ??
382
+ lastSegmentLabel }), _jsx(ChevronDown, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" })] }) }), _jsx(DropdownMenuContent, { align: "start", className: "min-w-[220px] max-w-[280px]", children: mobileSwitcher.views.map((v) => {
383
+ const isActive = v.id === mobileSwitcher.activeViewId;
384
+ return (_jsxs(DropdownMenuItem, { onSelect: () => {
385
+ if (!isActive)
386
+ mobileSwitcher.onChange(v.id);
387
+ }, className: "gap-2", children: [v.icon ? (_jsx("span", { className: "shrink-0 text-muted-foreground [&>svg]:h-4 [&>svg]:w-4", children: v.icon })) : null, _jsx("span", { className: "flex-1 truncate", children: v.label }), v.locked ? (_jsx(Lock, { className: "h-3 w-3 shrink-0 text-muted-foreground", "aria-hidden": true })) : null, isActive ? (_jsx(Check, { className: "h-4 w-4 shrink-0 text-foreground", "aria-hidden": true })) : null] }, v.id));
388
+ }) })] })) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: mobileSwitcher.triggerLabel ?? mobileSwitcher.views[0].label }))) : (_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(InboxPopover, { notifications: notifications, unreadCount: unreadCount, pendingApprovalsCount: pendingApprovalsCount, activities: activeActivities, onMarkAllRead: markAllRead, onMarkRead: markNotificationRead }), _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: [" ", _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, {}), _jsx(DropdownMenuLabel, { className: "text-[11px] font-normal text-muted-foreground uppercase tracking-wide px-2", children: t('user.preferences', { defaultValue: 'Preferences' }) }), _jsxs("div", { className: "flex items-center justify-between px-2 py-1.5 text-sm", children: [_jsx("span", { className: "text-foreground/80", children: t('user.theme', { defaultValue: 'Theme' }) }), _jsx(ModeToggle, {})] }), _jsxs("div", { className: "flex items-center justify-between px-2 py-1.5 text-sm", children: [_jsx("span", { className: "text-foreground/80", children: t('user.language', { defaultValue: 'Language' }) }), _jsx(LocaleSwitcher, {})] }), isAuthEnabled && (_jsxs(_Fragment, { children: [_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' })] })] }))] })] })] })] })] }));
379
389
  }
@@ -193,6 +193,7 @@ export function AppSidebar({ activeAppName, onAppChange }) {
193
193
  { id: 'sys-users', label: 'Users', type: 'url', url: '/apps/setup/system/users', icon: 'users' },
194
194
  { id: 'sys-orgs', label: 'Organizations', type: 'url', url: '/apps/setup/system/organizations', icon: 'building-2' },
195
195
  { id: 'sys-roles', label: 'Roles', type: 'url', url: '/apps/setup/system/roles', icon: 'shield' },
196
+ { id: 'sys-config', label: 'Configuration', type: 'url', url: '/apps/setup/system/settings', icon: 'sliders-horizontal' },
196
197
  { id: 'sys-create-app', label: 'Create App', type: 'url', url: '/create-app', icon: 'plus' },
197
198
  ], []);
198
199
  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: t('layout.appSwitcher.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: t('layout.appSwitcher.addApp') })] }), _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: t('layout.appSwitcher.editApp') })] }), _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: t('layout.appSwitcher.manageAllApps') })] })] })] })) : (_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: t('layout.appSwitcher.systemConsole') }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: t('layout.appSwitcher.noAppsConfigured') })] })] })) }) }) }), _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) => {
@@ -16,6 +16,7 @@ import { useDiscovery } from '@object-ui/react';
16
16
  const ConsoleFloatingChatbot = lazy(() => import('./ConsoleFloatingChatbot'));
17
17
  import { UnifiedSidebar } from './UnifiedSidebar';
18
18
  import { AppHeader } from './AppHeader';
19
+ import { MobileViewSwitcherProvider } from './MobileViewSwitcherContext';
19
20
  import { useResponsiveSidebar } from '../hooks/useResponsiveSidebar';
20
21
  import { useNavigationContext } from '../context/NavigationContext';
21
22
  import { resolveI18nLabel } from '../utils';
@@ -39,15 +40,15 @@ export function ConsoleLayout({ children, activeAppName, activeApp, onAppChange,
39
40
  setContext('app');
40
41
  setCurrentAppName(activeAppName);
41
42
  }, [setContext, setCurrentAppName, activeAppName]);
42
- return (_jsxs(AppShell, { sidebar: _jsx(UnifiedSidebar, { activeAppName: activeAppName, onAppChange: onAppChange }), navbar: _jsx(AppHeader, { variant: "app", appName: appLabel, objects: objects, connectionState: connectionState, activeAppName: activeAppName, onAppChange: onAppChange }), className: "!p-0 overflow-y-auto overflow-x-hidden bg-muted/5", branding: activeApp?.branding
43
- ? {
44
- primaryColor: activeApp.branding.primaryColor,
45
- accentColor: activeApp.branding.accentColor,
46
- favicon: activeApp.branding.favicon,
47
- logo: activeApp.branding.logo,
48
- title: activeApp.label
49
- ? `${resolveI18nLabel(activeApp.label)} — ObjectStack Console`
50
- : undefined,
51
- }
52
- : undefined, children: [_jsx(ConsoleLayoutInner, { children: children }), showChatbot && (_jsx(Suspense, { fallback: null, children: _jsx(ConsoleFloatingChatbot, { appLabel: appLabel, objects: objects }) }))] }));
43
+ return (_jsx(MobileViewSwitcherProvider, { children: _jsxs(AppShell, { sidebar: _jsx(UnifiedSidebar, { activeAppName: activeAppName, onAppChange: onAppChange }), navbar: _jsx(AppHeader, { variant: "app", appName: appLabel, objects: objects, connectionState: connectionState, activeAppName: activeAppName, onAppChange: onAppChange }), className: "!p-0 overflow-y-auto overflow-x-hidden bg-muted/5", branding: activeApp?.branding
44
+ ? {
45
+ primaryColor: activeApp.branding.primaryColor,
46
+ accentColor: activeApp.branding.accentColor,
47
+ favicon: activeApp.branding.favicon,
48
+ logo: activeApp.branding.logo,
49
+ title: activeApp.label
50
+ ? `${resolveI18nLabel(activeApp.label)} — ObjectStack Console`
51
+ : undefined,
52
+ }
53
+ : undefined, children: [_jsx(ConsoleLayoutInner, { children: children }), showChatbot && (_jsx(Suspense, { fallback: null, children: _jsx(ConsoleFloatingChatbot, { appLabel: appLabel, objects: objects }) }))] }) }));
53
54
  }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * MobileViewSwitcherContext
3
+ *
4
+ * Lightweight page → header data channel that lets a view-driven page
5
+ * (e.g. `ObjectView`) expose its list of available views, the active
6
+ * view id, and a change handler to the mobile `AppHeader` topbar.
7
+ *
8
+ * On mobile (<sm), the AppHeader replaces its static page label with a
9
+ * `<viewName> ▾` dropdown trigger when a switcher has been registered.
10
+ * Desktop continues to use the inline `ViewTabBar` and ignores this
11
+ * context entirely.
12
+ *
13
+ * Design notes:
14
+ * - The provider stores a single nullable value (last registered wins).
15
+ * This matches reality — only one ObjectView is rendered at a time
16
+ * under the AppHeader. Concurrent registrations are not supported.
17
+ * - Consumers must wrap their registration object in `useMemo` (or
18
+ * pass primitive refs) so the value identity is stable across renders
19
+ * and we don't spam AppHeader re-renders.
20
+ * - When the page unmounts or no longer wants to expose a switcher,
21
+ * the effect cleanup sets the value back to `null` and AppHeader
22
+ * falls back to the static breadcrumb label.
23
+ *
24
+ * @module
25
+ */
26
+ import React from 'react';
27
+ /** Shape of a single view option in the mobile dropdown. */
28
+ export interface MobileViewSwitcherItem {
29
+ /** Stable view identifier (matches `activeViewId`). */
30
+ id: string;
31
+ /** Display label shown in the dropdown row. */
32
+ label: string;
33
+ /** Optional Lucide icon node rendered before the label. */
34
+ icon?: React.ReactNode;
35
+ /** Optional secondary hint (e.g. owner, "private"). */
36
+ hint?: string;
37
+ /** When true, show a lock indicator (e.g. permission-locked). */
38
+ locked?: boolean;
39
+ }
40
+ /** Value registered by a page (e.g. ObjectView) for the mobile switcher. */
41
+ export interface MobileViewSwitcherValue {
42
+ /** All views available on this page. May be a single entry. */
43
+ views: MobileViewSwitcherItem[];
44
+ /** Currently active view id. */
45
+ activeViewId: string;
46
+ /** Invoked when the user picks a view from the dropdown. */
47
+ onChange: (id: string) => void;
48
+ /**
49
+ * Optional override for the trigger label. Defaults to the active
50
+ * view's `label`. Provide e.g. `Object · View` if you want both.
51
+ */
52
+ triggerLabel?: string;
53
+ }
54
+ /** Provider — mount once near the top of the console tree (above AppHeader). */
55
+ export declare function MobileViewSwitcherProvider({ children }: {
56
+ children: React.ReactNode;
57
+ }): import("react/jsx-runtime").JSX.Element;
58
+ /** Read the currently registered switcher value (or `null`). */
59
+ export declare function useMobileViewSwitcher(): MobileViewSwitcherValue | null;
60
+ /**
61
+ * Register a switcher value for the duration of the calling component's
62
+ * lifetime. Passing `null` (or `undefined`) is a no-op — useful for
63
+ * conditional pages that sometimes don't want a switcher.
64
+ *
65
+ * Wrap the value in `useMemo` and ensure `onChange` is stable
66
+ * (`useCallback`) so we only re-publish when something materially changes.
67
+ */
68
+ export declare function useRegisterMobileViewSwitcher(value: MobileViewSwitcherValue | null | undefined): void;
69
+ /**
70
+ * Convenience: build + register in one call. Stabilises identity via
71
+ * useMemo so callers don't need to memoize themselves.
72
+ */
73
+ export declare function useMobileViewSwitcherRegistration(input: {
74
+ views: MobileViewSwitcherItem[];
75
+ activeViewId: string;
76
+ onChange: (id: string) => void;
77
+ triggerLabel?: string;
78
+ enabled?: boolean;
79
+ }): void;
@@ -0,0 +1,81 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * MobileViewSwitcherContext
4
+ *
5
+ * Lightweight page → header data channel that lets a view-driven page
6
+ * (e.g. `ObjectView`) expose its list of available views, the active
7
+ * view id, and a change handler to the mobile `AppHeader` topbar.
8
+ *
9
+ * On mobile (<sm), the AppHeader replaces its static page label with a
10
+ * `<viewName> ▾` dropdown trigger when a switcher has been registered.
11
+ * Desktop continues to use the inline `ViewTabBar` and ignores this
12
+ * context entirely.
13
+ *
14
+ * Design notes:
15
+ * - The provider stores a single nullable value (last registered wins).
16
+ * This matches reality — only one ObjectView is rendered at a time
17
+ * under the AppHeader. Concurrent registrations are not supported.
18
+ * - Consumers must wrap their registration object in `useMemo` (or
19
+ * pass primitive refs) so the value identity is stable across renders
20
+ * and we don't spam AppHeader re-renders.
21
+ * - When the page unmounts or no longer wants to expose a switcher,
22
+ * the effect cleanup sets the value back to `null` and AppHeader
23
+ * falls back to the static breadcrumb label.
24
+ *
25
+ * @module
26
+ */
27
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
28
+ const MobileViewSwitcherContext = createContext(null);
29
+ /** Provider — mount once near the top of the console tree (above AppHeader). */
30
+ export function MobileViewSwitcherProvider({ children }) {
31
+ const [value, setValue] = useState(null);
32
+ const ctx = useMemo(() => ({ value, setValue }), [value]);
33
+ return (_jsx(MobileViewSwitcherContext.Provider, { value: ctx, children: children }));
34
+ }
35
+ /** Read the currently registered switcher value (or `null`). */
36
+ export function useMobileViewSwitcher() {
37
+ const ctx = useContext(MobileViewSwitcherContext);
38
+ return ctx?.value ?? null;
39
+ }
40
+ /**
41
+ * Register a switcher value for the duration of the calling component's
42
+ * lifetime. Passing `null` (or `undefined`) is a no-op — useful for
43
+ * conditional pages that sometimes don't want a switcher.
44
+ *
45
+ * Wrap the value in `useMemo` and ensure `onChange` is stable
46
+ * (`useCallback`) so we only re-publish when something materially changes.
47
+ */
48
+ export function useRegisterMobileViewSwitcher(value) {
49
+ const ctx = useContext(MobileViewSwitcherContext);
50
+ const lastRef = useRef(null);
51
+ useEffect(() => {
52
+ if (!ctx)
53
+ return undefined;
54
+ const next = value ?? null;
55
+ ctx.setValue(next);
56
+ lastRef.current = next;
57
+ return () => {
58
+ // Only clear if nobody else has overwritten us in the meantime.
59
+ // (Last-mounted wins; on unmount, we still reset to null so a stale
60
+ // switcher doesn't leak into the next page.)
61
+ ctx.setValue(null);
62
+ };
63
+ }, [ctx, value]);
64
+ }
65
+ /**
66
+ * Convenience: build + register in one call. Stabilises identity via
67
+ * useMemo so callers don't need to memoize themselves.
68
+ */
69
+ export function useMobileViewSwitcherRegistration(input) {
70
+ const { views, activeViewId, onChange, triggerLabel, enabled = true } = input;
71
+ // Stabilise onChange identity if caller didn't already useCallback.
72
+ const handlerRef = useRef(onChange);
73
+ handlerRef.current = onChange;
74
+ const stableOnChange = useCallback((id) => handlerRef.current(id), []);
75
+ const value = useMemo(() => {
76
+ if (!enabled)
77
+ return null;
78
+ return { views, activeViewId, onChange: stableOnChange, triggerLabel };
79
+ }, [enabled, views, activeViewId, stableOnChange, triggerLabel]);
80
+ useRegisterMobileViewSwitcher(value);
81
+ }
@@ -16,8 +16,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
16
16
  import * as React from 'react';
17
17
  import { Link, useLocation } from 'react-router-dom';
18
18
  import { getIcon } from '../utils/getIcon';
19
- import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarGroupContent, SidebarMenu, SidebarMenuItem, SidebarMenuButton, SidebarMenuAction, SidebarTrigger, useSidebar, } from '@object-ui/components';
20
- import { Clock, Star, StarOff, ChevronRight, Layers, } from 'lucide-react';
19
+ import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarGroup, SidebarGroupLabel, SidebarGroupContent, SidebarMenu, SidebarMenuItem, SidebarMenuButton, SidebarMenuAction, SidebarTrigger, useSidebar, } from '@object-ui/components';
20
+ import { Clock, Star, StarOff, ChevronRight, Home, Layers, } from 'lucide-react';
21
21
  import { NavigationRenderer } from '@object-ui/layout';
22
22
  import { useMetadata } from '../providers/MetadataProvider';
23
23
  import { useExpressionContext, evaluateVisibility } from '../providers/ExpressionProvider';
@@ -25,7 +25,6 @@ import { usePermissions } from '@object-ui/permissions';
25
25
  import { useRecentItems } from '../hooks/useRecentItems';
26
26
  import { useFavorites } from '../hooks/useFavorites';
27
27
  import { useNavPins } from '../hooks/useNavPins';
28
- import { resolveI18nLabel } from '../utils';
29
28
  import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n';
30
29
  // useObjectLabel provides appLabel/appDescription for convention-based
31
30
  // i18n lookup — `{ns}.apps.{name}.label` resolves to the translated label
@@ -86,7 +85,7 @@ function useNavOrder(appName) {
86
85
  return { applyOrder, handleReorder };
87
86
  }
88
87
  export function UnifiedSidebar({ activeAppName }) {
89
- const { isMobile } = useSidebar();
88
+ const { isMobile, setOpenMobile } = useSidebar();
90
89
  const location = useLocation();
91
90
  const { t } = useObjectTranslation();
92
91
  const { objectLabel: resolveNavObjectLabel, dashboardLabel: resolveNavDashboardLabel, navGroupLabel: resolveNavGroupLabel } = useObjectLabel();
@@ -172,43 +171,13 @@ export function UnifiedSidebar({ activeAppName }) {
172
171
  return true;
173
172
  }, [registeredObjectNames]);
174
173
  const basePath = context === 'app' && activeApp ? `/apps/${activeApp.name}` : '';
175
- 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) => {
176
- const AreaIcon = getIcon(area.icon);
177
- const isActiveArea = area.id === activeAreaId;
178
- 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));
179
- }) }) })] })), _jsx(NavigationRenderer, { items: processedNavigation, basePath: basePath, evaluateVisibility: evalVis, checkPermission: checkPerm, checkCapability: checkCap, 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) => {
180
- const NavIcon = getIcon(item.icon);
181
- const isActive = location.pathname === item.url;
182
- 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));
183
- }) }) }) }), 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: (() => {
184
- // Flatten group items so apps that organise navigation into groups
185
- // (e.g. Setup → Overview / Administration / …) still surface real
186
- // leaf links in the mobile bottom nav instead of rendering nothing.
187
- const leaves = [];
188
- for (const item of processedNavigation) {
189
- if (item.type === 'group') {
190
- for (const child of (item.children || [])) {
191
- if (child && child.type !== 'group')
192
- leaves.push(child);
193
- }
194
- }
195
- else {
196
- leaves.push(item);
197
- }
198
- }
199
- return leaves.slice(0, 5).map((item) => {
200
- const NavIcon = getIcon(item.icon);
201
- let href = item.url || '#';
202
- if (item.type === 'object') {
203
- href = `${basePath}/${item.objectName}`;
204
- if (item.viewName)
205
- href += `/view/${item.viewName}`;
206
- }
207
- else if (item.type === 'dashboard')
208
- href = item.dashboardName ? `${basePath}/dashboard/${item.dashboardName}` : '#';
209
- else if (item.type === 'page')
210
- href = item.pageName ? `${basePath}/page/${item.pageName}` : '#';
211
- return (_jsxs(Link, { to: href, className: "flex flex-col items-center gap-0.5 px-2 py-1.5 text-muted-foreground hover:text-foreground transition-colors min-w-[44px] min-h-[44px] justify-center", children: [_jsx(NavIcon, { className: "h-5 w-5" }), _jsx("span", { className: "text-[10px] truncate max-w-[60px]", children: resolveI18nLabel(item.label, t) })] }, item.id));
212
- });
213
- })() }))] }));
174
+ return (_jsx(_Fragment, { children: _jsxs(Sidebar, { collapsible: "icon", className: "!top-14 !h-[calc(100svh-3.5rem)]", children: [isMobile && context === 'app' && (_jsx(SidebarHeader, { className: "border-b p-1.5", children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, className: "h-9 text-sm font-medium", children: _jsxs(Link, { to: "/home", onClick: () => setOpenMobile(false), "data-testid": "mobile-sidebar-home", children: [_jsx(Home, { className: "h-4 w-4" }), _jsx("span", { children: t('home.nav', { defaultValue: 'Home' }) })] }) }) }) }) })), _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) => {
175
+ const AreaIcon = getIcon(area.icon);
176
+ const isActiveArea = area.id === activeAreaId;
177
+ 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));
178
+ }) }) })] })), _jsx(NavigationRenderer, { items: processedNavigation, basePath: basePath, evaluateVisibility: evalVis, checkPermission: checkPerm, checkCapability: checkCap, 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) => {
179
+ const NavIcon = getIcon(item.icon);
180
+ const isActive = location.pathname === item.url;
181
+ 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));
182
+ }) }) }) }), 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" }) })] }) }));
214
183
  }
@@ -18,13 +18,14 @@ import { ObjectView as PluginObjectView, ViewTabBar, ManageViewsDialog } from '@
18
18
  // Plugin registration is handled by the host app (e.g. apps/console/src/main.tsx
19
19
  // uses ComponentRegistry.registerLazy so heavy plugins stay code-split).
20
20
  // Do NOT add eager `import '@object-ui/plugin-*'` side-effect imports here.
21
- import { Button, Empty, EmptyTitle, EmptyDescription, NavigationOverlay } from '@object-ui/components';
21
+ import { Button, Empty, EmptyTitle, EmptyDescription, NavigationOverlay, } from '@object-ui/components';
22
22
  import { Plus, Upload, Table as TableIcon, KanbanSquare, Calendar, LayoutGrid, Activity, GanttChart, MapPin, BarChart3 } from 'lucide-react';
23
23
  import { getIcon } from '../utils/getIcon';
24
24
  import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
25
25
  import { ViewConfigPanel } from './ViewConfigPanel';
26
26
  import { CreateViewDialog } from './CreateViewDialog';
27
27
  import { PageHeader } from '../layout/PageHeader';
28
+ import { useMobileViewSwitcherRegistration } from '../layout/MobileViewSwitcherContext';
28
29
  import { ManagedByBadge } from '../components/ManagedByBadge';
29
30
  import { RecordDetailView } from './RecordDetailView';
30
31
  import { resolveCrudAffordances } from '../utils/crudAffordances';
@@ -674,6 +675,25 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
674
675
  navigate(`view/${matchedView.id}`);
675
676
  }
676
677
  };
678
+ // Mobile view switcher — registers our view list with the AppHeader so
679
+ // the topbar can render a `<viewName> ▾` dropdown instead of the static
680
+ // page label. Desktop ignores this (ViewTabBar handles switching there).
681
+ const mobileViewSwitcherItems = useMemo(() => {
682
+ return (views || []).map((view) => {
683
+ const Icon = VIEW_TYPE_ICONS[view.type];
684
+ return {
685
+ id: view.id,
686
+ label: viewLabel(objectDef.name, view.name || view.id, view.label || view.name || view.id),
687
+ icon: Icon ? _jsx(Icon, { className: "h-4 w-4" }) : undefined,
688
+ };
689
+ });
690
+ }, [views, objectDef.name, viewLabel]);
691
+ useMobileViewSwitcherRegistration({
692
+ views: mobileViewSwitcherItems,
693
+ activeViewId: activeViewId ?? '',
694
+ onChange: handleViewChange,
695
+ enabled: mobileViewSwitcherItems.length > 0 && !!activeViewId,
696
+ });
677
697
  // ViewSwitcher callbacks — wired to both PluginObjectView instances
678
698
  const handleCreateView = useCallback(() => {
679
699
  setShowCreateViewDialog(true);
@@ -1561,27 +1581,27 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
1561
1581
  activeOrganization: activeOrganization
1562
1582
  ? { id: activeOrganization.id, slug: activeOrganization.slug, name: activeOrganization.name }
1563
1583
  : null,
1564
- }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler }, children: [_jsxs("div", { className: "h-full flex flex-col bg-background min-w-0 overflow-hidden", children: [_jsx(PageHeader, { title: _jsxs("span", { className: "inline-flex items-center gap-2", children: [_jsx("span", { className: "truncate", children: objectLabel(objectDef) }), _jsx(ManagedByBadge, { managedBy: objectDef?.managedBy })] }), description: objectDef.description ? objectDesc(objectDef) : undefined, icon: (() => { const I = getIcon(objectDef?.icon); return _jsx(I, { className: "h-4 w-4" }); })(), actions: _jsxs(_Fragment, { children: [affordances.create && can(objectDef.name, 'create') && (_jsxs(Button, { size: "sm", onClick: actions.create, className: "shadow-none gap-1.5 sm:gap-2 h-8 sm:h-9", children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: t('console.objectView.new') })] })), affordances.import && can(objectDef.name, 'create') && (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => setShowImport(true), className: "shadow-none gap-1.5 sm:gap-2 h-8 sm:h-9", title: t('console.objectView.importTitle'), "data-testid": "object-view-import-button", children: [_jsx(Upload, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: t('console.objectView.import') })] })), objectDef.actions?.some((a) => a.locations?.includes('list_toolbar')) && (_jsx(SchemaRenderer, { schema: {
1565
- type: 'action:bar',
1566
- location: 'list_toolbar',
1567
- actions: (objectDef.actions || []).map((a) => ({
1568
- ...a,
1569
- label: actionLabel(objectDef.name, a.name, a.label || a.name),
1570
- ...(a.confirmText !== undefined && {
1571
- confirmText: actionConfirm(objectDef.name, a.name, a.confirmText),
1572
- }),
1573
- ...(a.successMessage !== undefined && {
1574
- successMessage: actionSuccess(objectDef.name, a.name, a.successMessage),
1575
- }),
1576
- })),
1577
- size: 'sm',
1578
- variant: 'outline',
1579
- // On mobile, collapse all schema-driven toolbar actions
1580
- // into a single overflow menu so the icon-only New /
1581
- // Import buttons stay visible without pushing the page
1582
- // title off-screen.
1583
- mobileMaxVisible: 0,
1584
- } }))] }) }), showImport && (_jsx(Suspense, { fallback: null, children: _jsx(ImportWizard, { open: showImport, onOpenChange: setShowImport, objectName: objectDef.name, objectLabel: objectLabel(objectDef), fields: Object.entries(objectDef.fields || {}).map(([name, def]) => ({
1584
+ }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler }, children: [_jsxs("div", { className: "h-full flex flex-col bg-background min-w-0 overflow-hidden", children: [_jsx("div", { className: "hidden sm:block", children: _jsx(PageHeader, { title: _jsxs("span", { className: "inline-flex items-center gap-2", children: [_jsx("span", { className: "truncate", children: objectLabel(objectDef) }), _jsx(ManagedByBadge, { managedBy: objectDef?.managedBy })] }), description: objectDef.description ? objectDesc(objectDef) : undefined, icon: (() => { const I = getIcon(objectDef?.icon); return _jsx(I, { className: "h-4 w-4" }); })(), actions: _jsxs(_Fragment, { children: [affordances.create && can(objectDef.name, 'create') && (_jsxs(Button, { size: "sm", onClick: actions.create, className: "shadow-none gap-1.5 sm:gap-2 h-8 sm:h-9", children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: t('console.objectView.new') })] })), affordances.import && can(objectDef.name, 'create') && (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => setShowImport(true), className: "hidden sm:inline-flex shadow-none gap-1.5 sm:gap-2 h-8 sm:h-9", title: t('console.objectView.importTitle'), "data-testid": "object-view-import-button", children: [_jsx(Upload, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: t('console.objectView.import') })] })), objectDef.actions?.some((a) => a.locations?.includes('list_toolbar')) && (_jsx(SchemaRenderer, { schema: {
1585
+ type: 'action:bar',
1586
+ location: 'list_toolbar',
1587
+ actions: (objectDef.actions || []).map((a) => ({
1588
+ ...a,
1589
+ label: actionLabel(objectDef.name, a.name, a.label || a.name),
1590
+ ...(a.confirmText !== undefined && {
1591
+ confirmText: actionConfirm(objectDef.name, a.name, a.confirmText),
1592
+ }),
1593
+ ...(a.successMessage !== undefined && {
1594
+ successMessage: actionSuccess(objectDef.name, a.name, a.successMessage),
1595
+ }),
1596
+ })),
1597
+ size: 'sm',
1598
+ variant: 'outline',
1599
+ // On mobile, collapse all schema-driven toolbar actions
1600
+ // into a single overflow menu so the icon-only New /
1601
+ // Import buttons stay visible without pushing the page
1602
+ // title off-screen.
1603
+ mobileMaxVisible: 0,
1604
+ } }))] }) }) }), affordances.create && can(objectDef.name, 'create') && (_jsx("button", { type: "button", onClick: actions.create, className: "sm:hidden fixed right-4 bottom-36 z-40 h-12 w-12 rounded-full bg-primary text-primary-foreground shadow-lg active:scale-95 transition-transform inline-flex items-center justify-center", "aria-label": t('console.objectView.new'), "data-testid": "mobile-fab-create", children: _jsx(Plus, { className: "h-5 w-5" }) })), showImport && (_jsx(Suspense, { fallback: null, children: _jsx(ImportWizard, { open: showImport, onOpenChange: setShowImport, objectName: objectDef.name, objectLabel: objectLabel(objectDef), fields: Object.entries(objectDef.fields || {}).map(([name, def]) => ({
1585
1605
  name,
1586
1606
  label: def?.label || name,
1587
1607
  type: def?.type || 'text',
@@ -1619,12 +1639,12 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
1619
1639
  : undefined,
1620
1640
  };
1621
1641
  });
1622
- return (_jsxs("div", { className: "border-b px-3 sm:px-4 bg-background overflow-x-auto shrink-0", children: [_jsx(ViewTabBar, { views: viewTabItems, activeViewId: activeViewId, onViewChange: handleViewChange, viewTypeIcons: VIEW_TYPE_ICONS, config: {
1623
- reorderable: false,
1624
- showAddButton: isAdmin,
1625
- showPinnedSection: true,
1626
- showVisibilityGroups: true,
1627
- }, onAddView: isAdmin ? handleAddView : undefined, onRenameView: isAdmin ? handleRenameView : undefined, onDeleteView: isAdmin ? handleDeleteView : undefined, onDuplicateView: isAdmin ? handleDuplicateView : undefined, onPinView: isAdmin ? handlePinView : undefined, onSetDefaultView: isAdmin ? handleSetDefaultView : undefined, onConfigView: isAdmin ? handleConfigView : undefined, onManageViews: isAdmin ? () => setManageViewsOpen(true) : undefined }), isAdmin && (_jsx(ManageViewsDialog, { open: manageViewsOpen, onOpenChange: setManageViewsOpen, views: viewTabItems, activeViewId: activeViewId, viewTypeIcons: VIEW_TYPE_ICONS, onRename: handleRenameView, onDelete: handleDeleteView, onDuplicate: handleDuplicateView, onSetDefault: handleSetDefaultView, onSetPinned: handlePinView, onReorder: handleReorderViews, onAddView: handleAddView, onConfigView: handleConfigView }))] }));
1642
+ return (_jsx(_Fragment, { children: _jsxs("div", { className: "hidden sm:block border-b px-3 sm:px-4 bg-background overflow-x-auto shrink-0", children: [_jsx(ViewTabBar, { views: viewTabItems, activeViewId: activeViewId, onViewChange: handleViewChange, viewTypeIcons: VIEW_TYPE_ICONS, config: {
1643
+ reorderable: false,
1644
+ showAddButton: isAdmin,
1645
+ showPinnedSection: true,
1646
+ showVisibilityGroups: true,
1647
+ }, onAddView: isAdmin ? handleAddView : undefined, onRenameView: isAdmin ? handleRenameView : undefined, onDeleteView: isAdmin ? handleDeleteView : undefined, onDuplicateView: isAdmin ? handleDuplicateView : undefined, onPinView: isAdmin ? handlePinView : undefined, onSetDefaultView: isAdmin ? handleSetDefaultView : undefined, onConfigView: isAdmin ? handleConfigView : undefined, onManageViews: isAdmin ? () => setManageViewsOpen(true) : undefined }), isAdmin && (_jsx(ManageViewsDialog, { open: manageViewsOpen, onOpenChange: setManageViewsOpen, views: viewTabItems, activeViewId: activeViewId, viewTypeIcons: VIEW_TYPE_ICONS, onRename: handleRenameView, onDelete: handleDeleteView, onDuplicate: handleDuplicateView, onSetDefault: handleSetDefaultView, onSetPinned: handlePinView, onReorder: handleReorderViews, onAddView: handleAddView, onConfigView: handleConfigView }))] }) }));
1628
1648
  })(), _jsxs("div", { className: "flex-1 overflow-hidden relative flex flex-row", children: [navOverlay.mode === 'split' && navOverlay.isOpen ? (_jsx(NavigationOverlay, { ...navOverlay, setIsOpen: (open) => { if (!open)
1629
1649
  handleDrawerClose(); }, title: objectLabel(objectDef), onExpand: handleExpandDrawer, expandLabel: t('console.objectView.expandToPage', { defaultValue: 'Open as full page' }), storageKey: `drawer-width:${objectDef.name}`, mainContent: _jsxs("div", { className: "flex-1 min-w-0 relative h-full flex flex-col", children: [_jsx("div", { className: "flex-1 relative overflow-hidden", children: _jsx("div", { className: "h-full overflow-auto", children: _jsx(PluginObjectView, { schema: objectViewSchema, dataSource: dataSource, views: mergedViews, activeViewId: activeViewId, onViewChange: handleViewChange, onEdit: (record) => onEdit?.(record), onRowClick: (record, event) => {
1630
1650
  handleRowClick(record, event);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/app-shell",
3
- "version": "4.7.0",
3
+ "version": "4.8.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Minimal application shell for ObjectUI - framework-agnostic rendering engine",
@@ -27,34 +27,34 @@
27
27
  "dependencies": {
28
28
  "lucide-react": "^1.16.0",
29
29
  "sonner": "^2.0.7",
30
- "@object-ui/auth": "4.7.0",
31
- "@object-ui/collaboration": "4.7.0",
32
- "@object-ui/components": "4.7.0",
33
- "@object-ui/core": "4.7.0",
34
- "@object-ui/data-objectstack": "4.7.0",
35
- "@object-ui/fields": "4.7.0",
36
- "@object-ui/i18n": "4.7.0",
37
- "@object-ui/layout": "4.7.0",
38
- "@object-ui/permissions": "4.7.0",
39
- "@object-ui/react": "4.7.0",
40
- "@object-ui/types": "4.7.0"
30
+ "@object-ui/auth": "4.8.0",
31
+ "@object-ui/collaboration": "4.8.0",
32
+ "@object-ui/components": "4.8.0",
33
+ "@object-ui/core": "4.8.0",
34
+ "@object-ui/data-objectstack": "4.8.0",
35
+ "@object-ui/fields": "4.8.0",
36
+ "@object-ui/i18n": "4.8.0",
37
+ "@object-ui/layout": "4.8.0",
38
+ "@object-ui/permissions": "4.8.0",
39
+ "@object-ui/react": "4.8.0",
40
+ "@object-ui/types": "4.8.0"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "react": "^18.0.0 || ^19.0.0",
44
44
  "react-dom": "^18.0.0 || ^19.0.0",
45
45
  "react-router-dom": "^6.0.0 || ^7.0.0",
46
- "@object-ui/plugin-calendar": "^4.7.0",
47
- "@object-ui/plugin-charts": "^4.7.0",
48
- "@object-ui/plugin-chatbot": "^4.7.0",
49
- "@object-ui/plugin-dashboard": "^4.7.0",
50
- "@object-ui/plugin-designer": "^4.7.0",
51
- "@object-ui/plugin-detail": "^4.7.0",
52
- "@object-ui/plugin-form": "^4.7.0",
53
- "@object-ui/plugin-grid": "^4.7.0",
54
- "@object-ui/plugin-kanban": "^4.7.0",
55
- "@object-ui/plugin-list": "^4.7.0",
56
- "@object-ui/plugin-report": "^4.7.0",
57
- "@object-ui/plugin-view": "^4.7.0"
46
+ "@object-ui/plugin-calendar": "^4.8.0",
47
+ "@object-ui/plugin-charts": "^4.8.0",
48
+ "@object-ui/plugin-chatbot": "^4.8.0",
49
+ "@object-ui/plugin-dashboard": "^4.8.0",
50
+ "@object-ui/plugin-designer": "^4.8.0",
51
+ "@object-ui/plugin-detail": "^4.8.0",
52
+ "@object-ui/plugin-form": "^4.8.0",
53
+ "@object-ui/plugin-grid": "^4.8.0",
54
+ "@object-ui/plugin-kanban": "^4.8.0",
55
+ "@object-ui/plugin-list": "^4.8.0",
56
+ "@object-ui/plugin-report": "^4.8.0",
57
+ "@object-ui/plugin-view": "^4.8.0"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/node": "^25.9.0",