@object-ui/app-shell 4.0.7 → 4.0.9
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 +76 -0
- package/dist/console/AppContent.js +4 -2
- package/dist/hooks/useObjectActions.js +26 -2
- package/dist/layout/AppHeader.js +14 -8
- package/dist/layout/AppSidebar.js +2 -2
- package/dist/layout/AppSwitcher.js +1 -1
- package/dist/layout/PageHeader.d.ts +42 -0
- package/dist/layout/PageHeader.js +13 -0
- package/dist/layout/index.d.ts +2 -0
- package/dist/layout/index.js +1 -0
- package/dist/utils/getIcon.d.ts +4 -1
- package/dist/utils/getIcon.js +11 -2
- package/dist/views/CreateViewDialog.js +162 -23
- package/dist/views/DashboardView.js +69 -22
- package/dist/views/ObjectView.d.ts +1 -1
- package/dist/views/ObjectView.js +255 -30
- package/dist/views/RecordDetailView.js +90 -8
- package/dist/views/RecordFormPage.js +20 -1
- package/dist/views/ReportView.js +68 -16
- package/dist/views/ViewConfigPanel.js +24 -6
- package/package.json +29 -29
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,81 @@
|
|
|
1
1
|
# @object-ui/app-shell — Changelog
|
|
2
2
|
|
|
3
|
+
## 4.0.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 19c044f: i18n
|
|
8
|
+
- @object-ui/types@4.0.9
|
|
9
|
+
- @object-ui/core@4.0.9
|
|
10
|
+
- @object-ui/i18n@4.0.9
|
|
11
|
+
- @object-ui/react@4.0.9
|
|
12
|
+
- @object-ui/components@4.0.9
|
|
13
|
+
- @object-ui/fields@4.0.9
|
|
14
|
+
- @object-ui/layout@4.0.9
|
|
15
|
+
- @object-ui/data-objectstack@4.0.9
|
|
16
|
+
- @object-ui/auth@4.0.9
|
|
17
|
+
- @object-ui/permissions@4.0.9
|
|
18
|
+
- @object-ui/plugin-calendar@4.0.9
|
|
19
|
+
- @object-ui/plugin-charts@4.0.9
|
|
20
|
+
- @object-ui/plugin-chatbot@4.0.9
|
|
21
|
+
- @object-ui/plugin-dashboard@4.0.9
|
|
22
|
+
- @object-ui/plugin-designer@4.0.9
|
|
23
|
+
- @object-ui/plugin-detail@4.0.9
|
|
24
|
+
- @object-ui/plugin-form@4.0.9
|
|
25
|
+
- @object-ui/plugin-grid@4.0.9
|
|
26
|
+
- @object-ui/plugin-kanban@4.0.9
|
|
27
|
+
- @object-ui/plugin-list@4.0.9
|
|
28
|
+
- @object-ui/plugin-report@4.0.9
|
|
29
|
+
- @object-ui/plugin-view@4.0.9
|
|
30
|
+
- @object-ui/collaboration@4.0.9
|
|
31
|
+
|
|
32
|
+
## 4.0.8
|
|
33
|
+
|
|
34
|
+
### Patch Changes
|
|
35
|
+
|
|
36
|
+
- 3d58eaa: fix(auth,app-shell): hide Log out menu item when auth is disabled (guest/preview mode)
|
|
37
|
+
|
|
38
|
+
When the console runs against a server with `discovery.services.auth.enabled === false`
|
|
39
|
+
(or in preview mode), `AuthProvider` hardcodes `isAuthenticated: true` and the mock
|
|
40
|
+
`signOut()` has no real backend. Previously, clicking "Log out" in the user menu had
|
|
41
|
+
no visible effect — the user/session were nulled but the UI stayed authenticated.
|
|
42
|
+
|
|
43
|
+
Changes:
|
|
44
|
+
- **`@object-ui/auth`** — added `isAuthEnabled: boolean` to `AuthContextValue`
|
|
45
|
+
(`true` only when real auth is in use, `false` for guest/preview modes).
|
|
46
|
+
- **`@object-ui/app-shell`** — `AppHeader` and `AppSidebar` now hide the "Log out"
|
|
47
|
+
menu item entirely when `!isAuthEnabled`, so users aren't presented with an action
|
|
48
|
+
that can't actually do anything. Also fixed two missed i18n strings in
|
|
49
|
+
`AppSidebar` ("Settings", "Log out").
|
|
50
|
+
- **`@object-ui/i18n`** — added `user.{profile,settings,logout}` namespace to all
|
|
51
|
+
10 built-in locales (en/zh translated; ja/ko/de/fr/es/pt/ru/ar fall back to
|
|
52
|
+
English pending native translation).
|
|
53
|
+
|
|
54
|
+
- Updated dependencies [3d58eaa]
|
|
55
|
+
- @object-ui/auth@4.0.8
|
|
56
|
+
- @object-ui/i18n@4.0.8
|
|
57
|
+
- @object-ui/components@4.0.8
|
|
58
|
+
- @object-ui/fields@4.0.8
|
|
59
|
+
- @object-ui/plugin-calendar@4.0.8
|
|
60
|
+
- @object-ui/plugin-charts@4.0.8
|
|
61
|
+
- @object-ui/plugin-dashboard@4.0.8
|
|
62
|
+
- @object-ui/plugin-designer@4.0.8
|
|
63
|
+
- @object-ui/plugin-list@4.0.8
|
|
64
|
+
- @object-ui/react@4.0.8
|
|
65
|
+
- @object-ui/layout@4.0.8
|
|
66
|
+
- @object-ui/plugin-chatbot@4.0.8
|
|
67
|
+
- @object-ui/plugin-detail@4.0.8
|
|
68
|
+
- @object-ui/plugin-form@4.0.8
|
|
69
|
+
- @object-ui/plugin-grid@4.0.8
|
|
70
|
+
- @object-ui/plugin-kanban@4.0.8
|
|
71
|
+
- @object-ui/plugin-report@4.0.8
|
|
72
|
+
- @object-ui/plugin-view@4.0.8
|
|
73
|
+
- @object-ui/types@4.0.8
|
|
74
|
+
- @object-ui/core@4.0.8
|
|
75
|
+
- @object-ui/data-objectstack@4.0.8
|
|
76
|
+
- @object-ui/permissions@4.0.8
|
|
77
|
+
- @object-ui/collaboration@4.0.8
|
|
78
|
+
|
|
3
79
|
## 4.0.7
|
|
4
80
|
|
|
5
81
|
### Patch Changes
|
|
@@ -264,7 +264,7 @@ export function AppContent({ extraRoutes, extraRoutesNoApp } = {}) {
|
|
|
264
264
|
const expressionUser = user
|
|
265
265
|
? { name: user.name, email: user.email, role: user.role ?? 'user' }
|
|
266
266
|
: { name: 'Anonymous', email: '', role: 'guest' };
|
|
267
|
-
return (_jsxs(ExpressionProvider, { user: expressionUser, app: activeApp, data: {}, children: [_jsx(NavigationSyncEffect, {}), _jsxs(ConsoleLayout, { activeAppName: activeApp.name, activeApp: activeApp, onAppChange: handleAppChange, objects: allObjects, connectionState: connectionState, children: [_jsx(CommandPalette, { apps: apps, activeApp: activeApp, objects: allObjects, onAppChange: handleAppChange }), _jsx(KeyboardShortcutsDialog, {}), _jsx(OnboardingWalkthrough, {}), _jsx(ErrorBoundary, { children: _jsx(Suspense, { fallback: _jsx(LoadingScreen, {}), children: _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(Navigate, { to: findFirstRoute(activeApp.navigation || []), replace: true }) }), _jsx(Route, { path: ":objectName", element: _jsx(ObjectView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit }) }), _jsx(Route, { path: ":objectName/new", element: _jsx(RecordFormPage, { mode: "create" }) }), _jsx(Route, { path: ":objectName/view/:viewId", element: _jsx(ObjectView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit }) }), _jsx(Route, { path: ":objectName/record/:recordId", element: _jsx(RecordDetailView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit }, refreshKey) }), _jsx(Route, { path: ":objectName/record/:recordId/edit", element: _jsx(RecordFormPage, { mode: "edit" }) }), _jsx(Route, { path: "dashboard/:dashboardName", element: _jsx(DashboardView, { dataSource: dataSource }) }), _jsx(Route, { path: "report/:reportName", element: _jsx(ReportView, { dataSource: dataSource }) }), _jsx(Route, { path: "page/:pageName", element: _jsx(PageView, {}) }), _jsx(Route, { path: "design/page/:pageName", element: _jsx(PageDesignPage, {}) }), _jsx(Route, { path: "design/dashboard/:dashboardName", element: _jsx(DashboardDesignPage, {}) }), _jsx(Route, { path: "search", element: _jsx(SearchResultsPage, {}) }), _jsx(Route, { path: "create-app", element: _jsx(CreateAppPage, {}) }), _jsx(Route, { path: "edit-app/:editAppName", element: _jsx(EditAppPage, {}) }), extraRoutes] }) }) }), currentObjectDef && (_jsx(ModalForm, { schema: {
|
|
267
|
+
return (_jsxs(ExpressionProvider, { user: expressionUser, app: activeApp, data: {}, children: [_jsx(NavigationSyncEffect, {}), _jsxs(ConsoleLayout, { activeAppName: activeApp.name, activeApp: activeApp, onAppChange: handleAppChange, objects: allObjects, connectionState: connectionState, children: [_jsx(CommandPalette, { apps: apps, activeApp: activeApp, objects: allObjects, onAppChange: handleAppChange }), _jsx(KeyboardShortcutsDialog, {}), _jsx(OnboardingWalkthrough, {}), _jsx(ErrorBoundary, { children: _jsx(Suspense, { fallback: _jsx(LoadingScreen, {}), children: _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(Navigate, { to: findFirstRoute(activeApp.navigation || []), replace: true }) }), _jsx(Route, { path: ":objectName", element: _jsx(ObjectView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit, externalRefreshKey: refreshKey }) }), _jsx(Route, { path: ":objectName/new", element: _jsx(RecordFormPage, { mode: "create" }) }), _jsx(Route, { path: ":objectName/view/:viewId", element: _jsx(ObjectView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit, externalRefreshKey: refreshKey }) }), _jsx(Route, { path: ":objectName/record/:recordId", element: _jsx(RecordDetailView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit }, refreshKey) }), _jsx(Route, { path: ":objectName/record/:recordId/edit", element: _jsx(RecordFormPage, { mode: "edit" }) }), _jsx(Route, { path: "dashboard/:dashboardName", element: _jsx(DashboardView, { dataSource: dataSource }) }), _jsx(Route, { path: "report/:reportName", element: _jsx(ReportView, { dataSource: dataSource }) }), _jsx(Route, { path: "page/:pageName", element: _jsx(PageView, {}) }), _jsx(Route, { path: "design/page/:pageName", element: _jsx(PageDesignPage, {}) }), _jsx(Route, { path: "design/dashboard/:dashboardName", element: _jsx(DashboardDesignPage, {}) }), _jsx(Route, { path: "search", element: _jsx(SearchResultsPage, {}) }), _jsx(Route, { path: "create-app", element: _jsx(CreateAppPage, {}) }), _jsx(Route, { path: "edit-app/:editAppName", element: _jsx(EditAppPage, {}) }), extraRoutes] }) }) }), currentObjectDef && (_jsx(ModalForm, { schema: {
|
|
268
268
|
type: 'object-form',
|
|
269
269
|
formType: 'modal',
|
|
270
270
|
objectName: currentObjectDef.name,
|
|
@@ -296,7 +296,9 @@ export function AppContent({ extraRoutes, extraRoutesNoApp } = {}) {
|
|
|
296
296
|
onCancel: handleDialogCancel,
|
|
297
297
|
showSubmit: true,
|
|
298
298
|
showCancel: true,
|
|
299
|
-
submitText:
|
|
299
|
+
submitText: editingRecord
|
|
300
|
+
? t('form.update', { defaultValue: t('common.save') || '保存' })
|
|
301
|
+
: t('form.create', { defaultValue: t('common.create') || '创建' }),
|
|
300
302
|
cancelText: t('common.cancel'),
|
|
301
303
|
}, dataSource: dataSource }, editingRecord?.id || 'new'))] })] }));
|
|
302
304
|
}
|
|
@@ -41,11 +41,35 @@ export function useObjectActions({ objectName, objectLabel, dataSource, onEdit,
|
|
|
41
41
|
// Accept several param shapes used across call sites:
|
|
42
42
|
// { params: { recordId } } — toolbar / programmatic deletes
|
|
43
43
|
// { params: { record } } — ObjectGrid row dropdown
|
|
44
|
-
// { params: { records: [...] } } — bulk delete
|
|
44
|
+
// { params: { records: [...] } } — bulk delete (multi-row)
|
|
45
45
|
// { recordId } (legacy) — pre-params shape
|
|
46
|
+
const records = Array.isArray(action.params?.records)
|
|
47
|
+
? action.params.records.filter((r) => r?.id != null)
|
|
48
|
+
: null;
|
|
49
|
+
// Bulk path — delete every record in parallel and report a summary.
|
|
50
|
+
if (records && records.length > 1) {
|
|
51
|
+
const results = await Promise.allSettled(records.map((r) => dataSource.delete(objectName, r.id)));
|
|
52
|
+
const failed = results.filter(r => r.status === 'rejected').length;
|
|
53
|
+
const succeeded = results.length - failed;
|
|
54
|
+
onRefresh?.();
|
|
55
|
+
if (failed === 0) {
|
|
56
|
+
toast.success(t('objectActions.bulkDeleteSuccess', {
|
|
57
|
+
count: succeeded,
|
|
58
|
+
label: objectLabel || objectName,
|
|
59
|
+
defaultValue: `Deleted ${succeeded} ${objectLabel || objectName} records`,
|
|
60
|
+
}));
|
|
61
|
+
return { success: true, reload: true };
|
|
62
|
+
}
|
|
63
|
+
toast.error(t('objectActions.bulkDeletePartial', {
|
|
64
|
+
succeeded,
|
|
65
|
+
failed,
|
|
66
|
+
defaultValue: `${succeeded} deleted, ${failed} failed`,
|
|
67
|
+
}));
|
|
68
|
+
return { success: false, error: `${failed} failed` };
|
|
69
|
+
}
|
|
46
70
|
const recordId = action.params?.recordId ??
|
|
47
71
|
action.params?.record?.id ??
|
|
48
|
-
|
|
72
|
+
records?.[0]?.id ??
|
|
49
73
|
action.recordId;
|
|
50
74
|
if (!recordId)
|
|
51
75
|
return { success: false, error: t('objectActions.noRecordId') };
|
package/dist/layout/AppHeader.js
CHANGED
|
@@ -50,10 +50,10 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
|
|
|
50
50
|
const params = useParams();
|
|
51
51
|
const navigate = useNavigate();
|
|
52
52
|
const { isOnline } = useOffline();
|
|
53
|
-
const { user, signOut, organizations, activeOrganization, isOrganizationsLoading, } = useAuth();
|
|
53
|
+
const { user, signOut, isAuthEnabled, organizations, activeOrganization, isOrganizationsLoading, } = useAuth();
|
|
54
54
|
const dataSource = useAdapter();
|
|
55
55
|
const { t } = useObjectTranslation();
|
|
56
|
-
const { objectLabel, dashboardLabel, pageLabel, reportLabel } = useObjectLabel();
|
|
56
|
+
const { objectLabel, dashboardLabel, pageLabel, reportLabel, viewLabel } = useObjectLabel();
|
|
57
57
|
const { apps: metadataApps, dashboards: metadataDashboards, pages: metadataPages, reports: metadataReports } = useMetadata();
|
|
58
58
|
const { currentAppName, recordTitle } = useNavigationContext();
|
|
59
59
|
const [apiPresenceUsers, setApiPresenceUsers] = useState(null);
|
|
@@ -66,10 +66,15 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
|
|
|
66
66
|
const fetchPresenceAndActivities = useCallback(async () => {
|
|
67
67
|
if (!dataSource || !isApp)
|
|
68
68
|
return;
|
|
69
|
+
// ObjectStack client throws Error objects with `httpStatus` (not `status`)
|
|
70
|
+
// and a `code` like `object_not_found` when the underlying object isn't
|
|
71
|
+
// registered on the server. Either signal means the feature is
|
|
72
|
+
// unavailable — disable it for the rest of the page.
|
|
73
|
+
const isMissingResource = (err) => err?.httpStatus === 404 || err?.status === 404 || err?.code === 'object_not_found';
|
|
69
74
|
const presenceP = presenceUnavailableRef.current
|
|
70
75
|
? Promise.resolve({ data: [] })
|
|
71
76
|
: dataSource.find('sys_presence').catch((err) => {
|
|
72
|
-
if (err
|
|
77
|
+
if (isMissingResource(err))
|
|
73
78
|
presenceUnavailableRef.current = true;
|
|
74
79
|
return { data: [] };
|
|
75
80
|
});
|
|
@@ -78,7 +83,7 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
|
|
|
78
83
|
: dataSource
|
|
79
84
|
.find('sys_activity', { $orderby: { timestamp: 'desc' }, $top: 20 })
|
|
80
85
|
.catch((err) => {
|
|
81
|
-
if (err
|
|
86
|
+
if (isMissingResource(err))
|
|
82
87
|
activityUnavailableRef.current = true;
|
|
83
88
|
return { data: [] };
|
|
84
89
|
});
|
|
@@ -187,15 +192,16 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
|
|
|
187
192
|
const viewName = pathParts[4];
|
|
188
193
|
const definedViews = currentObject.listViews || currentObject.list_views || {};
|
|
189
194
|
const viewDef = definedViews[viewName];
|
|
190
|
-
const
|
|
191
|
-
|
|
195
|
+
const fallbackLabel = (viewDef && (viewDef.label || viewDef.title)) || humanizeSlug(viewName);
|
|
196
|
+
const localizedViewLabel = viewLabel(currentObject.name, viewName, fallbackLabel);
|
|
197
|
+
extraSegments.push({ label: localizedViewLabel });
|
|
192
198
|
}
|
|
193
199
|
}
|
|
194
200
|
}
|
|
195
201
|
}
|
|
196
202
|
const lastSegmentLabel = extraSegments[extraSegments.length - 1]?.label || appName || '';
|
|
197
|
-
return (_jsxs("div", { className: "flex items-center justify-between w-full h-full", children: [_jsxs("div", { className: "flex items-center min-w-0 flex-1", children: [_jsx(Link, { to: "/home", className: "flex items-center justify-center h-7 w-7 shrink-0 rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors", title: "ObjectStack", children: _jsx(Boxes, { className: "h-4 w-4" }) }), resolvedVariant === 'home' && (_jsx("span", { className: "hidden sm:inline ml-2 text-sm font-semibold tracking-tight", children: "ObjectStack" })), resolvedVariant === 'orgs' && (_jsxs(_Fragment, { children: [_jsx(PathSep, {}), _jsx("span", { className: "text-sm font-medium text-foreground/80 px-1.5", children: t('organizations.title', { defaultValue: 'Organizations' }) })] })), isApp && (_jsxs(_Fragment, { children: [_jsx(SidebarTrigger, { className: "md:hidden shrink-0 ml-1" }), activeAppName && onAppChange ? (_jsxs(_Fragment, { children: [_jsx(PathSep, {}), _jsx(AppSwitcher, { activeAppName: activeAppName, onAppChange: onAppChange })] })) : appName ? (_jsxs(_Fragment, { children: [_jsx(PathSep, {}), _jsx("span", { className: "text-sm font-medium text-foreground/80 px-1.5", children: appName })] })) : null, extraSegments.map((seg, i) => {
|
|
203
|
+
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) => {
|
|
198
204
|
const isLast = i === extraSegments.length - 1;
|
|
199
205
|
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));
|
|
200
|
-
}), _jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: lastSegmentLabel })] }))] }), _jsxs("div", { className: "flex items-center gap-0.5 sm:gap-1 shrink-0 [&>*+*[data-topbar-group]]:ml-1 [&>[data-topbar-group]+[data-topbar-group]]:border-l [&>[data-topbar-group]+[data-topbar-group]]:border-border/60 [&>[data-topbar-group]+[data-topbar-group]]:pl-1 sm:[&>[data-topbar-group]+[data-topbar-group]]:pl-2 sm:[&>[data-topbar-group]+[data-topbar-group]]:ml-2", children: [!isOnline && (_jsxs("div", { className: "flex items-center gap-1 px-2 py-1 rounded-full bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200 text-xs font-medium", children: [_jsx("span", { className: "h-2 w-2 rounded-full bg-yellow-500 animate-pulse" }), "Offline"] })), isApp && connectionState && _jsx(ConnectionStatus, { state: connectionState }), isApp && activeUsers.length > 0 && (_jsx("div", { className: "hidden md:flex items-center shrink-0", title: "Users currently online", children: _jsx(PresenceAvatars, { users: activeUsers, size: "sm", maxVisible: 3, showStatus: true }) })), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 sm:gap-1 shrink-0", children: [_jsxs("button", { onClick: () => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true })), className: "hidden lg:flex relative items-center gap-2 w-48 xl:w-64 h-8 px-3 text-sm rounded-md border bg-muted/50 text-muted-foreground hover:bg-muted transition-colors", children: [_jsx(Search, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "flex-1 text-left text-xs", children: t('console.search', { defaultValue: 'Search...' }) }), _jsxs("kbd", { className: "pointer-events-none inline-flex h-5 items-center gap-0.5 rounded border bg-background px-1.5 text-[10px] font-medium text-muted-foreground", children: [_jsx("span", { className: "text-xs", children: "\u2318" }), "K"] })] }), _jsx(Button, { variant: "ghost", size: "icon", className: "lg:hidden h-8 w-8 shrink-0", onClick: () => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true })), "aria-label": t('console.search', { defaultValue: 'Search...' }), children: _jsx(Search, { className: "h-4 w-4" }) })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [_jsx("div", { className: "hidden sm:flex shrink-0", children: _jsx(ActivityFeed, { activities: activeActivities }) }), _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 hidden md:flex shrink-0", asChild: true, "aria-label": t('sidebar.helpTooltip', { defaultValue: 'Help & Documentation' }), children: _jsx("a", { href: "https://docs.objectstack.ai", target: "_blank", rel: "noopener noreferrer", children: _jsx(HelpCircle, { className: "h-4 w-4" }) }) })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [_jsx("div", { className: "hidden sm:flex shrink-0", children: _jsx(ModeToggle, {}) }), _jsx("div", { className: "hidden sm:flex shrink-0", children: _jsx(LocaleSwitcher, {}) }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0 rounded-full", children: _jsxs(Avatar, { className: "h-7 w-7 rounded-full", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-full bg-primary text-primary-foreground text-xs", children: getUserInitials(user) })] }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "min-w-64 rounded-lg", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "p-0 font-normal", children: _jsxs("div", { className: "flex items-center gap-2 px-2 py-2", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] })] }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuGroup, { children: [hasOrgSection && (_jsxs(DropdownMenuItem, { onClick: () => navigate('/organizations'), className: "cursor-pointer", children: [_jsx(Boxes, { className: "mr-2 h-4 w-4" }), t('organizations.mine', { defaultValue: 'My Organizations' })] })), _jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/setup/system/profile'), children: [_jsx(UserIcon, { className: "mr-2 h-4 w-4" }), t('user.profile', { defaultValue: 'Profile' })] }), _jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/setup'), children: [_jsx(Settings, { className: "mr-2 h-4 w-4" }), t('sidebar.settings', { defaultValue: 'Settings' })] })] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "text-destructive focus:text-destructive", onClick: () => signOut(), children: [_jsx(LogOut, { className: "mr-2 h-4 w-4" }), t('user.logout', { defaultValue: 'Log out' })] })] })] })] })] })] }));
|
|
206
|
+
}), _jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: lastSegmentLabel })] }))] }), _jsxs("div", { className: "flex items-center gap-0.5 sm:gap-1 shrink-0 [&>*+*[data-topbar-group]]:ml-1 [&>[data-topbar-group]+[data-topbar-group]]:border-l [&>[data-topbar-group]+[data-topbar-group]]:border-border/60 [&>[data-topbar-group]+[data-topbar-group]]:pl-1 sm:[&>[data-topbar-group]+[data-topbar-group]]:pl-2 sm:[&>[data-topbar-group]+[data-topbar-group]]:ml-2", children: [!isOnline && (_jsxs("div", { className: "flex items-center gap-1 px-2 py-1 rounded-full bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200 text-xs font-medium", children: [_jsx("span", { className: "h-2 w-2 rounded-full bg-yellow-500 animate-pulse" }), "Offline"] })), isApp && connectionState && _jsx(ConnectionStatus, { state: connectionState }), isApp && activeUsers.length > 0 && (_jsx("div", { className: "hidden md:flex items-center shrink-0", title: "Users currently online", children: _jsx(PresenceAvatars, { users: activeUsers, size: "sm", maxVisible: 3, showStatus: true }) })), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 sm:gap-1 shrink-0", children: [_jsxs("button", { onClick: () => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true })), className: "hidden lg:flex relative items-center gap-2 w-48 xl:w-64 h-8 px-3 text-sm rounded-md border bg-muted/50 text-muted-foreground hover:bg-muted transition-colors", children: [_jsx(Search, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "flex-1 text-left text-xs", children: t('console.search', { defaultValue: 'Search...' }) }), _jsxs("kbd", { className: "pointer-events-none inline-flex h-5 items-center gap-0.5 rounded border bg-background px-1.5 text-[10px] font-medium text-muted-foreground", children: [_jsx("span", { className: "text-xs", children: "\u2318" }), "K"] })] }), _jsx(Button, { variant: "ghost", size: "icon", className: "lg:hidden h-8 w-8 shrink-0", onClick: () => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true })), "aria-label": t('console.search', { defaultValue: 'Search...' }), children: _jsx(Search, { className: "h-4 w-4" }) })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [_jsx("div", { className: "hidden sm:flex shrink-0", children: _jsx(ActivityFeed, { activities: activeActivities }) }), _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 hidden md:flex shrink-0", asChild: true, "aria-label": t('sidebar.helpTooltip', { defaultValue: 'Help & Documentation' }), children: _jsx("a", { href: "https://docs.objectstack.ai", target: "_blank", rel: "noopener noreferrer", children: _jsx(HelpCircle, { className: "h-4 w-4" }) }) })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [_jsx("div", { className: "hidden sm:flex shrink-0", children: _jsx(ModeToggle, {}) }), _jsx("div", { className: "hidden sm:flex shrink-0", children: _jsx(LocaleSwitcher, {}) }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0 rounded-full", children: _jsxs(Avatar, { className: "h-7 w-7 rounded-full", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-full bg-primary text-primary-foreground text-xs", children: getUserInitials(user) })] }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "min-w-64 rounded-lg", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "p-0 font-normal", children: _jsxs("div", { className: "flex items-center gap-2 px-2 py-2", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] })] }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuGroup, { children: [hasOrgSection && (_jsxs(DropdownMenuItem, { onClick: () => navigate('/organizations'), className: "cursor-pointer", children: [_jsx(Boxes, { className: "mr-2 h-4 w-4" }), t('organizations.mine', { defaultValue: 'My Organizations' })] })), _jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/setup/system/profile'), children: [_jsx(UserIcon, { className: "mr-2 h-4 w-4" }), t('user.profile', { defaultValue: 'Profile' })] }), _jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/setup'), children: [_jsx(Settings, { className: "mr-2 h-4 w-4" }), t('sidebar.settings', { defaultValue: 'Settings' })] })] }), 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' })] })] }))] })] })] })] })] }));
|
|
201
207
|
}
|
|
@@ -91,7 +91,7 @@ function useNavOrder(appName) {
|
|
|
91
91
|
const getIcon = resolveIcon;
|
|
92
92
|
export function AppSidebar({ activeAppName, onAppChange }) {
|
|
93
93
|
const { isMobile } = useSidebar();
|
|
94
|
-
const { user, signOut } = useAuth();
|
|
94
|
+
const { user, signOut, isAuthEnabled } = useAuth();
|
|
95
95
|
const navigate = useNavigate();
|
|
96
96
|
const { t } = useObjectTranslation();
|
|
97
97
|
const { objectLabel: resolveNavObjectLabel } = useObjectLabel();
|
|
@@ -184,7 +184,7 @@ export function AppSidebar({ activeAppName, onAppChange }) {
|
|
|
184
184
|
}) }) })] })), _jsx(SidebarGroup, { className: "py-0", children: _jsxs(SidebarGroupContent, { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-2 top-1/2 size-4 -translate-y-1/2 select-none opacity-70" }), _jsx(SidebarInput, { placeholder: "Search navigation...", value: navSearchQuery, onChange: (e) => setNavSearchQuery(e.target.value), className: "pl-8" })] }) }), _jsx(NavigationRenderer, { items: processedNavigation, basePath: basePath, evaluateVisibility: evalVis, checkPermission: checkPerm, searchQuery: navSearchQuery, enablePinning: true, onPinToggle: togglePin, enableReorder: true, onReorder: handleReorder, resolveObjectLabel: (objectName, fallback) => resolveNavObjectLabel({ name: objectName, label: fallback }), t: t }), recentItems.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5 cursor-pointer select-none", onClick: () => setRecentExpanded(prev => !prev), children: [_jsx(ChevronRight, { className: `h-3 w-3 transition-transform duration-150 ${recentExpanded ? 'rotate-90' : ''}` }), _jsx(Clock, { className: "h-3.5 w-3.5" }), "Recent"] }), recentExpanded && (_jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: recentItems.slice(0, 5).map(item => (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : '📄' }), _jsx("span", { className: "truncate", children: item.label })] }) }) }, item.id))) }) }))] })), favorites.length > 0 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Star, { className: "h-3.5 w-3.5" }), "Favorites"] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: favorites.slice(0, 8).map(item => (_jsxs(SidebarMenuItem, { children: [_jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.href, children: [_jsx("span", { className: "text-muted-foreground", children: item.type === 'dashboard' ? '📊' : item.type === 'report' ? '📈' : item.type === 'page' ? '📄' : '📋' }), _jsx("span", { className: "truncate", children: item.label })] }) }), _jsx(SidebarMenuAction, { showOnHover: true, onClick: (e) => { e.stopPropagation(); removeFavorite(item.id); }, "aria-label": `Remove ${item.label} from favorites`, children: _jsx(StarOff, { className: "h-3 w-3" }) })] }, item.id))) }) })] }))] })) : (_jsxs(SidebarGroup, { "data-testid": "system-fallback-nav", children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Settings, { className: "h-3.5 w-3.5" }), "System"] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: systemFallbackNavigation.map((item) => {
|
|
185
185
|
const NavIcon = getIcon(item.icon);
|
|
186
186
|
return (_jsx(SidebarMenuItem, { children: _jsx(SidebarMenuButton, { asChild: true, tooltip: item.label, children: _jsxs(Link, { to: item.url || '/system', children: [_jsx(NavIcon, { className: "h-4 w-4" }), _jsx("span", { children: item.label })] }) }) }, item.id));
|
|
187
|
-
}) }) })] })) }), _jsx(SidebarFooter, { children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(SidebarMenuButton, { size: "lg", className: "data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] }), _jsx(ChevronsUpDown, { className: "ml-auto size-4" })] }) }), _jsxs(DropdownMenuContent, { className: "w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg", side: isMobile ? "bottom" : "right", align: "end", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "p-0 font-normal", children: _jsxs("div", { className: "flex items-center gap-2 px-1 py-1.5 text-left text-sm", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] })] }) }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuGroup, { children: _jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/setup'), children: [_jsx(Settings, { className: "mr-2 h-4 w-4" }),
|
|
187
|
+
}) }) })] })) }), _jsx(SidebarFooter, { children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(SidebarMenuButton, { size: "lg", className: "data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] }), _jsx(ChevronsUpDown, { className: "ml-auto size-4" })] }) }), _jsxs(DropdownMenuContent, { className: "w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg", side: isMobile ? "bottom" : "right", align: "end", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "p-0 font-normal", children: _jsxs("div", { className: "flex items-center gap-2 px-1 py-1.5 text-left text-sm", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] })] }) }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuGroup, { children: _jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/setup'), children: [_jsx(Settings, { className: "mr-2 h-4 w-4" }), 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: (activeApp ? resolvedNavigation : systemFallbackNavigation).filter((n) => n.type !== 'group').slice(0, 5).map((item) => {
|
|
188
188
|
const NavIcon = getIcon(item.icon);
|
|
189
189
|
const baseUrl = activeApp ? `/apps/${activeAppName}` : '';
|
|
190
190
|
let href = item.url || '#';
|
|
@@ -15,7 +15,7 @@ export function AppSwitcher({ activeAppName, onAppChange }) {
|
|
|
15
15
|
if (!activeApp)
|
|
16
16
|
return null;
|
|
17
17
|
const appLabelText = appLabel({ name: activeApp.name, label: resolveI18nLabel(activeApp.label, t) });
|
|
18
|
-
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs("button", { className: "flex items-center gap-1 rounded-md px-1.5 py-1 text-sm font-medium text-foreground/80 hover:bg-accent hover:text-foreground transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring", children: [appLabelText, _jsx(ChevronDown, { className: "h-3.5 w-3.5 text-muted-foreground" })] }) }), _jsxs(DropdownMenuContent, { align: "start", sideOffset: 8, className: "w-60", children: [_jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground font-normal", children: "Switch Application" }), _jsx(DropdownMenuSeparator, {}), activeApps.map((app) => {
|
|
18
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs("button", { className: "flex items-center gap-1 rounded-md px-1.5 py-1 text-sm font-medium text-foreground/80 hover:bg-accent hover:text-foreground transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring max-w-[40vw] sm:max-w-none", children: [_jsx("span", { className: "truncate whitespace-nowrap", children: appLabelText }), _jsx(ChevronDown, { className: "h-3.5 w-3.5 text-muted-foreground shrink-0" })] }) }), _jsxs(DropdownMenuContent, { align: "start", sideOffset: 8, className: "w-60", children: [_jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground font-normal", children: "Switch Application" }), _jsx(DropdownMenuSeparator, {}), activeApps.map((app) => {
|
|
19
19
|
const AppIcon = getIcon(app.icon);
|
|
20
20
|
const label = appLabel({ name: app.name, label: resolveI18nLabel(app.label, t) });
|
|
21
21
|
const isActive = activeApp.name === app.name;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* PageHeader Component
|
|
10
|
+
*
|
|
11
|
+
* Standardized page header used across console object/list/detail views.
|
|
12
|
+
* Provides consistent typography, icon framing, and action grouping so
|
|
13
|
+
* individual screens don't reinvent the title row.
|
|
14
|
+
*
|
|
15
|
+
* Layout:
|
|
16
|
+
* [icon] Title [actions...]
|
|
17
|
+
* description
|
|
18
|
+
*/
|
|
19
|
+
import * as React from 'react';
|
|
20
|
+
export interface PageHeaderProps {
|
|
21
|
+
/** Page title (required, becomes <h1>). */
|
|
22
|
+
title: React.ReactNode;
|
|
23
|
+
/** Optional secondary description shown beneath the title. */
|
|
24
|
+
description?: React.ReactNode;
|
|
25
|
+
/** Optional leading icon (already-instantiated React element). */
|
|
26
|
+
icon?: React.ReactNode;
|
|
27
|
+
/** Right-aligned actions (buttons, dropdowns, etc.). */
|
|
28
|
+
actions?: React.ReactNode;
|
|
29
|
+
/** Additional className applied to the outer container. */
|
|
30
|
+
className?: string;
|
|
31
|
+
/** Optional data-testid for E2E selectors. */
|
|
32
|
+
'data-testid'?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Reusable page header for the console.
|
|
36
|
+
*
|
|
37
|
+
* Mirrors Shadcn / Linear / Notion conventions: subtle border, comfortable
|
|
38
|
+
* vertical padding, primary-tinted icon chip, monolithic action cluster on
|
|
39
|
+
* the right with consistent gap.
|
|
40
|
+
*/
|
|
41
|
+
export declare function PageHeader({ title, description, icon, actions, className, 'data-testid': testId, }: PageHeaderProps): import("react/jsx-runtime").JSX.Element;
|
|
42
|
+
export default PageHeader;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from '@object-ui/components';
|
|
3
|
+
/**
|
|
4
|
+
* Reusable page header for the console.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors Shadcn / Linear / Notion conventions: subtle border, comfortable
|
|
7
|
+
* vertical padding, primary-tinted icon chip, monolithic action cluster on
|
|
8
|
+
* the right with consistent gap.
|
|
9
|
+
*/
|
|
10
|
+
export function PageHeader({ title, description, icon, actions, className, 'data-testid': testId, }) {
|
|
11
|
+
return (_jsxs("div", { className: cn('flex justify-between items-center gap-3 py-1.5 sm:py-2 px-3 sm:px-4 border-b shrink-0 bg-background z-10', className), "data-testid": testId ?? 'page-header', children: [_jsxs("div", { className: "flex items-center gap-2 sm:gap-2.5 min-w-0 flex-1", children: [icon && (_jsx("div", { className: "bg-primary/10 p-1 sm:p-1.5 rounded-md shrink-0 text-primary", children: icon })), _jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "text-sm sm:text-base font-semibold tracking-tight text-foreground truncate", children: title }), description && (_jsx("p", { className: "text-xs text-muted-foreground truncate hidden sm:block max-w-md", children: description }))] })] }), actions && (_jsx("div", { className: "flex items-center gap-1.5 sm:gap-2 shrink-0", children: actions }))] }));
|
|
12
|
+
}
|
|
13
|
+
export default PageHeader;
|
package/dist/layout/index.d.ts
CHANGED
|
@@ -9,3 +9,5 @@ export type { ActivityItem } from './ActivityFeed';
|
|
|
9
9
|
export { LocaleSwitcher } from './LocaleSwitcher';
|
|
10
10
|
export { ModeToggle } from './ModeToggle';
|
|
11
11
|
export { AuthPageLayout } from './AuthPageLayout';
|
|
12
|
+
export { PageHeader } from './PageHeader';
|
|
13
|
+
export type { PageHeaderProps } from './PageHeader';
|
package/dist/layout/index.js
CHANGED
package/dist/utils/getIcon.d.ts
CHANGED
|
@@ -16,7 +16,10 @@ import React from 'react';
|
|
|
16
16
|
*
|
|
17
17
|
* Returns a React component that lazy-loads the underlying SVG icon on
|
|
18
18
|
* mount. Falls back to the `Database` icon (statically imported) when no
|
|
19
|
-
* `name` is given
|
|
19
|
+
* `name` is given, or when the requested name is not a valid Lucide icon
|
|
20
|
+
* — server-driven metadata frequently references icons from other libraries
|
|
21
|
+
* (e.g. `box-open` from Font Awesome), and we silently degrade to the
|
|
22
|
+
* fallback rather than letting Lucide log a console error.
|
|
20
23
|
*
|
|
21
24
|
* The returned component is memoised per `name` so repeated calls with the
|
|
22
25
|
* same name yield the same component reference (stable for React.memo).
|
package/dist/utils/getIcon.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import React from 'react';
|
|
14
14
|
import { Database } from 'lucide-react';
|
|
15
15
|
// @ts-ignore - lucide-react has no `exports` field; subpath types live alongside dynamic.mjs
|
|
16
|
-
import { DynamicIcon } from 'lucide-react/dynamic.mjs';
|
|
16
|
+
import { DynamicIcon, iconNames } from 'lucide-react/dynamic.mjs';
|
|
17
17
|
/** Convert PascalCase / camelCase / mixed names to kebab-case for DynamicIcon. */
|
|
18
18
|
function toKebab(name) {
|
|
19
19
|
if (name.includes('-'))
|
|
@@ -23,13 +23,18 @@ function toKebab(name) {
|
|
|
23
23
|
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
|
|
24
24
|
.toLowerCase();
|
|
25
25
|
}
|
|
26
|
+
// Lucide ships ~3900 icon names; storing as a Set keeps lookups O(1).
|
|
27
|
+
const VALID_ICON_NAMES = new Set(iconNames);
|
|
26
28
|
const cache = new Map();
|
|
27
29
|
/**
|
|
28
30
|
* Resolve a Lucide icon by name (kebab-case or PascalCase).
|
|
29
31
|
*
|
|
30
32
|
* Returns a React component that lazy-loads the underlying SVG icon on
|
|
31
33
|
* mount. Falls back to the `Database` icon (statically imported) when no
|
|
32
|
-
* `name` is given
|
|
34
|
+
* `name` is given, or when the requested name is not a valid Lucide icon
|
|
35
|
+
* — server-driven metadata frequently references icons from other libraries
|
|
36
|
+
* (e.g. `box-open` from Font Awesome), and we silently degrade to the
|
|
37
|
+
* fallback rather than letting Lucide log a console error.
|
|
33
38
|
*
|
|
34
39
|
* The returned component is memoised per `name` so repeated calls with the
|
|
35
40
|
* same name yield the same component reference (stable for React.memo).
|
|
@@ -41,6 +46,10 @@ export function getIcon(name) {
|
|
|
41
46
|
if (cached)
|
|
42
47
|
return cached;
|
|
43
48
|
const kebab = toKebab(name);
|
|
49
|
+
if (!VALID_ICON_NAMES.has(kebab)) {
|
|
50
|
+
cache.set(name, Database);
|
|
51
|
+
return Database;
|
|
52
|
+
}
|
|
44
53
|
const Wrapped = (props) => React.createElement(DynamicIcon, {
|
|
45
54
|
name: kebab,
|
|
46
55
|
fallback: Database,
|