@proveanything/smartlinks-utils-ui 0.8.2 → 0.8.3

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.
@@ -658,6 +658,22 @@ interface RecordsAdminShellProps<TData = unknown> {
658
658
  variantId?: string;
659
659
  batchId?: string;
660
660
  };
661
+ /**
662
+ * How the rail should react when `contextScope.productId` is pinned.
663
+ *
664
+ * - `'strict'` (default) — the host has scoped this mount to one product.
665
+ * The rail hides `'collection'` (Global) and `'rule'` tabs entirely
666
+ * (they're collection-wide concerns the product inherits) and the
667
+ * product list collapses to that single product. When the product has
668
+ * no variants and no batches available for drill-down AND no other
669
+ * top-level scopes survive, the rail itself is hidden and the editor
670
+ * takes the whole pane.
671
+ * - `'expand'` — legacy behaviour. The full rail renders; pinning only
672
+ * biases the default tab + selected product.
673
+ *
674
+ * Has no effect when `contextScope.productId` is not set.
675
+ */
676
+ contextScopeMode?: 'strict' | 'expand';
661
677
  renderEditor: (ctx: EditorContext<TData>) => ReactNode;
662
678
  /**
663
679
  * Render the preview surface. Receives the editor's current value so the
@@ -1651,6 +1667,16 @@ interface UseCollectionItemsArgs {
1651
1667
  * the query disabled.
1652
1668
  */
1653
1669
  scope: ParsedRef | null;
1670
+ /**
1671
+ * When the scope is a rule (kind === 'rule'), pass the rule's `facetRule`
1672
+ * here. Items belonging to a rule are records that carry the *same*
1673
+ * facetRule — they share anchors only by coincidence (typically empty),
1674
+ * so anchor equality alone isn't enough to identify them. When this is
1675
+ * provided we match rule records by facetRule equality and ignore
1676
+ * anchors. When absent, anchor-based matching applies (and rule records
1677
+ * are skipped, since they belong on the Rules tab).
1678
+ */
1679
+ facetRule?: FacetRule | null;
1654
1680
  /** Per-page size requested from the SDK (default 100). */
1655
1681
  pageSize?: number;
1656
1682
  /**
@@ -2229,6 +2229,14 @@ function useItemViewPref(args) {
2229
2229
  return [value, set];
2230
2230
  }
2231
2231
  var QK_BASE2 = ["records-admin", "collection-items"];
2232
+ var canonicalFacetRule = (rule) => {
2233
+ if (!rule || !Array.isArray(rule.all)) return "";
2234
+ const clauses = rule.all.map((c) => ({
2235
+ facetKey: c.facetKey,
2236
+ anyOf: [...c.anyOf ?? []].sort()
2237
+ })).sort((a, b) => a.facetKey.localeCompare(b.facetKey));
2238
+ return JSON.stringify(clauses);
2239
+ };
2232
2240
  var defaultToSummary = (rec, base) => {
2233
2241
  const data = rec.data;
2234
2242
  const display = data?.display ?? void 0;
@@ -2238,8 +2246,13 @@ var defaultToSummary = (rec, base) => {
2238
2246
  const thumbnail = data.thumbnail ?? data.image ?? data.cover ?? data.home_image_url ?? base.thumbnail;
2239
2247
  return { ...base, label, subtitle, thumbnail };
2240
2248
  };
2241
- var recordMatchesScope = (rec, scope) => {
2249
+ var recordMatchesScope = (rec, scope, ruleSignature) => {
2242
2250
  const facetRule = rec.facetRule ?? null;
2251
+ if (scope.kind === "rule") {
2252
+ if (!facetRule) return false;
2253
+ if (!ruleSignature) return false;
2254
+ return canonicalFacetRule(facetRule) === ruleSignature;
2255
+ }
2243
2256
  if (facetRule) return false;
2244
2257
  return (rec.productId ?? void 0) === scope.productId && (rec.variantId ?? void 0) === scope.variantId && (rec.batchId ?? void 0) === scope.batchId && (rec.proofId ?? void 0) === scope.proofId;
2245
2258
  };
@@ -2247,15 +2260,20 @@ function useCollectionItems(args) {
2247
2260
  const {
2248
2261
  ctx,
2249
2262
  scope,
2263
+ facetRule,
2250
2264
  pageSize = 100,
2251
2265
  toSummary: toSummary2 = defaultToSummary,
2252
2266
  enabled = true
2253
2267
  } = args;
2254
2268
  const queryClient = useQueryClient();
2255
2269
  const scopeRef = scope?.raw ?? "";
2270
+ const ruleSignature = useMemo(
2271
+ () => scope?.kind === "rule" ? canonicalFacetRule(facetRule) : "",
2272
+ [scope?.kind, facetRule]
2273
+ );
2256
2274
  const queryKey = useMemo(
2257
- () => [...QK_BASE2, ctx.collectionId, ctx.appId, ctx.recordType, scopeRef],
2258
- [ctx.collectionId, ctx.appId, ctx.recordType, scopeRef]
2275
+ () => [...QK_BASE2, ctx.collectionId, ctx.appId, ctx.recordType, scopeRef, ruleSignature],
2276
+ [ctx.collectionId, ctx.appId, ctx.recordType, scopeRef, ruleSignature]
2259
2277
  );
2260
2278
  const query = useInfiniteQuery({
2261
2279
  queryKey,
@@ -2275,7 +2293,7 @@ function useCollectionItems(args) {
2275
2293
  const items = useMemo(() => {
2276
2294
  if (!scope) return [];
2277
2295
  const all = query.data?.pages.flatMap((p) => p.data) ?? [];
2278
- const relevant = all.filter((rec) => recordMatchesScope(rec, scope));
2296
+ const relevant = all.filter((rec) => recordMatchesScope(rec, scope, ruleSignature));
2279
2297
  return relevant.map((rec) => {
2280
2298
  const ref = rec.ref ?? "";
2281
2299
  const parsed = parseRef(ref);
@@ -2292,7 +2310,7 @@ function useCollectionItems(args) {
2292
2310
  };
2293
2311
  return toSummary2(rec, base);
2294
2312
  }).filter((x) => x !== null);
2295
- }, [query.data, toSummary2, scope]);
2313
+ }, [query.data, toSummary2, scope, ruleSignature]);
2296
2314
  const refetch = useCallback(() => {
2297
2315
  queryClient.invalidateQueries({ queryKey });
2298
2316
  }, [queryClient, queryKey]);
@@ -5535,6 +5553,7 @@ function RecordsAdminShellInner(props) {
5535
5553
  scopes: requestedScopes,
5536
5554
  defaultScope,
5537
5555
  contextScope,
5556
+ contextScopeMode = "strict",
5538
5557
  renderEditor,
5539
5558
  renderPreview,
5540
5559
  intro,
@@ -5657,6 +5676,11 @@ function RecordsAdminShellInner(props) {
5657
5676
  }
5658
5677
  return [...TOP_LEVEL_SCOPES];
5659
5678
  }, [requestedScopes]);
5679
+ const productPinnedFromContext = !!contextScope?.productId;
5680
+ const effectiveTopLevelScopes = useMemo(() => {
5681
+ if (!productPinnedFromContext || contextScopeMode !== "strict") return topLevelScopes;
5682
+ return topLevelScopes.filter((s) => s !== "collection" && s !== "rule");
5683
+ }, [productPinnedFromContext, contextScopeMode, topLevelScopes]);
5660
5684
  useEffect(() => {
5661
5685
  if (requestedScopes.includes("facet") && !WARNED_FACET_DEPRECATED) {
5662
5686
  WARNED_FACET_DEPRECATED = true;
@@ -5674,10 +5698,10 @@ function RecordsAdminShellInner(props) {
5674
5698
  [requestedScopes, probe.isLoading, probe.hasBatches]
5675
5699
  );
5676
5700
  const initialScope = useMemo(() => {
5677
- if (contextScope?.productId && topLevelScopes.includes("product")) return "product";
5678
- if (defaultScope && topLevelScopes.includes(defaultScope)) return defaultScope;
5679
- return topLevelScopes[0] ?? "product";
5680
- }, [contextScope?.productId, defaultScope, topLevelScopes]);
5701
+ if (contextScope?.productId && effectiveTopLevelScopes.includes("product")) return "product";
5702
+ if (defaultScope && effectiveTopLevelScopes.includes(defaultScope)) return defaultScope;
5703
+ return effectiveTopLevelScopes[0] ?? "product";
5704
+ }, [contextScope?.productId, defaultScope, effectiveTopLevelScopes]);
5681
5705
  const [activeScope, setActiveScope] = useState(initialScope);
5682
5706
  useEffect(() => {
5683
5707
  setActiveScope(initialScope);
@@ -5805,6 +5829,12 @@ function RecordsAdminShellInner(props) {
5805
5829
  const collectionItems = useCollectionItems({
5806
5830
  ctx,
5807
5831
  scope: isCollection ? editingScope : null,
5832
+ // When the editing scope is a rule, items belonging to that rule are
5833
+ // records carrying the *same* facetRule (anchors are typically empty
5834
+ // and would otherwise collide with global records). Look up the rule's
5835
+ // facetRule from the selected record's summary so `useCollectionItems`
5836
+ // can match by rule equality instead of by anchors.
5837
+ facetRule: editingScope?.kind === "rule" ? recordList.items.find((it) => it.id === selectedRecordId)?.facetRule ?? null : null,
5808
5838
  enabled: isCollection
5809
5839
  });
5810
5840
  const editingTargetScope = useMemo(() => {
@@ -6385,6 +6415,21 @@ function RecordsAdminShellInner(props) {
6385
6415
  };
6386
6416
  const isProductTab = activeScope === "product";
6387
6417
  const productPinned = !!contextScope?.productId;
6418
+ const railHidden = useMemo(() => {
6419
+ if (!productPinned) return false;
6420
+ if (contextScopeMode !== "strict") return false;
6421
+ if (cardinality === "collection") return false;
6422
+ if (effectiveTopLevelScopes.length > 1) return false;
6423
+ if (drillVariantsAllowed || drillBatchesAllowed) return false;
6424
+ return true;
6425
+ }, [
6426
+ productPinned,
6427
+ contextScopeMode,
6428
+ cardinality,
6429
+ effectiveTopLevelScopes.length,
6430
+ drillVariantsAllowed,
6431
+ drillBatchesAllowed
6432
+ ]);
6388
6433
  const effectivePresentation = isProductTab ? presentation : "list";
6389
6434
  const showPresentationSwitcher = isProductTab && presentations.length > 1;
6390
6435
  const effectiveGroupBy = groupBy;
@@ -6679,9 +6724,12 @@ function RecordsAdminShellInner(props) {
6679
6724
  "div",
6680
6725
  {
6681
6726
  className: "flex-1 grid border-t overflow-hidden",
6682
- style: { gridTemplateColumns: "minmax(260px, 320px) 1fr", borderColor: "hsl(var(--ra-border))" },
6727
+ style: {
6728
+ gridTemplateColumns: railHidden ? "1fr" : "minmax(260px, 320px) 1fr",
6729
+ borderColor: "hsl(var(--ra-border))"
6730
+ },
6683
6731
  children: [
6684
- /* @__PURE__ */ jsx("aside", { className: "border-r overflow-hidden flex flex-col", style: { borderColor: "hsl(var(--ra-border))", background: "hsl(var(--ra-surface))" }, children: isCollection && selectedItemId && collectionRailMode === "siblings" && ruleWizardStep === null ? /* @__PURE__ */ jsx(
6732
+ !railHidden && /* @__PURE__ */ jsx("aside", { className: "border-r overflow-hidden flex flex-col", style: { borderColor: "hsl(var(--ra-border))", background: "hsl(var(--ra-surface))" }, children: isCollection && selectedItemId && collectionRailMode === "siblings" && ruleWizardStep === null ? /* @__PURE__ */ jsx(
6685
6733
  SiblingRail,
6686
6734
  {
6687
6735
  items: collectionItems.items,
@@ -6698,7 +6746,7 @@ function RecordsAdminShellInner(props) {
6698
6746
  /* @__PURE__ */ jsx("div", { className: "p-2", children: /* @__PURE__ */ jsx(
6699
6747
  ScopeTabs,
6700
6748
  {
6701
- scopes: topLevelScopes,
6749
+ scopes: effectiveTopLevelScopes,
6702
6750
  active: activeScope,
6703
6751
  onChange: (s) => {
6704
6752
  void runWithGuard(() => {
@@ -6747,60 +6795,62 @@ function RecordsAdminShellInner(props) {
6747
6795
  ]
6748
6796
  }
6749
6797
  ),
6750
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
6751
- /* @__PURE__ */ jsxs("div", { className: "relative flex-1 min-w-0", children: [
6752
- /* @__PURE__ */ jsx(Search, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 opacity-50" }),
6798
+ !(isGlobalTab && !isCollection) && /* @__PURE__ */ jsxs(Fragment, { children: [
6799
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
6800
+ /* @__PURE__ */ jsxs("div", { className: "relative flex-1 min-w-0", children: [
6801
+ /* @__PURE__ */ jsx(Search, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 opacity-50" }),
6802
+ /* @__PURE__ */ jsx(
6803
+ "input",
6804
+ {
6805
+ type: "text",
6806
+ value: search,
6807
+ onChange: (e) => setSearch(e.target.value),
6808
+ placeholder: i18n.searchPlaceholder,
6809
+ className: "w-full pl-8 pr-3 py-1.5 text-xs rounded-md border bg-transparent focus:outline-none focus:ring-1",
6810
+ style: { borderColor: "hsl(var(--ra-border))", color: "hsl(var(--ra-text))" }
6811
+ }
6812
+ )
6813
+ ] }),
6753
6814
  /* @__PURE__ */ jsx(
6754
- "input",
6815
+ PresentationSwitcher,
6755
6816
  {
6756
- type: "text",
6757
- value: search,
6758
- onChange: (e) => setSearch(e.target.value),
6759
- placeholder: i18n.searchPlaceholder,
6760
- className: "w-full pl-8 pr-3 py-1.5 text-xs rounded-md border bg-transparent focus:outline-none focus:ring-1",
6761
- style: { borderColor: "hsl(var(--ra-border))", color: "hsl(var(--ra-text))" }
6817
+ options: showPresentationSwitcher ? presentations : [],
6818
+ value: presentation,
6819
+ onChange: onPresentationChange,
6820
+ i18n
6762
6821
  }
6763
6822
  )
6764
6823
  ] }),
6765
- /* @__PURE__ */ jsx(
6766
- PresentationSwitcher,
6824
+ !isProductTab && !isRuleTab && !isGlobalTab && /* @__PURE__ */ jsx(
6825
+ StatusFilterPills,
6767
6826
  {
6768
- options: showPresentationSwitcher ? presentations : [],
6769
- value: presentation,
6770
- onChange: onPresentationChange,
6827
+ value: filter,
6828
+ onChange: setFilter,
6829
+ counts: facetBrowse.counts,
6830
+ hideZero: ["partial"],
6771
6831
  i18n
6772
6832
  }
6833
+ ),
6834
+ isRuleTab && /* @__PURE__ */ jsx(
6835
+ RuleFilterChips,
6836
+ {
6837
+ source: recordList.items,
6838
+ value: ruleFilters,
6839
+ onChange: setRuleFilters
6840
+ }
6841
+ ),
6842
+ isRuleTab && /* @__PURE__ */ jsx(
6843
+ FacetBrowseFilter,
6844
+ {
6845
+ facets: facetBrowseFacets,
6846
+ value: facetBrowseFilter,
6847
+ onChange: setFacetBrowseFilter,
6848
+ isLoading: facetBrowse.isLoading
6849
+ }
6773
6850
  )
6774
- ] }),
6775
- !isProductTab && !isRuleTab && !isGlobalTab && /* @__PURE__ */ jsx(
6776
- StatusFilterPills,
6777
- {
6778
- value: filter,
6779
- onChange: setFilter,
6780
- counts: facetBrowse.counts,
6781
- hideZero: ["partial"],
6782
- i18n
6783
- }
6784
- ),
6785
- isRuleTab && /* @__PURE__ */ jsx(
6786
- RuleFilterChips,
6787
- {
6788
- source: recordList.items,
6789
- value: ruleFilters,
6790
- onChange: setRuleFilters
6791
- }
6792
- ),
6793
- (isRuleTab || isGlobalTab && !isCollection) && /* @__PURE__ */ jsx(
6794
- FacetBrowseFilter,
6795
- {
6796
- facets: facetBrowseFacets,
6797
- value: facetBrowseFilter,
6798
- onChange: setFacetBrowseFilter,
6799
- isLoading: facetBrowse.isLoading
6800
- }
6801
- )
6851
+ ] })
6802
6852
  ] }),
6803
- /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto", children: [
6853
+ /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto", children: isGlobalTab && !isCollection ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
6804
6854
  leftLoading && /* @__PURE__ */ jsx(LoadingState, {}),
6805
6855
  !leftLoading && leftError && /* @__PURE__ */ jsx(ErrorState, { error: leftError }),
6806
6856
  !leftLoading && !leftError && leftItems.length === 0 && (renderEmpty ? renderEmpty({ scope: editingScope }) : renderEmptyState ? renderEmptyState({ scope: activeScope }) : /* @__PURE__ */ jsx(
@@ -6844,7 +6894,7 @@ function RecordsAdminShellInner(props) {
6844
6894
  }
6845
6895
  ) })
6846
6896
  ] })
6847
- ] })
6897
+ ] }) })
6848
6898
  ] }) }),
6849
6899
  /* @__PURE__ */ jsxs("main", { className: "overflow-hidden", children: [
6850
6900
  ruleWizardStep !== null && /* @__PURE__ */ jsxs(