@proveanything/smartlinks-utils-ui 0.9.12 → 0.10.0

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.
@@ -6,7 +6,7 @@ import { cn } from '../../chunk-L7FQ52F5.js';
6
6
  import { parsedRefToTarget, parsedRefToScope, matchRecords, scopesEqual, getRecordById, listRecords, upsertRecord, updateRecord, createRecord, removeRecord } from '../../chunk-KA4MKRHL.js';
7
7
  export { bulkDelete, bulkUpsert, createRecord, getRecordById, listRecords, matchRecords, parsedRefToScope, parsedRefToTarget, removeRecord, restoreRecord, scopesEqual, upsertRecord } from '../../chunk-KA4MKRHL.js';
8
8
  import { createContext, useState, useEffect, useCallback, useMemo, useRef, useContext, useSyncExternalStore, useLayoutEffect, createElement, useId } from '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
+ 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, Target, Rows3, ChevronRight, Eraser, ClipboardPaste, Box, X, Search, Image, Table, ArrowLeft, ChevronLeft, AlertTriangle, Info, HelpCircle, CornerDownLeft, Circle, CopyPlus, AlertCircle, Undo2, Save, Loader2, XCircle, ArrowRight, BookOpen, Globe2, Check, Settings2 } from 'lucide-react';
10
10
  import { useQuery, useQueryClient, useInfiniteQuery } from '@tanstack/react-query';
11
11
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
12
12
  import { createPortal } from 'react-dom';
@@ -142,6 +142,8 @@ var DEFAULT_I18N = {
142
142
  pasteConfirmCancel: "Cancel",
143
143
  pasteWarnTitle: "Cross-scope paste",
144
144
  pasteWarnContinue: "Continue",
145
+ duplicateAction: "Duplicate",
146
+ duplicateToast: "Duplicated {name}. Review and Save.",
145
147
  itemsAllLabel: "All items",
146
148
  subtitleEmpty: "Not set",
147
149
  subtitleConfigured: "Configured",
@@ -1549,7 +1551,8 @@ function useShellClipboard(args) {
1549
1551
  onTelemetry,
1550
1552
  onCopyOverride,
1551
1553
  onPasteOverride,
1552
- onLeftSelectRef
1554
+ onLeftSelectRef,
1555
+ onCreateItemDraftRef
1553
1556
  } = args;
1554
1557
  const clipboard = useRecordClipboard({
1555
1558
  appId,
@@ -1681,8 +1684,7 @@ function useShellClipboard(args) {
1681
1684
  const rowClipboard = enabled ? (record) => {
1682
1685
  const summaryHasData = record.data != null;
1683
1686
  const sourceParsed = record.scope;
1684
- const compat = clipboard.entry ? checkPasteCompatibility(clipboard.entry.sourceScope, sourceParsed) : null;
1685
- const sameTarget = clipboard.entry ? clipboard.entry.sourceRecordId && record.id ? clipboard.entry.sourceRecordId === record.id : clipboard.entry.sourceScope.raw === anchorKey(record.scope) : false;
1687
+ const canDuplicate = summaryHasData && isCollection;
1686
1688
  return {
1687
1689
  onCopy: summaryHasData ? () => {
1688
1690
  const value = onCopyOverride ? onCopyOverride({ value: record.data, scope: sourceParsed }) : cloneValue(record.data);
@@ -1698,15 +1700,27 @@ function useShellClipboard(args) {
1698
1700
  variant: "copy"
1699
1701
  });
1700
1702
  } : void 0,
1701
- onPaste: () => {
1703
+ onDuplicate: canDuplicate ? () => {
1704
+ const value = onCopyOverride ? onCopyOverride({ value: record.data, scope: sourceParsed }) : cloneValue(record.data);
1705
+ clipboard.set({
1706
+ value,
1707
+ sourceScope: sourceParsed,
1708
+ sourceRecordId: record.id ?? void 0,
1709
+ sourceLabel: record.label
1710
+ });
1711
+ onTelemetry?.({ type: "clipboard.copy", recordType, sourceRef: anchorKey(record.scope) });
1702
1712
  onLeftSelectRef.current?.(record);
1703
- setPendingPasteTarget(
1704
- record.id ? { kind: "record", recordId: record.id } : { kind: "anchor", ref: anchorKey(record.scope) }
1705
- );
1706
- },
1707
- canPaste: !!clipboard.entry && !sameTarget && compat?.status !== "denied",
1708
- pasteWillReplace: record.status === "configured",
1709
- clipboardSourceLabel: clipboard.entry?.sourceLabel
1713
+ const create = onCreateItemDraftRef?.current;
1714
+ if (!create) return;
1715
+ window.setTimeout(() => {
1716
+ const newId = create();
1717
+ if (newId) setPendingPasteTarget({ kind: "record", recordId: newId });
1718
+ }, 0);
1719
+ setNotice({
1720
+ message: i18n.duplicateToast.replace("{name}", record.label),
1721
+ variant: "copy"
1722
+ });
1723
+ } : void 0
1710
1724
  };
1711
1725
  } : void 0;
1712
1726
  const toast = notice ? /* @__PURE__ */ jsx(
@@ -1860,14 +1874,17 @@ function useShellNavigation(args) {
1860
1874
  lastAppliedDLRef
1861
1875
  ]);
1862
1876
  const onItemCreate = useCallback(() => {
1863
- if (!isCollection) return;
1877
+ if (!isCollection) return void 0;
1878
+ let createdId;
1864
1879
  void runWithGuard(() => {
1865
1880
  const id = coerceDraftItemId(generateItemId);
1881
+ createdId = id;
1866
1882
  setSelectedItemId(id);
1867
1883
  onTelemetry?.({ type: "item.create", recordType, scopeRef: baseScopeRef });
1868
1884
  deepLinkState.emit({ recordId: null, scope: baseScopeRef || null }, "record.open");
1869
1885
  stampEcho(null, baseScopeRef || null);
1870
1886
  });
1887
+ return createdId;
1871
1888
  }, [
1872
1889
  isCollection,
1873
1890
  runWithGuard,
@@ -1951,7 +1968,9 @@ function useShellNavigation(args) {
1951
1968
  ]);
1952
1969
  const itemViewCtx = useMemo(() => ({
1953
1970
  onOpen: onItemOpen,
1954
- onCreate: onItemCreate,
1971
+ onCreate: () => {
1972
+ onItemCreate();
1973
+ },
1955
1974
  onDelete: (id) => {
1956
1975
  void onItemDelete(id);
1957
1976
  },
@@ -2108,23 +2127,12 @@ var useShellDeepLink = (args) => {
2108
2127
  const [pendingDeepLinkRecordId, setPendingDeepLinkRecordId] = useState(null);
2109
2128
  const preserveInitialRecordIdRef = useRef(!!deepLinkState.urlState.recordId);
2110
2129
  const warnedMissingRef = useRef(/* @__PURE__ */ new Set());
2111
- const bootLoggedRef = useRef(false);
2112
- if (!bootLoggedRef.current) {
2113
- bootLoggedRef.current = true;
2114
- console.info("[RecordsAdminShell] deep-link boot snapshot", {
2115
- enabled: deepLinkState.enabled,
2116
- urlStateRecordId: deepLinkState.urlState.recordId ?? null,
2117
- urlStateScope: deepLinkState.urlState.scope ?? null,
2118
- urlStateView: deepLinkState.urlState.view ?? null,
2119
- urlStateRaw: JSON.stringify(deepLinkState.urlState),
2120
- paramNames: deepLinkState.paramNames,
2121
- windowHash: typeof window !== "undefined" ? window.location.hash : null,
2122
- windowSearch: typeof window !== "undefined" ? window.location.search : null
2123
- });
2124
- }
2125
2130
  useEffect(() => {
2126
2131
  if (!deepLinkState.enabled) return;
2127
2132
  if (selectedItemId) return;
2133
+ if (lastAppliedDLRef.current === "" && (deepLinkState.urlState.recordId ?? null) !== null) {
2134
+ return;
2135
+ }
2128
2136
  const echoView = deepLinkState.urlState.view ?? "";
2129
2137
  const currentDeepLinkedRecordId = pendingDeepLinkRecordId ?? deepLinkState.urlState.recordId ?? null;
2130
2138
  const preservingPendingRecordId = preserveInitialRecordIdRef.current && !!currentDeepLinkedRecordId;
@@ -2137,14 +2145,6 @@ var useShellDeepLink = (args) => {
2137
2145
  useEffect(() => {
2138
2146
  const { recordId, scope, view } = deepLinkState.urlState;
2139
2147
  const sig = `${recordId ?? ""}|${scope ?? ""}|${view ?? ""}`;
2140
- console.info("[RecordsAdminShell] restore effect tick", {
2141
- enabled: deepLinkState.enabled,
2142
- recordId,
2143
- scope,
2144
- view,
2145
- sig,
2146
- lastApplied: lastAppliedDLRef.current
2147
- });
2148
2148
  if (!deepLinkState.enabled) return;
2149
2149
  if (sig === lastAppliedDLRef.current) return;
2150
2150
  lastAppliedDLRef.current = sig;
@@ -2171,31 +2171,9 @@ var useShellDeepLink = (args) => {
2171
2171
  }
2172
2172
  setPendingDeepLinkRecordId(recordId ?? null);
2173
2173
  if (recordId) preserveInitialRecordIdRef.current = true;
2174
- if (recordId) {
2175
- console.info("[RecordsAdminShell] deep-link restore \u2014 pending recordId set", {
2176
- recordId,
2177
- scope: scope ?? null,
2178
- view: view ?? null
2179
- });
2180
- }
2181
- if (!recordId && selectedItemId !== null) {
2182
- console.info("[RecordsAdminShell] preserving selected item during restore without recordId", {
2183
- selectedItemId,
2184
- scope,
2185
- view
2186
- });
2187
- }
2188
2174
  }, [deepLinkState.enabled, deepLinkState.urlState]);
2189
2175
  useEffect(() => {
2190
2176
  const pending = pendingDeepLinkRecordId;
2191
- console.info("[RecordsAdminShell] resolver tick", {
2192
- pending,
2193
- isCollection,
2194
- collectionItemsCount: collectionItems.items.length,
2195
- collectionItemsLoading: collectionItems.isLoading,
2196
- recordListCount: recordListItems.length,
2197
- recordListLoading
2198
- });
2199
2177
  if (!pending) return;
2200
2178
  if (isCollection) {
2201
2179
  const hit2 = collectionItems.items.find((it) => it.id === pending || it.itemId === pending);
@@ -3279,10 +3257,8 @@ var statusToneLabel = (tone) => {
3279
3257
  };
3280
3258
  var RowContextMenu = ({
3281
3259
  onCopy,
3282
- onPaste,
3283
- canPaste,
3284
- pasteWillReplace,
3285
- pasteSourceLabel,
3260
+ onDuplicate,
3261
+ actions,
3286
3262
  i18n
3287
3263
  }) => {
3288
3264
  const [open, setOpen] = useState(false);
@@ -3331,8 +3307,9 @@ var RowContextMenu = ({
3331
3307
  window.removeEventListener("scroll", update, true);
3332
3308
  };
3333
3309
  }, [open]);
3334
- if (!onCopy && !onPaste) return null;
3335
- const pasteLabel = !canPaste ? i18n.clipboardEmpty : pasteSourceLabel ? i18n.pasteFrom.replace("{name}", pasteSourceLabel) : pasteWillReplace ? i18n.pasteReplace : i18n.paste;
3310
+ const hasActions = (actions?.length ?? 0) > 0;
3311
+ if (!onCopy && !onDuplicate && !hasActions) return null;
3312
+ const showDivider = (onCopy || onDuplicate) && hasActions;
3336
3313
  return /* @__PURE__ */ jsxs("div", { ref: wrapperRef, className: "ra-row-menu-wrap relative", children: [
3337
3314
  /* @__PURE__ */ jsx(
3338
3315
  "button",
@@ -3378,24 +3355,49 @@ var RowContextMenu = ({
3378
3355
  ]
3379
3356
  }
3380
3357
  ),
3381
- onPaste && /* @__PURE__ */ jsxs(
3358
+ onDuplicate && /* @__PURE__ */ jsxs(
3382
3359
  "button",
3383
3360
  {
3384
3361
  type: "button",
3385
3362
  role: "menuitem",
3386
3363
  className: "ra-row-menu-item",
3387
- disabled: !canPaste,
3388
3364
  onClick: (e) => {
3389
3365
  e.stopPropagation();
3390
3366
  setOpen(false);
3391
- if (canPaste) onPaste();
3367
+ onDuplicate();
3392
3368
  },
3393
3369
  children: [
3394
- /* @__PURE__ */ jsx(ClipboardPaste, { className: "w-3 h-3", "aria-hidden": "true" }),
3395
- /* @__PURE__ */ jsx("span", { className: "truncate", children: pasteLabel })
3370
+ /* @__PURE__ */ jsx(CopyPlus, { className: "w-3 h-3", "aria-hidden": "true" }),
3371
+ /* @__PURE__ */ jsx("span", { children: i18n.duplicateAction })
3396
3372
  ]
3397
3373
  }
3398
- )
3374
+ ),
3375
+ showDivider && /* @__PURE__ */ jsx("div", { className: "ra-row-menu-divider", role: "separator" }),
3376
+ hasActions && actions.map((action) => {
3377
+ const Icon = action.icon;
3378
+ return /* @__PURE__ */ jsxs(
3379
+ "button",
3380
+ {
3381
+ type: "button",
3382
+ role: "menuitem",
3383
+ className: "ra-row-menu-item",
3384
+ "data-tone": action.variant === "danger" ? "danger" : void 0,
3385
+ disabled: action.disabled,
3386
+ title: action.disabled ? action.disabledReason : void 0,
3387
+ onClick: (e) => {
3388
+ e.stopPropagation();
3389
+ if (action.disabled) return;
3390
+ setOpen(false);
3391
+ void action.onAction();
3392
+ },
3393
+ children: [
3394
+ Icon && /* @__PURE__ */ jsx(Icon, { className: "w-3 h-3" }),
3395
+ /* @__PURE__ */ jsx("span", { children: action.label })
3396
+ ]
3397
+ },
3398
+ action.key
3399
+ );
3400
+ })
3399
3401
  ]
3400
3402
  }
3401
3403
  ),
@@ -3444,7 +3446,7 @@ var RuleLabelLookupProvider = ({
3444
3446
  return /* @__PURE__ */ jsx(RuleLabelLookupContext.Provider, { value: v, children });
3445
3447
  };
3446
3448
  var DefaultRecordRow = ({ record, ctx, compact = false }) => {
3447
- const { selected, onSelect, isDirty, hasError, onCopy, onPaste, canPaste, pasteWillReplace, clipboardSourceLabel } = ctx;
3449
+ const { selected, onSelect, isDirty, hasError, onCopy, onDuplicate, actions } = ctx;
3448
3450
  const ruleLabelLookup = useRuleLabelLookup();
3449
3451
  const ScopeIcon = record.scope.kind && record.scope.kind !== "collection" ? DEFAULT_ICONS.scope[record.scope.kind] : DEFAULT_ICONS.scope.product;
3450
3452
  const tone = resolveTone(void 0, record.status);
@@ -3497,7 +3499,7 @@ var DefaultRecordRow = ({ record, ctx, compact = false }) => {
3497
3499
  className: "ra-row-rule-pip",
3498
3500
  title: ruleSummary ? `Targeted: ${ruleSummary}` : "This record has targeting rules",
3499
3501
  "aria-label": ruleSummary ? `Targeted: ${ruleSummary}` : "This record has targeting rules",
3500
- children: /* @__PURE__ */ jsx(SlidersHorizontal, { className: "w-3 h-3", "aria-hidden": "true" })
3502
+ children: /* @__PURE__ */ jsx(Target, { className: "w-3 h-3", "aria-hidden": "true" })
3501
3503
  }
3502
3504
  ),
3503
3505
  hasError ? /* @__PURE__ */ jsx(
@@ -3516,20 +3518,15 @@ var DefaultRecordRow = ({ record, ctx, compact = false }) => {
3516
3518
  }
3517
3519
  )
3518
3520
  ] }),
3519
- (onCopy || onPaste) && /* @__PURE__ */ jsx(
3521
+ (onCopy || onDuplicate || actions && actions.length > 0) && /* @__PURE__ */ jsx(
3520
3522
  RowContextMenu,
3521
3523
  {
3522
3524
  onCopy,
3523
- onPaste,
3524
- canPaste,
3525
- pasteWillReplace,
3526
- pasteSourceLabel: clipboardSourceLabel,
3525
+ onDuplicate,
3526
+ actions,
3527
3527
  i18n: {
3528
3528
  copy: DEFAULT_I18N.copy,
3529
- paste: DEFAULT_I18N.paste,
3530
- pasteFrom: DEFAULT_I18N.pasteFrom,
3531
- pasteReplace: DEFAULT_I18N.pasteReplace,
3532
- clipboardEmpty: DEFAULT_I18N.clipboardEmpty
3529
+ duplicateAction: DEFAULT_I18N.duplicateAction
3533
3530
  }
3534
3531
  }
3535
3532
  )
@@ -3551,10 +3548,37 @@ var RecordList = ({
3551
3548
  groupBy,
3552
3549
  renderGroupActions,
3553
3550
  rowClipboard,
3551
+ rowActions,
3554
3552
  i18n
3555
3553
  }) => {
3554
+ const containerRef = useRef(null);
3555
+ const onKeyDown = useCallback((e) => {
3556
+ const key = e.key;
3557
+ if (key !== "ArrowDown" && key !== "ArrowUp" && key !== "Home" && key !== "End") {
3558
+ return;
3559
+ }
3560
+ const root = containerRef.current;
3561
+ if (!root) return;
3562
+ const buttons = Array.from(
3563
+ root.querySelectorAll("button.ra-row-hit")
3564
+ ).filter((btn) => !btn.hasAttribute("disabled") && btn.offsetParent !== null);
3565
+ if (buttons.length === 0) return;
3566
+ const active = document.activeElement;
3567
+ const currentIdx = active ? buttons.indexOf(active) : -1;
3568
+ let nextIdx = currentIdx;
3569
+ if (key === "ArrowDown") nextIdx = currentIdx < 0 ? 0 : Math.min(currentIdx + 1, buttons.length - 1);
3570
+ else if (key === "ArrowUp") nextIdx = currentIdx <= 0 ? 0 : currentIdx - 1;
3571
+ else if (key === "Home") nextIdx = 0;
3572
+ else if (key === "End") nextIdx = buttons.length - 1;
3573
+ if (nextIdx !== currentIdx && buttons[nextIdx]) {
3574
+ e.preventDefault();
3575
+ buttons[nextIdx].focus();
3576
+ buttons[nextIdx].scrollIntoView({ block: "nearest" });
3577
+ }
3578
+ }, []);
3556
3579
  const buildCtx = (item) => {
3557
3580
  const cb = rowClipboard ? rowClipboard(item) : null;
3581
+ const extraActions = rowActions ? rowActions(item) ?? void 0 : void 0;
3558
3582
  const itemAnchorKey = anchorKey(item.scope);
3559
3583
  const idMatch = selectedId != null && item.id != null && item.id === selectedId;
3560
3584
  const anchorMatch = selectedAnchorKey != null && !!itemAnchorKey && itemAnchorKey === selectedAnchorKey;
@@ -3569,7 +3593,8 @@ var RecordList = ({
3569
3593
  isDirty: dirtyIdMatch || dirtyAnchorMatch || idInStore || anchorInStore,
3570
3594
  hasError: errorInStore,
3571
3595
  i18n,
3572
- ...cb ?? {}
3596
+ ...cb ?? {},
3597
+ actions: extraActions && extraActions.length > 0 ? extraActions : void 0
3573
3598
  };
3574
3599
  };
3575
3600
  const groups = useMemo(() => {
@@ -3596,15 +3621,33 @@ var RecordList = ({
3596
3621
  };
3597
3622
  if (groups) {
3598
3623
  return /* @__PURE__ */ jsx(
3599
- GroupedList,
3624
+ "div",
3600
3625
  {
3601
- groups,
3602
- renderItems,
3603
- renderGroupActions
3626
+ ref: containerRef,
3627
+ onKeyDown,
3628
+ role: "listbox",
3629
+ "aria-label": "Records",
3630
+ children: /* @__PURE__ */ jsx(
3631
+ GroupedList,
3632
+ {
3633
+ groups,
3634
+ renderItems,
3635
+ renderGroupActions
3636
+ }
3637
+ )
3604
3638
  }
3605
3639
  );
3606
3640
  }
3607
- return renderItems(items);
3641
+ return /* @__PURE__ */ jsx(
3642
+ "div",
3643
+ {
3644
+ ref: containerRef,
3645
+ onKeyDown,
3646
+ role: "listbox",
3647
+ "aria-label": "Records",
3648
+ children: renderItems(items)
3649
+ }
3650
+ );
3608
3651
  };
3609
3652
  var GroupedList = ({
3610
3653
  groups,
@@ -5515,6 +5558,8 @@ function DefaultItemTable({
5515
5558
  selectedId,
5516
5559
  onOpen,
5517
5560
  onDelete,
5561
+ rowActions,
5562
+ rowClipboard,
5518
5563
  i18n
5519
5564
  }) {
5520
5565
  const cols = columns ?? [];
@@ -5552,6 +5597,31 @@ function DefaultItemTable({
5552
5597
  /* @__PURE__ */ jsx("td", { className: "ra-item-row-meta", children: formatDate(item.updatedAt) })
5553
5598
  ] }) : cols.map((c) => /* @__PURE__ */ jsx("td", { style: { textAlign: c.align ?? "left" }, children: c.render(item) }, c.key)),
5554
5599
  /* @__PURE__ */ jsxs("td", { className: "ra-item-row-actions", children: [
5600
+ (() => {
5601
+ const extra = rowActions ? rowActions(item) ?? void 0 : void 0;
5602
+ const cb = rowClipboard ? rowClipboard(item) : null;
5603
+ const hasExtras = !!(extra && extra.length > 0);
5604
+ if (!cb?.onCopy && !cb?.onDuplicate && !hasExtras) return null;
5605
+ return /* @__PURE__ */ jsx(
5606
+ "span",
5607
+ {
5608
+ onClick: (e) => e.stopPropagation(),
5609
+ onMouseDown: (e) => e.stopPropagation(),
5610
+ children: /* @__PURE__ */ jsx(
5611
+ RowContextMenu,
5612
+ {
5613
+ onCopy: cb?.onCopy,
5614
+ onDuplicate: cb?.onDuplicate,
5615
+ actions: extra ?? void 0,
5616
+ i18n: {
5617
+ copy: DEFAULT_I18N.copy,
5618
+ duplicateAction: DEFAULT_I18N.duplicateAction
5619
+ }
5620
+ }
5621
+ )
5622
+ }
5623
+ );
5624
+ })(),
5555
5625
  /* @__PURE__ */ jsx(
5556
5626
  "button",
5557
5627
  {
@@ -5595,6 +5665,8 @@ function DefaultItemCards({
5595
5665
  ctx,
5596
5666
  renderCard,
5597
5667
  cardSize = "md",
5668
+ rowActions,
5669
+ rowClipboard,
5598
5670
  i18n
5599
5671
  }) {
5600
5672
  const isGallery = variant === "gallery";
@@ -5607,10 +5679,13 @@ function DefaultItemCards({
5607
5679
  const id = item.itemId ?? "";
5608
5680
  const key = item.id ?? (id || anchorKey(item.scope) || `pos:${idx}`);
5609
5681
  const open = () => ctx.onOpen(id);
5682
+ const extraActions = rowActions ? rowActions(item) ?? void 0 : void 0;
5683
+ const cb = rowClipboard ? rowClipboard(item) : null;
5610
5684
  const slotCtx = {
5611
5685
  ...ctx,
5612
5686
  selected: selectedId === id,
5613
- onSelect: open
5687
+ onSelect: open,
5688
+ actions: extraActions && extraActions.length > 0 ? extraActions : void 0
5614
5689
  };
5615
5690
  if (renderCard) {
5616
5691
  return /* @__PURE__ */ jsx(
@@ -5637,6 +5712,26 @@ function DefaultItemCards({
5637
5712
  /* @__PURE__ */ jsx("div", { className: "ra-item-card-title", children: item.label }),
5638
5713
  item.subtitle && /* @__PURE__ */ jsx("div", { className: "ra-item-card-sub", children: item.subtitle })
5639
5714
  ] }),
5715
+ (cb?.onCopy || cb?.onDuplicate || extraActions && extraActions.length > 0) && /* @__PURE__ */ jsx(
5716
+ "span",
5717
+ {
5718
+ className: "ra-item-card-menu",
5719
+ onClick: (e) => e.stopPropagation(),
5720
+ onMouseDown: (e) => e.stopPropagation(),
5721
+ children: /* @__PURE__ */ jsx(
5722
+ RowContextMenu,
5723
+ {
5724
+ onCopy: cb?.onCopy,
5725
+ onDuplicate: cb?.onDuplicate,
5726
+ actions: extraActions,
5727
+ i18n: {
5728
+ copy: DEFAULT_I18N.copy,
5729
+ duplicateAction: DEFAULT_I18N.duplicateAction
5730
+ }
5731
+ }
5732
+ )
5733
+ }
5734
+ ),
5640
5735
  /* @__PURE__ */ jsx(
5641
5736
  "button",
5642
5737
  {
@@ -5674,6 +5769,8 @@ function ItemListView({
5674
5769
  renderItemEmpty,
5675
5770
  itemColumns,
5676
5771
  cardSize = "md",
5772
+ rowActions,
5773
+ rowClipboard,
5677
5774
  i18n
5678
5775
  }) {
5679
5776
  const newLabel = i18n.newItem.includes("{noun}") ? i18n.newItem.replace("{noun}", itemNoun) : i18n.newItem;
@@ -5744,6 +5841,8 @@ function ItemListView({
5744
5841
  selectedId: ctx.selectedId,
5745
5842
  onOpen: ctx.onOpen,
5746
5843
  onDelete: ctx.onDelete,
5844
+ rowActions,
5845
+ rowClipboard,
5747
5846
  i18n
5748
5847
  }
5749
5848
  );
@@ -5757,6 +5856,8 @@ function ItemListView({
5757
5856
  ctx,
5758
5857
  renderCard: renderItemCard,
5759
5858
  cardSize,
5859
+ rowActions,
5860
+ rowClipboard,
5760
5861
  i18n
5761
5862
  }
5762
5863
  );
@@ -5887,7 +5988,7 @@ function SiblingRail({
5887
5988
  className: "ra-row-rule-pip",
5888
5989
  title: ruleSummary ? `Targeted: ${ruleSummary}` : "This item has targeting rules",
5889
5990
  "aria-label": ruleSummary ? `Targeted: ${ruleSummary}` : "This item has targeting rules",
5890
- children: /* @__PURE__ */ jsx(SlidersHorizontal, { className: "w-3 h-3", "aria-hidden": "true" })
5991
+ children: /* @__PURE__ */ jsx(Target, { className: "w-3 h-3", "aria-hidden": "true" })
5891
5992
  }
5892
5993
  ),
5893
5994
  hasError ? /* @__PURE__ */ jsx(
@@ -7141,6 +7242,7 @@ function RecordsAdminShellInner(props) {
7141
7242
  unsaved,
7142
7243
  clipboard,
7143
7244
  actions,
7245
+ recordActions,
7144
7246
  icons: iconsOverride,
7145
7247
  // Deep linking
7146
7248
  deepLink
@@ -7597,6 +7699,7 @@ function RecordsAdminShellInner(props) {
7597
7699
  i18n
7598
7700
  ]);
7599
7701
  const onLeftSelectRef = useRef(null);
7702
+ const onCreateItemDraftRef = useRef(null);
7600
7703
  const { runWithGuard } = useDirtyNavigation({
7601
7704
  strategy: dirtyStrategy,
7602
7705
  isDirty: editorCtx.isDirty,
@@ -7651,19 +7754,37 @@ function RecordsAdminShellInner(props) {
7651
7754
  onTelemetry,
7652
7755
  onCopyOverride,
7653
7756
  onPasteOverride,
7654
- onLeftSelectRef
7757
+ onLeftSelectRef,
7758
+ onCreateItemDraftRef
7655
7759
  });
7656
7760
  const editorClipboard = shellClipboard.editorClipboard;
7657
7761
  const rowClipboard = shellClipboard.rowClipboard;
7762
+ const wrappedRecordActions = recordActions ? (record) => {
7763
+ const list = recordActions(record, record.scope);
7764
+ if (!list || list.length === 0) return list ?? void 0;
7765
+ return list.map((a) => ({
7766
+ ...a,
7767
+ onAction: () => {
7768
+ onTelemetry?.({
7769
+ type: "recordAction.invoke",
7770
+ recordType,
7771
+ key: a.key,
7772
+ ref: anchorKey(record.scope)
7773
+ });
7774
+ return a.onAction();
7775
+ }
7776
+ }));
7777
+ } : void 0;
7658
7778
  const baseScopeRef = editingScope?.raw ?? "";
7659
7779
  const itemNounLabel = itemNoun || "item";
7660
7780
  const {
7661
7781
  onItemOpen,
7782
+ onItemCreate,
7662
7783
  onItemBack,
7663
7784
  onItemPrev,
7664
7785
  onItemNext,
7665
7786
  itemPosition,
7666
- itemViewCtx} = useShellNavigation({
7787
+ itemViewCtx: baseItemViewCtx} = useShellNavigation({
7667
7788
  isCollection,
7668
7789
  runWithGuard,
7669
7790
  selectedItemId,
@@ -7683,6 +7804,7 @@ function RecordsAdminShellInner(props) {
7683
7804
  hooks: props.hooks,
7684
7805
  lastAppliedDLRef
7685
7806
  });
7807
+ const itemViewCtx = baseItemViewCtx;
7686
7808
  const renderEditorWithPreview = () => {
7687
7809
  if (!editingTargetScope) return null;
7688
7810
  const previewBody = renderPreview && effectivePreviewScope ? renderPreview({ resolved: editorCtx.value, previewScope: effectivePreviewScope }) : null;
@@ -8148,6 +8270,7 @@ function RecordsAdminShellInner(props) {
8148
8270
  });
8149
8271
  };
8150
8272
  onLeftSelectRef.current = onLeftSelect;
8273
+ onCreateItemDraftRef.current = onItemCreate;
8151
8274
  return /* @__PURE__ */ jsx(RuleLabelLookupProvider, { value: ruleLabelLookup, children: /* @__PURE__ */ jsxs(
8152
8275
  "div",
8153
8276
  {
@@ -8176,7 +8299,7 @@ function RecordsAdminShellInner(props) {
8176
8299
  }
8177
8300
  );
8178
8301
  })(),
8179
- /* @__PURE__ */ jsxs("div", { className: "px-4 pt-4 space-y-3", children: [
8302
+ /* @__PURE__ */ jsxs("div", { className: "px-2 pt-2 space-y-2", children: [
8180
8303
  intro && !dismissed && !(headerWillRender && resolvedReopenAffordance === "header") && /* @__PURE__ */ jsx(
8181
8304
  IntroCard,
8182
8305
  {
@@ -8488,6 +8611,7 @@ function RecordsAdminShellInner(props) {
8488
8611
  ),
8489
8612
  renderGroupActions: renderRuleGroupActions,
8490
8613
  rowClipboard,
8614
+ rowActions: wrappedRecordActions,
8491
8615
  i18n
8492
8616
  }
8493
8617
  ),
@@ -8586,6 +8710,8 @@ function RecordsAdminShellInner(props) {
8586
8710
  itemColumns,
8587
8711
  cardSize: itemCardSize,
8588
8712
  ruleSummary: activeRuleSummary,
8713
+ rowActions: wrappedRecordActions,
8714
+ rowClipboard,
8589
8715
  i18n
8590
8716
  }
8591
8717
  ),
@@ -8672,7 +8798,41 @@ var RecordBrowser = ({
8672
8798
  scopesLoading,
8673
8799
  i18n
8674
8800
  }) => {
8675
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", style: { background: "hsl(var(--ra-surface))" }, children: [
8801
+ const rootRef = useRef(null);
8802
+ const searchRef = useRef(null);
8803
+ useEffect(() => {
8804
+ const root = rootRef.current;
8805
+ if (!root) return;
8806
+ const handler = (e) => {
8807
+ if (e.key !== "/" || e.metaKey || e.ctrlKey || e.altKey) return;
8808
+ const target = e.target;
8809
+ if (!target || !root.contains(target)) return;
8810
+ const tag = target.tagName;
8811
+ if (tag === "INPUT" || tag === "TEXTAREA" || target.isContentEditable) return;
8812
+ e.preventDefault();
8813
+ searchRef.current?.focus();
8814
+ searchRef.current?.select();
8815
+ };
8816
+ root.addEventListener("keydown", handler);
8817
+ return () => root.removeEventListener("keydown", handler);
8818
+ }, []);
8819
+ const onSearchKeyDown = (e) => {
8820
+ if (e.key === "Escape") {
8821
+ if (search) {
8822
+ e.preventDefault();
8823
+ onSearchChange("");
8824
+ } else {
8825
+ e.currentTarget.blur();
8826
+ }
8827
+ } else if (e.key === "ArrowDown") {
8828
+ const first = rootRef.current?.querySelector("button.ra-row-hit");
8829
+ if (first) {
8830
+ e.preventDefault();
8831
+ first.focus();
8832
+ }
8833
+ }
8834
+ };
8835
+ return /* @__PURE__ */ jsxs("div", { ref: rootRef, className: "flex flex-col h-full", style: { background: "hsl(var(--ra-surface))" }, children: [
8676
8836
  /* @__PURE__ */ jsx(ScopeTabs, { scopes, active: activeScope, onChange: onActiveScopeChange, loading: scopesLoading }),
8677
8837
  /* @__PURE__ */ jsxs("div", { className: "p-3 space-y-2.5 border-b", style: { borderColor: "hsl(var(--ra-border))" }, children: [
8678
8838
  /* @__PURE__ */ jsxs("div", { className: "relative", children: [
@@ -8680,9 +8840,11 @@ var RecordBrowser = ({
8680
8840
  /* @__PURE__ */ jsx(
8681
8841
  "input",
8682
8842
  {
8843
+ ref: searchRef,
8683
8844
  type: "text",
8684
8845
  value: search,
8685
8846
  onChange: (e) => onSearchChange(e.target.value),
8847
+ onKeyDown: onSearchKeyDown,
8686
8848
  placeholder: i18n.searchPlaceholder,
8687
8849
  className: "w-full pl-8 pr-3 py-1.5 text-xs rounded-md border bg-transparent focus:outline-none focus:ring-1",
8688
8850
  style: {
@@ -8717,7 +8879,7 @@ var RecordBrowser = ({
8717
8879
  };
8718
8880
  var initials2 = (s) => s.split(/\s+/).filter(Boolean).slice(0, 2).map((p) => p[0]?.toUpperCase() ?? "").join("") || "?";
8719
8881
  var DefaultRecordCard = ({ record, ctx, variant = "grid" }) => {
8720
- const { selected, onSelect, isDirty, hasError, onCopy, onPaste, canPaste, pasteWillReplace, clipboardSourceLabel } = ctx;
8882
+ const { selected, onSelect, isDirty, hasError, onCopy, onDuplicate, actions } = ctx;
8721
8883
  const aspect = variant === "gallery" ? "aspect-video" : "aspect-square";
8722
8884
  return /* @__PURE__ */ jsxs(
8723
8885
  "button",
@@ -8778,20 +8940,15 @@ var DefaultRecordCard = ({ record, ctx, variant = "grid" }) => {
8778
8940
  /* @__PURE__ */ jsxs("div", { className: "p-2.5 min-w-0", children: [
8779
8941
  /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-1.5 min-w-0", children: [
8780
8942
  /* @__PURE__ */ jsx("div", { className: "ra-row-title flex-1 min-w-0", children: record.label }),
8781
- (onCopy || onPaste) && /* @__PURE__ */ jsx(
8943
+ (onCopy || onDuplicate || actions && actions.length > 0) && /* @__PURE__ */ jsx(
8782
8944
  RowContextMenu,
8783
8945
  {
8784
8946
  onCopy,
8785
- onPaste,
8786
- canPaste,
8787
- pasteWillReplace,
8788
- pasteSourceLabel: clipboardSourceLabel,
8947
+ onDuplicate,
8948
+ actions,
8789
8949
  i18n: {
8790
8950
  copy: DEFAULT_I18N.copy,
8791
- paste: DEFAULT_I18N.paste,
8792
- pasteFrom: DEFAULT_I18N.pasteFrom,
8793
- pasteReplace: DEFAULT_I18N.pasteReplace,
8794
- clipboardEmpty: DEFAULT_I18N.clipboardEmpty
8951
+ duplicateAction: DEFAULT_I18N.duplicateAction
8795
8952
  }
8796
8953
  }
8797
8954
  )