@proveanything/smartlinks-utils-ui 0.9.1 → 0.9.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.
@@ -1,3 +1,4 @@
1
+ import { assertComponentStylesLoaded } from '../../chunk-OLYC54YT.js';
1
2
  import '../../chunk-5UQQYXCX.js';
2
3
  import { FacetRuleEditor } from '../../chunk-JMCV6FOW.js';
3
4
  import { useFacets } from '../../chunk-4LHF5JB7.js';
@@ -5,7 +6,7 @@ import { cn } from '../../chunk-L7FQ52F5.js';
5
6
  import { parsedRefToTarget, parsedRefToScope, matchRecords, scopesEqual, getRecordById, listRecords, upsertRecord, updateRecord, createRecord, removeRecord } from '../../chunk-KFKVGUUP.js';
6
7
  export { bulkDelete, bulkUpsert, createRecord, getRecordById, listRecords, matchRecords, parsedRefToScope, parsedRefToTarget, removeRecord, restoreRecord, scopesEqual, upsertRecord } from '../../chunk-KFKVGUUP.js';
7
8
  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, 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
+ 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, Search, Image, Table, ArrowLeft, ChevronLeft, AlertTriangle, Info, HelpCircle, CornerDownLeft, Circle, AlertCircle, Undo2, Save, Loader2, XCircle, ArrowRight, BookOpen, Globe2, Target, Check, Settings2 } from 'lucide-react';
9
10
  import { useQuery, useQueryClient, useInfiniteQuery } from '@tanstack/react-query';
10
11
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
11
12
  import { createPortal } from 'react-dom';
@@ -146,7 +147,11 @@ var DEFAULT_I18N = {
146
147
  subtitleConfigured: "Configured",
147
148
  subtitleInherited: "Inherited",
148
149
  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
+ rulesEmptyBody: "Create a rule to target a subset of products or a specific audience \u2014 for example, \u201Conly premium tier customers in Germany\u201D.",
151
+ hookBeforeSaveFailed: "Couldn't save: {message}",
152
+ hookAfterSaveFailed: "Saved, but a follow-up step failed: {message}",
153
+ hookBeforeDeleteFailed: "Couldn't delete: {message}",
154
+ hookAfterDeleteFailed: "Deleted, but a follow-up step failed: {message}"
150
155
  };
151
156
 
152
157
  // src/components/RecordsAdmin/types/presentation.ts
@@ -1819,7 +1824,8 @@ function useShellNavigation(args) {
1819
1824
  deepLinkState,
1820
1825
  onTelemetry,
1821
1826
  onBeforeDelete,
1822
- generateItemId
1827
+ generateItemId,
1828
+ hooks
1823
1829
  } = args;
1824
1830
  const buildItemUrlValue = useCallback((id) => {
1825
1831
  if (!baseScopeRef && id.startsWith("item:")) return id;
@@ -1904,6 +1910,34 @@ function useShellNavigation(args) {
1904
1910
  const ok = await onBeforeDelete(editingScope ?? parseRef(""));
1905
1911
  if (!ok) return;
1906
1912
  }
1913
+ const row = collectionItems.items.find(
1914
+ (it) => it.itemId === itemId || it.id === itemId
1915
+ );
1916
+ const hookCtxBase = {
1917
+ collectionId: ctx.collectionId,
1918
+ appId: ctx.appId,
1919
+ recordType: ctx.recordType,
1920
+ scope: editingScope?.kind ?? "collection",
1921
+ targetRef: editingScope?.raw || void 0,
1922
+ admin: true
1923
+ };
1924
+ const recordSummary = row ?? {
1925
+ id: itemId,
1926
+ ref: editingScope?.raw ?? "",
1927
+ scope: editingScope ?? parseRef(""),
1928
+ data: null,
1929
+ status: "configured",
1930
+ label: itemId
1931
+ };
1932
+ if (hooks?.beforeDelete) {
1933
+ try {
1934
+ const result = await hooks.beforeDelete({ ...hookCtxBase, record: recordSummary });
1935
+ if (result === false) return;
1936
+ } catch (err) {
1937
+ console.warn("[RecordsAdmin] item beforeDelete hook cancelled delete", err);
1938
+ return;
1939
+ }
1940
+ }
1907
1941
  try {
1908
1942
  const { removeRecord: removeRecord2 } = await import('../../records-AYYQSP7E.js');
1909
1943
  await removeRecord2(ctx, itemId);
@@ -1911,6 +1945,13 @@ function useShellNavigation(args) {
1911
1945
  if (selectedItemId === itemId) setSelectedItemId(null);
1912
1946
  if (selectedItemId === itemId) deepLinkState.emit({ recordId: null }, "record.close");
1913
1947
  collectionItems.refetch();
1948
+ if (hooks?.afterDelete) {
1949
+ try {
1950
+ await hooks.afterDelete({ ...hookCtxBase, record: recordSummary });
1951
+ } catch (err) {
1952
+ console.warn("[RecordsAdmin] item afterDelete hook failed", err);
1953
+ }
1954
+ }
1914
1955
  } catch (err) {
1915
1956
  console.error("[RecordsAdminShell] item delete failed", err);
1916
1957
  }
@@ -1925,7 +1966,8 @@ function useShellNavigation(args) {
1925
1966
  collectionItems,
1926
1967
  deepLinkState,
1927
1968
  editingScope,
1928
- setSelectedItemId
1969
+ setSelectedItemId,
1970
+ hooks
1929
1971
  ]);
1930
1972
  const itemViewCtx = useMemo(() => ({
1931
1973
  onOpen: onItemOpen,
@@ -2072,13 +2114,24 @@ var useShellDeepLink = (args) => {
2072
2114
  setSelectedVariantId,
2073
2115
  selectedBatchId,
2074
2116
  setSelectedBatchId,
2075
- setFacetBrowseFilter
2117
+ setFacetBrowseFilter,
2118
+ isCollection,
2119
+ setSelectedItemId,
2120
+ collectionItems,
2121
+ recordListItems,
2122
+ recordListLoading,
2123
+ skipNextItemResetRef
2076
2124
  } = args;
2077
2125
  const lastAppliedDLRef = useRef("");
2078
2126
  const [pendingDeepLinkRecordId, setPendingDeepLinkRecordId] = useState(null);
2127
+ const warnedMissingRef = useRef(/* @__PURE__ */ new Set());
2079
2128
  useEffect(() => {
2080
2129
  if (!deepLinkState.enabled) return;
2081
2130
  if (selectedItemId) return;
2131
+ console.debug("[RecordsAdminShell] rail-scope emit", {
2132
+ scope: editingScope?.raw ?? null,
2133
+ itemView
2134
+ });
2082
2135
  deepLinkState.emit({ scope: editingScope?.raw ?? null, recordId: null }, "scope");
2083
2136
  lastAppliedDLRef.current = `${""}|${editingScope?.raw ?? ""}|${itemView}`;
2084
2137
  }, [deepLinkState.enabled, editingScope?.raw, selectedItemId, itemView]);
@@ -2118,6 +2171,53 @@ var useShellDeepLink = (args) => {
2118
2171
  });
2119
2172
  }
2120
2173
  }, [deepLinkState.enabled, deepLinkState.urlState]);
2174
+ useEffect(() => {
2175
+ const pending = pendingDeepLinkRecordId;
2176
+ if (!pending) return;
2177
+ if (isCollection) {
2178
+ const hit2 = collectionItems.items.find((it) => it.id === pending || it.itemId === pending);
2179
+ if (hit2) {
2180
+ skipNextItemResetRef.current = true;
2181
+ setSelectedItemId(pending);
2182
+ setPendingDeepLinkRecordId(null);
2183
+ return;
2184
+ }
2185
+ if (!collectionItems.isLoading) {
2186
+ if (!warnedMissingRef.current.has(pending)) {
2187
+ warnedMissingRef.current.add(pending);
2188
+ console.warn("[RecordsAdminShell] deep-linked recordId not found in collection items \u2014 clearing pending selection", {
2189
+ recordId: pending,
2190
+ scope: editingScope?.raw ?? null
2191
+ });
2192
+ }
2193
+ setPendingDeepLinkRecordId(null);
2194
+ }
2195
+ return;
2196
+ }
2197
+ const hit = recordListItems.find((it) => it.id === pending);
2198
+ if (hit) {
2199
+ if (selectedRecordId !== pending) setSelectedRecordId(pending);
2200
+ setPendingDeepLinkRecordId(null);
2201
+ return;
2202
+ }
2203
+ if (!recordListLoading) {
2204
+ if (!warnedMissingRef.current.has(pending)) {
2205
+ warnedMissingRef.current.add(pending);
2206
+ console.warn("[RecordsAdminShell] deep-linked recordId not found in records list \u2014 clearing pending selection", {
2207
+ recordId: pending,
2208
+ scope: editingScope?.raw ?? null
2209
+ });
2210
+ }
2211
+ setPendingDeepLinkRecordId(null);
2212
+ }
2213
+ }, [
2214
+ pendingDeepLinkRecordId,
2215
+ isCollection,
2216
+ collectionItems.items,
2217
+ collectionItems.isLoading,
2218
+ recordListItems,
2219
+ recordListLoading
2220
+ ]);
2121
2221
  return { pendingDeepLinkRecordId, lastAppliedDLRef };
2122
2222
  };
2123
2223
  var isFacetRuleValid = (rule) => {
@@ -2231,6 +2331,25 @@ var createEditorStore = () => {
2231
2331
  const listeners = /* @__PURE__ */ new Set();
2232
2332
  let cachedList = [];
2233
2333
  let nextOrder = 0;
2334
+ let hooksBundle = null;
2335
+ let notifier = null;
2336
+ const buildRecordSummary = (entry, value) => ({
2337
+ id: entry.recordId ?? null,
2338
+ ref: entry.spec.scope.raw,
2339
+ scope: entry.spec.scope,
2340
+ label: entry.label,
2341
+ status: "configured",
2342
+ data: value,
2343
+ facetRule: entry.facetRule
2344
+ });
2345
+ const buildHookCtxBase = (entry) => ({
2346
+ collectionId: entry.saveSpec.ctx.collectionId,
2347
+ appId: entry.saveSpec.ctx.appId,
2348
+ recordType: entry.saveSpec.ctx.recordType,
2349
+ scope: entry.spec.scope.kind,
2350
+ targetRef: entry.spec.scope.raw || void 0,
2351
+ admin: true
2352
+ });
2234
2353
  const recompute = () => {
2235
2354
  cachedList = Array.from(map.values()).sort((a, b) => a.order - b.order);
2236
2355
  };
@@ -2384,10 +2503,35 @@ var createEditorStore = () => {
2384
2503
  const entry = map.get(editorId);
2385
2504
  if (!entry) return;
2386
2505
  if (entry.status !== "dirty" && entry.status !== "error") return;
2387
- const persistedValue = entry.value;
2506
+ let persistedValue = entry.value;
2388
2507
  const persistedFacetRule = entry.facetRule;
2389
2508
  const { ctx, anchors } = entry.saveSpec;
2390
2509
  const spec = entry.spec;
2510
+ const isCreate = !(entry.recordId && entry.source === "self") || !!spec.createMode;
2511
+ if (hooksBundle?.beforeSave) {
2512
+ try {
2513
+ const hookCtx = {
2514
+ ...buildHookCtxBase(entry),
2515
+ isCreate,
2516
+ record: buildRecordSummary(entry, persistedValue),
2517
+ before: !isCreate ? buildRecordSummary(entry, entry.baseline) : void 0
2518
+ };
2519
+ const result = await hooksBundle.beforeSave(hookCtx);
2520
+ if (result === false) {
2521
+ return;
2522
+ }
2523
+ if (result && typeof result === "object") {
2524
+ persistedValue = persistedValue && typeof persistedValue === "object" ? { ...persistedValue, ...result } : result;
2525
+ }
2526
+ } catch (err) {
2527
+ notifier?.({ kind: "hook-before-save", error: err, recordLabel: entry.label });
2528
+ update(editorId, (e) => {
2529
+ e.status = "error";
2530
+ e.error = err;
2531
+ });
2532
+ return;
2533
+ }
2534
+ }
2391
2535
  update(editorId, (e) => {
2392
2536
  e.status = "saving";
2393
2537
  e.error = void 0;
@@ -2424,6 +2568,9 @@ var createEditorStore = () => {
2424
2568
  nextRecordId = upserted.record?.id ?? nextRecordId;
2425
2569
  }
2426
2570
  update(editorId, (e) => {
2571
+ if (persistedValue !== entry.value) {
2572
+ e.value = cloneDeep(persistedValue);
2573
+ }
2427
2574
  e.baseline = cloneDeep(e.value);
2428
2575
  e.baselineFacetRule = e.facetRule;
2429
2576
  e.recordId = nextRecordId;
@@ -2431,6 +2578,21 @@ var createEditorStore = () => {
2431
2578
  e.status = "saved";
2432
2579
  e.error = void 0;
2433
2580
  });
2581
+ if (hooksBundle?.afterSave) {
2582
+ const refreshed = map.get(editorId);
2583
+ const hookCtx = {
2584
+ ...buildHookCtxBase(refreshed ?? entry),
2585
+ isCreate,
2586
+ record: buildRecordSummary(refreshed ?? entry, persistedValue),
2587
+ before: !isCreate ? buildRecordSummary(entry, entry.baseline) : void 0
2588
+ };
2589
+ try {
2590
+ await hooksBundle.afterSave(hookCtx);
2591
+ } catch (err) {
2592
+ notifier?.({ kind: "hook-after-save", error: err, recordLabel: entry.label });
2593
+ console.warn("[RecordsAdmin] afterSave hook failed", err);
2594
+ }
2595
+ }
2434
2596
  } catch (err) {
2435
2597
  update(editorId, (e) => {
2436
2598
  e.status = "error";
@@ -2452,9 +2614,35 @@ var createEditorStore = () => {
2452
2614
  if (!entry) return;
2453
2615
  if (entry.source !== "self") return;
2454
2616
  if (!entry.recordId) return;
2617
+ if (hooksBundle?.beforeDelete) {
2618
+ try {
2619
+ const hookCtx = {
2620
+ ...buildHookCtxBase(entry),
2621
+ record: buildRecordSummary(entry, entry.value)
2622
+ };
2623
+ const result = await hooksBundle.beforeDelete(hookCtx);
2624
+ if (result === false) return;
2625
+ } catch (err) {
2626
+ notifier?.({ kind: "hook-before-delete", error: err, recordLabel: entry.label });
2627
+ console.warn("[RecordsAdmin] beforeDelete hook cancelled delete", err);
2628
+ return;
2629
+ }
2630
+ }
2631
+ const deletedSnapshot = hooksBundle?.afterDelete ? {
2632
+ ...buildHookCtxBase(entry),
2633
+ record: buildRecordSummary(entry, entry.value)
2634
+ } : null;
2455
2635
  await removeRecord(entry.saveSpec.ctx, entry.recordId);
2456
2636
  map.delete(editorId);
2457
2637
  emit();
2638
+ if (hooksBundle?.afterDelete && deletedSnapshot) {
2639
+ try {
2640
+ await hooksBundle.afterDelete(deletedSnapshot);
2641
+ } catch (err) {
2642
+ notifier?.({ kind: "hook-after-delete", error: err, recordLabel: entry.label });
2643
+ console.warn("[RecordsAdmin] afterDelete hook failed", err);
2644
+ }
2645
+ }
2458
2646
  },
2459
2647
  enforcePoolLimit(max, exceptEditorId) {
2460
2648
  const alive = Array.from(map.values());
@@ -2475,6 +2663,12 @@ var createEditorStore = () => {
2475
2663
  return () => {
2476
2664
  listeners.delete(listener);
2477
2665
  };
2666
+ },
2667
+ setHooks(hooks) {
2668
+ hooksBundle = hooks ?? null;
2669
+ },
2670
+ setNotifier(notify2) {
2671
+ notifier = notify2 ?? null;
2478
2672
  }
2479
2673
  };
2480
2674
  };
@@ -2496,10 +2690,14 @@ var EditorSessionProvider = ({
2496
2690
  ctx,
2497
2691
  children,
2498
2692
  maxOpenEditors = 8,
2499
- defaultValueFactory
2693
+ defaultValueFactory,
2694
+ hooks,
2695
+ onHookNotice
2500
2696
  }) => {
2501
2697
  const storeRef = useRef(null);
2502
2698
  if (!storeRef.current) storeRef.current = createEditorStore();
2699
+ storeRef.current.setHooks(hooks ?? null);
2700
+ storeRef.current.setNotifier(onHookNotice ?? null);
2503
2701
  const currentRef = useRef(void 0);
2504
2702
  const listenersRef = useRef(/* @__PURE__ */ new Set());
2505
2703
  const subscribeCurrent = useCallback((cb) => {
@@ -3873,7 +4071,7 @@ var classify2 = (mode, kind) => {
3873
4071
  return SMART_PUSH.includes(kind) ? "push" : "replace";
3874
4072
  };
3875
4073
  function useDeepLinkState(options) {
3876
- const enabled = !!options?.enabled;
4074
+ const enabled = options?.enabled !== false;
3877
4075
  const history = options?.history ?? "smart";
3878
4076
  const paramNames = useMemo(() => ({
3879
4077
  ...DEFAULT_DEEP_LINK_PARAM_NAMES,
@@ -3903,8 +4101,15 @@ function useDeepLinkState(options) {
3903
4101
  return adapter.subscribe(() => setUrlState(adapter.read()));
3904
4102
  }, [adapter]);
3905
4103
  const emit = useCallback((partial, kind) => {
3906
- if (!adapter) return;
4104
+ if (!adapter) {
4105
+ console.debug("[smartlinks-ui] deep-link emit skipped \u2014 no adapter (deepLink not enabled?)", {
4106
+ partial,
4107
+ kind
4108
+ });
4109
+ return;
4110
+ }
3907
4111
  const mode = classify2(history, kind);
4112
+ console.debug("[smartlinks-ui] deep-link emit", { partial, kind, mode });
3908
4113
  adapter.write(partial, mode);
3909
4114
  setUrlState((prev) => ({ ...prev, ...partial }));
3910
4115
  }, [adapter, history]);
@@ -4100,8 +4305,15 @@ function RecordEditor({
4100
4305
  const DeleteIcon = actionIcons?.delete;
4101
4306
  const showInherited = ctx.source === "inherited";
4102
4307
  const showEmpty = ctx.source === "empty";
4308
+ const hasBreadcrumb = (() => {
4309
+ const s = ctx.scope;
4310
+ return Boolean(s?.facetId || s?.productId || s?.variantId || s?.batchId);
4311
+ })();
4312
+ const hasLeftContent = Boolean(headerLabel) || hasBreadcrumb;
4313
+ const hasRightContent = showInherited || showEmpty || Boolean(headerMeta) || Boolean(bulkActions) || Boolean(targetingControl);
4314
+ const showHeader = hasLeftContent || hasRightContent;
4103
4315
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", children: [
4104
- /* @__PURE__ */ jsxs(
4316
+ showHeader && /* @__PURE__ */ jsxs(
4105
4317
  "header",
4106
4318
  {
4107
4319
  className: "sticky top-0 z-40 px-5 py-3 border-b flex items-start justify-between gap-3",
@@ -4955,14 +5167,16 @@ var PreviewScopePicker = ({
4955
5167
  onChange,
4956
5168
  showVariants,
4957
5169
  showBatches,
5170
+ activeScope,
4958
5171
  i18n
4959
5172
  }) => {
4960
5173
  const productPinned = !!editingScope.productId;
5174
+ const hideForTab = activeScope === "product" || activeScope === "variant" || activeScope === "batch";
4961
5175
  const products = useProductBrowse({
4962
5176
  SL,
4963
5177
  collectionId,
4964
5178
  pageSize: 100,
4965
- enabled: !productPinned
5179
+ enabled: !productPinned && !hideForTab
4966
5180
  });
4967
5181
  const variants = useProductChildren({
4968
5182
  SL,
@@ -4976,11 +5190,8 @@ var PreviewScopePicker = ({
4976
5190
  productId: value.productId,
4977
5191
  kind: showBatches ? "batch" : null
4978
5192
  });
4979
- const isDefault = useMemo(
4980
- () => value.raw === editingScope.raw,
4981
- [value.raw, editingScope.raw]
4982
- );
4983
5193
  useEffect(() => {
5194
+ if (hideForTab) return;
4984
5195
  if (productPinned) return;
4985
5196
  if (value.productId) return;
4986
5197
  const first = products.items[0];
@@ -4992,9 +5203,10 @@ var PreviewScopePicker = ({
4992
5203
  batchId: void 0,
4993
5204
  raw: `product:${first.id}`
4994
5205
  });
4995
- }, [productPinned, value.productId, products.items]);
4996
- const [productPickerOpen, setProductPickerOpen] = useState(false);
4997
- const showProductPicker = !productPinned && products.items.length > 1;
5206
+ }, [hideForTab, productPinned, value.productId, products.items]);
5207
+ const [lightboxOpen, setLightboxOpen] = useState(false);
5208
+ if (hideForTab) return null;
5209
+ const showProductPicker = !productPinned;
4998
5210
  const showVariantPicker = showVariants && !!value.productId && variants.items.length > 0;
4999
5211
  const showBatchPicker = showBatches && !!value.productId && batches.items.length > 0;
5000
5212
  if (!showProductPicker && !showVariantPicker && !showBatchPicker) {
@@ -5005,6 +5217,16 @@ var PreviewScopePicker = ({
5005
5217
  borderColor: "hsl(var(--ra-border))",
5006
5218
  color: "hsl(var(--ra-text))"
5007
5219
  };
5220
+ const pickProduct = (productId) => {
5221
+ onChange({
5222
+ ...value,
5223
+ productId,
5224
+ variantId: void 0,
5225
+ batchId: void 0,
5226
+ raw: `product:${productId}`
5227
+ });
5228
+ setLightboxOpen(false);
5229
+ };
5008
5230
  return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [
5009
5231
  /* @__PURE__ */ jsxs(
5010
5232
  "span",
@@ -5017,56 +5239,27 @@ var PreviewScopePicker = ({
5017
5239
  ]
5018
5240
  }
5019
5241
  ),
5020
- !isDefault && /* @__PURE__ */ jsxs(
5021
- "button",
5022
- {
5023
- type: "button",
5024
- onClick: () => onChange(editingScope),
5025
- className: "text-[10px] px-2 py-1 rounded-md border hover:bg-[hsl(var(--ra-muted))]",
5026
- style: selectStyle,
5027
- children: [
5028
- "\u21BA ",
5029
- i18n?.previewAsDefault ?? "Same as edited"
5030
- ]
5031
- }
5032
- ),
5033
- showProductPicker && !productPickerOpen && currentProductName && /* @__PURE__ */ jsxs(
5034
- "button",
5242
+ showProductPicker && /* @__PURE__ */ jsxs(
5243
+ "span",
5035
5244
  {
5036
- type: "button",
5037
- onClick: () => setProductPickerOpen(true),
5038
- className: "text-[10px] px-2 py-1 rounded-md border hover:bg-[hsl(var(--ra-muted))] inline-flex items-center gap-1 max-w-[12rem] truncate",
5039
- style: selectStyle,
5040
- title: `Preview as ${currentProductName} \u2014 click to change`,
5245
+ className: "inline-flex items-center gap-1.5 text-[11px] truncate max-w-[18rem]",
5246
+ style: { color: "hsl(var(--ra-text))" },
5041
5247
  children: [
5042
- "as ",
5043
- /* @__PURE__ */ jsx("span", { className: "font-medium truncate", children: currentProductName }),
5044
- /* @__PURE__ */ jsx(ChevronDown, { className: "w-3 h-3 opacity-60" })
5248
+ /* @__PURE__ */ jsx("span", { className: "font-medium truncate", title: currentProductName, children: currentProductName || "\u2014" }),
5249
+ /* @__PURE__ */ jsx(
5250
+ "button",
5251
+ {
5252
+ type: "button",
5253
+ onClick: () => setLightboxOpen(true),
5254
+ className: "text-[10px] px-1.5 py-0.5 rounded-md border hover:bg-[hsl(var(--ra-muted))]",
5255
+ style: selectStyle,
5256
+ title: i18n?.change ?? "Change preview product",
5257
+ children: i18n?.change ?? "Change"
5258
+ }
5259
+ )
5045
5260
  ]
5046
5261
  }
5047
5262
  ),
5048
- showProductPicker && productPickerOpen && /* @__PURE__ */ jsx(
5049
- "select",
5050
- {
5051
- className: SELECT_CLS,
5052
- style: selectStyle,
5053
- autoFocus: true,
5054
- onBlur: () => setProductPickerOpen(false),
5055
- value: value.productId ?? "",
5056
- onChange: (e) => {
5057
- const productId = e.target.value || void 0;
5058
- onChange({
5059
- ...value,
5060
- productId,
5061
- variantId: void 0,
5062
- batchId: void 0,
5063
- raw: productId ? `product:${productId}` : ""
5064
- });
5065
- setProductPickerOpen(false);
5066
- },
5067
- children: products.items.map((p) => /* @__PURE__ */ jsx("option", { value: p.id, children: p.name }, p.id))
5068
- }
5069
- ),
5070
5263
  showVariantPicker && /* @__PURE__ */ jsxs(
5071
5264
  "select",
5072
5265
  {
@@ -5107,9 +5300,161 @@ var PreviewScopePicker = ({
5107
5300
  batches.items.map((b) => /* @__PURE__ */ jsx("option", { value: b.id, children: b.name }, b.id))
5108
5301
  ]
5109
5302
  }
5303
+ ),
5304
+ lightboxOpen && /* @__PURE__ */ jsx(
5305
+ ProductPickerLightbox,
5306
+ {
5307
+ SL,
5308
+ collectionId,
5309
+ currentProductId: value.productId,
5310
+ onPick: pickProduct,
5311
+ onClose: () => setLightboxOpen(false),
5312
+ i18n
5313
+ }
5110
5314
  )
5111
5315
  ] });
5112
5316
  };
5317
+ var ProductPickerLightbox = ({
5318
+ SL,
5319
+ collectionId,
5320
+ currentProductId,
5321
+ onPick,
5322
+ onClose,
5323
+ i18n
5324
+ }) => {
5325
+ const [search, setSearch] = useState("");
5326
+ const inputRef = useRef(null);
5327
+ const browse = useProductBrowse({
5328
+ SL,
5329
+ collectionId,
5330
+ search,
5331
+ pageSize: 50,
5332
+ enabled: true
5333
+ });
5334
+ useEffect(() => {
5335
+ const prev = document.body.style.overflow;
5336
+ document.body.style.overflow = "hidden";
5337
+ const t = window.setTimeout(() => inputRef.current?.focus(), 0);
5338
+ const onKey = (e) => {
5339
+ if (e.key === "Escape") {
5340
+ e.stopPropagation();
5341
+ onClose();
5342
+ }
5343
+ };
5344
+ window.addEventListener("keydown", onKey, true);
5345
+ return () => {
5346
+ window.clearTimeout(t);
5347
+ window.removeEventListener("keydown", onKey, true);
5348
+ document.body.style.overflow = prev;
5349
+ };
5350
+ }, [onClose]);
5351
+ if (typeof document === "undefined") return null;
5352
+ return createPortal(
5353
+ /* @__PURE__ */ jsxs(
5354
+ "div",
5355
+ {
5356
+ className: "ra-shell ra-confirm-root",
5357
+ role: "presentation",
5358
+ onMouseDown: (e) => e.stopPropagation(),
5359
+ onClick: (e) => e.stopPropagation(),
5360
+ onTouchStart: (e) => e.stopPropagation(),
5361
+ children: [
5362
+ /* @__PURE__ */ jsx("div", { className: "ra-confirm-backdrop", onClick: onClose }),
5363
+ /* @__PURE__ */ jsxs(
5364
+ "div",
5365
+ {
5366
+ role: "dialog",
5367
+ "aria-modal": "true",
5368
+ "aria-label": i18n?.searchProducts ?? "Search products",
5369
+ className: "ra-confirm-card",
5370
+ style: { width: "min(520px, 100%)", padding: "1rem" },
5371
+ children: [
5372
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mb-3", children: [
5373
+ /* @__PURE__ */ jsx(Search, { className: "w-4 h-4 opacity-60" }),
5374
+ /* @__PURE__ */ jsx(
5375
+ "input",
5376
+ {
5377
+ ref: inputRef,
5378
+ type: "text",
5379
+ value: search,
5380
+ onChange: (e) => setSearch(e.target.value),
5381
+ placeholder: i18n?.searchProducts ?? "Search products\u2026",
5382
+ className: "flex-1 text-sm px-2 py-1.5 rounded-md border bg-transparent focus:outline-none focus:ring-1",
5383
+ style: {
5384
+ borderColor: "hsl(var(--ra-border))",
5385
+ color: "hsl(var(--ra-text))"
5386
+ }
5387
+ }
5388
+ ),
5389
+ /* @__PURE__ */ jsx(
5390
+ "button",
5391
+ {
5392
+ type: "button",
5393
+ onClick: onClose,
5394
+ className: "p-1 rounded-md hover:bg-[hsl(var(--ra-muted))]",
5395
+ "aria-label": i18n?.close ?? "Close",
5396
+ style: { color: "hsl(var(--ra-muted-text))" },
5397
+ children: /* @__PURE__ */ jsx(X, { className: "w-4 h-4" })
5398
+ }
5399
+ )
5400
+ ] }),
5401
+ /* @__PURE__ */ jsxs(
5402
+ "div",
5403
+ {
5404
+ className: "overflow-y-auto rounded-md border",
5405
+ style: {
5406
+ maxHeight: "50vh",
5407
+ borderColor: "hsl(var(--ra-border))"
5408
+ },
5409
+ children: [
5410
+ browse.isLoading && browse.items.length === 0 ? /* @__PURE__ */ jsx("div", { className: "text-xs px-3 py-4 text-center", style: { color: "hsl(var(--ra-muted-text))" }, children: "Loading\u2026" }) : browse.items.length === 0 ? /* @__PURE__ */ jsx("div", { className: "text-xs px-3 py-4 text-center", style: { color: "hsl(var(--ra-muted-text))" }, children: "No products match." }) : /* @__PURE__ */ jsx("ul", { className: "divide-y", style: { borderColor: "hsl(var(--ra-border))" }, children: browse.items.map((p) => {
5411
+ const active = p.id === currentProductId;
5412
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
5413
+ "button",
5414
+ {
5415
+ type: "button",
5416
+ onClick: () => onPick(p.id),
5417
+ className: "w-full text-left px-3 py-2 text-sm hover:bg-[hsl(var(--ra-muted))] flex items-center justify-between gap-3",
5418
+ style: {
5419
+ color: "hsl(var(--ra-text))",
5420
+ background: active ? "hsl(var(--ra-muted))" : "transparent"
5421
+ },
5422
+ children: [
5423
+ /* @__PURE__ */ jsxs("span", { className: "flex flex-col min-w-0", children: [
5424
+ /* @__PURE__ */ jsx("span", { className: "truncate font-medium", children: p.name }),
5425
+ p.sku ? /* @__PURE__ */ jsx("span", { className: "truncate text-[11px]", style: { color: "hsl(var(--ra-muted-text))" }, children: p.sku }) : null
5426
+ ] }),
5427
+ active ? /* @__PURE__ */ jsx("span", { className: "text-[10px] uppercase tracking-wide", style: { color: "hsl(var(--ra-muted-text))" }, children: "current" }) : null
5428
+ ]
5429
+ }
5430
+ ) }, p.id);
5431
+ }) }),
5432
+ browse.hasNextPage && /* @__PURE__ */ jsx("div", { className: "px-3 py-2 border-t", style: { borderColor: "hsl(var(--ra-border))" }, children: /* @__PURE__ */ jsx(
5433
+ "button",
5434
+ {
5435
+ type: "button",
5436
+ onClick: () => browse.fetchNextPage(),
5437
+ disabled: browse.isFetchingNextPage,
5438
+ className: "text-xs w-full py-1 rounded-md border hover:bg-[hsl(var(--ra-muted))]",
5439
+ style: {
5440
+ borderColor: "hsl(var(--ra-border))",
5441
+ color: "hsl(var(--ra-text))"
5442
+ },
5443
+ children: browse.isFetchingNextPage ? "Loading\u2026" : "Load more"
5444
+ }
5445
+ ) })
5446
+ ]
5447
+ }
5448
+ )
5449
+ ]
5450
+ }
5451
+ )
5452
+ ]
5453
+ }
5454
+ ),
5455
+ document.body
5456
+ );
5457
+ };
5113
5458
  var ICONS2 = {
5114
5459
  table: Table,
5115
5460
  cards: LayoutGrid,
@@ -5309,6 +5654,7 @@ function ItemListView({
5309
5654
  error,
5310
5655
  ctx,
5311
5656
  itemNoun,
5657
+ ruleSummary,
5312
5658
  view,
5313
5659
  views,
5314
5660
  onViewChange,
@@ -5406,6 +5752,27 @@ function ItemListView({
5406
5752
  }
5407
5753
  return /* @__PURE__ */ jsxs("div", { className: "ra-item-list", children: [
5408
5754
  toolbar,
5755
+ ruleSummary ? /* @__PURE__ */ jsxs(
5756
+ "div",
5757
+ {
5758
+ className: "ra-item-rule-summary",
5759
+ style: {
5760
+ padding: "6px 12px",
5761
+ fontSize: "11px",
5762
+ color: "hsl(var(--ra-muted-text))",
5763
+ borderBottom: "1px solid hsl(var(--ra-border))",
5764
+ background: "hsl(var(--ra-muted) / 0.4)",
5765
+ display: "flex",
5766
+ alignItems: "center",
5767
+ gap: "6px"
5768
+ },
5769
+ children: [
5770
+ /* @__PURE__ */ jsx("span", { style: { fontWeight: 500, color: "hsl(var(--ra-text))" }, children: "Rule" }),
5771
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
5772
+ /* @__PURE__ */ jsx("span", { children: ruleSummary })
5773
+ ]
5774
+ }
5775
+ ) : null,
5409
5776
  /* @__PURE__ */ jsx("div", { className: "ra-item-list-body", children: body })
5410
5777
  ] });
5411
5778
  }
@@ -6730,6 +7097,19 @@ function RecordsAdminShell(props) {
6730
7097
  {
6731
7098
  ctx,
6732
7099
  defaultValueFactory: props.defaultData,
7100
+ hooks: props.hooks,
7101
+ onHookNotice: (notice) => {
7102
+ console.warn(`[RecordsAdmin] ${notice.kind} hook failed`, notice.error);
7103
+ try {
7104
+ props.onTelemetry?.({
7105
+ type: "record.hook.error",
7106
+ recordType: props.recordType,
7107
+ kind: notice.kind,
7108
+ message: notice.error instanceof Error ? notice.error.message : String(notice.error)
7109
+ });
7110
+ } catch {
7111
+ }
7112
+ },
6733
7113
  children: /* @__PURE__ */ jsx(RecordsAdminShellInner, { ...props })
6734
7114
  }
6735
7115
  );
@@ -6829,6 +7209,18 @@ function RecordsAdminShellInner(props) {
6829
7209
  const i18n = { ...DEFAULT_I18N, ...i18nOverride ?? {} };
6830
7210
  const icons = useMemo(() => mergeIcons(iconsOverride), [iconsOverride]);
6831
7211
  const deepLinkState = useDeepLinkState(deepLink);
7212
+ const deepLinkLoggedRef = useRef(false);
7213
+ if (!deepLinkLoggedRef.current) {
7214
+ deepLinkLoggedRef.current = true;
7215
+ console.debug("[RecordsAdminShell] deep-link config", {
7216
+ enabled: deepLinkState.enabled,
7217
+ hostPassedOption: !!deepLink,
7218
+ // Resolved adapter mode: 'host' = host-supplied, 'default' = built-in
7219
+ // (window.location standalone, postMessage inside the SmartLinks iframe).
7220
+ adapterMode: deepLink?.adapter ? "host" : "default",
7221
+ paramNames: deepLinkState.paramNames
7222
+ });
7223
+ }
6832
7224
  const multiOpenWarnedRef = useRef(false);
6833
7225
  if (editorTabs === "multi" && !multiOpenWarnedRef.current) {
6834
7226
  multiOpenWarnedRef.current = true;
@@ -7059,7 +7451,18 @@ function RecordsAdminShellInner(props) {
7059
7451
  setSelectedVariantId,
7060
7452
  selectedBatchId,
7061
7453
  setSelectedBatchId,
7062
- setFacetBrowseFilter
7454
+ setFacetBrowseFilter,
7455
+ // Pending-recordId resolution — without these the URL's `recordId` is
7456
+ // parsed but never opens the editor on refresh / share-link load.
7457
+ isCollection,
7458
+ setSelectedItemId,
7459
+ collectionItems: {
7460
+ items: collectionItems.items,
7461
+ isLoading: collectionItems.isLoading
7462
+ },
7463
+ recordListItems: recordList.items,
7464
+ recordListLoading: recordList.isLoading,
7465
+ skipNextItemResetRef
7063
7466
  });
7064
7467
  const supportedForResolution = useMemo(() => requestedScopes, [requestedScopes]);
7065
7468
  const resolved = useResolvedRecord({
@@ -7290,7 +7693,8 @@ function RecordsAdminShellInner(props) {
7290
7693
  deepLinkState,
7291
7694
  onTelemetry,
7292
7695
  onBeforeDelete,
7293
- generateItemId
7696
+ generateItemId,
7697
+ hooks: props.hooks
7294
7698
  });
7295
7699
  const renderEditorWithPreview = () => {
7296
7700
  if (!editingTargetScope) return null;
@@ -7305,6 +7709,7 @@ function RecordsAdminShellInner(props) {
7305
7709
  onChange: setPreviewScope,
7306
7710
  showVariants: drillVariantsAllowed,
7307
7711
  showBatches: drillBatchesAllowed,
7712
+ activeScope,
7308
7713
  i18n: { previewAs: i18n.previewAs, previewAsDefault: i18n.previewAsDefault }
7309
7714
  }
7310
7715
  ) : null;
@@ -7497,12 +7902,13 @@ function RecordsAdminShellInner(props) {
7497
7902
  if (groupBy) return groupBy;
7498
7903
  if (isAllTab) return void 0;
7499
7904
  if (!isRuleTab) return void 0;
7905
+ if (isCollection) return void 0;
7500
7906
  return (record) => {
7501
7907
  const hash = ruleHash(record.facetRule);
7502
7908
  if (!hash) return null;
7503
7909
  return { key: `rule:${hash}`, label: summariseRule(record.facetRule) };
7504
7910
  };
7505
- }, [groupBy, isRuleTab, isAllTab]);
7911
+ }, [groupBy, isRuleTab, isAllTab, isCollection]);
7506
7912
  const [bulkRuleEditTarget, setBulkRuleEditTarget] = useState(null);
7507
7913
  const ruleCatalogue = useMemo(() => {
7508
7914
  const buckets = /* @__PURE__ */ new Map();
@@ -7601,6 +8007,32 @@ function RecordsAdminShellInner(props) {
7601
8007
  () => isRuleTab ? applyRuleFilters(recordList.items, ruleFilters) : recordList.items,
7602
8008
  [isRuleTab, recordList.items, ruleFilters]
7603
8009
  );
8010
+ const collectionRuleRailItems = useMemo(() => {
8011
+ if (!isRuleTab || !isCollection) return filteredRuleItems;
8012
+ const buckets = /* @__PURE__ */ new Map();
8013
+ for (const item of filteredRuleItems) {
8014
+ const hash = ruleHash(item.facetRule);
8015
+ if (!hash) continue;
8016
+ const existing = buckets.get(hash);
8017
+ if (existing) {
8018
+ existing.count += 1;
8019
+ } else {
8020
+ buckets.set(hash, { rep: item, count: 1 });
8021
+ }
8022
+ }
8023
+ return Array.from(buckets.values()).map(({ rep, count }) => ({
8024
+ ...rep,
8025
+ label: summariseRule(rep.facetRule),
8026
+ subtitle: `${count} ${itemNoun}${count === 1 ? "" : "s"}`
8027
+ }));
8028
+ }, [isRuleTab, isCollection, filteredRuleItems, itemNoun]);
8029
+ const activeRuleSummary = useMemo(() => {
8030
+ if (!isRuleTab || !isCollection) return null;
8031
+ if (!selectedRecordId || isDraftId3(selectedRecordId)) return null;
8032
+ const hit = recordList.items.find((it) => it.id === selectedRecordId);
8033
+ if (!hit?.facetRule) return null;
8034
+ return summariseRule(hit.facetRule);
8035
+ }, [isRuleTab, isCollection, selectedRecordId, recordList.items]);
7604
8036
  const applyFacetBrowseFilter = useCallback(
7605
8037
  (items2) => {
7606
8038
  if (!facetBrowseFilter) return items2;
@@ -7648,7 +8080,9 @@ function RecordsAdminShellInner(props) {
7648
8080
  }),
7649
8081
  [i18n.itemsAllLabel, collectionItems.items.length, itemNoun]
7650
8082
  );
7651
- const leftItems = isProductTab ? productListItems : isRuleTab ? applyFacetBrowseFilter(filteredRuleItems) : (isGlobalTab || isAllTab) && isCollection ? [collectionGlobalAllRow] : isRecordsTab ? applyFacetBrowseFilter(recordList.items) : [];
8083
+ const leftItems = isProductTab ? productListItems : isRuleTab ? applyFacetBrowseFilter(
8084
+ isCollection ? collectionRuleRailItems : filteredRuleItems
8085
+ ) : (isGlobalTab || isAllTab) && isCollection ? [collectionGlobalAllRow] : isRecordsTab ? applyFacetBrowseFilter(recordList.items) : [];
7652
8086
  const leftLoading = isProductTab ? !productPinned && productBrowse.isLoading : isRecordsTab ? recordList.isLoading || probe.isLoading : false;
7653
8087
  const leftError = isProductTab ? productBrowse.error : isRecordsTab ? recordList.error : null;
7654
8088
  const leftSelectedId = isProductTab ? void 0 : selectedRecordId && selectedRecordId !== DRAFT_ID3 ? selectedRecordId : void 0;
@@ -8089,6 +8523,7 @@ function RecordsAdminShellInner(props) {
8089
8523
  renderItemEmpty,
8090
8524
  itemColumns,
8091
8525
  cardSize: itemCardSize,
8526
+ ruleSummary: activeRuleSummary,
8092
8527
  i18n
8093
8528
  }
8094
8529
  ),
@@ -9008,6 +9443,9 @@ function useMergedRecord(args) {
9008
9443
  };
9009
9444
  }
9010
9445
 
9446
+ // src/components/RecordsAdmin/index.ts
9447
+ assertComponentStylesLoaded("records-admin");
9448
+
9011
9449
  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 };
9012
9450
  //# sourceMappingURL=index.js.map
9013
9451
  //# sourceMappingURL=index.js.map