@happyvertical/smrt-content 0.44.1 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/agents/content-list.md +32 -9
  2. package/dist/chunks/{content-query-D1bLBzMT.js → content-query-BV41W2Dq.js} +4 -4
  3. package/dist/chunks/{content-query-D1bLBzMT.js.map → content-query-BV41W2Dq.js.map} +1 -1
  4. package/dist/chunks/{content-versions-B0OUI1ut.js → content-versions-dZSBU8UF.js} +2 -2
  5. package/dist/chunks/{content-versions-B0OUI1ut.js.map → content-versions-dZSBU8UF.js.map} +1 -1
  6. package/dist/chunks/{contents-CRG2Snrb.js → contents-Ba7JHu2K.js} +2 -2
  7. package/dist/chunks/{contents-CRG2Snrb.js.map → contents-Ba7JHu2K.js.map} +1 -1
  8. package/dist/index.js +4 -4
  9. package/dist/manifest.json +2079 -4183
  10. package/dist/server/content-list-data-surface.d.ts +29 -0
  11. package/dist/server/content-list-data-surface.d.ts.map +1 -0
  12. package/dist/server.d.ts +1 -0
  13. package/dist/server.d.ts.map +1 -1
  14. package/dist/server.js +60 -2
  15. package/dist/server.js.map +1 -1
  16. package/dist/smrt-knowledge.json +4230 -2461
  17. package/dist/svelte/components/ContentList.svelte +346 -19
  18. package/dist/svelte/components/ContentList.svelte.d.ts +1 -1
  19. package/dist/svelte/components/ContentList.svelte.d.ts.map +1 -1
  20. package/dist/svelte/components/ContentListLifecyclePanel.svelte +52 -6
  21. package/dist/svelte/components/ContentListLifecyclePanel.svelte.d.ts.map +1 -1
  22. package/dist/svelte/content-list-controller.d.ts +10 -0
  23. package/dist/svelte/content-list-controller.d.ts.map +1 -1
  24. package/dist/svelte/content-list-controller.js +111 -26
  25. package/dist/svelte/content-list-data-surface.d.ts +45 -0
  26. package/dist/svelte/content-list-data-surface.d.ts.map +1 -0
  27. package/dist/svelte/content-list-data-surface.js +196 -0
  28. package/dist/svelte/index.d.ts +1 -0
  29. package/dist/svelte/index.d.ts.map +1 -1
  30. package/dist/svelte/index.js +1 -0
  31. package/package.json +15 -15
@@ -50,6 +50,11 @@ import {
50
50
  readContentListLifecycleSummary,
51
51
  reconcileContentListLifecycleSelection,
52
52
  } from '../content-list-lifecycle.js';
53
+ import {
54
+ registerContentListDataSurface,
55
+ type ContentListSurfaceContext,
56
+ type ContentListSurfaceHandle,
57
+ } from '../content-list-data-surface.js';
53
58
  import {
54
59
  applyContentListFilter,
55
60
  buildContentListColumns,
@@ -133,6 +138,104 @@ import ImageThumbnail from './ImageThumbnail.svelte';
133
138
 
134
139
  const { t } = useI18n();
135
140
 
141
+ const DEFAULT_CONTENT_LIST_SURFACE_IDENTITY: DataSurfaceIdentity = {
142
+ surfaceId: 'content-list',
143
+ kind: 'table',
144
+ };
145
+
146
+ function sameSurfaceIdentity(
147
+ left: DataSurfaceIdentity,
148
+ right: DataSurfaceIdentity,
149
+ ): boolean {
150
+ return (
151
+ left.kind === right.kind &&
152
+ left.surfaceId === right.surfaceId &&
153
+ left.subject?.type === right.subject?.type &&
154
+ left.subject?.id === right.subject?.id
155
+ );
156
+ }
157
+
158
+ function resolveSurfaceAuthority(
159
+ lifecycle: ContentListLifecycleBinding | undefined,
160
+ workflows: ContentListWorkflowBinding | undefined,
161
+ ): {
162
+ identity?: DataSurfaceIdentity;
163
+ maxSelectionSize?: number;
164
+ lifecycle?: ContentListLifecycleBinding;
165
+ workflows?: ContentListWorkflowBinding;
166
+ } {
167
+ const lifecycleIdentity = lifecycle
168
+ ? (lifecycle.identity ?? DEFAULT_CONTENT_LIST_SURFACE_IDENTITY)
169
+ : undefined;
170
+ const workflowIdentity = workflows
171
+ ? (workflows.identity ?? DEFAULT_CONTENT_LIST_SURFACE_IDENTITY)
172
+ : undefined;
173
+ if (
174
+ lifecycleIdentity &&
175
+ workflowIdentity &&
176
+ !sameSurfaceIdentity(lifecycleIdentity, workflowIdentity)
177
+ ) {
178
+ throw new Error(
179
+ 'ContentList lifecycle and workflow bindings must use the same data surface identity',
180
+ );
181
+ }
182
+ const bindings = [lifecycle, workflows].filter(
183
+ (binding): binding is ContentListLifecycleBinding | ContentListWorkflowBinding =>
184
+ binding !== undefined,
185
+ );
186
+ const maxSelectionSize =
187
+ bindings.length > 0
188
+ ? Math.min(...bindings.map((binding) => binding.maxSelectionSize ?? 200))
189
+ : undefined;
190
+ return {
191
+ ...(lifecycleIdentity ?? workflowIdentity
192
+ ? { identity: lifecycleIdentity ?? workflowIdentity }
193
+ : {}),
194
+ ...(maxSelectionSize === undefined ? {} : { maxSelectionSize }),
195
+ ...(lifecycle && lifecycleIdentity
196
+ ? {
197
+ lifecycle: {
198
+ ...lifecycle,
199
+ identity: lifecycleIdentity,
200
+ ...(maxSelectionSize === undefined ? {} : { maxSelectionSize }),
201
+ },
202
+ }
203
+ : {}),
204
+ ...(workflows && workflowIdentity
205
+ ? {
206
+ workflows: {
207
+ ...workflows,
208
+ identity: workflowIdentity,
209
+ ...(maxSelectionSize === undefined ? {} : { maxSelectionSize }),
210
+ },
211
+ }
212
+ : {}),
213
+ };
214
+ }
215
+
216
+ function reconcileSurfaceDescriptor(
217
+ descriptor: import('@happyvertical/smrt-ui/data').DataSurfaceDescriptor,
218
+ authority: ReturnType<typeof resolveSurfaceAuthority>,
219
+ ): import('@happyvertical/smrt-ui/data').DataSurfaceDescriptor {
220
+ if (
221
+ authority.identity &&
222
+ !sameSurfaceIdentity(descriptor.identity, authority.identity)
223
+ ) {
224
+ throw new Error(
225
+ 'ContentList custom data surface descriptor must use the lifecycle and workflow identity',
226
+ );
227
+ }
228
+ if (
229
+ authority.maxSelectionSize !== undefined &&
230
+ descriptor.limits.maxSelectionSize !== authority.maxSelectionSize
231
+ ) {
232
+ throw new Error(
233
+ 'ContentList custom data surface descriptor must use the lifecycle and workflow selection limit',
234
+ );
235
+ }
236
+ return descriptor;
237
+ }
238
+
136
239
  const WORKFLOW_LABEL_MESSAGES: Record<
137
240
  ContentListWorkflowId,
138
241
  keyof typeof M
@@ -324,7 +427,7 @@ interface Props {
324
427
  error?: string | null;
325
428
  /** Retry affordance rendered with an error. */
326
429
  onRetry?: () => void;
327
- /** Opt-in agent addressability. Non-table presentations land with #2456. */
430
+ /** Opt-in agent addressability shared by every list presentation. */
328
431
  dataSurface?: ContentListDataSurface;
329
432
  /**
330
433
  * Opt-in server-backed rows (#2452). `bind()` is called exactly once, during
@@ -370,6 +473,10 @@ let {
370
473
  lifecycleMode = 'active',
371
474
  }: Props = $props();
372
475
 
476
+ const surfaceAuthority = $derived(
477
+ resolveSurfaceAuthority(lifecycle, workflows),
478
+ );
479
+
373
480
  const initialQuery = untrack(() => query);
374
481
 
375
482
  // One controller owns search, filters, sorting, paging, and selection for
@@ -471,6 +578,14 @@ let settledLifecycleQuery = $state<ContentListDataQueryRequest | undefined>(
471
578
  undefined,
472
579
  );
473
580
  let settledLifecycleFingerprint = $state<string | undefined>(undefined);
581
+ let settledSurfaceMetadata = $state<
582
+ | {
583
+ signature: string;
584
+ queryFingerprint: string | null;
585
+ totalRows: number | null;
586
+ }
587
+ | undefined
588
+ >(undefined);
474
589
 
475
590
  function toSearchParams(
476
591
  input: URLSearchParams | string | null | undefined,
@@ -563,6 +678,10 @@ let completedJobs = $state<ContentListJob[]>([]);
563
678
  let completedJobsOverflowed = $state(false);
564
679
  let activeQueryKey = $state<string | undefined>(undefined);
565
680
  let lifecycleRefreshPending = $state(false);
681
+ let surfaceRoot: HTMLDivElement | undefined;
682
+ let surfaceHighlighted = $state(false);
683
+ let surfaceHighlightTimeout: ReturnType<typeof setTimeout> | undefined;
684
+ let surfaceHandle: ContentListSurfaceHandle | undefined;
566
685
 
567
686
  // Job state is subscribed once so hosts can use a framework-free controller.
568
687
  // Only transitions observed after the initial snapshot are completion events;
@@ -924,9 +1043,20 @@ function settleWorkflowQueryResult(
924
1043
  signature: string,
925
1044
  ): void {
926
1045
  settledSignature = signature;
1046
+ const envelope =
1047
+ result !== null && typeof result === 'object' && !Array.isArray(result)
1048
+ ? (result as Record<string, unknown>)
1049
+ : undefined;
1050
+ settledSurfaceMetadata = {
1051
+ signature,
1052
+ queryFingerprint:
1053
+ typeof envelope?.queryFingerprint === 'string'
1054
+ ? envelope.queryFingerprint
1055
+ : null,
1056
+ totalRows: surfaceMetadataTotalRows(envelope?.total),
1057
+ };
927
1058
  resultNotices = readContentListQueryNotices(result);
928
- if (result !== null && typeof result === 'object' && !Array.isArray(result)) {
929
- const envelope = result as Record<string, unknown>;
1059
+ if (envelope) {
930
1060
  settledWorkflowQuery = request;
931
1061
  settledWorkflowFingerprint =
932
1062
  typeof envelope.queryFingerprint === 'string'
@@ -947,6 +1077,17 @@ function settleWorkflowQueryResult(
947
1077
  settledWorkflowRevision = null;
948
1078
  }
949
1079
 
1080
+ function surfaceMetadataTotalRows(total: unknown): number | null {
1081
+ if (!total || typeof total !== 'object' || Array.isArray(total)) return null;
1082
+ const record = total as Record<string, unknown>;
1083
+ return (record.kind === 'exact' || record.kind === 'estimated') &&
1084
+ typeof record.value === 'number' &&
1085
+ Number.isSafeInteger(record.value) &&
1086
+ record.value >= 0
1087
+ ? record.value
1088
+ : null;
1089
+ }
1090
+
950
1091
  function settleLifecycleResult(
951
1092
  result: unknown,
952
1093
  request: ContentListDataQueryRequest | undefined,
@@ -1205,7 +1346,7 @@ const canSelectAllMatching = $derived(
1205
1346
  settledWorkflowRevision &&
1206
1347
  exactMatchingCount !== undefined &&
1207
1348
  exactMatchingCount > 0 &&
1208
- exactMatchingCount <= (workflows?.maxSelectionSize ?? 200),
1349
+ exactMatchingCount <= (surfaceAuthority.maxSelectionSize ?? 200),
1209
1350
  ),
1210
1351
  );
1211
1352
  const workflowPayloadValid = $derived(
@@ -1385,20 +1526,30 @@ const surfaceOptions = $derived(
1385
1526
  ? {
1386
1527
  registry: dataSurface.registry,
1387
1528
  descriptor:
1388
- dataSurface.descriptor ??
1529
+ dataSurface.descriptor
1530
+ ? reconcileSurfaceDescriptor(
1531
+ dataSurface.descriptor,
1532
+ surfaceAuthority,
1533
+ )
1534
+ :
1389
1535
  (() => {
1390
1536
  const descriptor = buildContentListSurfaceDescriptor({
1391
1537
  columnLabels,
1538
+ serverBacked: initialQuery !== undefined,
1539
+ lifecycle: lifecycle !== undefined,
1540
+ lifecycleMode,
1392
1541
  includeWorkflows: workflows !== undefined,
1542
+ refresh: queryBinding?.refresh !== undefined,
1543
+ retry: retryHandler !== undefined,
1393
1544
  });
1394
1545
  return {
1395
1546
  ...descriptor,
1396
- identity: workflows?.identity ?? descriptor.identity,
1547
+ identity: surfaceAuthority.identity ?? descriptor.identity,
1397
1548
  limits: {
1398
1549
  ...descriptor.limits,
1399
- ...(workflows?.maxSelectionSize === undefined
1550
+ ...(surfaceAuthority.maxSelectionSize === undefined
1400
1551
  ? {}
1401
- : { maxSelectionSize: workflows.maxSelectionSize }),
1552
+ : { maxSelectionSize: surfaceAuthority.maxSelectionSize }),
1402
1553
  },
1403
1554
  };
1404
1555
  })(),
@@ -1440,6 +1591,164 @@ const queryWarnings = $derived<ReadonlyArray<string>>(
1440
1591
  boundResultNotices?.warnings ??
1441
1592
  resultNotices.warnings,
1442
1593
  );
1594
+
1595
+ function surfaceLastUpdated(value: unknown): string | null {
1596
+ if (value instanceof Date) return value.toISOString();
1597
+ if (typeof value === 'string') return value;
1598
+ if (typeof value === 'number' && Number.isFinite(value)) {
1599
+ return new Date(value).toISOString();
1600
+ }
1601
+ return null;
1602
+ }
1603
+
1604
+ const matchingSurfaceMetadata = $derived(
1605
+ serverBacked && settledSurfaceMetadata?.signature === querySignature
1606
+ ? settledSurfaceMetadata
1607
+ : undefined,
1608
+ );
1609
+
1610
+ const surfaceContext = $derived<ContentListSurfaceContext>({
1611
+ viewMode,
1612
+ queryFingerprint: serverBacked
1613
+ ? (matchingSurfaceMetadata?.queryFingerprint ?? null)
1614
+ : null,
1615
+ totalRows: serverBacked
1616
+ ? (matchingSurfaceMetadata?.totalRows ?? null)
1617
+ : Number.isSafeInteger(totalRowCount)
1618
+ ? totalRowCount
1619
+ : null,
1620
+ freshness: {
1621
+ stale,
1622
+ refreshing,
1623
+ offline,
1624
+ lastUpdated: surfaceLastUpdated(lastUpdated),
1625
+ truncated: queryTruncated,
1626
+ warnings: queryWarnings,
1627
+ },
1628
+ });
1629
+
1630
+ function focusSurface(): void {
1631
+ if (!surfaceRoot) return;
1632
+ surfaceRoot.tabIndex = -1;
1633
+ surfaceRoot.focus();
1634
+ }
1635
+
1636
+ // ContentList owns the registration so grid, detailed, compact, empty, and
1637
+ // loading presentations all expose one stable identity and revision stream.
1638
+ // This ref is deliberately non-reactive: registration input changes must
1639
+ // advance a stable identity revision, but normal command acknowledgements must
1640
+ // not themselves cause a registration teardown/replay window.
1641
+ const surfaceRevision = { value: undefined as number | undefined };
1642
+
1643
+ function acceptsSurfaceTableCommand(
1644
+ command: import('@happyvertical/smrt-ui/data').DataTableCommand,
1645
+ ): boolean {
1646
+ if (command.type === 'setPageSize' && serverPageSize !== null) {
1647
+ if (command.pageSize === null || command.pageSize > maxPageSize) return false;
1648
+ }
1649
+ if (command.type === 'setPage') {
1650
+ if (command.page > maxReachablePage) return false;
1651
+ if (
1652
+ tableState.pageSize !== null &&
1653
+ clampableRowCount !== undefined &&
1654
+ // A server binding retains the previous query's exact total until its
1655
+ // replacement settles. That count cannot bound a command for the new
1656
+ // query: it may be smaller, and would reject a valid page before the
1657
+ // new query gets a chance to report its own total.
1658
+ (!serverBacked || settledSignature === querySignature) &&
1659
+ command.page > Math.max(1, Math.ceil(clampableRowCount / tableState.pageSize))
1660
+ )
1661
+ return false;
1662
+ }
1663
+ if (command.type === 'setFilters') {
1664
+ if (
1665
+ lockedType !== null &&
1666
+ !isContentListFilterExactly(
1667
+ { ...tableState, filters: command.filters },
1668
+ CONTENT_LIST_TYPE_FILTER_ID,
1669
+ lockedType,
1670
+ )
1671
+ )
1672
+ return false;
1673
+ if (
1674
+ lifecycleMode === 'trash' &&
1675
+ !isContentListFilterExactly(
1676
+ { ...tableState, filters: command.filters },
1677
+ CONTENT_LIST_STATUS_FILTER_ID,
1678
+ 'deleted',
1679
+ )
1680
+ )
1681
+ return false;
1682
+ }
1683
+ return command.type !== 'reset' || (lockedType === null && lifecycleMode !== 'trash');
1684
+ }
1685
+
1686
+ $effect(() => {
1687
+ const surface = surfaceOptions;
1688
+ if (!surface) {
1689
+ surfaceHandle = undefined;
1690
+ return;
1691
+ }
1692
+ const handle = registerContentListDataSurface({
1693
+ ...surface,
1694
+ controller,
1695
+ context: untrack(() => surfaceContext),
1696
+ initialRevision:
1697
+ surfaceRevision.value === undefined ? 0 : surfaceRevision.value + 1,
1698
+ onRevision: (revision) => {
1699
+ surfaceRevision.value = revision;
1700
+ },
1701
+ acceptsTableCommand: acceptsSurfaceTableCommand,
1702
+ setViewMode: (next) => {
1703
+ viewMode = next;
1704
+ },
1705
+ ...(queryBinding?.refresh
1706
+ ? {
1707
+ refresh: async () => {
1708
+ await refreshQuery();
1709
+ return true;
1710
+ },
1711
+ }
1712
+ : {}),
1713
+ ...(retryHandler
1714
+ ? {
1715
+ retry: async () => {
1716
+ await retryHandler();
1717
+ return true;
1718
+ },
1719
+ }
1720
+ : {}),
1721
+ focus: focusSurface,
1722
+ reveal: () => surfaceRoot?.scrollIntoView({ block: 'nearest' }),
1723
+ highlight: () => {
1724
+ if (surfaceHighlightTimeout !== undefined) {
1725
+ clearTimeout(surfaceHighlightTimeout);
1726
+ }
1727
+ surfaceHighlighted = true;
1728
+ const timeout = setTimeout(() => {
1729
+ if (surfaceHandle === handle) surfaceHighlighted = false;
1730
+ if (surfaceHighlightTimeout === timeout) {
1731
+ surfaceHighlightTimeout = undefined;
1732
+ }
1733
+ }, 1_000);
1734
+ surfaceHighlightTimeout = timeout;
1735
+ },
1736
+ });
1737
+ surfaceHandle = handle;
1738
+ return () => {
1739
+ if (surfaceHandle === handle) surfaceHandle = undefined;
1740
+ if (surfaceHighlightTimeout !== undefined) {
1741
+ clearTimeout(surfaceHighlightTimeout);
1742
+ surfaceHighlightTimeout = undefined;
1743
+ }
1744
+ surfaceHighlighted = false;
1745
+ handle.destroy();
1746
+ };
1747
+ });
1748
+
1749
+ $effect(() => {
1750
+ surfaceHandle?.update(surfaceContext);
1751
+ });
1443
1752
  /** Identity of the current set of refusals, so a dismissal is not permanent. */
1444
1753
  const dropNoticeKey = $derived(
1445
1754
  dropNotices.length > 0 || queryTruncated || queryWarnings.length > 0
@@ -1734,6 +2043,16 @@ function workflowSelection(): ContentListWorkflowRequest['selection'] | null {
1734
2043
  return { scope: 'explicit-ids', rowIds: effectiveRowIds };
1735
2044
  }
1736
2045
 
2046
+ function workflowSelectionCount(
2047
+ selection: ContentListWorkflowRequest['selection'],
2048
+ ): number {
2049
+ if (selection.scope === 'explicit-ids') return selection.rowIds.length;
2050
+ if (selection.scope === 'current-page') return selectablePageRowIds.length;
2051
+ return tableState.selection.scope === 'allMatching'
2052
+ ? tableState.selection.expectedCount
2053
+ : 0;
2054
+ }
2055
+
1737
2056
  function workflowJobTarget(
1738
2057
  request: ContentListWorkflowRequest,
1739
2058
  ): ContentListJobTarget {
@@ -1774,6 +2093,8 @@ function createWorkflowRequest(
1774
2093
  ): ContentListWorkflowRequest | null {
1775
2094
  const selection = workflowSelection();
1776
2095
  if (!selection) return null;
2096
+ const selectionCount = workflowSelectionCount(selection);
2097
+ if (selectionCount > (surfaceAuthority.maxSelectionSize ?? 200)) return null;
1777
2098
  const queryRequired = selection.scope !== 'explicit-ids';
1778
2099
  if (
1779
2100
  queryRequired &&
@@ -1786,7 +2107,8 @@ function createWorkflowRequest(
1786
2107
  return {
1787
2108
  version: 1,
1788
2109
  requestId: globalThis.crypto?.randomUUID?.() ?? `content-workflow-${Date.now()}`,
1789
- identity: workflows?.identity ?? { surfaceId: 'content-list', kind: 'table' },
2110
+ identity:
2111
+ surfaceAuthority.identity ?? DEFAULT_CONTENT_LIST_SURFACE_IDENTITY,
1790
2112
  actionId: selectedWorkflow,
1791
2113
  phase,
1792
2114
  selection,
@@ -1796,10 +2118,7 @@ function createWorkflowRequest(
1796
2118
  ...(queryRequired && settledWorkflowQuery
1797
2119
  ? { query: settledWorkflowQuery }
1798
2120
  : {}),
1799
- expectedCount:
1800
- selection.scope === 'explicit-ids'
1801
- ? selection.rowIds.length
1802
- : selectedCount,
2121
+ expectedCount: selectionCount,
1803
2122
  },
1804
2123
  };
1805
2124
  }
@@ -2380,7 +2699,11 @@ const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
2380
2699
  </div>
2381
2700
  {/snippet}
2382
2701
 
2383
- <div class="content-list-wrapper">
2702
+ <div
2703
+ class="content-list-wrapper"
2704
+ class:data-surface-highlighted={surfaceHighlighted}
2705
+ bind:this={surfaceRoot}
2706
+ >
2384
2707
 
2385
2708
  <div class="content-controls">
2386
2709
  <div class="search-filters">
@@ -2726,7 +3049,7 @@ const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
2726
3049
 
2727
3050
  {#if lifecycle}
2728
3051
  <ContentListLifecyclePanel
2729
- binding={lifecycle}
3052
+ binding={surfaceAuthority.lifecycle ?? lifecycle}
2730
3053
  mode={lifecycleMode}
2731
3054
  selectedRowIds={tableState.selectedRowIds}
2732
3055
  query={lifecycleQuery}
@@ -2855,9 +3178,9 @@ const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
2855
3178
 
2856
3179
  {#if viewMode === 'compact'}
2857
3180
  <!--
2858
- The compact table stays mounted for empty and loading results: it owns
2859
- the mounted data surface, so unmounting it on a zero-row query would
2860
- unregister the surface and leave an agent unable to undo its own search.
3181
+ The compact table stays mounted for empty and loading results so its
3182
+ table semantics and operator affordances remain stable across queries.
3183
+ ContentList itself owns the agent-addressable surface in every view.
2861
3184
  -->
2862
3185
  <div class="content-table-wrapper">
2863
3186
  <DataTable
@@ -2870,7 +3193,6 @@ const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
2870
3193
  loading={isLoading}
2871
3194
  caption={t(M['content.content_list.table_caption'])}
2872
3195
  rowLabel={(row: ContentListRow) => row.title}
2873
- dataSurface={surfaceOptions}
2874
3196
  empty={tableEmptyState}
2875
3197
  />
2876
3198
  </div>
@@ -3108,6 +3430,11 @@ const tableColumns: DataTableColumn<ContentListRow>[] = $derived([
3108
3430
  width: 100%;
3109
3431
  }
3110
3432
 
3433
+ .content-list-wrapper.data-surface-highlighted {
3434
+ outline: 2px solid var(--smrt-color-primary);
3435
+ outline-offset: 0.25rem;
3436
+ }
3437
+
3111
3438
  .content-controls {
3112
3439
  display: flex;
3113
3440
  justify-content: space-between;
@@ -60,7 +60,7 @@ interface Props {
60
60
  error?: string | null;
61
61
  /** Retry affordance rendered with an error. */
62
62
  onRetry?: () => void;
63
- /** Opt-in agent addressability. Non-table presentations land with #2456. */
63
+ /** Opt-in agent addressability shared by every list presentation. */
64
64
  dataSurface?: ContentListDataSurface;
65
65
  /**
66
66
  * Opt-in server-backed rows (#2452). `bind()` is called exactly once, during
@@ -1 +1 @@
1
- {"version":3,"file":"ContentList.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/ContentList.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,IAAI,6BAA6B,EAAE,MAAM,6BAA6B,CAAC;AACvG,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAE/E;;;;;;;GAOG;AACH,MAAM,WAAW,0BAA0B;IACzC;;;OAGG;IACH,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,GAAG,IAAI,CAAC;IACzC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CACT,MAAM,EAAE,eAAe,EACvB,KAAK,EAAE,6BAA6B,KACjC,IAAI,CAAC;IACV,uEAAuE;IACvE,OAAO,CAAC,EAAE,0BAA0B,CAAC;CACtC;AAcD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,8BAA8B,CAAC;AAKhF,OAAO,EASL,KAAK,sBAAsB,EAE3B,KAAK,mBAAmB,EAiBzB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAOL,KAAK,sBAAsB,EAU5B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAGL,KAAK,qBAAqB,EAG3B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAEL,KAAK,yBAAyB,EAG/B,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACL,KAAK,0BAA0B,EAOhC,MAAM,8BAA8B,CAAC;AAiBtC,UAAU,KAAK;IACb,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAC;IACzB,sEAAsE;IACtE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,6DAA6D;IAC7D,MAAM,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IACvC,+DAA+D;IAC/D,QAAQ,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IACzC,4DAA4D;IAC5D,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,MAAM,GAAG,IAAI,CAAC;IACtD,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,sBAAsB,CAAC;IACrC;;;;;OAKG;IACH,KAAK,CAAC,EAAE,sBAAsB,CAAC;IAC/B,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,0BAA0B,CAAC;IACtC,oFAAoF;IACpF,UAAU,CAAC,EAAE,yBAAyB,CAAC;IACvC,6EAA6E;IAC7E,IAAI,CAAC,EAAE,qBAAqB,CAAC;IAC7B,6EAA6E;IAC7E,SAAS,CAAC,EAAE,0BAA0B,CAAC;IACvC,6EAA6E;IAC7E,SAAS,CAAC,EAAE,2BAA2B,CAAC;IACxC,2EAA2E;IAC3E,aAAa,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;CACpC;AAgnFD,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"ContentList.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/ContentList.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,IAAI,6BAA6B,EAAE,MAAM,6BAA6B,CAAC;AACvG,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAE/E;;;;;;;GAOG;AACH,MAAM,WAAW,0BAA0B;IACzC;;;OAGG;IACH,MAAM,CAAC,EAAE,eAAe,GAAG,MAAM,GAAG,IAAI,CAAC;IACzC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CACT,MAAM,EAAE,eAAe,EACvB,KAAK,EAAE,6BAA6B,KACjC,IAAI,CAAC;IACV,uEAAuE;IACvE,OAAO,CAAC,EAAE,0BAA0B,CAAC;CACtC;AAcD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,8BAA8B,CAAC;AAUhF,OAAO,EASL,KAAK,sBAAsB,EAE3B,KAAK,mBAAmB,EAiBzB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAOL,KAAK,sBAAsB,EAU5B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAGL,KAAK,qBAAqB,EAG3B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAEL,KAAK,yBAAyB,EAG/B,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACL,KAAK,0BAA0B,EAOhC,MAAM,8BAA8B,CAAC;AAiBtC,UAAU,KAAK;IACb,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAC;IACzB,sEAAsE;IACtE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,6DAA6D;IAC7D,MAAM,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IACvC,+DAA+D;IAC/D,QAAQ,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IACzC,4DAA4D;IAC5D,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,MAAM,GAAG,IAAI,CAAC;IACtD,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,qEAAqE;IACrE,WAAW,CAAC,EAAE,sBAAsB,CAAC;IACrC;;;;;OAKG;IACH,KAAK,CAAC,EAAE,sBAAsB,CAAC;IAC/B,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,0BAA0B,CAAC;IACtC,oFAAoF;IACpF,UAAU,CAAC,EAAE,yBAAyB,CAAC;IACvC,6EAA6E;IAC7E,IAAI,CAAC,EAAE,qBAAqB,CAAC;IAC7B,6EAA6E;IAC7E,SAAS,CAAC,EAAE,0BAA0B,CAAC;IACvC,6EAA6E;IAC7E,SAAS,CAAC,EAAE,2BAA2B,CAAC;IACxC,2EAA2E;IAC3E,aAAa,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;CACpC;AA26FD,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -42,7 +42,7 @@ let {
42
42
  oncomplete,
43
43
  }: Props = $props();
44
44
 
45
- const controller = createContentListLifecycleController(untrack(() => binding));
45
+ let controller = createContentListLifecycleController(untrack(() => binding));
46
46
  let snapshot = $state<ContentListLifecycleSnapshot>(controller.snapshot());
47
47
  let lastResult = $state<ContentListLifecycleSnapshot['summary']>();
48
48
  let dialogOpen = $state(false);
@@ -50,10 +50,50 @@ let countConfirmation = $state('');
50
50
  let restoreStatus = $state<ContentListRestoreStatus>('draft');
51
51
  let intent = $state<'selected' | 'all-matching'>('selected');
52
52
 
53
- const unsubscribe = controller.subscribe((next) => {
53
+ let unsubscribe = controller.subscribe((next) => {
54
54
  snapshot = next;
55
55
  });
56
- onDestroy(unsubscribe);
56
+
57
+ function sameBindingAuthority(
58
+ left: ContentListLifecycleBinding,
59
+ right: ContentListLifecycleBinding,
60
+ ): boolean {
61
+ return (
62
+ left.client === right.client &&
63
+ left.client.preview === right.client.preview &&
64
+ left.client.apply === right.client.apply &&
65
+ left.revision === right.revision &&
66
+ left.maxSelectionSize === right.maxSelectionSize &&
67
+ left.identity?.kind === right.identity?.kind &&
68
+ left.identity?.surfaceId === right.identity?.surfaceId &&
69
+ left.identity?.subject?.type === right.identity?.subject?.type &&
70
+ left.identity?.subject?.id === right.identity?.subject?.id
71
+ );
72
+ }
73
+
74
+ let installedBinding = untrack(() => binding);
75
+
76
+ function replaceController(nextBinding: ContentListLifecycleBinding): void {
77
+ unsubscribe();
78
+ controller.reset();
79
+ controller = createContentListLifecycleController(nextBinding);
80
+ snapshot = controller.snapshot();
81
+ lastResult = undefined;
82
+ dialogOpen = false;
83
+ countConfirmation = '';
84
+ unsubscribe = controller.subscribe((next) => {
85
+ snapshot = next;
86
+ });
87
+ }
88
+
89
+ $effect(() => {
90
+ const nextBinding = binding;
91
+ if (sameBindingAuthority(installedBinding, nextBinding)) return;
92
+ replaceController(nextBinding);
93
+ installedBinding = nextBinding;
94
+ });
95
+
96
+ onDestroy(() => unsubscribe());
57
97
 
58
98
  $effect(() => {
59
99
  controller.invalidate(viewKey);
@@ -70,6 +110,7 @@ const busy = $derived(
70
110
  );
71
111
  const selectedCount = $derived(selectedRowIds.length);
72
112
  const selectionLimit = $derived(binding.maxSelectionSize ?? 200);
113
+ const selectedSelectionTooLarge = $derived(selectedCount > selectionLimit);
73
114
  const matchingSelectionTooLarge = $derived(
74
115
  exactMatchingCount !== undefined && exactMatchingCount > selectionLimit,
75
116
  );
@@ -92,6 +133,11 @@ async function begin(
92
133
  actionId: ContentListLifecycleActionId,
93
134
  scope: 'selected' | 'all-matching' = 'selected',
94
135
  ) {
136
+ if (
137
+ (scope === 'selected' && selectedSelectionTooLarge) ||
138
+ (scope === 'all-matching' && matchingSelectionTooLarge)
139
+ )
140
+ return;
95
141
  intent = scope;
96
142
  countConfirmation = '';
97
143
  dialogOpen = true;
@@ -136,7 +182,7 @@ function retryPreview() {
136
182
  variant="ghost"
137
183
  size="sm"
138
184
  type="button"
139
- disabled={selectedCount === 0 || busy}
185
+ disabled={selectedCount === 0 || selectedSelectionTooLarge || busy}
140
186
  onclick={() => begin('move-to-trash')}
141
187
  >{t(M['content.content_list.move_selected_to_trash'])}</Button>
142
188
  {:else}
@@ -153,14 +199,14 @@ function retryPreview() {
153
199
  variant="ghost"
154
200
  size="sm"
155
201
  type="button"
156
- disabled={selectedCount === 0 || busy}
202
+ disabled={selectedCount === 0 || selectedSelectionTooLarge || busy}
157
203
  onclick={() => begin('restore')}
158
204
  >{t(M['content.content_list.restore_selected'])}</Button>
159
205
  <Button
160
206
  variant="ghost"
161
207
  size="sm"
162
208
  type="button"
163
- disabled={selectedCount === 0 || busy}
209
+ disabled={selectedCount === 0 || selectedSelectionTooLarge || busy}
164
210
  onclick={() => begin('permanent-delete')}
165
211
  >{t(M['content.content_list.delete_selected_permanently'])}</Button>
166
212
  <Button
@@ -1 +1 @@
1
- {"version":3,"file":"ContentListLifecyclePanel.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/ContentListLifecyclePanel.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,uBAAuB,EACvB,gBAAgB,EACjB,MAAM,6BAA6B,CAAC;AAMrC,OAAO,EAEL,KAAK,2BAA2B,EAIjC,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAI5E,UAAU,KAAK;IACb,OAAO,EAAE,2BAA2B,CAAC;IACrC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;IACzB,cAAc,EAAE,gBAAgB,EAAE,CAAC;IACnC,KAAK,CAAC,EAAE,2BAA2B,CAAC;IACpC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,CAAC,MAAM,EAAE,uBAAuB,KAAK,IAAI,CAAC;CACvD;AAkPD,QAAA,MAAM,yBAAyB,2CAAwC,CAAC;AACxE,KAAK,yBAAyB,GAAG,UAAU,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAC9E,eAAe,yBAAyB,CAAC"}
1
+ {"version":3,"file":"ContentListLifecyclePanel.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/ContentListLifecyclePanel.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,uBAAuB,EACvB,gBAAgB,EACjB,MAAM,6BAA6B,CAAC;AAMrC,OAAO,EAEL,KAAK,2BAA2B,EAIjC,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAI5E,UAAU,KAAK;IACb,OAAO,EAAE,2BAA2B,CAAC;IACrC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;IACzB,cAAc,EAAE,gBAAgB,EAAE,CAAC;IACnC,KAAK,CAAC,EAAE,2BAA2B,CAAC;IACpC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,CAAC,MAAM,EAAE,uBAAuB,KAAK,IAAI,CAAC;CACvD;AAgSD,QAAA,MAAM,yBAAyB,2CAAwC,CAAC;AACxE,KAAK,yBAAyB,GAAG,UAAU,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAC9E,eAAe,yBAAyB,CAAC"}
@@ -105,6 +105,16 @@ export interface ContentListSurfaceDescriptorOptions {
105
105
  limits?: Partial<DataSurfaceLimits>;
106
106
  /** Publish bulk workflow actions only when the mounted list can execute them. */
107
107
  includeWorkflows?: boolean;
108
+ /** Narrows discovery to operations the server query translator can execute. */
109
+ serverBacked?: boolean;
110
+ /** Publish the server-authoritative lifecycle actions available to this list. */
111
+ lifecycle?: boolean;
112
+ /** Trash lists expose restore/delete rather than active editing actions. */
113
+ lifecycleMode?: 'active' | 'trash';
114
+ /** A mounted query can refresh the currently visible result. */
115
+ refresh?: boolean;
116
+ /** A mounted query or host callback can retry a failed result. */
117
+ retry?: boolean;
108
118
  }
109
119
  /**
110
120
  * Opt-in agent addressability for the compact table. The host owns the