@happyvertical/smrt-svelte 0.55.8 → 0.55.10
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/AGENTS.md +1 -1
- package/agents/overview-surfaces.md +122 -17
- package/agents/workspace.md +6 -0
- package/dist/components/activity/ActivityList.svelte +35 -0
- package/dist/components/activity/ActivityList.svelte.d.ts +11 -0
- package/dist/components/activity/ActivityList.svelte.d.ts.map +1 -0
- package/dist/components/activity/index.d.ts +3 -0
- package/dist/components/activity/index.d.ts.map +1 -0
- package/dist/components/activity/index.js +1 -0
- package/dist/components/activity/safe-href.d.ts +3 -0
- package/dist/components/activity/safe-href.d.ts.map +1 -0
- package/dist/components/activity/safe-href.js +12 -0
- package/dist/components/activity/types.d.ts +9 -0
- package/dist/components/activity/types.d.ts.map +1 -0
- package/dist/components/activity/types.js +1 -0
- package/dist/components/notifications/LocalizedNotifications.fixture.svelte +16 -0
- package/dist/components/notifications/LocalizedNotifications.fixture.svelte.d.ts +11 -0
- package/dist/components/notifications/LocalizedNotifications.fixture.svelte.d.ts.map +1 -0
- package/dist/components/notifications/NotificationBell.svelte +136 -0
- package/dist/components/notifications/NotificationBell.svelte.d.ts +13 -0
- package/dist/components/notifications/NotificationBell.svelte.d.ts.map +1 -0
- package/dist/components/notifications/index.d.ts +3 -0
- package/dist/components/notifications/index.d.ts.map +1 -0
- package/dist/components/notifications/index.js +1 -0
- package/dist/components/notifications/types.d.ts +27 -0
- package/dist/components/notifications/types.d.ts.map +1 -0
- package/dist/components/notifications/types.js +1 -0
- package/dist/components/overview/OverviewAssistantUndo.svelte +77 -0
- package/dist/components/overview/OverviewAssistantUndo.svelte.d.ts +13 -0
- package/dist/components/overview/OverviewAssistantUndo.svelte.d.ts.map +1 -0
- package/dist/components/overview/assistant-surface.d.ts +90 -0
- package/dist/components/overview/assistant-surface.d.ts.map +1 -0
- package/dist/components/overview/assistant-surface.js +65 -0
- package/dist/components/overview/assistant.svelte.d.ts +115 -0
- package/dist/components/overview/assistant.svelte.d.ts.map +1 -0
- package/dist/components/overview/assistant.svelte.js +260 -0
- package/dist/components/overview/controller.svelte.d.ts +8 -0
- package/dist/components/overview/controller.svelte.d.ts.map +1 -1
- package/dist/components/overview/controller.svelte.js +76 -13
- package/dist/components/overview/index.d.ts +2 -0
- package/dist/components/overview/index.d.ts.map +1 -1
- package/dist/components/overview/index.js +2 -0
- package/dist/components/overview/operations.d.ts +169 -0
- package/dist/components/overview/operations.d.ts.map +1 -0
- package/dist/components/overview/operations.js +358 -0
- package/dist/components/overview/server.d.ts +2 -0
- package/dist/components/overview/server.d.ts.map +1 -1
- package/dist/components/overview/server.js +2 -0
- package/dist/components/workspace/admin-shell/settings-delta.d.ts +21 -0
- package/dist/components/workspace/admin-shell/settings-delta.d.ts.map +1 -0
- package/dist/components/workspace/admin-shell/settings-delta.js +142 -0
- package/dist/components/workspace/index.d.ts +1 -0
- package/dist/components/workspace/index.d.ts.map +1 -1
- package/dist/components/workspace/index.js +1 -0
- package/dist/components/workspace/server/index.d.ts +6 -2
- package/dist/components/workspace/server/index.d.ts.map +1 -1
- package/dist/components/workspace/server/index.js +5 -2
- package/dist/i18n/server.d.ts +1 -0
- package/dist/i18n/server.d.ts.map +1 -1
- package/dist/i18n/server.js +1 -0
- package/dist/i18n/strings.notifications.d.ts +12 -0
- package/dist/i18n/strings.notifications.d.ts.map +1 -0
- package/dist/i18n/strings.notifications.js +13 -0
- package/dist/i18n/strings.overview.d.ts +6 -0
- package/dist/i18n/strings.overview.d.ts.map +1 -1
- package/dist/i18n/strings.overview.js +7 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/package.json +20 -14
package/AGENTS.md
CHANGED
|
@@ -19,7 +19,7 @@ subpath you are editing. This file keeps what holds in every module.
|
|
|
19
19
|
| `src/components/app/` (`./app`) | `AppShell` (Provider + ThemeProvider + theme CSS + AdminShell + nav/dock slots), `OwnerSetupForm` (props-driven first-run owner form), `ShellSettingsPage`, `RuntimeDiagnosticsWebMcp`. Browser-only; no `$app/*` or server imports | [agents/workspace.md](agents/workspace.md#app-shell-and-owner-setup-app) |
|
|
20
20
|
| `src/components/workspace/` (`./workspace` + `./web`) | the AdminShell family and its principles, the legacy ToolsDock surface, the `./web` activity-feed and `updateAvailable` adapters, and server-side dock gates | [agents/workspace.md](agents/workspace.md) |
|
|
21
21
|
| `src/components/command-palette/` (`./command-palette`) | `CommandPalette` trigger + dialog, `PaletteProvider` contract, nav and manifest providers (#3713) | [agents/command-palette.md](agents/command-palette.md) |
|
|
22
|
-
| `src/components/overview/` (`./overview` + `./overview/server`) | customizable overview surfaces: `OverviewGrid` flow grid (span 1-4, keyboard reorder/resize) joined to the shell's layout edit mode, host-owned `createOverview` controller, widget registry with versioned option schemas validated on save and load, the server `load(options, ctx)` contract (`loadOverview`), the metric/chart/records/shortcuts/note core widgets, and `registerRecipeWidgets` (recipe `widget` surfaces to registry registrations, injected module resolver) (#3727) | [agents/overview-surfaces.md](agents/overview-surfaces.md) |
|
|
22
|
+
| `src/components/overview/` (`./overview` + `./overview/server`) | customizable overview surfaces: `OverviewGrid` flow grid (span 1-4, keyboard reorder/resize) joined to the shell's layout edit mode, host-owned `createOverview` controller, widget registry with versioned option schemas validated on save and load, the server `load(options, ctx)` contract (`loadOverview`), the metric/chart/records/shortcuts/note core widgets, and `registerRecipeWidgets` (recipe `widget` surfaces to registry registrations, injected module resolver) (#3727); production persistence is `@happyvertical/smrt-preferences` (kind `overview`) | [agents/overview-surfaces.md](agents/overview-surfaces.md) |
|
|
23
23
|
| `src/web/remote-query.svelte.ts` | Svelte 5 binding for query-shaped remote pages: rows, page, totals, loading/refreshing/stale/error, retry, last-updated, and query-scoped live subscriptions (#2445) | — |
|
|
24
24
|
| `src/web/list-data-surface.svelte.ts` (`mountListDataSurface`) | Registers an existing non-`DataTable` list (mirroring a `DataTableController`) as a mounted `DataSurfaceDescriptor`; a same-behavior port of `registerContentListDataSurface`, not yet unified behind smrt-ui (#2917). The module doc comment is the authoritative list of divergences (#2906) | — |
|
|
25
25
|
| `src/web/webmcp-provider.ts` | Provider config for generated data/model WebMCP tools: definitions, effect policy, namespace, budget, legacy/canonical filters, and fetcher seams (#2520) | — |
|
|
@@ -11,15 +11,16 @@ its default arrangement; admins (or users, per role) add, reorder, resize,
|
|
|
11
11
|
configure and remove widgets inside that confined set, and reset returns to the
|
|
12
12
|
default. Phase 1 (this module) is the surface, the registry, the data model, the
|
|
13
13
|
server load contract and five core widgets; phase 2 adds package widgets from
|
|
14
|
-
recipes ("Recipe widgets")
|
|
15
|
-
|
|
14
|
+
recipes ("Recipe widgets"); phase 3 is production persistence in
|
|
15
|
+
`@happyvertical/smrt-preferences` ("Production persistence"); phase 4 adds
|
|
16
|
+
assistant operations with Undo ("Assistant operations").
|
|
16
17
|
|
|
17
18
|
## Two entries
|
|
18
19
|
|
|
19
20
|
| Entry | Contents |
|
|
20
21
|
|---|---|
|
|
21
|
-
| `./overview/server` (`server.ts`) | Svelte-free: types, `validateWidgetOptions`, the model (`resolveOverview`, `checkOverviewOverride`, `applyOverviewOverride`, `diffOverview`, `sanitizeOverview`, ...), `WidgetRegistry` / `registerWidget`, `loadOverview`. Safe in a `+page.server.ts` or a save endpoint. |
|
|
22
|
-
| `./overview` (`index.ts`) | Everything above plus `OverviewGrid`, `createOverview` (controller), `WidgetOptionsForm`, the core widgets and `registerCoreWidgets`, `shortcutsFromNav`, `formatWidgetValue`, `parseMarkdown` / `safeHref`. |
|
|
22
|
+
| `./overview/server` (`server.ts`) | Svelte-free: types, `validateWidgetOptions`, the model (`resolveOverview`, `checkOverviewOverride`, `applyOverviewOverride`, `diffOverview`, `sanitizeOverview`, ...), `WidgetRegistry` / `registerWidget`, `loadOverview`, the operations (`planOverviewOperations`, `describeOverview`) and `createOverviewAssistantSurface`. Safe in a `+page.server.ts` or a save endpoint. |
|
|
23
|
+
| `./overview` (`index.ts`) | Everything above plus `OverviewGrid`, `createOverview` (controller), `WidgetOptionsForm`, the core widgets and `registerCoreWidgets`, `shortcutsFromNav`, `formatWidgetValue`, `parseMarkdown` / `safeHref`, `createOverviewAssistant` / `useOverviewAssistantTools` / `OverviewAssistantUndo`. |
|
|
23
24
|
|
|
24
25
|
## Data model (`types.ts`, `model.ts`)
|
|
25
26
|
|
|
@@ -50,7 +51,8 @@ against (absent means 1).
|
|
|
50
51
|
(they would stop incrementing), and an exhausted high end restarts at `w1`.
|
|
51
52
|
- **Tiers.** `resolveOverview` merges one override. A host with tenant defaults
|
|
52
53
|
and user overrides merges the tenant tier into the definition's `defaults`
|
|
53
|
-
first
|
|
54
|
+
first; the user override then applies to that (smrt-preferences'
|
|
55
|
+
`withTenantDefaults`).
|
|
54
56
|
- **Reset** is `controller.reset()` (store `null`) or `resetWidget(id)`.
|
|
55
57
|
|
|
56
58
|
## Validation: what keeps a stored overview from running anything
|
|
@@ -136,7 +138,11 @@ return { overview }; // LoadedOverview: JSON, serializable
|
|
|
136
138
|
loaded?, loadWidget? })`, the `ShellLayout` pattern: **persistence is host-owned**.
|
|
137
139
|
With an `override` getter the host is the source of truth and feeds each edit back
|
|
138
140
|
(`onchange(override | null)`); without it the value is kept in memory. Reactive
|
|
139
|
-
reads: `document`, `base`, `override`, `
|
|
141
|
+
reads: `document`, `base`, `override`, `committedOverride` (with a host-owned
|
|
142
|
+
getter: the newest value handed to `onchange` that the getter has not fed back
|
|
143
|
+
yet, else `override`; emissions are acknowledged in order, so several saves in
|
|
144
|
+
flight never regress it, and a getter value matching none of them and not the
|
|
145
|
+
value before them is a change from elsewhere that drops the queue), `issues`, `customized`, `canCustomize`,
|
|
140
146
|
`addable`, `full`, `entry(widget)`. Operations, each returning
|
|
141
147
|
`{ ok: true, id } | { ok: false, reason, issues? }` and refused with
|
|
142
148
|
`not_allowed` when `canCustomize()` is false (the role gate; the server must check
|
|
@@ -239,17 +245,116 @@ turns the manifest's widget surfaces into `registry.register(...)` calls:
|
|
|
239
245
|
and a type-level assertion here keeps `RecipeWidgetOptionType` equal to
|
|
240
246
|
`WidgetOptionType`.
|
|
241
247
|
|
|
248
|
+
## Production persistence (phase 3, `@happyvertical/smrt-preferences`)
|
|
249
|
+
|
|
250
|
+
Overrides are stored as the `overview` kind of the generic user-interface
|
|
251
|
+
preference table `_smrt_ui_preferences` in
|
|
252
|
+
[smrt-preferences](../../preferences/AGENTS.md) (one row per tenant, kind,
|
|
253
|
+
surface id = `OverviewDefinition.id`, and tier; the kind contract is in its
|
|
254
|
+
[preference-kinds.md](../../preferences/agents/preference-kinds.md)). This
|
|
255
|
+
package stays free of database code: its overview entries import nothing from
|
|
256
|
+
core, tenancy or users. The `overview` kind validates with this package's
|
|
257
|
+
`checkOverviewOverride` on save (only the canonical override is stored) and
|
|
258
|
+
`resolveOverview` on load (bad entries dropped and reported per tier).
|
|
259
|
+
`createOverviewStore({ db })` is the overview-shaped layer over the generic
|
|
260
|
+
store and acts as the ambient principal:
|
|
261
|
+
|
|
262
|
+
| Call | Does |
|
|
263
|
+
| --- | --- |
|
|
264
|
+
| `load(definition, registry)` | Both tiers resolved page defaults < tenant < user; each tier's `override`, `revision`, `issues`, the merged `document` and `canCustomize: { tenant, user }` |
|
|
265
|
+
| `save(definition, registry, { scope, override, revision })` | Strict validation against the tier's definition (user: tenant-merged); `invalid` / `conflict` / `not_allowed` otherwise |
|
|
266
|
+
| `reset(definition, { scope, revision? })` | Deletes that tier's row |
|
|
267
|
+
| `loadPage(definition, registry, ctx, { scope? })` | `load` plus `loadOverview` of the tier's document, for `+page.server.ts` |
|
|
268
|
+
| `loadWidget(definition, registry, widget, ctx)` | One widget's data for the controller's `loadWidget` remote function (throw on `ok: false`) |
|
|
269
|
+
|
|
270
|
+
Host wiring for the personal layer: `createOverview({ definition:
|
|
271
|
+
withTenantDefaults(definition, data.overview.tenant.document), override: () =>
|
|
272
|
+
data.overview.user?.override ?? null, onchange: (next) => save({ scope:
|
|
273
|
+
'user', override: next, revision }), canCustomize: () =>
|
|
274
|
+
data.overview.canCustomize.user, loaded: data.overview.loaded, loadWidget })`;
|
|
275
|
+
the organization layer uses the page definition, `tenant.override` and
|
|
276
|
+
`canCustomize.tenant`. Keep the returned `revision` for the next save; on
|
|
277
|
+
`conflict` reload the page data. Permissions are `overviews.customize` (tenant
|
|
278
|
+
default) and `overviews.personalize` (own layout).
|
|
279
|
+
|
|
280
|
+
Known model limit: a tenant default that later adds a widget whose id equals
|
|
281
|
+
an id a user override already added hides the user's widget (an added widget
|
|
282
|
+
colliding with a base id is skipped). `nextWidgetId` avoids it within one tier
|
|
283
|
+
only.
|
|
284
|
+
|
|
285
|
+
## Assistant operations (`operations.ts`, `assistant-surface.ts`, `assistant.svelte.ts`)
|
|
286
|
+
|
|
287
|
+
Phase 4: the assistant adds and configures widgets with structured operations,
|
|
288
|
+
and the person can undo the change.
|
|
289
|
+
|
|
290
|
+
- **Operations** are data: `add { type, span?, options?, index? }`,
|
|
291
|
+
`configure { id, options }` (merged onto the current options; `null` clears a
|
|
292
|
+
key to its default), `move { id, index }`, `resize { id, span }`,
|
|
293
|
+
`remove { id }`. `OVERVIEW_OPERATION_SCHEMA` is their JSON schema.
|
|
294
|
+
- **`planOverviewOperations({ definition, registry, override, operations })`**
|
|
295
|
+
plans a batch (at most 20) against the current override. Strict: unknown ops
|
|
296
|
+
or fields, unregistered or disallowed types (`allowed`, `allowedIn`), invalid
|
|
297
|
+
options (including a `model` outside `definition.models`), spans outside the
|
|
298
|
+
type's range, unknown widget ids and the cap are `OverviewOperationIssue`s
|
|
299
|
+
(`index`, `op`, `widgetId`, `code`, `message`, `options`). The result is then
|
|
300
|
+
run through `checkOverviewOverride`, so it is always an override a save
|
|
301
|
+
accepts. **Atomic**: any issue rejects the batch and returns nothing to
|
|
302
|
+
apply. Later operations see earlier ones; an `add` takes the next free id
|
|
303
|
+
(`nextWidgetId`, never a removed default's). Total, never throws.
|
|
304
|
+
- **`describeOverview(...)`** is what a model reads first: allowed types with
|
|
305
|
+
option fields (type, required, default, range, `choices`, the page
|
|
306
|
+
`models`), span ranges, the current widgets with their index and title.
|
|
307
|
+
`translate` resolves i18n-key titles and labels.
|
|
308
|
+
- **Server: `createOverviewAssistantSurface({ definition, registry, override,
|
|
309
|
+
revision, canCustomize, persist })`** is one page for one principal:
|
|
310
|
+
`describe`, `current`, `plan`, `check` and `persist`. Writes are
|
|
311
|
+
conditional: `persist(override, { revision })` must write only if the
|
|
312
|
+
stored row is still the loaded `revision` and answer
|
|
313
|
+
`{ ok: false, reason: 'conflict' }` otherwise
|
|
314
|
+
(`OverviewAssistantPersistResult`, structurally `OverviewSaveResult` of
|
|
315
|
+
`@happyvertical/smrt-preferences`); on success the surface follows the
|
|
316
|
+
returned `revision`. With phase 3:
|
|
317
|
+
`persist: (override, { revision }) => store.save(definition, registry, { scope: 'user', override, revision })`
|
|
318
|
+
where `store = createOverviewStore({ db })` from `@happyvertical/smrt-preferences`,
|
|
319
|
+
`override` / `revision` come from `store.load(...)`'s `state.user`, and the
|
|
320
|
+
surface's `definition` is `withTenantDefaults(definition, state.tenant.document)`. It is the adapter
|
|
321
|
+
`@happyvertical/smrt-chat`'s `createOverviewTools()` drives (typed
|
|
322
|
+
structurally there; see
|
|
323
|
+
[chat overview-tools.md](../../chat/agents/overview-tools.md) for the tool
|
|
324
|
+
contract, error codes and principal/page-scoped Undo). Phase 3 supplies the
|
|
325
|
+
stored override, the role and `persist`.
|
|
326
|
+
- **Browser: `createOverviewAssistant(controller, { toolPrefix?, toaster?,
|
|
327
|
+
translate? })`** applies a batch through `controller.restore(plan.override)`,
|
|
328
|
+
so the grid updates live and the host's `onchange` persists it like any edit.
|
|
329
|
+
It refuses with `not_allowed` when `canCustomize` is false. `lastBatch`
|
|
330
|
+
(reactive) keeps `{ id, results, counts, before, after }`; `undo(id?)`
|
|
331
|
+
restores `before` exactly, once, and refuses with `changed_since` when the
|
|
332
|
+
override is no longer `after` (the person edited since). Both read
|
|
333
|
+
`controller.committedOverride`, so in host-owned mode a batch whose save the
|
|
334
|
+
host has not fed back yet is not mistaken for a change made elsewhere.
|
|
335
|
+
`useOverviewAssistantTools(assistant)` registers `<prefix>_describe` (read),
|
|
336
|
+
`<prefix>_apply` and `<prefix>_undo` (write) through `useWebMcpTool`, so the
|
|
337
|
+
AssistantDock offers them as page tools: the Provider's `webmcp.effects` must
|
|
338
|
+
allow `write`, the server's `clientToolAllowList` must list `overview_*`, and
|
|
339
|
+
the dock asks the person to Allow each write unless the host policy says
|
|
340
|
+
otherwise. Results are JSON strings (`{ ok: false, error, issues }` on a
|
|
341
|
+
rejected batch).
|
|
342
|
+
- **Undo affordance.** `OverviewAssistantUndo` (`assistant`, optional
|
|
343
|
+
`message`) shows the last batch with Undo and Dismiss buttons and announces
|
|
344
|
+
the outcome in its own polite live region; or pass a smrt-ui `toaster` and
|
|
345
|
+
each batch shows as a toast with an Undo action (the host mounts
|
|
346
|
+
`ToastViewport`). Strings are `ui.overview.assistant_*`.
|
|
347
|
+
- Tests: `__tests__/operations.test.ts`, `__tests__/assistant.test.ts` (live
|
|
348
|
+
grid update, Undo, the component with axe) and
|
|
349
|
+
`__tests__/assistant-tools.integration.test.ts` (chat's tools over this
|
|
350
|
+
model).
|
|
351
|
+
|
|
242
352
|
## Extension points left for later phases
|
|
243
353
|
|
|
244
|
-
- **Phase 2, package widgets (#3727, done).** See "Recipe widgets"
|
|
245
|
-
- **Phase 3, production persistence
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
`
|
|
249
|
-
`
|
|
250
|
-
that calls the same `load`. Role gating = `canCustomize` plus the same check in
|
|
251
|
-
the save endpoint. No migration or table lives in this package.
|
|
252
|
-
- **Phase 4, assistant.** The controller methods are the structured operations;
|
|
253
|
-
`restore(override)` plus the `override` getter give Undo (snapshot before,
|
|
254
|
-
restore after). `OverviewOpResult.issues` is the validation feedback to return.
|
|
354
|
+
- **Phase 2, package widgets (#3727, done).** See "Recipe widgets" above.
|
|
355
|
+
- **Phase 3, production persistence (#3727, done).** See "Production
|
|
356
|
+
persistence" above.
|
|
357
|
+
- **Phase 4, assistant (#3727, done).** See "Assistant operations" above;
|
|
358
|
+
`createOverviewAssistantSurface` is wired to the stored user tier through
|
|
359
|
+
`createOverviewStore(...).save` as shown there.
|
|
255
360
|
- **Not built:** the activity widget (Track B, #3710) and agenda.
|
package/agents/workspace.md
CHANGED
|
@@ -32,6 +32,12 @@ them from the published artifact versus exporting them is tracked in #2286.
|
|
|
32
32
|
- App-owned configuration for hidden edges, push/overlay presentation, and
|
|
33
33
|
exclusivity groups
|
|
34
34
|
- User-owned preferences persist as sparse `ShellSettingsDelta` values
|
|
35
|
+
through a `ShellSettingsAdapter` (`LocalStorageShellSettingsAdapter` by
|
|
36
|
+
default). Server-side, per tenant and user, they can persist as the
|
|
37
|
+
`shell-layout` kind of `@happyvertical/smrt-preferences`
|
|
38
|
+
(`createShellSettingsPreferences`, the server half of an adapter); it
|
|
39
|
+
validates with `sanitizeShellSettingsDelta`, the untrusted-input reader on
|
|
40
|
+
the Node-safe `./workspace/server` entry (with `mergeShellSettingsDelta`)
|
|
35
41
|
- Focus tools may register imperatively through `ShellState` or declaratively
|
|
36
42
|
through Svelte helpers
|
|
37
43
|
- Shell activities are client-side records; server jobs, polling, WebSockets,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import WorkspaceActivityList from '../workspace/admin-shell/ActivityList.svelte';
|
|
3
|
+
import { tryUseAdminShell } from '../workspace/admin-shell/context.js';
|
|
4
|
+
import { safeActivityHref } from './safe-href.js';
|
|
5
|
+
import type { ActivityListEntry } from './types.js';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
/** Already-authorized activity rows to display. */
|
|
9
|
+
entries: ActivityListEntry[];
|
|
10
|
+
/** Message displayed by the shell list when no activities match. */
|
|
11
|
+
emptyLabel?: string;
|
|
12
|
+
}
|
|
13
|
+
let { entries, emptyLabel = 'No activity.' }: Props = $props();
|
|
14
|
+
const shell = tryUseAdminShell();
|
|
15
|
+
const prefix = `audit-feed:${crypto.randomUUID()}:`;
|
|
16
|
+
$effect(() => {
|
|
17
|
+
if (!shell) return;
|
|
18
|
+
const ids = new Set(entries.map((entry) => `${prefix}${entry.id}`));
|
|
19
|
+
for (const entry of entries)
|
|
20
|
+
shell.upsertActivity({
|
|
21
|
+
id: `${prefix}${entry.id}`,
|
|
22
|
+
label: entry.title,
|
|
23
|
+
message: entry.detail,
|
|
24
|
+
detailHref: safeActivityHref(entry.href),
|
|
25
|
+
kind: prefix,
|
|
26
|
+
scope: 'system',
|
|
27
|
+
status: 'completed',
|
|
28
|
+
createdAt: entry.occurredAt,
|
|
29
|
+
});
|
|
30
|
+
return () => {
|
|
31
|
+
for (const id of ids) shell.removeActivity(id);
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
</script>
|
|
35
|
+
{#if shell}<WorkspaceActivityList filter={{ kind: prefix }} {emptyLabel} />{:else}<p>{emptyLabel}</p>{/if}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ActivityListEntry } from './types.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Already-authorized activity rows to display. */
|
|
4
|
+
entries: ActivityListEntry[];
|
|
5
|
+
/** Message displayed by the shell list when no activities match. */
|
|
6
|
+
emptyLabel?: string;
|
|
7
|
+
}
|
|
8
|
+
declare const ActivityList: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type ActivityList = ReturnType<typeof ActivityList>;
|
|
10
|
+
export default ActivityList;
|
|
11
|
+
//# sourceMappingURL=ActivityList.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ActivityList.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/activity/ActivityList.svelte.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAGpD,UAAU,KAAK;IACb,mDAAmD;IACnD,OAAO,EAAE,iBAAiB,EAAE,CAAC;IAC7B,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAiCD,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/activity/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAChE,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ActivityList } from './ActivityList.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-href.d.ts","sourceRoot":"","sources":["../../../src/components/activity/safe-href.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC/B,MAAM,GAAG,SAAS,CASpB"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Accept ordinary web links, never executable schemes or protocol-relative URLs. */
|
|
2
|
+
export function safeActivityHref(value) {
|
|
3
|
+
if (!value || value.startsWith('//') || value.includes('\\'))
|
|
4
|
+
return undefined;
|
|
5
|
+
try {
|
|
6
|
+
const url = new URL(value, 'https://smrt.invalid');
|
|
7
|
+
return ['https:', 'http:'].includes(url.protocol) ? value : undefined;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/activity/types.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { type I18nStore, setI18nContext } from '@happyvertical/smrt-ui/i18n';
|
|
3
|
+
import { untrack } from 'svelte';
|
|
4
|
+
import MailboxDemo from '../../../../messages/src/svelte/components/MailboxDemo.svelte';
|
|
5
|
+
import NotificationBell from './NotificationBell.svelte';
|
|
6
|
+
import type { NotificationProvider } from './types.js';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
store: I18nStore;
|
|
10
|
+
provider?: NotificationProvider;
|
|
11
|
+
mailbox?: boolean;
|
|
12
|
+
}
|
|
13
|
+
let { store, provider, mailbox = false }: Props = $props();
|
|
14
|
+
setI18nContext(untrack(() => store));
|
|
15
|
+
</script>
|
|
16
|
+
{#if mailbox}<MailboxDemo />{:else if provider}<NotificationBell {provider} />{/if}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type I18nStore } from '@happyvertical/smrt-ui/i18n';
|
|
2
|
+
import type { NotificationProvider } from './types.js';
|
|
3
|
+
interface Props {
|
|
4
|
+
store: I18nStore;
|
|
5
|
+
provider?: NotificationProvider;
|
|
6
|
+
mailbox?: boolean;
|
|
7
|
+
}
|
|
8
|
+
declare const LocalizedNotifications: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type LocalizedNotifications = ReturnType<typeof LocalizedNotifications>;
|
|
10
|
+
export default LocalizedNotifications;
|
|
11
|
+
//# sourceMappingURL=LocalizedNotifications.fixture.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocalizedNotifications.fixture.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/notifications/LocalizedNotifications.fixture.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,SAAS,EAAkB,MAAM,6BAA6B,CAAC;AAI7E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAGvD,UAAU,KAAK;IACb,KAAK,EAAE,SAAS,CAAC;IACjB,QAAQ,CAAC,EAAE,oBAAoB,CAAC;IAChC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAeD,QAAA,MAAM,sBAAsB,2CAAwC,CAAC;AACrE,KAAK,sBAAsB,GAAG,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAC;AACxE,eAAe,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
3
|
+
import { Button } from '@happyvertical/smrt-ui/ui';
|
|
4
|
+
import { M } from '../../i18n/strings.notifications.js';
|
|
5
|
+
import type { NotificationProvider } from './types.js';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
/** Authorized provider that supplies this user's notification state. */
|
|
9
|
+
provider: NotificationProvider;
|
|
10
|
+
/** Accessible trigger and panel heading. */
|
|
11
|
+
label?: string;
|
|
12
|
+
/** Maximum number of recent notifications requested from the provider. */
|
|
13
|
+
limit?: number;
|
|
14
|
+
}
|
|
15
|
+
const { t } = useI18n();
|
|
16
|
+
let { provider, label, limit = 20 }: Props = $props();
|
|
17
|
+
const heading = $derived(label ?? t(M['ui.notification_bell.label']));
|
|
18
|
+
const instanceId = $props.id();
|
|
19
|
+
let panel = $state<HTMLDivElement>();
|
|
20
|
+
let open = $state(false);
|
|
21
|
+
$effect(() => {
|
|
22
|
+
if (open) panel?.focus();
|
|
23
|
+
});
|
|
24
|
+
let loading = $state(true);
|
|
25
|
+
let error = $state<string | null>(null);
|
|
26
|
+
let unread = $state(0);
|
|
27
|
+
let items = $state<Awaited<ReturnType<NotificationProvider['list']>>>([]);
|
|
28
|
+
let request = 0;
|
|
29
|
+
let generation = 0;
|
|
30
|
+
let active = false;
|
|
31
|
+
|
|
32
|
+
async function refresh(source = provider, epoch = generation) {
|
|
33
|
+
if (!active || epoch !== generation) return;
|
|
34
|
+
const current = ++request;
|
|
35
|
+
loading = true;
|
|
36
|
+
error = null;
|
|
37
|
+
try {
|
|
38
|
+
const [nextUnread, nextItems] = await Promise.all([
|
|
39
|
+
source.getUnreadCount(),
|
|
40
|
+
source.list({ limit }),
|
|
41
|
+
]);
|
|
42
|
+
if (!active || epoch !== generation || current !== request) return;
|
|
43
|
+
unread =
|
|
44
|
+
Number.isSafeInteger(nextUnread) && nextUnread > 0 ? nextUnread : 0;
|
|
45
|
+
items = nextItems;
|
|
46
|
+
} catch {
|
|
47
|
+
if (!active || epoch !== generation || current !== request) return;
|
|
48
|
+
error = M['ui.notification_bell.unavailable'];
|
|
49
|
+
} finally {
|
|
50
|
+
if (active && epoch === generation && current === request) loading = false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function safeHref(value: string | null | undefined): string | null {
|
|
54
|
+
if (!value || value.startsWith('//') || value.includes('\\')) return null;
|
|
55
|
+
try {
|
|
56
|
+
const parsed = new URL(value, 'https://smrt.invalid');
|
|
57
|
+
return parsed.protocol === 'http:' || parsed.protocol === 'https:'
|
|
58
|
+
? value
|
|
59
|
+
: null;
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function close() {
|
|
65
|
+
open = false;
|
|
66
|
+
document.getElementById(`${instanceId}-trigger`)?.focus();
|
|
67
|
+
}
|
|
68
|
+
function keydown(event: KeyboardEvent) {
|
|
69
|
+
if (event.key === 'Escape' && open) {
|
|
70
|
+
event.preventDefault();
|
|
71
|
+
close();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function mutate(id?: string) {
|
|
75
|
+
const source = provider;
|
|
76
|
+
const epoch = generation;
|
|
77
|
+
try {
|
|
78
|
+
if (id) await source.markRead(id);
|
|
79
|
+
else await source.markAllRead();
|
|
80
|
+
await refresh(source, epoch);
|
|
81
|
+
} catch {
|
|
82
|
+
if (active && epoch === generation)
|
|
83
|
+
error = M['ui.notification_bell.mark_error'];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
$effect(() => {
|
|
87
|
+
const source = provider;
|
|
88
|
+
const epoch = ++generation;
|
|
89
|
+
active = true;
|
|
90
|
+
items = [];
|
|
91
|
+
unread = 0;
|
|
92
|
+
error = null;
|
|
93
|
+
void refresh(source, epoch);
|
|
94
|
+
let stop: (() => void) | undefined;
|
|
95
|
+
try {
|
|
96
|
+
stop = source.subscribe?.(() => void refresh(source, epoch));
|
|
97
|
+
} catch {
|
|
98
|
+
error = M['ui.notification_bell.live_error'];
|
|
99
|
+
}
|
|
100
|
+
return () => {
|
|
101
|
+
active = false;
|
|
102
|
+
++generation;
|
|
103
|
+
++request;
|
|
104
|
+
stop?.();
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<div class="smrt-notification-bell">
|
|
110
|
+
<Button id={`${instanceId}-trigger`} aria-controls={`${instanceId}-panel`} variant="ghost" aria-haspopup="dialog" aria-expanded={open} onclick={() => (open = !open)}>
|
|
111
|
+
{heading}{#if unread > 0}<span aria-label={t(M['ui.notification_bell.unread'], { count: unread })}> ({unread})</span>{/if}
|
|
112
|
+
</Button>
|
|
113
|
+
{#if open}
|
|
114
|
+
<div bind:this={panel} id={`${instanceId}-panel`} role="dialog" aria-label={heading} tabindex="-1" onkeydown={keydown} class="smrt-notification-bell__panel">
|
|
115
|
+
<header><strong>{heading}</strong><Button variant="ghost" size="sm" disabled={unread === 0 || loading} onclick={() => mutate()}>{t(M['ui.notification_bell.mark_all'])}</Button></header>
|
|
116
|
+
{#if error}<p role="alert">{t(error)}</p>{/if}
|
|
117
|
+
{#if loading}<p role="status">{t(M['ui.notification_bell.loading'])}</p>{:else if items.length === 0}<p>{t(M['ui.notification_bell.empty'])}</p>{:else}<ol>
|
|
118
|
+
{#each items as item (item.id)}<li data-read={item.readAt ? 'true' : 'false'}>
|
|
119
|
+
{#if safeHref(item.href)}<a href={safeHref(item.href) ?? undefined}>{item.title}</a>{:else}<strong>{item.title}</strong>{/if}
|
|
120
|
+
{#if item.body}<p>{item.body}</p>{/if}
|
|
121
|
+
{#if !item.readAt}<Button variant="ghost" size="sm" onclick={() => mutate(item.id)}>{t(M['ui.notification_bell.mark_one'])}</Button>{/if}
|
|
122
|
+
</li>{/each}
|
|
123
|
+
</ol>{/if}
|
|
124
|
+
</div>
|
|
125
|
+
{/if}
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<style>
|
|
129
|
+
.smrt-notification-bell { position: relative; }
|
|
130
|
+
.smrt-notification-bell__panel { position: absolute; z-index: 1; inset-block-start: 100%; inset-inline-end: 0; inline-size: min(24rem, 90vw); padding: var(--smrt-spacing-3); background: var(--smrt-color-surface); border: 1px solid var(--smrt-color-outline); border-radius: var(--smrt-radius-medium); }
|
|
131
|
+
header { display: flex; justify-content: space-between; gap: var(--smrt-spacing-2); }
|
|
132
|
+
ol { display: grid; gap: var(--smrt-spacing-2); padding: 0; list-style: none; }
|
|
133
|
+
li { padding-block: var(--smrt-spacing-2); border-block-end: 1px solid var(--smrt-color-outline-variant); }
|
|
134
|
+
li[data-read='false'] { border-inline-start: 3px solid var(--smrt-color-primary); padding-inline-start: var(--smrt-spacing-2); }
|
|
135
|
+
p { overflow-wrap: anywhere; }
|
|
136
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { NotificationProvider } from './types.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Authorized provider that supplies this user's notification state. */
|
|
4
|
+
provider: NotificationProvider;
|
|
5
|
+
/** Accessible trigger and panel heading. */
|
|
6
|
+
label?: string;
|
|
7
|
+
/** Maximum number of recent notifications requested from the provider. */
|
|
8
|
+
limit?: number;
|
|
9
|
+
}
|
|
10
|
+
declare const NotificationBell: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type NotificationBell = ReturnType<typeof NotificationBell>;
|
|
12
|
+
export default NotificationBell;
|
|
13
|
+
//# sourceMappingURL=NotificationBell.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NotificationBell.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/notifications/NotificationBell.svelte.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAGvD,UAAU,KAAK;IACb,wEAAwE;IACxE,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA4HD,QAAA,MAAM,gBAAgB,2CAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/notifications/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AACxE,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as NotificationBell } from './NotificationBell.svelte';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** A browser-safe notification supplied by an application-owned provider. */
|
|
2
|
+
export interface NotificationItem {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
body?: string;
|
|
6
|
+
href?: string | null;
|
|
7
|
+
severity?: 'info' | 'success' | 'warning' | 'error';
|
|
8
|
+
occurredAt: string;
|
|
9
|
+
readAt?: string | null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Application boundary for the notification bell.
|
|
13
|
+
*
|
|
14
|
+
* Providers authorize their own reads and writes. The component never accepts
|
|
15
|
+
* a user id or tenant id, so it cannot broaden that authority.
|
|
16
|
+
*/
|
|
17
|
+
export interface NotificationProvider {
|
|
18
|
+
getUnreadCount(): Promise<number>;
|
|
19
|
+
list(options?: {
|
|
20
|
+
limit?: number;
|
|
21
|
+
}): Promise<NotificationItem[]>;
|
|
22
|
+
markRead(id: string): Promise<void>;
|
|
23
|
+
markAllRead(): Promise<void>;
|
|
24
|
+
/** Optional live transport subscription. Returns an unsubscribe function. */
|
|
25
|
+
subscribe?(listener: () => void): () => void;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/notifications/types.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACnC,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,IAAI,CAAC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAAC;IAChE,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,6EAA6E;IAC7E,SAAS,CAAC,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;CAC9C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The inline Undo affordance for an assistant's overview edit (#3727 phase
|
|
4
|
+
* 4). While the last batch can be undone it shows a short message with an
|
|
5
|
+
* Undo button and a dismiss button; the outcome of Undo is announced in the
|
|
6
|
+
* same polite live region. Place it next to the `OverviewGrid` it belongs to.
|
|
7
|
+
*/
|
|
8
|
+
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
9
|
+
import { Button } from '@happyvertical/smrt-ui/ui';
|
|
10
|
+
import { M } from '../../i18n/strings.overview.js';
|
|
11
|
+
import type {
|
|
12
|
+
OverviewAssistant,
|
|
13
|
+
OverviewAssistantBatch,
|
|
14
|
+
} from './assistant.svelte.js';
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
/** The browser assistant from `createOverviewAssistant`. */
|
|
18
|
+
assistant: OverviewAssistant;
|
|
19
|
+
/** Message for a batch (default: a generic "updated" line). */
|
|
20
|
+
message?: (batch: OverviewAssistantBatch) => string;
|
|
21
|
+
/** Extra class names for the affordance's row, for host placement. */
|
|
22
|
+
class?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let { assistant, message, class: className = '' }: Props = $props();
|
|
26
|
+
const { t } = useI18n();
|
|
27
|
+
const batch = $derived(assistant.lastBatch);
|
|
28
|
+
let outcome = $state('');
|
|
29
|
+
|
|
30
|
+
function undo(): void {
|
|
31
|
+
const current = batch;
|
|
32
|
+
if (!current) return;
|
|
33
|
+
const result = assistant.undo(current.id);
|
|
34
|
+
outcome = result.ok
|
|
35
|
+
? t(M['ui.overview.assistant_undone'])
|
|
36
|
+
: result.reason === 'changed_since'
|
|
37
|
+
? t(M['ui.overview.assistant_changed_since'])
|
|
38
|
+
: t(M['ui.overview.assistant_undo_failed']);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
$effect(() => {
|
|
42
|
+
// A new batch replaces the previous outcome line.
|
|
43
|
+
if (batch) outcome = '';
|
|
44
|
+
});
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<div class="smrt-overview-assistant-undo {className}" data-active={batch ? '' : undefined}>
|
|
48
|
+
<!-- The live region stays mounted so each new message is announced. -->
|
|
49
|
+
<span class="smrt-overview-assistant-undo__message" role="status" aria-live="polite">
|
|
50
|
+
{#if batch}
|
|
51
|
+
{message ? message(batch) : t(M['ui.overview.assistant_applied'])}
|
|
52
|
+
{:else}
|
|
53
|
+
{outcome}
|
|
54
|
+
{/if}
|
|
55
|
+
</span>
|
|
56
|
+
{#if batch}
|
|
57
|
+
<Button size="sm" variant="secondary" onclick={undo}>
|
|
58
|
+
{t(M['ui.overview.assistant_undo'])}
|
|
59
|
+
</Button>
|
|
60
|
+
<Button size="sm" variant="ghost" onclick={() => assistant.dismiss()}>
|
|
61
|
+
{t(M['ui.overview.assistant_dismiss'])}
|
|
62
|
+
</Button>
|
|
63
|
+
{/if}
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<style>
|
|
67
|
+
.smrt-overview-assistant-undo {
|
|
68
|
+
display: flex;
|
|
69
|
+
flex-wrap: wrap;
|
|
70
|
+
align-items: center;
|
|
71
|
+
gap: var(--smrt-spacing-2, 0.5rem);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.smrt-overview-assistant-undo__message {
|
|
75
|
+
flex: 1 1 12rem;
|
|
76
|
+
}
|
|
77
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { OverviewAssistant, OverviewAssistantBatch } from './assistant.svelte.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** The browser assistant from `createOverviewAssistant`. */
|
|
4
|
+
assistant: OverviewAssistant;
|
|
5
|
+
/** Message for a batch (default: a generic "updated" line). */
|
|
6
|
+
message?: (batch: OverviewAssistantBatch) => string;
|
|
7
|
+
/** Extra class names for the affordance's row, for host placement. */
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
declare const OverviewAssistantUndo: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type OverviewAssistantUndo = ReturnType<typeof OverviewAssistantUndo>;
|
|
12
|
+
export default OverviewAssistantUndo;
|
|
13
|
+
//# sourceMappingURL=OverviewAssistantUndo.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OverviewAssistantUndo.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/overview/OverviewAssistantUndo.svelte.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,iBAAiB,EACjB,sBAAsB,EACvB,MAAM,uBAAuB,CAAC;AAG/B,UAAU,KAAK;IACb,4DAA4D;IAC5D,SAAS,EAAE,iBAAiB,CAAC;IAC7B,+DAA+D;IAC/D,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,MAAM,CAAC;IACpD,sEAAsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAsDD,QAAA,MAAM,qBAAqB,2CAAwC,CAAC;AACpE,KAAK,qBAAqB,GAAG,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC;AACtE,eAAe,qBAAqB,CAAC"}
|