@proveanything/smartlinks-utils-ui 0.10.7 → 0.10.9

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.
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { ComponentType, ReactNode } from 'react';
3
- import { FacetRule, MatchedAt, RecordTarget, AppRecord, MatchResult, ResolveAllEntry } from '@proveanything/smartlinks/dist/types/appObjects';
2
+ import { FacetRule, MatchedAt, AppRecord, RecordTarget, MatchResult, ResolveAllEntry } from '@proveanything/smartlinks/dist/types/appObjects';
3
+ import { ReactNode, ComponentType } from 'react';
4
4
  import { LucideIcon } from 'lucide-react';
5
5
  import * as _tanstack_query_core from '@tanstack/query-core';
6
6
  import * as _proveanything_smartlinks from '@proveanything/smartlinks';
@@ -93,6 +93,21 @@ interface RecordSummary<TData = unknown> {
93
93
  * browser uses this to render a friendly rule summary as the row subtitle.
94
94
  */
95
95
  facetRule?: FacetRule | null;
96
+ /**
97
+ * Optional visual hint for the leading status icon on the default row
98
+ * renderer. When set, takes precedence over the status-derived icon
99
+ * (configured / inherited / empty). Used by lifecycle rail buckets
100
+ * (success/warning/etc) and any host that wants per-row iconography
101
+ * without supplying a full `renderListRow`.
102
+ */
103
+ iconHint?: ReactNode;
104
+ /**
105
+ * Optional semantic tone for the leading status icon. Drives the icon
106
+ * colour via `ra-status-icon--<tone>` CSS classes. Has no effect unless
107
+ * `iconHint` is also set (or the tone is one that maps to a built-in
108
+ * default icon).
109
+ */
110
+ toneHint?: 'success' | 'warning' | 'danger' | 'muted' | 'info' | 'default';
96
111
  }
97
112
  interface ResolvedRecord<TData = unknown> {
98
113
  data: TData | null;
@@ -962,7 +977,35 @@ interface RailConfig<TData = unknown> {
962
977
  key: string;
963
978
  label: string;
964
979
  icon?: ReactNode;
980
+ /**
981
+ * Whether this bucket should be expanded by default (records-driven
982
+ * accordion mode). Last-write-wins per key. Has no effect in the
983
+ * collection-cardinality lifecycle-rail mode where buckets are flat
984
+ * selectable rows rather than accordion sections.
985
+ */
986
+ defaultOpen?: boolean;
987
+ /**
988
+ * Semantic tone for the bucket — drives a CSS data attribute on the
989
+ * lifecycle bucket row (`data-tone="success" | "warning" | "muted"`).
990
+ * Apps style via `[data-tone="…"]` selectors against existing
991
+ * semantic tokens.
992
+ */
993
+ tone?: 'default' | 'success' | 'warning' | 'muted' | 'danger';
965
994
  } | null;
995
+ /**
996
+ * Initial bucket key to select on first mount of the lifecycle rail
997
+ * (collection cardinality + `'all'` / `'global'` scope + `groupBy`).
998
+ * Ignored when a deep-link state restores a different selection. Pass
999
+ * the same key shape your `groupBy` returns (e.g. `"1-open"`).
1000
+ */
1001
+ defaultGroupKey?: string;
1002
+ /**
1003
+ * EXPERIMENTAL — invoked the first time a lifecycle bucket is selected
1004
+ * (collection-cardinality lifecycle rail). Hosts can use this to trigger
1005
+ * supplemental fetches before the bucket-filtered table renders. May be
1006
+ * called more than once per key. API may change before stabilising.
1007
+ */
1008
+ onGroupExpanded?: (key: string) => void;
966
1009
  /** Visual density. Default `comfortable`. */
967
1010
  density?: 'comfortable' | 'compact';
968
1011
  }
@@ -1020,6 +1063,20 @@ interface ItemsConfig<TData = unknown> {
1020
1063
  cardSize?: 'sm' | 'md' | 'lg';
1021
1064
  /** What the rail shows once an item is open. Default `'siblings'`. */
1022
1065
  railMode?: CollectionRailMode;
1066
+ /**
1067
+ * Optional projector that derives `label` / `subtitle` / `thumbnail`
1068
+ * (and any other `RecordSummary` fields) from a raw record. Runs for
1069
+ * every item in the collection-cardinality right-pane list. The
1070
+ * shell's default tries common keys (`title`, `name`, `label`,
1071
+ * `question`, …) — supply this when your record shape doesn't match
1072
+ * those, or when you want the global / all-items list to read from a
1073
+ * different field.
1074
+ *
1075
+ * `base` already carries the framework-derived fields (id, scope,
1076
+ * status, etc.); return a merged `RecordSummary` (e.g.
1077
+ * `{ ...base, label: rec.data?.headline ?? base.label }`).
1078
+ */
1079
+ toSummary?: (rec: AppRecord, base: RecordSummary<TData>) => RecordSummary<TData>;
1023
1080
  }
1024
1081
  interface UnsavedConfig<TData = unknown> {
1025
1082
  /**
@@ -1135,6 +1192,8 @@ interface Props$f {
1135
1192
  declare const StatusDot: ({ source, status, className }: Props$f) => react_jsx_runtime.JSX.Element;
1136
1193
 
1137
1194
  type StatusTone = 'own' | 'shared' | 'missing';
1195
+ /** Semantic tones used by host-driven iconography (e.g. lifecycle buckets). */
1196
+ type SemanticTone = 'success' | 'warning' | 'danger' | 'muted' | 'info' | 'default';
1138
1197
  interface Props$e {
1139
1198
  source?: RecordSource;
1140
1199
  status?: RecordStatus;
@@ -1143,8 +1202,21 @@ interface Props$e {
1143
1202
  size?: string;
1144
1203
  /** Optional accessible label — defaults to the tone name. */
1145
1204
  label?: string;
1205
+ /**
1206
+ * Host-supplied icon override. Either a React element (rendered as-is)
1207
+ * or any ReactNode. When set, takes precedence over the status-derived
1208
+ * icon. Pair with `semanticTone` to drive colour.
1209
+ */
1210
+ iconHint?: ReactNode;
1211
+ /**
1212
+ * Host-supplied semantic tone (success/warning/danger/muted/info). When
1213
+ * set, drives the icon colour AND — if no `iconHint` is supplied —
1214
+ * picks a sensible default lucide icon per tone (CheckCircle2 / Alert /
1215
+ * XCircle / MinusCircle / Info).
1216
+ */
1217
+ semanticTone?: SemanticTone;
1146
1218
  }
1147
- declare const StatusIcon: ({ source, status, className, size, label }: Props$e) => react_jsx_runtime.JSX.Element;
1219
+ declare const StatusIcon: ({ source, status, className, size, label, iconHint, semanticTone, }: Props$e) => react_jsx_runtime.JSX.Element;
1148
1220
  /** Short label rendered next to / under the row title. */
1149
1221
  declare const statusToneLabel: (tone: StatusTone) => string;
1150
1222
 
@@ -1696,7 +1768,7 @@ declare const useRecordList: (args: UseRecordListArgs) => {
1696
1768
  };
1697
1769
  isLoading: boolean;
1698
1770
  error: Error | null;
1699
- refetch: () => void;
1771
+ refetch: () => Promise<void>;
1700
1772
  hasNextPage: boolean;
1701
1773
  isFetchingNextPage: boolean;
1702
1774
  fetchNextPage: (options?: _tanstack_query_core.FetchNextPageOptions) => Promise<_tanstack_query_core.InfiniteQueryObserverResult<_tanstack_query_core.InfiniteData<{
@@ -1841,7 +1913,7 @@ declare function useCollectionItems<T = unknown>(args: UseCollectionItemsArgs):
1841
1913
  hasMore: boolean;
1842
1914
  nextOffset: number;
1843
1915
  }, unknown>, Error>>;
1844
- refetch: () => void;
1916
+ refetch: () => Promise<void>;
1845
1917
  };
1846
1918
 
1847
1919
  /**
@@ -1,3 +1,4 @@
1
+ import { AdminPageHeader } from '../../chunk-2MW54ZVG.js';
1
2
  import { assertComponentStylesLoaded } from '../../chunk-OLYC54YT.js';
2
3
  import '../../chunk-5UQQYXCX.js';
3
4
  import { FacetRuleEditor } from '../../chunk-JMCV6FOW.js';
@@ -5,8 +6,8 @@ import { useFacets } from '../../chunk-4LHF5JB7.js';
5
6
  import { cn } from '../../chunk-L7FQ52F5.js';
6
7
  import { parsedRefToTarget, parsedRefToScope, matchRecords, scopesEqual, getRecordById, listRecords, upsertRecord, updateRecord, createRecord, removeRecord } from '../../chunk-KA4MKRHL.js';
7
8
  export { bulkDelete, bulkUpsert, createRecord, getRecordById, listRecords, matchRecords, parsedRefToScope, parsedRefToTarget, removeRecord, restoreRecord, scopesEqual, upsertRecord } from '../../chunk-KA4MKRHL.js';
8
- import { createContext, useState, useEffect, useCallback, useMemo, useRef, useContext, useSyncExternalStore, useLayoutEffect, createElement, useId } from 'react';
9
- import { ChevronDown, Database, Lightbulb, SearchX, Inbox, LayoutGrid, Eye, MoreHorizontal, Download, Upload, Trash2, Copy, Pencil, Plus, CircleDashed, ArrowDownLeft, CheckCircle2, List, SlidersHorizontal, Globe, Tag, Boxes, Layers, Package, Target, Rows3, ChevronRight, Eraser, ClipboardPaste, Box, X, Search, Image, Table, ArrowLeft, ChevronLeft, AlertTriangle, Info, HelpCircle, CornerDownLeft, Circle, CopyPlus, AlertCircle, Undo2, Save, Loader2, XCircle, ArrowRight, BookOpen, Globe2, Check, Settings2 } from 'lucide-react';
9
+ import { createContext, useState, useEffect, useCallback, useMemo, useRef, isValidElement, useContext, useSyncExternalStore, useLayoutEffect, createElement } from 'react';
10
+ import { ChevronDown, Database, Lightbulb, SearchX, Inbox, LayoutGrid, Eye, MoreHorizontal, Download, Upload, Trash2, Copy, Pencil, Plus, CircleDashed, ArrowDownLeft, CheckCircle2, List, SlidersHorizontal, Globe, Tag, Boxes, Layers, Package, Target, Rows3, ChevronRight, Eraser, ClipboardPaste, Box, X, Search, Image, Table, ArrowLeft, ChevronLeft, AlertTriangle, Info, HelpCircle, CornerDownLeft, Circle, MinusCircle, XCircle, CopyPlus, AlertCircle, Undo2, Save, Loader2, ArrowRight, Globe2, Check, Settings2 } from 'lucide-react';
10
11
  import { useQuery, useQueryClient, useInfiniteQuery } from '@tanstack/react-query';
11
12
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
12
13
  import { createPortal } from 'react-dom';
@@ -690,9 +691,9 @@ var useRecordList = (args) => {
690
691
  partial: items.filter((r) => r.status === "partial").length,
691
692
  empty: items.filter((r) => r.status === "empty").length
692
693
  }), [items]);
693
- const refetch = useCallback(() => {
694
- queryClient.invalidateQueries({ queryKey: [...QK_BASE2, ctx.collectionId, ctx.appId, ctx.recordType] });
695
- }, [queryClient, ctx.collectionId, ctx.appId, ctx.recordType]);
694
+ const refetch = useCallback(() => queryClient.refetchQueries({
695
+ queryKey: [...QK_BASE2, ctx.collectionId, ctx.appId, ctx.recordType]
696
+ }), [queryClient, ctx.collectionId, ctx.appId, ctx.recordType]);
696
697
  const total = query.data?.pages[query.data.pages.length - 1]?.total ?? items.length;
697
698
  return {
698
699
  allItems: items,
@@ -3125,9 +3126,11 @@ var useShellSelection = (args) => {
3125
3126
  }, [contextScope?.batchId]);
3126
3127
  const [selectedItemId, setSelectedItemId] = useState(null);
3127
3128
  const skipNextItemResetRef = useRef(false);
3129
+ const [selectedLifecycleKey, setSelectedLifecycleKey] = useState(null);
3128
3130
  useEffect(() => {
3129
3131
  setSelectedRecordId(null);
3130
3132
  setDraftKind(null);
3133
+ setSelectedLifecycleKey(null);
3131
3134
  }, [activeScope]);
3132
3135
  return {
3133
3136
  activeScope,
@@ -3150,6 +3153,8 @@ var useShellSelection = (args) => {
3150
3153
  setSelectedBatchId,
3151
3154
  selectedItemId,
3152
3155
  setSelectedItemId,
3156
+ selectedLifecycleKey,
3157
+ setSelectedLifecycleKey,
3153
3158
  skipNextItemResetRef
3154
3159
  };
3155
3160
  };
@@ -3245,7 +3250,43 @@ var resolveTone = (source, status) => {
3245
3250
  if (source === "inherited" || status === "partial") return "shared";
3246
3251
  return "missing";
3247
3252
  };
3248
- var StatusIcon = ({ source, status, className, size = "1.05rem", label }) => {
3253
+ var SEMANTIC_DEFAULT_ICONS = {
3254
+ success: CheckCircle2,
3255
+ warning: AlertTriangle,
3256
+ danger: XCircle,
3257
+ muted: MinusCircle,
3258
+ info: Info
3259
+ };
3260
+ var StatusIcon = ({
3261
+ source,
3262
+ status,
3263
+ className,
3264
+ size = "1.05rem",
3265
+ label,
3266
+ iconHint,
3267
+ semanticTone
3268
+ }) => {
3269
+ if (semanticTone || iconHint) {
3270
+ const toneClass = semanticTone && semanticTone !== "default" ? `ra-status-icon--${semanticTone}` : "ra-status-icon--muted";
3271
+ let content;
3272
+ if (iconHint) {
3273
+ content = isValidElement(iconHint) ? iconHint : /* @__PURE__ */ jsx(Fragment, { children: iconHint });
3274
+ } else {
3275
+ const Icon2 = semanticTone && semanticTone !== "default" ? SEMANTIC_DEFAULT_ICONS[semanticTone] : MinusCircle;
3276
+ content = /* @__PURE__ */ jsx(Icon2, { className: "w-full h-full" });
3277
+ }
3278
+ return /* @__PURE__ */ jsx(
3279
+ "span",
3280
+ {
3281
+ className: cn("ra-status-icon", toneClass, className),
3282
+ style: { width: size, height: size },
3283
+ role: label ? "img" : void 0,
3284
+ "aria-label": label,
3285
+ "aria-hidden": label ? void 0 : "true",
3286
+ children: content
3287
+ }
3288
+ );
3289
+ }
3249
3290
  const tone = resolveTone(source, status);
3250
3291
  const Icon = DEFAULT_ICONS.status[tone === "own" ? "own" : tone === "shared" ? "inherited" : "missing"];
3251
3292
  return /* @__PURE__ */ jsx(
@@ -3478,7 +3519,9 @@ var DefaultRecordRow = ({ record, ctx, compact = false }) => {
3478
3519
  StatusIcon,
3479
3520
  {
3480
3521
  status: record.status,
3481
- label: statusToneLabel(tone)
3522
+ label: statusToneLabel(tone),
3523
+ iconHint: record.iconHint,
3524
+ semanticTone: record.toneHint
3482
3525
  }
3483
3526
  ) }),
3484
3527
  /* @__PURE__ */ jsxs("div", { className: "ra-row-body", children: [
@@ -4086,9 +4129,7 @@ function useCollectionItems(args) {
4086
4129
  return toSummary2(rec, base);
4087
4130
  }).filter((x) => x !== null);
4088
4131
  }, [query.data, toSummary2, scope, ruleSignature, includeAll]);
4089
- const refetch = useCallback(() => {
4090
- queryClient.invalidateQueries({ queryKey });
4091
- }, [queryClient, queryKey]);
4132
+ const refetch = useCallback(() => queryClient.refetchQueries({ queryKey }), [queryClient, queryKey]);
4092
4133
  return {
4093
4134
  items,
4094
4135
  total: query.data?.pages[query.data.pages.length - 1]?.total ?? items.length,
@@ -6193,89 +6234,6 @@ var UtilityRow = ({ label, customLabel, introHidden, onShowIntro }) => {
6193
6234
  }
6194
6235
  );
6195
6236
  };
6196
- var TONE_ICON2 = {
6197
- info: Lightbulb,
6198
- success: CheckCircle2,
6199
- warning: AlertTriangle
6200
- };
6201
- function AdminPageHeader({
6202
- title,
6203
- subtitle,
6204
- icon,
6205
- helpUrl,
6206
- helpLabel,
6207
- actions,
6208
- aside,
6209
- intro,
6210
- className
6211
- }) {
6212
- const titleId = useId();
6213
- const resolvedHelpLabel = helpLabel ?? "Help & documentation";
6214
- const resolvedReopenLabel = intro?.reopenLabel ?? "How it works";
6215
- const showReopen = !!intro && intro.dismissed && !!intro.onReopen;
6216
- const showIntro = !!intro && !intro.dismissed;
6217
- return /* @__PURE__ */ jsxs("header", { className: `sl-aph${className ? ` ${className}` : ""}`, "aria-labelledby": titleId, children: [
6218
- /* @__PURE__ */ jsxs("div", { className: "sl-aph__row", children: [
6219
- /* @__PURE__ */ jsx("div", { className: "sl-aph__main", children: /* @__PURE__ */ jsxs("div", { className: "sl-aph__text", children: [
6220
- /* @__PURE__ */ jsxs("h1", { className: "sl-aph__title", id: titleId, children: [
6221
- icon ? /* @__PURE__ */ jsx("span", { className: "sl-aph__icon", "aria-hidden": "true", children: icon }) : null,
6222
- /* @__PURE__ */ jsx("span", { children: title })
6223
- ] }),
6224
- subtitle ? /* @__PURE__ */ jsx("p", { className: "sl-aph__subtitle", children: subtitle }) : null
6225
- ] }) }),
6226
- actions || aside || helpUrl || showReopen ? /* @__PURE__ */ jsxs("div", { className: "sl-aph__aside", children: [
6227
- actions,
6228
- aside,
6229
- helpUrl ? /* @__PURE__ */ jsx(
6230
- "a",
6231
- {
6232
- href: helpUrl,
6233
- target: "_blank",
6234
- rel: "noopener noreferrer",
6235
- className: "sl-aph__icon-btn",
6236
- "aria-label": resolvedHelpLabel,
6237
- title: resolvedHelpLabel,
6238
- children: /* @__PURE__ */ jsx(BookOpen, { "aria-hidden": "true" })
6239
- }
6240
- ) : null,
6241
- showReopen ? /* @__PURE__ */ jsx(
6242
- "button",
6243
- {
6244
- type: "button",
6245
- onClick: intro.onReopen,
6246
- className: "sl-aph__icon-btn",
6247
- "aria-label": resolvedReopenLabel,
6248
- title: resolvedReopenLabel,
6249
- children: /* @__PURE__ */ jsx(HelpCircle, { "aria-hidden": "true" })
6250
- }
6251
- ) : null
6252
- ] }) : null
6253
- ] }),
6254
- showIntro ? /* @__PURE__ */ jsx(AdminPageHeaderIntroCard, { intro }) : null
6255
- ] });
6256
- }
6257
- function AdminPageHeaderIntroCard({ intro }) {
6258
- const tone = intro.tone ?? "info";
6259
- const Icon = TONE_ICON2[tone] ?? Info;
6260
- return /* @__PURE__ */ jsxs("div", { className: "sl-aph__intro", "data-tone": tone, role: "note", children: [
6261
- /* @__PURE__ */ jsx("div", { className: "sl-aph__intro-icon", children: /* @__PURE__ */ jsx(Icon, { "aria-hidden": "true", style: { width: "0.95rem", height: "0.95rem" } }) }),
6262
- /* @__PURE__ */ jsxs("div", { className: "sl-aph__intro-body", children: [
6263
- /* @__PURE__ */ jsx("h4", { className: "sl-aph__intro-title", children: intro.title }),
6264
- /* @__PURE__ */ jsx("span", { className: "sl-aph__intro-text", children: intro.body }),
6265
- intro.action ? /* @__PURE__ */ jsx("span", { className: "sl-aph__intro-action", children: intro.action }) : null
6266
- ] }),
6267
- /* @__PURE__ */ jsx(
6268
- "button",
6269
- {
6270
- type: "button",
6271
- onClick: intro.onDismiss,
6272
- "aria-label": "Dismiss",
6273
- className: "sl-aph__intro-dismiss",
6274
- children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true", style: { width: "0.875rem", height: "0.875rem" } })
6275
- }
6276
- )
6277
- ] });
6278
- }
6279
6237
  function ShellHeader({
6280
6238
  title,
6281
6239
  subtitle,
@@ -7452,6 +7410,8 @@ function RecordsAdminShellInner(props) {
7452
7410
  renderListRow,
7453
7411
  renderEmpty: renderEmptyState,
7454
7412
  groupBy,
7413
+ defaultGroupKey,
7414
+ onGroupExpanded,
7455
7415
  density = "comfortable"
7456
7416
  } = rail ?? {};
7457
7417
  const {
@@ -7474,7 +7434,8 @@ function RecordsAdminShellInner(props) {
7474
7434
  renderCard: renderItemCard,
7475
7435
  renderEmpty: renderItemEmpty,
7476
7436
  cardSize: itemCardSize = "md",
7477
- railMode: collectionRailMode = "siblings"
7437
+ railMode: collectionRailMode = "siblings",
7438
+ toSummary: itemToSummary
7478
7439
  } = items ?? {};
7479
7440
  const {
7480
7441
  strategy: dirtyStrategy = "keep",
@@ -7592,8 +7553,11 @@ function RecordsAdminShellInner(props) {
7592
7553
  setSelectedBatchId,
7593
7554
  selectedItemId,
7594
7555
  setSelectedItemId,
7556
+ selectedLifecycleKey,
7557
+ setSelectedLifecycleKey,
7595
7558
  skipNextItemResetRef
7596
7559
  } = selection;
7560
+ const [isReconcilingRecordSelection, setIsReconcilingRecordSelection] = useState(false);
7597
7561
  const { dismissed, dismiss, undismiss } = useIntroDismissed(SL, collectionId, appId, recordType);
7598
7562
  const headerWillRender = useMemo(() => {
7599
7563
  const headerCustomised = !!title || !!subtitle || !!headerIcon || !!headerActions || showStats || !!statsItems || !!statsTitle || !!statsIcon || !!helpUrl;
@@ -7654,6 +7618,7 @@ function RecordsAdminShellInner(props) {
7654
7618
  useEffect(() => {
7655
7619
  if (activeScope !== "rule" && activeScope !== "collection" && activeScope !== "all") return;
7656
7620
  if (selectedRecordId !== null) return;
7621
+ if (isReconcilingRecordSelection) return;
7657
7622
  if (ruleWizardStep !== null) return;
7658
7623
  if (draftKind !== null) return;
7659
7624
  if (activeScope === "collection" && cardinality === "collection") {
@@ -7664,7 +7629,7 @@ function RecordsAdminShellInner(props) {
7664
7629
  }
7665
7630
  const first = recordList.items[0];
7666
7631
  if (first?.id) setSelectedRecordId(first.id);
7667
- }, [activeScope, selectedRecordId, recordList.items, cardinality, ruleWizardStep, draftKind]);
7632
+ }, [activeScope, selectedRecordId, recordList.items, cardinality, ruleWizardStep, draftKind, isReconcilingRecordSelection]);
7668
7633
  const editingScopes = useEditingScope({
7669
7634
  activeScope,
7670
7635
  cardinality,
@@ -7692,7 +7657,8 @@ function RecordsAdminShellInner(props) {
7692
7657
  // record across the whole collection (anchored, ruled, global) so
7693
7658
  // host-supplied lifecycle grouping has the full picture.
7694
7659
  includeAll: isCollection && activeScope === "all",
7695
- enabled: isCollection
7660
+ enabled: isCollection,
7661
+ toSummary: itemToSummary
7696
7662
  });
7697
7663
  useEffect(() => {
7698
7664
  if (skipNextItemResetRef.current) {
@@ -7761,10 +7727,15 @@ function RecordsAdminShellInner(props) {
7761
7727
  supportedScopes: supportedForResolution,
7762
7728
  enabled: !!editingTargetScope
7763
7729
  });
7764
- const refetchAll = useCallback(() => {
7765
- recordList.refetch();
7766
- if (isCollection) collectionItems.refetch();
7767
- }, [recordList, isCollection, collectionItems]);
7730
+ const refetchAll = useCallback(async () => {
7731
+ await Promise.all([
7732
+ recordList.refetch(),
7733
+ isCollection ? collectionItems.refetch() : Promise.resolve(),
7734
+ queryClient.refetchQueries({
7735
+ queryKey: scopeCountsQueryKey(ctx.collectionId, ctx.appId, ctx.recordType)
7736
+ })
7737
+ ]);
7738
+ }, [recordList, isCollection, collectionItems, queryClient, ctx.collectionId, ctx.appId, ctx.recordType]);
7768
7739
  const { editorCtx } = useShellEditorTarget({
7769
7740
  editingTargetScope,
7770
7741
  isCollection,
@@ -7783,7 +7754,7 @@ function RecordsAdminShellInner(props) {
7783
7754
  },
7784
7755
  defaultData,
7785
7756
  deriveDraftLabel,
7786
- onSaved: (isCreate, savedRecordId) => {
7757
+ onSaved: async (isCreate, savedRecordId) => {
7787
7758
  onTelemetry?.({ type: "record.save", recordType, ref: editingTargetScope?.raw ?? "", isCreate });
7788
7759
  if (ruleWizardStep !== null) {
7789
7760
  setRuleWizardStep(null);
@@ -7791,15 +7762,20 @@ function RecordsAdminShellInner(props) {
7791
7762
  setDraftKind(null);
7792
7763
  }
7793
7764
  if (isCreate && selectedRecordId === DRAFT_ID3) {
7794
- setSelectedRecordId(savedRecordId ?? null);
7765
+ setIsReconcilingRecordSelection(true);
7766
+ setSelectedRecordId(null);
7795
7767
  setDraftKind(null);
7796
7768
  }
7797
7769
  if (isCreate && isCollection && savedRecordId && isDraftId3(selectedItemId)) {
7798
7770
  setSelectedItemId(savedRecordId);
7799
7771
  }
7800
- refetchAll();
7772
+ await refetchAll();
7773
+ if (!isCollection && isCreate && activeScope === "collection") {
7774
+ setSelectedRecordId(savedRecordId ?? null);
7775
+ }
7776
+ setIsReconcilingRecordSelection(false);
7801
7777
  },
7802
- onDeleted: () => {
7778
+ onDeleted: async () => {
7803
7779
  onTelemetry?.({ type: "record.delete", recordType, ref: editingTargetScope?.raw ?? "" });
7804
7780
  if (isCollection && selectedItemId) {
7805
7781
  setSelectedItemId(null);
@@ -7808,10 +7784,12 @@ function RecordsAdminShellInner(props) {
7808
7784
  } else if (drillTab === "batch") {
7809
7785
  setSelectedBatchId(void 0);
7810
7786
  } else if (selectedRecordId) {
7787
+ setIsReconcilingRecordSelection(true);
7811
7788
  setSelectedRecordId(null);
7812
7789
  setDraftKind(null);
7813
7790
  }
7814
- refetchAll();
7791
+ await refetchAll();
7792
+ setIsReconcilingRecordSelection(false);
7815
7793
  }
7816
7794
  });
7817
7795
  useUnsavedGuard({
@@ -8426,17 +8404,96 @@ function RecordsAdminShellInner(props) {
8426
8404
  }),
8427
8405
  [i18n.itemsAllLabel, collectionItems.items.length, itemNoun]
8428
8406
  );
8407
+ const isLifecycleRail = (isAllTab || isGlobalTab) && isCollection && !!groupBy;
8408
+ const lifecycleBuckets = useMemo(() => {
8409
+ if (!isLifecycleRail || !groupBy) return [];
8410
+ const map = /* @__PURE__ */ new Map();
8411
+ const order = [];
8412
+ for (const item of collectionItems.items) {
8413
+ const g = groupBy(item) ?? { key: "__other", label: "Other" };
8414
+ let bucket = map.get(g.key);
8415
+ if (!bucket) {
8416
+ bucket = { key: g.key, label: g.label, icon: g.icon, tone: g.tone, items: [] };
8417
+ map.set(g.key, bucket);
8418
+ order.push(g.key);
8419
+ }
8420
+ bucket.items.push(item);
8421
+ }
8422
+ return order.sort().map((k) => map.get(k));
8423
+ }, [isLifecycleRail, groupBy, collectionItems.items]);
8424
+ const LIFECYCLE_PREFIX = "lifecycle:";
8425
+ const lifecycleRows = useMemo(() => {
8426
+ if (!isLifecycleRail) return [];
8427
+ const rows = [
8428
+ {
8429
+ id: `${LIFECYCLE_PREFIX}__all`,
8430
+ ref: "",
8431
+ scope: parseRef(""),
8432
+ data: null,
8433
+ status: collectionItems.items.length ? "configured" : "empty",
8434
+ label: i18n.itemsAllLabel,
8435
+ subtitle: collectionItems.items.length ? `${collectionItems.items.length} ${itemNoun}${collectionItems.items.length === 1 ? "" : "s"}` : void 0
8436
+ }
8437
+ ];
8438
+ for (const b of lifecycleBuckets) {
8439
+ rows.push({
8440
+ id: `${LIFECYCLE_PREFIX}${b.key}`,
8441
+ ref: "",
8442
+ scope: parseRef(""),
8443
+ data: null,
8444
+ status: b.items.length ? "configured" : "empty",
8445
+ label: b.label,
8446
+ subtitle: `${b.items.length} ${itemNoun}${b.items.length === 1 ? "" : "s"}`,
8447
+ // Tone + icon drive the row's leading status icon (replacing the
8448
+ // generic green tick / dotted circle for these synthetic rows).
8449
+ // Hosts can override the icon entirely via `groupBy(...).icon`.
8450
+ iconHint: b.icon,
8451
+ toneHint: b.tone
8452
+ });
8453
+ }
8454
+ return rows;
8455
+ }, [isLifecycleRail, lifecycleBuckets, collectionItems.items.length, i18n.itemsAllLabel, itemNoun]);
8456
+ const lifecycleSeededRef = useRef(false);
8457
+ useEffect(() => {
8458
+ if (!isLifecycleRail) {
8459
+ lifecycleSeededRef.current = false;
8460
+ return;
8461
+ }
8462
+ if (lifecycleSeededRef.current) return;
8463
+ if (selectedLifecycleKey !== null) {
8464
+ lifecycleSeededRef.current = true;
8465
+ return;
8466
+ }
8467
+ if (!defaultGroupKey) return;
8468
+ if (!lifecycleBuckets.some((b) => b.key === defaultGroupKey)) return;
8469
+ setSelectedLifecycleKey(defaultGroupKey);
8470
+ lifecycleSeededRef.current = true;
8471
+ }, [isLifecycleRail, defaultGroupKey, lifecycleBuckets, selectedLifecycleKey, setSelectedLifecycleKey]);
8472
+ const filteredCollectionItems = useMemo(() => {
8473
+ if (!isLifecycleRail || !selectedLifecycleKey || !groupBy) return collectionItems.items;
8474
+ return collectionItems.items.filter((it) => {
8475
+ const g = groupBy(it) ?? { key: "__other" };
8476
+ return g.key === selectedLifecycleKey;
8477
+ });
8478
+ }, [isLifecycleRail, selectedLifecycleKey, groupBy, collectionItems.items]);
8429
8479
  const leftItems = isProductTab ? productListItems : isRuleTab ? applyFacetBrowseFilter(
8430
8480
  isCollection ? collectionRuleRailItems : filteredRuleItems
8431
- ) : (isGlobalTab || isAllTab) && isCollection ? [collectionGlobalAllRow] : isRecordsTab ? applyFacetBrowseFilter(recordList.items) : [];
8481
+ ) : isLifecycleRail ? lifecycleRows : (isGlobalTab || isAllTab) && isCollection ? [collectionGlobalAllRow] : isRecordsTab ? applyFacetBrowseFilter(recordList.items) : [];
8432
8482
  const leftLoading = isProductTab ? !productPinned && productBrowse.isLoading : isRecordsTab ? recordList.isLoading || probe.isLoading : false;
8433
8483
  const leftError = isProductTab ? productBrowse.error : isRecordsTab ? recordList.error : null;
8434
- const leftSelectedId = isProductTab ? void 0 : selectedRecordId && selectedRecordId !== DRAFT_ID3 ? selectedRecordId : void 0;
8484
+ const leftSelectedId = isProductTab ? void 0 : isLifecycleRail ? `${LIFECYCLE_PREFIX}${selectedLifecycleKey ?? "__all"}` : selectedRecordId && selectedRecordId !== DRAFT_ID3 ? selectedRecordId : void 0;
8435
8485
  const leftSelectedAnchorKey = isProductTab ? selectedProductId ? buildRef({ productId: selectedProductId }) : void 0 : void 0;
8436
8486
  const dirtyId = !editorCtx.isDirty ? void 0 : selectedRecordId && selectedRecordId !== DRAFT_ID3 ? selectedRecordId : void 0;
8437
8487
  const dirtyAnchorKey = !editorCtx.isDirty ? void 0 : isProductTab ? editingScope?.raw : void 0;
8438
8488
  const onLeftSelect = (item) => {
8439
8489
  void runWithGuard(() => {
8490
+ if (isLifecycleRail && typeof item.id === "string" && item.id.startsWith(LIFECYCLE_PREFIX)) {
8491
+ const key = item.id.slice(LIFECYCLE_PREFIX.length);
8492
+ const next = key === "__all" ? null : key;
8493
+ setSelectedLifecycleKey(next);
8494
+ if (next && onGroupExpanded) onGroupExpanded(next);
8495
+ return;
8496
+ }
8440
8497
  if (isProductTab) {
8441
8498
  setSelectedProductId(item.scope.productId);
8442
8499
  setSelectedVariantId(void 0);
@@ -8794,7 +8851,7 @@ function RecordsAdminShellInner(props) {
8794
8851
  // navigational anchor, not a real record — applying the
8795
8852
  // host's groupBy bucketed it under "Other" (its
8796
8853
  // data is null). Skip grouping for that single-row rail.
8797
- (isGlobalTab || isAllTab) && isCollection ? void 0 : effectiveGroupBy
8854
+ (isGlobalTab || isAllTab) && isCollection || isLifecycleRail ? void 0 : effectiveGroupBy
8798
8855
  ),
8799
8856
  renderGroupActions: renderRuleGroupActions,
8800
8857
  rowClipboard,
@@ -8883,7 +8940,7 @@ function RecordsAdminShellInner(props) {
8883
8940
  ruleWizardStep === null && isCollection && editingScope && !selectedItemId && /* @__PURE__ */ jsx(
8884
8941
  ItemListView,
8885
8942
  {
8886
- items: collectionItems.items,
8943
+ items: filteredCollectionItems,
8887
8944
  isLoading: collectionItems.isLoading,
8888
8945
  error: collectionItems.error,
8889
8946
  ctx: itemViewCtx,