@proveanything/smartlinks-utils-ui 0.8.1 → 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]);
@@ -2422,13 +2440,17 @@ var createPostMessageDeepLinkAdapter = (paramNames) => {
2422
2440
  overlay("scope", paramNames.scope);
2423
2441
  overlay("view", paramNames.view);
2424
2442
  try {
2425
- window.parent.postMessage({
2443
+ const message = {
2426
2444
  type: "smartlinks-route-change",
2427
2445
  path,
2428
2446
  context,
2429
2447
  state,
2430
2448
  appId: context.appId
2431
- }, "*");
2449
+ };
2450
+ console.debug("[smartlinks-ui] postMessage \u2192 parent", message, {
2451
+ sameWindow: window.parent === window
2452
+ });
2453
+ window.parent.postMessage(message, "*");
2432
2454
  } catch {
2433
2455
  }
2434
2456
  };
@@ -2481,7 +2503,13 @@ function useDeepLinkState(options) {
2481
2503
  if (!enabled) return null;
2482
2504
  if (options?.adapter) return options.adapter;
2483
2505
  if (!defaultAdapterRef.current) {
2484
- defaultAdapterRef.current = isInSmartLinksIframe() ? createPostMessageDeepLinkAdapter(paramNames) : createDefaultDeepLinkAdapter(paramNames);
2506
+ const inIframe = isInSmartLinksIframe();
2507
+ console.debug("[smartlinks-ui] deep-link adapter selected", {
2508
+ adapter: inIframe ? "postMessage" : "default",
2509
+ inIframe,
2510
+ href: typeof window !== "undefined" ? window.location.href : "(ssr)"
2511
+ });
2512
+ defaultAdapterRef.current = inIframe ? createPostMessageDeepLinkAdapter(paramNames) : createDefaultDeepLinkAdapter(paramNames);
2485
2513
  }
2486
2514
  return defaultAdapterRef.current;
2487
2515
  }, [enabled, options?.adapter, paramNames]);
@@ -4752,7 +4780,7 @@ var createEditorStore = () => {
4752
4780
  const editorId = input.editorId ?? mintEditorId();
4753
4781
  if (map.has(editorId)) return editorId;
4754
4782
  const seedValue = input.seed?.value ?? input.defaultValue;
4755
- const seedFacetRule = input.seed?.facetRule ?? input.spec.initialFacetRule ?? null;
4783
+ const seedFacetRule = input.spec.initialFacetRule !== void 0 && input.spec.initialFacetRule !== null ? input.spec.initialFacetRule : input.seed?.facetRule ?? null;
4756
4784
  const source = input.seed?.source ?? "empty";
4757
4785
  const value = cloneDeep(seedValue);
4758
4786
  const baseline = cloneDeep(seedValue);
@@ -4891,8 +4919,8 @@ var createEditorStore = () => {
4891
4919
  nextRecordId = upserted.record?.id ?? nextRecordId;
4892
4920
  }
4893
4921
  update(editorId, (e) => {
4894
- e.baseline = cloneDeep(persistedValue);
4895
- e.baselineFacetRule = persistedFacetRule;
4922
+ e.baseline = cloneDeep(e.value);
4923
+ e.baselineFacetRule = e.facetRule;
4896
4924
  e.recordId = nextRecordId;
4897
4925
  e.source = "self";
4898
4926
  e.status = "saved";
@@ -5525,6 +5553,7 @@ function RecordsAdminShellInner(props) {
5525
5553
  scopes: requestedScopes,
5526
5554
  defaultScope,
5527
5555
  contextScope,
5556
+ contextScopeMode = "strict",
5528
5557
  renderEditor,
5529
5558
  renderPreview,
5530
5559
  intro,
@@ -5647,6 +5676,11 @@ function RecordsAdminShellInner(props) {
5647
5676
  }
5648
5677
  return [...TOP_LEVEL_SCOPES];
5649
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]);
5650
5684
  useEffect(() => {
5651
5685
  if (requestedScopes.includes("facet") && !WARNED_FACET_DEPRECATED) {
5652
5686
  WARNED_FACET_DEPRECATED = true;
@@ -5664,10 +5698,10 @@ function RecordsAdminShellInner(props) {
5664
5698
  [requestedScopes, probe.isLoading, probe.hasBatches]
5665
5699
  );
5666
5700
  const initialScope = useMemo(() => {
5667
- if (contextScope?.productId && topLevelScopes.includes("product")) return "product";
5668
- if (defaultScope && topLevelScopes.includes(defaultScope)) return defaultScope;
5669
- return topLevelScopes[0] ?? "product";
5670
- }, [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]);
5671
5705
  const [activeScope, setActiveScope] = useState(initialScope);
5672
5706
  useEffect(() => {
5673
5707
  setActiveScope(initialScope);
@@ -5795,6 +5829,12 @@ function RecordsAdminShellInner(props) {
5795
5829
  const collectionItems = useCollectionItems({
5796
5830
  ctx,
5797
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,
5798
5838
  enabled: isCollection
5799
5839
  });
5800
5840
  const editingTargetScope = useMemo(() => {
@@ -6375,6 +6415,21 @@ function RecordsAdminShellInner(props) {
6375
6415
  };
6376
6416
  const isProductTab = activeScope === "product";
6377
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
+ ]);
6378
6433
  const effectivePresentation = isProductTab ? presentation : "list";
6379
6434
  const showPresentationSwitcher = isProductTab && presentations.length > 1;
6380
6435
  const effectiveGroupBy = groupBy;
@@ -6669,9 +6724,12 @@ function RecordsAdminShellInner(props) {
6669
6724
  "div",
6670
6725
  {
6671
6726
  className: "flex-1 grid border-t overflow-hidden",
6672
- 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
+ },
6673
6731
  children: [
6674
- /* @__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(
6675
6733
  SiblingRail,
6676
6734
  {
6677
6735
  items: collectionItems.items,
@@ -6688,7 +6746,7 @@ function RecordsAdminShellInner(props) {
6688
6746
  /* @__PURE__ */ jsx("div", { className: "p-2", children: /* @__PURE__ */ jsx(
6689
6747
  ScopeTabs,
6690
6748
  {
6691
- scopes: topLevelScopes,
6749
+ scopes: effectiveTopLevelScopes,
6692
6750
  active: activeScope,
6693
6751
  onChange: (s) => {
6694
6752
  void runWithGuard(() => {
@@ -6737,60 +6795,62 @@ function RecordsAdminShellInner(props) {
6737
6795
  ]
6738
6796
  }
6739
6797
  ),
6740
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
6741
- /* @__PURE__ */ jsxs("div", { className: "relative flex-1 min-w-0", children: [
6742
- /* @__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
+ ] }),
6743
6814
  /* @__PURE__ */ jsx(
6744
- "input",
6815
+ PresentationSwitcher,
6745
6816
  {
6746
- type: "text",
6747
- value: search,
6748
- onChange: (e) => setSearch(e.target.value),
6749
- placeholder: i18n.searchPlaceholder,
6750
- className: "w-full pl-8 pr-3 py-1.5 text-xs rounded-md border bg-transparent focus:outline-none focus:ring-1",
6751
- style: { borderColor: "hsl(var(--ra-border))", color: "hsl(var(--ra-text))" }
6817
+ options: showPresentationSwitcher ? presentations : [],
6818
+ value: presentation,
6819
+ onChange: onPresentationChange,
6820
+ i18n
6752
6821
  }
6753
6822
  )
6754
6823
  ] }),
6755
- /* @__PURE__ */ jsx(
6756
- PresentationSwitcher,
6824
+ !isProductTab && !isRuleTab && !isGlobalTab && /* @__PURE__ */ jsx(
6825
+ StatusFilterPills,
6757
6826
  {
6758
- options: showPresentationSwitcher ? presentations : [],
6759
- value: presentation,
6760
- onChange: onPresentationChange,
6827
+ value: filter,
6828
+ onChange: setFilter,
6829
+ counts: facetBrowse.counts,
6830
+ hideZero: ["partial"],
6761
6831
  i18n
6762
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
+ }
6763
6850
  )
6764
- ] }),
6765
- !isProductTab && !isRuleTab && !isGlobalTab && /* @__PURE__ */ jsx(
6766
- StatusFilterPills,
6767
- {
6768
- value: filter,
6769
- onChange: setFilter,
6770
- counts: facetBrowse.counts,
6771
- hideZero: ["partial"],
6772
- i18n
6773
- }
6774
- ),
6775
- isRuleTab && /* @__PURE__ */ jsx(
6776
- RuleFilterChips,
6777
- {
6778
- source: recordList.items,
6779
- value: ruleFilters,
6780
- onChange: setRuleFilters
6781
- }
6782
- ),
6783
- (isRuleTab || isGlobalTab && !isCollection) && /* @__PURE__ */ jsx(
6784
- FacetBrowseFilter,
6785
- {
6786
- facets: facetBrowseFacets,
6787
- value: facetBrowseFilter,
6788
- onChange: setFacetBrowseFilter,
6789
- isLoading: facetBrowse.isLoading
6790
- }
6791
- )
6851
+ ] })
6792
6852
  ] }),
6793
- /* @__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: [
6794
6854
  leftLoading && /* @__PURE__ */ jsx(LoadingState, {}),
6795
6855
  !leftLoading && leftError && /* @__PURE__ */ jsx(ErrorState, { error: leftError }),
6796
6856
  !leftLoading && !leftError && leftItems.length === 0 && (renderEmpty ? renderEmpty({ scope: editingScope }) : renderEmptyState ? renderEmptyState({ scope: activeScope }) : /* @__PURE__ */ jsx(
@@ -6834,7 +6894,7 @@ function RecordsAdminShellInner(props) {
6834
6894
  }
6835
6895
  ) })
6836
6896
  ] })
6837
- ] })
6897
+ ] }) })
6838
6898
  ] }) }),
6839
6899
  /* @__PURE__ */ jsxs("main", { className: "overflow-hidden", children: [
6840
6900
  ruleWizardStep !== null && /* @__PURE__ */ jsxs(