@object-ui/app-shell 6.1.0 → 6.2.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +10 -1
  3. package/dist/console/AppContent.js +24 -2
  4. package/dist/console/ai/AiChatPage.d.ts +8 -0
  5. package/dist/console/ai/AiChatPage.js +188 -0
  6. package/dist/console/ai/ConversationsSidebar.d.ts +7 -0
  7. package/dist/console/ai/ConversationsSidebar.js +111 -0
  8. package/dist/console/auth/LoginPage.js +19 -2
  9. package/dist/console/auth/RegisterPage.js +30 -1
  10. package/dist/console/marketplace/MarketplaceAccessDenied.js +3 -1
  11. package/dist/console/marketplace/MarketplaceInstalledPage.js +11 -3
  12. package/dist/console/marketplace/MarketplacePackagePage.js +57 -19
  13. package/dist/console/marketplace/MarketplacePage.js +55 -18
  14. package/dist/console/marketplace/marketplaceApi.d.ts +20 -0
  15. package/dist/console/marketplace/usePackageL10n.d.ts +38 -0
  16. package/dist/console/marketplace/usePackageL10n.js +110 -0
  17. package/dist/console/organizations/CreateWorkspaceDialog.js +29 -1
  18. package/dist/console/organizations/OrganizationsPage.js +24 -3
  19. package/dist/context/FavoritesProvider.d.ts +40 -2
  20. package/dist/context/FavoritesProvider.js +201 -20
  21. package/dist/hooks/index.d.ts +1 -0
  22. package/dist/hooks/index.js +1 -0
  23. package/dist/hooks/useChatConversation.d.ts +7 -0
  24. package/dist/hooks/useChatConversation.js +37 -5
  25. package/dist/hooks/useConversationList.d.ts +25 -0
  26. package/dist/hooks/useConversationList.js +131 -0
  27. package/dist/hooks/useNavPins.d.ts +11 -4
  28. package/dist/hooks/useNavPins.js +104 -53
  29. package/dist/index.d.ts +7 -0
  30. package/dist/index.js +14 -0
  31. package/dist/layout/AppHeader.js +2 -2
  32. package/dist/layout/AppSidebar.js +20 -1
  33. package/dist/layout/UnifiedSidebar.js +1 -1
  34. package/dist/providers/ExpressionProvider.d.ts +11 -1
  35. package/dist/providers/ExpressionProvider.js +11 -6
  36. package/dist/services/builtinComponents.d.ts +1 -0
  37. package/dist/services/builtinComponents.js +166 -0
  38. package/dist/services/componentRegistry.d.ts +63 -0
  39. package/dist/services/componentRegistry.js +36 -0
  40. package/dist/views/ComponentNavView.d.ts +6 -0
  41. package/dist/views/ComponentNavView.js +26 -0
  42. package/dist/views/RecordDetailView.js +66 -6
  43. package/dist/views/RecordFormPage.js +15 -3
  44. package/dist/views/SearchResultsPage.js +4 -0
  45. package/dist/views/metadata-admin/DesignerEditorWrapper.d.ts +58 -0
  46. package/dist/views/metadata-admin/DesignerEditorWrapper.js +140 -0
  47. package/dist/views/metadata-admin/DirectoryPage.d.ts +1 -0
  48. package/dist/views/metadata-admin/DirectoryPage.js +135 -0
  49. package/dist/views/metadata-admin/LayeredDiff.d.ts +6 -0
  50. package/dist/views/metadata-admin/LayeredDiff.js +26 -0
  51. package/dist/views/metadata-admin/PageShell.d.ts +34 -0
  52. package/dist/views/metadata-admin/PageShell.js +33 -0
  53. package/dist/views/metadata-admin/PermissionMatrixEditor.d.ts +5 -0
  54. package/dist/views/metadata-admin/PermissionMatrixEditor.js +288 -0
  55. package/dist/views/metadata-admin/QuickFind.d.ts +5 -0
  56. package/dist/views/metadata-admin/QuickFind.js +152 -0
  57. package/dist/views/metadata-admin/ResourceEditPage.d.ts +7 -0
  58. package/dist/views/metadata-admin/ResourceEditPage.js +256 -0
  59. package/dist/views/metadata-admin/ResourceHistoryPage.d.ts +5 -0
  60. package/dist/views/metadata-admin/ResourceHistoryPage.js +97 -0
  61. package/dist/views/metadata-admin/ResourceListPage.d.ts +4 -0
  62. package/dist/views/metadata-admin/ResourceListPage.js +144 -0
  63. package/dist/views/metadata-admin/ResourceRouter.d.ts +10 -0
  64. package/dist/views/metadata-admin/ResourceRouter.js +47 -0
  65. package/dist/views/metadata-admin/SchemaForm.d.ts +99 -0
  66. package/dist/views/metadata-admin/SchemaForm.js +556 -0
  67. package/dist/views/metadata-admin/default-schemas.d.ts +6 -0
  68. package/dist/views/metadata-admin/default-schemas.js +207 -0
  69. package/dist/views/metadata-admin/i18n.d.ts +33 -0
  70. package/dist/views/metadata-admin/i18n.js +303 -0
  71. package/dist/views/metadata-admin/index.d.ts +31 -0
  72. package/dist/views/metadata-admin/index.js +33 -0
  73. package/dist/views/metadata-admin/predicate.d.ts +31 -0
  74. package/dist/views/metadata-admin/predicate.js +150 -0
  75. package/dist/views/metadata-admin/registry.d.ts +125 -0
  76. package/dist/views/metadata-admin/registry.js +48 -0
  77. package/dist/views/metadata-admin/useMetadata.d.ts +37 -0
  78. package/dist/views/metadata-admin/useMetadata.js +96 -0
  79. package/dist/views/metadata-admin/widgets.d.ts +68 -0
  80. package/dist/views/metadata-admin/widgets.js +287 -0
  81. package/package.json +27 -26
@@ -0,0 +1,131 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+ /**
3
+ * Loads the signed-in user's AI conversation history.
4
+ *
5
+ * Backed by `GET /api/v1/ai/conversations` exposed by
6
+ * `@objectstack/service-ai`. The endpoint already filters by
7
+ * `req.user.userId` server-side so we just forward credentials.
8
+ */
9
+ import { useCallback, useEffect, useState } from 'react';
10
+ function extractPreview(messages) {
11
+ if (!messages || messages.length === 0)
12
+ return undefined;
13
+ // Prefer the most recent user message, fall back to last message.
14
+ for (let i = messages.length - 1; i >= 0; i--) {
15
+ const m = messages[i];
16
+ if (m.role === 'user') {
17
+ const text = stringifyContent(m.content);
18
+ if (text)
19
+ return text;
20
+ }
21
+ }
22
+ const last = messages[messages.length - 1];
23
+ return stringifyContent(last?.content);
24
+ }
25
+ function stringifyContent(content) {
26
+ if (typeof content === 'string')
27
+ return content.slice(0, 140);
28
+ if (Array.isArray(content)) {
29
+ const text = content
30
+ .map((p) => {
31
+ if (typeof p === 'string')
32
+ return p;
33
+ if (p && typeof p === 'object' && 'text' in p && typeof p.text === 'string') {
34
+ return p.text;
35
+ }
36
+ return '';
37
+ })
38
+ .join('');
39
+ return text ? text.slice(0, 140) : undefined;
40
+ }
41
+ return undefined;
42
+ }
43
+ function normalize(row) {
44
+ return {
45
+ id: row.id,
46
+ title: row.title,
47
+ agentId: row.agentId,
48
+ createdAt: row.createdAt,
49
+ updatedAt: row.updatedAt,
50
+ preview: extractPreview(row.messages),
51
+ };
52
+ }
53
+ export function useConversationList(options) {
54
+ const { userId, apiBase, limit = 50, refreshKey } = options;
55
+ const [conversations, setConversations] = useState([]);
56
+ const [isLoading, setIsLoading] = useState(Boolean(userId));
57
+ const [error, setError] = useState(undefined);
58
+ const [internalKey, setInternalKey] = useState(0);
59
+ const refetch = useCallback(async () => {
60
+ setInternalKey((k) => k + 1);
61
+ }, []);
62
+ const remove = useCallback(async (id) => {
63
+ try {
64
+ await fetch(`${apiBase}/conversations/${encodeURIComponent(id)}`, {
65
+ method: 'DELETE',
66
+ credentials: 'include',
67
+ });
68
+ }
69
+ finally {
70
+ setConversations((rows) => rows.filter((r) => r.id !== id));
71
+ }
72
+ }, [apiBase]);
73
+ const rename = useCallback(async (id, title) => {
74
+ const trimmed = title.trim();
75
+ // Optimistic update so the sidebar reflects the new title immediately.
76
+ setConversations((rows) => rows.map((r) => (r.id === id ? { ...r, title: trimmed || undefined } : r)));
77
+ const res = await fetch(`${apiBase}/conversations/${encodeURIComponent(id)}`, {
78
+ method: 'PATCH',
79
+ credentials: 'include',
80
+ headers: { 'Content-Type': 'application/json' },
81
+ body: JSON.stringify({ title: trimmed }),
82
+ });
83
+ if (!res.ok) {
84
+ // Roll back by refetching.
85
+ setInternalKey((k) => k + 1);
86
+ throw new Error(`PATCH conversation failed: ${res.status}`);
87
+ }
88
+ }, [apiBase]);
89
+ useEffect(() => {
90
+ if (!userId) {
91
+ setConversations([]);
92
+ setIsLoading(false);
93
+ setError(undefined);
94
+ return;
95
+ }
96
+ let cancelled = false;
97
+ setIsLoading(true);
98
+ setError(undefined);
99
+ (async () => {
100
+ try {
101
+ const res = await fetch(`${apiBase}/conversations?limit=${encodeURIComponent(String(limit))}`, { credentials: 'include' });
102
+ if (!res.ok)
103
+ throw new Error(`GET conversations failed: ${res.status}`);
104
+ const body = (await res.json());
105
+ const rows = Array.isArray(body)
106
+ ? body
107
+ : (body.conversations ?? body.items ?? []);
108
+ if (cancelled)
109
+ return;
110
+ const sorted = rows
111
+ .map(normalize)
112
+ .sort((a, b) => (b.updatedAt ?? '').localeCompare(a.updatedAt ?? ''));
113
+ setConversations(sorted);
114
+ }
115
+ catch (err) {
116
+ if (cancelled)
117
+ return;
118
+ setError(err instanceof Error ? err : new Error(String(err)));
119
+ setConversations([]);
120
+ }
121
+ finally {
122
+ if (!cancelled)
123
+ setIsLoading(false);
124
+ }
125
+ })();
126
+ return () => {
127
+ cancelled = true;
128
+ };
129
+ }, [userId, apiBase, limit, internalKey, refreshKey]);
130
+ return { conversations, isLoading, error, refetch, remove, rename };
131
+ }
@@ -1,16 +1,23 @@
1
1
  /**
2
2
  * useNavPins
3
3
  *
4
- * Tracks user-pinned navigation items with localStorage persistence.
5
- * Pinned items are shown at the top of the sidebar as a "Pinned" section.
6
- * Works alongside the existing `useFavorites` hook (record-level favorites).
4
+ * Thin shim over `useFavorites` that exposes the legacy pin API used by
5
+ * `NavigationRenderer`. Pin state for sidebar navigation items is stored as
6
+ * `type: 'nav'` favorites with `pinned: true` and `navId` pointing back at
7
+ * the originating `NavigationItem.id`. Backed by the same backend channel as
8
+ * regular favorites (UserDataAdapter), so pins now sync across devices.
9
+ *
10
+ * Migration from the old `objectui-nav-pins` localStorage key happens once
11
+ * on `<FavoritesProvider>` mount — see `migrateLegacyNavPins`.
12
+ *
7
13
  * @module
8
14
  */
9
15
  import type { NavigationItem } from '@object-ui/types';
10
16
  export declare function useNavPins(): {
11
17
  pinnedIds: string[];
12
- togglePin: (itemId: string, pinned: boolean) => void;
18
+ togglePin: (itemId: string, pinned: boolean, item?: NavigationItem, basePath?: string) => void;
13
19
  isPinned: (itemId: string) => boolean;
14
20
  applyPins: (items: NavigationItem[]) => NavigationItem[];
15
21
  clearPins: () => void;
16
22
  };
23
+ export type { FavoriteItem } from '../context/FavoritesProvider';
@@ -1,72 +1,123 @@
1
1
  /**
2
2
  * useNavPins
3
3
  *
4
- * Tracks user-pinned navigation items with localStorage persistence.
5
- * Pinned items are shown at the top of the sidebar as a "Pinned" section.
6
- * Works alongside the existing `useFavorites` hook (record-level favorites).
4
+ * Thin shim over `useFavorites` that exposes the legacy pin API used by
5
+ * `NavigationRenderer`. Pin state for sidebar navigation items is stored as
6
+ * `type: 'nav'` favorites with `pinned: true` and `navId` pointing back at
7
+ * the originating `NavigationItem.id`. Backed by the same backend channel as
8
+ * regular favorites (UserDataAdapter), so pins now sync across devices.
9
+ *
10
+ * Migration from the old `objectui-nav-pins` localStorage key happens once
11
+ * on `<FavoritesProvider>` mount — see `migrateLegacyNavPins`.
12
+ *
7
13
  * @module
8
14
  */
9
- import { useState, useCallback, useEffect } from 'react';
10
- const STORAGE_KEY = 'objectui-nav-pins';
15
+ import { useCallback, useMemo } from 'react';
16
+ import { useFavorites } from '../context/FavoritesProvider';
11
17
  const MAX_PINS = 20;
12
- function loadPins() {
13
- try {
14
- const raw = localStorage.getItem(STORAGE_KEY);
15
- if (!raw)
16
- return [];
17
- const parsed = JSON.parse(raw);
18
- if (!Array.isArray(parsed))
19
- return [];
20
- return parsed.filter((id) => typeof id === 'string');
21
- }
22
- catch {
23
- return [];
18
+ /**
19
+ * Synthesize the `href` for a NavigationItem in isolation from a sidebar.
20
+ *
21
+ * This duplicates a small portion of `NavigationRenderer.resolveHref` to keep
22
+ * the favorite record self-describing — useful if a future iteration surfaces
23
+ * nav-pins outside the sidebar (e.g. command palette / quick-jump). Returns
24
+ * an empty string for non-routable item types; consumers should treat empty
25
+ * href as "fall back to live nav tree".
26
+ */
27
+ function deriveHref(item, basePath = '') {
28
+ switch (item.type) {
29
+ case 'object':
30
+ if (!item.objectName)
31
+ return '';
32
+ return item.viewName
33
+ ? `${basePath}/${item.objectName}/view/${item.viewName}`
34
+ : `${basePath}/${item.objectName}`;
35
+ case 'dashboard':
36
+ return item.dashboardName ? `${basePath}/dashboard/${item.dashboardName}` : '';
37
+ case 'page':
38
+ return item.pageName ? `${basePath}/page/${item.pageName}` : '';
39
+ case 'report':
40
+ return item.reportName ? `${basePath}/report/${item.reportName}` : '';
41
+ case 'url':
42
+ return item.url ?? '';
43
+ default:
44
+ return '';
24
45
  }
25
46
  }
26
- function savePins(ids) {
27
- try {
28
- localStorage.setItem(STORAGE_KEY, JSON.stringify(ids));
29
- }
30
- catch {
31
- // Storage full — silently ignore
32
- }
47
+ function favoriteIdFor(navId) {
48
+ return `nav:${navId}`;
33
49
  }
34
50
  export function useNavPins() {
35
- const [pinnedIds, setPinnedIds] = useState(loadPins);
36
- // Sync from storage on mount
37
- useEffect(() => {
38
- setPinnedIds(loadPins());
39
- }, []);
40
- const togglePin = useCallback((itemId, pinned) => {
41
- setPinnedIds(prev => {
42
- const filtered = prev.filter(id => id !== itemId);
43
- const updated = pinned
44
- ? [...filtered, itemId].slice(0, MAX_PINS)
45
- : filtered;
46
- savePins(updated);
47
- return updated;
48
- });
49
- }, []);
50
- const isPinned = useCallback((itemId) => pinnedIds.includes(itemId), [pinnedIds]);
51
+ const { favorites, addFavorite, removeFavorite, setPinned, pinnedNavIds, } = useFavorites();
52
+ const togglePin = useCallback((itemId, pinned, item, basePath) => {
53
+ const favId = favoriteIdFor(itemId);
54
+ if (pinned) {
55
+ // If a NavigationItem is provided, register/refresh the favorite with
56
+ // proper label/href so backend sync carries portable data. Otherwise
57
+ // just flip the flag — the existing favorite (if any) keeps its data.
58
+ if (item) {
59
+ addFavorite({
60
+ id: favId,
61
+ label: item.label,
62
+ href: deriveHref(item, basePath ?? ''),
63
+ type: 'nav',
64
+ navId: itemId,
65
+ pinned: true,
66
+ });
67
+ // addFavorite is idempotent by id — for an already-present favorite
68
+ // that may have been unpinned via setPinned(false), flip the flag
69
+ // back on explicitly.
70
+ setPinned(favId, true);
71
+ }
72
+ else {
73
+ setPinned(favId, true);
74
+ }
75
+ }
76
+ else {
77
+ // Unpinning a nav favorite removes it entirely — nav-pins exist
78
+ // solely to surface the item in the sidebar Pinned section.
79
+ removeFavorite(favId);
80
+ }
81
+ }, [addFavorite, removeFavorite, setPinned]);
82
+ const isPinned = useCallback((itemId) => pinnedNavIds.has(itemId), [pinnedNavIds]);
51
83
  /**
52
- * Apply pinned state to a navigation item tree.
53
- * Returns new items with `pinned` property set based on stored pin state.
84
+ * Apply pinned state to a navigation item tree. Returns new items with
85
+ * `pinned` property set based on stored pin state. Caps the displayed pin
86
+ * count at `MAX_PINS` — additional `pinnedNavIds` (e.g. coming from another
87
+ * device with a larger cap) are silently ignored at render time.
54
88
  */
55
89
  const applyPins = useCallback((items) => {
56
- return items.map(item => {
57
- const pinned = pinnedIds.includes(item.id);
58
- const children = item.children?.length
59
- ? applyPins(item.children)
60
- : item.children;
61
- if (pinned !== (item.pinned ?? false) || children !== item.children) {
62
- return { ...item, pinned, children };
90
+ if (pinnedNavIds.size === 0) {
91
+ // Fast path: drop any stale `pinned: true` left by previous renders.
92
+ let anyStale = false;
93
+ for (const it of items)
94
+ if (it.pinned) {
95
+ anyStale = true;
96
+ break;
97
+ }
98
+ if (!anyStale)
99
+ return items;
100
+ }
101
+ let pinCount = 0;
102
+ const walk = (list) => list.map(item => {
103
+ const shouldPin = pinnedNavIds.has(item.id) && pinCount < MAX_PINS;
104
+ if (shouldPin)
105
+ pinCount++;
106
+ const children = item.children?.length ? walk(item.children) : item.children;
107
+ if (shouldPin !== (item.pinned ?? false) || children !== item.children) {
108
+ return { ...item, pinned: shouldPin, children };
63
109
  }
64
110
  return item;
65
111
  });
66
- }, [pinnedIds]);
112
+ return walk(items);
113
+ }, [pinnedNavIds]);
67
114
  const clearPins = useCallback(() => {
68
- setPinnedIds([]);
69
- savePins([]);
70
- }, []);
115
+ // Remove every nav-pin favorite — content favorites are untouched.
116
+ for (const f of favorites) {
117
+ if (f.type === 'nav')
118
+ removeFavorite(f.id);
119
+ }
120
+ }, [favorites, removeFavorite]);
121
+ const pinnedIds = useMemo(() => Array.from(pinnedNavIds), [pinnedNavIds]);
71
122
  return { pinnedIds, togglePin, isPinned, applyPins, clearPins };
72
123
  }
package/dist/index.d.ts CHANGED
@@ -43,3 +43,10 @@ export { MembersPage as DefaultMembersPage } from './console/organizations/manag
43
43
  export { InvitationsPage as DefaultInvitationsPage } from './console/organizations/manage/InvitationsPage';
44
44
  export { SettingsPage as DefaultSettingsPage } from './console/organizations/manage/SettingsPage';
45
45
  export { AcceptInvitationPage as DefaultAcceptInvitationPage } from './console/organizations/manage/AcceptInvitationPage';
46
+ export { AiChatPage as DefaultAiChatPage, AiChatPage } from './console/ai/AiChatPage';
47
+ export { ConversationsSidebar } from './console/ai/ConversationsSidebar';
48
+ export { registerAppComponent, getAppComponent, listAppComponents, componentRefToUrlSegments, urlSegmentsToComponentRef, } from './services/componentRegistry';
49
+ export type { AppComponentRegistryEntry } from './services/componentRegistry';
50
+ import './services/builtinComponents';
51
+ export { MetadataDirectoryPage, MetadataResourceRouter, MetadataResourceListPage, MetadataResourceEditPage, MetadataResourceHistoryPage, MetadataQuickFind, MetadataPageShell, SchemaForm, LayeredDiff, registerMetadataResource, getMetadataResource, listMetadataResources, resolveResourceConfig, useMetadataClient, useMetadataTypes, useTypesIndex, matchesQuery, } from './views/metadata-admin';
52
+ export type { MetadataResourceConfig, MetadataDomain, RichMetadataTypeEntry, } from './views/metadata-admin';
package/dist/index.js CHANGED
@@ -49,3 +49,17 @@ export { MembersPage as DefaultMembersPage } from './console/organizations/manag
49
49
  export { InvitationsPage as DefaultInvitationsPage } from './console/organizations/manage/InvitationsPage';
50
50
  export { SettingsPage as DefaultSettingsPage } from './console/organizations/manage/SettingsPage';
51
51
  export { AcceptInvitationPage as DefaultAcceptInvitationPage } from './console/organizations/manage/AcceptInvitationPage';
52
+ export { AiChatPage as DefaultAiChatPage, AiChatPage } from './console/ai/AiChatPage';
53
+ export { ConversationsSidebar } from './console/ai/ConversationsSidebar';
54
+ // Phase 3b: Component nav registry — plugins use this to register
55
+ // admin/setup UI surfaces that are addressable from App metadata via
56
+ // `{ type: 'component', componentRef: 'ns:name' }` nav items.
57
+ export { registerAppComponent, getAppComponent, listAppComponents, componentRefToUrlSegments, urlSegmentsToComponentRef, } from './services/componentRegistry';
58
+ // Side-effect import: registers built-in admin components
59
+ // (metadata:directory, metadata:resource) at module load.
60
+ import './services/builtinComponents';
61
+ // Phase 3c — generic metadata admin engine. Re-exported so plugins
62
+ // can call `registerMetadataResource()` to override the per-type
63
+ // list / edit / create components, and host apps can compose the
64
+ // page primitives directly when needed.
65
+ export { MetadataDirectoryPage, MetadataResourceRouter, MetadataResourceListPage, MetadataResourceEditPage, MetadataResourceHistoryPage, MetadataQuickFind, MetadataPageShell, SchemaForm, LayeredDiff, registerMetadataResource, getMetadataResource, listMetadataResources, resolveResourceConfig, useMetadataClient, useMetadataTypes, useTypesIndex, matchesQuery, } from './views/metadata-admin';
@@ -19,7 +19,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
19
19
  */
20
20
  import { useLocation, useParams, Link, useNavigate } from 'react-router-dom';
21
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';
22
+ import { Search, HelpCircle, ChevronDown, Check, Lock, Settings, LogOut, User as UserIcon, Boxes, Bot, } from 'lucide-react';
23
23
  import { useState, useEffect, useCallback, useRef } from 'react';
24
24
  import { useOffline } from '@object-ui/react';
25
25
  import { PresenceAvatars, useTenantPresence } from '@object-ui/collaboration';
@@ -389,5 +389,5 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
389
389
  if (!isActive)
390
390
  mobileSwitcher.onChange(v.id);
391
391
  }, 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));
392
- }) })] })) : (_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' })] })] }))] })] })] })] })] }));
392
+ }) })] })) : (_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 shrink-0", asChild: true, "aria-label": t('topbar.aiAssistant', { defaultValue: 'AI Assistant' }), children: _jsx(Link, { to: "/ai", children: _jsx(Bot, { className: "h-4 w-4" }) }) }), _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' })] })] }))] })] })] })] })] }));
393
393
  }
@@ -204,7 +204,7 @@ export function AppSidebar({ activeAppName, onAppChange }) {
204
204
  const AreaIcon = getIcon(area.icon);
205
205
  const isActiveArea = area.id === activeAreaId;
206
206
  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));
207
- }) }) })] })), _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, checkCapability: checkCap, searchQuery: navSearchQuery, enablePinning: true, onPinToggle: togglePin, enableReorder: true, onReorder: handleReorder, resolveObjectLabel: (objectName, fallback) => resolveNavObjectLabel({ name: objectName, label: fallback }), resolveViewLabel: (objectName, viewName, fallback) => resolveNavViewLabel(objectName, viewName, 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) => {
207
+ }) }) })] })), _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, checkCapability: checkCap, searchQuery: navSearchQuery, enablePinning: true, onPinToggle: togglePin, enableReorder: true, onReorder: handleReorder, resolveObjectLabel: (objectName, fallback) => resolveNavObjectLabel({ name: objectName, label: fallback }), resolveViewLabel: (objectName, viewName, fallback) => resolveNavViewLabel(objectName, viewName, 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.some(f => f.type !== 'nav') && (_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.filter(f => f.type !== 'nav').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) => {
208
208
  const NavIcon = getIcon(item.icon);
209
209
  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));
210
210
  }) }) })] })) }), _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" }), t('user.settings', { defaultValue: 'Settings' })] }) }), 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' })] })] }))] })] }) }) }) })] }), 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: (() => {
@@ -237,6 +237,25 @@ export function AppSidebar({ activeAppName, onAppChange }) {
237
237
  href = item.dashboardName ? `${baseUrl}/dashboard/${item.dashboardName}` : '#';
238
238
  else if (item.type === 'page')
239
239
  href = item.pageName ? `${baseUrl}/page/${item.pageName}` : '#';
240
+ else if (item.type === 'component') {
241
+ const ref = item.componentRef;
242
+ if (ref) {
243
+ const segs = ref.split(':').filter(Boolean);
244
+ href = `${baseUrl}/component/${segs.join('/')}`;
245
+ const navParams = item.params;
246
+ if (navParams) {
247
+ const usp = new URLSearchParams();
248
+ for (const [k, v] of Object.entries(navParams)) {
249
+ if (v === undefined || v === null)
250
+ continue;
251
+ usp.set(k, typeof v === 'string' ? v : JSON.stringify(v));
252
+ }
253
+ const qs = usp.toString();
254
+ if (qs)
255
+ href += `?${qs}`;
256
+ }
257
+ }
258
+ }
240
259
  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));
241
260
  });
242
261
  })() }))] }));
@@ -175,7 +175,7 @@ export function UnifiedSidebar({ activeAppName }) {
175
175
  const AreaIcon = getIcon(area.icon);
176
176
  const isActiveArea = area.id === activeAreaId;
177
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, resolveViewLabel: (objectName, viewName, fallback) => resolveNavViewLabel(objectName, viewName, 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" }), 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) => {
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, resolveItemLabel: activeApp ? (itemId, fallback) => resolveNavGroupLabel(activeApp.name, itemId, fallback) : undefined, resolveViewLabel: (objectName, viewName, fallback) => resolveNavViewLabel(objectName, viewName, 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" }), 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.some(f => f.type !== 'nav') && (_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.filter(f => f.type !== 'nav').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
179
  const NavIcon = getIcon(item.icon);
180
180
  const isActive = location.pathname === item.url;
181
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));
@@ -22,6 +22,15 @@ export interface ExpressionContextValue {
22
22
  app: Record<string, any>;
23
23
  /** Additional data scope */
24
24
  data: Record<string, any>;
25
+ /**
26
+ * Deployment-level feature flags surfaced by `/api/v1/auth/config`
27
+ * (e.g. `multiOrgEnabled`). Used by CEL/template predicates on
28
+ * metadata actions and views to hide entries that would otherwise
29
+ * hit a forbidden endpoint. Empty `{}` when auth config hasn't
30
+ * loaded yet — predicates should default to "visible" in that case
31
+ * (see `sys_organization.create_organization.visible`).
32
+ */
33
+ features: Record<string, any>;
25
34
  /** The evaluator instance (for imperative use) */
26
35
  evaluator: ExpressionEvaluator;
27
36
  }
@@ -30,8 +39,9 @@ interface ExpressionProviderProps {
30
39
  user?: Record<string, any>;
31
40
  app?: Record<string, any>;
32
41
  data?: Record<string, any>;
42
+ features?: Record<string, any>;
33
43
  }
34
- export declare function ExpressionProvider({ children, user, app, data }: ExpressionProviderProps): import("react/jsx-runtime").JSX.Element;
44
+ export declare function ExpressionProvider({ children, user, app, data, features }: ExpressionProviderProps): import("react/jsx-runtime").JSX.Element;
35
45
  /**
36
46
  * Hook to access the expression context.
37
47
  * Returns the full context value or a default empty context.
@@ -16,14 +16,19 @@ import { jsx as _jsx } from "react/jsx-runtime";
16
16
  */
17
17
  import { createContext, useContext, useMemo } from 'react';
18
18
  import { ExpressionEvaluator } from '@object-ui/core';
19
+ import { PredicateScopeProvider } from '@object-ui/react';
19
20
  const ExprCtx = createContext(null);
20
- export function ExpressionProvider({ children, user = {}, app = {}, data = {} }) {
21
+ export function ExpressionProvider({ children, user = {}, app = {}, data = {}, features = {} }) {
21
22
  const value = useMemo(() => {
22
- const context = { user, app, data };
23
+ const context = { user, app, data, features };
23
24
  const evaluator = new ExpressionEvaluator(context);
24
- return { user, app, data, evaluator };
25
- }, [user, app, data]);
26
- return _jsx(ExprCtx.Provider, { value: value, children: children });
25
+ return { user, app, data, features, evaluator };
26
+ }, [user, app, data, features]);
27
+ // Also feed the predicate scope used by useCondition/useExpression in
28
+ // @object-ui/react so action visibility predicates (e.g. on toolbar
29
+ // buttons) can see deployment-level flags like features.multiOrgEnabled.
30
+ const scope = useMemo(() => ({ user, app, data, features }), [user, app, data, features]);
31
+ return (_jsx(ExprCtx.Provider, { value: value, children: _jsx(PredicateScopeProvider, { scope: scope, children: children }) }));
27
32
  }
28
33
  /**
29
34
  * Hook to access the expression context.
@@ -33,7 +38,7 @@ export function useExpressionContext() {
33
38
  const ctx = useContext(ExprCtx);
34
39
  if (!ctx) {
35
40
  // Return a safe default so components can be used outside the provider
36
- const fallback = { user: {}, app: {}, data: {} };
41
+ const fallback = { user: {}, app: {}, data: {}, features: {} };
37
42
  return { ...fallback, evaluator: new ExpressionEvaluator(fallback) };
38
43
  }
39
44
  return ctx;
@@ -0,0 +1 @@
1
+ export {};