@object-ui/app-shell 3.3.0 → 3.3.1

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 (173) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +22 -2
  3. package/dist/chrome/CommandPalette.d.ts +16 -0
  4. package/dist/chrome/CommandPalette.js +88 -0
  5. package/dist/chrome/ConditionalAuthWrapper.d.ts +23 -0
  6. package/dist/chrome/ConditionalAuthWrapper.js +135 -0
  7. package/dist/chrome/ConsoleToaster.d.ts +11 -0
  8. package/dist/chrome/ConsoleToaster.js +28 -0
  9. package/dist/chrome/ErrorBoundary.d.ts +39 -0
  10. package/dist/chrome/ErrorBoundary.js +64 -0
  11. package/dist/chrome/KeyboardShortcutsDialog.d.ts +7 -0
  12. package/dist/chrome/KeyboardShortcutsDialog.js +63 -0
  13. package/dist/chrome/LoadingScreen.d.ts +12 -0
  14. package/dist/chrome/LoadingScreen.js +44 -0
  15. package/dist/chrome/OnboardingWalkthrough.d.ts +9 -0
  16. package/dist/chrome/OnboardingWalkthrough.js +11 -0
  17. package/dist/chrome/ThemeProvider.d.ts +13 -0
  18. package/dist/chrome/ThemeProvider.js +37 -0
  19. package/dist/chrome/index.d.ts +8 -0
  20. package/dist/chrome/index.js +8 -0
  21. package/dist/console/AppContent.d.ts +26 -0
  22. package/dist/console/AppContent.js +266 -0
  23. package/dist/console/ConsoleShell.d.ts +66 -0
  24. package/dist/console/ConsoleShell.js +97 -0
  25. package/dist/console/auth/AuthPageLayout.d.ts +9 -0
  26. package/dist/console/auth/AuthPageLayout.js +4 -0
  27. package/dist/console/auth/ForgotPasswordPage.d.ts +4 -0
  28. package/dist/console/auth/ForgotPasswordPage.js +23 -0
  29. package/dist/console/auth/LoginPage.d.ts +4 -0
  30. package/dist/console/auth/LoginPage.js +24 -0
  31. package/dist/console/auth/RegisterPage.d.ts +4 -0
  32. package/dist/console/auth/RegisterPage.js +29 -0
  33. package/dist/console/auth/index.d.ts +4 -0
  34. package/dist/console/auth/index.js +4 -0
  35. package/dist/console/home/AppCard.d.ts +14 -0
  36. package/dist/console/home/AppCard.js +36 -0
  37. package/dist/console/home/HomeLayout.d.ts +15 -0
  38. package/dist/console/home/HomeLayout.js +20 -0
  39. package/dist/console/home/HomePage.d.ts +18 -0
  40. package/dist/console/home/HomePage.js +54 -0
  41. package/dist/console/home/QuickActions.d.ts +9 -0
  42. package/dist/console/home/QuickActions.js +53 -0
  43. package/dist/console/home/RecentApps.d.ts +13 -0
  44. package/dist/console/home/RecentApps.js +32 -0
  45. package/dist/console/home/StarredApps.d.ts +13 -0
  46. package/dist/console/home/StarredApps.js +29 -0
  47. package/dist/console/home/index.d.ts +11 -0
  48. package/dist/console/home/index.js +11 -0
  49. package/dist/console/organizations/CreateWorkspaceDialog.d.ts +16 -0
  50. package/dist/console/organizations/CreateWorkspaceDialog.js +71 -0
  51. package/dist/console/organizations/OrganizationsLayout.d.ts +15 -0
  52. package/dist/console/organizations/OrganizationsLayout.js +20 -0
  53. package/dist/console/organizations/OrganizationsPage.d.ts +9 -0
  54. package/dist/console/organizations/OrganizationsPage.js +76 -0
  55. package/dist/console/organizations/index.d.ts +8 -0
  56. package/dist/console/organizations/index.js +8 -0
  57. package/dist/console/organizations/manage/AcceptInvitationPage.d.ts +7 -0
  58. package/dist/console/organizations/manage/AcceptInvitationPage.js +100 -0
  59. package/dist/console/organizations/manage/InvitationsPage.d.ts +6 -0
  60. package/dist/console/organizations/manage/InvitationsPage.js +107 -0
  61. package/dist/console/organizations/manage/InviteMemberDialog.d.ts +16 -0
  62. package/dist/console/organizations/manage/InviteMemberDialog.js +83 -0
  63. package/dist/console/organizations/manage/MembersPage.d.ts +6 -0
  64. package/dist/console/organizations/manage/MembersPage.js +89 -0
  65. package/dist/console/organizations/manage/OrganizationLayout.d.ts +8 -0
  66. package/dist/console/organizations/manage/OrganizationLayout.js +51 -0
  67. package/dist/console/organizations/manage/SettingsPage.d.ts +6 -0
  68. package/dist/console/organizations/manage/SettingsPage.js +141 -0
  69. package/dist/console/organizations/manage/orgContext.d.ts +5 -0
  70. package/dist/console/organizations/manage/orgContext.js +4 -0
  71. package/dist/context/FavoritesProvider.d.ts +50 -0
  72. package/dist/context/FavoritesProvider.js +129 -0
  73. package/dist/context/NavigationContext.d.ts +46 -0
  74. package/dist/context/NavigationContext.js +59 -0
  75. package/dist/context/index.d.ts +3 -0
  76. package/dist/context/index.js +2 -0
  77. package/dist/hooks/index.d.ts +7 -0
  78. package/dist/hooks/index.js +7 -0
  79. package/dist/hooks/useFavorites.d.ts +13 -0
  80. package/dist/hooks/useFavorites.js +13 -0
  81. package/dist/hooks/useMetadataService.d.ts +13 -0
  82. package/dist/hooks/useMetadataService.js +18 -0
  83. package/dist/hooks/useNavPins.d.ts +16 -0
  84. package/dist/hooks/useNavPins.js +72 -0
  85. package/dist/hooks/useNavigationSync.d.ts +82 -0
  86. package/dist/hooks/useNavigationSync.js +495 -0
  87. package/dist/hooks/useObjectActions.d.ts +38 -0
  88. package/dist/hooks/useObjectActions.js +92 -0
  89. package/dist/hooks/useRecentItems.d.ts +21 -0
  90. package/dist/hooks/useRecentItems.js +50 -0
  91. package/dist/hooks/useResponsiveSidebar.d.ts +8 -0
  92. package/dist/hooks/useResponsiveSidebar.js +28 -0
  93. package/dist/index.d.ts +29 -0
  94. package/dist/index.js +35 -0
  95. package/dist/layout/ActivityFeed.d.ts +22 -0
  96. package/dist/layout/ActivityFeed.js +57 -0
  97. package/dist/layout/AppHeader.d.ts +33 -0
  98. package/dist/layout/AppHeader.js +182 -0
  99. package/dist/layout/AppSidebar.d.ts +14 -0
  100. package/dist/layout/AppSidebar.js +217 -0
  101. package/dist/layout/AppSwitcher.d.ts +12 -0
  102. package/dist/layout/AppSwitcher.js +39 -0
  103. package/dist/layout/AuthPageLayout.d.ts +9 -0
  104. package/dist/layout/AuthPageLayout.js +4 -0
  105. package/dist/layout/ConnectionStatus.d.ts +14 -0
  106. package/dist/layout/ConnectionStatus.js +36 -0
  107. package/dist/layout/ConsoleLayout.d.ts +21 -0
  108. package/dist/layout/ConsoleLayout.js +71 -0
  109. package/dist/layout/LocaleSwitcher.d.ts +7 -0
  110. package/dist/layout/LocaleSwitcher.js +27 -0
  111. package/dist/layout/ModeToggle.d.ts +1 -0
  112. package/dist/layout/ModeToggle.js +10 -0
  113. package/dist/layout/UnifiedSidebar.d.ts +22 -0
  114. package/dist/layout/UnifiedSidebar.js +210 -0
  115. package/dist/layout/index.d.ts +11 -0
  116. package/dist/layout/index.js +10 -0
  117. package/dist/providers/AdapterProvider.d.ts +21 -0
  118. package/dist/providers/AdapterProvider.js +52 -0
  119. package/dist/providers/ExpressionProvider.d.ts +50 -0
  120. package/dist/providers/ExpressionProvider.js +67 -0
  121. package/dist/providers/MetadataProvider.d.ts +18 -0
  122. package/dist/providers/MetadataProvider.js +324 -0
  123. package/dist/services/MetadataService.d.ts +119 -0
  124. package/dist/services/MetadataService.js +211 -0
  125. package/dist/skeletons/SkeletonDashboard.d.ts +13 -0
  126. package/dist/skeletons/SkeletonDashboard.js +12 -0
  127. package/dist/skeletons/SkeletonDetail.d.ts +15 -0
  128. package/dist/skeletons/SkeletonDetail.js +12 -0
  129. package/dist/skeletons/SkeletonGrid.d.ts +15 -0
  130. package/dist/skeletons/SkeletonGrid.js +12 -0
  131. package/dist/skeletons/index.d.ts +3 -0
  132. package/dist/skeletons/index.js +3 -0
  133. package/dist/utils/getIcon.d.ts +10 -0
  134. package/dist/utils/getIcon.js +24 -0
  135. package/dist/utils/index.d.ts +33 -0
  136. package/dist/utils/index.js +62 -0
  137. package/dist/utils/metadataConverters.d.ts +78 -0
  138. package/dist/utils/metadataConverters.js +74 -0
  139. package/dist/views/ActionConfirmDialog.d.ts +22 -0
  140. package/dist/views/ActionConfirmDialog.js +22 -0
  141. package/dist/views/ActionParamDialog.d.ts +23 -0
  142. package/dist/views/ActionParamDialog.js +67 -0
  143. package/dist/views/DashboardView.d.ts +9 -0
  144. package/dist/views/DashboardView.js +357 -0
  145. package/dist/views/DesignDrawer.d.ts +28 -0
  146. package/dist/views/DesignDrawer.js +51 -0
  147. package/dist/views/MetadataInspector.d.ts +40 -0
  148. package/dist/views/MetadataInspector.js +69 -0
  149. package/dist/views/ObjectView.d.ts +14 -0
  150. package/dist/views/ObjectView.js +770 -0
  151. package/dist/views/PageView.d.ts +6 -0
  152. package/dist/views/PageView.js +49 -0
  153. package/dist/views/RecordDetailView.d.ts +14 -0
  154. package/dist/views/RecordDetailView.js +508 -0
  155. package/dist/views/ReportView.d.ts +4 -0
  156. package/dist/views/ReportView.js +284 -0
  157. package/dist/views/SearchResultsPage.d.ts +9 -0
  158. package/dist/views/SearchResultsPage.js +107 -0
  159. package/dist/views/ViewConfigPanel.d.ts +56 -0
  160. package/dist/views/ViewConfigPanel.js +91 -0
  161. package/dist/views/index.d.ts +11 -0
  162. package/dist/views/index.js +11 -0
  163. package/package.json +59 -11
  164. package/.turbo/turbo-build.log +0 -4
  165. package/src/components/AppShell.tsx +0 -31
  166. package/src/components/DashboardRenderer.tsx +0 -36
  167. package/src/components/FormRenderer.tsx +0 -71
  168. package/src/components/ObjectRenderer.tsx +0 -122
  169. package/src/components/PageRenderer.tsx +0 -28
  170. package/src/index.ts +0 -20
  171. package/src/types.ts +0 -78
  172. package/tsconfig.json +0 -20
  173. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,141 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * SettingsPage
4
+ *
5
+ * Organization settings: general info form + danger zone.
6
+ */
7
+ import { useCallback, useEffect, useState } from 'react';
8
+ import { Button, Input, Label, Separator, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@object-ui/components';
9
+ import { useAuth } from '@object-ui/auth';
10
+ import { useObjectTranslation } from '@object-ui/i18n';
11
+ import { Loader2 } from 'lucide-react';
12
+ import { toast } from 'sonner';
13
+ import { useNavigate } from 'react-router-dom';
14
+ import { useOrgContext } from './orgContext';
15
+ export function SettingsPage() {
16
+ const { t } = useObjectTranslation();
17
+ const { org } = useOrgContext();
18
+ const { user, getMembers, updateOrganization, deleteOrganization, leaveOrganization, } = useAuth();
19
+ const navigate = useNavigate();
20
+ // Form state
21
+ const [name, setName] = useState(org.name);
22
+ const [slug, setSlug] = useState(org.slug ?? '');
23
+ const [logo, setLogo] = useState(org.logo ?? '');
24
+ const [isSaving, setIsSaving] = useState(false);
25
+ // Owner check
26
+ const [isOwner, setIsOwner] = useState(null);
27
+ const [membersLoading, setMembersLoading] = useState(true);
28
+ // Danger zone dialogs
29
+ const [isLeaveOpen, setIsLeaveOpen] = useState(false);
30
+ const [isDeleteOpen, setIsDeleteOpen] = useState(false);
31
+ const [deleteConfirmSlug, setDeleteConfirmSlug] = useState('');
32
+ const [isDangerLoading, setIsDangerLoading] = useState(false);
33
+ // Sync form when org changes (e.g., after save)
34
+ useEffect(() => {
35
+ setName(org.name);
36
+ setSlug(org.slug ?? '');
37
+ setLogo(org.logo ?? '');
38
+ }, [org]);
39
+ // Load members to determine if current user is owner
40
+ useEffect(() => {
41
+ let cancelled = false;
42
+ setMembersLoading(true);
43
+ getMembers(org.id)
44
+ .then((members) => {
45
+ if (cancelled)
46
+ return;
47
+ const me = members.find((m) => m.userId === user?.id);
48
+ setIsOwner(me?.role === 'owner');
49
+ })
50
+ .catch(() => {
51
+ if (!cancelled)
52
+ setIsOwner(false);
53
+ })
54
+ .finally(() => {
55
+ if (!cancelled)
56
+ setMembersLoading(false);
57
+ });
58
+ return () => {
59
+ cancelled = true;
60
+ };
61
+ }, [org.id, user?.id, getMembers]);
62
+ const handleSave = useCallback(async (e) => {
63
+ e.preventDefault();
64
+ setIsSaving(true);
65
+ try {
66
+ await updateOrganization(org.id, {
67
+ name: name.trim(),
68
+ slug: slug.trim() || undefined,
69
+ logo: logo.trim() || undefined,
70
+ });
71
+ toast.success(t('organization.settings.saved', { defaultValue: 'Settings saved' }));
72
+ }
73
+ catch (err) {
74
+ toast.error(err instanceof Error
75
+ ? err.message
76
+ : t('organization.settings.saveFailed', { defaultValue: 'Failed to save settings' }));
77
+ }
78
+ finally {
79
+ setIsSaving(false);
80
+ }
81
+ }, [org.id, name, slug, logo, updateOrganization, t]);
82
+ const handleLeave = async () => {
83
+ setIsDangerLoading(true);
84
+ try {
85
+ await leaveOrganization(org.id);
86
+ toast.success(t('organization.settings.leftOrg', { defaultValue: 'You have left the organization' }));
87
+ navigate('/organizations');
88
+ }
89
+ catch (err) {
90
+ toast.error(err instanceof Error
91
+ ? err.message
92
+ : t('organization.settings.leaveFailed', { defaultValue: 'Failed to leave organization' }));
93
+ }
94
+ finally {
95
+ setIsDangerLoading(false);
96
+ }
97
+ };
98
+ const handleDelete = async () => {
99
+ setIsDangerLoading(true);
100
+ try {
101
+ await deleteOrganization(org.id);
102
+ toast.success(t('organization.settings.deleted', { defaultValue: 'Organization deleted' }));
103
+ navigate('/organizations');
104
+ }
105
+ catch (err) {
106
+ toast.error(err instanceof Error
107
+ ? err.message
108
+ : t('organization.settings.deleteFailed', { defaultValue: 'Failed to delete organization' }));
109
+ }
110
+ finally {
111
+ setIsDangerLoading(false);
112
+ }
113
+ };
114
+ if (membersLoading) {
115
+ return (_jsx("div", { className: "flex items-center justify-center py-20", children: _jsx(Loader2, { className: "h-5 w-5 animate-spin text-muted-foreground" }) }));
116
+ }
117
+ return (_jsxs("div", { className: "space-y-8", "data-testid": "settings-page", children: [_jsxs("section", { children: [_jsx("h2", { className: "text-lg font-semibold", children: t('organization.settings.generalTitle', { defaultValue: 'General' }) }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: t('organization.settings.generalDescription', {
118
+ defaultValue: 'Update your organization information.',
119
+ }) }), _jsx(Separator, { className: "my-4" }), !isOwner ? (_jsx("div", { className: "rounded-lg border bg-muted/50 p-4 text-sm text-muted-foreground", children: t('organization.settings.readOnlyNote', {
120
+ defaultValue: 'Only owners can change settings.',
121
+ }) })) : (_jsxs("form", { onSubmit: handleSave, className: "space-y-4 max-w-md", children: [_jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { htmlFor: "org-name", children: t('organization.settings.nameLabel', { defaultValue: 'Organization name' }) }), _jsx(Input, { id: "org-name", value: name, onChange: (e) => setName(e.target.value), required: true, "data-testid": "settings-name-input" })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { htmlFor: "org-slug", children: t('organization.settings.slugLabel', { defaultValue: 'Slug' }) }), _jsx(Input, { id: "org-slug", value: slug, onChange: (e) => setSlug(e.target.value), "data-testid": "settings-slug-input" })] }), _jsxs("div", { className: "grid gap-2", children: [_jsx(Label, { htmlFor: "org-logo", children: t('organization.settings.logoLabel', { defaultValue: 'Logo URL (optional)' }) }), _jsx(Input, { id: "org-logo", type: "url", value: logo, onChange: (e) => setLogo(e.target.value), placeholder: "https://example.com/logo.png", "data-testid": "settings-logo-input" })] }), _jsxs(Button, { type: "submit", disabled: isSaving, "data-testid": "settings-save-btn", children: [isSaving && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), t('organization.settings.save', { defaultValue: 'Save changes' })] })] }))] }), _jsxs("section", { children: [_jsx("h2", { className: "text-lg font-semibold text-destructive", children: t('organization.settings.dangerZone', { defaultValue: 'Danger zone' }) }), _jsx(Separator, { className: "my-4" }), _jsxs("div", { className: "space-y-4 rounded-lg border border-destructive/50 bg-destructive/5 p-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-4", children: [_jsxs("div", { children: [_jsx("p", { className: "font-medium text-sm", children: t('organization.settings.leaveTitle', { defaultValue: 'Leave organization' }) }), _jsx("p", { className: "text-xs text-muted-foreground", children: t('organization.settings.leaveDescription', {
122
+ defaultValue: 'You will lose access to this organization.',
123
+ }) })] }), _jsx(Button, { variant: "destructive", size: "sm", onClick: () => setIsLeaveOpen(true), children: t('organization.settings.leaveAction', { defaultValue: 'Leave' }) })] }), _jsx(Separator, {}), _jsxs("div", { className: "flex items-center justify-between gap-4", children: [_jsxs("div", { children: [_jsx("p", { className: "font-medium text-sm", children: t('organization.settings.deleteTitle', { defaultValue: 'Delete organization' }) }), _jsx("p", { className: "text-xs text-muted-foreground", children: t('organization.settings.deleteDescription', {
124
+ defaultValue: 'Permanently delete this organization and all its data.',
125
+ }) })] }), _jsx(Button, { variant: "destructive", size: "sm", onClick: () => {
126
+ setDeleteConfirmSlug('');
127
+ setIsDeleteOpen(true);
128
+ }, disabled: !isOwner, children: t('organization.settings.deleteAction', { defaultValue: 'Delete' }) })] })] })] }), _jsx(AlertDialog, { open: isLeaveOpen, onOpenChange: setIsLeaveOpen, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: t('organization.settings.leaveConfirmTitle', { defaultValue: 'Leave organization?' }) }), _jsx(AlertDialogDescription, { children: t('organization.settings.leaveConfirmDescription', {
129
+ defaultValue: 'Are you sure you want to leave {{name}}? You will lose access immediately.',
130
+ name: org.name,
131
+ }) })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: isDangerLoading, children: t('common.cancel', { defaultValue: 'Cancel' }) }), _jsxs(AlertDialogAction, { className: "bg-destructive text-destructive-foreground hover:bg-destructive/90", onClick: handleLeave, disabled: isDangerLoading, children: [isDangerLoading && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), t('organization.settings.leaveConfirmAction', { defaultValue: 'Leave' })] })] })] }) }), _jsx(AlertDialog, { open: isDeleteOpen, onOpenChange: (open) => {
132
+ setIsDeleteOpen(open);
133
+ if (!open)
134
+ setDeleteConfirmSlug('');
135
+ }, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: t('organization.settings.deleteConfirmTitle', { defaultValue: 'Delete organization?' }) }), _jsx(AlertDialogDescription, { children: t('organization.settings.deleteConfirmDescription', {
136
+ defaultValue: 'This action is irreversible. All data will be permanently deleted. Type the organization slug to confirm.',
137
+ }) })] }), _jsxs("div", { className: "px-6 pb-2", children: [_jsx(Label, { htmlFor: "delete-confirm-slug", className: "text-sm font-medium", children: t('organization.settings.deleteConfirmSlugLabel', {
138
+ defaultValue: 'Type "{{slug}}" to confirm',
139
+ slug: org.slug,
140
+ }) }), _jsx(Input, { id: "delete-confirm-slug", className: "mt-2", value: deleteConfirmSlug, onChange: (e) => setDeleteConfirmSlug(e.target.value), placeholder: org.slug, "data-testid": "delete-confirm-slug-input" })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { disabled: isDangerLoading, children: t('common.cancel', { defaultValue: 'Cancel' }) }), _jsxs(AlertDialogAction, { className: "bg-destructive text-destructive-foreground hover:bg-destructive/90", onClick: handleDelete, disabled: isDangerLoading || deleteConfirmSlug !== org.slug, children: [isDangerLoading && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), t('organization.settings.deleteConfirmAction', { defaultValue: 'Delete organization' })] })] })] }) })] }));
141
+ }
@@ -0,0 +1,5 @@
1
+ import type { AuthOrganization } from '@object-ui/auth';
2
+ export interface OrgOutletContext {
3
+ org: AuthOrganization;
4
+ }
5
+ export declare function useOrgContext(): OrgOutletContext;
@@ -0,0 +1,4 @@
1
+ import { useOutletContext } from 'react-router-dom';
2
+ export function useOrgContext() {
3
+ return useOutletContext();
4
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * FavoritesProvider
3
+ *
4
+ * React Context + Provider for shared favorites state across all consumers
5
+ * (HomePage, AppCard, AppSidebar, UnifiedSidebar, StarredApps).
6
+ *
7
+ * Replaces the standalone `useFavorites` hook pattern — all callers now share
8
+ * a single state instance so that toggling a star in AppCard immediately
9
+ * reflects in HomePage's Starred section and sidebar.
10
+ *
11
+ * Persistence: localStorage (key: "objectui-favorites", max 20 items).
12
+ *
13
+ * TODO: Migrate persistence to server-side storage via the adapter/API layer
14
+ * (e.g. PUT /api/user/preferences) so favorites sync across devices and browsers.
15
+ * The provider should accept an optional `persistenceAdapter` prop that implements
16
+ * `load(): Promise<FavoriteItem[]>` and `save(items: FavoriteItem[]): Promise<void>`.
17
+ * When the adapter is provided, localStorage should be used only as a fallback
18
+ * during the initial load while the server response is in-flight.
19
+ *
20
+ * @module
21
+ */
22
+ import { type ReactNode } from 'react';
23
+ export interface FavoriteItem {
24
+ /** Unique key, e.g. "object:contact" or "dashboard:sales_overview" */
25
+ id: string;
26
+ label: string;
27
+ href: string;
28
+ type: 'object' | 'dashboard' | 'page' | 'report';
29
+ /** ISO timestamp of when the item was favorited */
30
+ favoritedAt: string;
31
+ }
32
+ interface FavoritesContextValue {
33
+ favorites: FavoriteItem[];
34
+ addFavorite: (item: Omit<FavoriteItem, 'favoritedAt'>) => void;
35
+ removeFavorite: (id: string) => void;
36
+ toggleFavorite: (item: Omit<FavoriteItem, 'favoritedAt'>) => void;
37
+ isFavorite: (id: string) => boolean;
38
+ clearFavorites: () => void;
39
+ }
40
+ interface FavoritesProviderProps {
41
+ children: ReactNode;
42
+ }
43
+ export declare function FavoritesProvider({ children }: FavoritesProviderProps): import("react/jsx-runtime").JSX.Element;
44
+ /**
45
+ * Access shared favorites state.
46
+ *
47
+ * Must be used inside `<FavoritesProvider>`.
48
+ */
49
+ export declare function useFavorites(): FavoritesContextValue;
50
+ export {};
@@ -0,0 +1,129 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * FavoritesProvider
4
+ *
5
+ * React Context + Provider for shared favorites state across all consumers
6
+ * (HomePage, AppCard, AppSidebar, UnifiedSidebar, StarredApps).
7
+ *
8
+ * Replaces the standalone `useFavorites` hook pattern — all callers now share
9
+ * a single state instance so that toggling a star in AppCard immediately
10
+ * reflects in HomePage's Starred section and sidebar.
11
+ *
12
+ * Persistence: localStorage (key: "objectui-favorites", max 20 items).
13
+ *
14
+ * TODO: Migrate persistence to server-side storage via the adapter/API layer
15
+ * (e.g. PUT /api/user/preferences) so favorites sync across devices and browsers.
16
+ * The provider should accept an optional `persistenceAdapter` prop that implements
17
+ * `load(): Promise<FavoriteItem[]>` and `save(items: FavoriteItem[]): Promise<void>`.
18
+ * When the adapter is provided, localStorage should be used only as a fallback
19
+ * during the initial load while the server response is in-flight.
20
+ *
21
+ * @module
22
+ */
23
+ import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from 'react';
24
+ // ---------------------------------------------------------------------------
25
+ // Storage helpers
26
+ // ---------------------------------------------------------------------------
27
+ const STORAGE_KEY = 'objectui-favorites';
28
+ const MAX_FAVORITES = 20;
29
+ function loadFavorites() {
30
+ try {
31
+ const raw = localStorage.getItem(STORAGE_KEY);
32
+ return raw ? JSON.parse(raw) : [];
33
+ }
34
+ catch {
35
+ return [];
36
+ }
37
+ }
38
+ function saveFavorites(items) {
39
+ try {
40
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
41
+ }
42
+ catch {
43
+ // Storage full — silently ignore
44
+ }
45
+ }
46
+ // ---------------------------------------------------------------------------
47
+ // Context
48
+ // ---------------------------------------------------------------------------
49
+ const FavoritesContext = createContext(null);
50
+ export function FavoritesProvider({ children }) {
51
+ const [favorites, setFavorites] = useState(loadFavorites);
52
+ // Re-sync from storage on mount (handles cases where storage was written
53
+ // before this provider was mounted, e.g. on initial page load).
54
+ useEffect(() => {
55
+ setFavorites(loadFavorites());
56
+ }, []);
57
+ const addFavorite = useCallback((item) => {
58
+ setFavorites(prev => {
59
+ if (prev.some(f => f.id === item.id))
60
+ return prev;
61
+ const updated = [
62
+ { ...item, favoritedAt: new Date().toISOString() },
63
+ ...prev,
64
+ ].slice(0, MAX_FAVORITES);
65
+ saveFavorites(updated);
66
+ return updated;
67
+ });
68
+ }, []);
69
+ const removeFavorite = useCallback((id) => {
70
+ setFavorites(prev => {
71
+ const updated = prev.filter(f => f.id !== id);
72
+ saveFavorites(updated);
73
+ return updated;
74
+ });
75
+ }, []);
76
+ const toggleFavorite = useCallback((item) => {
77
+ setFavorites(prev => {
78
+ const exists = prev.some(f => f.id === item.id);
79
+ const updated = exists
80
+ ? prev.filter(f => f.id !== item.id)
81
+ : [{ ...item, favoritedAt: new Date().toISOString() }, ...prev].slice(0, MAX_FAVORITES);
82
+ saveFavorites(updated);
83
+ return updated;
84
+ });
85
+ }, []);
86
+ const clearFavorites = useCallback(() => {
87
+ setFavorites([]);
88
+ saveFavorites([]);
89
+ }, []);
90
+ const value = useMemo(() => ({
91
+ favorites,
92
+ addFavorite,
93
+ removeFavorite,
94
+ toggleFavorite,
95
+ // Inlined here so useMemo sees the freshest `favorites` without needing
96
+ // a separate useCallback([favorites]) entry in the deps array.
97
+ isFavorite: (id) => favorites.some(f => f.id === id),
98
+ clearFavorites,
99
+ }),
100
+ // addFavorite / removeFavorite / toggleFavorite / clearFavorites are all
101
+ // stable (useCallback with [] deps) and never cause extra re-renders.
102
+ [favorites, addFavorite, removeFavorite, toggleFavorite, clearFavorites]);
103
+ return (_jsx(FavoritesContext.Provider, { value: value, children: children }));
104
+ }
105
+ // ---------------------------------------------------------------------------
106
+ // Hook
107
+ // ---------------------------------------------------------------------------
108
+ /**
109
+ * Access shared favorites state.
110
+ *
111
+ * Must be used inside `<FavoritesProvider>`.
112
+ */
113
+ export function useFavorites() {
114
+ const ctx = useContext(FavoritesContext);
115
+ if (!ctx) {
116
+ // Graceful fallback: when a consumer (e.g. AppSidebar) is rendered outside
117
+ // a FavoritesProvider — common in unit tests that only need to assert on
118
+ // navigation rendering — return a no-op implementation rather than crash.
119
+ return {
120
+ favorites: [],
121
+ addFavorite: () => { },
122
+ removeFavorite: () => { },
123
+ toggleFavorite: () => { },
124
+ isFavorite: () => false,
125
+ clearFavorites: () => { },
126
+ };
127
+ }
128
+ return ctx;
129
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * NavigationContext
3
+ *
4
+ * Provides global navigation state for the unified sidebar and breadcrumb.
5
+ * Tracks whether the user is in "Home" context (workspace view) or "App"
6
+ * context (specific app), plus an optional record-detail title that record
7
+ * pages publish so the top-bar breadcrumb can show the human-readable title
8
+ * (e.g. "Acme Platform Upgrade") instead of the raw record ID.
9
+ *
10
+ * @module
11
+ */
12
+ import { type ReactNode } from 'react';
13
+ export type NavigationContextType = 'home' | 'app';
14
+ interface NavigationContextValue {
15
+ /** Current navigation context (home or app) */
16
+ context: NavigationContextType;
17
+ /** Set the navigation context */
18
+ setContext: (context: NavigationContextType) => void;
19
+ /** Current app name when in app context */
20
+ currentAppName?: string;
21
+ /** Set the current app name */
22
+ setCurrentAppName: (appName?: string) => void;
23
+ /**
24
+ * Human-readable title of the currently displayed record (when on a record
25
+ * detail page). Published by RecordDetailView once data loads; consumed by
26
+ * AppHeader to replace the raw `#shortId` segment in the breadcrumb.
27
+ */
28
+ recordTitle?: string;
29
+ /** Update the current record title (or clear it when leaving a record page). */
30
+ setRecordTitle: (title?: string) => void;
31
+ }
32
+ interface NavigationProviderProps {
33
+ children: ReactNode;
34
+ }
35
+ export declare function NavigationProvider({ children }: NavigationProviderProps): import("react/jsx-runtime").JSX.Element;
36
+ /**
37
+ * Hook to access navigation context
38
+ */
39
+ export declare function useNavigationContext(): NavigationContextValue;
40
+ /**
41
+ * Helper hook for record pages: sets the record title on mount and clears it
42
+ * on unmount, so the breadcrumb only shows a record title while a record page
43
+ * is actually visible.
44
+ */
45
+ export declare function useRecordBreadcrumbTitle(title: string | undefined): void;
46
+ export {};
@@ -0,0 +1,59 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * NavigationContext
4
+ *
5
+ * Provides global navigation state for the unified sidebar and breadcrumb.
6
+ * Tracks whether the user is in "Home" context (workspace view) or "App"
7
+ * context (specific app), plus an optional record-detail title that record
8
+ * pages publish so the top-bar breadcrumb can show the human-readable title
9
+ * (e.g. "Acme Platform Upgrade") instead of the raw record ID.
10
+ *
11
+ * @module
12
+ */
13
+ import { createContext, useContext, useState, useMemo, useEffect } from 'react';
14
+ const NavigationContext = createContext(undefined);
15
+ export function NavigationProvider({ children }) {
16
+ const [context, setContext] = useState('home');
17
+ const [currentAppName, setCurrentAppName] = useState();
18
+ const [recordTitle, setRecordTitle] = useState();
19
+ const value = useMemo(() => ({
20
+ context,
21
+ setContext,
22
+ currentAppName,
23
+ setCurrentAppName,
24
+ recordTitle,
25
+ setRecordTitle,
26
+ }), [context, currentAppName, recordTitle]);
27
+ return (_jsx(NavigationContext.Provider, { value: value, children: children }));
28
+ }
29
+ /**
30
+ * Hook to access navigation context
31
+ */
32
+ export function useNavigationContext() {
33
+ const context = useContext(NavigationContext);
34
+ if (!context) {
35
+ // Graceful fallback for consumers rendered outside <NavigationProvider>
36
+ // (common in lightweight unit tests). Production paths always wrap.
37
+ return {
38
+ context: 'app',
39
+ setContext: () => { },
40
+ currentAppName: undefined,
41
+ setCurrentAppName: () => { },
42
+ recordTitle: undefined,
43
+ setRecordTitle: () => { },
44
+ };
45
+ }
46
+ return context;
47
+ }
48
+ /**
49
+ * Helper hook for record pages: sets the record title on mount and clears it
50
+ * on unmount, so the breadcrumb only shows a record title while a record page
51
+ * is actually visible.
52
+ */
53
+ export function useRecordBreadcrumbTitle(title) {
54
+ const { setRecordTitle } = useNavigationContext();
55
+ useEffect(() => {
56
+ setRecordTitle(title);
57
+ return () => setRecordTitle(undefined);
58
+ }, [title, setRecordTitle]);
59
+ }
@@ -0,0 +1,3 @@
1
+ export { NavigationProvider, useNavigationContext } from './NavigationContext';
2
+ export { FavoritesProvider, useFavorites } from './FavoritesProvider';
3
+ export type { FavoriteItem } from './FavoritesProvider';
@@ -0,0 +1,2 @@
1
+ export { NavigationProvider, useNavigationContext } from './NavigationContext';
2
+ export { FavoritesProvider, useFavorites } from './FavoritesProvider';
@@ -0,0 +1,7 @@
1
+ export { useFavorites, type FavoriteItem } from './useFavorites';
2
+ export { useMetadataService } from './useMetadataService';
3
+ export { useNavPins } from './useNavPins';
4
+ export { useNavigationSync, NavigationSyncEffect, generateNavId, addNavigationItem, removeNavigationItems, renameNavigationItems, navigationEqual, type UseNavigationSyncReturn, } from './useNavigationSync';
5
+ export { useObjectActions } from './useObjectActions';
6
+ export { useRecentItems, type RecentItem } from './useRecentItems';
7
+ export { useResponsiveSidebar } from './useResponsiveSidebar';
@@ -0,0 +1,7 @@
1
+ export { useFavorites } from './useFavorites';
2
+ export { useMetadataService } from './useMetadataService';
3
+ export { useNavPins } from './useNavPins';
4
+ export { useNavigationSync, NavigationSyncEffect, generateNavId, addNavigationItem, removeNavigationItems, renameNavigationItems, navigationEqual, } from './useNavigationSync';
5
+ export { useObjectActions } from './useObjectActions';
6
+ export { useRecentItems } from './useRecentItems';
7
+ export { useResponsiveSidebar } from './useResponsiveSidebar';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * useFavorites — re-export shim
3
+ *
4
+ * The favorites state has been migrated to a React Context so all consumers
5
+ * share a single state instance (fixes star toggle not updating HomePage).
6
+ *
7
+ * All existing imports of `useFavorites` and `FavoriteItem` from this path
8
+ * continue to work without any changes at the call sites.
9
+ *
10
+ * @see apps/console/src/context/FavoritesProvider.tsx
11
+ * @module
12
+ */
13
+ export { useFavorites, type FavoriteItem } from '../context/FavoritesProvider';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * useFavorites — re-export shim
3
+ *
4
+ * The favorites state has been migrated to a React Context so all consumers
5
+ * share a single state instance (fixes star toggle not updating HomePage).
6
+ *
7
+ * All existing imports of `useFavorites` and `FavoriteItem` from this path
8
+ * continue to work without any changes at the call sites.
9
+ *
10
+ * @see apps/console/src/context/FavoritesProvider.tsx
11
+ * @module
12
+ */
13
+ export { useFavorites } from '../context/FavoritesProvider';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * useMetadataService
3
+ *
4
+ * React hook that creates a memoised `MetadataService` instance from the
5
+ * current `ObjectStackAdapter` (via `useAdapter`).
6
+ *
7
+ * Returns `null` when the adapter is not yet available (e.g. while
8
+ * the connection is being established).
9
+ *
10
+ * @module hooks/useMetadataService
11
+ */
12
+ import { MetadataService } from '../services/MetadataService';
13
+ export declare function useMetadataService(): MetadataService | null;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * useMetadataService
3
+ *
4
+ * React hook that creates a memoised `MetadataService` instance from the
5
+ * current `ObjectStackAdapter` (via `useAdapter`).
6
+ *
7
+ * Returns `null` when the adapter is not yet available (e.g. while
8
+ * the connection is being established).
9
+ *
10
+ * @module hooks/useMetadataService
11
+ */
12
+ import { useMemo } from 'react';
13
+ import { useAdapter } from '../providers/AdapterProvider';
14
+ import { MetadataService } from '../services/MetadataService';
15
+ export function useMetadataService() {
16
+ const adapter = useAdapter();
17
+ return useMemo(() => (adapter ? new MetadataService(adapter) : null), [adapter]);
18
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * useNavPins
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).
7
+ * @module
8
+ */
9
+ import type { NavigationItem } from '@object-ui/types';
10
+ export declare function useNavPins(): {
11
+ pinnedIds: string[];
12
+ togglePin: (itemId: string, pinned: boolean) => void;
13
+ isPinned: (itemId: string) => boolean;
14
+ applyPins: (items: NavigationItem[]) => NavigationItem[];
15
+ clearPins: () => void;
16
+ };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * useNavPins
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).
7
+ * @module
8
+ */
9
+ import { useState, useCallback, useEffect } from 'react';
10
+ const STORAGE_KEY = 'objectui-nav-pins';
11
+ 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 [];
24
+ }
25
+ }
26
+ function savePins(ids) {
27
+ try {
28
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(ids));
29
+ }
30
+ catch {
31
+ // Storage full — silently ignore
32
+ }
33
+ }
34
+ 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
+ /**
52
+ * Apply pinned state to a navigation item tree.
53
+ * Returns new items with `pinned` property set based on stored pin state.
54
+ */
55
+ 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 };
63
+ }
64
+ return item;
65
+ });
66
+ }, [pinnedIds]);
67
+ const clearPins = useCallback(() => {
68
+ setPinnedIds([]);
69
+ savePins([]);
70
+ }, []);
71
+ return { pinnedIds, togglePin, isPinned, applyPins, clearPins };
72
+ }