@proveanything/smartlinks-utils-ui 0.9.7 → 0.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1235,6 +1235,14 @@ interface Props$a<T> {
1235
1235
  headerLabel?: string;
1236
1236
  /** Optional subtitle shown beneath `headerLabel` (e.g. facet name). */
1237
1237
  headerSubtitle?: string;
1238
+ /**
1239
+ * Optional content rendered at the very start of the header's left
1240
+ * cluster, before the title/breadcrumb. Used by the shell to embed the
1241
+ * collection-mode `EditorItemNav` (Back / position / arrows) inline so
1242
+ * it shares the row with the Global / scope chip instead of consuming
1243
+ * its own strip above the editor.
1244
+ */
1245
+ headerLeading?: ReactNode;
1238
1246
  /**
1239
1247
  * Optional technical reference (e.g. productId / SKU) shown as a tiny muted
1240
1248
  * caption pinned to the top-right of the header. Power users can scan it
@@ -1259,7 +1267,7 @@ interface Props$a<T> {
1259
1267
  /** Host-provided icons rendered before save / discard / delete labels. */
1260
1268
  actionIcons?: Partial<Record<RecordsAdminActionKey, RecordsAdminActionIcon>>;
1261
1269
  }
1262
- declare function RecordEditor<T>({ ctx, i18n, children, preview, targeting, targetingControl, bulkActions, footerExtra, onBeforeDelete, headerLabel, headerSubtitle, headerMeta, clipboard, actionLabels, actionIcons, }: Props$a<T>): react_jsx_runtime.JSX.Element;
1270
+ declare function RecordEditor<T>({ ctx, i18n, children, preview, targeting, targetingControl, bulkActions, footerExtra, onBeforeDelete, headerLabel, headerSubtitle, headerMeta, headerLeading, clipboard, actionLabels, actionIcons, }: Props$a<T>): react_jsx_runtime.JSX.Element;
1263
1271
 
1264
1272
  interface InheritanceCtx {
1265
1273
  parentValue?: Record<string, unknown> | null;
@@ -1897,6 +1905,16 @@ interface UseScopeCountsResult {
1897
1905
  error: Error | null;
1898
1906
  /** True when we hit `maxRecords` and stopped paginating. */
1899
1907
  truncated: boolean;
1908
+ /**
1909
+ * Set of distinct `productId`s observed across the scanned records.
1910
+ * Used by the rail to:
1911
+ * • surface "X products configured" as the Products tab badge
1912
+ * (consistent with Global/Rules counts — never catalogue size);
1913
+ * • sort configured products to the top of the browse rail;
1914
+ * • back the Configured/Not-configured filter pills.
1915
+ * Lower bound when `truncated` is true.
1916
+ */
1917
+ productIds: ReadonlySet<string>;
1900
1918
  }
1901
1919
  declare const useScopeCounts: (args: UseScopeCountsArgs) => UseScopeCountsResult;
1902
1920
  /** React Query key factory for cache invalidation from save/delete sites. */
@@ -2283,8 +2301,15 @@ interface Props$1 {
2283
2301
  canPrev: boolean;
2284
2302
  canNext: boolean;
2285
2303
  i18n: Pick<RecordsAdminI18n, 'backToList' | 'prevItem' | 'nextItem'>;
2304
+ /**
2305
+ * When true, render without the row-level border/background/padding so
2306
+ * the nav can sit inline inside another header strip (the shell embeds
2307
+ * it in `RecordEditor`'s header to avoid a dedicated row of whitespace
2308
+ * above the editor).
2309
+ */
2310
+ embedded?: boolean;
2286
2311
  }
2287
- declare const EditorItemNav: ({ label, position, total, onBack, onPrev, onNext, canPrev, canNext, i18n, }: Props$1) => react_jsx_runtime.JSX.Element;
2312
+ declare const EditorItemNav: ({ label, position, total, onBack, onPrev, onNext, canPrev, canNext, i18n, embedded, }: Props$1) => react_jsx_runtime.JSX.Element;
2288
2313
 
2289
2314
  interface Props<T> {
2290
2315
  items: RecordSummary<T>[];
@@ -531,14 +531,20 @@ var useScopeCounts = (args) => {
531
531
  rule: 0,
532
532
  all: 0
533
533
  };
534
- for (const rec of records) counts[classify(rec)] += 1;
534
+ const productIds = /* @__PURE__ */ new Set();
535
+ for (const rec of records) {
536
+ counts[classify(rec)] += 1;
537
+ const pid = rec.productId ?? void 0;
538
+ if (pid) productIds.add(pid);
539
+ }
535
540
  counts.all = records.length;
536
541
  return {
537
542
  counts,
538
543
  total: records.length,
539
544
  isLoading: query.isLoading,
540
545
  error: query.error ?? null,
541
- truncated
546
+ truncated,
547
+ productIds
542
548
  };
543
549
  }, [query.data, query.isLoading, query.error]);
544
550
  return result;
@@ -731,26 +737,15 @@ var useFacetBrowse = ({
731
737
  enabled: queryEnabled,
732
738
  staleTime: 3e4,
733
739
  queryFn: async () => {
734
- const t0 = performance.now();
735
740
  try {
736
741
  if (SL?.facets?.list) {
737
- console.info(`${LOG} \u2192 SL.facets.list("${collectionId}", { includeValues: true })`);
738
742
  const res = await SL.facets.list(collectionId, { includeValues: true });
739
743
  const items = res?.items ?? [];
740
- console.info(
741
- `${LOG} \u2190 SL.facets.list ok in ${Math.round(performance.now() - t0)}ms \u2014 ${items.length} facet(s)`,
742
- items
743
- );
744
744
  return items;
745
745
  }
746
746
  if (SL?.facets?.publicList) {
747
- console.info(`${LOG} \u2192 SL.facets.publicList("${collectionId}", { includeValues: true })`);
748
747
  const res = await SL.facets.publicList(collectionId, { includeValues: true });
749
748
  const items = res?.items ?? [];
750
- console.info(
751
- `${LOG} \u2190 SL.facets.publicList ok in ${Math.round(performance.now() - t0)}ms \u2014 ${items.length} facet(s)`,
752
- items
753
- );
754
749
  return items;
755
750
  }
756
751
  console.warn(`${LOG} queryFn ran but no facets API is available on SL`);
@@ -767,7 +762,6 @@ var useFacetBrowse = ({
767
762
  if (signature === lastLoggedRef.current) return;
768
763
  lastLoggedRef.current = signature;
769
764
  if (!enabled) {
770
- console.info(`${LOG} skipped \u2014 enabled=false (shell not on facet tab yet)`);
771
765
  return;
772
766
  }
773
767
  if (!collectionId) {
@@ -781,9 +775,6 @@ var useFacetBrowse = ({
781
775
  );
782
776
  return;
783
777
  }
784
- console.info(
785
- `${LOG} will fetch facets for collection "${collectionId}" via ${hasAdminList ? "SL.facets.list (admin)" : "SL.facets.publicList (fallback)"}`
786
- );
787
778
  }, [enabled, collectionId, hasAdminList, hasPublicList, hasAnyList, SL]);
788
779
  const mergedItems = useMemo(() => {
789
780
  const existingByAnchor = new Map(
@@ -2158,6 +2149,13 @@ var useShellDeepLink = (args) => {
2158
2149
  }
2159
2150
  setPendingDeepLinkRecordId(recordId ?? null);
2160
2151
  if (recordId) preserveInitialRecordIdRef.current = true;
2152
+ if (recordId) {
2153
+ console.info("[RecordsAdminShell] deep-link restore \u2014 pending recordId set", {
2154
+ recordId,
2155
+ scope: scope ?? null,
2156
+ view: view ?? null
2157
+ });
2158
+ }
2161
2159
  if (!recordId && selectedItemId !== null) {
2162
2160
  console.info("[RecordsAdminShell] preserving selected item during restore without recordId", {
2163
2161
  selectedItemId,
@@ -2183,7 +2181,9 @@ var useShellDeepLink = (args) => {
2183
2181
  warnedMissingRef.current.add(pending);
2184
2182
  console.warn("[RecordsAdminShell] deep-linked recordId not found in collection items \u2014 clearing pending selection", {
2185
2183
  recordId: pending,
2186
- scope: editingScope?.raw ?? null
2184
+ scope: editingScope?.raw ?? null,
2185
+ collectionItemsCount: collectionItems.items.length,
2186
+ sampleIds: collectionItems.items.slice(0, 5).map((it) => ({ id: it.id, itemId: it.itemId }))
2187
2187
  });
2188
2188
  }
2189
2189
  preserveInitialRecordIdRef.current = false;
@@ -2203,7 +2203,10 @@ var useShellDeepLink = (args) => {
2203
2203
  warnedMissingRef.current.add(pending);
2204
2204
  console.warn("[RecordsAdminShell] deep-linked recordId not found in records list \u2014 clearing pending selection", {
2205
2205
  recordId: pending,
2206
- scope: editingScope?.raw ?? null
2206
+ scope: editingScope?.raw ?? null,
2207
+ recordListCount: recordListItems.length,
2208
+ sampleIds: recordListItems.slice(0, 5).map((it) => it.id),
2209
+ activeScopeHint: "check that the shell tab matches the record's scope"
2207
2210
  });
2208
2211
  }
2209
2212
  preserveInitialRecordIdRef.current = false;
@@ -4245,6 +4248,7 @@ function RecordEditor({
4245
4248
  headerLabel,
4246
4249
  headerSubtitle,
4247
4250
  headerMeta,
4251
+ headerLeading,
4248
4252
  clipboard,
4249
4253
  actionLabels,
4250
4254
  actionIcons
@@ -4261,7 +4265,7 @@ function RecordEditor({
4261
4265
  const s = ctx.scope;
4262
4266
  return Boolean(s?.facetId || s?.productId || s?.variantId || s?.batchId);
4263
4267
  })();
4264
- const hasLeftContent = Boolean(headerLabel) || hasBreadcrumb;
4268
+ const hasLeftContent = Boolean(headerLabel) || hasBreadcrumb || Boolean(headerLeading);
4265
4269
  const hasRightContent = showInherited || showEmpty || Boolean(headerMeta) || Boolean(bulkActions) || Boolean(targetingControl);
4266
4270
  const showHeader = hasLeftContent || hasRightContent;
4267
4271
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", children: [
@@ -4271,26 +4275,29 @@ function RecordEditor({
4271
4275
  className: "sticky top-0 z-40 px-5 py-3 border-b flex items-start justify-between gap-3",
4272
4276
  style: { borderColor: "hsl(var(--ra-border))", background: "hsl(var(--ra-surface))" },
4273
4277
  children: [
4274
- /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: headerLabel ? /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
4275
- /* @__PURE__ */ jsx(
4276
- "div",
4277
- {
4278
- className: "text-sm font-medium truncate",
4279
- style: { color: "hsl(var(--ra-text))" },
4280
- title: headerLabel,
4281
- children: headerLabel
4282
- }
4283
- ),
4284
- headerSubtitle && /* @__PURE__ */ jsx(
4285
- "div",
4286
- {
4287
- className: "text-xs truncate",
4288
- style: { color: "hsl(var(--ra-muted-text))" },
4289
- title: headerSubtitle,
4290
- children: headerSubtitle
4291
- }
4292
- )
4293
- ] }) : /* @__PURE__ */ jsx(ScopeBreadcrumb, { scope: ctx.scope }) }),
4278
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 flex items-center gap-3", children: [
4279
+ headerLeading && /* @__PURE__ */ jsx("div", { className: "shrink-0", children: headerLeading }),
4280
+ headerLabel ? /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
4281
+ /* @__PURE__ */ jsx(
4282
+ "div",
4283
+ {
4284
+ className: "text-sm font-medium truncate",
4285
+ style: { color: "hsl(var(--ra-text))" },
4286
+ title: headerLabel,
4287
+ children: headerLabel
4288
+ }
4289
+ ),
4290
+ headerSubtitle && /* @__PURE__ */ jsx(
4291
+ "div",
4292
+ {
4293
+ className: "text-xs truncate",
4294
+ style: { color: "hsl(var(--ra-muted-text))" },
4295
+ title: headerSubtitle,
4296
+ children: headerSubtitle
4297
+ }
4298
+ )
4299
+ ] }) : /* @__PURE__ */ jsx(ScopeBreadcrumb, { scope: ctx.scope })
4300
+ ] }),
4294
4301
  /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 shrink-0", children: [
4295
4302
  showInherited && /* @__PURE__ */ jsxs(
4296
4303
  "span",
@@ -5759,8 +5766,9 @@ var EditorItemNav = ({
5759
5766
  onNext,
5760
5767
  canPrev,
5761
5768
  canNext,
5762
- i18n
5763
- }) => /* @__PURE__ */ jsxs("div", { className: "ra-item-nav", children: [
5769
+ i18n,
5770
+ embedded
5771
+ }) => /* @__PURE__ */ jsxs("div", { className: "ra-item-nav", "data-embedded": embedded ? "true" : void 0, children: [
5764
5772
  /* @__PURE__ */ jsxs(
5765
5773
  "button",
5766
5774
  {
@@ -5774,12 +5782,7 @@ var EditorItemNav = ({
5774
5782
  ]
5775
5783
  }
5776
5784
  ),
5777
- typeof position === "number" && typeof total === "number" && total > 0 && /* @__PURE__ */ jsxs("span", { className: "ra-item-nav-position", "aria-label": label, children: [
5778
- position,
5779
- " / ",
5780
- total
5781
- ] }),
5782
- /* @__PURE__ */ jsxs("div", { className: "ra-item-nav-arrows", children: [
5785
+ /* @__PURE__ */ jsxs("div", { className: "ra-item-nav-cluster", children: [
5783
5786
  /* @__PURE__ */ jsx(
5784
5787
  "button",
5785
5788
  {
@@ -5792,6 +5795,11 @@ var EditorItemNav = ({
5792
5795
  children: /* @__PURE__ */ jsx(ChevronLeft, { className: "w-4 h-4" })
5793
5796
  }
5794
5797
  ),
5798
+ typeof position === "number" && typeof total === "number" && total > 0 && /* @__PURE__ */ jsxs("span", { className: "ra-item-nav-position", "aria-label": label, children: [
5799
+ position,
5800
+ " / ",
5801
+ total
5802
+ ] }),
5795
5803
  /* @__PURE__ */ jsx(
5796
5804
  "button",
5797
5805
  {
@@ -7665,6 +7673,7 @@ function RecordsAdminShellInner(props) {
7665
7673
  const itemNav = isCollection && selectedItemId && itemPosition && ruleWizardStep === null ? /* @__PURE__ */ jsx(
7666
7674
  EditorItemNav,
7667
7675
  {
7676
+ embedded: true,
7668
7677
  label: editorHeaderLabel,
7669
7678
  position: itemPosition.index + 1,
7670
7679
  total: itemPosition.total,
@@ -7717,6 +7726,7 @@ function RecordsAdminShellInner(props) {
7717
7726
  headerLabel: editorHeaderLabel,
7718
7727
  headerSubtitle: editorHeaderSubtitle,
7719
7728
  headerMeta: editorHeaderMeta,
7729
+ headerLeading: itemNav,
7720
7730
  clipboard: editorClipboard,
7721
7731
  actionLabels,
7722
7732
  actionIcons,
@@ -7735,10 +7745,7 @@ function RecordsAdminShellInner(props) {
7735
7745
  )
7736
7746
  }
7737
7747
  );
7738
- const withNav = (node) => itemNav ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full min-h-0", children: [
7739
- itemNav,
7740
- /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0 overflow-hidden", children: node })
7741
- ] }) : node;
7748
+ const withNav = (node) => node;
7742
7749
  if (!previewBody) return withNav(baseEditor());
7743
7750
  if (previewMode === "inline") {
7744
7751
  return withNav(baseEditor(
@@ -7841,8 +7848,26 @@ function RecordsAdminShellInner(props) {
7841
7848
  subtitle: pinnedProduct.item?.sku ?? void 0
7842
7849
  }];
7843
7850
  }
7844
- return productBrowse.items.map(productItemToSummary);
7845
- }, [productPinned, contextScope, productBrowse.items, pinnedProduct.item]);
7851
+ const configured = scopeCounts.productIds;
7852
+ const all = productBrowse.items.map(productItemToSummary);
7853
+ const isConfigured = (s) => {
7854
+ const pid = s.scope.productId;
7855
+ return !!pid && configured.has(pid);
7856
+ };
7857
+ if (filter === "configured") return all.filter(isConfigured);
7858
+ if (filter === "empty") return all.filter((s) => !isConfigured(s));
7859
+ const yes = [];
7860
+ const no = [];
7861
+ for (const s of all) (isConfigured(s) ? yes : no).push(s);
7862
+ return [...yes, ...no];
7863
+ }, [
7864
+ productPinned,
7865
+ contextScope,
7866
+ productBrowse.items,
7867
+ pinnedProduct.item,
7868
+ scopeCounts.productIds,
7869
+ filter
7870
+ ]);
7846
7871
  const isRuleTab = activeScope === "rule";
7847
7872
  const isGlobalTab = activeScope === "collection";
7848
7873
  const isAllTab = activeScope === "all";
@@ -8281,10 +8306,14 @@ function RecordsAdminShellInner(props) {
8281
8306
  },
8282
8307
  loading: probe.isLoading,
8283
8308
  counts: {
8284
- // `product` continues to show the catalogue size (browse
8285
- // affordance), not the per-product record count that's
8286
- // the long-standing semantics for the Products tab.
8287
- product: productBrowse.items.length,
8309
+ // Products badge counts DISTINCT products that have at
8310
+ // least one custom record — same semantics as Global /
8311
+ // Rules. Catalogue size (which can be 1k–10k+) was
8312
+ // misleading: "Products: 247" implied 247 customised
8313
+ // products when in fact zero might be configured. Falls
8314
+ // back to a lower bound when scope-counts truncated at
8315
+ // the hard cap.
8316
+ product: scopeCounts.productIds.size,
8288
8317
  // The remaining tabs show actual record counts so hidden
8289
8318
  // state (e.g. a rule-scoped competition) is visible from
8290
8319
  // any tab. `useScopeCounts` returns 0 while loading, which
@@ -8365,6 +8394,27 @@ function RecordsAdminShellInner(props) {
8365
8394
  i18n
8366
8395
  }
8367
8396
  ),
8397
+ isProductTab && !productPinned && (() => {
8398
+ const cfg = scopeCounts.productIds;
8399
+ let configured = 0;
8400
+ for (const p of productBrowse.items) if (cfg.has(p.id)) configured += 1;
8401
+ const total = productBrowse.items.length;
8402
+ return /* @__PURE__ */ jsx(
8403
+ StatusFilterPills,
8404
+ {
8405
+ value: filter,
8406
+ onChange: setFilter,
8407
+ counts: {
8408
+ all: total,
8409
+ configured,
8410
+ partial: 0,
8411
+ empty: total - configured
8412
+ },
8413
+ hideZero: ["partial"],
8414
+ i18n
8415
+ }
8416
+ );
8417
+ })(),
8368
8418
  isRuleTab && /* @__PURE__ */ jsx(
8369
8419
  RuleFilterChips,
8370
8420
  {