@object-ui/app-shell 4.7.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,421 @@
1
1
  # @object-ui/app-shell — Changelog
2
2
 
3
+ ## 5.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8930b15: feat(detail): close the gap between Page-assigned and default record detail pages (Track 1)
8
+
9
+ Custom Lightning-style record detail pages (assigned via `assignedPage` /
10
+ `Page` schemas) used to feel meaningfully poorer than the auto-generated
11
+ default detail view. They were missing cross-cutting affordances and
12
+ shipped with English-only tab labels and heavy bordered section cards
13
+ even when the host locale was Chinese. Track 1 closes the visible gap:
14
+ - **app-shell `RecordDetailView`**: the `assignedPage` branch now wears
15
+ the same chrome as the default branch — lifecycle managed-by badge
16
+ and presence avatars in the top-right, `MetadataPanel` debug panel,
17
+ `ActionConfirmDialog` / `ActionParamDialog`, and an auto-appended
18
+ `RecordChatterPanel` at the bottom of the page. Authors opt out of
19
+ the auto-discussion with `assignedPage.disableDiscussion = true`.
20
+ - **plugin-detail `record:details`**: defaults to `inlineEdit: true` so
21
+ fields are click-to-edit just like the default page, and synthesises
22
+ sections with `showBorder: false` by default so a Lightning page
23
+ doesn't double-wrap every block in a heavy Card.
24
+ - **components `page:tabs` / `page:accordion`**: well-known English
25
+ labels (Details / Related / Activity / History / Notes / Files /
26
+ Tasks / Events / Attachments / Chatter / Discussion / Comments /
27
+ Overview / Summary) auto-translate to Chinese (`zh-CN` / `zh-TW`)
28
+ via a built-in dictionary keyed off `document.documentElement.lang`.
29
+ Authors supplying explicit localised labels (string or
30
+ `{ default, zh-CN, ... }`) are not affected.
31
+ - **i18n provider**: applies the initial language to
32
+ `document.documentElement.lang` on mount (i18next does not fire
33
+ `languageChanged` for the bootstrap language), so locale-aware
34
+ renderers downstream see the right value from the first render.
35
+
36
+ - 186aee8: feat(detail): default-on renderViaSchema for non-assignedPage records
37
+
38
+ Track 3 Phase G slice 6. The synthesized Page schema path (slice 2,
39
+ behind `?renderViaSchema=1`) is now the default rendering pipeline for
40
+ every object without a custom assignedPage. Visual and functional
41
+ parity verified on task and account before flipping.
42
+
43
+ Switches preserved: `?renderViaSchema=0` URL fallback,
44
+ `objectDef.detail.renderViaSchema = false` per-object opt-out.
45
+
46
+ - 927187a: Phase N.1 + N.2: visual polish for record detail pages.
47
+
48
+ **N.1 — System actions on full Lightning pages.** `PageHeaderRenderer`
49
+ now merges `headerSystemActions` from `RecordContext` with authored
50
+ actions (authored wins on name/id collision), so full custom pages
51
+ (lead, opportunity, ...) once again show 编辑 / 分享 / 删除 alongside
52
+ their authored actions. `sys_share` and `sys_delete` now use the
53
+ `outline` variant instead of `destructive` to read better in
54
+ multi-button clusters.
55
+
56
+ **N.2 — Hide empty fields by default in synth detail pages.**
57
+ `record:details` defaults `section.hideEmpty` to `true` so synthesized
58
+ pages don't render label graveyards on first load. The "显示 N 个空字段"
59
+ reveal toggle is preserved as the user-facing escape hatch. Authors can
60
+ opt back into showing every field by setting `hideEmpty: false` on the
61
+ section schema.
62
+
63
+ - 8435860: Phase N.4b: highlight↔body dedup now works for hand-authored Lightning
64
+ pages too.
65
+
66
+ Adds a small `HighlightFieldsContext` registry. `record:highlights`
67
+ registers the field names it currently surfaces; `record:details` unions
68
+ that live set into its `hideFieldNames` filter so a field shown in the
69
+ highlight strip is never duplicated in the section grid below.
70
+
71
+ Previously the dedup only fired for synth-generated pages (via the
72
+ `hideFields` prop passed by `buildDefaultPageSchema`). Custom Lightning
73
+ pages (e.g. opportunity) showed `所属客户` both in the strip and in the
74
+ body. The registry-based approach covers both code paths uniformly with
75
+ no schema author work required.
76
+
77
+ The registry uses `useSyncExternalStore` so adding/removing highlights
78
+ notifies consumers without triggering the provider value identity to
79
+ change — avoiding the update-loop that a naive context implementation
80
+ would cause.
81
+
82
+ `RecordDetailView` mounts `<HighlightFieldsProvider>` once per record
83
+ page so the two renderers share state.
84
+
85
+ - 74962b0: feat(detail): record:discussion schema component + flush accordion variant
86
+ - New `record:discussion` schema type lets authors place the record
87
+ chatter feed anywhere in a custom Page schema. Wired through a
88
+ shared `DiscussionContext` provider on the `assignedPage` branch
89
+ of `RecordDetailView`; auto-append still applies when no explicit
90
+ `record:discussion` / `record:chatter` node is present.
91
+ - `page:accordion` gains a `variant` prop. Default `flush` strips the
92
+ per-item border so accordion sections no longer double-wrap inner
93
+ Card-bearing renderers (RelatedList, etc.). Authors who want the
94
+ old visual pass `variant: 'card'`.
95
+ - `translateLabel` now handles compound labels split by `&`, `and`,
96
+ or `和` (e.g. `Notes & Attachments` → `备注与附件`).
97
+
98
+ - fa4c2cb: feat(detail): renderViaSchema opt-in routes default detail through SchemaRenderer (Track 3 Phase G slice 2)
99
+
100
+ When `?renderViaSchema=1` is in the URL, or `objectDef.detail.renderViaSchema === true`,
101
+ `RecordDetailView`'s no-assignedPage branch now synthesizes a canonical
102
+ Page schema (`page:header` → `record:highlights` → `record:path` →
103
+ `page:tabs(record:details)` → `record:discussion`) via
104
+ `buildDefaultPageSchema(objectDef, { sections, highlightFields })` and
105
+ renders it through the existing `<SchemaRenderer>` pipeline.
106
+
107
+ This means every object without a custom assigned page can opt in to
108
+ the same chrome (record-aware header chip, chevron path, flush
109
+ accordion, discussion slot) that custom Lightning pages already enjoy.
110
+
111
+ Changes:
112
+ - `buildDefaultPageSchema` now emits `page:tabs.items` (correct shape
113
+ for the renderer) rather than `tabs`.
114
+ - `PageHeaderRenderer.resolvedTitle` honors `objectSchema.primaryField`
115
+ before the legacy `name/title/display_name/label` fallbacks.
116
+ - `RecordDetailView` rebuilds the synthesized schema with
117
+ `detailSchema.sections` + `highlightFields` at render time so
118
+ `record:details` inherits the same field layout the legacy
119
+ `<DetailView>` would have produced.
120
+
121
+ Flag is intentionally off by default — flipping the default is a
122
+ separate explicit commit after empirical parity validation across
123
+ multiple objects. Known gaps tracked for slice 3: titleFormat
124
+ fallback for objects without `primaryField`, auto Activity / History
125
+ tabs, header-action buttons.
126
+
127
+ - 7213027: feat(detail): slotted record pages (Track 3 Phase I)
128
+
129
+ Introduce `kind: "slotted"` record pages that override one or more
130
+ named slots while letting the default-page synthesizer fill in the
131
+ rest. Authors no longer need to re-author the entire page just to
132
+ customize the header or one tab.
133
+
134
+ **Slot menu (v1):**
135
+ - `header` — replaces `page:header`
136
+ - `actions` — replaces the `record:quick_actions` action bar
137
+ - `highlights` — replaces the chips + chevron path strip
138
+ - `details` — replaces the Details tab body (other tabs stay synthesized)
139
+ - `tabs` — replaces the entire `page:tabs` node (wins over `details`)
140
+ - `discussion` — replaces the inline `record:discussion` footer
141
+
142
+ Each slot is a full replacement at the slot boundary. To compose
143
+ default + custom, call the corresponding `buildDefault*` sub-builder
144
+ (now exported from `@object-ui/plugin-detail`):
145
+ `buildDefaultHeader`, `buildDefaultActions`, `buildDefaultHighlights`,
146
+ `buildDefaultDetails`, `buildDefaultTabs`, `buildDefaultDiscussion`.
147
+
148
+ **Author shape:**
149
+
150
+ ```ts
151
+ {
152
+ type: 'record',
153
+ object: 'account',
154
+ kind: 'slotted',
155
+ slots: {
156
+ header: { type: 'page:header', properties: { ... } },
157
+ },
158
+ }
159
+ ```
160
+
161
+ **API changes:**
162
+ - `PageSchema` (in `@object-ui/types`): adds `kind?: 'full' | 'slotted'`
163
+ (default `'full'`) and `slots?: PageSlotMap`.
164
+ - `usePageAssignment` (in `@object-ui/react`): result now exposes a
165
+ `slots` field populated when the matched page has `kind === 'slotted'`.
166
+ Existing `page` field is unchanged for full pages.
167
+ - `buildDefaultPageSchema` (in `@object-ui/plugin-detail`): accepts an
168
+ `options.slots` map that overrides individual regions at synthesis time.
169
+
170
+ - 34b66bf: feat(detail): synthesize Related / Activity / History tabs + record:quick_actions header (Track 3 Phase G slice 4)
171
+ - `buildDefaultPageSchema` now accepts `headerActions`, `related`,
172
+ `showActivity`, and `history` options. When provided, the synthesizer
173
+ emits a `record:quick_actions` node after `page:header` and appends
174
+ the corresponding tabs to `page:tabs.items` in stable order
175
+ (Details / Related / Activity / History).
176
+ - New `record:history` renderer wraps the existing `HistoryTimeline`,
177
+ reading `entries` / `loading` from the schema. Host owns fetching.
178
+ - `RecordDetailView` forwards `detailSchema.actions[0].actions`,
179
+ `detailSchema.related[]` (unwrapped to `{objectName,relationshipField}`),
180
+ and `detailSchema.history` into the synthesizer call so the
181
+ `renderViaSchema` path reaches parity with the monolithic DetailView
182
+ tab strip and header action bar.
183
+ - 6 new unit tests covering headerActions emit/skip, Related tab
184
+ shape, Activity opt-in, History entries pass-through, and stable
185
+ tab ordering.
186
+
187
+ No behavior change for objects without the `renderViaSchema` opt-in.
188
+
189
+ - c7561a7: **Unify per-user UI state storage onto `sys_user_preference`.**
190
+
191
+ `createObjectStackUserStateAdapter` previously wrote to a bespoke
192
+ `user_app_state` object using `(user_id, kind, payload)` columns. That
193
+ parallel KV table duplicated the canonical per-user preference store
194
+ shipped by `@objectstack/plugin-auth`, and pulled UI traces (favorites,
195
+ recent items, grid widths) out of the place users actually look for
196
+ their settings.
197
+
198
+ The adapter now defaults to:
199
+ - `resource`: `sys_user_preference`
200
+ - field shape: `(user_id, key, value)` instead of `(user_id, kind, payload)`
201
+ - option name: **`key`** instead of `kind`
202
+
203
+ `ConsoleShell` is updated to attach favorites/recent under the namespaced
204
+ keys `ui.favorites` and `ui.recent`. Recommended convention for new
205
+ adapters: keep machine-written UI traces under `ui.*` so they stay
206
+ distinguishable from user-facing preferences (`theme`, `locale`, ...).
207
+
208
+ **Migration**: callers passing `kind:` need to switch to `key:`. Callers
209
+ relying on the old `user_app_state` table can pin
210
+ `resource: 'user_app_state'` to keep the legacy behaviour, but no
211
+ backend ships that schema and the new default works against any
212
+ plugin-auth-enabled environment with zero extra setup.
213
+
214
+ ### Patch Changes
215
+
216
+ - 983d5ad: fix(app-shell): suppress duplicate discussion panel on record detail pages
217
+
218
+ `RecordDetailView` auto-appends a `RecordChatterPanel` below the
219
+ rendered page unless an explicit `record:discussion` / `record:chatter`
220
+ node is found in the schema. The detection walker recursed into
221
+ `children / items / body / components / properties.*` but **not**
222
+ `regions[]`. Synthesised pages (`buildDefaultPageSchema`) and authored
223
+ full-Lightning pages place `record:discussion` inside
224
+ `regions[0].components`, so the walker missed it and a second
225
+ discussion panel rendered on top of the first.
226
+
227
+ Extracted the walker into `utils/pageSchemaIntrospect.ts`, added a
228
+ `regions` branch, and covered both shapes with unit tests.
229
+
230
+ Verified in browser on account (slotted), opportunity (full), lead,
231
+ contact, and task — each renders exactly one discussion panel.
232
+
233
+ - a4c10b2: Restore Edit / Share / Delete system actions on synthesized record detail headers.
234
+
235
+ Phase G slice 6 flipped the synth detail page on by default but did not
236
+ forward the legacy DetailView's built-in system actions to the new
237
+ `record:quick_actions` bar. Objects without authored `record_header`
238
+ business actions ended up with a bare header (only the ★ favorite +
239
+ copy-id chip from `page:header`).
240
+
241
+ This patch injects gated system actions into `synthHeaderActions` for
242
+ both the synth and slotted paths:
243
+ - `sys_edit` — visible when `affordances.edit`. Calls the existing
244
+ `onEdit` prop, opening the same form modal as before.
245
+ - `sys_share` — always visible. Uses `navigator.share` when available;
246
+ falls back to clipboard copy of the current URL with a toast.
247
+ - `sys_delete` — visible when `affordances.delete`. Confirms via
248
+ `window.confirm`, calls `dataSource.delete`, then navigates back to
249
+ the list.
250
+
251
+ Business / custom actions (e.g. Lead.convert, Contact.set_primary)
252
+ continue to render alongside the system actions, unchanged. Full
253
+ Lightning pages (objects with an `assignedPage`) are unaffected — they
254
+ remain author-owned.
255
+
256
+ - Updated dependencies [542cca9]
257
+ - Updated dependencies [8930b15]
258
+ - Updated dependencies [95b6b21]
259
+ - Updated dependencies [ddb08a7]
260
+ - Updated dependencies [f16a762]
261
+ - Updated dependencies [765d50f]
262
+ - Updated dependencies [927187a]
263
+ - Updated dependencies [bae8ba8]
264
+ - Updated dependencies [8435860]
265
+ - Updated dependencies [bece8ca]
266
+ - Updated dependencies [bb2ea48]
267
+ - Updated dependencies [77c1877]
268
+ - Updated dependencies [b14fe09]
269
+ - Updated dependencies [1911d34]
270
+ - Updated dependencies [ba98039]
271
+ - Updated dependencies [a7bef6e]
272
+ - Updated dependencies [86c04f1]
273
+ - Updated dependencies [74962b0]
274
+ - Updated dependencies [8b850b5]
275
+ - Updated dependencies [3154334]
276
+ - Updated dependencies [fa4c2cb]
277
+ - Updated dependencies [7213027]
278
+ - Updated dependencies [34b66bf]
279
+ - Updated dependencies [c7561a7]
280
+ - @object-ui/plugin-detail@5.0.0
281
+ - @object-ui/components@5.0.0
282
+ - @object-ui/i18n@5.0.0
283
+ - @object-ui/layout@5.0.0
284
+ - @object-ui/react@5.0.0
285
+ - @object-ui/types@5.0.0
286
+ - @object-ui/data-objectstack@5.0.0
287
+ - @object-ui/plugin-calendar@5.0.0
288
+ - @object-ui/plugin-kanban@5.0.0
289
+ - @object-ui/fields@5.0.0
290
+ - @object-ui/plugin-charts@5.0.0
291
+ - @object-ui/plugin-chatbot@5.0.0
292
+ - @object-ui/plugin-dashboard@5.0.0
293
+ - @object-ui/plugin-designer@5.0.0
294
+ - @object-ui/plugin-form@5.0.0
295
+ - @object-ui/plugin-grid@5.0.0
296
+ - @object-ui/plugin-list@5.0.0
297
+ - @object-ui/plugin-report@5.0.0
298
+ - @object-ui/plugin-view@5.0.0
299
+ - @object-ui/auth@5.0.0
300
+ - @object-ui/collaboration@5.0.0
301
+ - @object-ui/core@5.0.0
302
+ - @object-ui/permissions@5.0.0
303
+
304
+ ## 4.8.0
305
+
306
+ ### Minor Changes
307
+
308
+ - 3a17c8d: Mobile UI: aggressive chrome reduction to match real mobile-app conventions.
309
+
310
+ Real mobile CRMs (Salesforce, HubSpot, Notion, Linear) keep one row of
311
+ chrome on phones: title + 1 primary action, plus content. We were
312
+ shipping ~5 rows of toolbars + chips + tabs above the data. This commit
313
+ hides the desktop-only chrome at the `<sm` breakpoint:
314
+ - **ListView**: TabBar (view switcher), UserFilters chip row, quick-filters
315
+ chip row, Sort button, list-scoped Search popover, and the
316
+ (newly-added) mobile-only ViewSettingsPopover gear are all hidden on
317
+ phones. Only the **Filter** icon survives on mobile — paired with the
318
+ global ⌘K top-bar search, that is the entire mobile control surface.
319
+ - **Kanban**: previous commit replaced verbose swipe text with a dot
320
+ indicator; that stands.
321
+ - **ObjectView page header**: the Import (CSV upload) button is hidden
322
+ on mobile — CSV import is a desktop workflow.
323
+
324
+ Net effect on a 390px viewport: ListView toolbar collapses from
325
+ ~10 controls (5 chips + 5 icons) to a single Filter icon next to the
326
+ title; the body of the page is reachable without scrolling past 3 rows
327
+ of chrome.
328
+
329
+ Desktop and tablet behavior is unchanged.
330
+
331
+ - 51e274a: feat(app-shell,plugin-list): mobile Airtable-style topbar + filter chip row
332
+
333
+ Refactor mobile object-view layout to match the Airtable Interface
334
+ pattern:
335
+ - **AppHeader**: the mobile topbar's static page label is now a
336
+ view-switcher dropdown (`<viewName> ▾`). Tapping opens a list of
337
+ available views with icons + active-state checkmark. Falls back to
338
+ plain text when only one view exists, or when the current page has
339
+ no view-switching surface (Home, Settings, …).
340
+ - **ObjectView**: drops the standalone mobile `sm:hidden` view-select
341
+ row that previously lived between the desktop tab bar and the
342
+ content area. View switching is now exposed exclusively via the
343
+ topbar dropdown on mobile, eliminating the duplicated `object name`
344
+ vs `view name` rows.
345
+ - **ListView**: un-hides the `UserFilters` chip row on mobile.
346
+ Single-line, horizontally scrollable, matches the Airtable Interface
347
+ filter chip strip.
348
+ - New lightweight `MobileViewSwitcherContext` provides a
349
+ page → header data channel (no zustand dependency added).
350
+
351
+ Net effect on mobile (390×844):
352
+
353
+ ```
354
+ ☰ 客户卡片 ▾ 🔍 🔔 M ← topbar
355
+ 类型 ▾ 行业 ▾ 是否活跃 ▾ 更多 3 ▾ ⛛ ← chip row
356
+ [content cards] ← content
357
+ (+) ← FAB
358
+ [Leads | Accounts | Contacts | …] ← bottom nav
359
+ ```
360
+
361
+ - 7feed12: Mobile UX: Home affordance + chrome reduction
362
+
363
+ Two fixes that match what users actually need on a 390px viewport:
364
+ - **Add Home link to mobile sidebar.** When inside an app, the sidebar
365
+ drawer previously listed only the current app's nav groups, with no
366
+ way back to the home page (the desktop topbar's logo and AppSwitcher
367
+ pill are hidden on phones). Now the mobile sidebar opens with a
368
+ prominent "Home" row (`/home`) at the top, gated to mobile + app
369
+ context so the desktop layout is untouched.
370
+ - **Cut a row of top chrome.** The list/object PageHeader (icon + title
371
+ - create / import / more actions) duplicated the page title already
372
+ shown in the topbar. On mobile it's hidden entirely; the primary
373
+ create action moves to a floating "+" button anchored above the
374
+ bottom nav. Desktop still renders the full PageHeader.
375
+
376
+ - 00363fd: feat(app-shell): remove mobile bottom-tab navigation
377
+
378
+ The mobile bottom-tab strip was rendering the first 5 leaf items of
379
+ the app's navigation tree — exactly the same items that the drawer
380
+ (`☰`) surfaces, just without grouping, favourites, or recents.
381
+
382
+ Per the Notion / Linear mobile convention, we now rely on the drawer
383
+ alone. Bottom-tab strips work when they expose **orthogonal**
384
+ top-level sections (Airtable's Home / Bases / Notifications / Account)
385
+ — but ours was a duplicate of the drawer, so it was pure visual
386
+ weight: ~52px of vertical real estate, redundant taps, and clashes
387
+ with the FAB and chat-bubble stack at the bottom-right corner.
388
+
389
+ Net effect:
390
+ - Drawer remains the single source of in-app navigation.
391
+ - ~52px reclaimed for list/kanban content on every mobile screen.
392
+ - FAB and chat-bubble keep their existing offsets (no overlap;
393
+ bottom-nav was already accounted for above them).
394
+
395
+ - faba0e3: Mobile UX cleanup:
396
+ - `app-shell/AppHeader`: hide the platform-logo, app-switcher pill, and
397
+ intermediate path separators on mobile when inside an app route. The
398
+ sidebar already exposes those affordances; the topbar now reads
399
+ `☰ + page title + Search + Inbox + Avatar`.
400
+ - `plugin-list`: replace the hidden mobile TabBar with a new compact
401
+ `TabBarSelect` dropdown (current view name + chevron → menu of every
402
+ view). Phone users keep view-switching without burning a row on chip
403
+ pills. Desktop continues to render the inline TabBar.
404
+
405
+ ### Patch Changes
406
+
407
+ - @object-ui/types@4.8.0
408
+ - @object-ui/core@4.8.0
409
+ - @object-ui/i18n@4.8.0
410
+ - @object-ui/react@4.8.0
411
+ - @object-ui/components@4.8.0
412
+ - @object-ui/fields@4.8.0
413
+ - @object-ui/layout@4.8.0
414
+ - @object-ui/data-objectstack@4.8.0
415
+ - @object-ui/auth@4.8.0
416
+ - @object-ui/permissions@4.8.0
417
+ - @object-ui/collaboration@4.8.0
418
+
3
419
  ## 4.7.0
4
420
 
5
421
  ### Patch Changes
@@ -80,12 +80,12 @@ function UserStateBridge() {
80
80
  const favorites = createObjectStackUserStateAdapter({
81
81
  dataSource,
82
82
  userId: user.id,
83
- kind: 'favorites',
83
+ key: 'ui.favorites',
84
84
  });
85
85
  const recent = createObjectStackUserStateAdapter({
86
86
  dataSource,
87
87
  userId: user.id,
88
- kind: 'recent',
88
+ key: 'ui.recent',
89
89
  });
90
90
  attach('favorites', favorites);
91
91
  attach('recent', recent);
@@ -18,8 +18,8 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
18
18
  * @module
19
19
  */
20
20
  import { useLocation, useParams, Link, useNavigate } from 'react-router-dom';
21
- import { SidebarTrigger, Button, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuGroup, Avatar, AvatarImage, AvatarFallback, } from '@object-ui/components';
22
- import { Search, HelpCircle, ChevronDown, Settings, LogOut, User as UserIcon, Boxes, } from 'lucide-react';
21
+ import { SidebarTrigger, Button, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuGroup, Avatar, AvatarImage, AvatarFallback, cn, } from '@object-ui/components';
22
+ import { Search, HelpCircle, ChevronDown, Check, Lock, Settings, LogOut, User as UserIcon, Boxes, } from 'lucide-react';
23
23
  import { useState, useEffect, useCallback, useRef } from 'react';
24
24
  import { useOffline } from '@object-ui/react';
25
25
  import { PresenceAvatars } from '@object-ui/collaboration';
@@ -32,6 +32,7 @@ import { useAdapter } from '../providers/AdapterProvider';
32
32
  import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n';
33
33
  import { useAuth, getUserInitials } from '@object-ui/auth';
34
34
  import { useMetadata } from '../providers/MetadataProvider';
35
+ import { useMobileViewSwitcher } from './MobileViewSwitcherContext';
35
36
  import { useNavigationContext } from '../context/NavigationContext';
36
37
  function humanizeSlug(slug) {
37
38
  return slug.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
@@ -56,6 +57,7 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
56
57
  const { objectLabel, dashboardLabel, pageLabel, reportLabel, viewLabel } = useObjectLabel();
57
58
  const { apps: metadataApps, dashboards: metadataDashboards, pages: metadataPages, reports: metadataReports } = useMetadata();
58
59
  const { currentAppName, recordTitle } = useNavigationContext();
60
+ const mobileSwitcher = useMobileViewSwitcher();
59
61
  const [apiPresenceUsers, setApiPresenceUsers] = useState(null);
60
62
  const [apiActivities, setApiActivities] = useState(null);
61
63
  /** M10.8: in-header notifications. Polled from sys_notification scoped to current user. */
@@ -372,8 +374,16 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
372
374
  }
373
375
  }
374
376
  const lastSegmentLabel = extraSegments[extraSegments.length - 1]?.label || appName || '';
375
- return (_jsxs("div", { className: "flex items-center justify-between w-full h-full", children: [_jsxs("div", { className: "flex items-center min-w-0 flex-1", children: [_jsx(Link, { to: "/home", className: "flex items-center justify-center h-7 w-7 shrink-0 rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors", title: "ObjectStack", children: _jsx(Boxes, { className: "h-4 w-4" }) }), resolvedVariant === 'home' && (_jsx("span", { className: "hidden sm:inline ml-2 text-sm font-semibold tracking-tight", children: "ObjectStack" })), resolvedVariant === 'orgs' && (_jsxs(_Fragment, { children: [_jsx(PathSep, {}), _jsx("span", { className: "text-sm font-medium text-foreground/80 px-1.5", children: t('organizations.title', { defaultValue: 'Organizations' }) })] })), isApp && (_jsxs(_Fragment, { children: [_jsx(SidebarTrigger, { className: "md:hidden shrink-0 ml-1", "aria-label": t('common.toggleSidebar') || 'Toggle sidebar' }), activeAppName && onAppChange ? (_jsxs(_Fragment, { children: [_jsx(PathSep, {}), _jsx(AppSwitcher, { activeAppName: activeAppName, onAppChange: onAppChange })] })) : appName ? (_jsxs(_Fragment, { children: [_jsx(PathSep, {}), _jsx("span", { className: "text-sm font-medium text-foreground/80 px-1.5", children: appName })] })) : null, extraSegments.map((seg, i) => {
377
+ return (_jsxs("div", { className: "flex items-center justify-between w-full h-full", children: [_jsxs("div", { className: "flex items-center min-w-0 flex-1", children: [_jsx(Link, { to: "/home", className: cn("flex items-center justify-center h-7 w-7 shrink-0 rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors", isApp && "hidden sm:flex"), title: "ObjectStack", children: _jsx(Boxes, { className: "h-4 w-4" }) }), resolvedVariant === 'home' && (_jsx("span", { className: "hidden sm:inline ml-2 text-sm font-semibold tracking-tight", children: "ObjectStack" })), resolvedVariant === 'orgs' && (_jsxs(_Fragment, { children: [_jsx(PathSep, {}), _jsx("span", { className: "text-sm font-medium text-foreground/80 px-1.5", children: t('organizations.title', { defaultValue: 'Organizations' }) })] })), isApp && (_jsxs(_Fragment, { children: [_jsx(SidebarTrigger, { className: "md:hidden shrink-0 ml-1", "aria-label": t('common.toggleSidebar') || 'Toggle sidebar' }), activeAppName && onAppChange ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "hidden sm:flex items-center", children: _jsx(PathSep, {}) }), _jsx("div", { className: "hidden sm:flex items-center", children: _jsx(AppSwitcher, { activeAppName: activeAppName, onAppChange: onAppChange }) })] })) : appName ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "hidden sm:flex items-center", children: _jsx(PathSep, {}) }), _jsx("span", { className: "hidden sm:inline text-sm font-medium text-foreground/80 px-1.5", children: appName })] })) : null, extraSegments.map((seg, i) => {
376
378
  const isLast = i === extraSegments.length - 1;
377
379
  return (_jsxs("span", { className: "hidden sm:flex items-center min-w-0", children: [_jsx(PathSep, {}), seg.siblings && seg.siblings.length > 1 ? (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { className: `flex items-center gap-1 rounded-md px-1.5 py-1 text-sm font-medium transition-colors outline-none hover:bg-accent hover:text-foreground ${!isLast ? 'text-foreground/60' : 'text-foreground/80'}`, children: [seg.label, _jsx(ChevronDown, { className: "h-3.5 w-3.5 text-muted-foreground" })] }), _jsxs(DropdownMenuContent, { align: "start", sideOffset: 8, className: "w-56 max-h-72 overflow-y-auto", children: [_jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground font-normal", children: "Switch Object" }), _jsx(DropdownMenuSeparator, {}), seg.siblings.map((sibling) => (_jsx(DropdownMenuItem, { asChild: true, children: _jsx(Link, { to: sibling.href, className: "w-full", children: sibling.label }) }, sibling.href)))] })] })) : seg.href ? (_jsx(Link, { to: seg.href, className: `rounded-md px-1.5 py-1 text-sm font-medium transition-colors hover:bg-accent hover:text-foreground truncate max-w-[160px] ${isLast ? 'text-foreground/80' : 'text-foreground/60'}`, children: seg.label })) : (_jsx("span", { className: `px-1.5 py-1 text-sm font-medium truncate max-w-[160px] ${isLast ? 'text-foreground/80' : 'text-foreground/60'}`, children: seg.label }))] }, i));
378
- }), _jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: lastSegmentLabel })] }))] }), _jsxs("div", { className: "flex items-center gap-0.5 sm:gap-1 shrink-0 [&>*+*[data-topbar-group]]:ml-1 [&>[data-topbar-group]+[data-topbar-group]]:border-l [&>[data-topbar-group]+[data-topbar-group]]:border-border/60 [&>[data-topbar-group]+[data-topbar-group]]:pl-1 sm:[&>[data-topbar-group]+[data-topbar-group]]:pl-2 sm:[&>[data-topbar-group]+[data-topbar-group]]:ml-2", children: [!isOnline && (_jsxs("div", { className: "flex items-center gap-1 px-2 py-1 rounded-full bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200 text-xs font-medium", children: [_jsx("span", { className: "h-2 w-2 rounded-full bg-yellow-500 animate-pulse" }), "Offline"] })), isApp && connectionState && _jsx(ConnectionStatus, { state: connectionState }), isApp && activeUsers.length > 0 && (_jsx("div", { className: "hidden md:flex items-center shrink-0", title: "Users currently online", children: _jsx(PresenceAvatars, { users: activeUsers, size: "sm", maxVisible: 3, showStatus: true }) })), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 sm:gap-1 shrink-0", children: [_jsxs("button", { onClick: () => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true })), className: "hidden lg:flex relative items-center gap-2 w-48 xl:w-64 h-8 px-3 text-sm rounded-md border bg-muted/50 text-muted-foreground hover:bg-muted transition-colors", children: [_jsx(Search, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "flex-1 text-left text-xs", children: t('console.search', { defaultValue: 'Search...' }) }), _jsxs("kbd", { className: "pointer-events-none inline-flex h-5 items-center gap-0.5 rounded border bg-background px-1.5 text-[10px] font-medium text-muted-foreground", children: [_jsx("span", { className: "text-xs", children: "\u2318" }), "K"] })] }), _jsx(Button, { variant: "ghost", size: "icon", className: "lg:hidden h-8 w-8 shrink-0", onClick: () => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true })), "aria-label": t('console.search', { defaultValue: 'Search...' }), children: _jsx(Search, { className: "h-4 w-4" }) })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [_jsx(InboxPopover, { notifications: notifications, unreadCount: unreadCount, pendingApprovalsCount: pendingApprovalsCount, activities: activeActivities, onMarkAllRead: markAllRead, onMarkRead: markNotificationRead }), _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 hidden md:flex shrink-0", asChild: true, "aria-label": t('sidebar.helpTooltip', { defaultValue: 'Help & Documentation' }), children: _jsx("a", { href: "https://docs.objectstack.ai", target: "_blank", rel: "noopener noreferrer", children: _jsx(HelpCircle, { className: "h-4 w-4" }) }) })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [" ", _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0 rounded-full", children: _jsxs(Avatar, { className: "h-7 w-7 rounded-full", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-full bg-primary text-primary-foreground text-xs", children: getUserInitials(user) })] }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "min-w-64 rounded-lg", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "p-0 font-normal", children: _jsxs("div", { className: "flex items-center gap-2 px-2 py-2", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] })] }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuGroup, { children: [hasOrgSection && (_jsxs(DropdownMenuItem, { onClick: () => navigate('/organizations'), className: "cursor-pointer", children: [_jsx(Boxes, { className: "mr-2 h-4 w-4" }), t('organizations.mine', { defaultValue: 'My Organizations' })] })), _jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/setup/system/profile'), children: [_jsx(UserIcon, { className: "mr-2 h-4 w-4" }), t('user.profile', { defaultValue: 'Profile' })] }), _jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/setup'), children: [_jsx(Settings, { className: "mr-2 h-4 w-4" }), t('sidebar.settings', { defaultValue: 'Settings' })] })] }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { className: "text-[11px] font-normal text-muted-foreground uppercase tracking-wide px-2", children: t('user.preferences', { defaultValue: 'Preferences' }) }), _jsxs("div", { className: "flex items-center justify-between px-2 py-1.5 text-sm", children: [_jsx("span", { className: "text-foreground/80", children: t('user.theme', { defaultValue: 'Theme' }) }), _jsx(ModeToggle, {})] }), _jsxs("div", { className: "flex items-center justify-between px-2 py-1.5 text-sm", children: [_jsx("span", { className: "text-foreground/80", children: t('user.language', { defaultValue: 'Language' }) }), _jsx(LocaleSwitcher, {})] }), isAuthEnabled && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "text-destructive focus:text-destructive", onClick: () => signOut(), children: [_jsx(LogOut, { className: "mr-2 h-4 w-4" }), t('user.logout', { defaultValue: 'Log out' })] })] }))] })] })] })] })] }));
380
+ }), mobileSwitcher && mobileSwitcher.views.length > 0 ? (mobileSwitcher.views.length > 1 ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: "sm:hidden flex items-center gap-0.5 min-w-0 ml-1 rounded-md px-1.5 py-1 text-sm font-medium hover:bg-accent active:bg-accent/80 transition-colors", "aria-label": "Switch view", children: [_jsx("span", { className: "truncate max-w-[180px]", children: mobileSwitcher.triggerLabel ??
381
+ mobileSwitcher.views.find((v) => v.id === mobileSwitcher.activeViewId)?.label ??
382
+ lastSegmentLabel }), _jsx(ChevronDown, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground" })] }) }), _jsx(DropdownMenuContent, { align: "start", className: "min-w-[220px] max-w-[280px]", children: mobileSwitcher.views.map((v) => {
383
+ const isActive = v.id === mobileSwitcher.activeViewId;
384
+ return (_jsxs(DropdownMenuItem, { onSelect: () => {
385
+ if (!isActive)
386
+ mobileSwitcher.onChange(v.id);
387
+ }, className: "gap-2", children: [v.icon ? (_jsx("span", { className: "shrink-0 text-muted-foreground [&>svg]:h-4 [&>svg]:w-4", children: v.icon })) : null, _jsx("span", { className: "flex-1 truncate", children: v.label }), v.locked ? (_jsx(Lock, { className: "h-3 w-3 shrink-0 text-muted-foreground", "aria-hidden": true })) : null, isActive ? (_jsx(Check, { className: "h-4 w-4 shrink-0 text-foreground", "aria-hidden": true })) : null] }, v.id));
388
+ }) })] })) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: mobileSwitcher.triggerLabel ?? mobileSwitcher.views[0].label }))) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: lastSegmentLabel }))] }))] }), _jsxs("div", { className: "flex items-center gap-0.5 sm:gap-1 shrink-0 [&>*+*[data-topbar-group]]:ml-1 [&>[data-topbar-group]+[data-topbar-group]]:border-l [&>[data-topbar-group]+[data-topbar-group]]:border-border/60 [&>[data-topbar-group]+[data-topbar-group]]:pl-1 sm:[&>[data-topbar-group]+[data-topbar-group]]:pl-2 sm:[&>[data-topbar-group]+[data-topbar-group]]:ml-2", children: [!isOnline && (_jsxs("div", { className: "flex items-center gap-1 px-2 py-1 rounded-full bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200 text-xs font-medium", children: [_jsx("span", { className: "h-2 w-2 rounded-full bg-yellow-500 animate-pulse" }), "Offline"] })), isApp && connectionState && _jsx(ConnectionStatus, { state: connectionState }), isApp && activeUsers.length > 0 && (_jsx("div", { className: "hidden md:flex items-center shrink-0", title: "Users currently online", children: _jsx(PresenceAvatars, { users: activeUsers, size: "sm", maxVisible: 3, showStatus: true }) })), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 sm:gap-1 shrink-0", children: [_jsxs("button", { onClick: () => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true })), className: "hidden lg:flex relative items-center gap-2 w-48 xl:w-64 h-8 px-3 text-sm rounded-md border bg-muted/50 text-muted-foreground hover:bg-muted transition-colors", children: [_jsx(Search, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "flex-1 text-left text-xs", children: t('console.search', { defaultValue: 'Search...' }) }), _jsxs("kbd", { className: "pointer-events-none inline-flex h-5 items-center gap-0.5 rounded border bg-background px-1.5 text-[10px] font-medium text-muted-foreground", children: [_jsx("span", { className: "text-xs", children: "\u2318" }), "K"] })] }), _jsx(Button, { variant: "ghost", size: "icon", className: "lg:hidden h-8 w-8 shrink-0", onClick: () => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true })), "aria-label": t('console.search', { defaultValue: 'Search...' }), children: _jsx(Search, { className: "h-4 w-4" }) })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [_jsx(InboxPopover, { notifications: notifications, unreadCount: unreadCount, pendingApprovalsCount: pendingApprovalsCount, activities: activeActivities, onMarkAllRead: markAllRead, onMarkRead: markNotificationRead }), _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 hidden md:flex shrink-0", asChild: true, "aria-label": t('sidebar.helpTooltip', { defaultValue: 'Help & Documentation' }), children: _jsx("a", { href: "https://docs.objectstack.ai", target: "_blank", rel: "noopener noreferrer", children: _jsx(HelpCircle, { className: "h-4 w-4" }) }) })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [" ", _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0 rounded-full", children: _jsxs(Avatar, { className: "h-7 w-7 rounded-full", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-full bg-primary text-primary-foreground text-xs", children: getUserInitials(user) })] }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "min-w-64 rounded-lg", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "p-0 font-normal", children: _jsxs("div", { className: "flex items-center gap-2 px-2 py-2", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] })] }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuGroup, { children: [hasOrgSection && (_jsxs(DropdownMenuItem, { onClick: () => navigate('/organizations'), className: "cursor-pointer", children: [_jsx(Boxes, { className: "mr-2 h-4 w-4" }), t('organizations.mine', { defaultValue: 'My Organizations' })] })), _jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/setup/system/profile'), children: [_jsx(UserIcon, { className: "mr-2 h-4 w-4" }), t('user.profile', { defaultValue: 'Profile' })] }), _jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/setup'), children: [_jsx(Settings, { className: "mr-2 h-4 w-4" }), t('sidebar.settings', { defaultValue: 'Settings' })] })] }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuLabel, { className: "text-[11px] font-normal text-muted-foreground uppercase tracking-wide px-2", children: t('user.preferences', { defaultValue: 'Preferences' }) }), _jsxs("div", { className: "flex items-center justify-between px-2 py-1.5 text-sm", children: [_jsx("span", { className: "text-foreground/80", children: t('user.theme', { defaultValue: 'Theme' }) }), _jsx(ModeToggle, {})] }), _jsxs("div", { className: "flex items-center justify-between px-2 py-1.5 text-sm", children: [_jsx("span", { className: "text-foreground/80", children: t('user.language', { defaultValue: 'Language' }) }), _jsx(LocaleSwitcher, {})] }), isAuthEnabled && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "text-destructive focus:text-destructive", onClick: () => signOut(), children: [_jsx(LogOut, { className: "mr-2 h-4 w-4" }), t('user.logout', { defaultValue: 'Log out' })] })] }))] })] })] })] })] }));
379
389
  }
@@ -193,6 +193,7 @@ export function AppSidebar({ activeAppName, onAppChange }) {
193
193
  { id: 'sys-users', label: 'Users', type: 'url', url: '/apps/setup/system/users', icon: 'users' },
194
194
  { id: 'sys-orgs', label: 'Organizations', type: 'url', url: '/apps/setup/system/organizations', icon: 'building-2' },
195
195
  { id: 'sys-roles', label: 'Roles', type: 'url', url: '/apps/setup/system/roles', icon: 'shield' },
196
+ { id: 'sys-config', label: 'Configuration', type: 'url', url: '/apps/setup/system/settings', icon: 'sliders-horizontal' },
196
197
  { id: 'sys-create-app', label: 'Create App', type: 'url', url: '/create-app', icon: 'plus' },
197
198
  ], []);
198
199
  return (_jsxs(_Fragment, { children: [_jsxs(Sidebar, { collapsible: "icon", children: [_jsx(SidebarHeader, { children: _jsx(SidebarMenu, { children: _jsx(SidebarMenuItem, { children: activeApp ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(SidebarMenuButton, { size: "lg", className: "data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", children: [_jsx("div", { className: "flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground", style: primaryColor ? { backgroundColor: primaryColor } : undefined, children: logo ? (_jsx("img", { src: logo, alt: resolveI18nLabel(activeApp.label, t), className: "size-6 object-contain" })) : (React.createElement(getIcon(activeApp.icon), { className: "size-4" })) }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: resolveI18nLabel(activeApp.label, t) }), _jsx("span", { className: "truncate text-xs", children: resolveI18nLabel(activeApp.description, t) || `${activeApps.length} Apps Available` })] }), _jsx(ChevronsUpDown, { className: "ml-auto" })] }) }), _jsxs(DropdownMenuContent, { className: "w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg", align: "start", side: isMobile ? "bottom" : "right", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "text-xs text-muted-foreground", children: "Switch Application" }), activeApps.map((app) => (_jsxs(DropdownMenuItem, { onClick: () => onAppChange(app.name), className: "gap-2 p-2", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-sm border", children: app.icon ? React.createElement(getIcon(app.icon), { className: "size-3" }) : _jsx(Database, { className: "size-3" }) }), resolveI18nLabel(app.label, t), activeApp.name === app.name && _jsx("span", { className: "ml-auto text-xs", children: "\u2713" })] }, app.name))), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate('/home'), "data-testid": "home-link-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Home, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: t('layout.appSwitcher.home') })] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate(`/apps/${activeAppName}/create-app`), "data-testid": "add-app-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Plus, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: t('layout.appSwitcher.addApp') })] }), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate(`/apps/${activeAppName}/edit-app/${activeAppName}`), "data-testid": "edit-app-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Pencil, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: t('layout.appSwitcher.editApp') })] }), _jsxs(DropdownMenuItem, { className: "gap-2 p-2", onClick: () => navigate('/apps/setup/system/apps'), "data-testid": "manage-all-apps-btn", children: [_jsx("div", { className: "flex size-6 items-center justify-center rounded-md border bg-background", children: _jsx(Settings, { className: "size-4" }) }), _jsx("div", { className: "font-medium text-muted-foreground", children: t('layout.appSwitcher.manageAllApps') })] })] })] })) : (_jsxs(SidebarMenuButton, { size: "lg", onClick: () => navigate('/apps/setup'), "data-testid": "system-sidebar-header", children: [_jsx("div", { className: "flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground", children: _jsx(Settings, { className: "size-4" }) }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: t('layout.appSwitcher.systemConsole') }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: t('layout.appSwitcher.noAppsConfigured') })] })] })) }) }) }), _jsx(SidebarContent, { children: activeApp ? (_jsxs(_Fragment, { children: [areas.length > 1 && (_jsxs(SidebarGroup, { children: [_jsxs(SidebarGroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(Layers, { className: "h-3.5 w-3.5" }), "Area"] }), _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: areas.map((area) => {
@@ -16,6 +16,7 @@ import { useDiscovery } from '@object-ui/react';
16
16
  const ConsoleFloatingChatbot = lazy(() => import('./ConsoleFloatingChatbot'));
17
17
  import { UnifiedSidebar } from './UnifiedSidebar';
18
18
  import { AppHeader } from './AppHeader';
19
+ import { MobileViewSwitcherProvider } from './MobileViewSwitcherContext';
19
20
  import { useResponsiveSidebar } from '../hooks/useResponsiveSidebar';
20
21
  import { useNavigationContext } from '../context/NavigationContext';
21
22
  import { resolveI18nLabel } from '../utils';
@@ -39,15 +40,15 @@ export function ConsoleLayout({ children, activeAppName, activeApp, onAppChange,
39
40
  setContext('app');
40
41
  setCurrentAppName(activeAppName);
41
42
  }, [setContext, setCurrentAppName, activeAppName]);
42
- return (_jsxs(AppShell, { sidebar: _jsx(UnifiedSidebar, { activeAppName: activeAppName, onAppChange: onAppChange }), navbar: _jsx(AppHeader, { variant: "app", appName: appLabel, objects: objects, connectionState: connectionState, activeAppName: activeAppName, onAppChange: onAppChange }), className: "!p-0 overflow-y-auto overflow-x-hidden bg-muted/5", branding: activeApp?.branding
43
- ? {
44
- primaryColor: activeApp.branding.primaryColor,
45
- accentColor: activeApp.branding.accentColor,
46
- favicon: activeApp.branding.favicon,
47
- logo: activeApp.branding.logo,
48
- title: activeApp.label
49
- ? `${resolveI18nLabel(activeApp.label)} — ObjectStack Console`
50
- : undefined,
51
- }
52
- : undefined, children: [_jsx(ConsoleLayoutInner, { children: children }), showChatbot && (_jsx(Suspense, { fallback: null, children: _jsx(ConsoleFloatingChatbot, { appLabel: appLabel, objects: objects }) }))] }));
43
+ return (_jsx(MobileViewSwitcherProvider, { children: _jsxs(AppShell, { sidebar: _jsx(UnifiedSidebar, { activeAppName: activeAppName, onAppChange: onAppChange }), navbar: _jsx(AppHeader, { variant: "app", appName: appLabel, objects: objects, connectionState: connectionState, activeAppName: activeAppName, onAppChange: onAppChange }), className: "!p-0 overflow-y-auto overflow-x-hidden bg-muted/5", branding: activeApp?.branding
44
+ ? {
45
+ primaryColor: activeApp.branding.primaryColor,
46
+ accentColor: activeApp.branding.accentColor,
47
+ favicon: activeApp.branding.favicon,
48
+ logo: activeApp.branding.logo,
49
+ title: activeApp.label
50
+ ? `${resolveI18nLabel(activeApp.label)} — ObjectStack Console`
51
+ : undefined,
52
+ }
53
+ : undefined, children: [_jsx(ConsoleLayoutInner, { children: children }), showChatbot && (_jsx(Suspense, { fallback: null, children: _jsx(ConsoleFloatingChatbot, { appLabel: appLabel, objects: objects }) }))] }) }));
53
54
  }