@proveanything/smartlinks-utils-ui 0.12.6 → 0.12.8

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,13 +1,13 @@
1
+ import { parsedRefToTarget, parsedRefToScope, matchRecords, scopesEqual, getRecordById, listRecords, upsertRecord, updateRecord, createRecord, removeRecord } from '../../chunk-KA4MKRHL.js';
2
+ export { bulkDelete, bulkUpsert, createRecord, getRecordById, listRecords, matchRecords, parsedRefToScope, parsedRefToTarget, removeRecord, restoreRecord, scopesEqual, upsertRecord } from '../../chunk-KA4MKRHL.js';
1
3
  import { useIntroState, AdminPageHeader } from '../../chunk-3RRHM4LP.js';
2
- import { assertComponentStylesLoaded } from '../../chunk-OLYC54YT.js';
3
- import '../../chunk-5UQQYXCX.js';
4
4
  import { FacetRuleEditor } from '../../chunk-JMCV6FOW.js';
5
5
  import { useFacets } from '../../chunk-4LHF5JB7.js';
6
+ import { assertComponentStylesLoaded } from '../../chunk-OLYC54YT.js';
7
+ import '../../chunk-5UQQYXCX.js';
6
8
  import { cn } from '../../chunk-L7FQ52F5.js';
7
- import { parsedRefToTarget, parsedRefToScope, matchRecords, scopesEqual, getRecordById, listRecords, upsertRecord, updateRecord, createRecord, removeRecord } from '../../chunk-KA4MKRHL.js';
8
- export { bulkDelete, bulkUpsert, createRecord, getRecordById, listRecords, matchRecords, parsedRefToScope, parsedRefToTarget, removeRecord, restoreRecord, scopesEqual, upsertRecord } from '../../chunk-KA4MKRHL.js';
9
9
  import { createContext, useMemo, useState, useEffect, useCallback, useRef, isValidElement, useLayoutEffect, useContext, useSyncExternalStore, 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, Check, Rows3, ChevronRight, Eraser, FilePlus2, CopyPlus, ClipboardPaste, Box, X, Search, Image, Table, ArrowLeft, ChevronLeft, AlertTriangle, Info, HelpCircle, CornerDownLeft, Circle, ArrowUpDown, ArrowUp, ArrowDown, MinusCircle, XCircle, AlertCircle, Undo2, Save, Loader2, Archive, ArrowRight, Globe2, Sparkles, Settings2 } from 'lucide-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, Check, Rows3, ChevronRight, Eraser, FilePlus2, CopyPlus, ClipboardPaste, Box, X, Search, Image, Table, ArrowLeft, ChevronLeft, AlertTriangle, Info, HelpCircle, PanelLeftClose, CornerDownLeft, Circle, ArrowUpDown, ArrowUp, ArrowDown, MinusCircle, XCircle, AlertCircle, Undo2, Save, Loader2, Archive, ArrowRight, Globe2, Sparkles, Settings2 } from 'lucide-react';
11
11
  import { useQuery, useQueryClient, useInfiniteQuery } from '@tanstack/react-query';
12
12
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
13
13
  import { createPortal } from 'react-dom';
@@ -183,6 +183,8 @@ var DEFAULT_I18N = {
183
183
  editor: "Editor",
184
184
  closePreview: "Close preview",
185
185
  openPreview: "Open preview",
186
+ closeList: "Hide list",
187
+ openList: "Show list",
186
188
  previewAs: "Preview as",
187
189
  previewAsDefault: "Same as edited",
188
190
  confirmDelete: "Confirm delete",
@@ -276,7 +278,15 @@ var DEFAULT_I18N = {
276
278
  conflictArchiveDuplicates: "Archive duplicates",
277
279
  conflictDeleteDuplicates: "Delete duplicates",
278
280
  conflictDeleteConfirm: "Permanently delete {n} duplicate record(s)? This cannot be undone.",
279
- conflictResolveLabel: "Resolve"
281
+ conflictResolveLabel: "Resolve",
282
+ ruleSortLabel: "Sort",
283
+ ruleSortRecent: "Recently updated",
284
+ ruleSortName: "Name",
285
+ ruleSortActiveCount: "Active count",
286
+ ruleSortHasArchived: "Has archived",
287
+ lifecycleBadgeActive: "{n} active",
288
+ lifecycleBadgeArchived: "{n} archived",
289
+ lifecycleBadgeDraft: "{n} draft"
280
290
  };
281
291
 
282
292
  // src/components/RecordsAdmin/types/presentation.ts
@@ -851,6 +861,24 @@ var useRecordList = (args) => {
851
861
  }
852
862
  return map;
853
863
  }, [historyItems]);
864
+ const ruleLifecycleCounts = useMemo(() => {
865
+ const map = /* @__PURE__ */ new Map();
866
+ for (const r of items) {
867
+ const h = ruleHash(r.facetRule ?? null);
868
+ if (!h) continue;
869
+ let bucket = map.get(h);
870
+ if (!bucket) {
871
+ bucket = { active: 0, archived: 0, draft: 0, other: 0 };
872
+ map.set(h, bucket);
873
+ }
874
+ const ls = r.lifecycleStatus;
875
+ if (ls == null || ls === "" || activeStatuses.includes(ls)) bucket.active += 1;
876
+ else if (ls === "archived") bucket.archived += 1;
877
+ else if (ls === "draft") bucket.draft += 1;
878
+ else bucket.other += 1;
879
+ }
880
+ return map;
881
+ }, [items, activeStatuses]);
854
882
  const refetch = useCallback(() => queryClient.refetchQueries({
855
883
  queryKey: [...QK_BASE2, ctx.collectionId, ctx.appId, ctx.recordType]
856
884
  }), [queryClient, ctx.collectionId, ctx.appId, ctx.recordType]);
@@ -861,6 +889,7 @@ var useRecordList = (args) => {
861
889
  activeItems,
862
890
  historyItems,
863
891
  historyBySlot,
892
+ ruleLifecycleCounts,
864
893
  total,
865
894
  counts,
866
895
  isLoading: query.isLoading,
@@ -2185,10 +2214,54 @@ function useShellClipboard(args) {
2185
2214
  };
2186
2215
  }
2187
2216
  var useShellPreviewPane = (args) => {
2188
- const { editingScope, activeScope, selectedProductId, productBrowseItems } = args;
2217
+ const {
2218
+ editingScope,
2219
+ activeScope,
2220
+ selectedProductId,
2221
+ productBrowseItems,
2222
+ pinPreviewMinWidth = 1400,
2223
+ preferenceStorageKey = "smartlinks-ui:recordsAdmin:sidePreviewOpen"
2224
+ } = args;
2189
2225
  const [previewScope, setPreviewScope] = useState(null);
2190
2226
  const [drawerOpen, setDrawerOpen] = useState(false);
2191
- const [sidePreviewOpen, setSidePreviewOpen] = useState(true);
2227
+ const readStoredPref = () => {
2228
+ if (typeof window === "undefined") return null;
2229
+ try {
2230
+ const raw = window.sessionStorage.getItem(preferenceStorageKey);
2231
+ if (raw === "1") return true;
2232
+ if (raw === "0") return false;
2233
+ } catch {
2234
+ }
2235
+ return null;
2236
+ };
2237
+ const initialIsWide = typeof window !== "undefined" ? window.innerWidth >= pinPreviewMinWidth : true;
2238
+ const [isWide, setIsWide] = useState(initialIsWide);
2239
+ const userPrefRef = useRef(readStoredPref());
2240
+ const [sidePreviewOpen, setSidePreviewOpenState] = useState(
2241
+ userPrefRef.current ?? initialIsWide
2242
+ );
2243
+ useEffect(() => {
2244
+ if (typeof window === "undefined") return;
2245
+ const mql = window.matchMedia(`(min-width: ${pinPreviewMinWidth}px)`);
2246
+ const onChange = () => {
2247
+ const wide = mql.matches;
2248
+ setIsWide(wide);
2249
+ if (userPrefRef.current === null) {
2250
+ setSidePreviewOpenState(wide);
2251
+ }
2252
+ };
2253
+ onChange();
2254
+ mql.addEventListener("change", onChange);
2255
+ return () => mql.removeEventListener("change", onChange);
2256
+ }, [pinPreviewMinWidth]);
2257
+ const setSidePreviewOpen = useCallback((next) => {
2258
+ userPrefRef.current = next;
2259
+ try {
2260
+ window.sessionStorage.setItem(preferenceStorageKey, next ? "1" : "0");
2261
+ } catch {
2262
+ }
2263
+ setSidePreviewOpenState(next);
2264
+ }, [preferenceStorageKey]);
2192
2265
  useEffect(() => {
2193
2266
  if (!editingScope) return;
2194
2267
  setPreviewScope((cur) => cur === null ? editingScope : cur);
@@ -2222,6 +2295,7 @@ var useShellPreviewPane = (args) => {
2222
2295
  setDrawerOpen,
2223
2296
  sidePreviewOpen,
2224
2297
  setSidePreviewOpen,
2298
+ isPreviewNarrow: !isWide,
2225
2299
  editorHeaderLabel,
2226
2300
  editorHeaderSubtitle,
2227
2301
  editorHeaderMeta
@@ -6881,6 +6955,8 @@ function ItemListView({
6881
6955
  hasNextPage,
6882
6956
  isFetchingNextPage,
6883
6957
  onLoadMore,
6958
+ groupByLifecycle = false,
6959
+ lifecycle,
6884
6960
  i18n
6885
6961
  }) {
6886
6962
  const newLabel = i18n.newItem.includes("{noun}") ? i18n.newItem.replace("{noun}", itemNoun) : i18n.newItem;
@@ -6955,6 +7031,69 @@ function ItemListView({
6955
7031
  }), [ctx]);
6956
7032
  const confirmName = pendingRecord?.label ?? itemNoun;
6957
7033
  const confirmBody = i18n.deleteConfirmBody.includes("{name}") ? i18n.deleteConfirmBody.replace("{name}", confirmName) : i18n.deleteConfirmBody;
7034
+ const activeValues = useMemo(
7035
+ () => getActiveStatusValues(lifecycle),
7036
+ [lifecycle]
7037
+ );
7038
+ const buckets = useMemo(() => {
7039
+ if (!groupByLifecycle) return null;
7040
+ const map = /* @__PURE__ */ new Map();
7041
+ for (const item of visibleItems) {
7042
+ const def = resolveLifecycleStatus(item, lifecycle);
7043
+ const existing = map.get(def.value);
7044
+ if (existing) existing.items.push(item);
7045
+ else map.set(def.value, { label: def.label, items: [item] });
7046
+ }
7047
+ return Array.from(map.entries()).map(([key, v]) => ({
7048
+ key,
7049
+ label: v.label,
7050
+ items: v.items,
7051
+ isActive: activeValues.includes(key)
7052
+ })).sort((a, b) => compareLifecycleBuckets(a.key, b.key)).filter((b) => b.items.length > 0);
7053
+ }, [groupByLifecycle, visibleItems, lifecycle, activeValues]);
7054
+ const [collapsedBuckets, setCollapsedBuckets] = useState(() => /* @__PURE__ */ new Set());
7055
+ const toggleBucket = (key) => {
7056
+ setCollapsedBuckets((prev) => {
7057
+ const next = new Set(prev);
7058
+ if (next.has(key)) next.delete(key);
7059
+ else next.add(key);
7060
+ return next;
7061
+ });
7062
+ };
7063
+ const renderBody = (rows) => {
7064
+ if (renderItemList) return renderItemList(rows, guardedCtx);
7065
+ if (view === "table") {
7066
+ return /* @__PURE__ */ jsx(
7067
+ DefaultItemTable,
7068
+ {
7069
+ items: rows,
7070
+ columns: itemColumns,
7071
+ selectedId: guardedCtx.selectedId,
7072
+ onOpen: guardedCtx.onOpen,
7073
+ onDelete: guardedCtx.onDelete,
7074
+ sort,
7075
+ onToggleSort,
7076
+ rowActions,
7077
+ rowClipboard,
7078
+ i18n
7079
+ }
7080
+ );
7081
+ }
7082
+ return /* @__PURE__ */ jsx(
7083
+ DefaultItemCards,
7084
+ {
7085
+ items: rows,
7086
+ variant: view,
7087
+ selectedId: guardedCtx.selectedId,
7088
+ ctx: guardedCtx,
7089
+ renderCard: renderItemCard,
7090
+ cardSize,
7091
+ rowActions,
7092
+ rowClipboard,
7093
+ i18n
7094
+ }
7095
+ );
7096
+ };
6958
7097
  const toolbar = /* @__PURE__ */ jsxs("div", { className: "ra-item-toolbar", children: [
6959
7098
  /* @__PURE__ */ jsxs("div", { className: "ra-item-toolbar-title", children: [
6960
7099
  /* @__PURE__ */ jsx("h2", { className: "ra-display", style: { fontSize: "0.95rem", margin: 0 }, children: i18n.itemListTitle }),
@@ -7072,39 +7211,62 @@ function ItemListView({
7072
7211
  body: `No ${itemNoun}s match "${search}".`
7073
7212
  }
7074
7213
  );
7075
- } else if (renderItemList) {
7076
- body = renderItemList(visibleItems, guardedCtx);
7077
- } else if (view === "table") {
7078
- body = /* @__PURE__ */ jsx(
7079
- DefaultItemTable,
7080
- {
7081
- items: visibleItems,
7082
- columns: itemColumns,
7083
- selectedId: guardedCtx.selectedId,
7084
- onOpen: guardedCtx.onOpen,
7085
- onDelete: guardedCtx.onDelete,
7086
- sort,
7087
- onToggleSort,
7088
- rowActions,
7089
- rowClipboard,
7090
- i18n
7091
- }
7092
- );
7214
+ } else if (buckets && buckets.length > 0) {
7215
+ body = /* @__PURE__ */ jsx("div", { className: "ra-item-buckets", children: buckets.map((bucket) => {
7216
+ const open = bucket.isActive || !collapsedBuckets.has(bucket.key);
7217
+ return /* @__PURE__ */ jsxs(
7218
+ "section",
7219
+ {
7220
+ className: "ra-item-bucket",
7221
+ "data-bucket": bucket.key,
7222
+ children: [
7223
+ /* @__PURE__ */ jsxs(
7224
+ "button",
7225
+ {
7226
+ type: "button",
7227
+ className: "ra-item-bucket-header",
7228
+ onClick: () => !bucket.isActive && toggleBucket(bucket.key),
7229
+ "aria-expanded": open,
7230
+ disabled: bucket.isActive,
7231
+ style: {
7232
+ display: "flex",
7233
+ alignItems: "center",
7234
+ gap: "6px",
7235
+ width: "100%",
7236
+ padding: "6px 12px",
7237
+ background: "transparent",
7238
+ border: 0,
7239
+ borderTop: "1px solid hsl(var(--ra-border))",
7240
+ font: "inherit",
7241
+ fontSize: "11px",
7242
+ fontWeight: 600,
7243
+ textTransform: "uppercase",
7244
+ letterSpacing: "0.04em",
7245
+ color: "hsl(var(--ra-muted-text))",
7246
+ cursor: bucket.isActive ? "default" : "pointer"
7247
+ },
7248
+ children: [
7249
+ !bucket.isActive ? open ? /* @__PURE__ */ jsx(ChevronDown, { className: "w-3 h-3", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "w-3 h-3", "aria-hidden": "true" }) : null,
7250
+ /* @__PURE__ */ jsx("span", { children: bucket.label }),
7251
+ /* @__PURE__ */ jsx("span", { style: { marginLeft: "auto", fontWeight: 500 }, children: bucket.items.length })
7252
+ ]
7253
+ }
7254
+ ),
7255
+ open && /* @__PURE__ */ jsx(
7256
+ "div",
7257
+ {
7258
+ className: "ra-item-bucket-body",
7259
+ style: bucket.isActive ? void 0 : { opacity: 0.75 },
7260
+ children: renderBody(bucket.items)
7261
+ }
7262
+ )
7263
+ ]
7264
+ },
7265
+ bucket.key
7266
+ );
7267
+ }) });
7093
7268
  } else {
7094
- body = /* @__PURE__ */ jsx(
7095
- DefaultItemCards,
7096
- {
7097
- items: visibleItems,
7098
- variant: view,
7099
- selectedId: guardedCtx.selectedId,
7100
- ctx: guardedCtx,
7101
- renderCard: renderItemCard,
7102
- cardSize,
7103
- rowActions,
7104
- rowClipboard,
7105
- i18n
7106
- }
7107
- );
7269
+ body = renderBody(visibleItems);
7108
7270
  }
7109
7271
  return /* @__PURE__ */ jsxs("div", { className: "ra-item-list", children: [
7110
7272
  toolbar,
@@ -7247,10 +7409,42 @@ function SiblingRail({
7247
7409
  isFetchingNextPage,
7248
7410
  onLoadMore,
7249
7411
  rowClipboard,
7250
- rowActions
7412
+ rowActions,
7413
+ groupByLifecycle = false,
7414
+ lifecycle
7251
7415
  }) {
7252
7416
  const ruleLabelLookup = useRuleLabelLookup();
7253
7417
  const newLabel = i18n.newItem.includes("{noun}") ? i18n.newItem.replace("{noun}", itemNoun ?? "item") : i18n.newItem;
7418
+ const activeValues = useMemo(
7419
+ () => getActiveStatusValues(lifecycle),
7420
+ [lifecycle]
7421
+ );
7422
+ const buckets = useMemo(() => {
7423
+ if (!groupByLifecycle) return null;
7424
+ const map = /* @__PURE__ */ new Map();
7425
+ for (const item of items) {
7426
+ const def = resolveLifecycleStatus(item, lifecycle);
7427
+ const key = def.value;
7428
+ const existing = map.get(key);
7429
+ if (existing) existing.items.push(item);
7430
+ else map.set(key, { label: def.label, items: [item] });
7431
+ }
7432
+ return Array.from(map.entries()).map(([key, v]) => ({
7433
+ key,
7434
+ label: v.label,
7435
+ items: v.items,
7436
+ isActive: activeValues.includes(key)
7437
+ })).sort((a, b) => compareLifecycleBuckets(a.key, b.key)).filter((b) => b.items.length > 0);
7438
+ }, [groupByLifecycle, items, lifecycle, activeValues]);
7439
+ const [collapsed, setCollapsed] = useState(() => /* @__PURE__ */ new Set());
7440
+ const toggleBucket = (key) => {
7441
+ setCollapsed((prev) => {
7442
+ const next = new Set(prev);
7443
+ if (next.has(key)) next.delete(key);
7444
+ else next.add(key);
7445
+ return next;
7446
+ });
7447
+ };
7254
7448
  return /* @__PURE__ */ jsxs("div", { className: "ra-sibling-rail", children: [
7255
7449
  (onBack || contextKind) && /* @__PURE__ */ jsxs("div", { className: "ra-sibling-context", children: [
7256
7450
  onBack && /* @__PURE__ */ jsx(
@@ -7276,79 +7470,125 @@ function SiblingRail({
7276
7470
  isLoading && /* @__PURE__ */ jsx(LoadingState, {}),
7277
7471
  !isLoading && error && /* @__PURE__ */ jsx(ErrorState, { error }),
7278
7472
  !isLoading && !error && items.length === 0 && /* @__PURE__ */ jsx(EmptyState, { title: i18n.noItemsTitle, body: i18n.noItemsBody }),
7279
- !isLoading && !error && items.length > 0 && /* @__PURE__ */ jsx("ul", { className: "ra-sibling-list", children: items.map((item, idx) => {
7280
- const id = item.itemId ?? "";
7281
- const key = item.id ?? (id || anchorKey(item.scope) || `pos:${idx}`);
7282
- const akey = anchorKey(item.scope);
7283
- const selected = selectedItemId === id;
7284
- const isDirty = !!(id && dirtyKeys?.has(id) || akey && dirtyKeys?.has(akey));
7285
- const hasError = !!(id && errorKeys?.has(id) || akey && errorKeys?.has(akey));
7286
- const ruleClauses = item.facetRule ? summarizeFacetRule(item.facetRule, ruleLabelLookup) : [];
7287
- const isTargeted = ruleClauses.length > 0;
7288
- const ruleSummary = isTargeted ? ruleClauses.map((c) => c.label).join(" \xB7 ") : null;
7289
- return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("div", { className: "ra-row-shell", "data-selected": selected, children: [
7290
- /* @__PURE__ */ jsxs(
7291
- "button",
7292
- {
7293
- type: "button",
7294
- onClick: () => onSelect(id),
7295
- className: "ra-row",
7296
- "data-selected": selected,
7297
- children: [
7298
- /* @__PURE__ */ jsxs("div", { className: "ra-row-body", children: [
7299
- /* @__PURE__ */ jsx("div", { className: "ra-row-title", children: item.label }),
7300
- item.subtitle && /* @__PURE__ */ jsx("div", { className: "ra-row-sub", children: item.subtitle })
7301
- ] }),
7302
- isTargeted && /* @__PURE__ */ jsx(
7303
- "span",
7304
- {
7305
- className: "ra-row-rule-pip",
7306
- title: ruleSummary ? `Targeted: ${ruleSummary}` : "This item has targeting rules",
7307
- "aria-label": ruleSummary ? `Targeted: ${ruleSummary}` : "This item has targeting rules",
7308
- children: /* @__PURE__ */ jsx(Target, { className: "w-3 h-3", "aria-hidden": "true" })
7309
- }
7310
- ),
7311
- hasError ? /* @__PURE__ */ jsx(
7312
- "span",
7313
- {
7314
- className: "ra-error-pip",
7315
- title: "Save failed",
7316
- "aria-label": "Save failed"
7317
- }
7318
- ) : isDirty ? /* @__PURE__ */ jsx(
7319
- "span",
7320
- {
7321
- className: "ra-dirty-pip",
7322
- title: "Unsaved changes",
7323
- "aria-label": "Unsaved changes"
7324
- }
7325
- ) : null
7326
- ]
7327
- }
7328
- ),
7329
- (() => {
7330
- const cb = rowClipboard ? rowClipboard(item) : null;
7331
- const extra = rowActions ? rowActions(item) ?? void 0 : void 0;
7332
- if (!cb?.onCopy && !cb?.onDuplicate && !cb?.onCopyAndNewRule && !(extra && extra.length)) {
7333
- return null;
7334
- }
7335
- return /* @__PURE__ */ jsx(
7336
- RowContextMenu,
7473
+ !isLoading && !error && items.length > 0 && (() => {
7474
+ const renderRow = (item, idx, dimmed) => {
7475
+ const id = item.itemId ?? "";
7476
+ const key = item.id ?? (id || anchorKey(item.scope) || `pos:${idx}`);
7477
+ const akey = anchorKey(item.scope);
7478
+ const selected = selectedItemId === id;
7479
+ const isDirty = !!(id && dirtyKeys?.has(id) || akey && dirtyKeys?.has(akey));
7480
+ const hasError = !!(id && errorKeys?.has(id) || akey && errorKeys?.has(akey));
7481
+ const ruleClauses = item.facetRule ? summarizeFacetRule(item.facetRule, ruleLabelLookup) : [];
7482
+ const isTargeted = ruleClauses.length > 0;
7483
+ const ruleSummary = isTargeted ? ruleClauses.map((c) => c.label).join(" \xB7 ") : null;
7484
+ return /* @__PURE__ */ jsx("li", { "data-dimmed": dimmed || void 0, children: /* @__PURE__ */ jsxs("div", { className: "ra-row-shell", "data-selected": selected, children: [
7485
+ /* @__PURE__ */ jsxs(
7486
+ "button",
7337
7487
  {
7338
- onCopy: cb?.onCopy,
7339
- onDuplicate: cb?.onDuplicate,
7340
- onCopyAndNewRule: cb?.onCopyAndNewRule,
7341
- actions: extra,
7342
- i18n: {
7343
- copy: i18n.copy,
7344
- duplicateAction: i18n.duplicateAction,
7345
- copyAndNewRuleAction: i18n.copyAndNewRuleAction
7488
+ type: "button",
7489
+ onClick: () => onSelect(id),
7490
+ className: "ra-row",
7491
+ "data-selected": selected,
7492
+ style: dimmed ? { opacity: 0.7 } : void 0,
7493
+ children: [
7494
+ /* @__PURE__ */ jsxs("div", { className: "ra-row-body", children: [
7495
+ /* @__PURE__ */ jsx("div", { className: "ra-row-title", children: item.label }),
7496
+ item.subtitle && /* @__PURE__ */ jsx("div", { className: "ra-row-sub", children: item.subtitle })
7497
+ ] }),
7498
+ isTargeted && /* @__PURE__ */ jsx(
7499
+ "span",
7500
+ {
7501
+ className: "ra-row-rule-pip",
7502
+ title: ruleSummary ? `Targeted: ${ruleSummary}` : "This item has targeting rules",
7503
+ "aria-label": ruleSummary ? `Targeted: ${ruleSummary}` : "This item has targeting rules",
7504
+ children: /* @__PURE__ */ jsx(Target, { className: "w-3 h-3", "aria-hidden": "true" })
7505
+ }
7506
+ ),
7507
+ hasError ? /* @__PURE__ */ jsx(
7508
+ "span",
7509
+ {
7510
+ className: "ra-error-pip",
7511
+ title: "Save failed",
7512
+ "aria-label": "Save failed"
7513
+ }
7514
+ ) : isDirty ? /* @__PURE__ */ jsx(
7515
+ "span",
7516
+ {
7517
+ className: "ra-dirty-pip",
7518
+ title: "Unsaved changes",
7519
+ "aria-label": "Unsaved changes"
7520
+ }
7521
+ ) : null
7522
+ ]
7523
+ }
7524
+ ),
7525
+ (() => {
7526
+ const cb = rowClipboard ? rowClipboard(item) : null;
7527
+ const extra = rowActions ? rowActions(item) ?? void 0 : void 0;
7528
+ if (!cb?.onCopy && !cb?.onDuplicate && !cb?.onCopyAndNewRule && !(extra && extra.length)) {
7529
+ return null;
7530
+ }
7531
+ return /* @__PURE__ */ jsx(
7532
+ RowContextMenu,
7533
+ {
7534
+ onCopy: cb?.onCopy,
7535
+ onDuplicate: cb?.onDuplicate,
7536
+ onCopyAndNewRule: cb?.onCopyAndNewRule,
7537
+ actions: extra,
7538
+ i18n: {
7539
+ copy: i18n.copy,
7540
+ duplicateAction: i18n.duplicateAction,
7541
+ copyAndNewRuleAction: i18n.copyAndNewRuleAction
7542
+ }
7346
7543
  }
7544
+ );
7545
+ })()
7546
+ ] }) }, key);
7547
+ };
7548
+ if (!buckets) {
7549
+ return /* @__PURE__ */ jsx("ul", { className: "ra-sibling-list", children: items.map((item, idx) => renderRow(item, idx, false)) });
7550
+ }
7551
+ return /* @__PURE__ */ jsx("div", { className: "ra-sibling-buckets", children: buckets.map((bucket) => {
7552
+ const open = bucket.isActive || !collapsed.has(bucket.key);
7553
+ return /* @__PURE__ */ jsxs("section", { className: "ra-sibling-bucket", "data-bucket": bucket.key, children: [
7554
+ /* @__PURE__ */ jsxs(
7555
+ "button",
7556
+ {
7557
+ type: "button",
7558
+ className: "ra-sibling-bucket-header",
7559
+ onClick: () => !bucket.isActive && toggleBucket(bucket.key),
7560
+ "aria-expanded": open,
7561
+ disabled: bucket.isActive,
7562
+ style: {
7563
+ display: "flex",
7564
+ alignItems: "center",
7565
+ gap: "6px",
7566
+ width: "100%",
7567
+ padding: "6px 10px",
7568
+ background: "transparent",
7569
+ border: 0,
7570
+ borderTop: "1px solid hsl(var(--ra-border))",
7571
+ font: "inherit",
7572
+ fontSize: "11px",
7573
+ fontWeight: 600,
7574
+ textTransform: "uppercase",
7575
+ letterSpacing: "0.04em",
7576
+ color: "hsl(var(--ra-muted-text))",
7577
+ cursor: bucket.isActive ? "default" : "pointer"
7578
+ },
7579
+ children: [
7580
+ !bucket.isActive ? open ? /* @__PURE__ */ jsx(ChevronDown, { className: "w-3 h-3", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "w-3 h-3", "aria-hidden": "true" }) : null,
7581
+ /* @__PURE__ */ jsx("span", { children: bucket.label }),
7582
+ /* @__PURE__ */ jsx("span", { style: { marginLeft: "auto", fontWeight: 500 }, children: bucket.items.length })
7583
+ ]
7347
7584
  }
7348
- );
7349
- })()
7350
- ] }) }, key);
7351
- }) })
7585
+ ),
7586
+ open && /* @__PURE__ */ jsx("ul", { className: "ra-sibling-list", children: bucket.items.map(
7587
+ (item, idx) => renderRow(item, idx, !bucket.isActive)
7588
+ ) })
7589
+ ] }, bucket.key);
7590
+ }) });
7591
+ })()
7352
7592
  ] }),
7353
7593
  onLoadMore && /* @__PURE__ */ jsx(
7354
7594
  LoadMoreFooter,
@@ -8413,14 +8653,21 @@ var SaveAllProgress = ({
8413
8653
  }
8414
8654
  );
8415
8655
  };
8416
- function PreviewReopenPill({ anchorRef, onClick, ariaLabel, title, children }) {
8656
+ function PreviewReopenPill({
8657
+ anchorRef,
8658
+ onClick,
8659
+ ariaLabel,
8660
+ title,
8661
+ children,
8662
+ side = "right"
8663
+ }) {
8417
8664
  const [pos, setPos] = useState(null);
8418
8665
  const rafRef = useRef(null);
8419
8666
  useLayoutEffect(() => {
8420
8667
  const el = anchorRef.current;
8421
8668
  if (typeof window === "undefined") return;
8422
8669
  if (!el) {
8423
- setPos({ top: window.innerHeight / 2, right: 8 });
8670
+ setPos({ top: window.innerHeight / 2, right: 8, left: 8 });
8424
8671
  return;
8425
8672
  }
8426
8673
  const measure = () => {
@@ -8428,12 +8675,13 @@ function PreviewReopenPill({ anchorRef, onClick, ariaLabel, title, children }) {
8428
8675
  rafRef.current = requestAnimationFrame(() => {
8429
8676
  const rect = el.getBoundingClientRect();
8430
8677
  if (rect.width === 0 && rect.height === 0) {
8431
- setPos({ top: window.innerHeight / 2, right: 8 });
8678
+ setPos({ top: window.innerHeight / 2, right: 8, left: 8 });
8432
8679
  return;
8433
8680
  }
8434
8681
  setPos({
8435
8682
  top: rect.top + rect.height / 2,
8436
- right: Math.max(0, window.innerWidth - rect.right)
8683
+ right: Math.max(0, window.innerWidth - rect.right),
8684
+ left: Math.max(0, rect.left)
8437
8685
  });
8438
8686
  });
8439
8687
  };
@@ -8453,7 +8701,8 @@ function PreviewReopenPill({ anchorRef, onClick, ariaLabel, title, children }) {
8453
8701
  if (typeof document === "undefined") return null;
8454
8702
  const effectivePos = pos ?? {
8455
8703
  top: typeof window !== "undefined" ? window.innerHeight / 2 : 200,
8456
- right: 8
8704
+ right: 8,
8705
+ left: 8
8457
8706
  };
8458
8707
  return createPortal(
8459
8708
  /* @__PURE__ */ jsx(
@@ -8467,10 +8716,10 @@ function PreviewReopenPill({ anchorRef, onClick, ariaLabel, title, children }) {
8467
8716
  style: {
8468
8717
  position: "fixed",
8469
8718
  top: effectivePos.top,
8470
- right: effectivePos.right,
8719
+ ...side === "right" ? { right: effectivePos.right } : { left: effectivePos.left },
8471
8720
  // Pull half the pill width out into the gutter so it visually
8472
8721
  // anchors *to* the editor edge rather than sitting inside it.
8473
- transform: "translate(50%, -50%)"
8722
+ transform: side === "right" ? "translate(50%, -50%)" : "translate(-50%, -50%)"
8474
8723
  },
8475
8724
  children
8476
8725
  }
@@ -8892,6 +9141,7 @@ function RecordsAdminShellInner(props) {
8892
9141
  onTelemetry?.({ type: "presentation.change", recordType, from: presentation, to: next });
8893
9142
  setPresentation(next);
8894
9143
  }, [onTelemetry, recordType, presentation, setPresentation]);
9144
+ const [ruleSort, setRuleSort] = useState("recent");
8895
9145
  const [itemView, setItemView] = useItemViewPref({
8896
9146
  appId,
8897
9147
  recordType,
@@ -9502,6 +9752,25 @@ function RecordsAdminShellInner(props) {
9502
9752
  const ruleCatalogueRef = useRef([]);
9503
9753
  const onCreateRuleFromClipboardRef = useRef(null);
9504
9754
  const previewReopenAnchorRef = useRef(null);
9755
+ const railReopenAnchorRef = useRef(null);
9756
+ const RAIL_OPEN_STORAGE_KEY = "smartlinks-ui:recordsAdmin:railOpen";
9757
+ const [railOpen, setRailOpenState] = useState(() => {
9758
+ if (typeof window === "undefined") return true;
9759
+ try {
9760
+ const raw = window.sessionStorage.getItem(RAIL_OPEN_STORAGE_KEY);
9761
+ if (raw === "0") return false;
9762
+ if (raw === "1") return true;
9763
+ } catch {
9764
+ }
9765
+ return true;
9766
+ });
9767
+ const setRailOpen = useCallback((next) => {
9768
+ try {
9769
+ window.sessionStorage.setItem(RAIL_OPEN_STORAGE_KEY, next ? "1" : "0");
9770
+ } catch {
9771
+ }
9772
+ setRailOpenState(next);
9773
+ }, []);
9505
9774
  const { runWithGuard } = useDirtyNavigation({
9506
9775
  strategy: dirtyStrategy,
9507
9776
  isDirty: editorCtx.isDirty,
@@ -9532,6 +9801,7 @@ function RecordsAdminShellInner(props) {
9532
9801
  setDrawerOpen,
9533
9802
  sidePreviewOpen,
9534
9803
  setSidePreviewOpen,
9804
+ isPreviewNarrow,
9535
9805
  editorHeaderLabel: editorHeaderLabelRaw,
9536
9806
  editorHeaderSubtitle,
9537
9807
  editorHeaderMeta
@@ -9853,6 +10123,23 @@ function RecordsAdminShellInner(props) {
9853
10123
  ] })
9854
10124
  );
9855
10125
  }
10126
+ if (isPreviewNarrow) {
10127
+ return withNav(
10128
+ /* @__PURE__ */ jsxs("div", { className: "relative h-full", ref: previewAnchorRef, children: [
10129
+ baseEditor(),
10130
+ /* @__PURE__ */ jsx(
10131
+ DrawerPreview,
10132
+ {
10133
+ open: true,
10134
+ onClose: () => setSidePreviewOpen(false),
10135
+ label: i18n.preview,
10136
+ scopePicker,
10137
+ children: previewBody
10138
+ }
10139
+ )
10140
+ ] })
10141
+ );
10142
+ }
9856
10143
  return withNav(
9857
10144
  /* @__PURE__ */ jsxs("div", { className: "grid h-full", style: { gridTemplateColumns: "minmax(0, 1fr) minmax(280px, 420px)" }, children: [
9858
10145
  /* @__PURE__ */ jsx("div", { className: "overflow-hidden", children: baseEditor() }),
@@ -10070,6 +10357,7 @@ function RecordsAdminShellInner(props) {
10070
10357
  if (cardinality === "collection") {
10071
10358
  setRuleWizardStep(2);
10072
10359
  if (ruleWizardSeedMode) {
10360
+ skipNextItemResetRef.current = true;
10073
10361
  const id = coerceDraftItemId2(generateItemId);
10074
10362
  setSelectedItemId(id);
10075
10363
  }
@@ -10080,7 +10368,7 @@ function RecordsAdminShellInner(props) {
10080
10368
  setSelectedRecordId(DRAFT_ID3);
10081
10369
  }
10082
10370
  }
10083
- }, [cardinality, ruleWizardSeedMode, mintRuleWizardDraftKey, generateItemId]);
10371
+ }, [cardinality, ruleWizardSeedMode, mintRuleWizardDraftKey, generateItemId, skipNextItemResetRef]);
10084
10372
  const startRuleWizardDraft = useCallback((seed) => {
10085
10373
  setRuleWizardSeedMode(seed);
10086
10374
  setRuleWizardDraftKey(mintRuleWizardDraftKey());
@@ -10134,17 +10422,51 @@ function RecordsAdminShellInner(props) {
10134
10422
  buckets.set(hash, { rep: item, count: 1 });
10135
10423
  }
10136
10424
  }
10137
- return Array.from(buckets.values()).map(({ rep, count }) => ({
10138
- ...rep,
10139
- // Title carries the count + identity; the rule chips below render
10140
- // the friendly facet/value labels (via the lookup) so we don't
10141
- // repeat the same information in two places. Without this, a row
10142
- // had three echoes of the same rule (raw-key title, friendly chip,
10143
- // and the right-pane "Rule · …" header).
10144
- label: `${count} ${itemNoun}${count === 1 ? "" : "s"}`,
10145
- subtitle: void 0
10146
- }));
10147
- }, [isRuleTab, isCollection, filteredRuleItems, itemNoun]);
10425
+ const counts = recordList.ruleLifecycleCounts;
10426
+ return Array.from(buckets.entries()).map(([hash, { rep, count }]) => {
10427
+ const lc = counts.get(hash) ?? { active: 0, archived: 0, draft: 0, other: 0 };
10428
+ const lifecycleBadges = [];
10429
+ if (lc.active > 0) lifecycleBadges.push({
10430
+ label: i18n.lifecycleBadgeActive.replace("{n}", String(lc.active)),
10431
+ tone: "success"
10432
+ });
10433
+ if (lc.draft > 0) lifecycleBadges.push({
10434
+ label: i18n.lifecycleBadgeDraft.replace("{n}", String(lc.draft)),
10435
+ tone: "warning"
10436
+ });
10437
+ if (lc.archived > 0) lifecycleBadges.push({
10438
+ label: i18n.lifecycleBadgeArchived.replace("{n}", String(lc.archived)),
10439
+ tone: "neutral"
10440
+ });
10441
+ return {
10442
+ ...rep,
10443
+ // Title carries the count + identity; the rule chips below render
10444
+ // the friendly facet/value labels (via the lookup) so we don't
10445
+ // repeat the same information in two places.
10446
+ label: `${count} ${itemNoun}${count === 1 ? "" : "s"}`,
10447
+ subtitle: void 0,
10448
+ badges: [...rep.badges ?? [], ...lifecycleBadges],
10449
+ // Stash counts on the row for the sort comparator below.
10450
+ __lifecycleCounts: lc
10451
+ };
10452
+ });
10453
+ }, [isRuleTab, isCollection, filteredRuleItems, itemNoun, recordList.ruleLifecycleCounts, i18n]);
10454
+ const sortedCollectionRuleRailItems = useMemo(() => {
10455
+ if (!isRuleTab || !isCollection || ruleSort === "recent") return collectionRuleRailItems;
10456
+ const arr = [...collectionRuleRailItems];
10457
+ const counts = (r) => r.__lifecycleCounts ?? { active: 0, archived: 0 };
10458
+ if (ruleSort === "name") arr.sort((a, b) => a.label.localeCompare(b.label));
10459
+ else if (ruleSort === "activeCount") arr.sort((a, b) => counts(b).active - counts(a).active);
10460
+ else if (ruleSort === "hasArchived") {
10461
+ arr.sort((a, b) => {
10462
+ const ah = counts(a).archived > 0 ? 1 : 0;
10463
+ const bh = counts(b).archived > 0 ? 1 : 0;
10464
+ if (ah !== bh) return bh - ah;
10465
+ return counts(b).archived - counts(a).archived;
10466
+ });
10467
+ }
10468
+ return arr;
10469
+ }, [collectionRuleRailItems, isRuleTab, isCollection, ruleSort]);
10148
10470
  const activeRuleSummary = useMemo(() => {
10149
10471
  if (!isRuleTab || !isCollection) return null;
10150
10472
  if (!selectedRecordId || isDraftId3(selectedRecordId)) return null;
@@ -10283,9 +10605,9 @@ function RecordsAdminShellInner(props) {
10283
10605
  });
10284
10606
  }, [isLifecycleRail, selectedLifecycleKey, lcGroupBy, collectionItems.items]);
10285
10607
  const leftItems = isProductTab ? productListItems : isRuleTab ? applyFacetBrowseFilter(
10286
- isCollection ? collectionRuleRailItems : filteredRuleItems
10608
+ isCollection ? sortedCollectionRuleRailItems : filteredRuleItems
10287
10609
  ) : isLifecycleRail ? lifecycleRows : (isGlobalTab || isAllTab) && isCollection ? [collectionGlobalAllRow] : isRecordsTab ? applyFacetBrowseFilter(recordList.items) : [];
10288
- const railShowsHistoryDisclosure = isRecordsTab && !isLifecycleRail && !((isGlobalTab || isAllTab) && isCollection);
10610
+ const railShowsHistoryDisclosure = isRecordsTab && !isLifecycleRail && !isCollection;
10289
10611
  const filteredLeftItems = useMemo(() => {
10290
10612
  if (!railShowsHistoryDisclosure) return leftItems;
10291
10613
  return leftItems.filter((r) => {
@@ -10529,295 +10851,351 @@ function RecordsAdminShellInner(props) {
10529
10851
  {
10530
10852
  className: "flex-1 grid border-t overflow-hidden",
10531
10853
  style: {
10532
- gridTemplateColumns: railHidden ? "1fr" : "minmax(260px, 320px) 1fr",
10854
+ gridTemplateColumns: railHidden || !railOpen ? "1fr" : "minmax(260px, 320px) 1fr",
10533
10855
  borderColor: "hsl(var(--ra-border))"
10534
10856
  },
10535
10857
  children: [
10536
- !railHidden && /* @__PURE__ */ jsx("aside", { className: "border-r overflow-hidden flex flex-col", style: { borderColor: "hsl(var(--ra-border))", background: "hsl(var(--ra-surface))" }, children: isCollection && selectedItemId && collectionRailMode === "siblings" && ruleWizardStep === null ? /* @__PURE__ */ jsx(
10537
- SiblingRail,
10538
- {
10539
- items: scopedCollectionItemsList,
10540
- selectedItemId,
10541
- isLoading: collectionItems.isLoading,
10542
- error: collectionItems.error,
10543
- onBack: onItemBack,
10544
- onSelect: onItemOpen,
10545
- onCreate: onItemCreate,
10546
- itemNoun: itemNounLabel,
10547
- dirtyKeys,
10548
- errorKeys,
10549
- hasNextPage: !!collectionItems.hasNextPage,
10550
- isFetchingNextPage: !!collectionItems.isFetchingNextPage,
10551
- onLoadMore: () => {
10552
- void collectionItems.fetchNextPage();
10553
- },
10554
- rowClipboard,
10555
- rowActions: wrappedRecordActions,
10556
- contextKind: isLifecycleRailEarly && lifecycleBucketLabel ? lifecycleBucketLabel : activeScope === "rule" ? "Rule" : activeScope === "product" ? "Product" : activeScope === "collection" ? "Global" : activeScope === "all" ? "All records" : activeScope === "variant" ? "Variant" : activeScope === "batch" ? "Batch" : activeScope === "facet" ? "Facet" : void 0,
10557
- contextSummary: isLifecycleRailEarly && lifecycleBucketLabel ? `${scopedCollectionItemsList.length} ${itemNounLabel}${scopedCollectionItemsList.length === 1 ? "" : "s"}` : activeScope === "rule" ? activeRuleSummary : activeScope === "product" ? editorHeaderLabel ?? null : null,
10558
- i18n
10559
- }
10560
- ) : /* @__PURE__ */ jsxs(Fragment, { children: [
10561
- /* @__PURE__ */ jsx("div", { className: "px-1.5 py-2", children: /* @__PURE__ */ jsx(
10562
- ScopeTabs,
10858
+ !railHidden && railOpen && /* @__PURE__ */ jsxs("aside", { className: "border-r overflow-hidden flex flex-col relative", style: { borderColor: "hsl(var(--ra-border))", background: "hsl(var(--ra-surface))" }, children: [
10859
+ /* @__PURE__ */ jsx(
10860
+ "button",
10563
10861
  {
10564
- scopes: effectiveTopLevelScopes,
10565
- active: activeScope,
10566
- onChange: (s) => {
10567
- void runWithGuard(() => {
10568
- onTelemetry?.({ type: "scope.change", recordType, from: activeScope, to: s });
10569
- if (ruleWizardStep !== null) {
10570
- setRuleWizardStep(null);
10571
- setRuleWizardRule(null);
10572
- setDraftKind(null);
10573
- }
10574
- setActiveScope(s);
10575
- });
10576
- },
10577
- loading: probe.isLoading,
10578
- counts: {
10579
- // Products badge counts DISTINCT products that have at
10580
- // least one custom record — same semantics as Global /
10581
- // Rules. Catalogue size (which can be 1k–10k+) was
10582
- // misleading: "Products: 247" implied 247 customised
10583
- // products when in fact zero might be configured. Falls
10584
- // back to a lower bound when scope-counts truncated at
10585
- // the hard cap.
10586
- product: scopeCounts.productIds.size,
10587
- // The remaining tabs show actual record counts so hidden
10588
- // state (e.g. a rule-scoped competition) is visible from
10589
- // any tab. `useScopeCounts` returns 0 while loading, which
10590
- // is fine — the badges just appear once data lands.
10591
- collection: scopeCounts.counts.collection,
10592
- rule: scopeCounts.counts.rule,
10593
- variant: scopeCounts.counts.variant,
10594
- batch: scopeCounts.counts.batch,
10595
- facet: scopeCounts.counts.facet,
10596
- all: scopeCounts.counts.all
10862
+ type: "button",
10863
+ onClick: () => setRailOpen(false),
10864
+ "aria-label": i18n.closeList,
10865
+ title: i18n.closeList,
10866
+ className: "absolute top-1.5 right-1.5 z-10 p-1 rounded hover:bg-[hsl(var(--ra-muted))]",
10867
+ style: { color: "hsl(var(--ra-muted-text))" },
10868
+ children: /* @__PURE__ */ jsx(PanelLeftClose, { className: "w-3.5 h-3.5" })
10869
+ }
10870
+ ),
10871
+ isCollection && selectedItemId && collectionRailMode === "siblings" && ruleWizardStep === null ? /* @__PURE__ */ jsx(
10872
+ SiblingRail,
10873
+ {
10874
+ items: scopedCollectionItemsList,
10875
+ selectedItemId,
10876
+ isLoading: collectionItems.isLoading,
10877
+ error: collectionItems.error,
10878
+ onBack: onItemBack,
10879
+ onSelect: onItemOpen,
10880
+ onCreate: onItemCreate,
10881
+ itemNoun: itemNounLabel,
10882
+ dirtyKeys,
10883
+ errorKeys,
10884
+ hasNextPage: !!collectionItems.hasNextPage,
10885
+ isFetchingNextPage: !!collectionItems.isFetchingNextPage,
10886
+ onLoadMore: () => {
10887
+ void collectionItems.fetchNextPage();
10597
10888
  },
10598
- tooltips: { rule: i18n.rulesTabTooltip },
10599
- icons: icons.scope
10889
+ rowClipboard,
10890
+ rowActions: wrappedRecordActions,
10891
+ groupByLifecycle: true,
10892
+ lifecycle: lifecycleConfig,
10893
+ contextKind: isLifecycleRailEarly && lifecycleBucketLabel ? lifecycleBucketLabel : activeScope === "rule" ? "Rule" : activeScope === "product" ? "Product" : activeScope === "collection" ? "Global" : activeScope === "all" ? "All records" : activeScope === "variant" ? "Variant" : activeScope === "batch" ? "Batch" : activeScope === "facet" ? "Facet" : void 0,
10894
+ contextSummary: isLifecycleRailEarly && lifecycleBucketLabel ? `${scopedCollectionItemsList.length} ${itemNounLabel}${scopedCollectionItemsList.length === 1 ? "" : "s"}` : activeScope === "rule" ? activeRuleSummary : activeScope === "product" ? editorHeaderLabel ?? null : null,
10895
+ i18n
10600
10896
  }
10601
- ) }),
10602
- /* @__PURE__ */ jsxs("div", { className: "p-3 space-y-2.5 border-b", style: { borderColor: "hsl(var(--ra-border))" }, children: [
10603
- isRuleTab && /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-1.5", children: /* @__PURE__ */ jsxs(
10604
- "button",
10897
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
10898
+ /* @__PURE__ */ jsx("div", { className: "px-1.5 py-2", children: /* @__PURE__ */ jsx(
10899
+ ScopeTabs,
10605
10900
  {
10606
- type: "button",
10607
- onClick: () => onCreateRule(),
10608
- className: "ra-btn w-full",
10609
- "data-variant": "primary",
10610
- "aria-label": "New rule",
10611
- children: [
10612
- /* @__PURE__ */ jsx(Plus, { className: "w-3.5 h-3.5", "aria-hidden": "true" }),
10613
- /* @__PURE__ */ jsx("span", { children: "New rule" })
10614
- ]
10901
+ scopes: effectiveTopLevelScopes,
10902
+ active: activeScope,
10903
+ onChange: (s) => {
10904
+ void runWithGuard(() => {
10905
+ onTelemetry?.({ type: "scope.change", recordType, from: activeScope, to: s });
10906
+ if (ruleWizardStep !== null) {
10907
+ setRuleWizardStep(null);
10908
+ setRuleWizardRule(null);
10909
+ setDraftKind(null);
10910
+ }
10911
+ setActiveScope(s);
10912
+ });
10913
+ },
10914
+ loading: probe.isLoading,
10915
+ counts: {
10916
+ // Products badge counts DISTINCT products that have at
10917
+ // least one custom record — same semantics as Global /
10918
+ // Rules. Catalogue size (which can be 1k–10k+) was
10919
+ // misleading: "Products: 247" implied 247 customised
10920
+ // products when in fact zero might be configured. Falls
10921
+ // back to a lower bound when scope-counts truncated at
10922
+ // the hard cap.
10923
+ product: scopeCounts.productIds.size,
10924
+ // The remaining tabs show actual record counts so hidden
10925
+ // state (e.g. a rule-scoped competition) is visible from
10926
+ // any tab. `useScopeCounts` returns 0 while loading, which
10927
+ // is fine — the badges just appear once data lands.
10928
+ collection: scopeCounts.counts.collection,
10929
+ rule: scopeCounts.counts.rule,
10930
+ variant: scopeCounts.counts.variant,
10931
+ batch: scopeCounts.counts.batch,
10932
+ facet: scopeCounts.counts.facet,
10933
+ all: scopeCounts.counts.all
10934
+ },
10935
+ tooltips: { rule: i18n.rulesTabTooltip },
10936
+ icons: icons.scope
10615
10937
  }
10616
10938
  ) }),
10617
- showCreateGlobal && /* @__PURE__ */ jsxs(
10618
- "button",
10619
- {
10620
- type: "button",
10621
- onClick: onCreateGlobal,
10622
- className: "ra-btn w-full",
10623
- "data-variant": "primary",
10624
- "aria-label": "New global default",
10625
- children: [
10626
- /* @__PURE__ */ jsx(Plus, { className: "w-3.5 h-3.5", "aria-hidden": "true" }),
10627
- /* @__PURE__ */ jsx("span", { children: "New global default" })
10628
- ]
10629
- }
10630
- ),
10631
- !(isGlobalTab && !isCollection) && /* @__PURE__ */ jsxs(Fragment, { children: [
10632
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
10633
- /* @__PURE__ */ jsxs("div", { className: "relative flex-1 min-w-0", children: [
10634
- /* @__PURE__ */ jsx(Search, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 opacity-50" }),
10939
+ /* @__PURE__ */ jsxs("div", { className: "p-3 space-y-2.5 border-b", style: { borderColor: "hsl(var(--ra-border))" }, children: [
10940
+ isRuleTab && /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-1.5", children: /* @__PURE__ */ jsxs(
10941
+ "button",
10942
+ {
10943
+ type: "button",
10944
+ onClick: () => onCreateRule(),
10945
+ className: "ra-btn w-full",
10946
+ "data-variant": "primary",
10947
+ "aria-label": "New rule",
10948
+ children: [
10949
+ /* @__PURE__ */ jsx(Plus, { className: "w-3.5 h-3.5", "aria-hidden": "true" }),
10950
+ /* @__PURE__ */ jsx("span", { children: "New rule" })
10951
+ ]
10952
+ }
10953
+ ) }),
10954
+ showCreateGlobal && /* @__PURE__ */ jsxs(
10955
+ "button",
10956
+ {
10957
+ type: "button",
10958
+ onClick: onCreateGlobal,
10959
+ className: "ra-btn w-full",
10960
+ "data-variant": "primary",
10961
+ "aria-label": "New global default",
10962
+ children: [
10963
+ /* @__PURE__ */ jsx(Plus, { className: "w-3.5 h-3.5", "aria-hidden": "true" }),
10964
+ /* @__PURE__ */ jsx("span", { children: "New global default" })
10965
+ ]
10966
+ }
10967
+ ),
10968
+ !(isGlobalTab && !isCollection) && /* @__PURE__ */ jsxs(Fragment, { children: [
10969
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
10970
+ /* @__PURE__ */ jsxs("div", { className: "relative flex-1 min-w-0", children: [
10971
+ /* @__PURE__ */ jsx(Search, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 opacity-50" }),
10972
+ /* @__PURE__ */ jsx(
10973
+ "input",
10974
+ {
10975
+ type: "text",
10976
+ value: search,
10977
+ onChange: (e) => setSearch(e.target.value),
10978
+ placeholder: i18n.searchPlaceholder,
10979
+ className: "w-full pl-8 pr-3 py-1.5 text-xs rounded-md border bg-transparent focus:outline-none focus:ring-1",
10980
+ style: { borderColor: "hsl(var(--ra-border))", color: "hsl(var(--ra-text))" }
10981
+ }
10982
+ )
10983
+ ] }),
10635
10984
  /* @__PURE__ */ jsx(
10636
- "input",
10985
+ PresentationSwitcher,
10637
10986
  {
10638
- type: "text",
10639
- value: search,
10640
- onChange: (e) => setSearch(e.target.value),
10641
- placeholder: i18n.searchPlaceholder,
10642
- className: "w-full pl-8 pr-3 py-1.5 text-xs rounded-md border bg-transparent focus:outline-none focus:ring-1",
10643
- style: { borderColor: "hsl(var(--ra-border))", color: "hsl(var(--ra-text))" }
10987
+ options: showPresentationSwitcher ? presentations : [],
10988
+ value: presentation,
10989
+ onChange: onPresentationChange,
10990
+ i18n
10644
10991
  }
10645
10992
  )
10646
10993
  ] }),
10647
- /* @__PURE__ */ jsx(
10648
- PresentationSwitcher,
10649
- {
10650
- options: showPresentationSwitcher ? presentations : [],
10651
- value: presentation,
10652
- onChange: onPresentationChange,
10653
- i18n
10654
- }
10655
- )
10656
- ] }),
10657
- isProductTab && !productPinned && (() => {
10658
- const cfg = scopeCounts.productIds;
10659
- let configured = 0;
10660
- for (const p of productBrowse.items) if (cfg.has(p.id)) configured += 1;
10661
- const total = productBrowse.items.length;
10662
- return /* @__PURE__ */ jsx(
10663
- StatusFilterPills,
10994
+ isProductTab && !productPinned && (() => {
10995
+ const cfg = scopeCounts.productIds;
10996
+ let configured = 0;
10997
+ for (const p of productBrowse.items) if (cfg.has(p.id)) configured += 1;
10998
+ const total = productBrowse.items.length;
10999
+ return /* @__PURE__ */ jsx(
11000
+ StatusFilterPills,
11001
+ {
11002
+ value: filter,
11003
+ onChange: setFilter,
11004
+ counts: {
11005
+ all: total,
11006
+ configured,
11007
+ partial: 0,
11008
+ empty: total - configured
11009
+ },
11010
+ hideZero: ["partial"],
11011
+ i18n
11012
+ }
11013
+ );
11014
+ })(),
11015
+ isRuleTab && /* @__PURE__ */ jsx(
11016
+ FacetBrowseFilter,
10664
11017
  {
10665
- value: filter,
10666
- onChange: setFilter,
10667
- counts: {
10668
- all: total,
10669
- configured,
10670
- partial: 0,
10671
- empty: total - configured
10672
- },
10673
- hideZero: ["partial"],
10674
- i18n
11018
+ facets: facetBrowseFacets,
11019
+ value: facetBrowseFilter,
11020
+ onChange: setFacetBrowseFilter,
11021
+ isLoading: facetBrowse.isLoading
10675
11022
  }
10676
- );
10677
- })(),
10678
- isRuleTab && /* @__PURE__ */ jsx(
10679
- FacetBrowseFilter,
11023
+ ),
11024
+ isRuleTab && isCollection && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs", children: [
11025
+ /* @__PURE__ */ jsx(
11026
+ "label",
11027
+ {
11028
+ htmlFor: "ra-rule-sort",
11029
+ style: { color: "hsl(var(--ra-text-muted))" },
11030
+ children: i18n.ruleSortLabel
11031
+ }
11032
+ ),
11033
+ /* @__PURE__ */ jsxs(
11034
+ "select",
11035
+ {
11036
+ id: "ra-rule-sort",
11037
+ value: ruleSort,
11038
+ onChange: (e) => setRuleSort(e.target.value),
11039
+ className: "text-xs px-2 py-1 rounded-md border bg-transparent focus:outline-none focus:ring-1",
11040
+ style: { borderColor: "hsl(var(--ra-border))", color: "hsl(var(--ra-text))" },
11041
+ children: [
11042
+ /* @__PURE__ */ jsx("option", { value: "recent", children: i18n.ruleSortRecent }),
11043
+ /* @__PURE__ */ jsx("option", { value: "name", children: i18n.ruleSortName }),
11044
+ /* @__PURE__ */ jsx("option", { value: "activeCount", children: i18n.ruleSortActiveCount }),
11045
+ /* @__PURE__ */ jsx("option", { value: "hasArchived", children: i18n.ruleSortHasArchived })
11046
+ ]
11047
+ }
11048
+ )
11049
+ ] })
11050
+ ] })
11051
+ ] }),
11052
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto", children: [
11053
+ hasSingletonConflicts && /* @__PURE__ */ jsx(
11054
+ SingletonConflictBanner,
10680
11055
  {
10681
- facets: facetBrowseFacets,
10682
- value: facetBrowseFilter,
10683
- onChange: setFacetBrowseFilter,
10684
- isLoading: facetBrowse.isLoading
10685
- }
10686
- )
10687
- ] })
10688
- ] }),
10689
- /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto", children: [
10690
- hasSingletonConflicts && /* @__PURE__ */ jsx(
10691
- SingletonConflictBanner,
10692
- {
10693
- conflictCount: singletonConflicts.length,
10694
- duplicateCount: totalDuplicateCount,
10695
- onResolve: () => {
10696
- const first = singletonConflicts[0]?.duplicates[0] ?? singletonConflicts[0]?.active;
10697
- if (first?.id) {
10698
- void runWithGuard(() => {
10699
- setSelectedRecordId(first.id);
10700
- });
10701
- }
10702
- },
10703
- onArchiveDuplicates: enableArchiveDuplicates ? async () => {
10704
- const ids = singletonConflicts.flatMap((c) => c.duplicates.map((d) => d.id)).filter((id) => !!id);
10705
- for (const id of ids) {
10706
- try {
10707
- const updated = await SL.app.records.update(collectionId, appId, id, { status: archivedStatusValue }, true);
10708
- if (updated) patchRecordIntoCaches(queryClient, ctx, updated);
10709
- onTelemetry?.({
10710
- type: "recordAction.invoke",
10711
- recordType,
10712
- key: "conflict.archiveDuplicates",
10713
- ref: id
11056
+ conflictCount: singletonConflicts.length,
11057
+ duplicateCount: totalDuplicateCount,
11058
+ onResolve: () => {
11059
+ const first = singletonConflicts[0]?.duplicates[0] ?? singletonConflicts[0]?.active;
11060
+ if (first?.id) {
11061
+ void runWithGuard(() => {
11062
+ setSelectedRecordId(first.id);
10714
11063
  });
10715
- } catch (err) {
10716
- console.warn("[RecordsAdminShell] archive-duplicate failed", id, err);
10717
11064
  }
10718
- }
10719
- queryClient.invalidateQueries({
10720
- queryKey: scopeCountsQueryKey(ctx.collectionId, ctx.appId, ctx.recordType)
10721
- });
10722
- } : void 0,
10723
- onDeleteDuplicates: enableDeleteDuplicates ? async () => {
10724
- const ids = singletonConflicts.flatMap((c) => c.duplicates.map((d) => d.id)).filter((id) => !!id);
10725
- for (const id of ids) {
10726
- try {
10727
- await SL.app.records.remove(collectionId, appId, id, true);
10728
- removeRecordFromCaches(queryClient, ctx, id);
10729
- onTelemetry?.({
10730
- type: "recordAction.invoke",
10731
- recordType,
10732
- key: "conflict.deleteDuplicates",
10733
- ref: id
10734
- });
10735
- } catch (err) {
10736
- console.warn("[RecordsAdminShell] delete-duplicate failed", id, err);
11065
+ },
11066
+ onArchiveDuplicates: enableArchiveDuplicates ? async () => {
11067
+ const ids = singletonConflicts.flatMap((c) => c.duplicates.map((d) => d.id)).filter((id) => !!id);
11068
+ for (const id of ids) {
11069
+ try {
11070
+ const updated = await SL.app.records.update(collectionId, appId, id, { status: archivedStatusValue }, true);
11071
+ if (updated) patchRecordIntoCaches(queryClient, ctx, updated);
11072
+ onTelemetry?.({
11073
+ type: "recordAction.invoke",
11074
+ recordType,
11075
+ key: "conflict.archiveDuplicates",
11076
+ ref: id
11077
+ });
11078
+ } catch (err) {
11079
+ console.warn("[RecordsAdminShell] archive-duplicate failed", id, err);
11080
+ }
11081
+ }
11082
+ queryClient.invalidateQueries({
11083
+ queryKey: scopeCountsQueryKey(ctx.collectionId, ctx.appId, ctx.recordType)
11084
+ });
11085
+ } : void 0,
11086
+ onDeleteDuplicates: enableDeleteDuplicates ? async () => {
11087
+ const ids = singletonConflicts.flatMap((c) => c.duplicates.map((d) => d.id)).filter((id) => !!id);
11088
+ for (const id of ids) {
11089
+ try {
11090
+ await SL.app.records.remove(collectionId, appId, id, true);
11091
+ removeRecordFromCaches(queryClient, ctx, id);
11092
+ onTelemetry?.({
11093
+ type: "recordAction.invoke",
11094
+ recordType,
11095
+ key: "conflict.deleteDuplicates",
11096
+ ref: id
11097
+ });
11098
+ } catch (err) {
11099
+ console.warn("[RecordsAdminShell] delete-duplicate failed", id, err);
11100
+ }
10737
11101
  }
11102
+ queryClient.invalidateQueries({
11103
+ queryKey: scopeCountsQueryKey(ctx.collectionId, ctx.appId, ctx.recordType)
11104
+ });
11105
+ } : void 0,
11106
+ i18n: {
11107
+ title: i18n.conflictBannerTitle,
11108
+ bodyOne: i18n.conflictBannerBodyOne,
11109
+ bodyMany: i18n.conflictBannerBodyMany,
11110
+ archiveLabel: i18n.conflictArchiveDuplicates,
11111
+ deleteLabel: i18n.conflictDeleteDuplicates,
11112
+ deleteConfirm: i18n.conflictDeleteConfirm,
11113
+ resolveLabel: i18n.conflictResolveLabel
10738
11114
  }
10739
- queryClient.invalidateQueries({
10740
- queryKey: scopeCountsQueryKey(ctx.collectionId, ctx.appId, ctx.recordType)
10741
- });
10742
- } : void 0,
10743
- i18n: {
10744
- title: i18n.conflictBannerTitle,
10745
- bodyOne: i18n.conflictBannerBodyOne,
10746
- bodyMany: i18n.conflictBannerBodyMany,
10747
- archiveLabel: i18n.conflictArchiveDuplicates,
10748
- deleteLabel: i18n.conflictDeleteDuplicates,
10749
- deleteConfirm: i18n.conflictDeleteConfirm,
10750
- resolveLabel: i18n.conflictResolveLabel
10751
- }
10752
- }
10753
- ),
10754
- isGlobalTab && !isCollection && !hasSingletonConflicts ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
10755
- leftLoading && /* @__PURE__ */ jsx(LoadingState, {}),
10756
- !leftLoading && leftError && /* @__PURE__ */ jsx(ErrorState, { error: leftError }),
10757
- !leftLoading && !leftError && decoratedLeftItems.length === 0 && (renderEmptyState ? renderEmptyState({ scope: activeScope }) : /* @__PURE__ */ jsx(
10758
- EmptyState,
10759
- {
10760
- icon: search ? icons.empty.search : icons.empty.default,
10761
- title: search ? i18n.noResults : i18n.railEmptyTitle,
10762
- body: search ? void 0 : isRuleTab ? i18n.rulesEmptyBody : i18n.railEmptyBody
10763
11115
  }
10764
- )),
10765
- !leftLoading && !leftError && decoratedLeftItems.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
10766
- /* @__PURE__ */ jsx(
10767
- RecordList,
11116
+ ),
11117
+ isGlobalTab && !isCollection && !hasSingletonConflicts ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
11118
+ leftLoading && /* @__PURE__ */ jsx(LoadingState, {}),
11119
+ !leftLoading && leftError && /* @__PURE__ */ jsx(ErrorState, { error: leftError }),
11120
+ !leftLoading && !leftError && decoratedLeftItems.length === 0 && (renderEmptyState ? renderEmptyState({ scope: activeScope }) : /* @__PURE__ */ jsx(
11121
+ EmptyState,
10768
11122
  {
10769
- items: decoratedLeftItems,
10770
- selectedId: leftSelectedId,
10771
- selectedAnchorKey: leftSelectedAnchorKey,
10772
- onSelect: onLeftSelect,
10773
- dirtyId,
10774
- dirtyAnchorKey,
10775
- dirtyKeys,
10776
- errorKeys,
10777
- presentation: effectivePresentation,
10778
- renderListRow,
10779
- groupBy: (
10780
- // The synthetic "All items" row in collection mode is a
10781
- // navigational anchor, not a real record — applying the
10782
- // host's groupBy bucketed it under "Other" (its
10783
- // data is null). Skip grouping for that single-row rail.
10784
- (isGlobalTab || isAllTab) && isCollection || isLifecycleRail ? void 0 : effectiveGroupBy
10785
- ),
10786
- renderGroupActions: renderRuleGroupActions,
10787
- rowClipboard,
10788
- rowActions: wrappedRecordActions,
10789
- i18n,
10790
- historyBySlot: railHistoryBySlot
11123
+ icon: search ? icons.empty.search : icons.empty.default,
11124
+ title: search ? i18n.noResults : i18n.railEmptyTitle,
11125
+ body: search ? void 0 : isRuleTab ? i18n.rulesEmptyBody : i18n.railEmptyBody
10791
11126
  }
10792
- ),
10793
- isProductTab && !productPinned && /* @__PURE__ */ jsx(
10794
- LoadMoreFooter,
10795
- {
10796
- shown: leftItems.length,
10797
- hasNextPage: !!productBrowse.hasNextPage,
10798
- isFetchingNextPage: !!productBrowse.isFetchingNextPage,
10799
- onLoadMore: () => {
10800
- void productBrowse.fetchNextPage();
11127
+ )),
11128
+ !leftLoading && !leftError && decoratedLeftItems.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
11129
+ /* @__PURE__ */ jsx(
11130
+ RecordList,
11131
+ {
11132
+ items: decoratedLeftItems,
11133
+ selectedId: leftSelectedId,
11134
+ selectedAnchorKey: leftSelectedAnchorKey,
11135
+ onSelect: onLeftSelect,
11136
+ dirtyId,
11137
+ dirtyAnchorKey,
11138
+ dirtyKeys,
11139
+ errorKeys,
11140
+ presentation: effectivePresentation,
11141
+ renderListRow,
11142
+ groupBy: (
11143
+ // The synthetic "All items" row in collection mode is a
11144
+ // navigational anchor, not a real record — applying the
11145
+ // host's groupBy bucketed it under "Other" (its
11146
+ // data is null). Skip grouping for that single-row rail.
11147
+ (isGlobalTab || isAllTab) && isCollection || isLifecycleRail ? void 0 : effectiveGroupBy
11148
+ ),
11149
+ renderGroupActions: renderRuleGroupActions,
11150
+ rowClipboard,
11151
+ rowActions: wrappedRecordActions,
11152
+ i18n,
11153
+ historyBySlot: railHistoryBySlot
10801
11154
  }
10802
- }
10803
- ),
10804
- isRecordsTab && (!isCollection || !(isAllTab || isGlobalTab)) && /* @__PURE__ */ jsx(
10805
- LoadMoreFooter,
10806
- {
10807
- shown: recordList.items.length,
10808
- total: recordList.total,
10809
- hasNextPage: !!recordList.hasNextPage,
10810
- isFetchingNextPage: !!recordList.isFetchingNextPage,
10811
- onLoadMore: () => {
10812
- void recordList.fetchNextPage();
11155
+ ),
11156
+ isProductTab && !productPinned && /* @__PURE__ */ jsx(
11157
+ LoadMoreFooter,
11158
+ {
11159
+ shown: leftItems.length,
11160
+ hasNextPage: !!productBrowse.hasNextPage,
11161
+ isFetchingNextPage: !!productBrowse.isFetchingNextPage,
11162
+ onLoadMore: () => {
11163
+ void productBrowse.fetchNextPage();
11164
+ }
10813
11165
  }
10814
- }
10815
- )
11166
+ ),
11167
+ isRecordsTab && (!isCollection || !(isAllTab || isGlobalTab)) && /* @__PURE__ */ jsx(
11168
+ LoadMoreFooter,
11169
+ {
11170
+ shown: recordList.items.length,
11171
+ total: recordList.total,
11172
+ hasNextPage: !!recordList.hasNextPage,
11173
+ isFetchingNextPage: !!recordList.isFetchingNextPage,
11174
+ onLoadMore: () => {
11175
+ void recordList.fetchNextPage();
11176
+ }
11177
+ }
11178
+ )
11179
+ ] })
10816
11180
  ] })
10817
11181
  ] })
10818
11182
  ] })
10819
- ] }) }),
10820
- /* @__PURE__ */ jsxs("main", { className: "overflow-hidden", children: [
11183
+ ] }),
11184
+ /* @__PURE__ */ jsxs("main", { className: "overflow-hidden relative", ref: railReopenAnchorRef, children: [
11185
+ !railHidden && !railOpen && /* @__PURE__ */ jsxs(
11186
+ PreviewReopenPill,
11187
+ {
11188
+ anchorRef: railReopenAnchorRef,
11189
+ side: "left",
11190
+ onClick: () => setRailOpen(true),
11191
+ ariaLabel: i18n.openList,
11192
+ title: i18n.openList,
11193
+ children: [
11194
+ /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true" }),
11195
+ i18n.openList
11196
+ ]
11197
+ }
11198
+ ),
10821
11199
  ruleWizardStep !== null && /* @__PURE__ */ jsxs(
10822
11200
  NewRuleWizard,
10823
11201
  {
@@ -10920,6 +11298,8 @@ function RecordsAdminShellInner(props) {
10920
11298
  onLoadMore: () => {
10921
11299
  void collectionItems.fetchNextPage();
10922
11300
  },
11301
+ groupByLifecycle: true,
11302
+ lifecycle: lifecycleConfig,
10923
11303
  i18n
10924
11304
  }
10925
11305
  ),