@object-ui/app-shell 5.1.1 → 5.3.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 (46) hide show
  1. package/CHANGELOG.md +322 -0
  2. package/dist/chrome/CommandPalette.d.ts +6 -1
  3. package/dist/chrome/CommandPalette.js +40 -4
  4. package/dist/chrome/ConsoleToaster.js +8 -1
  5. package/dist/chrome/ErrorBoundary.js +3 -0
  6. package/dist/chrome/RouteFader.d.ts +32 -0
  7. package/dist/chrome/RouteFader.js +52 -0
  8. package/dist/chrome/index.d.ts +2 -0
  9. package/dist/chrome/index.js +2 -0
  10. package/dist/chrome/toast-helpers.d.ts +48 -0
  11. package/dist/chrome/toast-helpers.js +59 -0
  12. package/dist/console/AppContent.js +108 -2
  13. package/dist/console/home/HomePage.js +5 -4
  14. package/dist/console/marketplace/MarkdownText.d.ts +19 -0
  15. package/dist/console/marketplace/MarkdownText.js +141 -0
  16. package/dist/console/marketplace/MarketplaceInstalledPage.d.ts +17 -0
  17. package/dist/console/marketplace/MarketplaceInstalledPage.js +64 -0
  18. package/dist/console/marketplace/MarketplacePackagePage.d.ts +7 -0
  19. package/dist/console/marketplace/MarketplacePackagePage.js +200 -0
  20. package/dist/console/marketplace/MarketplacePage.d.ts +8 -0
  21. package/dist/console/marketplace/MarketplacePage.js +94 -0
  22. package/dist/console/marketplace/PackageIcon.d.ts +19 -0
  23. package/dist/console/marketplace/PackageIcon.js +17 -0
  24. package/dist/console/marketplace/marketplaceApi.d.ts +122 -0
  25. package/dist/console/marketplace/marketplaceApi.js +207 -0
  26. package/dist/index.d.ts +6 -6
  27. package/dist/index.js +6 -5
  28. package/dist/layout/AppHeader.js +16 -2
  29. package/dist/layout/AppSidebar.js +15 -11
  30. package/dist/layout/InboxPopover.js +43 -3
  31. package/dist/observability/index.d.ts +9 -0
  32. package/dist/observability/index.js +9 -0
  33. package/dist/observability/sentry.d.ts +46 -0
  34. package/dist/observability/sentry.js +120 -0
  35. package/dist/types.d.ts +0 -46
  36. package/dist/views/RecordDetailView.js +79 -15
  37. package/package.json +26 -25
  38. package/src/styles.css +49 -0
  39. package/dist/components/DashboardRenderer.d.ts +0 -8
  40. package/dist/components/DashboardRenderer.js +0 -16
  41. package/dist/components/FormRenderer.d.ts +0 -8
  42. package/dist/components/FormRenderer.js +0 -31
  43. package/dist/components/ObjectRenderer.d.ts +0 -8
  44. package/dist/components/ObjectRenderer.js +0 -74
  45. package/dist/components/PageRenderer.d.ts +0 -7
  46. package/dist/components/PageRenderer.js +0 -14
package/CHANGELOG.md CHANGED
@@ -1,5 +1,327 @@
1
1
  # @object-ui/app-shell — Changelog
2
2
 
3
+ ## 5.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - efb4c00: feat(observability): Sentry integration + bundle splitting for production launch
8
+
9
+ **Sentry (opt-in via `VITE_SENTRY_DSN`)**
10
+ - New `initSentry()` / `captureError()` / `setSentryUser()` / `getSentry()`
11
+ helpers exported from `@object-ui/app-shell`.
12
+ - Dynamic-import design: when `VITE_SENTRY_DSN` is unset, `@sentry/react`
13
+ is **never fetched** — zero bundle cost for self-hosted users.
14
+ - `ErrorBoundary.componentDidCatch` now best-effort reports to Sentry.
15
+ - Console app calls `initSentry()` before React mount; never blocks first
16
+ paint.
17
+ - Configurable via:
18
+ - `VITE_SENTRY_DSN` — required to enable
19
+ - `VITE_SENTRY_ENVIRONMENT` — defaults to `MODE`
20
+ - `VITE_SENTRY_RELEASE` — defaults to `VITE_APP_VERSION`
21
+ - `VITE_SENTRY_TRACES_SAMPLE_RATE` — defaults to `0.1`
22
+ - `VITE_SENTRY_REPLAY=true` — opt-in to 10% on-error replay
23
+ - Sensitive URL params (`token`, `access_token`, `apiKey`, etc.) are
24
+ stripped from breadcrumb URLs before send.
25
+
26
+ **Bundle splitting**
27
+ - `plugin-dashboard` (8 component types) now lazy-registered via
28
+ `ComponentRegistry.registerLazy()` — only loads on dashboard pages.
29
+ - `plugin-dashboard` and `plugin-report` each get their own chunk
30
+ (previously merged into `plugins-views`).
31
+ - Net first-paint JS reduction: **~200 KB** when the user never visits a
32
+ dashboard or report page.
33
+ - New chunks: `plugin-dashboard` (119 K), `plugin-report` (92 K),
34
+ `vendor-sentry` (346 K raw / 97 K brotli, lazy).
35
+ - `plugins-views` shrinks 387 K → 180 K (now `plugin-list` + `plugin-detail` only).
36
+
37
+ ### Patch Changes
38
+
39
+ - @object-ui/types@5.3.0
40
+ - @object-ui/core@5.3.0
41
+ - @object-ui/i18n@5.3.0
42
+ - @object-ui/react@5.3.0
43
+ - @object-ui/components@5.3.0
44
+ - @object-ui/fields@5.3.0
45
+ - @object-ui/layout@5.3.0
46
+ - @object-ui/data-objectstack@5.3.0
47
+ - @object-ui/auth@5.3.0
48
+ - @object-ui/permissions@5.3.0
49
+ - @object-ui/collaboration@5.3.0
50
+ - @object-ui/providers@5.3.0
51
+
52
+ ## 5.2.1
53
+
54
+ ### Patch Changes
55
+
56
+ - 9ccda28: security: force DOMPurify to `^3.4.5` via pnpm override
57
+
58
+ Resolves 8 moderate-severity GHSA advisories against the transitive
59
+ `dompurify@3.2.7` pulled in by `monaco-editor`. Vulnerabilities covered:
60
+ - SAFE_FOR_TEMPLATES bypass in RETURN_DOM mode
61
+ - FORBID_TAGS bypassed by function-based ADD_TAGS predicate
62
+ - Prototype Pollution to XSS via CUSTOM_ELEMENT_HANDLING fallback
63
+ - ADD_TAGS function-form short-circuit bypass of FORBID_TAGS
64
+ - ADD_ATTR predicate skipping URI validation
65
+ - USE_PROFILES prototype pollution enabling event handlers
66
+ - mutation-XSS via Re-Contextualization
67
+ - Generic XSS vector
68
+
69
+ No API changes; override is transparent to consumers.
70
+ - @object-ui/types@5.2.1
71
+ - @object-ui/core@5.2.1
72
+ - @object-ui/i18n@5.2.1
73
+ - @object-ui/react@5.2.1
74
+ - @object-ui/components@5.2.1
75
+ - @object-ui/fields@5.2.1
76
+ - @object-ui/layout@5.2.1
77
+ - @object-ui/data-objectstack@5.2.1
78
+ - @object-ui/auth@5.2.1
79
+ - @object-ui/permissions@5.2.1
80
+ - @object-ui/collaboration@5.2.1
81
+ - @object-ui/providers@5.2.1
82
+
83
+ ## 5.2.0
84
+
85
+ ### Minor Changes
86
+
87
+ - 321294c: Cmd-K now shows recently viewed records in its empty state, sourced
88
+ from the existing cloud-synced `sys_user_preference` adapter (already
89
+ wired by `RecentItemsProvider` + `useTrackRouteAsRecent` +
90
+ `RecordDetailView`). Multi-device by construction: open a record on
91
+ laptop, see it in `⌘K → Recently viewed` on phone.
92
+ - Group renders only when input is empty (no competition with search).
93
+ - Limited to the 5 most recent record-type entries.
94
+ - New i18n key `console.commandPalette.recentRecords` (en + zh seeded;
95
+ other locales fall back to `defaultValue: "Recently viewed"`).
96
+
97
+ - b2d1704: feat(cmdk): record search across objects in the Command Palette
98
+ - New `useRecordSearch` hook in `@object-ui/react` debounces a query, fans out
99
+ to `dataSource.find(name, { $search, $top })` across candidate objects, and
100
+ aggregates hits. Race-safe via a monotonic runId; per-object 404s are
101
+ silently dropped via `Promise.allSettled`.
102
+ - `CommandPalette` (`@object-ui/app-shell`) now accepts a `dataSource` prop;
103
+ when supplied, the palette renders a `Records` group at the top with hits
104
+ scoped to the active app's nav objects. Item `value` embeds the live query
105
+ so cmdk's client-side filter doesn't hide async results.
106
+ - Added `console.commandPalette.records` i18n key (`Records` / `记录`).
107
+
108
+ - 921bd28: Console now honors `App.homePageId` for the bare `/console/apps/:appName`
109
+ landing route. Previously it always redirected to the first reachable nav
110
+ item, so CRM-style apps with KPI dashboards still landed users on the
111
+ first object list (e.g. Leads) rather than the configured home page.
112
+
113
+ The new `resolveLandingRoute` looks up the `homePageId` nav item, builds
114
+ its route (object / view / page / dashboard / report), and falls back to
115
+ the existing `findFirstRoute` only when no `homePageId` is set or it
116
+ resolves to a routeless item type.
117
+
118
+ - 3ebba63: Fix silent blank page on shorthand record deep-links.
119
+
120
+ Three related fixes that all addressed the same UX: a user follows a URL
121
+ shaped `/{object}/{recordId}` and sees a completely blank content area.
122
+ 1. **`useNavigationOverlay` produced the broken URL itself.** When
123
+ middle-click / Cmd-click opened a gallery card in a new tab and no
124
+ `onNavigate` was provided, the hook built `/{object}/{id}` — a URL
125
+ shape that does not match any route in the console route table. The
126
+ builder now emits the canonical `/{object}/record/{id}`.
127
+ 2. **Shorthand redirect for externally shared links.** Even with the
128
+ producer fixed, links pasted from email / Slack / older builds
129
+ still use the shorthand. The console now intercepts
130
+ `/{:objectName}/:maybeRecordId` and, when the second segment looks
131
+ like a record id (URL-safe slug ≥ 6 chars, not a reserved keyword),
132
+ redirects to `/{objectName}/record/{recordId}` preserving query and
133
+ hash.
134
+ 3. **Visible 404 fallback.** Routes that match nothing at all now
135
+ render an explicit "Page not found" empty state with a "Go back"
136
+ action instead of leaving the content area blank. Silent failures
137
+ are now visible failures.
138
+
139
+ - a4a0e1d: Add `<PresenceProvider>` abstraction with `useTenantPresence()` and
140
+ `useRecordPresence(objectName, recordId)` hooks. The default source is a
141
+ no-op so hooks return `[]` until a host app wires in a realtime
142
+ transport (WebSocket / SSE). Replaces the two architectural TODOs in
143
+ `AppHeader` (tenant scope) and `RecordDetailView` (record scope) that
144
+ were waiting on this abstraction.
145
+
146
+ `AppHeader` now falls back to `useTenantPresence()` when the
147
+ `presenceUsers` prop is omitted, and `RecordDetailView` renders
148
+ `<PresenceAvatars>` next to the lifecycle badge when other users are
149
+ viewing the same record. Both code paths render exactly as before when
150
+ no provider is mounted, so this change is non-visual for existing
151
+ consumers.
152
+
153
+ ### Patch Changes
154
+
155
+ - 9997cae: DataSource: add optional `bulkUpdate(resource, ids, patch)` for "same patch, many rows" interactions (Slack "mark all as read", Linear "archive selected"). The ObjectStack adapter routes to `POST /api/v1/data/:object/updateMany` so the client pays one HTTP/auth/RLS round-trip instead of N parallel PATCHes, eliminating mark-all-read jank on inboxes with 50+ unread.
156
+
157
+ AppHeader's `markAllRead` now prefers `bulkUpdate`, with a transparent fallback to the per-id loop for adapters that don't implement the helper.
158
+
159
+ - 0a644f0: feat(app-shell): CommandPalette searching indicator
160
+
161
+ When `useRecordSearch` is mid-flight (debounced fetch across objects
162
+ hasn't returned yet), the palette now surfaces a subtle visual:
163
+ - A small pulsing primary-coloured dot next to the **Records** group
164
+ heading, so the user sees that more results may still appear.
165
+ - A `Searching…` placeholder inside the empty state when the user has
166
+ typed something but no hits exist yet — replaces the static
167
+ "No results found." message until the request settles.
168
+
169
+ New i18n key `console.commandPalette.searching` (en + zh).
170
+
171
+ - 5f71924: feat(app-shell): better default toast UX in ConsoleToaster
172
+
173
+ `ConsoleToaster` now ships UX-positive defaults that match the Linear
174
+ / Notion pattern users expect from an enterprise console:
175
+ - `position="top-right"` — keeps the user's primary work area (centre
176
+ - bottom) unobstructed.
177
+ - `closeButton` — every toast has an explicit X so users can dismiss
178
+ rather than wait the duration out.
179
+ - `richColors` — type-aware coloured backgrounds (success / error /
180
+ warning / info) so the kind of message is legible at a glance.
181
+ - `expand` — toast stack expands on hover so users can read multiple
182
+ recent toasts without dismissing.
183
+ - `visibleToasts={4}` — prevents the corner from being overrun.
184
+ - `duration: 4000` — long enough to read + click an `Undo` action.
185
+
186
+ All of these are still overridable via `<ConsoleToaster …>` props.
187
+
188
+ - 5425608: CRM UX polish pass — calmer enterprise look across detail + kanban.
189
+ - **plugin-kanban**: column headers now use a 2px muted accent stripe with
190
+ neutral foreground titles + a quiet grey count pill instead of full
191
+ rainbow gradient + colored title + colored count. Pipeline boards
192
+ (Opportunity, Case, Task, Lead) look like Salesforce/Linear instead of
193
+ a toy. WIP-limit overflow remains destructive-red so urgency stays loud.
194
+ - **plugin-detail (`record:reference_rail`)**: new `hideEmpty` prop
195
+ (default true) collapses entries whose total === 0 into a single
196
+ `+ N empty (Quotes · Products …)` chip at the bottom of the rail.
197
+ Removes the 4–7 "No records" stack that dominated the aside.
198
+ - **plugin-detail (`record:path`)**: completed stages now render with an
199
+ emerald-tinted background + bold green check instead of low-contrast
200
+ `bg-muted text-muted-foreground` (which read as "light grey on white"
201
+ and was borderline unreadable).
202
+ - **app-shell (`RecordDetailView`)**: record-not-found short-circuit.
203
+ Previously a stale/missing recordId still rendered the page chrome
204
+ (rail, discussion, breadcrumb with the raw id), making invalid links
205
+ look like a partially broken page. Now renders a clean centered
206
+ `Empty` state with database icon + i18n'd "Record not found" copy.
207
+ - **i18n**: added `detail.showEmptyRelated_{one,other}` and
208
+ `empty.recordNotFound{,Description}` keys (en + zh).
209
+
210
+ - 710fbe6: feat(app-shell): notification center animation polish
211
+
212
+ InboxPopover now animates every signal that matters for "noticing":
213
+ - Bell button **bounces once** when total pressure increases (new
214
+ notification or approval arrives). Tracks previous total via a ref
215
+ so the very first render — when the server-side counts hydrate —
216
+ does not trigger a spurious bounce.
217
+ - Bell badge **zooms in** on every count change (re-keyed on
218
+ `totalBadge` so each transition is an independent animation).
219
+ - Per-tab counter badges (Notifications / Approvals) get the same
220
+ zoom-in treatment on count change.
221
+ - Notification list rows **fade + slide in from top** with a small
222
+ staggered delay (capped at 6×20ms so a full list never feels
223
+ laggy).
224
+ - Activity rows mirror the same fade/slide pattern.
225
+ - Empty states (`You're all caught up`, `No recent activity`, `No
226
+ pending approvals`) fade in instead of popping in.
227
+ - The unread dot (•) is now always rendered but fades its opacity
228
+ when `is_read` flips, instead of disappearing instantly — gives a
229
+ smooth "marked read" affordance.
230
+
231
+ All animations are wrapped in `motion-safe:` utility variants so
232
+ users with `prefers-reduced-motion` see the previous (instant) UI.
233
+ No new dependencies; reuses `tailwindcss-animate` utilities already
234
+ present in the design system.
235
+
236
+ - 7c441f5: End-to-end @-mention notifications.
237
+
238
+ `@object-ui/plugin-detail` now exports `extractMentions(text, suggestions)`
239
+ — a small utility that resolves `@<label>` tokens in a comment body to
240
+ user ids, using the same suggestion list that drives the in-editor
241
+ dropdown. Handles labels with spaces ("@QA Test"), CJK ("@王小明"),
242
+ longest-match disambiguation ("Anna Lee" wins over "Anna"), and ignores
243
+ unknown @-tokens. 9 unit tests.
244
+
245
+ `@object-ui/app-shell` `RecordDetailView` now:
246
+ 1. Serializes the resolved mention ids into `sys_comment.mentions`
247
+ (previously hard-coded `'[]'`, so servers had no idea who was being
248
+ pinged).
249
+ 2. Fan-outs a `sys_notification` row per mentioned recipient
250
+ (self-mentions are filtered as noise) with the canonical bell-inbox
251
+ shape: `type: 'mention'`, `recipient_id`, `actor_name`, `title`,
252
+ `body` preview (≤140 chars), `source_object`/`source_id`/
253
+ `source_comment_id`, `is_read: false`, `created_at`.
254
+
255
+ The notification write tolerates 404 silently, so deployments without
256
+ a notification collection degrade to the previous behavior (mention
257
+ text + highlight, no inbox row). Spec-compliant servers that emit
258
+ notifications via their own sys_comment after-create hook can ignore
259
+ the client-side write — the bell de-dupes by id at the polling layer.
260
+
261
+ - 072cad0: Always seed @-mention suggestions with the current user so the dropdown
262
+ appears even when the backend has no `sys_user` directory (or the fetch
263
+ fails). Hosts with a real user roster still get the merged list —
264
+ current user first, then directory entries de-duped by id.
265
+
266
+ Previously, typing `@` in the discussion comment box was a no-op on
267
+ example backends that don't serve `sys_user`, making the feature look
268
+ broken. Authors can now at minimum mention themselves; richer rosters
269
+ are merged in automatically when available.
270
+
271
+ - 54e3dfb: Remove unused stub renderers from `@object-ui/app-shell`:
272
+ - `ObjectRenderer` / `ObjectRendererProps`
273
+ - `DashboardRenderer` / `DashboardRendererProps`
274
+ - `PageRenderer` / `PageRendererProps`
275
+ - `FormRenderer` / `FormRendererProps`
276
+
277
+ These were placeholder components that never delegated to a real
278
+ SchemaRenderer — they rendered a literal `"TODO"` string and were not
279
+ consumed anywhere in the monorepo or in the official Console app.
280
+ Because they were non-functional, no working production code could
281
+ have depended on them; this is treated as a patch-level cleanup rather
282
+ than a semver-major break.
283
+
284
+ If you were importing one of the removed stubs (and somehow got past
285
+ the "TODO" placeholder render), the real renderers ship from the
286
+ respective plugin packages:
287
+ - Dashboard → `@object-ui/plugin-dashboard` (`DashboardRenderer`)
288
+ - Page / Object / Form → `@object-ui/react` (`SchemaRenderer`) +
289
+ `@object-ui/plugin-form` / `@object-ui/plugin-grid` etc.
290
+
291
+ - Updated dependencies [de0c5e6]
292
+ - Updated dependencies [9997cae]
293
+ - Updated dependencies [321294c]
294
+ - Updated dependencies [b2d1704]
295
+ - Updated dependencies [0a644f0]
296
+ - Updated dependencies [a3cb88f]
297
+ - Updated dependencies [5425608]
298
+ - Updated dependencies [6c3f018]
299
+ - Updated dependencies [d912a60]
300
+ - Updated dependencies [87bc8ff]
301
+ - Updated dependencies [3ebba63]
302
+ - Updated dependencies [e919433]
303
+ - Updated dependencies [a8d12ec]
304
+ - Updated dependencies [a4a0e1d]
305
+ - Updated dependencies [70b5570]
306
+ - Updated dependencies [aa063db]
307
+ - Updated dependencies [d9c3bae]
308
+ - Updated dependencies [d1442e3]
309
+ - Updated dependencies [7c7400a]
310
+ - Updated dependencies [b703480]
311
+ - Updated dependencies [e7b6eae]
312
+ - @object-ui/types@5.2.0
313
+ - @object-ui/data-objectstack@5.2.0
314
+ - @object-ui/core@5.2.0
315
+ - @object-ui/i18n@5.2.0
316
+ - @object-ui/react@5.2.0
317
+ - @object-ui/fields@5.2.0
318
+ - @object-ui/components@5.2.0
319
+ - @object-ui/collaboration@5.2.0
320
+ - @object-ui/layout@5.2.0
321
+ - @object-ui/auth@5.2.0
322
+ - @object-ui/permissions@5.2.0
323
+ - @object-ui/providers@5.2.0
324
+
3
325
  ## 5.1.1
4
326
 
5
327
  ### Patch Changes
@@ -11,6 +11,11 @@ interface CommandPaletteProps {
11
11
  activeApp: any;
12
12
  objects: any[];
13
13
  onAppChange: (name: string) => void;
14
+ /**
15
+ * Optional data source used to power record search across objects. When
16
+ * omitted, the palette behaves exactly as before — nav items only.
17
+ */
18
+ dataSource?: any;
14
19
  }
15
- export declare function CommandPalette({ apps, activeApp, objects: _objects, onAppChange }: CommandPaletteProps): import("react/jsx-runtime").JSX.Element;
20
+ export declare function CommandPalette({ apps, activeApp, objects, onAppChange, dataSource }: CommandPaletteProps): import("react/jsx-runtime").JSX.Element;
16
21
  export {};
@@ -7,17 +7,20 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
7
7
  *
8
8
  * Uses Shadcn's Command (cmdk) component — keyboard-accessible, fuzzy search.
9
9
  */
10
- import { useEffect, useState, useCallback } from 'react';
10
+ import { useEffect, useState, useCallback, useMemo } from 'react';
11
11
  import { useNavigate, useParams } from 'react-router-dom';
12
12
  import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from '@object-ui/components';
13
13
  import { LayoutDashboard, FileText, BarChart3, Moon, Sun, Monitor, Search, Plus, } from 'lucide-react';
14
+ import { useRecordSearch } from '@object-ui/react';
14
15
  import { useTheme } from './ThemeProvider';
15
16
  import { useExpressionContext, evaluateVisibility } from '../providers/ExpressionProvider';
16
17
  import { useObjectTranslation } from '@object-ui/i18n';
17
- import { resolveI18nLabel } from '../utils';
18
+ import { resolveI18nLabel, getRecordDisplayName } from '../utils';
18
19
  import { getIcon } from '../utils/getIcon';
19
- export function CommandPalette({ apps, activeApp, objects: _objects, onAppChange }) {
20
+ import { useRecentItems } from '../context/RecentItemsProvider';
21
+ export function CommandPalette({ apps, activeApp, objects, onAppChange, dataSource }) {
20
22
  const [open, setOpen] = useState(false);
23
+ const [inputValue, setInputValue] = useState('');
21
24
  const navigate = useNavigate();
22
25
  const { appName } = useParams();
23
26
  const { setTheme } = useTheme();
@@ -34,6 +37,11 @@ export function CommandPalette({ apps, activeApp, objects: _objects, onAppChange
34
37
  document.addEventListener('keydown', down);
35
38
  return () => document.removeEventListener('keydown', down);
36
39
  }, []);
40
+ // Reset query when the palette closes so reopening doesn't show stale state.
41
+ useEffect(() => {
42
+ if (!open)
43
+ setInputValue('');
44
+ }, [open]);
37
45
  const baseUrl = `/apps/${appName || activeApp?.name}`;
38
46
  const runCommand = useCallback((command) => {
39
47
  setOpen(false);
@@ -41,7 +49,35 @@ export function CommandPalette({ apps, activeApp, objects: _objects, onAppChange
41
49
  }, []);
42
50
  // Extract navigation items from active app, filtering by visibility expressions
43
51
  const navItems = flattenNavigation(activeApp?.navigation || []).filter((item) => evaluateVisibility(item.visible ?? item.visibleOn, evaluator));
44
- return (_jsxs(CommandDialog, { open: open, onOpenChange: setOpen, children: [_jsx(CommandInput, { placeholder: t('console.commandPalette.placeholder') }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: t('console.commandPalette.noResults') }), navItems.filter(i => i.type === 'object').length > 0 && (_jsx(CommandGroup, { heading: t('console.commandPalette.objects'), children: navItems
52
+ // Whitelist of object names visible in this app's nav used as the search
53
+ // scope so we don't fan out to every object in the tenant.
54
+ const searchableObjectNames = useMemo(() => navItems
55
+ .filter((i) => i.type === 'object' && typeof i.objectName === 'string')
56
+ .map((i) => i.objectName),
57
+ // navItems is rebuilt every render (filtered list); use a stable signature.
58
+ [activeApp?.name, navItems.map((i) => i.objectName || '').join('|')]);
59
+ const { results: recordHits, isSearching } = useRecordSearch({
60
+ query: inputValue,
61
+ objects,
62
+ dataSource,
63
+ objectNames: searchableObjectNames,
64
+ enabled: open && Boolean(dataSource),
65
+ getDisplayName: getRecordDisplayName,
66
+ });
67
+ // Cloud-synced (sys_user_preference) recently-visited records,
68
+ // surfaced in the empty state so the palette is useful before the
69
+ // user types anything. Filtered down to record-type entries so we
70
+ // don't double up with the per-app nav above.
71
+ const { recentItems } = useRecentItems();
72
+ const recentRecords = useMemo(() => recentItems.filter((it) => it.type === 'record').slice(0, 5), [recentItems]);
73
+ const showRecentRecords = open && inputValue.trim().length === 0 && recentRecords.length > 0;
74
+ return (_jsxs(CommandDialog, { open: open, onOpenChange: setOpen, children: [_jsx(CommandInput, { placeholder: t('console.commandPalette.placeholder'), value: inputValue, onValueChange: setInputValue }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: isSearching ? (_jsxs("span", { className: "inline-flex items-center gap-2 text-muted-foreground", children: [_jsx("span", { "aria-hidden": true, className: "inline-block h-1.5 w-1.5 rounded-full bg-primary motion-safe:animate-pulse" }), t('console.commandPalette.searching', { defaultValue: 'Searching…' })] })) : (t('console.commandPalette.noResults')) }), showRecentRecords && (_jsx(CommandGroup, { heading: t('console.commandPalette.recentRecords', { defaultValue: 'Recently viewed' }), children: recentRecords.map((item) => (_jsxs(CommandItem, { value: `recent ${item.label} ${item.id}`, onSelect: () => runCommand(() => navigate(item.href)), children: [_jsx(Search, { className: "mr-2 h-4 w-4" }), _jsx("span", { className: "truncate", children: item.label })] }, `recent:${item.id}`))) })), recordHits.length > 0 && (_jsx(CommandGroup, { heading: _jsxs("span", { className: "inline-flex items-center gap-2", children: [t('console.commandPalette.records', { defaultValue: 'Records' }), isSearching && (_jsx("span", { "aria-hidden": true, className: "inline-block h-1.5 w-1.5 rounded-full bg-primary motion-safe:animate-pulse" }))] }), children: recordHits.map((hit) => {
75
+ const Icon = getIcon(hit.icon);
76
+ return (_jsxs(CommandItem, {
77
+ // Embed the live query so cmdk's client-side filter doesn't
78
+ // hide async hits that don't textually match the input.
79
+ value: `record ${inputValue} ${hit.display} ${hit.objectLabel} ${hit.objectName} ${hit.recordId}`, onSelect: () => runCommand(() => navigate(`${baseUrl}/${hit.objectName}/record/${hit.recordId}`)), children: [_jsx(Icon, { className: "mr-2 h-4 w-4" }), _jsx("span", { className: "truncate", children: hit.display }), _jsx("span", { className: "ml-auto text-xs text-muted-foreground", children: hit.objectLabel })] }, `${hit.objectName}:${hit.recordId}`));
80
+ }) })), navItems.filter(i => i.type === 'object').length > 0 && (_jsx(CommandGroup, { heading: t('console.commandPalette.objects'), children: navItems
45
81
  .filter(i => i.type === 'object')
46
82
  .map(item => {
47
83
  const Icon = getIcon(item.icon);
@@ -11,13 +11,20 @@ import { CircleCheck, Info, LoaderCircle, OctagonX, TriangleAlert } from 'lucide
11
11
  import { useTheme } from './ThemeProvider';
12
12
  export function ConsoleToaster(props) {
13
13
  const { theme = 'system' } = useTheme();
14
- return (_jsx(Sonner, { theme: theme, className: "toaster group", icons: {
14
+ return (_jsx(Sonner, { theme: theme, className: "toaster group",
15
+ // UX defaults chosen for an enterprise console — match the Linear /
16
+ // Notion pattern users expect. Callers can still override any of
17
+ // these via the spread `{...props}` below.
18
+ position: "top-right", closeButton: true, richColors: true, expand: true, visibleToasts: 4, icons: {
15
19
  success: _jsx(CircleCheck, { className: "h-4 w-4" }),
16
20
  info: _jsx(Info, { className: "h-4 w-4" }),
17
21
  warning: _jsx(TriangleAlert, { className: "h-4 w-4" }),
18
22
  error: _jsx(OctagonX, { className: "h-4 w-4" }),
19
23
  loading: _jsx(LoaderCircle, { className: "h-4 w-4 animate-spin" }),
20
24
  }, toastOptions: {
25
+ // 4s default keeps actionable toasts visible long enough to
26
+ // click an Undo button without feeling sticky.
27
+ duration: 4000,
21
28
  classNames: {
22
29
  toast: 'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
23
30
  description: 'group-[.toast]:text-muted-foreground',
@@ -19,6 +19,7 @@ import { Component } from 'react';
19
19
  import { Button, Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
20
20
  import { AlertTriangle, RotateCcw, Home } from 'lucide-react';
21
21
  import { useObjectTranslation } from '@object-ui/i18n';
22
+ import { captureError } from '../observability';
22
23
  /** Inner fallback component that uses the i18n hook */
23
24
  function DefaultErrorFallback({ error, onReset }) {
24
25
  const { t } = useObjectTranslation();
@@ -42,6 +43,8 @@ export class ErrorBoundary extends Component {
42
43
  }
43
44
  componentDidCatch(error, errorInfo) {
44
45
  console.error('[ErrorBoundary] Caught error:', error, errorInfo);
46
+ // Best-effort: report to Sentry if initialized. No-op when DSN absent.
47
+ captureError(error, { componentStack: errorInfo.componentStack });
45
48
  this.props.onError?.(error, errorInfo);
46
49
  }
47
50
  render() {
@@ -0,0 +1,32 @@
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
+ * `RouteFader` — light-touch fade-in animation that replays whenever
10
+ * the route pathname changes.
11
+ *
12
+ * Why this implementation choice:
13
+ * The textbook approach (`<div key={pathname}>`) would remount every
14
+ * page on navigation. That breaks scroll position, loses form state,
15
+ * and re-fetches data that didn't need to refetch.
16
+ *
17
+ * Instead we keep the wrapper stable and replay the CSS animation by
18
+ * manipulating className directly via a layout effect: strip the
19
+ * animation classes, force a reflow, then re-add them. The browser
20
+ * restarts the animation against the same DOM node. React's VDOM
21
+ * doesn't see the temporary class swap — it's pure DOM choreography
22
+ * — so children are never remounted.
23
+ *
24
+ * The animation is gated on `motion-safe:` so users with
25
+ * `prefers-reduced-motion: reduce` see hard page swaps.
26
+ */
27
+ import * as React from 'react';
28
+ export interface RouteFaderProps {
29
+ children: React.ReactNode;
30
+ className?: string;
31
+ }
32
+ export declare function RouteFader({ children, className }: RouteFaderProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,52 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * ObjectUI
4
+ * Copyright (c) 2024-present ObjectStack Inc.
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+ /**
10
+ * `RouteFader` — light-touch fade-in animation that replays whenever
11
+ * the route pathname changes.
12
+ *
13
+ * Why this implementation choice:
14
+ * The textbook approach (`<div key={pathname}>`) would remount every
15
+ * page on navigation. That breaks scroll position, loses form state,
16
+ * and re-fetches data that didn't need to refetch.
17
+ *
18
+ * Instead we keep the wrapper stable and replay the CSS animation by
19
+ * manipulating className directly via a layout effect: strip the
20
+ * animation classes, force a reflow, then re-add them. The browser
21
+ * restarts the animation against the same DOM node. React's VDOM
22
+ * doesn't see the temporary class swap — it's pure DOM choreography
23
+ * — so children are never remounted.
24
+ *
25
+ * The animation is gated on `motion-safe:` so users with
26
+ * `prefers-reduced-motion: reduce` see hard page swaps.
27
+ */
28
+ import * as React from 'react';
29
+ import { useLocation } from 'react-router-dom';
30
+ const ANIM_CLASSES = ['motion-safe:animate-in', 'motion-safe:fade-in-0', 'motion-safe:duration-150'];
31
+ export function RouteFader({ children, className }) {
32
+ const location = useLocation();
33
+ const ref = React.useRef(null);
34
+ const prevPath = React.useRef(location.pathname);
35
+ React.useLayoutEffect(() => {
36
+ if (prevPath.current === location.pathname)
37
+ return;
38
+ prevPath.current = location.pathname;
39
+ const el = ref.current;
40
+ if (!el)
41
+ return;
42
+ // Drop the animation classes, force a reflow read, then re-add
43
+ // them. This is the canonical CSS "restart animation" trick.
44
+ el.classList.remove(...ANIM_CLASSES);
45
+ // Reading `offsetWidth` forces layout, which is what tells the
46
+ // browser to flush the previous animation state.
47
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
48
+ el.offsetWidth;
49
+ el.classList.add(...ANIM_CLASSES);
50
+ }, [location.pathname]);
51
+ return (_jsx("div", { ref: ref, className: [...ANIM_CLASSES, className ?? ''].filter(Boolean).join(' '), children: children }));
52
+ }
@@ -3,6 +3,8 @@ export { KeyboardShortcutsDialog } from './KeyboardShortcutsDialog';
3
3
  export { OnboardingWalkthrough } from './OnboardingWalkthrough';
4
4
  export { ConditionalAuthWrapper } from './ConditionalAuthWrapper';
5
5
  export { ConsoleToaster } from './ConsoleToaster';
6
+ export { RouteFader } from './RouteFader';
6
7
  export { ErrorBoundary } from './ErrorBoundary';
7
8
  export { LoadingScreen } from './LoadingScreen';
8
9
  export { ThemeProvider, useTheme } from './ThemeProvider';
10
+ export { toastWithUndo, type ToastWithUndoOptions } from './toast-helpers';
@@ -3,6 +3,8 @@ export { KeyboardShortcutsDialog } from './KeyboardShortcutsDialog';
3
3
  export { OnboardingWalkthrough } from './OnboardingWalkthrough';
4
4
  export { ConditionalAuthWrapper } from './ConditionalAuthWrapper';
5
5
  export { ConsoleToaster } from './ConsoleToaster';
6
+ export { RouteFader } from './RouteFader';
6
7
  export { ErrorBoundary } from './ErrorBoundary';
7
8
  export { LoadingScreen } from './LoadingScreen';
8
9
  export { ThemeProvider, useTheme } from './ThemeProvider';
10
+ export { toastWithUndo } from './toast-helpers';
@@ -0,0 +1,48 @@
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
+ * Toast helpers — convention-encoding wrappers around `sonner` so
10
+ * destructive flows (delete, archive, bulk-update) consistently expose
11
+ * an Undo affordance.
12
+ *
13
+ * Why a helper instead of raw `toast.success(...)` at call sites:
14
+ * 1. Most "delete OK" toasts in the codebase do not offer Undo, even
15
+ * when the underlying API supports a reversal. The helper makes
16
+ * adding Undo a one-line change.
17
+ * 2. The label, duration, and aria semantics stay consistent across
18
+ * surfaces — recently-deleted records, archived comments, bulk
19
+ * operations — so users don't have to relearn the affordance per
20
+ * view.
21
+ *
22
+ * Usage:
23
+ *
24
+ * import { toastWithUndo } from '@object-ui/app-shell';
25
+ *
26
+ * toastWithUndo('Lead deleted', {
27
+ * onUndo: () => dataSource.restore('lead', id),
28
+ * });
29
+ */
30
+ import { type ExternalToast } from 'sonner';
31
+ export interface ToastWithUndoOptions extends ExternalToast {
32
+ /**
33
+ * Callback invoked when the user clicks the Undo action. May return a
34
+ * promise; the toast stays visible until it resolves and a follow-up
35
+ * success/error toast is emitted automatically.
36
+ */
37
+ onUndo: () => void | Promise<unknown>;
38
+ /** Label for the Undo button. Defaults to "Undo". */
39
+ undoLabel?: string;
40
+ /** Toast intent. Defaults to "success" (green check). */
41
+ intent?: 'success' | 'info' | 'warning';
42
+ /**
43
+ * Toast visible duration in ms. Slightly longer than default 4s so
44
+ * users have time to click Undo on screen-edge toasts.
45
+ */
46
+ duration?: number;
47
+ }
48
+ export declare function toastWithUndo(message: string, opts: ToastWithUndoOptions): string | number;