@proveanything/smartlinks-utils-ui 0.9.0 → 0.9.2

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,11 +1,11 @@
1
- import { styleInject } from '../../chunk-NSQRNFT2.js';
1
+ import '../../chunk-5UQQYXCX.js';
2
2
  import { FacetRuleEditor } from '../../chunk-JMCV6FOW.js';
3
3
  import { useFacets } from '../../chunk-4LHF5JB7.js';
4
4
  import { cn } from '../../chunk-L7FQ52F5.js';
5
5
  import { parsedRefToTarget, parsedRefToScope, matchRecords, scopesEqual, getRecordById, listRecords, upsertRecord, updateRecord, createRecord, removeRecord } from '../../chunk-KFKVGUUP.js';
6
6
  export { bulkDelete, bulkUpsert, createRecord, getRecordById, listRecords, matchRecords, parsedRefToScope, parsedRefToTarget, removeRecord, restoreRecord, scopesEqual, upsertRecord } from '../../chunk-KFKVGUUP.js';
7
7
  import { createContext, useState, useEffect, useCallback, useMemo, useRef, useContext, useSyncExternalStore, createElement, useId } from 'react';
8
- import { ChevronDown, Database, Lightbulb, SearchX, Inbox, LayoutGrid, Eye, MoreHorizontal, Download, Upload, Trash2, Copy, Pencil, Plus, CircleDashed, ArrowDownLeft, CheckCircle2, SlidersHorizontal, Globe, Tag, Boxes, Layers, Package, Rows3, List, ChevronRight, Eraser, ClipboardPaste, Box, X, Image, Table, ArrowLeft, ChevronLeft, AlertTriangle, Info, HelpCircle, Search, CornerDownLeft, Circle, AlertCircle, Undo2, Save, Loader2, XCircle, ArrowRight, BookOpen, Target, Settings2 } from 'lucide-react';
8
+ 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, Rows3, ChevronRight, Eraser, ClipboardPaste, Box, X, Image, Table, ArrowLeft, ChevronLeft, AlertTriangle, Info, HelpCircle, Search, CornerDownLeft, Circle, AlertCircle, Undo2, Save, Loader2, XCircle, ArrowRight, BookOpen, Globe2, Target, Check, Settings2 } from 'lucide-react';
9
9
  import { useQuery, useQueryClient, useInfiniteQuery } from '@tanstack/react-query';
10
10
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
11
11
  import { createPortal } from 'react-dom';
@@ -18,7 +18,8 @@ var DEFAULT_ICONS = {
18
18
  batch: Boxes,
19
19
  facet: Tag,
20
20
  universal: Globe,
21
- rule: SlidersHorizontal
21
+ rule: SlidersHorizontal,
22
+ all: List
22
23
  },
23
24
  status: { own: CheckCircle2, inherited: ArrowDownLeft, missing: CircleDashed },
24
25
  action: {
@@ -143,7 +144,13 @@ var DEFAULT_I18N = {
143
144
  itemsAllLabel: "All items",
144
145
  subtitleEmpty: "Not set",
145
146
  subtitleConfigured: "Configured",
146
- subtitleInherited: "Inherited"
147
+ subtitleInherited: "Inherited",
148
+ rulesTabTooltip: "Use rules to scope records to a targeted audience or a subset of products.",
149
+ rulesEmptyBody: "Create a rule to target a subset of products or a specific audience \u2014 for example, \u201Conly premium tier customers in Germany\u201D.",
150
+ hookBeforeSaveFailed: "Couldn't save: {message}",
151
+ hookAfterSaveFailed: "Saved, but a follow-up step failed: {message}",
152
+ hookBeforeDeleteFailed: "Couldn't delete: {message}",
153
+ hookAfterDeleteFailed: "Deleted, but a follow-up step failed: {message}"
147
154
  };
148
155
 
149
156
  // src/components/RecordsAdmin/types/presentation.ts
@@ -204,6 +211,17 @@ var buildRef = (a) => {
204
211
  if (a.proofId) parts.push(`proof:${a.proofId}`);
205
212
  return parts.join("/");
206
213
  };
214
+ var anchorKey = (scope) => {
215
+ if (!scope) return "";
216
+ return buildRef({
217
+ productId: scope.productId,
218
+ variantId: scope.variantId,
219
+ batchId: scope.batchId,
220
+ facetId: scope.facetId,
221
+ facetValue: scope.facetValue,
222
+ proofId: scope.proofId
223
+ });
224
+ };
207
225
  var resolutionChain = (target, supportedScopes) => {
208
226
  const chain = [];
209
227
  if (supportedScopes.includes("batch") && target.batchId && target.productId) {
@@ -224,6 +242,38 @@ var resolutionChain = (target, supportedScopes) => {
224
242
  return Array.from(new Set(chain));
225
243
  };
226
244
 
245
+ // src/components/RecordsAdmin/data/ruleHash.ts
246
+ var normaliseRule = (rule) => {
247
+ if (!rule || !Array.isArray(rule.all) || rule.all.length === 0) return null;
248
+ const clauses = rule.all.map((clause) => {
249
+ const facetKey = String(clause.facetKey ?? "").trim();
250
+ const anyOfRaw = clause.anyOf ?? [];
251
+ const anyOf = Array.isArray(anyOfRaw) ? Array.from(new Set(anyOfRaw.map((v) => String(v)))).sort() : [];
252
+ return { facetKey, anyOf };
253
+ }).filter((c) => c.facetKey && c.anyOf.length > 0).sort((a, b) => a.facetKey.localeCompare(b.facetKey));
254
+ if (clauses.length === 0) return null;
255
+ return { all: clauses };
256
+ };
257
+ var fnv1a = (s) => {
258
+ let h = 2166136261;
259
+ for (let i = 0; i < s.length; i += 1) {
260
+ h ^= s.charCodeAt(i);
261
+ h = Math.imul(h, 16777619);
262
+ }
263
+ return (h >>> 0).toString(16).padStart(8, "0");
264
+ };
265
+ var ruleHash = (rule) => {
266
+ const norm = normaliseRule(rule);
267
+ if (!norm) return null;
268
+ return fnv1a(JSON.stringify(norm));
269
+ };
270
+ var summariseRule = (rule) => {
271
+ const norm = normaliseRule(rule);
272
+ if (!norm) return "No rule";
273
+ return norm.all.map((c) => `${c.facetKey}=${c.anyOf.join(",")}`).join(" \xB7 ");
274
+ };
275
+ var rulesEqual = (a, b) => ruleHash(a) === ruleHash(b);
276
+
227
277
  // src/components/RecordsAdmin/data/resolveRecord.ts
228
278
  var resolveRecord = async (args) => {
229
279
  const target = parsedRefToTarget(args.target);
@@ -449,6 +499,70 @@ var useScopeProbe = ({ SL, collectionId, admin = true, enabled = true }) => {
449
499
  error: query.error ?? null
450
500
  };
451
501
  };
502
+ var QK_BASE = ["records-admin", "scope-counts"];
503
+ var classify = (rec) => {
504
+ const hasRule = !!rec.facetRule;
505
+ if (hasRule) return "rule";
506
+ const productId = rec.productId ?? void 0;
507
+ const variantId = rec.variantId ?? void 0;
508
+ const batchId = rec.batchId ?? void 0;
509
+ if (batchId) return "batch";
510
+ if (variantId) return "variant";
511
+ if (productId) return "product";
512
+ return "collection";
513
+ };
514
+ var useScopeCounts = (args) => {
515
+ const { ctx, enabled = true, maxRecords = 500, pageSize = 100 } = args;
516
+ const queryKey = useMemo(
517
+ () => [...QK_BASE, ctx.collectionId, ctx.appId, ctx.recordType ?? null],
518
+ [ctx.collectionId, ctx.appId, ctx.recordType]
519
+ );
520
+ const query = useQuery({
521
+ queryKey,
522
+ enabled: enabled && !!ctx.collectionId && !!ctx.appId,
523
+ staleTime: 3e4,
524
+ queryFn: async () => {
525
+ const all = [];
526
+ let offset = 0;
527
+ let truncated = false;
528
+ for (let page = 0; page < Math.ceil(maxRecords / pageSize); page += 1) {
529
+ const res = await listRecords(ctx, { limit: pageSize, offset });
530
+ all.push(...res.data);
531
+ if (!res.hasMore) break;
532
+ offset += res.data.length;
533
+ if (all.length >= maxRecords) {
534
+ truncated = true;
535
+ break;
536
+ }
537
+ }
538
+ return { records: all, truncated };
539
+ }
540
+ });
541
+ const result = useMemo(() => {
542
+ const records = query.data?.records ?? [];
543
+ const truncated = query.data?.truncated ?? false;
544
+ const counts = {
545
+ collection: 0,
546
+ product: 0,
547
+ variant: 0,
548
+ batch: 0,
549
+ facet: 0,
550
+ rule: 0,
551
+ all: 0
552
+ };
553
+ for (const rec of records) counts[classify(rec)] += 1;
554
+ counts.all = records.length;
555
+ return {
556
+ counts,
557
+ total: records.length,
558
+ isLoading: query.isLoading,
559
+ error: query.error ?? null,
560
+ truncated
561
+ };
562
+ }, [query.data, query.isLoading, query.error]);
563
+ return result;
564
+ };
565
+ var scopeCountsQueryKey = (collectionId, appId, recordType) => [...QK_BASE, collectionId, appId, recordType ?? null];
452
566
  var defaultClassify = (r) => {
453
567
  if (!r.data) return "empty";
454
568
  const keys = Object.keys(r.data);
@@ -457,6 +571,7 @@ var defaultClassify = (r) => {
457
571
  };
458
572
  var matchesScope = (kind, rec, _p) => {
459
573
  const hasRule = !!rec.facetRule;
574
+ if (kind === "all") return true;
460
575
  if (kind === "rule") return hasRule;
461
576
  if (hasRule) return false;
462
577
  const productId = rec.productId ?? void 0;
@@ -479,14 +594,14 @@ var matchesContext = (p, ctx) => {
479
594
  return true;
480
595
  };
481
596
  var toSummary = (rec) => {
482
- const ref = rec.ref ?? "";
483
597
  const productId = rec.productId ?? void 0;
484
598
  const variantId = rec.variantId ?? void 0;
485
599
  const batchId = rec.batchId ?? void 0;
486
600
  const proofId = rec.proofId ?? void 0;
601
+ const synthRaw = batchId ? `product:${productId ?? ""}/batch:${batchId}` : variantId ? `product:${productId ?? ""}/variant:${variantId}` : productId ? `product:${productId}` : "";
487
602
  const scope = {
488
603
  kind: batchId ? "batch" : variantId ? "variant" : productId ? "product" : "collection",
489
- raw: ref,
604
+ raw: synthRaw,
490
605
  productId,
491
606
  variantId,
492
607
  batchId,
@@ -495,10 +610,13 @@ var toSummary = (rec) => {
495
610
  const facetRule = rec.facetRule ?? null;
496
611
  if (facetRule) scope.kind = "rule";
497
612
  const ruleLabel = facetRule && facetRule.all && facetRule.all.length > 0 ? facetRule.all.length === 1 ? "Rule \xB7 1 facet" : `Rule \xB7 ${facetRule.all.length} facets` : null;
498
- const fallbackLabel = scope.batchId ?? scope.variantId ?? scope.productId ?? ruleLabel ?? (ref || "Global record");
613
+ const fallbackLabel = scope.batchId ?? scope.variantId ?? scope.productId ?? ruleLabel ?? "Global record";
499
614
  return {
500
615
  id: rec.id,
501
- ref,
616
+ // `RecordSummary.ref` is preserved on the type for host display only
617
+ // (e.g. row renderers that want to show whatever the host put in
618
+ // `record.ref`). Framework code never uses it.
619
+ ref: rec.ref ?? "",
502
620
  scope,
503
621
  data: rec.data ?? null,
504
622
  status: "configured",
@@ -507,14 +625,14 @@ var toSummary = (rec) => {
507
625
  facetRule
508
626
  };
509
627
  };
510
- var QK_BASE = ["records-admin", "list"];
628
+ var QK_BASE2 = ["records-admin", "list"];
511
629
  var useRecordList = (args) => {
512
630
  const {
513
631
  ctx,
514
632
  scopeKind,
515
633
  search = "",
516
634
  filter = "all",
517
- classify: classify2,
635
+ classify: classify3,
518
636
  enabled = true,
519
637
  scaffolder,
520
638
  contextScope,
@@ -523,7 +641,7 @@ var useRecordList = (args) => {
523
641
  const queryClient = useQueryClient();
524
642
  const queryKey = useMemo(
525
643
  () => [
526
- ...QK_BASE,
644
+ ...QK_BASE2,
527
645
  ctx.collectionId,
528
646
  ctx.appId,
529
647
  ctx.recordType,
@@ -549,9 +667,9 @@ var useRecordList = (args) => {
549
667
  const [scaffolded, setScaffolded] = useState(null);
550
668
  const rawItems = useMemo(() => {
551
669
  const all = query.data?.pages.flatMap((p) => p.data) ?? [];
552
- const cls = classify2 ?? defaultClassify;
670
+ const cls = classify3 ?? defaultClassify;
553
671
  return all.map((rec) => ({ rec, summary: toSummary(rec) })).filter(({ rec, summary }) => matchesScope(scopeKind, rec, summary.scope)).filter(({ summary }) => matchesContext(summary.scope, contextScope)).map(({ summary }) => ({ ...summary, status: cls(summary) }));
554
- }, [query.data, scopeKind, classify2, contextScope]);
672
+ }, [query.data, scopeKind, classify3, contextScope]);
555
673
  useEffect(() => {
556
674
  if (!scaffolder) {
557
675
  setScaffolded(null);
@@ -571,7 +689,7 @@ var useRecordList = (args) => {
571
689
  if (filter !== "all") out = out.filter((r) => r.status === filter);
572
690
  if (search.trim()) {
573
691
  const q = search.trim().toLowerCase();
574
- out = out.filter((r) => `${r.label} ${r.subtitle ?? ""} ${r.ref}`.toLowerCase().includes(q));
692
+ out = out.filter((r) => `${r.label} ${r.subtitle ?? ""}`.toLowerCase().includes(q));
575
693
  }
576
694
  return out;
577
695
  }, [items, filter, search]);
@@ -582,7 +700,7 @@ var useRecordList = (args) => {
582
700
  empty: items.filter((r) => r.status === "empty").length
583
701
  }), [items]);
584
702
  const refetch = useCallback(() => {
585
- queryClient.invalidateQueries({ queryKey: [...QK_BASE, ctx.collectionId, ctx.appId, ctx.recordType] });
703
+ queryClient.invalidateQueries({ queryKey: [...QK_BASE2, ctx.collectionId, ctx.appId, ctx.recordType] });
586
704
  }, [queryClient, ctx.collectionId, ctx.appId, ctx.recordType]);
587
705
  const total = query.data?.pages[query.data.pages.length - 1]?.total ?? items.length;
588
706
  return {
@@ -603,11 +721,11 @@ var QK = ["records-admin", "facet-browse"];
603
721
  var toScaffoldSummary = (facet, value) => {
604
722
  const facetKey = facet.key ?? "";
605
723
  const valueKey = value.key ?? "";
606
- const ref = buildRef({ facetId: facetKey, facetValue: valueKey });
724
+ const synthScopeRaw = buildRef({ facetId: facetKey, facetValue: valueKey });
607
725
  return {
608
726
  id: null,
609
- ref,
610
- scope: parseRef(ref),
727
+ ref: "",
728
+ scope: parseRef(synthScopeRaw),
611
729
  data: null,
612
730
  status: "empty",
613
731
  label: value.name ?? valueKey ?? "Untitled value",
@@ -687,14 +805,18 @@ var useFacetBrowse = ({
687
805
  );
688
806
  }, [enabled, collectionId, hasAdminList, hasPublicList, hasAnyList, SL]);
689
807
  const mergedItems = useMemo(() => {
690
- const existingByRef = new Map(existing.map((item) => [item.ref, item]));
808
+ const existingByAnchor = new Map(
809
+ existing.map((item) => [anchorKey(item.scope), item]).filter(([k]) => !!k)
810
+ );
691
811
  const scaffolded = (query.data ?? []).flatMap(
692
812
  (facet) => (facet.values ?? []).filter((value) => !!facet.key && !!value.key).map((value) => {
693
813
  const scaffold = toScaffoldSummary(facet, value);
694
- return existingByRef.get(scaffold.ref) ?? scaffold;
814
+ const k = anchorKey(scaffold.scope);
815
+ return existingByAnchor.get(k) ?? scaffold;
695
816
  })
696
817
  );
697
- const extras = existing.filter((item) => !scaffolded.some((scaffold) => scaffold.ref === item.ref));
818
+ const scaffoldedKeys = new Set(scaffolded.map((s) => anchorKey(s.scope)));
819
+ const extras = existing.filter((item) => !scaffoldedKeys.has(anchorKey(item.scope)));
698
820
  return [...scaffolded, ...extras];
699
821
  }, [existing, query.data]);
700
822
  const filteredItems = useMemo(() => {
@@ -702,7 +824,7 @@ var useFacetBrowse = ({
702
824
  if (filter !== "all") next = next.filter((item) => item.status === filter);
703
825
  if (search.trim()) {
704
826
  const q = search.trim().toLowerCase();
705
- next = next.filter((item) => `${item.label} ${item.subtitle ?? ""} ${item.ref}`.toLowerCase().includes(q));
827
+ next = next.filter((item) => `${item.label} ${item.subtitle ?? ""}`.toLowerCase().includes(q));
706
828
  }
707
829
  return next;
708
830
  }, [mergedItems, filter, search]);
@@ -950,7 +1072,7 @@ function useShellBrowser(opts) {
950
1072
  probeIsLoading,
951
1073
  selectedProductId,
952
1074
  drillTab,
953
- classify: classify2
1075
+ classify: classify3
954
1076
  } = opts;
955
1077
  const [search, setSearch] = useState("");
956
1078
  const [filter, setFilter] = useState("all");
@@ -968,13 +1090,13 @@ function useShellBrowser(opts) {
968
1090
  search: activeScope === "product" ? search : "",
969
1091
  enabled: activeScope === "product" && !contextScope?.productId
970
1092
  });
971
- const recordListEnabled = (activeScope === "rule" || activeScope === "collection") && !probeIsLoading;
1093
+ const recordListEnabled = (activeScope === "rule" || activeScope === "collection" || activeScope === "all") && !probeIsLoading;
972
1094
  const recordList = useRecordList({
973
1095
  ctx,
974
1096
  scopeKind: activeScope,
975
1097
  search,
976
1098
  filter,
977
- classify: classify2,
1099
+ classify: classify3,
978
1100
  contextScope,
979
1101
  enabled: recordListEnabled
980
1102
  });
@@ -982,7 +1104,7 @@ function useShellBrowser(opts) {
982
1104
  SL,
983
1105
  collectionId,
984
1106
  existing: [],
985
- enabled: (activeScope === "rule" || activeScope === "collection") && !probeIsLoading
1107
+ enabled: (activeScope === "rule" || activeScope === "collection" || activeScope === "all") && !probeIsLoading
986
1108
  });
987
1109
  const variantChildren = useProductChildren({
988
1110
  SL,
@@ -1588,7 +1710,7 @@ function useShellClipboard(args) {
1588
1710
  const summaryHasData = record.data != null;
1589
1711
  const sourceParsed = record.scope;
1590
1712
  const compat = clipboard.entry ? checkPasteCompatibility(clipboard.entry.sourceScope, sourceParsed) : null;
1591
- const sameTarget = clipboard.entry ? clipboard.entry.sourceRecordId && record.id ? clipboard.entry.sourceRecordId === record.id : clipboard.entry.sourceScope.raw === record.ref : false;
1713
+ const sameTarget = clipboard.entry ? clipboard.entry.sourceRecordId && record.id ? clipboard.entry.sourceRecordId === record.id : clipboard.entry.sourceScope.raw === anchorKey(record.scope) : false;
1592
1714
  return {
1593
1715
  onCopy: summaryHasData ? () => {
1594
1716
  const value = onCopyOverride ? onCopyOverride({ value: record.data, scope: sourceParsed }) : cloneValue(record.data);
@@ -1598,7 +1720,7 @@ function useShellClipboard(args) {
1598
1720
  sourceRecordId: record.id ?? void 0,
1599
1721
  sourceLabel: record.label
1600
1722
  });
1601
- onTelemetry?.({ type: "clipboard.copy", recordType, sourceRef: record.ref });
1723
+ onTelemetry?.({ type: "clipboard.copy", recordType, sourceRef: anchorKey(record.scope) });
1602
1724
  setNotice({
1603
1725
  message: i18n.copyToast.replace("{name}", record.label),
1604
1726
  variant: "copy"
@@ -1607,7 +1729,7 @@ function useShellClipboard(args) {
1607
1729
  onPaste: () => {
1608
1730
  onLeftSelectRef.current?.(record);
1609
1731
  setPendingPasteTarget(
1610
- record.id ? { kind: "record", recordId: record.id } : { kind: "anchor", ref: record.ref }
1732
+ record.id ? { kind: "record", recordId: record.id } : { kind: "anchor", ref: anchorKey(record.scope) }
1611
1733
  );
1612
1734
  },
1613
1735
  canPaste: !!clipboard.entry && !sameTarget && compat?.status !== "denied",
@@ -1701,7 +1823,8 @@ function useShellNavigation(args) {
1701
1823
  deepLinkState,
1702
1824
  onTelemetry,
1703
1825
  onBeforeDelete,
1704
- generateItemId
1826
+ generateItemId,
1827
+ hooks
1705
1828
  } = args;
1706
1829
  const buildItemUrlValue = useCallback((id) => {
1707
1830
  if (!baseScopeRef && id.startsWith("item:")) return id;
@@ -1786,6 +1909,34 @@ function useShellNavigation(args) {
1786
1909
  const ok = await onBeforeDelete(editingScope ?? parseRef(""));
1787
1910
  if (!ok) return;
1788
1911
  }
1912
+ const row = collectionItems.items.find(
1913
+ (it) => it.itemId === itemId || it.id === itemId
1914
+ );
1915
+ const hookCtxBase = {
1916
+ collectionId: ctx.collectionId,
1917
+ appId: ctx.appId,
1918
+ recordType: ctx.recordType,
1919
+ scope: editingScope?.kind ?? "collection",
1920
+ targetRef: editingScope?.raw || void 0,
1921
+ admin: true
1922
+ };
1923
+ const recordSummary = row ?? {
1924
+ id: itemId,
1925
+ ref: editingScope?.raw ?? "",
1926
+ scope: editingScope ?? parseRef(""),
1927
+ data: null,
1928
+ status: "configured",
1929
+ label: itemId
1930
+ };
1931
+ if (hooks?.beforeDelete) {
1932
+ try {
1933
+ const result = await hooks.beforeDelete({ ...hookCtxBase, record: recordSummary });
1934
+ if (result === false) return;
1935
+ } catch (err) {
1936
+ console.warn("[RecordsAdmin] item beforeDelete hook cancelled delete", err);
1937
+ return;
1938
+ }
1939
+ }
1789
1940
  try {
1790
1941
  const { removeRecord: removeRecord2 } = await import('../../records-AYYQSP7E.js');
1791
1942
  await removeRecord2(ctx, itemId);
@@ -1793,6 +1944,13 @@ function useShellNavigation(args) {
1793
1944
  if (selectedItemId === itemId) setSelectedItemId(null);
1794
1945
  if (selectedItemId === itemId) deepLinkState.emit({ recordId: null }, "record.close");
1795
1946
  collectionItems.refetch();
1947
+ if (hooks?.afterDelete) {
1948
+ try {
1949
+ await hooks.afterDelete({ ...hookCtxBase, record: recordSummary });
1950
+ } catch (err) {
1951
+ console.warn("[RecordsAdmin] item afterDelete hook failed", err);
1952
+ }
1953
+ }
1796
1954
  } catch (err) {
1797
1955
  console.error("[RecordsAdminShell] item delete failed", err);
1798
1956
  }
@@ -1807,7 +1965,8 @@ function useShellNavigation(args) {
1807
1965
  collectionItems,
1808
1966
  deepLinkState,
1809
1967
  editingScope,
1810
- setSelectedItemId
1968
+ setSelectedItemId,
1969
+ hooks
1811
1970
  ]);
1812
1971
  const itemViewCtx = useMemo(() => ({
1813
1972
  onOpen: onItemOpen,
@@ -1879,12 +2038,12 @@ var useEditingScope = (args) => {
1879
2038
  const isCollection = cardinality === "collection";
1880
2039
  const editingItemRecordId = isCollection ? selectedItemId : null;
1881
2040
  const editingScope = useMemo(() => {
1882
- if (activeScope === "rule" || activeScope === "collection") {
2041
+ if (activeScope === "rule" || activeScope === "collection" || activeScope === "all") {
1883
2042
  if (activeScope === "rule" && ruleWizardStep === 2 && selectedRecordId === null) {
1884
2043
  return { ...parseRef(""), kind: "rule", raw: "rule:__draft__" };
1885
2044
  }
1886
2045
  if (selectedRecordId === null) {
1887
- if (activeScope === "collection" && cardinality === "collection") {
2046
+ if ((activeScope === "collection" || activeScope === "all") && cardinality === "collection") {
1888
2047
  return parseRef("");
1889
2048
  }
1890
2049
  return null;
@@ -2113,6 +2272,25 @@ var createEditorStore = () => {
2113
2272
  const listeners = /* @__PURE__ */ new Set();
2114
2273
  let cachedList = [];
2115
2274
  let nextOrder = 0;
2275
+ let hooksBundle = null;
2276
+ let notifier = null;
2277
+ const buildRecordSummary = (entry, value) => ({
2278
+ id: entry.recordId ?? null,
2279
+ ref: entry.spec.scope.raw,
2280
+ scope: entry.spec.scope,
2281
+ label: entry.label,
2282
+ status: "configured",
2283
+ data: value,
2284
+ facetRule: entry.facetRule
2285
+ });
2286
+ const buildHookCtxBase = (entry) => ({
2287
+ collectionId: entry.saveSpec.ctx.collectionId,
2288
+ appId: entry.saveSpec.ctx.appId,
2289
+ recordType: entry.saveSpec.ctx.recordType,
2290
+ scope: entry.spec.scope.kind,
2291
+ targetRef: entry.spec.scope.raw || void 0,
2292
+ admin: true
2293
+ });
2116
2294
  const recompute = () => {
2117
2295
  cachedList = Array.from(map.values()).sort((a, b) => a.order - b.order);
2118
2296
  };
@@ -2266,10 +2444,35 @@ var createEditorStore = () => {
2266
2444
  const entry = map.get(editorId);
2267
2445
  if (!entry) return;
2268
2446
  if (entry.status !== "dirty" && entry.status !== "error") return;
2269
- const persistedValue = entry.value;
2447
+ let persistedValue = entry.value;
2270
2448
  const persistedFacetRule = entry.facetRule;
2271
2449
  const { ctx, anchors } = entry.saveSpec;
2272
2450
  const spec = entry.spec;
2451
+ const isCreate = !(entry.recordId && entry.source === "self") || !!spec.createMode;
2452
+ if (hooksBundle?.beforeSave) {
2453
+ try {
2454
+ const hookCtx = {
2455
+ ...buildHookCtxBase(entry),
2456
+ isCreate,
2457
+ record: buildRecordSummary(entry, persistedValue),
2458
+ before: !isCreate ? buildRecordSummary(entry, entry.baseline) : void 0
2459
+ };
2460
+ const result = await hooksBundle.beforeSave(hookCtx);
2461
+ if (result === false) {
2462
+ return;
2463
+ }
2464
+ if (result && typeof result === "object") {
2465
+ persistedValue = persistedValue && typeof persistedValue === "object" ? { ...persistedValue, ...result } : result;
2466
+ }
2467
+ } catch (err) {
2468
+ notifier?.({ kind: "hook-before-save", error: err, recordLabel: entry.label });
2469
+ update(editorId, (e) => {
2470
+ e.status = "error";
2471
+ e.error = err;
2472
+ });
2473
+ return;
2474
+ }
2475
+ }
2273
2476
  update(editorId, (e) => {
2274
2477
  e.status = "saving";
2275
2478
  e.error = void 0;
@@ -2283,11 +2486,14 @@ var createEditorStore = () => {
2283
2486
  });
2284
2487
  } else if (spec.createMode) {
2285
2488
  const created = await createRecord(ctx, {
2286
- // Draft scopes synthesise placeholder refs (`rule:__draft__`,
2287
- // `/item:draft:...`) for shell-internal routing only never
2288
- // forward them to the SDK as an external ref. Saved rules
2289
- // still pass their real `rule:{id}` string through.
2290
- ref: spec.isDraftScope ? spec.ref : spec.scope.kind === "rule" && spec.scope.raw ? spec.scope.raw : spec.ref,
2489
+ // `record.ref` is a host-owned external handle. The framework
2490
+ // never writes to itrule identity is content-addressed via
2491
+ // `facetRule`, record identity via `record.id` (UUID).
2492
+ // `spec.ref` is always `undefined` from `useShellEditorTarget`
2493
+ // for the same reason; we keep the prop on the spec so hosts
2494
+ // that wire the editor session directly can still pass an
2495
+ // explicit external ref if they choose.
2496
+ ref: spec.ref,
2291
2497
  scope: anchors,
2292
2498
  data: persistedValue,
2293
2499
  facetRule: persistedFacetRule
@@ -2295,7 +2501,7 @@ var createEditorStore = () => {
2295
2501
  nextRecordId = created?.id ?? nextRecordId;
2296
2502
  } else {
2297
2503
  const upserted = await upsertRecord(ctx, {
2298
- ref: spec.isDraftScope ? spec.ref : spec.scope.kind === "rule" && spec.scope.raw ? spec.scope.raw : spec.ref,
2504
+ ref: spec.ref,
2299
2505
  scope: anchors,
2300
2506
  data: persistedValue,
2301
2507
  facetRule: persistedFacetRule
@@ -2303,6 +2509,9 @@ var createEditorStore = () => {
2303
2509
  nextRecordId = upserted.record?.id ?? nextRecordId;
2304
2510
  }
2305
2511
  update(editorId, (e) => {
2512
+ if (persistedValue !== entry.value) {
2513
+ e.value = cloneDeep(persistedValue);
2514
+ }
2306
2515
  e.baseline = cloneDeep(e.value);
2307
2516
  e.baselineFacetRule = e.facetRule;
2308
2517
  e.recordId = nextRecordId;
@@ -2310,6 +2519,21 @@ var createEditorStore = () => {
2310
2519
  e.status = "saved";
2311
2520
  e.error = void 0;
2312
2521
  });
2522
+ if (hooksBundle?.afterSave) {
2523
+ const refreshed = map.get(editorId);
2524
+ const hookCtx = {
2525
+ ...buildHookCtxBase(refreshed ?? entry),
2526
+ isCreate,
2527
+ record: buildRecordSummary(refreshed ?? entry, persistedValue),
2528
+ before: !isCreate ? buildRecordSummary(entry, entry.baseline) : void 0
2529
+ };
2530
+ try {
2531
+ await hooksBundle.afterSave(hookCtx);
2532
+ } catch (err) {
2533
+ notifier?.({ kind: "hook-after-save", error: err, recordLabel: entry.label });
2534
+ console.warn("[RecordsAdmin] afterSave hook failed", err);
2535
+ }
2536
+ }
2313
2537
  } catch (err) {
2314
2538
  update(editorId, (e) => {
2315
2539
  e.status = "error";
@@ -2331,9 +2555,35 @@ var createEditorStore = () => {
2331
2555
  if (!entry) return;
2332
2556
  if (entry.source !== "self") return;
2333
2557
  if (!entry.recordId) return;
2558
+ if (hooksBundle?.beforeDelete) {
2559
+ try {
2560
+ const hookCtx = {
2561
+ ...buildHookCtxBase(entry),
2562
+ record: buildRecordSummary(entry, entry.value)
2563
+ };
2564
+ const result = await hooksBundle.beforeDelete(hookCtx);
2565
+ if (result === false) return;
2566
+ } catch (err) {
2567
+ notifier?.({ kind: "hook-before-delete", error: err, recordLabel: entry.label });
2568
+ console.warn("[RecordsAdmin] beforeDelete hook cancelled delete", err);
2569
+ return;
2570
+ }
2571
+ }
2572
+ const deletedSnapshot = hooksBundle?.afterDelete ? {
2573
+ ...buildHookCtxBase(entry),
2574
+ record: buildRecordSummary(entry, entry.value)
2575
+ } : null;
2334
2576
  await removeRecord(entry.saveSpec.ctx, entry.recordId);
2335
2577
  map.delete(editorId);
2336
2578
  emit();
2579
+ if (hooksBundle?.afterDelete && deletedSnapshot) {
2580
+ try {
2581
+ await hooksBundle.afterDelete(deletedSnapshot);
2582
+ } catch (err) {
2583
+ notifier?.({ kind: "hook-after-delete", error: err, recordLabel: entry.label });
2584
+ console.warn("[RecordsAdmin] afterDelete hook failed", err);
2585
+ }
2586
+ }
2337
2587
  },
2338
2588
  enforcePoolLimit(max, exceptEditorId) {
2339
2589
  const alive = Array.from(map.values());
@@ -2354,6 +2604,12 @@ var createEditorStore = () => {
2354
2604
  return () => {
2355
2605
  listeners.delete(listener);
2356
2606
  };
2607
+ },
2608
+ setHooks(hooks) {
2609
+ hooksBundle = hooks ?? null;
2610
+ },
2611
+ setNotifier(notify2) {
2612
+ notifier = notify2 ?? null;
2357
2613
  }
2358
2614
  };
2359
2615
  };
@@ -2375,10 +2631,14 @@ var EditorSessionProvider = ({
2375
2631
  ctx,
2376
2632
  children,
2377
2633
  maxOpenEditors = 8,
2378
- defaultValueFactory
2634
+ defaultValueFactory,
2635
+ hooks,
2636
+ onHookNotice
2379
2637
  }) => {
2380
2638
  const storeRef = useRef(null);
2381
2639
  if (!storeRef.current) storeRef.current = createEditorStore();
2640
+ storeRef.current.setHooks(hooks ?? null);
2641
+ storeRef.current.setNotifier(onHookNotice ?? null);
2382
2642
  const currentRef = useRef(void 0);
2383
2643
  const listenersRef = useRef(/* @__PURE__ */ new Set());
2384
2644
  const subscribeCurrent = useCallback((cb) => {
@@ -2706,7 +2966,14 @@ function useShellEditorTarget(args) {
2706
2966
  // would update the rule record in place.
2707
2967
  recordId: createMode ? void 0 : resolved.recordId,
2708
2968
  createMode,
2709
- ref: editingTargetScope.kind === "rule" && !isDraftScope ? editingTargetScope.raw : void 0,
2969
+ // We deliberately do NOT propagate `editingTargetScope.raw` (e.g.
2970
+ // `rule:<ulid>`) into `spec.ref`. `record.ref` is a host-owned
2971
+ // EXTERNAL HANDLE — this framework must remain agnostic about how
2972
+ // hosts use it. Rule identity is content-addressed via `facetRule`
2973
+ // (see `data/ruleHash`); record identity is the UUID `record.id`.
2974
+ // Whatever the SDK chooses to back-fill in `ref` is the SDK's
2975
+ // problem, not ours, and we never write to it.
2976
+ ref: void 0,
2710
2977
  initialFacetRule,
2711
2978
  label,
2712
2979
  draftKey,
@@ -2813,7 +3080,8 @@ var LABELS = {
2813
3080
  facet: "Shared",
2814
3081
  variant: "Variants",
2815
3082
  batch: "Batches",
2816
- rule: "Rules"
3083
+ rule: "Rules",
3084
+ all: "All"
2817
3085
  };
2818
3086
  var ScopeTabs = ({
2819
3087
  scopes,
@@ -2821,6 +3089,7 @@ var ScopeTabs = ({
2821
3089
  onChange,
2822
3090
  loading = false,
2823
3091
  counts,
3092
+ tooltips,
2824
3093
  icons
2825
3094
  }) => {
2826
3095
  const iconMap = icons ?? DEFAULT_ICONS.scope;
@@ -2828,6 +3097,7 @@ var ScopeTabs = ({
2828
3097
  const Icon = iconMap[s] ?? DEFAULT_ICONS.scope[s];
2829
3098
  const isActive = active === s;
2830
3099
  const count = counts?.[s];
3100
+ const tooltip = tooltips?.[s];
2831
3101
  return /* @__PURE__ */ jsxs(
2832
3102
  "button",
2833
3103
  {
@@ -2837,6 +3107,7 @@ var ScopeTabs = ({
2837
3107
  onClick: () => onChange(s),
2838
3108
  disabled: loading,
2839
3109
  className: "ra-tab",
3110
+ title: tooltip,
2840
3111
  children: [
2841
3112
  /* @__PURE__ */ jsx(Icon, { className: cn("ra-tab-icon w-3.5 h-3.5", loading && "animate-pulse") }),
2842
3113
  /* @__PURE__ */ jsx("span", { children: LABELS[s] }),
@@ -3069,6 +3340,15 @@ var DefaultRecordRow = ({ record, ctx, compact = false }) => {
3069
3340
  compact && /* @__PURE__ */ jsx(StatusIcon, { status: record.status, size: "0.85rem" }),
3070
3341
  !compact && record.scope.kind && record.scope.kind !== "collection" && /* @__PURE__ */ jsx("span", { className: "ra-row-scope", "aria-hidden": "true", children: /* @__PURE__ */ jsx(ScopeIcon, { className: "w-3 h-3" }) }),
3071
3342
  record.badges?.slice(0, 1).map((b, i) => /* @__PURE__ */ jsx("span", { className: "ra-chip", "data-tone": "muted", children: b.label }, `${b.label}-${i}`)),
3343
+ record.facetRule && !compact && /* @__PURE__ */ jsx(
3344
+ "span",
3345
+ {
3346
+ className: "ra-row-rule-pip",
3347
+ title: ruleSummary ? `Targeted: ${ruleSummary}` : "This record has targeting rules",
3348
+ "aria-label": ruleSummary ? `Targeted: ${ruleSummary}` : "This record has targeting rules",
3349
+ children: /* @__PURE__ */ jsx(SlidersHorizontal, { className: "w-3 h-3", "aria-hidden": "true" })
3350
+ }
3351
+ ),
3072
3352
  hasError ? /* @__PURE__ */ jsx(
3073
3353
  "span",
3074
3354
  {
@@ -3117,22 +3397,24 @@ var RecordList = ({
3117
3397
  presentation = "list",
3118
3398
  renderListRow,
3119
3399
  groupBy,
3400
+ renderGroupActions,
3120
3401
  rowClipboard,
3121
3402
  i18n
3122
3403
  }) => {
3123
3404
  const buildCtx = (item) => {
3124
3405
  const cb = rowClipboard ? rowClipboard(item) : null;
3406
+ const itemAnchorKey = anchorKey(item.scope);
3125
3407
  const idMatch = selectedId != null && item.id != null && item.id === selectedId;
3126
- const anchorMatch = selectedAnchorKey != null && item.ref === selectedAnchorKey;
3408
+ const anchorMatch = selectedAnchorKey != null && !!itemAnchorKey && itemAnchorKey === selectedAnchorKey;
3127
3409
  const dirtyIdMatch = dirtyId != null && item.id != null && item.id === dirtyId;
3128
- const dirtyAnchorMatch = dirtyAnchorKey != null && item.ref === dirtyAnchorKey;
3410
+ const dirtyAnchorMatch = dirtyAnchorKey != null && !!itemAnchorKey && itemAnchorKey === dirtyAnchorKey;
3129
3411
  const idInStore = !!(item.id && dirtyKeys?.has(item.id));
3130
- const refInStore = !!(item.ref && dirtyKeys?.has(item.ref));
3131
- const errorInStore = !!(item.id && errorKeys?.has(item.id) || item.ref && errorKeys?.has(item.ref));
3412
+ const anchorInStore = !!(itemAnchorKey && dirtyKeys?.has(itemAnchorKey));
3413
+ const errorInStore = !!(item.id && errorKeys?.has(item.id) || itemAnchorKey && errorKeys?.has(itemAnchorKey));
3132
3414
  return {
3133
3415
  selected: idMatch || anchorMatch,
3134
3416
  onSelect: () => onSelect(item),
3135
- isDirty: dirtyIdMatch || dirtyAnchorMatch || idInStore || refInStore,
3417
+ isDirty: dirtyIdMatch || dirtyAnchorMatch || idInStore || anchorInStore,
3136
3418
  hasError: errorInStore,
3137
3419
  i18n,
3138
3420
  ...cb ?? {}
@@ -3154,19 +3436,28 @@ var RecordList = ({
3154
3436
  }, [items, groupBy]);
3155
3437
  const renderItems = (rows) => {
3156
3438
  const compact = presentation === "compact";
3157
- return /* @__PURE__ */ jsx("ul", { children: rows.map((item) => {
3439
+ return /* @__PURE__ */ jsx("ul", { children: rows.map((item, idx) => {
3158
3440
  const ctx = buildCtx(item);
3159
- return /* @__PURE__ */ jsx("li", { children: renderListRow ? renderListRow(item, ctx) : /* @__PURE__ */ jsx(DefaultRecordRow, { record: item, ctx, compact }) }, item.id ?? item.ref);
3441
+ const key = item.id ?? (anchorKey(item.scope) || `pos:${idx}`);
3442
+ return /* @__PURE__ */ jsx("li", { children: renderListRow ? renderListRow(item, ctx) : /* @__PURE__ */ jsx(DefaultRecordRow, { record: item, ctx, compact }) }, key);
3160
3443
  }) });
3161
3444
  };
3162
3445
  if (groups) {
3163
- return /* @__PURE__ */ jsx(GroupedList, { groups, renderItems });
3446
+ return /* @__PURE__ */ jsx(
3447
+ GroupedList,
3448
+ {
3449
+ groups,
3450
+ renderItems,
3451
+ renderGroupActions
3452
+ }
3453
+ );
3164
3454
  }
3165
3455
  return renderItems(items);
3166
3456
  };
3167
3457
  var GroupedList = ({
3168
3458
  groups,
3169
- renderItems
3459
+ renderItems,
3460
+ renderGroupActions
3170
3461
  }) => {
3171
3462
  const Chevron = DEFAULT_ICONS.group.chevron;
3172
3463
  const [open, setOpen] = useState(
@@ -3174,22 +3465,34 @@ var GroupedList = ({
3174
3465
  );
3175
3466
  return /* @__PURE__ */ jsx(Fragment, { children: groups.map((g) => {
3176
3467
  const isOpen = open[g.key] !== false;
3468
+ const actions = renderGroupActions?.({ key: g.key, label: g.label, items: g.items });
3177
3469
  return /* @__PURE__ */ jsxs("section", { className: "ra-group", "data-open": isOpen ? "true" : "false", children: [
3178
- /* @__PURE__ */ jsxs(
3179
- "button",
3180
- {
3181
- type: "button",
3182
- className: "ra-group-summary",
3183
- onClick: () => setOpen((s) => ({ ...s, [g.key]: !isOpen })),
3184
- "aria-expanded": isOpen,
3185
- children: [
3186
- /* @__PURE__ */ jsx(Chevron, { className: "ra-group-chevron w-3 h-3" }),
3187
- g.icon,
3188
- /* @__PURE__ */ jsx("span", { className: "ra-group-name", children: g.label }),
3189
- /* @__PURE__ */ jsx("span", { className: "ra-group-count", children: g.items.length })
3190
- ]
3191
- }
3192
- ),
3470
+ /* @__PURE__ */ jsxs("div", { className: "ra-group-summary-row flex items-center", children: [
3471
+ /* @__PURE__ */ jsxs(
3472
+ "button",
3473
+ {
3474
+ type: "button",
3475
+ className: "ra-group-summary flex-1 min-w-0",
3476
+ onClick: () => setOpen((s) => ({ ...s, [g.key]: !isOpen })),
3477
+ "aria-expanded": isOpen,
3478
+ children: [
3479
+ /* @__PURE__ */ jsx(Chevron, { className: "ra-group-chevron w-3 h-3" }),
3480
+ g.icon,
3481
+ /* @__PURE__ */ jsx("span", { className: "ra-group-name", children: g.label }),
3482
+ /* @__PURE__ */ jsx("span", { className: "ra-group-count", children: g.items.length })
3483
+ ]
3484
+ }
3485
+ ),
3486
+ actions ? /* @__PURE__ */ jsx(
3487
+ "div",
3488
+ {
3489
+ className: "ra-group-actions flex-shrink-0 pr-2",
3490
+ onClick: (e) => e.stopPropagation(),
3491
+ onMouseDown: (e) => e.stopPropagation(),
3492
+ children: actions
3493
+ }
3494
+ ) : null
3495
+ ] }),
3193
3496
  /* @__PURE__ */ jsx("div", { className: "ra-group-body", children: renderItems(g.items) })
3194
3497
  ] }, g.key);
3195
3498
  }) });
@@ -3442,7 +3745,7 @@ function useItemViewPref(args) {
3442
3745
  }, [key]);
3443
3746
  return [value, set];
3444
3747
  }
3445
- var QK_BASE2 = ["records-admin", "collection-items"];
3748
+ var QK_BASE3 = ["records-admin", "collection-items"];
3446
3749
  var canonicalFacetRule = (rule) => {
3447
3750
  if (!rule || !Array.isArray(rule.all)) return "";
3448
3751
  const clauses = rule.all.map((c) => ({
@@ -3475,23 +3778,24 @@ function useCollectionItems(args) {
3475
3778
  ctx,
3476
3779
  scope,
3477
3780
  facetRule,
3781
+ includeAll = false,
3478
3782
  pageSize = 100,
3479
3783
  toSummary: toSummary2 = defaultToSummary,
3480
3784
  enabled = true
3481
3785
  } = args;
3482
3786
  const queryClient = useQueryClient();
3483
- const scopeRef = scope?.raw ?? "";
3787
+ const scopeRef = includeAll ? "__all__" : scope?.raw ?? "";
3484
3788
  const ruleSignature = useMemo(
3485
- () => scope?.kind === "rule" ? canonicalFacetRule(facetRule) : "",
3486
- [scope?.kind, facetRule]
3789
+ () => !includeAll && scope?.kind === "rule" ? canonicalFacetRule(facetRule) : "",
3790
+ [includeAll, scope?.kind, facetRule]
3487
3791
  );
3488
3792
  const queryKey = useMemo(
3489
- () => [...QK_BASE2, ctx.collectionId, ctx.appId, ctx.recordType, scopeRef, ruleSignature],
3793
+ () => [...QK_BASE3, ctx.collectionId, ctx.appId, ctx.recordType, scopeRef, ruleSignature],
3490
3794
  [ctx.collectionId, ctx.appId, ctx.recordType, scopeRef, ruleSignature]
3491
3795
  );
3492
3796
  const query = useInfiniteQuery({
3493
3797
  queryKey,
3494
- enabled: enabled && !!scope,
3798
+ enabled: enabled && (includeAll || !!scope),
3495
3799
  initialPageParam: 0,
3496
3800
  queryFn: async ({ pageParam }) => {
3497
3801
  const offset = pageParam;
@@ -3505,12 +3809,13 @@ function useCollectionItems(args) {
3505
3809
  staleTime: 15e3
3506
3810
  });
3507
3811
  const items = useMemo(() => {
3508
- if (!scope) return [];
3812
+ if (!includeAll && !scope) return [];
3509
3813
  const all = query.data?.pages.flatMap((p) => p.data) ?? [];
3510
- const relevant = all.filter((rec) => recordMatchesScope(rec, scope, ruleSignature));
3814
+ const relevant = includeAll ? all : all.filter((rec) => recordMatchesScope(rec, scope, ruleSignature));
3511
3815
  return relevant.map((rec) => {
3512
3816
  const ref = rec.ref ?? "";
3513
3817
  const parsed = parseRef(ref);
3818
+ const recFacetRule = rec.facetRule ?? null;
3514
3819
  const stableItemId = rec.id;
3515
3820
  const base = {
3516
3821
  id: rec.id,
@@ -3520,11 +3825,12 @@ function useCollectionItems(args) {
3520
3825
  status: rec.data ? "configured" : "empty",
3521
3826
  label: stableItemId,
3522
3827
  updatedAt: rec.updatedAt,
3523
- itemId: stableItemId
3828
+ itemId: stableItemId,
3829
+ facetRule: recFacetRule
3524
3830
  };
3525
3831
  return toSummary2(rec, base);
3526
3832
  }).filter((x) => x !== null);
3527
- }, [query.data, toSummary2, scope, ruleSignature]);
3833
+ }, [query.data, toSummary2, scope, ruleSignature, includeAll]);
3528
3834
  const refetch = useCallback(() => {
3529
3835
  queryClient.invalidateQueries({ queryKey });
3530
3836
  }, [queryClient, queryKey]);
@@ -3700,7 +4006,7 @@ var createPostMessageDeepLinkAdapter = (paramNames) => {
3700
4006
 
3701
4007
  // src/components/RecordsAdmin/hooks/useDeepLinkState.ts
3702
4008
  var SMART_PUSH = ["record.open", "record.close", "scope"];
3703
- var classify = (mode, kind) => {
4009
+ var classify2 = (mode, kind) => {
3704
4010
  if (mode === "push") return "push";
3705
4011
  if (mode === "replace") return "replace";
3706
4012
  return SMART_PUSH.includes(kind) ? "push" : "replace";
@@ -3737,7 +4043,7 @@ function useDeepLinkState(options) {
3737
4043
  }, [adapter]);
3738
4044
  const emit = useCallback((partial, kind) => {
3739
4045
  if (!adapter) return;
3740
- const mode = classify(history, kind);
4046
+ const mode = classify2(history, kind);
3741
4047
  adapter.write(partial, mode);
3742
4048
  setUrlState((prev) => ({ ...prev, ...partial }));
3743
4049
  }, [adapter, history]);
@@ -3914,6 +4220,7 @@ function RecordEditor({
3914
4220
  children,
3915
4221
  preview,
3916
4222
  targeting,
4223
+ targetingControl,
3917
4224
  bulkActions,
3918
4225
  footerExtra,
3919
4226
  onBeforeDelete,
@@ -3932,8 +4239,15 @@ function RecordEditor({
3932
4239
  const DeleteIcon = actionIcons?.delete;
3933
4240
  const showInherited = ctx.source === "inherited";
3934
4241
  const showEmpty = ctx.source === "empty";
4242
+ const hasBreadcrumb = (() => {
4243
+ const s = ctx.scope;
4244
+ return Boolean(s?.facetId || s?.productId || s?.variantId || s?.batchId);
4245
+ })();
4246
+ const hasLeftContent = Boolean(headerLabel) || hasBreadcrumb;
4247
+ const hasRightContent = showInherited || showEmpty || Boolean(headerMeta) || Boolean(bulkActions) || Boolean(targetingControl);
4248
+ const showHeader = hasLeftContent || hasRightContent;
3935
4249
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", children: [
3936
- /* @__PURE__ */ jsxs(
4250
+ showHeader && /* @__PURE__ */ jsxs(
3937
4251
  "header",
3938
4252
  {
3939
4253
  className: "sticky top-0 z-40 px-5 py-3 border-b flex items-start justify-between gap-3",
@@ -4005,7 +4319,8 @@ function RecordEditor({
4005
4319
  children: headerMeta
4006
4320
  }
4007
4321
  ),
4008
- bulkActions && /* @__PURE__ */ jsx(BulkActionsMenu, { ...bulkActions, i18n })
4322
+ bulkActions && /* @__PURE__ */ jsx(BulkActionsMenu, { ...bulkActions, i18n }),
4323
+ targetingControl
4009
4324
  ] })
4010
4325
  ]
4011
4326
  }
@@ -4114,7 +4429,7 @@ function RecordEditor({
4114
4429
  )
4115
4430
  ] });
4116
4431
  }
4117
- function summariseRule(clauses, facets) {
4432
+ function summariseRule2(clauses, facets) {
4118
4433
  let valueCount = 0;
4119
4434
  const parts = clauses.map((c) => {
4120
4435
  const facet = facets.find((f) => f.key === c.facetKey);
@@ -4127,20 +4442,36 @@ function summariseRule(clauses, facets) {
4127
4442
  });
4128
4443
  return { text: parts.join(" \xB7 "), clauseCount: clauses.length, valueCount };
4129
4444
  }
4130
- function RecordTargeting({ SL, collectionId, appId, ctx }) {
4445
+ function RecordTargeting({
4446
+ SL,
4447
+ collectionId,
4448
+ appId,
4449
+ ctx,
4450
+ forceOpen,
4451
+ onOpenChange
4452
+ }) {
4131
4453
  const facets = useFacets(collectionId, void 0);
4132
4454
  const preview = useRulePreview({ SL, collectionId, appId, rule: ctx.facetRule ?? null });
4133
4455
  const clauses = ctx.facetRule?.all ?? [];
4134
4456
  const hasAnyClause = clauses.length > 0;
4457
+ const hasInflightRule = ctx.facetRule != null;
4135
4458
  const isInvalid = ctx.canSave === false;
4136
- const shouldAutoOpen = !hasAnyClause || isInvalid;
4459
+ const shouldAutoOpen = hasInflightRule && (!hasAnyClause || isInvalid);
4137
4460
  const [open, setOpen] = useState(shouldAutoOpen);
4138
4461
  useEffect(() => {
4139
4462
  if (shouldAutoOpen) setOpen(true);
4140
4463
  }, [shouldAutoOpen]);
4141
- const summary = useMemo(() => summariseRule(clauses, facets), [clauses, facets]);
4464
+ useEffect(() => {
4465
+ if (forceOpen) setOpen(true);
4466
+ }, [forceOpen]);
4467
+ const summary = useMemo(() => summariseRule2(clauses, facets), [clauses, facets]);
4142
4468
  if (!ctx.onFacetRuleChange) return null;
4469
+ if (!hasInflightRule) return null;
4143
4470
  const headerTone = isInvalid ? { color: "hsl(var(--ra-danger, 0 70% 45%))" } : { color: "hsl(var(--ra-muted-text))" };
4471
+ const setOpenAndNotify = (next) => {
4472
+ setOpen(next);
4473
+ onOpenChange?.(next);
4474
+ };
4144
4475
  return /* @__PURE__ */ jsxs(
4145
4476
  "div",
4146
4477
  {
@@ -4150,12 +4481,12 @@ function RecordTargeting({ SL, collectionId, appId, ctx }) {
4150
4481
  background: "hsl(var(--ra-muted) / 0.3)"
4151
4482
  },
4152
4483
  children: [
4153
- /* @__PURE__ */ jsxs(
4484
+ /* @__PURE__ */ jsx("div", { className: "w-full flex items-center gap-2 px-3 py-2", children: /* @__PURE__ */ jsxs(
4154
4485
  "button",
4155
4486
  {
4156
4487
  type: "button",
4157
- onClick: () => setOpen((o) => !o),
4158
- className: "w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-[hsl(var(--ra-muted)/0.5)] rounded-lg transition-colors",
4488
+ onClick: () => setOpenAndNotify(!open),
4489
+ className: "flex items-center gap-2 flex-1 min-w-0 text-left rounded transition-colors hover:bg-[hsl(var(--ra-muted)/0.5)] -mx-1 px-1 py-0.5",
4159
4490
  "aria-expanded": open,
4160
4491
  children: [
4161
4492
  open ? /* @__PURE__ */ jsx(ChevronDown, { className: "w-3.5 h-3.5 shrink-0", style: headerTone }) : /* @__PURE__ */ jsx(ChevronRight, { className: "w-3.5 h-3.5 shrink-0", style: headerTone }),
@@ -4173,7 +4504,7 @@ function RecordTargeting({ SL, collectionId, appId, ctx }) {
4173
4504
  {
4174
4505
  className: "text-xs truncate flex-1 min-w-0",
4175
4506
  style: { color: "hsl(var(--ra-text))" },
4176
- title: summary.text || void 0,
4507
+ title: hasAnyClause ? summary.text : void 0,
4177
4508
  children: hasAnyClause ? summary.text || `${summary.clauseCount} facet${summary.clauseCount === 1 ? "" : "s"}` : /* @__PURE__ */ jsx("span", { style: { color: "hsl(var(--ra-muted-text))", fontStyle: "italic" }, children: "No rule yet \u2014 pick a facet to begin" })
4178
4509
  }
4179
4510
  ),
@@ -4206,7 +4537,7 @@ function RecordTargeting({ SL, collectionId, appId, ctx }) {
4206
4537
  /* @__PURE__ */ jsx(Settings2, { className: "w-3.5 h-3.5 shrink-0", style: headerTone })
4207
4538
  ]
4208
4539
  }
4209
- ),
4540
+ ) }),
4210
4541
  open && /* @__PURE__ */ jsx(
4211
4542
  "div",
4212
4543
  {
@@ -4228,6 +4559,262 @@ function RecordTargeting({ SL, collectionId, appId, ctx }) {
4228
4559
  }
4229
4560
  );
4230
4561
  }
4562
+ function deepCloneRule(rule) {
4563
+ if (typeof structuredClone === "function") return structuredClone(rule);
4564
+ return JSON.parse(JSON.stringify(rule));
4565
+ }
4566
+ function TargetingPopover({
4567
+ ctx,
4568
+ catalogue,
4569
+ onCustomise,
4570
+ disabled
4571
+ }) {
4572
+ const [open, setOpen] = useState(false);
4573
+ const triggerRef = useRef(null);
4574
+ const [anchor, setAnchor] = useState(null);
4575
+ const currentHash = useMemo(() => ruleHash(ctx.facetRule), [ctx.facetRule]);
4576
+ const currentMatchesPreset = !!currentHash && catalogue.some((e) => e.hash === currentHash);
4577
+ const sharedCount = useMemo(() => {
4578
+ if (!currentHash) return 0;
4579
+ const entry = catalogue.find((e) => e.hash === currentHash);
4580
+ if (!entry) return 0;
4581
+ return Math.max(0, entry.count - 1);
4582
+ }, [catalogue, currentHash]);
4583
+ useEffect(() => {
4584
+ if (!open || !triggerRef.current) return;
4585
+ const r = triggerRef.current.getBoundingClientRect();
4586
+ setAnchor({ top: r.bottom + 6, right: window.innerWidth - r.right });
4587
+ }, [open]);
4588
+ useEffect(() => {
4589
+ if (!open) return;
4590
+ const onKey = (e) => {
4591
+ if (e.key === "Escape") {
4592
+ e.stopPropagation();
4593
+ setOpen(false);
4594
+ }
4595
+ };
4596
+ const onDown = (e) => {
4597
+ const t = e.target;
4598
+ if (!t) return;
4599
+ if (triggerRef.current?.contains(t)) return;
4600
+ const card = document.getElementById("ra-targeting-popover-card");
4601
+ if (card?.contains(t)) return;
4602
+ setOpen(false);
4603
+ };
4604
+ window.addEventListener("keydown", onKey, true);
4605
+ window.addEventListener("mousedown", onDown, true);
4606
+ return () => {
4607
+ window.removeEventListener("keydown", onKey, true);
4608
+ window.removeEventListener("mousedown", onDown, true);
4609
+ };
4610
+ }, [open]);
4611
+ if (disabled || !ctx.onFacetRuleChange) return null;
4612
+ const mode = !ctx.facetRule ? "global" : currentMatchesPreset ? "preset" : "custom";
4613
+ const onPickGlobal = () => {
4614
+ ctx.onFacetRuleChange?.(null);
4615
+ setOpen(false);
4616
+ };
4617
+ const onPickPreset = (entry) => {
4618
+ ctx.onFacetRuleChange?.(deepCloneRule(entry.rule));
4619
+ setOpen(false);
4620
+ };
4621
+ const onPickCustom = () => {
4622
+ if (!ctx.facetRule) ctx.onFacetRuleChange?.({ all: [] });
4623
+ onCustomise();
4624
+ setOpen(false);
4625
+ };
4626
+ const triggerLabel = mode === "global" ? "Global" : "Targeted";
4627
+ const TriggerIcon = mode === "global" ? Globe2 : Target;
4628
+ const popover = open && anchor ? createPortal(
4629
+ /* @__PURE__ */ jsxs(
4630
+ "div",
4631
+ {
4632
+ id: "ra-targeting-popover-card",
4633
+ role: "dialog",
4634
+ "aria-label": "Targeting",
4635
+ className: "ra-shell",
4636
+ style: {
4637
+ position: "fixed",
4638
+ top: anchor.top,
4639
+ right: anchor.right,
4640
+ zIndex: 60,
4641
+ width: 320,
4642
+ maxHeight: "min(70vh, 480px)",
4643
+ overflowY: "auto",
4644
+ background: "hsl(var(--ra-surface))",
4645
+ border: "1px solid hsl(var(--ra-border))",
4646
+ borderRadius: 8,
4647
+ boxShadow: "0 8px 24px hsl(0 0% 0% / 0.12)"
4648
+ },
4649
+ onMouseDown: (e) => e.stopPropagation(),
4650
+ onClick: (e) => e.stopPropagation(),
4651
+ children: [
4652
+ /* @__PURE__ */ jsx(
4653
+ "div",
4654
+ {
4655
+ className: "px-3 py-2 border-b text-[11px] uppercase tracking-wide font-medium",
4656
+ style: {
4657
+ borderColor: "hsl(var(--ra-border))",
4658
+ color: "hsl(var(--ra-muted-text))"
4659
+ },
4660
+ children: "Targeting"
4661
+ }
4662
+ ),
4663
+ sharedCount > 0 && /* @__PURE__ */ jsxs(
4664
+ "div",
4665
+ {
4666
+ className: "mx-3 mt-2 px-2 py-1.5 text-[11px] rounded flex items-start gap-1.5",
4667
+ style: {
4668
+ background: "hsl(var(--ra-muted) / 0.5)",
4669
+ color: "hsl(var(--ra-muted-text))"
4670
+ },
4671
+ children: [
4672
+ /* @__PURE__ */ jsx(Info, { className: "w-3 h-3 mt-0.5 shrink-0" }),
4673
+ /* @__PURE__ */ jsxs("span", { children: [
4674
+ /* @__PURE__ */ jsx("strong", { style: { color: "hsl(var(--ra-text))" }, children: sharedCount }),
4675
+ " ",
4676
+ "other record",
4677
+ sharedCount === 1 ? "" : "s",
4678
+ " share this rule. Changing it here only affects this record. To retarget all of them, use ",
4679
+ /* @__PURE__ */ jsx("em", { children: "Edit rule" }),
4680
+ " on the rail group."
4681
+ ] })
4682
+ ]
4683
+ }
4684
+ ),
4685
+ /* @__PURE__ */ jsx(
4686
+ Option,
4687
+ {
4688
+ active: mode === "global",
4689
+ onClick: onPickGlobal,
4690
+ icon: /* @__PURE__ */ jsx(Globe2, { className: "w-3.5 h-3.5" }),
4691
+ title: "Apply globally",
4692
+ subtitle: "Applies to every product in the collection."
4693
+ }
4694
+ ),
4695
+ catalogue.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
4696
+ /* @__PURE__ */ jsx(
4697
+ "div",
4698
+ {
4699
+ className: "px-3 pt-2 pb-1 text-[10px] uppercase tracking-wide",
4700
+ style: { color: "hsl(var(--ra-muted-text))" },
4701
+ children: "Existing rule sets"
4702
+ }
4703
+ ),
4704
+ catalogue.map((entry) => /* @__PURE__ */ jsx(
4705
+ Option,
4706
+ {
4707
+ active: mode === "preset" && currentHash === entry.hash,
4708
+ onClick: () => onPickPreset(entry),
4709
+ icon: /* @__PURE__ */ jsx(Target, { className: "w-3.5 h-3.5" }),
4710
+ title: entry.summary || "Untitled rule",
4711
+ subtitle: `${entry.count} record${entry.count === 1 ? "" : "s"}`
4712
+ },
4713
+ entry.hash
4714
+ ))
4715
+ ] }),
4716
+ /* @__PURE__ */ jsx(
4717
+ "div",
4718
+ {
4719
+ className: "border-t mt-1",
4720
+ style: { borderColor: "hsl(var(--ra-border) / 0.6)" }
4721
+ }
4722
+ ),
4723
+ /* @__PURE__ */ jsx(
4724
+ Option,
4725
+ {
4726
+ active: mode === "custom",
4727
+ onClick: onPickCustom,
4728
+ icon: /* @__PURE__ */ jsx(Pencil, { className: "w-3.5 h-3.5" }),
4729
+ title: mode === "custom" ? "Customise rule" : "Create a new rule",
4730
+ subtitle: "Open the rule editor below to author a bespoke rule."
4731
+ }
4732
+ )
4733
+ ]
4734
+ }
4735
+ ),
4736
+ document.body
4737
+ ) : null;
4738
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
4739
+ /* @__PURE__ */ jsxs(
4740
+ "button",
4741
+ {
4742
+ ref: triggerRef,
4743
+ type: "button",
4744
+ onClick: () => setOpen((o) => !o),
4745
+ className: "text-[11px] inline-flex items-center gap-1 px-2 py-1 rounded-md border transition-colors hover:bg-[hsl(var(--ra-muted))]",
4746
+ style: {
4747
+ borderColor: "hsl(var(--ra-border))",
4748
+ color: "hsl(var(--ra-text))"
4749
+ },
4750
+ "aria-haspopup": "dialog",
4751
+ "aria-expanded": open,
4752
+ title: "Use rules to scope this record to a targeted audience",
4753
+ children: [
4754
+ /* @__PURE__ */ jsx(TriggerIcon, { className: "w-3 h-3" }),
4755
+ triggerLabel,
4756
+ mode === "preset" && /* @__PURE__ */ jsx(Check, { className: "w-3 h-3", style: { color: "hsl(var(--ra-muted-text))" } })
4757
+ ]
4758
+ }
4759
+ ),
4760
+ popover
4761
+ ] });
4762
+ }
4763
+ function Option({
4764
+ active,
4765
+ onClick,
4766
+ icon,
4767
+ title,
4768
+ subtitle
4769
+ }) {
4770
+ return /* @__PURE__ */ jsxs(
4771
+ "button",
4772
+ {
4773
+ type: "button",
4774
+ onClick,
4775
+ className: "w-full text-left px-3 py-2 flex items-start gap-2 transition-colors hover:bg-[hsl(var(--ra-muted)/0.6)]",
4776
+ style: {
4777
+ background: active ? "hsl(var(--ra-accent) / 0.08)" : void 0
4778
+ },
4779
+ children: [
4780
+ /* @__PURE__ */ jsx(
4781
+ "span",
4782
+ {
4783
+ className: "mt-0.5 shrink-0",
4784
+ style: { color: active ? "hsl(var(--ra-accent))" : "hsl(var(--ra-muted-text))" },
4785
+ children: icon
4786
+ }
4787
+ ),
4788
+ /* @__PURE__ */ jsxs("span", { className: "flex-1 min-w-0", children: [
4789
+ /* @__PURE__ */ jsx(
4790
+ "span",
4791
+ {
4792
+ className: "block text-xs font-medium truncate",
4793
+ style: { color: "hsl(var(--ra-text))" },
4794
+ title,
4795
+ children: title
4796
+ }
4797
+ ),
4798
+ subtitle && /* @__PURE__ */ jsx(
4799
+ "span",
4800
+ {
4801
+ className: "block text-[11px] truncate",
4802
+ style: { color: "hsl(var(--ra-muted-text))" },
4803
+ children: subtitle
4804
+ }
4805
+ )
4806
+ ] }),
4807
+ active && /* @__PURE__ */ jsx(
4808
+ Check,
4809
+ {
4810
+ className: "w-3.5 h-3.5 mt-0.5 shrink-0",
4811
+ style: { color: "hsl(var(--ra-accent))" }
4812
+ }
4813
+ )
4814
+ ]
4815
+ }
4816
+ );
4817
+ }
4231
4818
  var TAB_META = {
4232
4819
  product: { icon: Box, label: "Product" },
4233
4820
  variant: { icon: Layers, label: "Variants" },
@@ -4736,9 +5323,10 @@ function DefaultItemTable({
4736
5323
  }
4737
5324
  )
4738
5325
  ] }) }),
4739
- /* @__PURE__ */ jsx("tbody", { children: items.map((item) => {
5326
+ /* @__PURE__ */ jsx("tbody", { children: items.map((item, idx) => {
4740
5327
  const id = item.itemId ?? "";
4741
5328
  const isSelected = !!selectedId && selectedId === id;
5329
+ const key = item.id ?? (id || anchorKey(item.scope) || `pos:${idx}`);
4742
5330
  return /* @__PURE__ */ jsxs(
4743
5331
  "tr",
4744
5332
  {
@@ -4784,7 +5372,7 @@ function DefaultItemTable({
4784
5372
  ] })
4785
5373
  ]
4786
5374
  },
4787
- item.ref
5375
+ key
4788
5376
  );
4789
5377
  }) })
4790
5378
  ] }) });
@@ -4805,8 +5393,9 @@ function DefaultItemCards({
4805
5393
  {
4806
5394
  className: isGallery ? "ra-item-gallery" : "ra-item-cards",
4807
5395
  "data-card-size": cardSize,
4808
- children: items.map((item) => {
5396
+ children: items.map((item, idx) => {
4809
5397
  const id = item.itemId ?? "";
5398
+ const key = item.id ?? (id || anchorKey(item.scope) || `pos:${idx}`);
4810
5399
  const open = () => ctx.onOpen(id);
4811
5400
  const slotCtx = {
4812
5401
  ...ctx,
@@ -4822,7 +5411,7 @@ function DefaultItemCards({
4822
5411
  "data-selected": slotCtx.selected,
4823
5412
  children: renderCard(item, slotCtx)
4824
5413
  },
4825
- item.ref
5414
+ key
4826
5415
  );
4827
5416
  }
4828
5417
  return /* @__PURE__ */ jsxs(
@@ -4854,7 +5443,7 @@ function DefaultItemCards({
4854
5443
  )
4855
5444
  ]
4856
5445
  },
4857
- item.ref
5446
+ key
4858
5447
  );
4859
5448
  })
4860
5449
  }
@@ -4866,6 +5455,7 @@ function ItemListView({
4866
5455
  error,
4867
5456
  ctx,
4868
5457
  itemNoun,
5458
+ ruleSummary,
4869
5459
  view,
4870
5460
  views,
4871
5461
  onViewChange,
@@ -4963,6 +5553,27 @@ function ItemListView({
4963
5553
  }
4964
5554
  return /* @__PURE__ */ jsxs("div", { className: "ra-item-list", children: [
4965
5555
  toolbar,
5556
+ ruleSummary ? /* @__PURE__ */ jsxs(
5557
+ "div",
5558
+ {
5559
+ className: "ra-item-rule-summary",
5560
+ style: {
5561
+ padding: "6px 12px",
5562
+ fontSize: "11px",
5563
+ color: "hsl(var(--ra-muted-text))",
5564
+ borderBottom: "1px solid hsl(var(--ra-border))",
5565
+ background: "hsl(var(--ra-muted) / 0.4)",
5566
+ display: "flex",
5567
+ alignItems: "center",
5568
+ gap: "6px"
5569
+ },
5570
+ children: [
5571
+ /* @__PURE__ */ jsx("span", { style: { fontWeight: 500, color: "hsl(var(--ra-text))" }, children: "Rule" }),
5572
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
5573
+ /* @__PURE__ */ jsx("span", { children: ruleSummary })
5574
+ ]
5575
+ }
5576
+ ) : null,
4966
5577
  /* @__PURE__ */ jsx("div", { className: "ra-item-list-body", children: body })
4967
5578
  ] });
4968
5579
  }
@@ -5051,11 +5662,13 @@ function SiblingRail({
5051
5662
  isLoading && /* @__PURE__ */ jsx(LoadingState, {}),
5052
5663
  !isLoading && error && /* @__PURE__ */ jsx(ErrorState, { error }),
5053
5664
  !isLoading && !error && items.length === 0 && /* @__PURE__ */ jsx(EmptyState, { title: i18n.noItemsTitle, body: i18n.noItemsBody }),
5054
- !isLoading && !error && items.length > 0 && /* @__PURE__ */ jsx("ul", { className: "ra-sibling-list", children: items.map((item) => {
5665
+ !isLoading && !error && items.length > 0 && /* @__PURE__ */ jsx("ul", { className: "ra-sibling-list", children: items.map((item, idx) => {
5055
5666
  const id = item.itemId ?? "";
5667
+ const key = item.id ?? (id || anchorKey(item.scope) || `pos:${idx}`);
5668
+ const akey = anchorKey(item.scope);
5056
5669
  const selected = selectedItemId === id;
5057
- const isDirty = !!(id && dirtyKeys?.has(id) || item.ref && dirtyKeys?.has(item.ref));
5058
- const hasError = !!(id && errorKeys?.has(id) || item.ref && errorKeys?.has(item.ref));
5670
+ const isDirty = !!(id && dirtyKeys?.has(id) || akey && dirtyKeys?.has(akey));
5671
+ const hasError = !!(id && errorKeys?.has(id) || akey && errorKeys?.has(akey));
5059
5672
  return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
5060
5673
  "button",
5061
5674
  {
@@ -5085,7 +5698,7 @@ function SiblingRail({
5085
5698
  ) : null
5086
5699
  ]
5087
5700
  }
5088
- ) }, item.ref);
5701
+ ) }, key);
5089
5702
  }) })
5090
5703
  ] })
5091
5704
  ] });
@@ -5143,9 +5756,6 @@ var UtilityRow = ({ label, customLabel, introHidden, onShowIntro }) => {
5143
5756
  }
5144
5757
  );
5145
5758
  };
5146
-
5147
- // src/components/AdminPageHeader/admin-page-header.css
5148
- styleInject('.sl-aph {\n width: 100%;\n font-family: var(--ra-font-ui, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);\n color: hsl(var(--ra-text, 222 47% 11%));\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n.sl-aph *,\n.sl-aph *::before,\n.sl-aph *::after {\n box-sizing: border-box;\n}\n.sl-aph__row {\n position: relative;\n display: flex;\n align-items: flex-start;\n gap: 0.6rem;\n padding: 0.25rem 0.25rem 0.5rem;\n}\n.sl-aph__main {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: flex-start;\n gap: 0.55rem;\n}\n.sl-aph__aside {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n flex-shrink: 0;\n}\n.sl-aph__text {\n flex: 1;\n min-width: 0;\n}\n.sl-aph__title {\n font-family: var(--ra-font-display, var(--ra-font-ui, ui-sans-serif, system-ui, sans-serif));\n font-weight: 700;\n font-size: 1.2rem;\n line-height: 1.2;\n color: hsl(var(--ra-text, 222 47% 11%));\n letter-spacing: -0.015em;\n margin: 0;\n display: inline-flex;\n align-items: center;\n gap: 0.5rem;\n}\n.sl-aph__icon {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n justify-content: flex-start;\n background: transparent;\n color: hsl(var(--ra-text, 222 47% 11%));\n border: 0;\n padding: 0;\n}\n.sl-aph__icon > svg {\n width: 1.05rem;\n height: 1.05rem;\n}\n.sl-aph__subtitle {\n font-size: 0.78rem;\n color: hsl(var(--ra-muted-text, 220 9% 46%));\n margin: 0.1rem 0 0;\n line-height: 1.3;\n}\n.sl-aph__icon-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 2rem;\n height: 2rem;\n padding: 0;\n border-radius: 999px;\n background: transparent;\n border: 1px solid transparent;\n color: hsl(var(--ra-muted-text, 220 9% 46%));\n cursor: pointer;\n transition:\n background .15s ease,\n color .15s ease,\n border-color .15s ease;\n text-decoration: none;\n}\n.sl-aph__icon-btn:hover {\n background: hsl(var(--ra-text, 222 47% 11%) / 0.06);\n color: hsl(var(--ra-text, 222 47% 11%));\n}\n.sl-aph__icon-btn:focus-visible {\n outline: none;\n box-shadow: 0 0 0 3px var(--ra-focus-ring, hsl(222 47% 11% / 0.35));\n}\n.sl-aph__icon-btn > svg {\n width: 1rem;\n height: 1rem;\n}\n.sl-aph__intro {\n position: relative;\n display: flex;\n align-items: center;\n gap: 0.55rem;\n padding: 0.4rem 2rem 0.4rem 0.5rem;\n border-radius: var(--ra-radius, 0.625rem);\n border: 1px solid hsl(var(--ra-info, 214 95% 55%) / 0.30);\n background: hsl(var(--ra-info, 214 95% 55%) / 0.08);\n}\n.sl-aph__intro[data-tone=success] {\n border-color: hsl(var(--ra-success, 142 71% 45%) / 0.30);\n background: hsl(var(--ra-success, 142 71% 45%) / 0.08);\n}\n.sl-aph__intro[data-tone=warning] {\n border-color: hsl(var(--ra-warning, 38 92% 50%) / 0.35);\n background: hsl(var(--ra-warning, 38 92% 50%) / 0.10);\n}\n.sl-aph__intro-icon {\n flex-shrink: 0;\n width: 1.5rem;\n height: 1.5rem;\n border-radius: 999px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n background: hsl(var(--ra-info, 214 95% 55%) / 0.18);\n color: hsl(var(--ra-info, 214 95% 55%));\n}\n.sl-aph__intro[data-tone=success] .sl-aph__intro-icon {\n background: hsl(var(--ra-success, 142 71% 45%) / 0.18);\n color: hsl(var(--ra-success, 142 71% 45%));\n}\n.sl-aph__intro[data-tone=warning] .sl-aph__intro-icon {\n background: hsl(var(--ra-warning, 38 92% 50%) / 0.20);\n color: hsl(var(--ra-warning, 38 92% 50%));\n}\n.sl-aph__intro-body {\n flex: 1;\n min-width: 0;\n}\n.sl-aph__intro-title {\n font-family: var(--ra-font-display, var(--ra-font-ui, ui-sans-serif, system-ui, sans-serif));\n font-weight: var(--ra-title-weight, 600);\n font-size: 0.8rem;\n color: hsl(var(--ra-text, 222 47% 11%));\n margin: 0;\n line-height: 1.2;\n display: inline;\n}\n.sl-aph__intro-text {\n font-size: 0.78rem;\n color: hsl(var(--ra-text, 222 47% 11%) / 0.85);\n line-height: 1.35;\n display: inline;\n margin-left: 0.4rem;\n}\n.sl-aph__intro-action {\n margin-left: 0.375rem;\n}\n.sl-aph__intro-dismiss {\n position: absolute;\n top: 50%;\n right: 0.35rem;\n transform: translateY(-50%);\n width: 1.4rem;\n height: 1.4rem;\n border-radius: 999px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n background: transparent;\n border: 0;\n color: hsl(var(--ra-muted-text, 220 9% 46%));\n cursor: pointer;\n padding: 0;\n}\n.sl-aph__intro-dismiss:hover {\n background: hsl(var(--ra-text, 222 47% 11%) / 0.06);\n color: hsl(var(--ra-text, 222 47% 11%));\n}\n');
5149
5759
  var TONE_ICON2 = {
5150
5760
  info: Lightbulb,
5151
5761
  success: CheckCircle2,
@@ -5496,6 +6106,183 @@ function WizardFooter({
5496
6106
  }
5497
6107
  );
5498
6108
  }
6109
+ var RuleGroupEditDialog = ({
6110
+ open,
6111
+ ctx,
6112
+ currentRule,
6113
+ recordIds,
6114
+ onClose,
6115
+ onApplied
6116
+ }) => {
6117
+ const [draftRule, setDraftRule] = useState(currentRule);
6118
+ const [isApplying, setIsApplying] = useState(false);
6119
+ const [error, setError] = useState(null);
6120
+ const [completed, setCompleted] = useState(() => /* @__PURE__ */ new Set());
6121
+ const cancelledRef = useRef(false);
6122
+ const facets = useFacets(ctx.collectionId, void 0);
6123
+ useEffect(() => {
6124
+ if (!open) return;
6125
+ setDraftRule(currentRule);
6126
+ setError(null);
6127
+ setCompleted(/* @__PURE__ */ new Set());
6128
+ cancelledRef.current = false;
6129
+ }, [open, currentRule]);
6130
+ useEffect(() => {
6131
+ if (!open) return;
6132
+ const prev = document.body.style.overflow;
6133
+ document.body.style.overflow = "hidden";
6134
+ const onKey = (e) => {
6135
+ if (e.key === "Escape" && !isApplying) {
6136
+ e.stopPropagation();
6137
+ onClose();
6138
+ }
6139
+ };
6140
+ window.addEventListener("keydown", onKey, true);
6141
+ return () => {
6142
+ window.removeEventListener("keydown", onKey, true);
6143
+ document.body.style.overflow = prev;
6144
+ };
6145
+ }, [open, isApplying, onClose]);
6146
+ const isUnchanged = useMemo(
6147
+ () => rulesEqual(draftRule, currentRule),
6148
+ [draftRule, currentRule]
6149
+ );
6150
+ const remaining = useMemo(
6151
+ () => recordIds.filter((id) => !completed.has(id)),
6152
+ [recordIds, completed]
6153
+ );
6154
+ const apply = async () => {
6155
+ if (isUnchanged) {
6156
+ onClose();
6157
+ return;
6158
+ }
6159
+ setIsApplying(true);
6160
+ setError(null);
6161
+ cancelledRef.current = false;
6162
+ try {
6163
+ for (const id of remaining) {
6164
+ if (cancelledRef.current) break;
6165
+ await updateRecord(ctx, id, { facetRule: draftRule });
6166
+ setCompleted((prev) => {
6167
+ const next = new Set(prev);
6168
+ next.add(id);
6169
+ return next;
6170
+ });
6171
+ }
6172
+ if (!cancelledRef.current) {
6173
+ onApplied?.(draftRule);
6174
+ onClose();
6175
+ }
6176
+ } catch (err) {
6177
+ setError(err instanceof Error ? err.message : String(err));
6178
+ } finally {
6179
+ setIsApplying(false);
6180
+ }
6181
+ };
6182
+ if (!open || typeof document === "undefined") return null;
6183
+ const total = recordIds.length;
6184
+ const done = completed.size;
6185
+ return createPortal(
6186
+ /* @__PURE__ */ jsxs(
6187
+ "div",
6188
+ {
6189
+ className: "ra-shell ra-confirm-root",
6190
+ role: "presentation",
6191
+ onMouseDown: (e) => e.stopPropagation(),
6192
+ onClick: (e) => e.stopPropagation(),
6193
+ children: [
6194
+ /* @__PURE__ */ jsx("div", { className: "ra-confirm-backdrop", onClick: () => !isApplying && onClose() }),
6195
+ /* @__PURE__ */ jsxs(
6196
+ "div",
6197
+ {
6198
+ role: "dialog",
6199
+ "aria-modal": "true",
6200
+ "aria-labelledby": "ra-rule-bulk-title",
6201
+ className: "ra-confirm-card",
6202
+ style: { maxWidth: 560, width: "92vw" },
6203
+ children: [
6204
+ /* @__PURE__ */ jsxs("div", { className: "ra-confirm-header", style: { justifyContent: "space-between" }, children: [
6205
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
6206
+ /* @__PURE__ */ jsx("span", { className: "ra-confirm-icon", "aria-hidden": "true", children: /* @__PURE__ */ jsx(AlertTriangle, { className: "w-4 h-4" }) }),
6207
+ /* @__PURE__ */ jsxs("h2", { id: "ra-rule-bulk-title", className: "ra-confirm-title", children: [
6208
+ "Edit rule for ",
6209
+ total,
6210
+ " record",
6211
+ total === 1 ? "" : "s"
6212
+ ] })
6213
+ ] }),
6214
+ /* @__PURE__ */ jsx(
6215
+ "button",
6216
+ {
6217
+ type: "button",
6218
+ className: "ra-confirm-btn ra-confirm-btn-ghost",
6219
+ style: { padding: 4 },
6220
+ onClick: () => !isApplying && onClose(),
6221
+ "aria-label": "Close",
6222
+ children: /* @__PURE__ */ jsx(X, { className: "w-4 h-4" })
6223
+ }
6224
+ )
6225
+ ] }),
6226
+ /* @__PURE__ */ jsxs("div", { className: "px-4 pb-2 text-xs", style: { color: "hsl(var(--ra-muted-text))" }, children: [
6227
+ "Currently targeting:",
6228
+ " ",
6229
+ /* @__PURE__ */ jsx("span", { style: { color: "hsl(var(--ra-text))" }, children: summariseRule(currentRule) })
6230
+ ] }),
6231
+ /* @__PURE__ */ jsx("div", { className: "px-4 pb-4", children: /* @__PURE__ */ jsx(
6232
+ FacetRuleEditor,
6233
+ {
6234
+ value: draftRule,
6235
+ onChange: setDraftRule,
6236
+ collectionId: ctx.collectionId,
6237
+ description: "The new rule will be applied to every record in this group."
6238
+ }
6239
+ ) }),
6240
+ error && /* @__PURE__ */ jsxs(
6241
+ "div",
6242
+ {
6243
+ className: "mx-4 mb-2 px-3 py-2 text-xs rounded",
6244
+ style: {
6245
+ background: "hsl(var(--ra-danger-soft, var(--destructive) / 0.12))",
6246
+ color: "hsl(var(--ra-danger, var(--destructive)))"
6247
+ },
6248
+ role: "alert",
6249
+ children: [
6250
+ error,
6251
+ done > 0 && ` Applied to ${done} of ${total} so far.`
6252
+ ]
6253
+ }
6254
+ ),
6255
+ /* @__PURE__ */ jsxs("div", { className: "ra-confirm-actions", children: [
6256
+ /* @__PURE__ */ jsx(
6257
+ "button",
6258
+ {
6259
+ type: "button",
6260
+ className: "ra-confirm-btn ra-confirm-btn-ghost",
6261
+ onClick: onClose,
6262
+ disabled: isApplying,
6263
+ children: "Cancel"
6264
+ }
6265
+ ),
6266
+ /* @__PURE__ */ jsx(
6267
+ "button",
6268
+ {
6269
+ type: "button",
6270
+ className: "ra-confirm-btn ra-confirm-btn-primary",
6271
+ onClick: apply,
6272
+ disabled: isApplying || isUnchanged || !facets.length,
6273
+ children: isApplying ? `Applying ${done + 1} of ${total}\u2026` : error ? `Retry (${remaining.length} left)` : `Apply to all ${total}`
6274
+ }
6275
+ )
6276
+ ] })
6277
+ ]
6278
+ }
6279
+ )
6280
+ ]
6281
+ }
6282
+ ),
6283
+ document.body
6284
+ );
6285
+ };
5499
6286
  var statusDot = (status) => {
5500
6287
  switch (status) {
5501
6288
  case "saving":
@@ -6063,12 +6850,6 @@ function useShellCsvBulk(args) {
6063
6850
  [csvSchema, handleImport, handleExport]
6064
6851
  );
6065
6852
  }
6066
-
6067
- // src/components/RecordsAdmin/shell/tokens.css
6068
- styleInject(':root {\n --ra-status-own: var(--ra-emerald, 142 71% 45%);\n --ra-status-shared: var(--ra-amber, 38 92% 50%);\n --ra-status-missing: var(--muted-foreground, 220 9% 46%);\n --ra-accent: var(--primary, 222 47% 11%);\n --ra-surface: var(--card, 0 0% 100%);\n --ra-border: var(--border, 220 13% 91%);\n --ra-text: var(--foreground, 222 47% 11%);\n --ra-muted: var(--muted, 220 14% 96%);\n --ra-muted-text: var(--muted-foreground, 220 9% 46%);\n --ra-radius: var(--radius, 0.625rem);\n --ra-dot-size: 0.5rem;\n --ra-page-bg: var(--background, 220 14% 98%);\n --ra-card-shadow: 0 1px 2px hsl(var(--ra-accent) / 0.04), 0 4px 12px hsl(var(--ra-accent) / 0.05);\n --ra-card-shadow-hover: 0 2px 4px hsl(var(--ra-accent) / 0.06), 0 8px 24px hsl(var(--ra-accent) / 0.08);\n --ra-row-hover: hsl(var(--ra-accent) / 0.05);\n --ra-row-active-bg: hsl(var(--ra-accent) / 0.10);\n --ra-row-active-bd: hsl(var(--ra-accent) / 0.45);\n --ra-focus-ring: hsl(var(--ra-accent) / 0.35);\n --ra-font-display: var(--font-display, var(--font-sans, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif));\n --ra-font-ui: var(--font-sans, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);\n --ra-title-weight: 600;\n --ra-display-weight: 700;\n --ra-info: var(--ra-blue, 214 95% 55%);\n --ra-success: var(--ra-emerald, 142 71% 45%);\n --ra-warning: var(--ra-amber, 38 92% 50%);\n --ra-danger: var(--destructive, 0 72% 51%);\n}\n:root {\n --sl-control-bg: var(--muted, 220 14% 96%);\n --sl-control-fg: var(--muted-foreground, 220 9% 40%);\n --sl-control-border: var(--border, 220 13% 88%);\n --sl-control-active-bg: var(--primary, 222 47% 11%);\n --sl-control-active-fg: var(--primary-foreground, 0 0% 100%);\n --sl-control-active-bd: var(--primary, 222 47% 11%);\n --sl-control-hover-bg: var(--sl-control-active-bg, 222 47% 11%);\n --sl-control-hover-fg: var(--foreground, 222 47% 11%);\n --sl-control-focus-ring: var(--sl-control-active-bg, 222 47% 11%);\n --sl-control-radius: var(--radius, 0.5rem);\n --sl-control-weight: 500;\n --sl-control-active-weight: 600;\n}\n.ra-status-dot {\n display: inline-block;\n width: var(--ra-dot-size);\n height: var(--ra-dot-size);\n border-radius: 9999px;\n flex-shrink: 0;\n}\n.ra-status-own {\n background: hsl(var(--ra-status-own));\n}\n.ra-status-shared {\n background: hsl(var(--ra-status-shared));\n}\n.ra-status-missing {\n background: hsl(var(--ra-status-missing) / 0.4);\n border: 1px solid hsl(var(--ra-status-missing) / 0.6);\n}\n.ra-row-active {\n background: var(--ra-row-active-bg);\n border-color: var(--ra-row-active-bd) !important;\n}\n');
6069
-
6070
- // src/components/RecordsAdmin/shell/shell.css
6071
- styleInject(".ra-shell {\n color: hsl(var(--ra-text));\n background: hsl(var(--ra-page-bg));\n font-family: var(--ra-font-ui);\n}\n.ra-shell *,\n.ra-shell *::before,\n.ra-shell *::after {\n box-sizing: border-box;\n}\n.ra-shell .ra-card {\n background: hsl(var(--ra-surface));\n border: 1px solid hsl(var(--ra-border));\n border-radius: var(--ra-radius);\n box-shadow: var(--ra-card-shadow);\n}\n.ra-shell .ra-card-hover {\n transition:\n box-shadow .18s ease,\n transform .18s ease,\n border-color .18s ease;\n}\n.ra-shell .ra-card-hover:hover {\n box-shadow: var(--ra-card-shadow-hover);\n}\n.ra-shell .ra-display {\n font-family: var(--ra-font-display);\n font-weight: var(--ra-display-weight);\n letter-spacing: -0.01em;\n}\n.ra-shell .ra-title {\n font-weight: var(--ra-title-weight);\n}\n.ra-shell :where(button, [role=button], input, select, textarea, a):focus-visible {\n outline: none;\n box-shadow: 0 0 0 3px var(--ra-focus-ring);\n border-radius: calc(var(--ra-radius) * 0.6);\n}\n.ra-shell .ra-header {\n display: block;\n width: 100%;\n}\n.ra-shell .ra-header__main {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: flex-start;\n gap: 0.55rem;\n}\n.ra-shell .ra-header-aside {\n display: flex;\n align-items: flex-start;\n gap: 0.5rem;\n flex-shrink: 0;\n}\n.ra-shell .ra-header-icon {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n justify-content: flex-start;\n background: transparent;\n color: hsl(var(--ra-text));\n border: 0;\n padding: 0;\n margin-top: 0.1rem;\n}\n.ra-shell .ra-header-icon > svg {\n width: 1.05rem;\n height: 1.05rem;\n}\n.ra-shell .ra-header-text {\n flex: 1;\n min-width: 0;\n}\n.ra-shell .ra-header-title {\n font-family: var(--ra-font-display);\n font-weight: 700;\n font-size: 1.2rem;\n line-height: 1.2;\n color: hsl(var(--ra-text));\n letter-spacing: -0.015em;\n margin: 0;\n display: inline-flex;\n align-items: center;\n gap: 0.5rem;\n}\n.ra-shell .ra-header-subtitle {\n font-size: 0.78rem;\n color: hsl(var(--ra-muted-text));\n margin-top: 0.1rem;\n line-height: 1.3;\n}\n.ra-shell .ra-header-stats {\n display: flex;\n align-items: stretch;\n gap: 0.15rem;\n padding: 0.15rem 0.4rem;\n border-radius: calc(var(--ra-radius) * 0.75);\n background: hsl(var(--ra-surface) / 0.7);\n border: 1px solid hsl(var(--ra-border));\n}\n.ra-shell .ra-header-stats--titled {\n flex-direction: column;\n align-items: stretch;\n padding: 0.4rem 0.55rem;\n gap: 0.3rem;\n}\n.ra-shell .ra-header-stats .ra-stats-items {\n display: flex;\n align-items: stretch;\n gap: 0.15rem;\n}\n.ra-shell .ra-header-stats .ra-stats-heading {\n display: flex;\n align-items: center;\n gap: 0.35rem;\n color: hsl(var(--ra-muted-text));\n font-size: 0.65rem;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n}\n.ra-shell .ra-header-stats .ra-stats-heading-icon {\n display: inline-flex;\n align-items: center;\n color: hsl(var(--ra-text));\n opacity: 0.75;\n}\n.ra-shell .ra-header-stats .ra-stats-heading-icon > svg {\n width: 0.85rem;\n height: 0.85rem;\n}\n.ra-shell .ra-stat {\n display: flex;\n flex-direction: column;\n align-items: center;\n padding: 0.15rem 0.45rem;\n min-width: 2.5rem;\n}\n.ra-shell .ra-stat-value {\n font-family: var(--ra-font-display);\n font-weight: var(--ra-display-weight);\n font-size: 0.85rem;\n color: hsl(var(--ra-text));\n line-height: 1;\n}\n.ra-shell .ra-stat-label {\n font-size: 0.6rem;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: hsl(var(--ra-muted-text));\n margin-top: 0.15rem;\n}\n.ra-shell .ra-stat-divider {\n width: 1px;\n background: hsl(var(--ra-border));\n margin: 0.25rem 0;\n}\n.ra-shell .ra-header-actions {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n.ra-shell .ra-tabs {\n display: flex;\n gap: 0.25rem;\n padding: 0.25rem;\n background: hsl(var(--sl-control-bg));\n border-radius: var(--sl-control-radius);\n border: 1px solid hsl(var(--sl-control-border));\n}\n.ra-shell .ra-tab {\n display: inline-flex;\n align-items: center;\n gap: 0.4rem;\n padding: 0.4rem 0.7rem;\n border-radius: calc(var(--sl-control-radius) - 2px);\n font-size: 0.78rem;\n font-weight: var(--sl-control-weight);\n color: hsl(var(--sl-control-fg));\n background: transparent;\n border: 0;\n cursor: pointer;\n transition:\n background .15s ease,\n color .15s ease,\n transform .15s ease;\n white-space: nowrap;\n}\n.ra-shell .ra-tab:hover {\n background: hsl(var(--sl-control-hover-bg) / 0.10);\n color: hsl(var(--sl-control-hover-fg));\n}\n.ra-shell .ra-tab:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px hsl(var(--sl-control-focus-ring) / 0.45);\n}\n.ra-shell .ra-tab[aria-selected=true] {\n background: hsl(var(--sl-control-active-bg));\n color: hsl(var(--sl-control-active-fg));\n border-color: hsl(var(--sl-control-active-bd));\n font-weight: var(--sl-control-active-weight);\n box-shadow: 0 1px 2px hsl(var(--sl-control-active-bg) / 0.25);\n}\n.ra-shell .ra-tab[aria-selected=true]:hover {\n background: hsl(var(--sl-control-active-bg) / 0.92);\n color: hsl(var(--sl-control-active-fg));\n}\n.ra-shell .ra-tab[aria-selected=true] .ra-tab-icon {\n color: hsl(var(--sl-control-active-fg));\n}\n.ra-shell .ra-tab[disabled] {\n opacity: .5;\n cursor: not-allowed;\n}\n.ra-shell .ra-tab-count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 1.25rem;\n padding: 0 0.35rem;\n height: 1.1rem;\n border-radius: 999px;\n background: hsl(var(--sl-control-active-fg) / 0.20);\n color: hsl(var(--sl-control-active-fg));\n font-size: 0.625rem;\n font-weight: 600;\n line-height: 1;\n}\n.ra-shell .ra-tab[aria-selected=false] .ra-tab-count {\n background: hsl(var(--sl-control-fg) / 0.15);\n color: hsl(var(--sl-control-fg));\n}\n.ra-shell[data-density=compact] .ra-row {\n padding-block: 0.4rem;\n}\n.ra-shell .ra-row {\n display: flex;\n align-items: center;\n gap: 0.55rem;\n width: 100%;\n text-align: left;\n padding: 0.45rem 0.75rem;\n border-left: 3px solid transparent;\n background: transparent;\n border-bottom: 1px solid transparent;\n transition: background .12s ease, border-color .12s ease;\n cursor: pointer;\n color: hsl(var(--ra-text));\n font-family: inherit;\n}\n.ra-shell .ra-row + .ra-row {\n border-top: 1px solid hsl(var(--ra-border) / 0.6);\n}\n.ra-shell .ra-row:hover {\n background: var(--ra-row-hover);\n}\n.ra-shell .ra-row[data-selected=true] {\n background: var(--ra-row-active-bg);\n border-left-color: var(--ra-row-active-bd);\n}\n.ra-shell .ra-row-compact {\n padding-block: 0.3rem;\n}\n.ra-shell .ra-row-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.5rem;\n height: 1.5rem;\n border-radius: calc(var(--ra-radius) * 0.6);\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-muted-text));\n flex-shrink: 0;\n}\n.ra-shell .ra-row[data-selected=true] .ra-row-icon {\n background: hsl(var(--ra-accent) / 0.15);\n color: hsl(var(--ra-accent));\n}\n.ra-shell .ra-row-body {\n flex: 1;\n min-width: 0;\n}\n.ra-shell .ra-row-title {\n font-weight: var(--ra-title-weight);\n font-size: 0.8125rem;\n line-height: 1.2;\n color: hsl(var(--ra-text));\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ra-shell .ra-row-sub {\n font-size: 0.6875rem;\n color: hsl(var(--ra-muted-text));\n margin-top: 0.05rem;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ra-shell .ra-row-rule-chips {\n display: flex;\n flex-wrap: wrap;\n gap: 0.2rem;\n margin-top: 0.2rem;\n}\n.ra-shell .ra-rule-chip {\n display: inline-flex;\n align-items: center;\n max-width: 100%;\n padding: 0.05rem 0.4rem;\n border-radius: 999px;\n font-size: 0.625rem;\n font-weight: 500;\n line-height: 1.4;\n background: hsl(var(--ra-accent) / 0.10);\n color: hsl(var(--ra-accent));\n border: 1px solid hsl(var(--ra-accent) / 0.20);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ra-shell .ra-rule-chip-more {\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-muted-text));\n border-color: hsl(var(--ra-border));\n}\n.ra-shell[data-density=compact] .ra-row-rule-chips {\n margin-top: 0.15rem;\n gap: 0.15rem;\n}\n.ra-shell[data-density=compact] .ra-rule-chip {\n font-size: 0.6rem;\n padding: 0.02rem 0.35rem;\n}\n.ra-shell .ra-rule-filters {\n display: flex;\n flex-direction: column;\n gap: 0.3rem;\n}\n.ra-shell .ra-rule-filters-row {\n display: flex;\n flex-wrap: wrap;\n gap: 0.25rem;\n}\n.ra-shell .ra-rule-filter-chip {\n display: inline-flex;\n align-items: center;\n gap: 0.3rem;\n padding: 0.15rem 0.5rem;\n border-radius: 999px;\n font-size: 0.65rem;\n font-weight: 500;\n line-height: 1.4;\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-muted-text));\n border: 1px solid hsl(var(--ra-border));\n cursor: pointer;\n transition:\n background .12s ease,\n color .12s ease,\n border-color .12s ease;\n max-width: 100%;\n}\n.ra-shell .ra-rule-filter-chip:hover {\n background: hsl(var(--ra-accent) / 0.10);\n color: hsl(var(--ra-text));\n border-color: hsl(var(--ra-accent) / 0.25);\n}\n.ra-shell .ra-rule-filter-chip[data-active=true] {\n background: hsl(var(--ra-accent) / 0.15);\n color: hsl(var(--ra-accent));\n border-color: hsl(var(--ra-accent) / 0.40);\n}\n.ra-shell .ra-rule-filter-chip[data-tone=complexity][data-active=true] {\n background: hsl(var(--ra-info) / 0.15);\n color: hsl(var(--ra-info));\n border-color: hsl(var(--ra-info) / 0.40);\n}\n.ra-shell .ra-rule-filter-chip-label {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 9rem;\n}\n.ra-shell .ra-rule-filter-chip-count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 1.1rem;\n height: 1rem;\n padding: 0 0.3rem;\n border-radius: 999px;\n background: hsl(var(--ra-surface));\n color: hsl(var(--ra-muted-text));\n font-size: 0.6rem;\n font-weight: 600;\n}\n.ra-shell .ra-rule-filter-chip[data-active=true] .ra-rule-filter-chip-count {\n background: hsl(var(--ra-accent) / 0.18);\n color: hsl(var(--ra-accent));\n}\n.ra-shell .ra-rule-filter-clear {\n align-self: flex-start;\n background: transparent;\n border: 0;\n padding: 0;\n color: hsl(var(--ra-muted-text));\n font-size: 0.65rem;\n cursor: pointer;\n text-decoration: underline;\n text-decoration-style: dotted;\n}\n.ra-shell .ra-rule-filter-clear:hover {\n color: hsl(var(--ra-text));\n}\n.ra-shell[data-density=compact] .ra-row {\n padding-block: 0.3rem;\n gap: 0.45rem;\n}\n.ra-shell[data-density=compact] .ra-row-title {\n font-size: 0.78125rem;\n}\n.ra-shell .ra-row-actions {\n display: inline-flex;\n align-items: center;\n gap: 0.15rem;\n margin-left: auto;\n opacity: 0;\n transition: opacity .15s ease;\n}\n.ra-shell .ra-row:hover .ra-row-actions,\n.ra-shell .ra-row:focus-within .ra-row-actions {\n opacity: 1;\n}\n.ra-shell .ra-row-action {\n width: 1.6rem;\n height: 1.6rem;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 999px;\n background: transparent;\n color: hsl(var(--ra-muted-text));\n border: 0;\n cursor: pointer;\n transition: background .15s ease, color .15s ease;\n}\n.ra-shell .ra-row-action:hover {\n background: hsl(var(--ra-accent) / 0.10);\n color: hsl(var(--ra-text));\n}\n.ra-shell .ra-row-action[data-tone=danger]:hover {\n background: hsl(var(--ra-danger) / 0.12);\n color: hsl(var(--ra-danger));\n}\n.ra-shell .ra-chip {\n display: inline-flex;\n align-items: center;\n gap: 0.3rem;\n padding: 0.15rem 0.5rem;\n border-radius: 999px;\n font-size: 0.6875rem;\n font-weight: 500;\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-muted-text));\n border: 1px solid hsl(var(--ra-border));\n white-space: nowrap;\n max-width: 14rem;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ra-shell .ra-chip[data-tone=success] {\n background: hsl(var(--ra-success) / 0.12);\n color: hsl(var(--ra-success));\n border-color: hsl(var(--ra-success) / 0.30);\n}\n.ra-shell .ra-chip[data-tone=warning] {\n background: hsl(var(--ra-warning) / 0.14);\n color: hsl(var(--ra-warning));\n border-color: hsl(var(--ra-warning) / 0.35);\n}\n.ra-shell .ra-chip[data-tone=info] {\n background: hsl(var(--ra-info) / 0.10);\n color: hsl(var(--ra-info));\n border-color: hsl(var(--ra-info) / 0.30);\n}\n.ra-shell .ra-chip[data-tone=danger] {\n background: hsl(var(--ra-danger) / 0.10);\n color: hsl(var(--ra-danger));\n border-color: hsl(var(--ra-danger) / 0.30);\n}\n.ra-shell .ra-chip[data-tone=muted] {\n background: transparent;\n color: hsl(var(--ra-muted-text));\n border-style: dashed;\n}\n.ra-shell .ra-group {\n border-bottom: 1px solid hsl(var(--ra-border));\n}\n.ra-shell .ra-group:last-child {\n border-bottom: 0;\n}\n.ra-shell .ra-group-summary {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n width: 100%;\n padding: 0.5rem 0.85rem;\n background: hsl(var(--ra-muted) / 0.6);\n font-size: 0.7rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: hsl(var(--ra-muted-text));\n border: 0;\n cursor: pointer;\n transition: background .12s ease;\n}\n.ra-shell .ra-group-summary:hover {\n background: hsl(var(--ra-muted));\n}\n.ra-shell .ra-group-summary .ra-group-chevron {\n transition: transform .15s ease;\n}\n.ra-shell .ra-group[data-open=false] .ra-group-chevron {\n transform: rotate(-90deg);\n}\n.ra-shell .ra-group-name {\n flex: 1;\n text-align: left;\n}\n.ra-shell .ra-group-count {\n font-size: 0.65rem;\n font-weight: 600;\n color: hsl(var(--ra-muted-text));\n background: hsl(var(--ra-surface));\n border: 1px solid hsl(var(--ra-border));\n border-radius: 999px;\n padding: 0.05rem 0.4rem;\n}\n.ra-shell .ra-group[data-open=false] .ra-group-body {\n display: none;\n}\n.ra-shell .ra-empty {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n padding: 2.5rem 1.5rem;\n gap: 0.75rem;\n}\n.ra-shell .ra-empty-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 3.25rem;\n height: 3.25rem;\n border-radius: 999px;\n background: hsl(var(--ra-accent) / 0.08);\n color: hsl(var(--ra-accent));\n margin-bottom: 0.25rem;\n}\n.ra-shell .ra-empty-title {\n font-family: var(--ra-font-display);\n font-weight: var(--ra-display-weight);\n font-size: 1rem;\n color: hsl(var(--ra-text));\n margin: 0;\n letter-spacing: -0.01em;\n}\n.ra-shell .ra-empty-body {\n font-size: 0.8125rem;\n color: hsl(var(--ra-muted-text));\n max-width: 22rem;\n line-height: 1.45;\n}\n.ra-shell .ra-empty-actions {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n margin-top: 0.25rem;\n flex-wrap: wrap;\n justify-content: center;\n}\n.ra-shell .ra-btn {\n display: inline-flex;\n align-items: center;\n gap: 0.4rem;\n padding: 0.45rem 0.85rem;\n border-radius: calc(var(--ra-radius) * 0.7);\n font-size: 0.8125rem;\n font-weight: 500;\n border: 1px solid hsl(var(--ra-border));\n background: hsl(var(--ra-surface));\n color: hsl(var(--ra-text));\n cursor: pointer;\n transition:\n background .15s ease,\n border-color .15s ease,\n box-shadow .15s ease,\n transform .1s ease;\n}\n.ra-shell .ra-btn:hover {\n background: hsl(var(--ra-muted));\n box-shadow: var(--ra-card-shadow);\n}\n.ra-shell .ra-btn:active {\n transform: translateY(1px);\n}\n.ra-shell .ra-btn[data-variant=primary] {\n background: hsl(var(--ra-accent));\n color: hsl(var(--ra-surface));\n border-color: hsl(var(--ra-accent));\n}\n.ra-shell .ra-btn[data-variant=primary]:hover {\n background: hsl(var(--ra-accent) / 0.92);\n}\n.ra-shell .ra-btn[data-variant=ghost] {\n background: transparent;\n border-color: transparent;\n color: hsl(var(--ra-muted-text));\n}\n.ra-shell .ra-btn[data-variant=ghost]:hover {\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-text));\n}\n.ra-shell .ra-btn[data-variant=danger] {\n color: hsl(var(--ra-danger));\n}\n.ra-shell .ra-btn[data-variant=danger]:hover {\n background: hsl(var(--ra-danger) / 0.10);\n border-color: hsl(var(--ra-danger) / 0.40);\n}\n.ra-shell .ra-intro {\n position: relative;\n display: flex;\n align-items: center;\n gap: 0.55rem;\n padding: 0.4rem 2rem 0.4rem 0.5rem;\n border-radius: var(--ra-radius);\n border: 1px solid hsl(var(--ra-info) / 0.30);\n background: hsl(var(--ra-info) / 0.08);\n}\n.ra-shell .ra-intro[data-tone=success] {\n border-color: hsl(var(--ra-success) / 0.30);\n background: hsl(var(--ra-success) / 0.08);\n}\n.ra-shell .ra-intro[data-tone=warning] {\n border-color: hsl(var(--ra-warning) / 0.35);\n background: hsl(var(--ra-warning) / 0.10);\n}\n.ra-shell .ra-intro-icon {\n flex-shrink: 0;\n width: 1.5rem;\n height: 1.5rem;\n border-radius: 999px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n background: hsl(var(--ra-info) / 0.18);\n color: hsl(var(--ra-info));\n}\n.ra-shell .ra-intro[data-tone=success] .ra-intro-icon {\n background: hsl(var(--ra-success) / 0.18);\n color: hsl(var(--ra-success));\n}\n.ra-shell .ra-intro[data-tone=warning] .ra-intro-icon {\n background: hsl(var(--ra-warning) / 0.20);\n color: hsl(var(--ra-warning));\n}\n.ra-shell .ra-intro-body {\n flex: 1;\n min-width: 0;\n}\n.ra-shell .ra-intro-title {\n font-family: var(--ra-font-display);\n font-weight: var(--ra-title-weight);\n font-size: 0.8rem;\n color: hsl(var(--ra-text));\n margin: 0;\n line-height: 1.2;\n display: inline;\n}\n.ra-shell .ra-intro-text {\n font-size: 0.78rem;\n color: hsl(var(--ra-text) / 0.85);\n line-height: 1.35;\n display: inline;\n margin-left: 0.4rem;\n}\n.ra-shell .ra-intro-dismiss {\n position: absolute;\n top: 50%;\n right: 0.35rem;\n transform: translateY(-50%);\n width: 1.4rem;\n height: 1.4rem;\n border-radius: 999px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n background: transparent;\n border: 0;\n color: hsl(var(--ra-muted-text));\n cursor: pointer;\n padding: 0;\n flex-shrink: 0;\n}\n.ra-shell .ra-intro-dismiss:hover {\n background: hsl(var(--ra-text) / 0.06);\n color: hsl(var(--ra-text));\n}\n.ra-shell .ra-bulk-menu {\n min-width: 12rem;\n background: hsl(var(--ra-surface));\n border: 1px solid hsl(var(--ra-border));\n border-radius: calc(var(--ra-radius) * 0.85);\n box-shadow: var(--ra-card-shadow-hover);\n padding: 0.3rem;\n z-index: 60;\n}\n.ra-shell .ra-bulk-item {\n display: flex;\n align-items: center;\n gap: 0.55rem;\n width: 100%;\n padding: 0.45rem 0.6rem;\n border-radius: calc(var(--ra-radius) * 0.6);\n font-size: 0.8125rem;\n color: hsl(var(--ra-text));\n background: transparent;\n border: 0;\n cursor: pointer;\n text-align: left;\n transition: background .12s ease, color .12s ease;\n}\n.ra-shell .ra-bulk-item:hover {\n background: hsl(var(--ra-muted));\n}\n.ra-shell .ra-bulk-item[data-tone=danger] {\n color: hsl(var(--ra-danger));\n}\n.ra-shell .ra-bulk-item[data-tone=danger]:hover {\n background: hsl(var(--ra-danger) / 0.10);\n}\n.ra-shell .ra-bulk-divider {\n height: 1px;\n background: hsl(var(--ra-border));\n margin: 0.25rem 0;\n}\n.ra-shell .ra-preview-rail {\n background: hsl(var(--ra-surface));\n border-left: 1px solid hsl(var(--ra-border));\n box-shadow: -4px 0 16px hsl(var(--ra-accent) / 0.04);\n display: flex;\n flex-direction: column;\n height: 100%;\n overflow: hidden;\n}\n.ra-shell .ra-preview-rail-header {\n position: sticky;\n top: 0;\n z-index: 1;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.75rem 1rem;\n background:\n linear-gradient(\n 180deg,\n hsl(var(--ra-surface)) 0%,\n hsl(var(--ra-surface) / 0.92) 100%);\n border-bottom: 1px solid hsl(var(--ra-border));\n backdrop-filter: blur(6px);\n}\n.ra-shell .ra-preview-rail-title {\n display: inline-flex;\n align-items: center;\n gap: 0.4rem;\n font-size: 0.7rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: hsl(var(--ra-muted-text));\n}\n.ra-shell .ra-preview-rail-body {\n flex: 1;\n overflow-y: auto;\n padding: 1rem;\n}\n.ra-confirm-root {\n position: fixed;\n inset: 0;\n z-index: 2147483000;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 1rem;\n background: transparent;\n}\n.ra-confirm-root .ra-confirm-backdrop {\n position: absolute;\n inset: 0;\n background: hsl(0 0% 0% / 0.45);\n backdrop-filter: blur(2px);\n animation: ra-confirm-fade .12s ease-out;\n}\n.ra-confirm-root .ra-confirm-card {\n position: relative;\n width: min(440px, 100%);\n background: hsl(var(--ra-surface));\n color: hsl(var(--ra-text));\n border: 1px solid hsl(var(--ra-border));\n border-radius: var(--ra-radius);\n box-shadow: 0 1px 2px hsl(0 0% 0% / 0.08), 0 24px 48px -12px hsl(0 0% 0% / 0.32);\n padding: 1.25rem;\n animation: ra-confirm-pop .14s ease-out;\n}\n.ra-confirm-root .ra-confirm-header {\n display: flex;\n align-items: center;\n gap: 0.6rem;\n margin-bottom: 0.5rem;\n}\n.ra-confirm-root .ra-confirm-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.75rem;\n height: 1.75rem;\n border-radius: 999px;\n background: hsl(var(--ra-warning, 38 92% 50%) / 0.12);\n color: hsl(var(--ra-warning, 38 92% 50%));\n}\n.ra-confirm-root .ra-confirm-title {\n font-family: var(--ra-font-display);\n font-weight: 600;\n font-size: 1rem;\n margin: 0;\n}\n.ra-confirm-root .ra-confirm-body {\n font-size: 0.875rem;\n color: hsl(var(--ra-muted-text));\n margin: 0 0 1.1rem;\n line-height: 1.45;\n}\n.ra-confirm-root .ra-confirm-actions {\n display: flex;\n justify-content: flex-end;\n gap: 0.5rem;\n flex-wrap: wrap;\n}\n.ra-confirm-root .ra-confirm-btn {\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n border: 1px solid transparent;\n border-radius: calc(var(--ra-radius) - 2px);\n padding: 0.45rem 0.85rem;\n font-size: 0.8125rem;\n font-weight: 500;\n cursor: pointer;\n transition:\n background-color .12s ease,\n border-color .12s ease,\n color .12s ease;\n}\n.ra-confirm-root .ra-confirm-btn:focus-visible {\n outline: none;\n box-shadow: 0 0 0 3px var(--ra-focus-ring);\n}\n.ra-confirm-root .ra-confirm-btn-ghost {\n background: transparent;\n color: hsl(var(--ra-muted-text));\n border-color: hsl(var(--ra-border));\n}\n.ra-confirm-root .ra-confirm-btn-ghost:hover {\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-text));\n}\n.ra-confirm-root .ra-confirm-btn-danger {\n background: transparent;\n color: hsl(var(--ra-danger, 0 72% 51%));\n border-color: hsl(var(--ra-danger, 0 72% 51%) / 0.45);\n}\n.ra-confirm-root .ra-confirm-btn-danger:hover {\n background: hsl(var(--ra-danger, 0 72% 51%) / 0.08);\n border-color: hsl(var(--ra-danger, 0 72% 51%));\n}\n.ra-confirm-root .ra-confirm-btn-primary {\n background: hsl(var(--ra-accent));\n color: hsl(var(--ra-accent-fg, 0 0% 100%));\n border-color: hsl(var(--ra-accent));\n}\n.ra-confirm-root .ra-confirm-btn-primary:hover {\n filter: brightness(0.95);\n}\n@keyframes ra-confirm-fade {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n@keyframes ra-confirm-pop {\n from {\n opacity: 0;\n transform: translateY(4px) scale(.98);\n }\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n.ra-shell .ra-unsaved-banner {\n display: flex;\n align-items: center;\n gap: 0.6rem;\n padding: 0.5rem 0.75rem;\n border: 1px solid hsl(var(--ra-warning, 38 92% 50%) / 0.35);\n background: hsl(var(--ra-warning, 38 92% 50%) / 0.08);\n border-radius: var(--ra-radius);\n font-size: 0.8125rem;\n color: hsl(var(--ra-text));\n animation: ra-unsaved-slide .14s ease-out;\n}\n.ra-shell .ra-unsaved-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: hsl(var(--ra-warning, 38 92% 50%));\n flex-shrink: 0;\n}\n.ra-shell .ra-unsaved-text {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.ra-shell .ra-unsaved-context {\n color: hsl(var(--ra-muted-text));\n font-weight: 400;\n}\n.ra-shell .ra-unsaved-error {\n color: hsl(var(--ra-danger, 0 72% 51%));\n font-size: 0.75rem;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n font-weight: 500;\n}\n.ra-shell .ra-unsaved-actions {\n display: inline-flex;\n gap: 0.4rem;\n flex-shrink: 0;\n}\n.ra-shell .ra-unsaved-btn {\n display: inline-flex;\n align-items: center;\n gap: 0.3rem;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n border: 1px solid transparent;\n border-radius: calc(var(--ra-radius) - 2px);\n padding: 0.3rem 0.6rem;\n font-size: 0.75rem;\n font-weight: 500;\n cursor: pointer;\n transition:\n background-color .12s ease,\n border-color .12s ease,\n color .12s ease,\n opacity .12s ease;\n}\n.ra-shell .ra-unsaved-btn:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.ra-shell .ra-unsaved-btn:focus-visible {\n outline: none;\n box-shadow: 0 0 0 3px var(--ra-focus-ring);\n}\n.ra-shell .ra-unsaved-btn-ghost {\n background: transparent;\n color: hsl(var(--ra-muted-text));\n border-color: hsl(var(--ra-border));\n}\n.ra-shell .ra-unsaved-btn-ghost:hover:not(:disabled) {\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-text));\n}\n.ra-shell .ra-unsaved-btn-primary {\n background: hsl(var(--ra-accent));\n color: hsl(var(--ra-accent-fg, 0 0% 100%));\n border-color: hsl(var(--ra-accent));\n}\n.ra-shell .ra-unsaved-btn-primary:hover:not(:disabled) {\n filter: brightness(0.95);\n}\n.sl-aph .ra-unsaved-btn {\n display: inline-flex;\n align-items: center;\n gap: 0.3rem;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n border: 1px solid transparent;\n border-radius: calc(var(--ra-radius, 8px) - 2px);\n padding: 0.3rem 0.6rem;\n font-size: 0.75rem;\n font-weight: 500;\n cursor: pointer;\n transition:\n background-color .12s ease,\n border-color .12s ease,\n color .12s ease,\n opacity .12s ease;\n}\n.sl-aph .ra-unsaved-btn:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.sl-aph .ra-unsaved-btn:focus-visible {\n outline: none;\n box-shadow: 0 0 0 3px var(--ra-focus-ring, 0 0 0 3px hsl(var(--ra-accent) / 0.35));\n}\n.sl-aph .ra-unsaved-btn-ghost {\n background: transparent;\n color: hsl(var(--ra-muted-text));\n border-color: hsl(var(--ra-border));\n}\n.sl-aph .ra-unsaved-btn-ghost:hover:not(:disabled) {\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-text));\n}\n.sl-aph .ra-unsaved-btn-primary {\n background: hsl(var(--ra-accent));\n color: hsl(var(--ra-accent-fg, 0 0% 100%));\n border-color: hsl(var(--ra-accent));\n}\n.sl-aph .ra-unsaved-btn-primary:hover:not(:disabled) {\n filter: brightness(0.95);\n}\n.sl-aph .ra-unsaved-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: hsl(var(--ra-warning, 38 92% 50%));\n flex-shrink: 0;\n}\n@keyframes ra-unsaved-slide {\n from {\n opacity: 0;\n transform: translateY(-3px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n.ra-shell .ra-unsaved-pill,\n.sl-aph .ra-unsaved-pill {\n display: inline-flex;\n align-items: center;\n gap: 0.4rem;\n padding: 0.25rem 0.5rem 0.25rem 0.6rem;\n border: 1px solid hsl(var(--ra-warning, 38 92% 50%) / 0.35);\n background: hsl(var(--ra-warning, 38 92% 50%) / 0.08);\n border-radius: 999px;\n animation: ra-unsaved-slide .14s ease-out;\n}\n.ra-shell .ra-unsaved-pill .ra-unsaved-pill-text,\n.sl-aph .ra-unsaved-pill .ra-unsaved-pill-text {\n font-size: 0.75rem;\n font-weight: 500;\n color: hsl(var(--ra-text));\n white-space: nowrap;\n margin-right: 0.15rem;\n}\n.ra-shell .ra-clipboard-toast {\n position: fixed;\n bottom: 1.25rem;\n left: 50%;\n transform: translateX(-50%);\n z-index: 90;\n display: inline-flex;\n align-items: center;\n gap: 0.5rem;\n max-width: min(28rem, calc(100vw - 2rem));\n padding: 0.55rem 0.85rem;\n border-radius: 999px;\n background: hsl(var(--ra-text));\n color: hsl(var(--ra-surface));\n font-size: 0.75rem;\n line-height: 1;\n box-shadow: 0 8px 24px -10px hsl(0 0% 0% / 0.45);\n animation: ra-clipboard-pop 0.18s ease-out both;\n pointer-events: none;\n}\n@keyframes ra-clipboard-pop {\n from {\n opacity: 0;\n transform: translate(-50%, 6px);\n }\n to {\n opacity: 1;\n transform: translate(-50%, 0);\n }\n}\n.ra-shell .ra-row-menu-wrap {\n display: inline-flex;\n align-items: center;\n margin-left: 0.25rem;\n}\n.ra-shell .ra-row-menu-trigger {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.5rem;\n height: 1.5rem;\n border-radius: 0.35rem;\n background: transparent;\n color: hsl(var(--ra-muted-text));\n opacity: 0;\n transition:\n opacity .15s ease,\n background .15s ease,\n color .15s ease;\n border: 1px solid transparent;\n}\n.ra-shell .ra-row:hover .ra-row-menu-trigger,\n.ra-shell .ra-card-hover:hover .ra-row-menu-trigger,\n.ra-shell .ra-row-menu-trigger:focus-visible,\n.ra-shell .ra-row-menu-trigger[aria-expanded=true] {\n opacity: 1;\n}\n.ra-shell .ra-row-menu-trigger:hover {\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-text));\n}\n.ra-shell .ra-row-menu {\n position: absolute;\n right: 0;\n top: calc(100% + 4px);\n z-index: 50;\n min-width: 11rem;\n padding: 0.25rem;\n border-radius: 0.5rem;\n background: hsl(var(--ra-surface));\n border: 1px solid hsl(var(--ra-border));\n box-shadow: 0 12px 28px -10px hsl(0 0% 0% / 0.25);\n display: flex;\n flex-direction: column;\n gap: 0.125rem;\n}\n.ra-shell .ra-row-menu-item {\n display: inline-flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.4rem 0.55rem;\n border-radius: 0.35rem;\n font-size: 0.75rem;\n color: hsl(var(--ra-text));\n background: transparent;\n border: 0;\n text-align: left;\n width: 100%;\n cursor: pointer;\n}\n.ra-shell .ra-row-menu-item:hover:not(:disabled) {\n background: hsl(var(--ra-muted));\n}\n.ra-shell .ra-row-menu-item:disabled {\n opacity: 0.45;\n cursor: not-allowed;\n}\n.ra-shell .ra-item-list {\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 0;\n}\n.ra-shell .ra-item-list-body {\n flex: 1;\n min-height: 0;\n overflow: auto;\n padding: 1rem 1.25rem 1.5rem;\n}\n.ra-shell .ra-item-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 0.75rem;\n padding: 0.75rem 1.25rem;\n border-bottom: 1px solid hsl(var(--ra-border));\n background: hsl(var(--ra-surface));\n}\n.ra-shell .ra-item-toolbar-title {\n display: flex;\n align-items: baseline;\n gap: 0.5rem;\n min-width: 0;\n}\n.ra-shell .ra-item-toolbar-count {\n font-size: 0.7rem;\n font-weight: 600;\n color: hsl(var(--ra-muted-text));\n background: hsl(var(--ra-muted));\n border: 1px solid hsl(var(--ra-border));\n border-radius: 999px;\n padding: 0.05rem 0.45rem;\n}\n.ra-shell .ra-item-toolbar-actions {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n flex-shrink: 0;\n}\n.ra-shell .ra-item-table-wrap {\n border: 1px solid hsl(var(--ra-border));\n border-radius: var(--ra-radius);\n background: hsl(var(--ra-surface));\n overflow: hidden;\n box-shadow: var(--ra-card-shadow);\n}\n.ra-shell .ra-item-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 0.85rem;\n color: hsl(var(--ra-text));\n}\n.ra-shell .ra-item-table thead th {\n text-align: left;\n font-size: 0.7rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: hsl(var(--ra-muted-text));\n padding: 0.65rem 0.85rem;\n background: hsl(var(--ra-muted) / 0.55);\n border-bottom: 1px solid hsl(var(--ra-border));\n}\n.ra-shell .ra-item-table tbody td {\n padding: 0.65rem 0.85rem;\n border-bottom: 1px solid hsl(var(--ra-border) / 0.7);\n vertical-align: middle;\n}\n.ra-shell .ra-item-table tbody tr:last-child td {\n border-bottom: 0;\n}\n.ra-shell .ra-item-row {\n cursor: pointer;\n transition: background .12s ease;\n}\n.ra-shell .ra-item-row:hover {\n background: var(--ra-row-hover);\n}\n.ra-shell .ra-item-row[data-selected=true] {\n background: var(--ra-row-active-bg);\n}\n.ra-shell .ra-item-row-title {\n font-weight: var(--ra-title-weight);\n color: hsl(var(--ra-text));\n}\n.ra-shell .ra-item-row-sub {\n font-size: 0.75rem;\n color: hsl(var(--ra-muted-text));\n margin-top: 0.15rem;\n}\n.ra-shell .ra-item-row-meta {\n font-size: 0.78rem;\n color: hsl(var(--ra-muted-text));\n}\n.ra-shell .ra-item-row-actions {\n text-align: right;\n white-space: nowrap;\n}\n.ra-shell .ra-item-row-actions .ra-row-action + .ra-row-action {\n margin-left: 0.15rem;\n}\n.ra-shell .ra-item-cards {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(var(--ra-item-card-min, 240px), 1fr));\n gap: 0.85rem;\n}\n.ra-shell .ra-item-gallery {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(var(--ra-item-gallery-min, 320px), 1fr));\n gap: 1rem;\n}\n.ra-shell .ra-item-cards[data-card-size=sm] {\n --ra-item-card-min: 180px;\n}\n.ra-shell .ra-item-cards[data-card-size=md] {\n --ra-item-card-min: 240px;\n}\n.ra-shell .ra-item-cards[data-card-size=lg] {\n --ra-item-card-min: 320px;\n gap: 1rem;\n}\n.ra-shell .ra-item-gallery[data-card-size=sm] {\n --ra-item-gallery-min: 240px;\n}\n.ra-shell .ra-item-gallery[data-card-size=md] {\n --ra-item-gallery-min: 320px;\n}\n.ra-shell .ra-item-gallery[data-card-size=lg] {\n --ra-item-gallery-min: 420px;\n gap: 1.25rem;\n}\n.ra-shell .ra-item-cards[data-card-size=lg] .ra-item-card-title,\n.ra-shell .ra-item-gallery[data-card-size=lg] .ra-item-card-title {\n font-size: 0.95rem;\n white-space: normal;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n}\n.ra-shell .ra-item-cards[data-card-size=lg] .ra-item-card-body,\n.ra-shell .ra-item-gallery[data-card-size=lg] .ra-item-card-body {\n padding: 0.85rem 1rem 1rem;\n}\n.ra-shell .ra-item-card {\n position: relative;\n display: flex;\n flex-direction: column;\n align-items: stretch;\n text-align: left;\n padding: 0;\n background: hsl(var(--ra-surface));\n border: 1px solid hsl(var(--ra-border));\n border-radius: var(--ra-radius);\n overflow: hidden;\n cursor: pointer;\n transition:\n box-shadow .18s ease,\n transform .12s ease,\n border-color .15s ease;\n box-shadow: var(--ra-card-shadow);\n font-family: inherit;\n color: inherit;\n}\n.ra-shell .ra-item-card:hover {\n box-shadow: var(--ra-card-shadow-hover);\n border-color: hsl(var(--ra-accent) / 0.30);\n}\n.ra-shell .ra-item-card[data-selected=true] {\n border-color: hsl(var(--ra-accent) / 0.55);\n box-shadow: var(--ra-card-shadow-hover);\n}\n.ra-shell .ra-item-card-thumb {\n width: 100%;\n aspect-ratio: 1 / 1;\n background:\n linear-gradient(\n 135deg,\n hsl(var(--ra-accent) / 0.12),\n hsl(var(--ra-accent) / 0.04));\n display: flex;\n align-items: center;\n justify-content: center;\n color: hsl(var(--ra-accent));\n overflow: hidden;\n}\n.ra-shell .ra-item-card-thumb--gallery {\n aspect-ratio: 16 / 9;\n}\n.ra-shell .ra-item-card-thumb img {\n width: 100%;\n height: 100%;\n -o-object-fit: cover;\n object-fit: cover;\n}\n.ra-shell .ra-item-card-initials {\n font-family: var(--ra-font-display);\n font-weight: var(--ra-display-weight);\n font-size: 1.5rem;\n letter-spacing: 0.02em;\n}\n.ra-shell .ra-item-card-body {\n padding: 0.65rem 0.8rem 0.85rem;\n min-width: 0;\n}\n.ra-shell .ra-item-card-title {\n font-weight: var(--ra-title-weight);\n font-size: 0.85rem;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ra-shell .ra-item-card-sub {\n font-size: 0.75rem;\n color: hsl(var(--ra-muted-text));\n margin-top: 0.15rem;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ra-shell .ra-item-card-delete {\n position: absolute;\n top: 0.4rem;\n right: 0.4rem;\n background: hsl(var(--ra-surface) / 0.85);\n backdrop-filter: blur(4px);\n opacity: 0;\n transition: opacity .15s ease;\n}\n.ra-shell .ra-item-card:hover .ra-item-card-delete,\n.ra-shell .ra-item-card:focus-within .ra-item-card-delete {\n opacity: 1;\n}\n.ra-shell .ra-item-nav {\n display: flex;\n align-items: center;\n gap: 0.6rem;\n padding: 0.5rem 1.25rem;\n border-bottom: 1px solid hsl(var(--ra-border));\n background: hsl(var(--ra-surface));\n}\n.ra-shell .ra-item-nav-position {\n font-size: 0.72rem;\n color: hsl(var(--ra-muted-text));\n font-variant-numeric: tabular-nums;\n}\n.ra-shell .ra-item-nav-arrows {\n margin-left: auto;\n display: inline-flex;\n align-items: center;\n gap: 0.15rem;\n}\n.ra-shell .ra-item-nav-arrows .ra-row-action[disabled] {\n opacity: 0.35;\n cursor: not-allowed;\n}\n.ra-shell .ra-sibling-rail {\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 0;\n}\n.ra-shell .ra-sibling-back {\n display: inline-flex;\n align-items: center;\n gap: 0.4rem;\n padding: 0.6rem 0.85rem;\n font-size: 0.75rem;\n font-weight: 500;\n color: hsl(var(--ra-muted-text));\n background: hsl(var(--ra-muted) / 0.5);\n border: 0;\n border-bottom: 1px solid hsl(var(--ra-border));\n cursor: pointer;\n text-align: left;\n transition: background .12s ease, color .12s ease;\n}\n.ra-shell .ra-sibling-back:hover {\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-text));\n}\n.ra-shell .ra-sibling-heading {\n padding: 0.6rem 0.85rem 0.4rem;\n font-size: 0.65rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: hsl(var(--ra-muted-text));\n}\n.ra-shell .ra-sibling-body {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n}\n.ra-shell .ra-sibling-list {\n list-style: none;\n margin: 0;\n padding: 0;\n}\n.ra-shell .ra-status-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n border-radius: 9999px;\n}\n.ra-shell .ra-status-icon > svg {\n width: 100%;\n height: 100%;\n display: block;\n}\n.ra-shell .ra-status-icon--own {\n color: hsl(var(--ra-status-own));\n}\n.ra-shell .ra-status-icon--shared {\n color: hsl(var(--ra-status-shared));\n}\n.ra-shell .ra-status-icon--missing {\n color: hsl(var(--ra-status-missing) / 0.7);\n}\n.ra-shell .ra-row-status {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.5rem;\n height: 1.5rem;\n flex-shrink: 0;\n}\n.ra-shell .ra-row-scope {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.25rem;\n height: 1.25rem;\n border-radius: calc(var(--ra-radius) * 0.5);\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-muted-text));\n flex-shrink: 0;\n margin-left: auto;\n opacity: 0.55;\n transition:\n opacity .12s ease,\n color .12s ease,\n background .12s ease;\n}\n.ra-shell .ra-row:hover .ra-row-scope {\n opacity: 0.85;\n}\n.ra-shell .ra-row[data-selected=true] .ra-row-scope {\n opacity: 1;\n background: hsl(var(--ra-accent) / 0.12);\n color: hsl(var(--ra-accent));\n}\n.ra-shell .ra-row[data-tone=own] .ra-row-sub {\n color: hsl(var(--ra-status-own));\n}\n.ra-shell .ra-row[data-tone=shared] .ra-row-sub {\n color: hsl(var(--ra-status-shared));\n}\n.ra-shell .ra-row[data-selected=true] {\n background:\n linear-gradient(\n 90deg,\n hsl(var(--ra-accent) / 0.10) 0%,\n hsl(var(--ra-accent) / 0.04) 100%);\n border-left-width: 3px;\n border-left-color: hsl(var(--ra-accent));\n}\n.ra-shell .ra-dirty-pip {\n display: inline-block;\n width: 0.45rem;\n height: 0.45rem;\n border-radius: 9999px;\n background: hsl(var(--ra-warning));\n box-shadow: 0 0 0 2px hsl(var(--ra-warning) / 0.18);\n flex-shrink: 0;\n}\n.ra-shell .ra-error-pip {\n display: inline-block;\n width: 0.45rem;\n height: 0.45rem;\n border-radius: 9999px;\n background: hsl(var(--ra-danger, 0 72% 51%));\n box-shadow: 0 0 0 2px hsl(var(--ra-danger, 0 72% 51%) / 0.22);\n flex-shrink: 0;\n}\n.ra-shell .ra-group-summary {\n background: transparent;\n}\n.ra-shell {\n position: relative;\n}\n.ra-shell .ra-help-float {\n position: absolute;\n top: 0.65rem;\n right: 0.85rem;\n z-index: 5;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.6rem;\n height: 1.6rem;\n padding: 0;\n color: hsl(var(--ra-muted-text));\n background: hsl(var(--ra-surface) / 0.85);\n backdrop-filter: blur(6px);\n border: 1px solid hsl(var(--ra-border));\n border-radius: 999px;\n cursor: pointer;\n transition:\n color .12s ease,\n background .12s ease,\n border-color .12s ease;\n}\n.ra-shell .ra-help-float:hover {\n color: hsl(var(--ra-accent));\n border-color: hsl(var(--ra-accent) / 0.4);\n background: hsl(var(--ra-surface));\n}\n.ra-shell .ra-help-float svg {\n width: 0.95rem;\n height: 0.95rem;\n}\n.ra-shell .ra-help-float > span {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n.ra-shell .ra-preview-reopen {\n position: absolute;\n top: 50%;\n right: 0;\n transform: translateY(-50%);\n z-index: 4;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 0.4rem;\n padding: 0.65rem 0.45rem;\n background: hsl(var(--ra-surface));\n color: hsl(var(--ra-muted-text));\n border: 1px solid hsl(var(--ra-border));\n border-right: 0;\n border-radius: calc(var(--ra-radius) * 0.85) 0 0 calc(var(--ra-radius) * 0.85);\n box-shadow: var(--ra-card-shadow);\n cursor: pointer;\n transition:\n color .12s ease,\n background .12s ease,\n padding-right .15s ease;\n writing-mode: vertical-rl;\n font-size: 0.7rem;\n font-weight: 600;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n}\n.ra-shell .ra-preview-reopen:hover {\n color: hsl(var(--ra-accent));\n background: hsl(var(--ra-accent) / 0.04);\n padding-right: 0.6rem;\n}\n.ra-shell .ra-preview-reopen svg {\n width: 0.85rem;\n height: 0.85rem;\n writing-mode: horizontal-tb;\n}\n.ra-shell .ra-unsaved-tray {\n position: relative;\n display: flex;\n align-items: center;\n gap: 0.6rem;\n padding: 0.5rem 0.75rem;\n border: 1px solid hsl(var(--ra-warning, 38 92% 50%) / 0.35);\n background: hsl(var(--ra-warning, 38 92% 50%) / 0.08);\n border-radius: var(--ra-radius);\n font-size: 0.8125rem;\n color: hsl(var(--ra-text));\n animation: ra-unsaved-slide .14s ease-out;\n}\n.ra-shell .ra-unsaved-count {\n flex: 1;\n min-width: 0;\n display: inline-flex;\n align-items: center;\n gap: 0.3rem;\n padding: 0.15rem 0.4rem;\n margin: -0.15rem -0.4rem;\n background: transparent;\n border: 0;\n color: inherit;\n font: inherit;\n font-weight: 500;\n text-align: left;\n cursor: pointer;\n border-radius: calc(var(--ra-radius) - 4px);\n}\n.ra-shell .ra-unsaved-count:hover {\n background: hsl(var(--ra-muted) / 0.6);\n}\n.ra-shell .ra-unsaved-error-chip {\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n border: 1px solid hsl(var(--ra-danger, 0 72% 51%) / 0.35);\n background: hsl(var(--ra-danger, 0 72% 51%) / 0.08);\n color: hsl(var(--ra-danger, 0 72% 51%));\n border-radius: 999px;\n padding: 0.15rem 0.55rem;\n font-size: 0.7rem;\n font-weight: 500;\n cursor: pointer;\n}\n.ra-shell .ra-unsaved-error-chip:hover {\n filter: brightness(0.97);\n}\n.ra-shell .ra-unsaved-popover {\n position: absolute;\n top: calc(100% + 6px);\n left: 0;\n z-index: 60;\n min-width: 18rem;\n max-height: 18rem;\n overflow: auto;\n background: hsl(var(--ra-surface));\n border: 1px solid hsl(var(--ra-border));\n border-radius: var(--ra-radius);\n box-shadow: 0 12px 30px -10px hsl(0 0% 0% / 0.25);\n padding: 0.3rem;\n display: flex;\n flex-direction: column;\n gap: 0.15rem;\n}\n.ra-shell .ra-unsaved-popover-row {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.4rem 0.55rem;\n background: transparent;\n border: 0;\n border-radius: calc(var(--ra-radius) - 2px);\n cursor: pointer;\n text-align: left;\n font: inherit;\n color: hsl(var(--ra-text));\n}\n.ra-shell .ra-unsaved-popover-row:hover {\n background: hsl(var(--ra-muted));\n}\n.ra-shell .ra-unsaved-popover-dot {\n width: 0.5rem;\n height: 0.5rem;\n border-radius: 999px;\n flex-shrink: 0;\n}\n.ra-shell .ra-unsaved-popover-label {\n flex: 1;\n min-width: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n font-size: 0.8125rem;\n}\n.ra-shell .ra-unsaved-popover-ctx {\n color: hsl(var(--ra-muted-text));\n font-size: 0.7rem;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n}\n.ra-shell .ra-unsaved-popover-err {\n color: hsl(var(--ra-danger, 0 72% 51%));\n font-size: 0.7rem;\n font-weight: 500;\n}\n.ra-saveall-overlay {\n position: fixed;\n inset: 0;\n z-index: 100;\n background: hsl(0 0% 0% / 0.45);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 1rem;\n animation: ra-confirm-fade .12s ease-out;\n}\n.ra-saveall-card {\n background: hsl(var(--ra-surface));\n color: hsl(var(--ra-text));\n border: 1px solid hsl(var(--ra-border));\n border-radius: var(--ra-radius);\n box-shadow: 0 24px 48px -16px hsl(0 0% 0% / 0.45);\n width: min(28rem, 100%);\n max-height: min(80vh, 36rem);\n display: flex;\n flex-direction: column;\n animation: ra-confirm-pop .14s ease-out;\n}\n.ra-saveall-header {\n padding: 1rem 1rem 0.5rem;\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n.ra-saveall-title {\n font-weight: 600;\n font-size: 0.95rem;\n}\n.ra-saveall-progress {\n height: 4px;\n background: hsl(var(--ra-muted));\n border-radius: 999px;\n overflow: hidden;\n}\n.ra-saveall-progress-bar {\n height: 100%;\n background: hsl(var(--ra-accent));\n transition: width .2s ease;\n}\n.ra-saveall-counter {\n color: hsl(var(--ra-muted-text));\n font-size: 0.75rem;\n font-variant-numeric: tabular-nums;\n}\n.ra-saveall-list {\n list-style: none;\n margin: 0;\n padding: 0.25rem 0.5rem;\n overflow: auto;\n flex: 1;\n}\n.ra-saveall-row {\n display: flex;\n align-items: center;\n gap: 0.6rem;\n padding: 0.45rem 0.5rem;\n border-radius: calc(var(--ra-radius) - 4px);\n font-size: 0.8125rem;\n}\n.ra-saveall-row[data-status=saving] {\n background: hsl(var(--ra-accent) / 0.06);\n}\n.ra-saveall-row[data-status=saved] {\n color: hsl(var(--ra-muted-text));\n}\n.ra-saveall-row[data-status=error] {\n background: hsl(var(--ra-danger, 0 72% 51%) / 0.06);\n}\n.ra-saveall-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n color: hsl(var(--ra-muted-text));\n}\n.ra-saveall-row[data-status=saved] .ra-saveall-icon {\n color: hsl(var(--ra-success, 142 71% 45%));\n}\n.ra-saveall-row[data-status=saving] .ra-saveall-icon {\n color: hsl(var(--ra-accent));\n}\n.ra-saveall-row[data-status=error] .ra-saveall-icon {\n color: hsl(var(--ra-danger, 0 72% 51%));\n}\n.ra-saveall-label {\n flex: 1;\n min-width: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ra-saveall-err {\n color: hsl(var(--ra-danger, 0 72% 51%));\n font-size: 0.7rem;\n max-width: 12rem;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ra-saveall-actions {\n padding: 0.75rem 1rem 1rem;\n display: flex;\n justify-content: flex-end;\n gap: 0.4rem;\n border-top: 1px solid hsl(var(--ra-border));\n}\n.ra-spin {\n animation: ra-spin 1s linear infinite;\n}\n@keyframes ra-spin {\n to {\n transform: rotate(360deg);\n }\n}\n");
6072
6853
  var EditorPoolBody = ({
6073
6854
  editorId,
6074
6855
  renderEditor
@@ -6078,6 +6859,7 @@ var EditorPoolBody = ({
6078
6859
  return renderEditor(ctx);
6079
6860
  };
6080
6861
  var TOP_LEVEL_SCOPES = ["collection", "rule", "product"];
6862
+ var OPT_IN_TOP_LEVEL_SCOPES = ["all"];
6081
6863
  var WARNED_FACET_DEPRECATED = false;
6082
6864
  var DRAFT_ID3 = "__draft__";
6083
6865
  var isDraftId3 = (id) => !!id && (id === DRAFT_ID3 || id.startsWith("draft:"));
@@ -6116,6 +6898,19 @@ function RecordsAdminShell(props) {
6116
6898
  {
6117
6899
  ctx,
6118
6900
  defaultValueFactory: props.defaultData,
6901
+ hooks: props.hooks,
6902
+ onHookNotice: (notice) => {
6903
+ console.warn(`[RecordsAdmin] ${notice.kind} hook failed`, notice.error);
6904
+ try {
6905
+ props.onTelemetry?.({
6906
+ type: "record.hook.error",
6907
+ recordType: props.recordType,
6908
+ kind: notice.kind,
6909
+ message: notice.error instanceof Error ? notice.error.message : String(notice.error)
6910
+ });
6911
+ } catch {
6912
+ }
6913
+ },
6119
6914
  children: /* @__PURE__ */ jsx(RecordsAdminShellInner, { ...props })
6120
6915
  }
6121
6916
  );
@@ -6134,7 +6929,7 @@ function RecordsAdminShellInner(props) {
6134
6929
  renderEditor,
6135
6930
  intro,
6136
6931
  csvSchema,
6137
- classify: classify2,
6932
+ classify: classify3,
6138
6933
  defaultData,
6139
6934
  i18n: i18nOverride,
6140
6935
  onTelemetry,
@@ -6256,13 +7051,15 @@ function RecordsAdminShellInner(props) {
6256
7051
  );
6257
7052
  const queryClient = useQueryClient();
6258
7053
  const probe = useScopeProbe({ SL, collectionId });
7054
+ const scopeCounts = useScopeCounts({ ctx });
6259
7055
  const topLevelScopes = useMemo(() => {
6260
7056
  const requested = requestedScopes ?? [];
7057
+ const allowed = /* @__PURE__ */ new Set([...TOP_LEVEL_SCOPES, ...OPT_IN_TOP_LEVEL_SCOPES]);
6261
7058
  if (requested.length > 0) {
6262
7059
  const seen = /* @__PURE__ */ new Set();
6263
7060
  const ordered = [];
6264
7061
  for (const s of requested) {
6265
- if (!TOP_LEVEL_SCOPES.includes(s)) continue;
7062
+ if (!allowed.has(s)) continue;
6266
7063
  if (seen.has(s)) continue;
6267
7064
  seen.add(s);
6268
7065
  ordered.push(s);
@@ -6274,7 +7071,7 @@ function RecordsAdminShellInner(props) {
6274
7071
  const productPinnedFromContext = !!contextScope?.productId;
6275
7072
  const effectiveTopLevelScopes = useMemo(() => {
6276
7073
  if (!productPinnedFromContext || contextScopeMode !== "strict") return topLevelScopes;
6277
- return topLevelScopes.filter((s) => s !== "collection" && s !== "rule");
7074
+ return topLevelScopes.filter((s) => s !== "collection" && s !== "rule" && s !== "all");
6278
7075
  }, [productPinnedFromContext, contextScopeMode, topLevelScopes]);
6279
7076
  useEffect(() => {
6280
7077
  if (requestedScopes.includes("facet") && !WARNED_FACET_DEPRECATED) {
@@ -6340,7 +7137,7 @@ function RecordsAdminShellInner(props) {
6340
7137
  probeIsLoading: probe.isLoading,
6341
7138
  selectedProductId,
6342
7139
  drillTab,
6343
- classify: classify2
7140
+ classify: classify3
6344
7141
  });
6345
7142
  const {
6346
7143
  search,
@@ -6374,13 +7171,16 @@ function RecordsAdminShellInner(props) {
6374
7171
  if (first) setSelectedProductId(first.id);
6375
7172
  }, [activeScope, selectedProductId, productBrowse.items]);
6376
7173
  useEffect(() => {
6377
- if (activeScope !== "rule" && activeScope !== "collection") return;
7174
+ if (activeScope !== "rule" && activeScope !== "collection" && activeScope !== "all") return;
6378
7175
  if (selectedRecordId !== null) return;
6379
7176
  if (ruleWizardStep !== null) return;
6380
7177
  if (draftKind !== null) return;
6381
7178
  if (activeScope === "collection" && cardinality === "collection") {
6382
7179
  return;
6383
7180
  }
7181
+ if (activeScope === "all" && cardinality === "collection") {
7182
+ return;
7183
+ }
6384
7184
  const first = recordList.items[0];
6385
7185
  if (first?.id) setSelectedRecordId(first.id);
6386
7186
  }, [activeScope, selectedRecordId, recordList.items, cardinality, ruleWizardStep, draftKind]);
@@ -6407,6 +7207,10 @@ function RecordsAdminShellInner(props) {
6407
7207
  // facetRule from the selected record's summary so `useCollectionItems`
6408
7208
  // can match by rule equality instead of by anchors.
6409
7209
  facetRule: editingScope?.kind === "rule" ? recordList.items.find((it) => it.id === selectedRecordId)?.facetRule ?? null : null,
7210
+ // 'all' tab + collection cardinality: pull every item-cardinality
7211
+ // record across the whole collection (anchored, ruled, global) so
7212
+ // host-supplied lifecycle grouping has the full picture.
7213
+ includeAll: isCollection && activeScope === "all",
6410
7214
  enabled: isCollection
6411
7215
  });
6412
7216
  useEffect(() => {
@@ -6498,6 +7302,10 @@ function RecordsAdminShellInner(props) {
6498
7302
  setRuleWizardRule(null);
6499
7303
  setDraftKind(null);
6500
7304
  }
7305
+ if (isCreate && selectedRecordId === DRAFT_ID3) {
7306
+ setSelectedRecordId(null);
7307
+ setDraftKind(null);
7308
+ }
6501
7309
  refetchAll();
6502
7310
  },
6503
7311
  onDeleted: () => {
@@ -6663,7 +7471,8 @@ function RecordsAdminShellInner(props) {
6663
7471
  deepLinkState,
6664
7472
  onTelemetry,
6665
7473
  onBeforeDelete,
6666
- generateItemId
7474
+ generateItemId,
7475
+ hooks: props.hooks
6667
7476
  });
6668
7477
  const renderEditorWithPreview = () => {
6669
7478
  if (!editingTargetScope) return null;
@@ -6703,13 +7512,33 @@ function RecordsAdminShellInner(props) {
6703
7512
  bulkActions: { ...csvBulk, i18n },
6704
7513
  preview: inlinePreviewBody,
6705
7514
  footerExtra: extraFooter,
6706
- targeting: editingTargetScope.kind === "rule" ? /* @__PURE__ */ jsx(
6707
- RecordTargeting,
7515
+ targeting: (
7516
+ // Mount the Targeting section for:
7517
+ // • Native rule scopes (existing behaviour — author the rule).
7518
+ // • Non-pinned Global (collection-root) scopes — admins can grow
7519
+ // a rule on the fly to convert Global → Rule, or stay global.
7520
+ // Pinned scopes (product / variant / batch / proof) are anchored
7521
+ // by ID, not by rule, so the section would be a no-op there.
7522
+ editingTargetScope.kind === "rule" || editingTargetScope.kind === "collection" && !editingTargetScope.productId && !editingTargetScope.variantId && !editingTargetScope.batchId && !editingTargetScope.proofId ? /* @__PURE__ */ jsx(
7523
+ RecordTargeting,
7524
+ {
7525
+ SL,
7526
+ collectionId,
7527
+ appId,
7528
+ ctx: editorCtx,
7529
+ forceOpen: targetingExpandNonce > 0,
7530
+ onOpenChange: (o) => {
7531
+ if (!o) setTargetingExpandNonce(0);
7532
+ }
7533
+ }
7534
+ ) : void 0
7535
+ ),
7536
+ targetingControl: editingTargetScope.kind === "rule" || editingTargetScope.kind === "collection" && !editingTargetScope.productId && !editingTargetScope.variantId && !editingTargetScope.batchId && !editingTargetScope.proofId ? /* @__PURE__ */ jsx(
7537
+ TargetingPopover,
6708
7538
  {
6709
- SL,
6710
- collectionId,
6711
- appId,
6712
- ctx: editorCtx
7539
+ ctx: editorCtx,
7540
+ catalogue: ruleCatalogue,
7541
+ onCustomise: () => setTargetingExpandNonce((n) => n + 1)
6713
7542
  }
6714
7543
  ) : void 0,
6715
7544
  onBeforeDelete: onBeforeDelete && editingTargetScope ? () => onBeforeDelete(editingTargetScope) : void 0,
@@ -6827,7 +7656,6 @@ function RecordsAdminShellInner(props) {
6827
7656
  ]);
6828
7657
  const effectivePresentation = isProductTab ? presentation : "list";
6829
7658
  const showPresentationSwitcher = isProductTab && presentations.length > 1;
6830
- const effectiveGroupBy = groupBy;
6831
7659
  const productListItems = useMemo(() => {
6832
7660
  if (productPinned) {
6833
7661
  const pid = contextScope.productId;
@@ -6845,7 +7673,66 @@ function RecordsAdminShellInner(props) {
6845
7673
  }, [productPinned, contextScope, productBrowse.items, pinnedProduct.item]);
6846
7674
  const isRuleTab = activeScope === "rule";
6847
7675
  const isGlobalTab = activeScope === "collection";
6848
- const isRecordsTab = isRuleTab || isGlobalTab;
7676
+ const isAllTab = activeScope === "all";
7677
+ const isRecordsTab = isRuleTab || isGlobalTab || isAllTab;
7678
+ const effectiveGroupBy = useMemo(() => {
7679
+ if (groupBy) return groupBy;
7680
+ if (isAllTab) return void 0;
7681
+ if (!isRuleTab) return void 0;
7682
+ if (isCollection) return void 0;
7683
+ return (record) => {
7684
+ const hash = ruleHash(record.facetRule);
7685
+ if (!hash) return null;
7686
+ return { key: `rule:${hash}`, label: summariseRule(record.facetRule) };
7687
+ };
7688
+ }, [groupBy, isRuleTab, isAllTab, isCollection]);
7689
+ const [bulkRuleEditTarget, setBulkRuleEditTarget] = useState(null);
7690
+ const ruleCatalogue = useMemo(() => {
7691
+ const buckets = /* @__PURE__ */ new Map();
7692
+ for (const item of recordList.items) {
7693
+ const hash = ruleHash(item.facetRule);
7694
+ if (!hash || !item.facetRule) continue;
7695
+ const existing = buckets.get(hash);
7696
+ if (existing) {
7697
+ existing.count += 1;
7698
+ } else {
7699
+ buckets.set(hash, {
7700
+ hash,
7701
+ rule: item.facetRule,
7702
+ summary: summariseRule(item.facetRule),
7703
+ count: 1
7704
+ });
7705
+ }
7706
+ }
7707
+ return Array.from(buckets.values()).sort((a, b) => {
7708
+ if (b.count !== a.count) return b.count - a.count;
7709
+ return a.summary.localeCompare(b.summary);
7710
+ });
7711
+ }, [recordList.items]);
7712
+ const [targetingExpandNonce, setTargetingExpandNonce] = useState(0);
7713
+ const renderRuleGroupActions = useCallback(
7714
+ (group) => {
7715
+ if (!isRuleTab || groupBy) return null;
7716
+ const ids = group.items.map((it) => it.id).filter((id) => !!id);
7717
+ if (ids.length < 2) return null;
7718
+ const rule = group.items.find((it) => it.facetRule)?.facetRule ?? null;
7719
+ return /* @__PURE__ */ jsx(
7720
+ "button",
7721
+ {
7722
+ type: "button",
7723
+ className: "text-[11px] px-2 py-0.5 rounded border hover:bg-[hsl(var(--ra-muted))] transition-colors",
7724
+ style: {
7725
+ borderColor: "hsl(var(--ra-border))",
7726
+ color: "hsl(var(--ra-muted-text))"
7727
+ },
7728
+ title: `Edit this rule and apply the change to all ${ids.length} records that share it`,
7729
+ onClick: () => setBulkRuleEditTarget({ rule, recordIds: ids }),
7730
+ children: "Edit rule"
7731
+ }
7732
+ );
7733
+ },
7734
+ [isRuleTab, groupBy]
7735
+ );
6849
7736
  const onCreateRule = useCallback(() => {
6850
7737
  void runWithGuard(() => {
6851
7738
  if (activeScope !== "rule") setActiveScope("rule");
@@ -6897,6 +7784,32 @@ function RecordsAdminShellInner(props) {
6897
7784
  () => isRuleTab ? applyRuleFilters(recordList.items, ruleFilters) : recordList.items,
6898
7785
  [isRuleTab, recordList.items, ruleFilters]
6899
7786
  );
7787
+ const collectionRuleRailItems = useMemo(() => {
7788
+ if (!isRuleTab || !isCollection) return filteredRuleItems;
7789
+ const buckets = /* @__PURE__ */ new Map();
7790
+ for (const item of filteredRuleItems) {
7791
+ const hash = ruleHash(item.facetRule);
7792
+ if (!hash) continue;
7793
+ const existing = buckets.get(hash);
7794
+ if (existing) {
7795
+ existing.count += 1;
7796
+ } else {
7797
+ buckets.set(hash, { rep: item, count: 1 });
7798
+ }
7799
+ }
7800
+ return Array.from(buckets.values()).map(({ rep, count }) => ({
7801
+ ...rep,
7802
+ label: summariseRule(rep.facetRule),
7803
+ subtitle: `${count} ${itemNoun}${count === 1 ? "" : "s"}`
7804
+ }));
7805
+ }, [isRuleTab, isCollection, filteredRuleItems, itemNoun]);
7806
+ const activeRuleSummary = useMemo(() => {
7807
+ if (!isRuleTab || !isCollection) return null;
7808
+ if (!selectedRecordId || isDraftId3(selectedRecordId)) return null;
7809
+ const hit = recordList.items.find((it) => it.id === selectedRecordId);
7810
+ if (!hit?.facetRule) return null;
7811
+ return summariseRule(hit.facetRule);
7812
+ }, [isRuleTab, isCollection, selectedRecordId, recordList.items]);
6900
7813
  const applyFacetBrowseFilter = useCallback(
6901
7814
  (items2) => {
6902
7815
  if (!facetBrowseFilter) return items2;
@@ -6944,7 +7857,9 @@ function RecordsAdminShellInner(props) {
6944
7857
  }),
6945
7858
  [i18n.itemsAllLabel, collectionItems.items.length, itemNoun]
6946
7859
  );
6947
- const leftItems = isProductTab ? productListItems : isRuleTab ? applyFacetBrowseFilter(filteredRuleItems) : isGlobalTab && isCollection ? [collectionGlobalAllRow] : isRecordsTab ? applyFacetBrowseFilter(recordList.items) : [];
7860
+ const leftItems = isProductTab ? productListItems : isRuleTab ? applyFacetBrowseFilter(
7861
+ isCollection ? collectionRuleRailItems : filteredRuleItems
7862
+ ) : (isGlobalTab || isAllTab) && isCollection ? [collectionGlobalAllRow] : isRecordsTab ? applyFacetBrowseFilter(recordList.items) : [];
6948
7863
  const leftLoading = isProductTab ? !productPinned && productBrowse.isLoading : isRecordsTab ? recordList.isLoading || probe.isLoading : false;
6949
7864
  const leftError = isProductTab ? productBrowse.error : isRecordsTab ? recordList.error : null;
6950
7865
  const leftSelectedId = isProductTab ? void 0 : selectedRecordId && selectedRecordId !== DRAFT_ID3 ? selectedRecordId : void 0;
@@ -7157,8 +8072,22 @@ function RecordsAdminShellInner(props) {
7157
8072
  },
7158
8073
  loading: probe.isLoading,
7159
8074
  counts: {
7160
- product: productBrowse.items.length
8075
+ // `product` continues to show the catalogue size (browse
8076
+ // affordance), not the per-product record count — that's
8077
+ // the long-standing semantics for the Products tab.
8078
+ product: productBrowse.items.length,
8079
+ // The remaining tabs show actual record counts so hidden
8080
+ // state (e.g. a rule-scoped competition) is visible from
8081
+ // any tab. `useScopeCounts` returns 0 while loading, which
8082
+ // is fine — the badges just appear once data lands.
8083
+ collection: scopeCounts.counts.collection,
8084
+ rule: scopeCounts.counts.rule,
8085
+ variant: scopeCounts.counts.variant,
8086
+ batch: scopeCounts.counts.batch,
8087
+ facet: scopeCounts.counts.facet,
8088
+ all: scopeCounts.counts.all
7161
8089
  },
8090
+ tooltips: { rule: i18n.rulesTabTooltip },
7162
8091
  icons: icons.scope
7163
8092
  }
7164
8093
  ) }),
@@ -7254,7 +8183,7 @@ function RecordsAdminShellInner(props) {
7254
8183
  {
7255
8184
  icon: search ? icons.empty.search : icons.empty.default,
7256
8185
  title: search ? i18n.noResults : i18n.railEmptyTitle,
7257
- body: search ? void 0 : i18n.railEmptyBody
8186
+ body: search ? void 0 : isRuleTab ? i18n.rulesEmptyBody : i18n.railEmptyBody
7258
8187
  }
7259
8188
  )),
7260
8189
  !leftLoading && !leftError && leftItems.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -7272,6 +8201,7 @@ function RecordsAdminShellInner(props) {
7272
8201
  presentation: effectivePresentation,
7273
8202
  renderListRow,
7274
8203
  groupBy: effectiveGroupBy,
8204
+ renderGroupActions: renderRuleGroupActions,
7275
8205
  rowClipboard,
7276
8206
  i18n
7277
8207
  }
@@ -7370,6 +8300,7 @@ function RecordsAdminShellInner(props) {
7370
8300
  renderItemEmpty,
7371
8301
  itemColumns,
7372
8302
  cardSize: itemCardSize,
8303
+ ruleSummary: activeRuleSummary,
7373
8304
  i18n
7374
8305
  }
7375
8306
  ),
@@ -7415,6 +8346,22 @@ function RecordsAdminShellInner(props) {
7415
8346
  ] })
7416
8347
  ]
7417
8348
  }
8349
+ ),
8350
+ /* @__PURE__ */ jsx(
8351
+ RuleGroupEditDialog,
8352
+ {
8353
+ open: !!bulkRuleEditTarget,
8354
+ ctx,
8355
+ currentRule: bulkRuleEditTarget?.rule ?? null,
8356
+ recordIds: bulkRuleEditTarget?.recordIds ?? [],
8357
+ onClose: () => setBulkRuleEditTarget(null),
8358
+ onApplied: () => {
8359
+ recordList.refetch();
8360
+ queryClient.invalidateQueries({
8361
+ queryKey: scopeCountsQueryKey(ctx.collectionId, ctx.appId, ctx.recordType)
8362
+ });
8363
+ }
8364
+ }
7418
8365
  )
7419
8366
  ]
7420
8367
  }
@@ -7839,8 +8786,8 @@ function useRecordEditor(args) {
7839
8786
  const anchors = parsedRefToScope(scope);
7840
8787
  const hasAnchors = !!(anchors.productId || anchors.variantId || anchors.batchId || anchors.proofId);
7841
8788
  const hasRule = !!(facetRule && facetRule.all && facetRule.all.length > 0);
7842
- const isRuleScope2 = scope.kind === "rule";
7843
- if (isRuleScope2 && !hasAnchors && !hasRule && !resolved.recordId && !createMode) {
8789
+ const isRuleScope = scope.kind === "rule";
8790
+ if (isRuleScope && !hasAnchors && !hasRule && !resolved.recordId && !createMode) {
7844
8791
  console.warn("[useRecordEditor] save() bailed \u2014 rule scope with no clauses, no recordId, not in createMode");
7845
8792
  return;
7846
8793
  }
@@ -7881,23 +8828,28 @@ function useRecordEditor(args) {
7881
8828
  });
7882
8829
  } else if (createMode) {
7883
8830
  await createRecord(ctx, {
7884
- ref: scope.kind === "rule" && scope.raw ? scope.raw : void 0,
8831
+ // The framework does NOT write to `record.ref` that's a
8832
+ // host-owned field. Rule identity flows through `facetRule`
8833
+ // (content-addressed via `ruleHash`); record identity is the
8834
+ // UUID assigned by the server.
8835
+ ref: void 0,
7885
8836
  scope: anchors,
7886
8837
  data: value,
7887
8838
  facetRule
7888
8839
  });
7889
8840
  } else {
7890
8841
  await upsertRecord(ctx, {
7891
- // External ref only when the underlying scope already carries
7892
- // one (e.g. rule:{id} for an existing rule record). Empty for
7893
- // anchor-only writes — server addresses by anchors.
7894
- ref: scope.kind === "rule" && scope.raw ? scope.raw : void 0,
8842
+ // See note above never write to `ref`.
8843
+ ref: void 0,
7895
8844
  scope: anchors,
7896
8845
  data: value,
7897
8846
  facetRule
7898
8847
  });
7899
8848
  }
7900
8849
  draftStore.clearDraft(draftKey);
8850
+ queryClient.invalidateQueries({
8851
+ queryKey: scopeCountsQueryKey(ctx.collectionId, ctx.appId, ctx.recordType)
8852
+ });
7901
8853
  onSaved?.();
7902
8854
  } catch (err) {
7903
8855
  setSavedSnapshot(previousSnapshot);
@@ -7926,6 +8878,9 @@ function useRecordEditor(args) {
7926
8878
  if (!resolved.recordId) return;
7927
8879
  await removeRecord(ctx, resolved.recordId);
7928
8880
  draftStore.clearDraft(draftKey);
8881
+ queryClient.invalidateQueries({
8882
+ queryKey: scopeCountsQueryKey(ctx.collectionId, ctx.appId, ctx.recordType)
8883
+ });
7929
8884
  onDeleted?.();
7930
8885
  }, [resolved.source, resolved.recordId, draftKey]);
7931
8886
  const prevDraftKeyRef = useRef(draftKey);
@@ -7991,7 +8946,9 @@ function useRecordEditor(args) {
7991
8946
  baselineFacetRule: savedFacetRule,
7992
8947
  createMode,
7993
8948
  scopeAnchors: anchors,
7994
- ref: scope.kind === "rule" && scope.raw ? scope.raw : void 0,
8949
+ // Draft-store entry never carries a framework-derived `ref`. The
8950
+ // value belongs to the host; we don't even pass it through here.
8951
+ ref: void 0,
7995
8952
  saveKind,
7996
8953
  save: async () => {
7997
8954
  await save();
@@ -7999,8 +8956,8 @@ function useRecordEditor(args) {
7999
8956
  });
8000
8957
  }, [isDirty, value, facetRule, savedSnapshot, savedFacetRule, scope.raw, resolved.recordId, resolved.source, createMode, save]);
8001
8958
  const effectiveSource = optimisticSource ?? resolved.source;
8002
- const isRuleScope = scope.kind === "rule";
8003
- const ruleValid = !isRuleScope || isFacetRuleValid(facetRule);
8959
+ const hasRuleInFlight = !!(facetRule && facetRule.all && facetRule.all.length > 0);
8960
+ const ruleValid = !hasRuleInFlight || isFacetRuleValid(facetRule);
8004
8961
  const canSave = ruleValid;
8005
8962
  const cannotSaveReason = !ruleValid ? "Pick at least one value for every facet in the rule before saving." : void 0;
8006
8963
  return {
@@ -8263,6 +9220,6 @@ function useMergedRecord(args) {
8263
9220
  };
8264
9221
  }
8265
9222
 
8266
- export { ALL_ITEM_VIEWS, ALL_PRESENTATIONS, BatchList, BulkActionsMenu, DEFAULT_DEEP_LINK_PARAM_NAMES, DEFAULT_I18N, DEFAULT_ICONS, DefaultItemCards, DefaultItemTable, DefaultRecordCard, DefaultRecordRow, DeleteButton, DirtyDraftProvider, DrawerPreview, EditorItemNav, EmptyState, ErrorState, FacetList, InheritanceMarker, InheritanceProvider, InlinePreview, IntroCard, ItemListView, ItemViewSwitcher, LoadingState, PresentationSwitcher, PreviewScopePicker, PreviewToggleButton, ProductDrillDown, ProductList, RecordBrowser, RecordEditor, RecordList, RecordsAdminShell, ResolvedPreview, ScopeBreadcrumb, ScopeTabs, SiblingRail, SidePreview, StatusDot, StatusFilterPills, StatusIcon, TabbedPreview, UtilityRow, VariantList, buildDraftKey, buildRef, checkPasteCompatibility, cloneValue, createDefaultDeepLinkAdapter, createPostMessageDeepLinkAdapter, downloadBlob, exportCsv, importCsv, isInSmartLinksIframe, mergeIcons, parseRef, pickHeaderIcon, resolutionChain, resolveRecord, statusToneLabel, useCollectedRecords, useCollectionItems, useDeepLinkState, useDirtyDraft, useDirtyDraftActions, useDirtyDraftStore, useDirtyDrafts, useDirtyNavigation, useFacetBrowse, useIntroDismissed, useItemViewPref, useMergedRecord, usePresentationPref, useProductBrowse, useProductChildren, useRecordClipboard, useRecordEditor, useRecordList, useResolveAllRecords, useResolvedRecord, useRulePreview, useScopeProbe, useUnsavedGuard };
9223
+ export { ALL_ITEM_VIEWS, ALL_PRESENTATIONS, BatchList, BulkActionsMenu, DEFAULT_DEEP_LINK_PARAM_NAMES, DEFAULT_I18N, DEFAULT_ICONS, DefaultItemCards, DefaultItemTable, DefaultRecordCard, DefaultRecordRow, DeleteButton, DirtyDraftProvider, DrawerPreview, EditorItemNav, EmptyState, ErrorState, FacetList, InheritanceMarker, InheritanceProvider, InlinePreview, IntroCard, ItemListView, ItemViewSwitcher, LoadingState, PresentationSwitcher, PreviewScopePicker, PreviewToggleButton, ProductDrillDown, ProductList, RecordBrowser, RecordEditor, RecordList, RecordsAdminShell, ResolvedPreview, ScopeBreadcrumb, ScopeTabs, SiblingRail, SidePreview, StatusDot, StatusFilterPills, StatusIcon, TabbedPreview, UtilityRow, VariantList, buildDraftKey, buildRef, checkPasteCompatibility, cloneValue, createDefaultDeepLinkAdapter, createPostMessageDeepLinkAdapter, downloadBlob, exportCsv, importCsv, isInSmartLinksIframe, mergeIcons, normaliseRule, parseRef, pickHeaderIcon, resolutionChain, resolveRecord, ruleHash, rulesEqual, scopeCountsQueryKey, statusToneLabel, summariseRule, useCollectedRecords, useCollectionItems, useDeepLinkState, useDirtyDraft, useDirtyDraftActions, useDirtyDraftStore, useDirtyDrafts, useDirtyNavigation, useFacetBrowse, useIntroDismissed, useItemViewPref, useMergedRecord, usePresentationPref, useProductBrowse, useProductChildren, useRecordClipboard, useRecordEditor, useRecordList, useResolveAllRecords, useResolvedRecord, useRulePreview, useScopeCounts, useScopeProbe, useUnsavedGuard };
8267
9224
  //# sourceMappingURL=index.js.map
8268
9225
  //# sourceMappingURL=index.js.map