@object-ui/app-shell 5.0.2 → 5.2.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.
- package/CHANGELOG.md +401 -0
- package/dist/chrome/CommandPalette.d.ts +6 -1
- package/dist/chrome/CommandPalette.js +40 -4
- package/dist/chrome/ConsoleToaster.js +8 -1
- package/dist/chrome/RouteFader.d.ts +32 -0
- package/dist/chrome/RouteFader.js +52 -0
- package/dist/chrome/index.d.ts +2 -0
- package/dist/chrome/index.js +2 -0
- package/dist/chrome/toast-helpers.d.ts +48 -0
- package/dist/chrome/toast-helpers.js +59 -0
- package/dist/console/AppContent.js +108 -2
- package/dist/console/home/HomePage.js +5 -4
- package/dist/console/marketplace/MarketplaceInstalledPage.d.ts +17 -0
- package/dist/console/marketplace/MarketplaceInstalledPage.js +64 -0
- package/dist/console/marketplace/MarketplacePackagePage.d.ts +7 -0
- package/dist/console/marketplace/MarketplacePackagePage.js +199 -0
- package/dist/console/marketplace/MarketplacePage.d.ts +8 -0
- package/dist/console/marketplace/MarketplacePage.js +94 -0
- package/dist/console/marketplace/PackageIcon.d.ts +19 -0
- package/dist/console/marketplace/PackageIcon.js +17 -0
- package/dist/console/marketplace/marketplaceApi.d.ts +122 -0
- package/dist/console/marketplace/marketplaceApi.js +207 -0
- package/dist/index.d.ts +5 -6
- package/dist/index.js +4 -5
- package/dist/layout/AppHeader.js +25 -21
- package/dist/layout/AppSidebar.js +15 -11
- package/dist/layout/InboxPopover.js +43 -3
- package/dist/types.d.ts +0 -46
- package/dist/views/ObjectView.js +20 -7
- package/dist/views/RecordDetailView.js +213 -45
- package/package.json +25 -25
- package/src/styles.css +49 -0
- package/dist/components/DashboardRenderer.d.ts +0 -8
- package/dist/components/DashboardRenderer.js +0 -16
- package/dist/components/FormRenderer.d.ts +0 -8
- package/dist/components/FormRenderer.js +0 -31
- package/dist/components/ObjectRenderer.d.ts +0 -8
- package/dist/components/ObjectRenderer.js +0 -74
- package/dist/components/PageRenderer.d.ts +0 -7
- package/dist/components/PageRenderer.js +0 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,406 @@
|
|
|
1
1
|
# @object-ui/app-shell — Changelog
|
|
2
2
|
|
|
3
|
+
## 5.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 9ccda28: security: force DOMPurify to `^3.4.5` via pnpm override
|
|
8
|
+
|
|
9
|
+
Resolves 8 moderate-severity GHSA advisories against the transitive
|
|
10
|
+
`dompurify@3.2.7` pulled in by `monaco-editor`. Vulnerabilities covered:
|
|
11
|
+
- SAFE_FOR_TEMPLATES bypass in RETURN_DOM mode
|
|
12
|
+
- FORBID_TAGS bypassed by function-based ADD_TAGS predicate
|
|
13
|
+
- Prototype Pollution to XSS via CUSTOM_ELEMENT_HANDLING fallback
|
|
14
|
+
- ADD_TAGS function-form short-circuit bypass of FORBID_TAGS
|
|
15
|
+
- ADD_ATTR predicate skipping URI validation
|
|
16
|
+
- USE_PROFILES prototype pollution enabling event handlers
|
|
17
|
+
- mutation-XSS via Re-Contextualization
|
|
18
|
+
- Generic XSS vector
|
|
19
|
+
|
|
20
|
+
No API changes; override is transparent to consumers.
|
|
21
|
+
- @object-ui/types@5.2.1
|
|
22
|
+
- @object-ui/core@5.2.1
|
|
23
|
+
- @object-ui/i18n@5.2.1
|
|
24
|
+
- @object-ui/react@5.2.1
|
|
25
|
+
- @object-ui/components@5.2.1
|
|
26
|
+
- @object-ui/fields@5.2.1
|
|
27
|
+
- @object-ui/layout@5.2.1
|
|
28
|
+
- @object-ui/data-objectstack@5.2.1
|
|
29
|
+
- @object-ui/auth@5.2.1
|
|
30
|
+
- @object-ui/permissions@5.2.1
|
|
31
|
+
- @object-ui/collaboration@5.2.1
|
|
32
|
+
- @object-ui/providers@5.2.1
|
|
33
|
+
|
|
34
|
+
## 5.2.0
|
|
35
|
+
|
|
36
|
+
### Minor Changes
|
|
37
|
+
|
|
38
|
+
- 321294c: Cmd-K now shows recently viewed records in its empty state, sourced
|
|
39
|
+
from the existing cloud-synced `sys_user_preference` adapter (already
|
|
40
|
+
wired by `RecentItemsProvider` + `useTrackRouteAsRecent` +
|
|
41
|
+
`RecordDetailView`). Multi-device by construction: open a record on
|
|
42
|
+
laptop, see it in `⌘K → Recently viewed` on phone.
|
|
43
|
+
- Group renders only when input is empty (no competition with search).
|
|
44
|
+
- Limited to the 5 most recent record-type entries.
|
|
45
|
+
- New i18n key `console.commandPalette.recentRecords` (en + zh seeded;
|
|
46
|
+
other locales fall back to `defaultValue: "Recently viewed"`).
|
|
47
|
+
|
|
48
|
+
- b2d1704: feat(cmdk): record search across objects in the Command Palette
|
|
49
|
+
- New `useRecordSearch` hook in `@object-ui/react` debounces a query, fans out
|
|
50
|
+
to `dataSource.find(name, { $search, $top })` across candidate objects, and
|
|
51
|
+
aggregates hits. Race-safe via a monotonic runId; per-object 404s are
|
|
52
|
+
silently dropped via `Promise.allSettled`.
|
|
53
|
+
- `CommandPalette` (`@object-ui/app-shell`) now accepts a `dataSource` prop;
|
|
54
|
+
when supplied, the palette renders a `Records` group at the top with hits
|
|
55
|
+
scoped to the active app's nav objects. Item `value` embeds the live query
|
|
56
|
+
so cmdk's client-side filter doesn't hide async results.
|
|
57
|
+
- Added `console.commandPalette.records` i18n key (`Records` / `记录`).
|
|
58
|
+
|
|
59
|
+
- 921bd28: Console now honors `App.homePageId` for the bare `/console/apps/:appName`
|
|
60
|
+
landing route. Previously it always redirected to the first reachable nav
|
|
61
|
+
item, so CRM-style apps with KPI dashboards still landed users on the
|
|
62
|
+
first object list (e.g. Leads) rather than the configured home page.
|
|
63
|
+
|
|
64
|
+
The new `resolveLandingRoute` looks up the `homePageId` nav item, builds
|
|
65
|
+
its route (object / view / page / dashboard / report), and falls back to
|
|
66
|
+
the existing `findFirstRoute` only when no `homePageId` is set or it
|
|
67
|
+
resolves to a routeless item type.
|
|
68
|
+
|
|
69
|
+
- 3ebba63: Fix silent blank page on shorthand record deep-links.
|
|
70
|
+
|
|
71
|
+
Three related fixes that all addressed the same UX: a user follows a URL
|
|
72
|
+
shaped `/{object}/{recordId}` and sees a completely blank content area.
|
|
73
|
+
1. **`useNavigationOverlay` produced the broken URL itself.** When
|
|
74
|
+
middle-click / Cmd-click opened a gallery card in a new tab and no
|
|
75
|
+
`onNavigate` was provided, the hook built `/{object}/{id}` — a URL
|
|
76
|
+
shape that does not match any route in the console route table. The
|
|
77
|
+
builder now emits the canonical `/{object}/record/{id}`.
|
|
78
|
+
2. **Shorthand redirect for externally shared links.** Even with the
|
|
79
|
+
producer fixed, links pasted from email / Slack / older builds
|
|
80
|
+
still use the shorthand. The console now intercepts
|
|
81
|
+
`/{:objectName}/:maybeRecordId` and, when the second segment looks
|
|
82
|
+
like a record id (URL-safe slug ≥ 6 chars, not a reserved keyword),
|
|
83
|
+
redirects to `/{objectName}/record/{recordId}` preserving query and
|
|
84
|
+
hash.
|
|
85
|
+
3. **Visible 404 fallback.** Routes that match nothing at all now
|
|
86
|
+
render an explicit "Page not found" empty state with a "Go back"
|
|
87
|
+
action instead of leaving the content area blank. Silent failures
|
|
88
|
+
are now visible failures.
|
|
89
|
+
|
|
90
|
+
- a4a0e1d: Add `<PresenceProvider>` abstraction with `useTenantPresence()` and
|
|
91
|
+
`useRecordPresence(objectName, recordId)` hooks. The default source is a
|
|
92
|
+
no-op so hooks return `[]` until a host app wires in a realtime
|
|
93
|
+
transport (WebSocket / SSE). Replaces the two architectural TODOs in
|
|
94
|
+
`AppHeader` (tenant scope) and `RecordDetailView` (record scope) that
|
|
95
|
+
were waiting on this abstraction.
|
|
96
|
+
|
|
97
|
+
`AppHeader` now falls back to `useTenantPresence()` when the
|
|
98
|
+
`presenceUsers` prop is omitted, and `RecordDetailView` renders
|
|
99
|
+
`<PresenceAvatars>` next to the lifecycle badge when other users are
|
|
100
|
+
viewing the same record. Both code paths render exactly as before when
|
|
101
|
+
no provider is mounted, so this change is non-visual for existing
|
|
102
|
+
consumers.
|
|
103
|
+
|
|
104
|
+
### Patch Changes
|
|
105
|
+
|
|
106
|
+
- 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.
|
|
107
|
+
|
|
108
|
+
AppHeader's `markAllRead` now prefers `bulkUpdate`, with a transparent fallback to the per-id loop for adapters that don't implement the helper.
|
|
109
|
+
|
|
110
|
+
- 0a644f0: feat(app-shell): CommandPalette searching indicator
|
|
111
|
+
|
|
112
|
+
When `useRecordSearch` is mid-flight (debounced fetch across objects
|
|
113
|
+
hasn't returned yet), the palette now surfaces a subtle visual:
|
|
114
|
+
- A small pulsing primary-coloured dot next to the **Records** group
|
|
115
|
+
heading, so the user sees that more results may still appear.
|
|
116
|
+
- A `Searching…` placeholder inside the empty state when the user has
|
|
117
|
+
typed something but no hits exist yet — replaces the static
|
|
118
|
+
"No results found." message until the request settles.
|
|
119
|
+
|
|
120
|
+
New i18n key `console.commandPalette.searching` (en + zh).
|
|
121
|
+
|
|
122
|
+
- 5f71924: feat(app-shell): better default toast UX in ConsoleToaster
|
|
123
|
+
|
|
124
|
+
`ConsoleToaster` now ships UX-positive defaults that match the Linear
|
|
125
|
+
/ Notion pattern users expect from an enterprise console:
|
|
126
|
+
- `position="top-right"` — keeps the user's primary work area (centre
|
|
127
|
+
- bottom) unobstructed.
|
|
128
|
+
- `closeButton` — every toast has an explicit X so users can dismiss
|
|
129
|
+
rather than wait the duration out.
|
|
130
|
+
- `richColors` — type-aware coloured backgrounds (success / error /
|
|
131
|
+
warning / info) so the kind of message is legible at a glance.
|
|
132
|
+
- `expand` — toast stack expands on hover so users can read multiple
|
|
133
|
+
recent toasts without dismissing.
|
|
134
|
+
- `visibleToasts={4}` — prevents the corner from being overrun.
|
|
135
|
+
- `duration: 4000` — long enough to read + click an `Undo` action.
|
|
136
|
+
|
|
137
|
+
All of these are still overridable via `<ConsoleToaster …>` props.
|
|
138
|
+
|
|
139
|
+
- 5425608: CRM UX polish pass — calmer enterprise look across detail + kanban.
|
|
140
|
+
- **plugin-kanban**: column headers now use a 2px muted accent stripe with
|
|
141
|
+
neutral foreground titles + a quiet grey count pill instead of full
|
|
142
|
+
rainbow gradient + colored title + colored count. Pipeline boards
|
|
143
|
+
(Opportunity, Case, Task, Lead) look like Salesforce/Linear instead of
|
|
144
|
+
a toy. WIP-limit overflow remains destructive-red so urgency stays loud.
|
|
145
|
+
- **plugin-detail (`record:reference_rail`)**: new `hideEmpty` prop
|
|
146
|
+
(default true) collapses entries whose total === 0 into a single
|
|
147
|
+
`+ N empty (Quotes · Products …)` chip at the bottom of the rail.
|
|
148
|
+
Removes the 4–7 "No records" stack that dominated the aside.
|
|
149
|
+
- **plugin-detail (`record:path`)**: completed stages now render with an
|
|
150
|
+
emerald-tinted background + bold green check instead of low-contrast
|
|
151
|
+
`bg-muted text-muted-foreground` (which read as "light grey on white"
|
|
152
|
+
and was borderline unreadable).
|
|
153
|
+
- **app-shell (`RecordDetailView`)**: record-not-found short-circuit.
|
|
154
|
+
Previously a stale/missing recordId still rendered the page chrome
|
|
155
|
+
(rail, discussion, breadcrumb with the raw id), making invalid links
|
|
156
|
+
look like a partially broken page. Now renders a clean centered
|
|
157
|
+
`Empty` state with database icon + i18n'd "Record not found" copy.
|
|
158
|
+
- **i18n**: added `detail.showEmptyRelated_{one,other}` and
|
|
159
|
+
`empty.recordNotFound{,Description}` keys (en + zh).
|
|
160
|
+
|
|
161
|
+
- 710fbe6: feat(app-shell): notification center animation polish
|
|
162
|
+
|
|
163
|
+
InboxPopover now animates every signal that matters for "noticing":
|
|
164
|
+
- Bell button **bounces once** when total pressure increases (new
|
|
165
|
+
notification or approval arrives). Tracks previous total via a ref
|
|
166
|
+
so the very first render — when the server-side counts hydrate —
|
|
167
|
+
does not trigger a spurious bounce.
|
|
168
|
+
- Bell badge **zooms in** on every count change (re-keyed on
|
|
169
|
+
`totalBadge` so each transition is an independent animation).
|
|
170
|
+
- Per-tab counter badges (Notifications / Approvals) get the same
|
|
171
|
+
zoom-in treatment on count change.
|
|
172
|
+
- Notification list rows **fade + slide in from top** with a small
|
|
173
|
+
staggered delay (capped at 6×20ms so a full list never feels
|
|
174
|
+
laggy).
|
|
175
|
+
- Activity rows mirror the same fade/slide pattern.
|
|
176
|
+
- Empty states (`You're all caught up`, `No recent activity`, `No
|
|
177
|
+
pending approvals`) fade in instead of popping in.
|
|
178
|
+
- The unread dot (•) is now always rendered but fades its opacity
|
|
179
|
+
when `is_read` flips, instead of disappearing instantly — gives a
|
|
180
|
+
smooth "marked read" affordance.
|
|
181
|
+
|
|
182
|
+
All animations are wrapped in `motion-safe:` utility variants so
|
|
183
|
+
users with `prefers-reduced-motion` see the previous (instant) UI.
|
|
184
|
+
No new dependencies; reuses `tailwindcss-animate` utilities already
|
|
185
|
+
present in the design system.
|
|
186
|
+
|
|
187
|
+
- 7c441f5: End-to-end @-mention notifications.
|
|
188
|
+
|
|
189
|
+
`@object-ui/plugin-detail` now exports `extractMentions(text, suggestions)`
|
|
190
|
+
— a small utility that resolves `@<label>` tokens in a comment body to
|
|
191
|
+
user ids, using the same suggestion list that drives the in-editor
|
|
192
|
+
dropdown. Handles labels with spaces ("@QA Test"), CJK ("@王小明"),
|
|
193
|
+
longest-match disambiguation ("Anna Lee" wins over "Anna"), and ignores
|
|
194
|
+
unknown @-tokens. 9 unit tests.
|
|
195
|
+
|
|
196
|
+
`@object-ui/app-shell` `RecordDetailView` now:
|
|
197
|
+
1. Serializes the resolved mention ids into `sys_comment.mentions`
|
|
198
|
+
(previously hard-coded `'[]'`, so servers had no idea who was being
|
|
199
|
+
pinged).
|
|
200
|
+
2. Fan-outs a `sys_notification` row per mentioned recipient
|
|
201
|
+
(self-mentions are filtered as noise) with the canonical bell-inbox
|
|
202
|
+
shape: `type: 'mention'`, `recipient_id`, `actor_name`, `title`,
|
|
203
|
+
`body` preview (≤140 chars), `source_object`/`source_id`/
|
|
204
|
+
`source_comment_id`, `is_read: false`, `created_at`.
|
|
205
|
+
|
|
206
|
+
The notification write tolerates 404 silently, so deployments without
|
|
207
|
+
a notification collection degrade to the previous behavior (mention
|
|
208
|
+
text + highlight, no inbox row). Spec-compliant servers that emit
|
|
209
|
+
notifications via their own sys_comment after-create hook can ignore
|
|
210
|
+
the client-side write — the bell de-dupes by id at the polling layer.
|
|
211
|
+
|
|
212
|
+
- 072cad0: Always seed @-mention suggestions with the current user so the dropdown
|
|
213
|
+
appears even when the backend has no `sys_user` directory (or the fetch
|
|
214
|
+
fails). Hosts with a real user roster still get the merged list —
|
|
215
|
+
current user first, then directory entries de-duped by id.
|
|
216
|
+
|
|
217
|
+
Previously, typing `@` in the discussion comment box was a no-op on
|
|
218
|
+
example backends that don't serve `sys_user`, making the feature look
|
|
219
|
+
broken. Authors can now at minimum mention themselves; richer rosters
|
|
220
|
+
are merged in automatically when available.
|
|
221
|
+
|
|
222
|
+
- 54e3dfb: Remove unused stub renderers from `@object-ui/app-shell`:
|
|
223
|
+
- `ObjectRenderer` / `ObjectRendererProps`
|
|
224
|
+
- `DashboardRenderer` / `DashboardRendererProps`
|
|
225
|
+
- `PageRenderer` / `PageRendererProps`
|
|
226
|
+
- `FormRenderer` / `FormRendererProps`
|
|
227
|
+
|
|
228
|
+
These were placeholder components that never delegated to a real
|
|
229
|
+
SchemaRenderer — they rendered a literal `"TODO"` string and were not
|
|
230
|
+
consumed anywhere in the monorepo or in the official Console app.
|
|
231
|
+
Because they were non-functional, no working production code could
|
|
232
|
+
have depended on them; this is treated as a patch-level cleanup rather
|
|
233
|
+
than a semver-major break.
|
|
234
|
+
|
|
235
|
+
If you were importing one of the removed stubs (and somehow got past
|
|
236
|
+
the "TODO" placeholder render), the real renderers ship from the
|
|
237
|
+
respective plugin packages:
|
|
238
|
+
- Dashboard → `@object-ui/plugin-dashboard` (`DashboardRenderer`)
|
|
239
|
+
- Page / Object / Form → `@object-ui/react` (`SchemaRenderer`) +
|
|
240
|
+
`@object-ui/plugin-form` / `@object-ui/plugin-grid` etc.
|
|
241
|
+
|
|
242
|
+
- Updated dependencies [de0c5e6]
|
|
243
|
+
- Updated dependencies [9997cae]
|
|
244
|
+
- Updated dependencies [321294c]
|
|
245
|
+
- Updated dependencies [b2d1704]
|
|
246
|
+
- Updated dependencies [0a644f0]
|
|
247
|
+
- Updated dependencies [a3cb88f]
|
|
248
|
+
- Updated dependencies [5425608]
|
|
249
|
+
- Updated dependencies [6c3f018]
|
|
250
|
+
- Updated dependencies [d912a60]
|
|
251
|
+
- Updated dependencies [87bc8ff]
|
|
252
|
+
- Updated dependencies [3ebba63]
|
|
253
|
+
- Updated dependencies [e919433]
|
|
254
|
+
- Updated dependencies [a8d12ec]
|
|
255
|
+
- Updated dependencies [a4a0e1d]
|
|
256
|
+
- Updated dependencies [70b5570]
|
|
257
|
+
- Updated dependencies [aa063db]
|
|
258
|
+
- Updated dependencies [d9c3bae]
|
|
259
|
+
- Updated dependencies [d1442e3]
|
|
260
|
+
- Updated dependencies [7c7400a]
|
|
261
|
+
- Updated dependencies [b703480]
|
|
262
|
+
- Updated dependencies [e7b6eae]
|
|
263
|
+
- @object-ui/types@5.2.0
|
|
264
|
+
- @object-ui/data-objectstack@5.2.0
|
|
265
|
+
- @object-ui/core@5.2.0
|
|
266
|
+
- @object-ui/i18n@5.2.0
|
|
267
|
+
- @object-ui/react@5.2.0
|
|
268
|
+
- @object-ui/fields@5.2.0
|
|
269
|
+
- @object-ui/components@5.2.0
|
|
270
|
+
- @object-ui/collaboration@5.2.0
|
|
271
|
+
- @object-ui/layout@5.2.0
|
|
272
|
+
- @object-ui/auth@5.2.0
|
|
273
|
+
- @object-ui/permissions@5.2.0
|
|
274
|
+
- @object-ui/providers@5.2.0
|
|
275
|
+
|
|
276
|
+
## 5.1.1
|
|
277
|
+
|
|
278
|
+
### Patch Changes
|
|
279
|
+
|
|
280
|
+
- Updated dependencies [8955b9c]
|
|
281
|
+
- @object-ui/components@5.1.1
|
|
282
|
+
- @object-ui/fields@5.1.1
|
|
283
|
+
- @object-ui/layout@5.1.1
|
|
284
|
+
- @object-ui/types@5.1.1
|
|
285
|
+
- @object-ui/core@5.1.1
|
|
286
|
+
- @object-ui/i18n@5.1.1
|
|
287
|
+
- @object-ui/react@5.1.1
|
|
288
|
+
- @object-ui/data-objectstack@5.1.1
|
|
289
|
+
- @object-ui/auth@5.1.1
|
|
290
|
+
- @object-ui/permissions@5.1.1
|
|
291
|
+
- @object-ui/collaboration@5.1.1
|
|
292
|
+
- @object-ui/providers@5.1.1
|
|
293
|
+
|
|
294
|
+
## 5.1.0
|
|
295
|
+
|
|
296
|
+
### Minor Changes
|
|
297
|
+
|
|
298
|
+
- d1ec6a2: Fold inline-edit into the page-header overflow menu (HubSpot/Lightning
|
|
299
|
+
pattern) and remove the orphan "Edit fields" toolbar row that previously
|
|
300
|
+
floated between the tab strip and the first detail section.
|
|
301
|
+
- `@object-ui/app-shell` `RecordDetailView`: injects a new `sys_inline_edit`
|
|
302
|
+
system action that appears in the ⋯ overflow menu and dispatches a
|
|
303
|
+
`objectui:record:inline-edit-toggle` window CustomEvent (filtered by
|
|
304
|
+
recordId + objectName).
|
|
305
|
+
- `@object-ui/plugin-detail` `DetailView`: listens for that event to
|
|
306
|
+
toggle inline-edit mode; the in-page toolbar now renders only during
|
|
307
|
+
active editing / save error / locked states, so the idle layout flows
|
|
308
|
+
tabs → first section card with no orphan row.
|
|
309
|
+
- `@object-ui/components` layout containers: extended `KNOWN_LABEL_DICT`
|
|
310
|
+
with zh-CN + zh-TW translations for common CRM related-list labels
|
|
311
|
+
(Quotes / Products / Contacts / Accounts / Leads / Opportunities /
|
|
312
|
+
Cases / Campaigns / Approvals / Documents / Emails / Calls / Meetings
|
|
313
|
+
/ Open Tasks / Closed Tasks), so authored English labels auto-translate
|
|
314
|
+
in `page:accordion` / `page:tabs` items.
|
|
315
|
+
|
|
316
|
+
- cf30cc2: Polish Lightning record detail page layout.
|
|
317
|
+
- `record:details` sections now render with Card chrome by default when a `title` is present, restoring visual grouping that was missing on pages like the opportunity detail page.
|
|
318
|
+
- Section labels can be translated via the `{ns}.objects.{objectName}._sections.{name}.label` convention. Author each section with a stable `name` (e.g. `info`, `forecast`) and the renderer picks up the locale-specific label automatically. Falls back to the literal `label` when no translation exists.
|
|
319
|
+
- The `page:header` action toolbar now collapses into a `⋯` overflow menu when more than two actions are present. The first business action stays inline; secondary system actions (Edit / Share / Delete) move into the menu, with destructive styling applied to Delete.
|
|
320
|
+
- Header action labels resolve via the `{ns}.objects.{objectName}._actions.{name}.label` convention.
|
|
321
|
+
- Removed the meaningless field-count Badge from collapsible section headers (the `2` chip next to "Description"). Field-count metadata wasn't useful in the header and added visual noise.
|
|
322
|
+
- Synth-path `sys_delete` now carries `variant: 'destructive'` so the overflow menu can color it appropriately.
|
|
323
|
+
|
|
324
|
+
- c0b236f: Platform detail/form polish:
|
|
325
|
+
- **Auto-section grouping**: When an object has no authored `views.form.sections`, the detail page now splits fields into a primary section and a collapsible "More details" section based on a field-type/name heuristic (textarea / markdown / description / notes / remarks). Eliminates the wall-of-fields layout on objects without explicit detail metadata.
|
|
326
|
+
- **FormSection card chrome**: `FormSection` now accepts `showBorder`. Defaults to `true` for titled sections (Card wrapper) and `false` for untitled sections (flat). Same auto-default already applied to `DetailSection`.
|
|
327
|
+
- **Origin breadcrumb**: Navigating from a list/kanban into a record now records the source view; the detail page shows a `← <view label>` back-link above the page header.
|
|
328
|
+
- New i18n key `detail.sectionMoreDetails` (en + zh-CN).
|
|
329
|
+
|
|
330
|
+
### Patch Changes
|
|
331
|
+
|
|
332
|
+
- d51a577: feat(platform): Discussion attachments + @mention directory + Reference Rail aside
|
|
333
|
+
- **Discussion attachments** — `RichTextCommentInput` now accepts an `extraSlot`
|
|
334
|
+
and a `canSubmitEmpty` flag so hosts can mount the existing
|
|
335
|
+
`CommentAttachment` composer beneath the editor without forking the toolbar.
|
|
336
|
+
`RecordActivityTimeline` plumbs the attachments through
|
|
337
|
+
`DiscussionContext.onUploadAttachments` and submits attachment-only comments.
|
|
338
|
+
- **@mention directory** — `DiscussionContext` gains a `mentionSuggestions`
|
|
339
|
+
field; `RecordDetailView` populates it from the host `sys_user` collection so
|
|
340
|
+
`@` autocomplete in the composer now resolves against real users.
|
|
341
|
+
- **Reference Rail** — New `record:reference_rail` renderer + a dedicated
|
|
342
|
+
`aside` region emitted by `buildDefaultPageSchema` whenever a record has
|
|
343
|
+
≥ 2 related lists. The rail surfaces a Salesforce/HubSpot-style snapshot
|
|
344
|
+
of related collections (count badge + top 3 records) on `xl+` viewports.
|
|
345
|
+
- **Layout** — `PageRenderer`'s structured-layout `<aside>` wrappers now honor
|
|
346
|
+
`aside.className`, letting schemas attach responsive utilities like
|
|
347
|
+
`hidden xl:flex` to the rail region.
|
|
348
|
+
|
|
349
|
+
- 1976691: Fix the drawer "Open as full page" (maximize) button on the record drawer
|
|
350
|
+
which threw `TypeError: name.indexOf is not a function` and prevented
|
|
351
|
+
navigation to the dedicated detail page.
|
|
352
|
+
- `@object-ui/app-shell` `ObjectView`: pass `objectDef.name` (string) — not
|
|
353
|
+
the whole `objectDef` — into `viewLabel(...)` when computing the
|
|
354
|
+
`originState.from.label` for both drawer-navigate and list-navigate
|
|
355
|
+
flows. Two call sites fixed.
|
|
356
|
+
- `@object-ui/i18n` `useObjectLabel`: harden `stripNamespace` so it
|
|
357
|
+
tolerates non-string inputs and returns an empty string instead of
|
|
358
|
+
throwing, providing a safety net for similar future regressions.
|
|
359
|
+
|
|
360
|
+
- a49f300: feat(detail): per-object Reference Rail opt-out via `objectDef.detail.hideReferenceRail`
|
|
361
|
+
|
|
362
|
+
The Record-detail Reference Rail (right-hand related-list summary cards)
|
|
363
|
+
can now be suppressed on a per-object basis without authoring a full
|
|
364
|
+
custom `Page`. Catalog-style objects (Product, Task) ship with the rail
|
|
365
|
+
off by default; hub objects (Account, Opportunity, Contact, Case) keep it
|
|
366
|
+
on.
|
|
367
|
+
- `RecordDetailView` now reads `(objectDef as any)?.detail?.hideReferenceRail`
|
|
368
|
+
and `…?.hideRelatedTab` and threads them to `buildDefaultPageSchema`.
|
|
369
|
+
- The Reference Rail renderer also accepts entries authored as either a
|
|
370
|
+
flat `entries` array or nested under `properties.entries`, so explicit
|
|
371
|
+
`Page` authors can opt-in via the standard spec shape.
|
|
372
|
+
- See `packages/plugin-detail/README.md` (Reference Rail decision matrix)
|
|
373
|
+
for the rationale and per-object guidance.
|
|
374
|
+
|
|
375
|
+
- e9767b0: Remove dead `sys_presence` REST probes from `RecordDetailView` and `AppHeader`. Real-time
|
|
376
|
+
presence does not belong in a regular REST collection — the feature is being redesigned
|
|
377
|
+
behind a transport-level `<PresenceProvider>` (see ROADMAP). This change removes the
|
|
378
|
+
probe (and associated state / unused UI mounts) so the browser no longer makes silently
|
|
379
|
+
swallowed 404 requests on every record open / app navigation. UI surface area is
|
|
380
|
+
unchanged for end users (the previous code never rendered viewers when the probe failed).
|
|
381
|
+
- Updated dependencies [bd8447d]
|
|
382
|
+
- Updated dependencies [fbd5052]
|
|
383
|
+
- Updated dependencies [d51a577]
|
|
384
|
+
- Updated dependencies [1976691]
|
|
385
|
+
- Updated dependencies [d1ec6a2]
|
|
386
|
+
- Updated dependencies [cf30cc2]
|
|
387
|
+
- Updated dependencies [5b80cfd]
|
|
388
|
+
- Updated dependencies [49b1760]
|
|
389
|
+
- Updated dependencies [c0b236f]
|
|
390
|
+
- Updated dependencies [d548d6b]
|
|
391
|
+
- @object-ui/components@5.1.0
|
|
392
|
+
- @object-ui/react@5.1.0
|
|
393
|
+
- @object-ui/i18n@5.1.0
|
|
394
|
+
- @object-ui/types@5.1.0
|
|
395
|
+
- @object-ui/core@5.1.0
|
|
396
|
+
- @object-ui/data-objectstack@5.1.0
|
|
397
|
+
- @object-ui/fields@5.1.0
|
|
398
|
+
- @object-ui/layout@5.1.0
|
|
399
|
+
- @object-ui/auth@5.1.0
|
|
400
|
+
- @object-ui/collaboration@5.1.0
|
|
401
|
+
- @object-ui/permissions@5.1.0
|
|
402
|
+
- @object-ui/providers@5.1.0
|
|
403
|
+
|
|
3
404
|
## 5.0.2
|
|
4
405
|
|
|
5
406
|
### 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
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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',
|
|
@@ -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
|
+
}
|
package/dist/chrome/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/chrome/index.js
CHANGED
|
@@ -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';
|