@proveanything/smartlinks-utils-ui 0.7.9 → 0.8.1

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,12 +1,12 @@
1
- import { styleInject } from '../../chunk-B34MMOND.js';
1
+ import { styleInject } from '../../chunk-F2YH3NVP.js';
2
2
  import { FacetRuleEditor } from '../../chunk-JMCV6FOW.js';
3
3
  import { useFacets } from '../../chunk-4LHF5JB7.js';
4
4
  import { cn } from '../../chunk-L7FQ52F5.js';
5
- import { listRecords, parsedRefToTarget, parsedRefToScope, matchRecords, scopesEqual, getRecordById, updateRecord, createRecord, upsertRecord, removeRecord } from '../../chunk-KFKVGUUP.js';
5
+ import { parsedRefToTarget, parsedRefToScope, matchRecords, scopesEqual, getRecordById, listRecords, upsertRecord, updateRecord, createRecord, removeRecord } from '../../chunk-KFKVGUUP.js';
6
6
  export { bulkDelete, bulkUpsert, createRecord, getRecordById, listRecords, matchRecords, parsedRefToScope, parsedRefToTarget, removeRecord, restoreRecord, scopesEqual, upsertRecord } from '../../chunk-KFKVGUUP.js';
7
- import { createContext, useMemo, useState, useEffect, useCallback, useRef, useContext, useSyncExternalStore, createElement, useId } from 'react';
7
+ import { createContext, useState, useEffect, useCallback, useMemo, useRef, useContext, useSyncExternalStore, createElement, useId } from 'react';
8
8
  import { ChevronDown, Database, Lightbulb, SearchX, Inbox, LayoutGrid, Eye, MoreHorizontal, Download, Upload, Trash2, Copy, Pencil, Plus, CircleDashed, ArrowDownLeft, CheckCircle2, SlidersHorizontal, Globe, Tag, Boxes, Layers, Package, Rows3, List, ChevronRight, Eraser, ClipboardPaste, Box, X, Image, Table, ArrowLeft, ChevronLeft, AlertTriangle, Info, HelpCircle, Search, CornerDownLeft, Circle, AlertCircle, Undo2, Save, Loader2, XCircle, ArrowRight, BookOpen, Target, Settings2 } from 'lucide-react';
9
- import { useQueryClient, useInfiniteQuery, useQuery } from '@tanstack/react-query';
9
+ import { useQuery, useQueryClient, useInfiniteQuery } from '@tanstack/react-query';
10
10
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
11
11
  import { createPortal } from 'react-dom';
12
12
 
@@ -223,155 +223,6 @@ var resolutionChain = (target, supportedScopes) => {
223
223
  }
224
224
  return Array.from(new Set(chain));
225
225
  };
226
- var defaultClassify = (r) => {
227
- if (!r.data) return "empty";
228
- const keys = Object.keys(r.data);
229
- if (keys.length === 0) return "empty";
230
- return "configured";
231
- };
232
- var matchesScope = (kind, rec, _p) => {
233
- const hasRule = !!rec.facetRule;
234
- if (kind === "rule") return hasRule;
235
- if (hasRule) return false;
236
- const productId = rec.productId ?? void 0;
237
- const variantId = rec.variantId ?? void 0;
238
- const batchId = rec.batchId ?? void 0;
239
- const proofId = rec.proofId ?? void 0;
240
- if (kind === "collection") {
241
- return !productId && !variantId && !batchId && !proofId;
242
- }
243
- if (kind === "product") return !!productId && !variantId && !batchId;
244
- if (kind === "variant") return !!variantId;
245
- if (kind === "batch") return !!batchId;
246
- return false;
247
- };
248
- var matchesContext = (p, ctx) => {
249
- if (!ctx) return true;
250
- if (ctx.productId && p.productId !== ctx.productId) return false;
251
- if (ctx.variantId && p.variantId !== ctx.variantId) return false;
252
- if (ctx.batchId && p.batchId !== ctx.batchId) return false;
253
- return true;
254
- };
255
- var toSummary = (rec) => {
256
- const ref = rec.ref ?? "";
257
- const productId = rec.productId ?? void 0;
258
- const variantId = rec.variantId ?? void 0;
259
- const batchId = rec.batchId ?? void 0;
260
- const proofId = rec.proofId ?? void 0;
261
- const scope = {
262
- kind: batchId ? "batch" : variantId ? "variant" : productId ? "product" : "collection",
263
- raw: ref,
264
- productId,
265
- variantId,
266
- batchId,
267
- proofId
268
- };
269
- const facetRule = rec.facetRule ?? null;
270
- if (facetRule) scope.kind = "rule";
271
- const ruleLabel = facetRule && facetRule.all && facetRule.all.length > 0 ? facetRule.all.length === 1 ? "Rule \xB7 1 facet" : `Rule \xB7 ${facetRule.all.length} facets` : null;
272
- const fallbackLabel = scope.batchId ?? scope.variantId ?? scope.productId ?? ruleLabel ?? (ref || "Global record");
273
- return {
274
- id: rec.id,
275
- ref,
276
- scope,
277
- data: rec.data ?? null,
278
- status: "configured",
279
- label: fallbackLabel,
280
- updatedAt: rec.updatedAt,
281
- facetRule
282
- };
283
- };
284
- var QK_BASE = ["records-admin", "list"];
285
- var useRecordList = (args) => {
286
- const {
287
- ctx,
288
- scopeKind,
289
- search = "",
290
- filter = "all",
291
- classify: classify2,
292
- enabled = true,
293
- scaffolder,
294
- contextScope,
295
- pageSize = 100
296
- } = args;
297
- const queryClient = useQueryClient();
298
- const queryKey = useMemo(
299
- () => [
300
- ...QK_BASE,
301
- ctx.collectionId,
302
- ctx.appId,
303
- ctx.recordType,
304
- scopeKind,
305
- contextScope?.productId ?? null,
306
- contextScope?.variantId ?? null,
307
- contextScope?.batchId ?? null
308
- ],
309
- [ctx.collectionId, ctx.appId, ctx.recordType, scopeKind, contextScope?.productId, contextScope?.variantId, contextScope?.batchId]
310
- );
311
- const query = useInfiniteQuery({
312
- queryKey,
313
- enabled,
314
- initialPageParam: 0,
315
- queryFn: async ({ pageParam }) => {
316
- const offset = pageParam;
317
- const { data, total: total2, hasMore } = await listRecords(ctx, { limit: pageSize, offset });
318
- return { data, total: total2, hasMore, nextOffset: offset + data.length };
319
- },
320
- getNextPageParam: (last) => last.hasMore ? last.nextOffset : void 0,
321
- staleTime: 3e4
322
- });
323
- const [scaffolded, setScaffolded] = useState(null);
324
- const rawItems = useMemo(() => {
325
- const all = query.data?.pages.flatMap((p) => p.data) ?? [];
326
- const cls = classify2 ?? defaultClassify;
327
- return all.map((rec) => ({ rec, summary: toSummary(rec) })).filter(({ rec, summary }) => matchesScope(scopeKind, rec, summary.scope)).filter(({ summary }) => matchesContext(summary.scope, contextScope)).map(({ summary }) => ({ ...summary, status: cls(summary) }));
328
- }, [query.data, scopeKind, classify2, contextScope]);
329
- useEffect(() => {
330
- if (!scaffolder) {
331
- setScaffolded(null);
332
- return;
333
- }
334
- let cancelled = false;
335
- Promise.resolve(scaffolder(rawItems)).then((next) => {
336
- if (!cancelled) setScaffolded(next);
337
- });
338
- return () => {
339
- cancelled = true;
340
- };
341
- }, [rawItems, scaffolder]);
342
- const items = scaffolded ?? rawItems;
343
- const filtered = useMemo(() => {
344
- let out = items;
345
- if (filter !== "all") out = out.filter((r) => r.status === filter);
346
- if (search.trim()) {
347
- const q = search.trim().toLowerCase();
348
- out = out.filter((r) => `${r.label} ${r.subtitle ?? ""} ${r.ref}`.toLowerCase().includes(q));
349
- }
350
- return out;
351
- }, [items, filter, search]);
352
- const counts = useMemo(() => ({
353
- all: items.length,
354
- configured: items.filter((r) => r.status === "configured").length,
355
- partial: items.filter((r) => r.status === "partial").length,
356
- empty: items.filter((r) => r.status === "empty").length
357
- }), [items]);
358
- const refetch = useCallback(() => {
359
- queryClient.invalidateQueries({ queryKey: [...QK_BASE, ctx.collectionId, ctx.appId, ctx.recordType] });
360
- }, [queryClient, ctx.collectionId, ctx.appId, ctx.recordType]);
361
- const total = query.data?.pages[query.data.pages.length - 1]?.total ?? items.length;
362
- return {
363
- allItems: items,
364
- items: filtered,
365
- total,
366
- counts,
367
- isLoading: query.isLoading,
368
- error: query.error ?? null,
369
- refetch,
370
- hasNextPage: query.hasNextPage,
371
- isFetchingNextPage: query.isFetchingNextPage,
372
- fetchNextPage: query.fetchNextPage
373
- };
374
- };
375
226
 
376
227
  // src/components/RecordsAdmin/data/resolveRecord.ts
377
228
  var resolveRecord = async (args) => {
@@ -525,591 +376,303 @@ function useResolvedRecord(args) {
525
376
  error: query.error ?? null
526
377
  };
527
378
  }
528
- var isFacetRuleValid = (rule) => {
529
- if (!rule || !Array.isArray(rule.all) || rule.all.length === 0) return false;
530
- return rule.all.every(
531
- (c) => !!c.facetKey && Array.isArray(c.anyOf) && c.anyOf.length > 0
532
- );
533
- };
534
- function useRulePreview(args) {
535
- const {
536
- SL,
537
- collectionId,
538
- appId,
539
- rule,
540
- limit = 20,
541
- debounceMs = 350,
542
- enabled = true
543
- } = args;
544
- const [debouncedRule, setDebouncedRule] = useState(rule);
545
- const timer = useRef(null);
379
+ var RT_KEY = (recordType) => recordType ?? "_default";
380
+ var lsKey = (appId, recordType) => `ra:intro:${appId}:${RT_KEY(recordType)}`;
381
+ var useIntroDismissed = (SL, collectionId, appId, recordType) => {
382
+ const [dismissed, setDismissed] = useState(() => {
383
+ try {
384
+ return localStorage.getItem(lsKey(appId, recordType)) === "1";
385
+ } catch {
386
+ return false;
387
+ }
388
+ });
546
389
  useEffect(() => {
547
- if (timer.current) clearTimeout(timer.current);
548
- timer.current = setTimeout(() => setDebouncedRule(rule), debounceMs);
390
+ let cancelled = false;
391
+ (async () => {
392
+ try {
393
+ const cfg = await SL?.appConfiguration?.getConfig?.({ collectionId, appId, admin: true });
394
+ if (cancelled) return;
395
+ const flag = cfg?._meta?.introDismissed?.[RT_KEY(recordType)];
396
+ if (flag) setDismissed(true);
397
+ } catch {
398
+ }
399
+ })();
549
400
  return () => {
550
- if (timer.current) clearTimeout(timer.current);
401
+ cancelled = true;
551
402
  };
552
- }, [rule, debounceMs]);
553
- const valid = isFacetRuleValid(debouncedRule);
403
+ }, [SL, collectionId, appId, recordType]);
404
+ const dismiss = useCallback(async () => {
405
+ setDismissed(true);
406
+ try {
407
+ localStorage.setItem(lsKey(appId, recordType), "1");
408
+ } catch {
409
+ }
410
+ try {
411
+ const cfg = await SL?.appConfiguration?.getConfig?.({ collectionId, appId, admin: true }).catch(() => ({}));
412
+ const next = {
413
+ ...cfg ?? {},
414
+ _meta: {
415
+ ...cfg?._meta ?? {},
416
+ introDismissed: { ...cfg?._meta?.introDismissed ?? {}, [RT_KEY(recordType)]: true }
417
+ }
418
+ };
419
+ await SL?.appConfiguration?.setConfig?.({ collectionId, appId, admin: true, config: next });
420
+ } catch {
421
+ }
422
+ }, [SL, collectionId, appId, recordType]);
423
+ const undismiss = useCallback(() => {
424
+ setDismissed(false);
425
+ try {
426
+ localStorage.removeItem(lsKey(appId, recordType));
427
+ } catch {
428
+ }
429
+ }, [appId, recordType]);
430
+ return { dismissed, dismiss, undismiss };
431
+ };
432
+ var useScopeProbe = ({ SL, collectionId, admin = true, enabled = true }) => {
554
433
  const query = useQuery({
555
- queryKey: ["records-admin", "preview-rule", collectionId, appId, debouncedRule, limit],
556
- enabled: enabled && !!collectionId && !!appId && valid,
557
- staleTime: 3e4,
558
- queryFn: () => SL.app.records.previewRule(collectionId, appId, {
559
- facetRule: debouncedRule,
560
- limit
561
- })
434
+ queryKey: ["records-admin", "scope-probe", collectionId, admin],
435
+ enabled: enabled && !!collectionId && !!SL?.collection?.get,
436
+ staleTime: 5 * 6e4,
437
+ queryFn: async () => {
438
+ const c = await SL.collection.get(collectionId, admin);
439
+ return {
440
+ hasVariants: !!c?.variants,
441
+ hasBatches: !!c?.batches
442
+ };
443
+ }
562
444
  });
563
445
  return {
564
- totalMatches: query.data?.total ?? null,
565
- sampleProductIds: (query.data?.matchingProducts ?? []).map((p) => p.productId),
566
- isLoading: query.isFetching,
567
- isStale: rule !== debouncedRule,
446
+ hasVariants: query.data?.hasVariants ?? false,
447
+ hasBatches: query.data?.hasBatches ?? false,
448
+ isLoading: query.isLoading,
568
449
  error: query.error ?? null
569
450
  };
570
- }
571
- var createStore = () => {
572
- const map = /* @__PURE__ */ new Map();
573
- const listeners = /* @__PURE__ */ new Set();
574
- let nextOrder = 0;
575
- let cachedList = [];
576
- const recompute = () => {
577
- cachedList = Array.from(map.values()).sort((a, b) => a.order - b.order);
578
- };
579
- const emit = () => {
580
- recompute();
581
- listeners.forEach((l) => l());
582
- };
583
- return {
584
- list: () => cachedList,
585
- get: (key) => map.get(key),
586
- has: (key) => map.has(key),
587
- upsertDraft(input) {
588
- const existing = map.get(input.key);
589
- const order = existing?.order ?? input.order ?? nextOrder++;
590
- const status = input.status ?? existing?.status ?? "dirty";
591
- map.set(input.key, {
592
- ...input,
593
- order,
594
- status
595
- });
596
- emit();
597
- },
598
- setStatus(key, status, error) {
599
- const existing = map.get(key);
600
- if (!existing) return;
601
- map.set(key, { ...existing, status, error });
602
- emit();
603
- },
604
- clearDraft(key) {
605
- if (map.delete(key)) emit();
606
- },
607
- clearAll() {
608
- if (map.size === 0) return;
609
- map.clear();
610
- emit();
611
- },
612
- subscribe(listener) {
613
- listeners.add(listener);
614
- return () => {
615
- listeners.delete(listener);
616
- };
617
- }
618
- };
619
- };
620
- var DirtyDraftContext = createContext(null);
621
- var DirtyDraftProvider = ({ children }) => {
622
- const storeRef = useRef(null);
623
- if (!storeRef.current) storeRef.current = createStore();
624
- return /* @__PURE__ */ jsx(DirtyDraftContext.Provider, { value: storeRef.current, children });
625
- };
626
- var useDirtyDraftStore = () => {
627
- const ctx = useContext(DirtyDraftContext);
628
- const fallbackRef = useRef(null);
629
- if (!ctx && !fallbackRef.current) fallbackRef.current = createStore();
630
- return ctx ?? fallbackRef.current;
631
- };
632
- var useDirtyDrafts = () => {
633
- const store = useDirtyDraftStore();
634
- return useSyncExternalStore(
635
- useCallback((cb) => store.subscribe(cb), [store]),
636
- useCallback(() => store.list(), [store]),
637
- useCallback(() => store.list(), [store])
638
- );
639
- };
640
- var useDirtyDraft = (key) => {
641
- const store = useDirtyDraftStore();
642
- const getSnapshot = useCallback(
643
- () => key ? store.get(key) : void 0,
644
- [store, key]
645
- );
646
- return useSyncExternalStore(
647
- useCallback((cb) => store.subscribe(cb), [store]),
648
- getSnapshot,
649
- getSnapshot
650
- );
651
451
  };
652
- var buildDraftKey = (opts) => {
653
- if (opts.recordId) return opts.recordId;
654
- const kind = opts.draftKind ?? "rec";
655
- return `draft:${kind}:${opts.scopeRaw}`;
656
- };
657
- var useDirtyDraftActions = () => {
658
- const store = useDirtyDraftStore();
659
- const drafts = useDirtyDrafts();
660
- const count = useMemo(() => drafts.filter((d) => d.status !== "saved").length, [drafts]);
661
- const errorCount = useMemo(() => drafts.filter((d) => d.status === "error").length, [drafts]);
662
- return { drafts, count, errorCount, store };
452
+ var defaultClassify = (r) => {
453
+ if (!r.data) return "empty";
454
+ const keys = Object.keys(r.data);
455
+ if (keys.length === 0) return "empty";
456
+ return "configured";
663
457
  };
664
-
665
- // src/components/RecordsAdmin/hooks/useRecordEditor.ts
666
- var isEqual = (a, b) => {
667
- try {
668
- return JSON.stringify(a) === JSON.stringify(b);
669
- } catch {
670
- return false;
458
+ var matchesScope = (kind, rec, _p) => {
459
+ const hasRule = !!rec.facetRule;
460
+ if (kind === "rule") return hasRule;
461
+ if (hasRule) return false;
462
+ const productId = rec.productId ?? void 0;
463
+ const variantId = rec.variantId ?? void 0;
464
+ const batchId = rec.batchId ?? void 0;
465
+ const proofId = rec.proofId ?? void 0;
466
+ if (kind === "collection") {
467
+ return !productId && !variantId && !batchId && !proofId;
671
468
  }
469
+ if (kind === "product") return !!productId && !variantId && !batchId;
470
+ if (kind === "variant") return !!variantId;
471
+ if (kind === "batch") return !!batchId;
472
+ return false;
672
473
  };
673
- var cloneSeed = (resolved, defaultData) => {
674
- if (resolved.source === "self") return resolved.data;
675
- if (resolved.source === "inherited" && resolved.data != null) {
676
- try {
677
- return structuredClone(resolved.data);
678
- } catch {
679
- return JSON.parse(JSON.stringify(resolved.data));
680
- }
681
- }
682
- return defaultData?.() ?? {};
474
+ var matchesContext = (p, ctx) => {
475
+ if (!ctx) return true;
476
+ if (ctx.productId && p.productId !== ctx.productId) return false;
477
+ if (ctx.variantId && p.variantId !== ctx.variantId) return false;
478
+ if (ctx.batchId && p.batchId !== ctx.batchId) return false;
479
+ return true;
683
480
  };
684
- function useRecordEditor(args) {
481
+ var toSummary = (rec) => {
482
+ const ref = rec.ref ?? "";
483
+ const productId = rec.productId ?? void 0;
484
+ const variantId = rec.variantId ?? void 0;
485
+ const batchId = rec.batchId ?? void 0;
486
+ const proofId = rec.proofId ?? void 0;
487
+ const scope = {
488
+ kind: batchId ? "batch" : variantId ? "variant" : productId ? "product" : "collection",
489
+ raw: ref,
490
+ productId,
491
+ variantId,
492
+ batchId,
493
+ proofId
494
+ };
495
+ const facetRule = rec.facetRule ?? null;
496
+ if (facetRule) scope.kind = "rule";
497
+ const ruleLabel = facetRule && facetRule.all && facetRule.all.length > 0 ? facetRule.all.length === 1 ? "Rule \xB7 1 facet" : `Rule \xB7 ${facetRule.all.length} facets` : null;
498
+ const fallbackLabel = scope.batchId ?? scope.variantId ?? scope.productId ?? ruleLabel ?? (ref || "Global record");
499
+ return {
500
+ id: rec.id,
501
+ ref,
502
+ scope,
503
+ data: rec.data ?? null,
504
+ status: "configured",
505
+ label: fallbackLabel,
506
+ updatedAt: rec.updatedAt,
507
+ facetRule
508
+ };
509
+ };
510
+ var QK_BASE = ["records-admin", "list"];
511
+ var useRecordList = (args) => {
685
512
  const {
686
513
  ctx,
687
- scope,
688
- resolved,
689
- defaultData,
690
- onSaved,
691
- onDeleted,
692
- onSaveError,
693
- reseed = "always",
694
- initialFacetRule = null,
695
- createMode = false,
696
- deriveDraftLabel
514
+ scopeKind,
515
+ search = "",
516
+ filter = "all",
517
+ classify: classify2,
518
+ enabled = true,
519
+ scaffolder,
520
+ contextScope,
521
+ pageSize = 100
697
522
  } = args;
698
523
  const queryClient = useQueryClient();
699
- const draftStore = useDirtyDraftStore();
700
- const draftKey = buildDraftKey({
701
- recordId: resolved.recordId,
702
- scopeRaw: scope.raw,
703
- draftKind: scope.kind
704
- });
705
- const initialDraft = draftStore.get(draftKey);
706
- const cleanSeed = cloneSeed(resolved, defaultData);
707
- const seed = initialDraft && reseed === "preserve-dirty" ? initialDraft.value : cleanSeed;
708
- const seedFacetRule = initialDraft && reseed === "preserve-dirty" ? initialDraft.facetRule : initialFacetRule;
709
- const [value, setValue] = useState(seed);
710
- const [savedSnapshot, setSavedSnapshot] = useState(
711
- initialDraft ? initialDraft.baseline : cleanSeed
712
- );
713
- const [facetRule, setFacetRule] = useState(seedFacetRule);
714
- const [savedFacetRule, setSavedFacetRule] = useState(
715
- initialDraft ? initialDraft.baselineFacetRule : initialFacetRule
524
+ const queryKey = useMemo(
525
+ () => [
526
+ ...QK_BASE,
527
+ ctx.collectionId,
528
+ ctx.appId,
529
+ ctx.recordType,
530
+ scopeKind,
531
+ contextScope?.productId ?? null,
532
+ contextScope?.variantId ?? null,
533
+ contextScope?.batchId ?? null
534
+ ],
535
+ [ctx.collectionId, ctx.appId, ctx.recordType, scopeKind, contextScope?.productId, contextScope?.variantId, contextScope?.batchId]
716
536
  );
717
- const [userInteracted, setUserInteracted] = useState(!!initialDraft);
718
- const [optimisticSource, setOptimisticSource] = useState(null);
719
- const [isSaving, setIsSaving] = useState(false);
720
- const [saveError, setSaveError] = useState(null);
721
- const valueRef = useRef(seed);
722
- useEffect(() => {
723
- valueRef.current = value;
724
- }, [value]);
725
- const prevTargetRef = useRef({
726
- scopeRaw: scope.raw,
727
- recordId: resolved.recordId
537
+ const query = useInfiniteQuery({
538
+ queryKey,
539
+ enabled,
540
+ initialPageParam: 0,
541
+ queryFn: async ({ pageParam }) => {
542
+ const offset = pageParam;
543
+ const { data, total: total2, hasMore } = await listRecords(ctx, { limit: pageSize, offset });
544
+ return { data, total: total2, hasMore, nextOffset: offset + data.length };
545
+ },
546
+ getNextPageParam: (last) => last.hasMore ? last.nextOffset : void 0,
547
+ staleTime: 3e4
728
548
  });
549
+ const [scaffolded, setScaffolded] = useState(null);
550
+ const rawItems = useMemo(() => {
551
+ const all = query.data?.pages.flatMap((p) => p.data) ?? [];
552
+ const cls = classify2 ?? defaultClassify;
553
+ return all.map((rec) => ({ rec, summary: toSummary(rec) })).filter(({ rec, summary }) => matchesScope(scopeKind, rec, summary.scope)).filter(({ summary }) => matchesContext(summary.scope, contextScope)).map(({ summary }) => ({ ...summary, status: cls(summary) }));
554
+ }, [query.data, scopeKind, classify2, contextScope]);
729
555
  useEffect(() => {
730
- const prev = prevTargetRef.current;
731
- const targetChanged = prev.scopeRaw !== scope.raw || (prev.recordId ?? null) !== (resolved.recordId ?? null);
732
- prevTargetRef.current = { scopeRaw: scope.raw, recordId: resolved.recordId };
733
- const fresh = cloneSeed(resolved, defaultData);
734
- if (targetChanged) {
735
- const incomingKey = buildDraftKey({
736
- recordId: resolved.recordId,
737
- scopeRaw: scope.raw,
738
- draftKind: scope.kind
739
- });
740
- const incomingDraft = draftStore.get(incomingKey);
741
- if (incomingDraft && reseed === "preserve-dirty") {
742
- setValue(incomingDraft.value);
743
- setSavedSnapshot(incomingDraft.baseline);
744
- setFacetRule(incomingDraft.facetRule);
745
- setSavedFacetRule(incomingDraft.baselineFacetRule);
746
- setUserInteracted(true);
747
- } else {
748
- setValue(fresh);
749
- setSavedSnapshot(fresh);
750
- setFacetRule(initialFacetRule);
751
- setSavedFacetRule(initialFacetRule);
752
- setUserInteracted(false);
753
- }
754
- } else {
755
- if (reseed === "preserve-dirty") {
756
- const hasUnsaved = !isEqual(valueRef.current, savedSnapshot);
757
- if (!hasUnsaved) setValue(fresh);
758
- } else {
759
- setValue(fresh);
760
- }
761
- setSavedSnapshot(fresh);
762
- setFacetRule(initialFacetRule);
763
- setSavedFacetRule(initialFacetRule);
764
- }
765
- setOptimisticSource(null);
766
- }, [scope.raw, resolved.recordId, resolved.source, resolved.sourceRef]);
767
- const valueDiff = !isEqual(value, savedSnapshot) || !isEqual(facetRule, savedFacetRule);
768
- const isDirty = userInteracted && valueDiff;
769
- const handleChange = useCallback((next) => {
770
- setUserInteracted(true);
771
- setValue(next);
772
- }, []);
773
- const handleFacetRuleChange = useCallback((next) => {
774
- setUserInteracted(true);
775
- setFacetRule(next);
776
- }, []);
777
- const save = useCallback(async () => {
778
- const anchors = parsedRefToScope(scope);
779
- const hasAnchors = !!(anchors.productId || anchors.variantId || anchors.batchId || anchors.proofId);
780
- const hasRule = !!(facetRule && facetRule.all && facetRule.all.length > 0);
781
- const isRuleScope2 = scope.kind === "rule";
782
- if (isRuleScope2 && !hasAnchors && !hasRule && !resolved.recordId && !createMode) {
783
- console.warn("[useRecordEditor] save() bailed \u2014 rule scope with no clauses, no recordId, not in createMode");
556
+ if (!scaffolder) {
557
+ setScaffolded(null);
784
558
  return;
785
559
  }
786
- const previousSnapshot = savedSnapshot;
787
- const previousRuleSnapshot = savedFacetRule;
788
- resolved.source;
789
- const cacheKey = resolvedRecordQueryKey({
790
- collectionId: ctx.collectionId,
791
- appId: ctx.appId,
792
- recordType: ctx.recordType,
793
- productId: scope.productId,
794
- variantId: scope.variantId,
795
- batchId: scope.batchId,
796
- facetId: scope.facetId,
797
- facetValue: scope.facetValue,
798
- proofId: scope.proofId,
799
- recordId: resolved.recordId,
800
- withParent: true
801
- });
802
- const previousCache = queryClient.getQueryData(cacheKey);
803
- setSaveError(null);
804
- setIsSaving(true);
805
- setSavedSnapshot(value);
806
- setSavedFacetRule(facetRule);
807
- setOptimisticSource("self");
808
- queryClient.setQueryData(cacheKey, {
809
- data: value,
810
- source: "self",
811
- sourceRef: scope.raw,
812
- recordId: resolved.recordId,
813
- parentValue: previousCache?.parentValue ?? resolved.parentValue
560
+ let cancelled = false;
561
+ Promise.resolve(scaffolder(rawItems)).then((next) => {
562
+ if (!cancelled) setScaffolded(next);
814
563
  });
815
- try {
816
- if (resolved.recordId && resolved.source === "self") {
817
- await updateRecord(ctx, resolved.recordId, {
818
- data: value,
819
- facetRule
820
- });
821
- } else if (createMode) {
822
- await createRecord(ctx, {
823
- ref: scope.kind === "rule" && scope.raw ? scope.raw : void 0,
824
- scope: anchors,
825
- data: value,
826
- facetRule
827
- });
828
- } else {
829
- await upsertRecord(ctx, {
830
- // External ref only when the underlying scope already carries
831
- // one (e.g. rule:{id} for an existing rule record). Empty for
832
- // anchor-only writes server addresses by anchors.
833
- ref: scope.kind === "rule" && scope.raw ? scope.raw : void 0,
834
- scope: anchors,
835
- data: value,
836
- facetRule
837
- });
838
- }
839
- draftStore.clearDraft(draftKey);
840
- onSaved?.();
841
- } catch (err) {
842
- setSavedSnapshot(previousSnapshot);
843
- setSavedFacetRule(previousRuleSnapshot);
844
- setOptimisticSource(null);
845
- if (previousCache !== void 0) {
846
- queryClient.setQueryData(cacheKey, previousCache);
847
- } else {
848
- queryClient.invalidateQueries({ queryKey: cacheKey });
564
+ return () => {
565
+ cancelled = true;
566
+ };
567
+ }, [rawItems, scaffolder]);
568
+ const items = scaffolded ?? rawItems;
569
+ const filtered = useMemo(() => {
570
+ let out = items;
571
+ if (filter !== "all") out = out.filter((r) => r.status === filter);
572
+ if (search.trim()) {
573
+ const q = search.trim().toLowerCase();
574
+ out = out.filter((r) => `${r.label} ${r.subtitle ?? ""} ${r.ref}`.toLowerCase().includes(q));
575
+ }
576
+ return out;
577
+ }, [items, filter, search]);
578
+ const counts = useMemo(() => ({
579
+ all: items.length,
580
+ configured: items.filter((r) => r.status === "configured").length,
581
+ partial: items.filter((r) => r.status === "partial").length,
582
+ empty: items.filter((r) => r.status === "empty").length
583
+ }), [items]);
584
+ const refetch = useCallback(() => {
585
+ queryClient.invalidateQueries({ queryKey: [...QK_BASE, ctx.collectionId, ctx.appId, ctx.recordType] });
586
+ }, [queryClient, ctx.collectionId, ctx.appId, ctx.recordType]);
587
+ const total = query.data?.pages[query.data.pages.length - 1]?.total ?? items.length;
588
+ return {
589
+ allItems: items,
590
+ items: filtered,
591
+ total,
592
+ counts,
593
+ isLoading: query.isLoading,
594
+ error: query.error ?? null,
595
+ refetch,
596
+ hasNextPage: query.hasNextPage,
597
+ isFetchingNextPage: query.isFetchingNextPage,
598
+ fetchNextPage: query.fetchNextPage
599
+ };
600
+ };
601
+ var LOG = "[RecordsAdmin/useFacetBrowse]";
602
+ var QK = ["records-admin", "facet-browse"];
603
+ var toScaffoldSummary = (facet, value) => {
604
+ const facetKey = facet.key ?? "";
605
+ const valueKey = value.key ?? "";
606
+ const ref = buildRef({ facetId: facetKey, facetValue: valueKey });
607
+ return {
608
+ id: null,
609
+ ref,
610
+ scope: parseRef(ref),
611
+ data: null,
612
+ status: "empty",
613
+ label: value.name ?? valueKey ?? "Untitled value",
614
+ subtitle: facet.name ?? facetKey ?? void 0
615
+ };
616
+ };
617
+ var useFacetBrowse = ({
618
+ SL,
619
+ collectionId,
620
+ existing,
621
+ search = "",
622
+ filter = "all",
623
+ enabled = true
624
+ }) => {
625
+ const queryClient = useQueryClient();
626
+ const hasAdminList = !!SL?.facets?.list;
627
+ const hasPublicList = !!SL?.facets?.publicList;
628
+ const hasAnyList = hasAdminList || hasPublicList;
629
+ const queryEnabled = enabled && !!collectionId && hasAnyList;
630
+ const query = useQuery({
631
+ queryKey: [...QK, collectionId],
632
+ enabled: queryEnabled,
633
+ staleTime: 3e4,
634
+ queryFn: async () => {
635
+ const t0 = performance.now();
636
+ try {
637
+ if (SL?.facets?.list) {
638
+ console.info(`${LOG} \u2192 SL.facets.list("${collectionId}", { includeValues: true })`);
639
+ const res = await SL.facets.list(collectionId, { includeValues: true });
640
+ const items = res?.items ?? [];
641
+ console.info(
642
+ `${LOG} \u2190 SL.facets.list ok in ${Math.round(performance.now() - t0)}ms \u2014 ${items.length} facet(s)`,
643
+ items
644
+ );
645
+ return items;
646
+ }
647
+ if (SL?.facets?.publicList) {
648
+ console.info(`${LOG} \u2192 SL.facets.publicList("${collectionId}", { includeValues: true })`);
649
+ const res = await SL.facets.publicList(collectionId, { includeValues: true });
650
+ const items = res?.items ?? [];
651
+ console.info(
652
+ `${LOG} \u2190 SL.facets.publicList ok in ${Math.round(performance.now() - t0)}ms \u2014 ${items.length} facet(s)`,
653
+ items
654
+ );
655
+ return items;
656
+ }
657
+ console.warn(`${LOG} queryFn ran but no facets API is available on SL`);
658
+ return [];
659
+ } catch (err) {
660
+ console.error(`${LOG} \u2716 facets fetch failed`, err);
661
+ throw err;
849
662
  }
850
- setSaveError(err);
851
- onSaveError?.(err);
852
- throw err;
853
- } finally {
854
- setIsSaving(false);
855
663
  }
856
- }, [scope.raw, value, savedSnapshot, facetRule, savedFacetRule, resolved.source, resolved.parentValue, resolved.recordId, createMode]);
857
- const reset = useCallback(() => {
858
- setValue(savedSnapshot);
859
- setFacetRule(savedFacetRule);
860
- setUserInteracted(false);
861
- draftStore.clearDraft(draftKey);
862
- }, [savedSnapshot, savedFacetRule]);
863
- const remove = useCallback(async () => {
864
- if (resolved.source !== "self") return;
865
- if (!resolved.recordId) return;
866
- await removeRecord(ctx, resolved.recordId);
867
- draftStore.clearDraft(draftKey);
868
- onDeleted?.();
869
- }, [resolved.source, resolved.recordId]);
870
- const prevDraftKeyRef = useRef(draftKey);
871
- const prevScopeRawRef = useRef(scope.raw);
664
+ });
665
+ const lastLoggedRef = useRef("");
872
666
  useEffect(() => {
873
- const prevKey = prevDraftKeyRef.current;
874
- const prevScopeRaw = prevScopeRawRef.current;
875
- if (prevKey && prevKey !== draftKey && prevScopeRaw === scope.raw) {
876
- const stale = draftStore.get(prevKey);
877
- if (stale) draftStore.clearDraft(prevKey);
667
+ const signature = `${enabled}|${collectionId}|${hasAdminList}|${hasPublicList}`;
668
+ if (signature === lastLoggedRef.current) return;
669
+ lastLoggedRef.current = signature;
670
+ if (!enabled) {
671
+ console.info(`${LOG} skipped \u2014 enabled=false (shell not on facet tab yet)`);
672
+ return;
878
673
  }
879
- prevDraftKeyRef.current = draftKey;
880
- prevScopeRawRef.current = scope.raw;
881
- }, [draftKey, scope.raw]);
882
- useEffect(() => {
883
- if (!isDirty) {
884
- return;
885
- }
886
- const anchors = parsedRefToScope(scope);
887
- const saveKind = resolved.recordId && resolved.source === "self" ? "update" : createMode ? "create" : "upsert";
888
- const deriveLabel = () => {
889
- if (deriveDraftLabel) {
890
- try {
891
- const custom = deriveDraftLabel(value, scope);
892
- if (typeof custom === "string" && custom.trim()) return custom.trim();
893
- } catch {
894
- }
895
- }
896
- const KEYS = ["title", "name", "label", "heading", "question", "slug"];
897
- const pickString = (obj) => {
898
- if (!obj || typeof obj !== "object") return void 0;
899
- for (const key of KEYS) {
900
- const raw = obj[key];
901
- if (typeof raw === "string" && raw.trim()) return raw.trim();
902
- }
903
- return void 0;
904
- };
905
- const v = value;
906
- const top = pickString(v);
907
- if (top) return top;
908
- if (v && typeof v === "object") {
909
- for (const wrapper of ["display", "content", "meta", "data"]) {
910
- const nested = pickString(v[wrapper]);
911
- if (nested) return nested;
912
- }
913
- }
914
- if (scope.raw?.startsWith("item:")) return "Untitled item";
915
- if (scope.kind === "rule") return "Rule";
916
- if (scope.kind && scope.kind !== "collection") {
917
- return scope.kind.charAt(0).toUpperCase() + scope.kind.slice(1);
918
- }
919
- return "Default";
920
- };
921
- draftStore.upsertDraft({
922
- key: draftKey,
923
- label: deriveLabel(),
924
- context: scope.kind,
925
- scopeRaw: scope.raw,
926
- recordId: resolved.recordId,
927
- value,
928
- facetRule,
929
- baseline: savedSnapshot,
930
- baselineFacetRule: savedFacetRule,
931
- createMode,
932
- scopeAnchors: anchors,
933
- ref: scope.kind === "rule" && scope.raw ? scope.raw : void 0,
934
- saveKind,
935
- save: async () => {
936
- await save();
937
- }
938
- });
939
- }, [isDirty, value, facetRule, savedSnapshot, savedFacetRule, scope.raw, resolved.recordId, resolved.source, createMode, save]);
940
- const effectiveSource = optimisticSource ?? resolved.source;
941
- const isRuleScope = scope.kind === "rule";
942
- const ruleValid = !isRuleScope || isFacetRuleValid(facetRule);
943
- const canSave = ruleValid;
944
- const cannotSaveReason = !ruleValid ? "Pick at least one value for every facet in the rule before saving." : void 0;
945
- return {
946
- value,
947
- onChange: handleChange,
948
- source: effectiveSource,
949
- recordId: resolved.recordId,
950
- parentValue: resolved.parentValue,
951
- scope,
952
- isDirty,
953
- save,
954
- reset,
955
- remove,
956
- canRemove: effectiveSource === "self",
957
- canSave,
958
- cannotSaveReason,
959
- isSaving,
960
- saveError,
961
- facetRule,
962
- onFacetRuleChange: handleFacetRuleChange
963
- };
964
- }
965
- var RT_KEY = (recordType) => recordType ?? "_default";
966
- var lsKey = (appId, recordType) => `ra:intro:${appId}:${RT_KEY(recordType)}`;
967
- var useIntroDismissed = (SL, collectionId, appId, recordType) => {
968
- const [dismissed, setDismissed] = useState(() => {
969
- try {
970
- return localStorage.getItem(lsKey(appId, recordType)) === "1";
971
- } catch {
972
- return false;
973
- }
974
- });
975
- useEffect(() => {
976
- let cancelled = false;
977
- (async () => {
978
- try {
979
- const cfg = await SL?.appConfiguration?.getConfig?.({ collectionId, appId, admin: true });
980
- if (cancelled) return;
981
- const flag = cfg?._meta?.introDismissed?.[RT_KEY(recordType)];
982
- if (flag) setDismissed(true);
983
- } catch {
984
- }
985
- })();
986
- return () => {
987
- cancelled = true;
988
- };
989
- }, [SL, collectionId, appId, recordType]);
990
- const dismiss = useCallback(async () => {
991
- setDismissed(true);
992
- try {
993
- localStorage.setItem(lsKey(appId, recordType), "1");
994
- } catch {
995
- }
996
- try {
997
- const cfg = await SL?.appConfiguration?.getConfig?.({ collectionId, appId, admin: true }).catch(() => ({}));
998
- const next = {
999
- ...cfg ?? {},
1000
- _meta: {
1001
- ...cfg?._meta ?? {},
1002
- introDismissed: { ...cfg?._meta?.introDismissed ?? {}, [RT_KEY(recordType)]: true }
1003
- }
1004
- };
1005
- await SL?.appConfiguration?.setConfig?.({ collectionId, appId, admin: true, config: next });
1006
- } catch {
1007
- }
1008
- }, [SL, collectionId, appId, recordType]);
1009
- const undismiss = useCallback(() => {
1010
- setDismissed(false);
1011
- try {
1012
- localStorage.removeItem(lsKey(appId, recordType));
1013
- } catch {
1014
- }
1015
- }, [appId, recordType]);
1016
- return { dismissed, dismiss, undismiss };
1017
- };
1018
- var useScopeProbe = ({ SL, collectionId, admin = true, enabled = true }) => {
1019
- const query = useQuery({
1020
- queryKey: ["records-admin", "scope-probe", collectionId, admin],
1021
- enabled: enabled && !!collectionId && !!SL?.collection?.get,
1022
- staleTime: 5 * 6e4,
1023
- queryFn: async () => {
1024
- const c = await SL.collection.get(collectionId, admin);
1025
- return {
1026
- hasVariants: !!c?.variants,
1027
- hasBatches: !!c?.batches
1028
- };
1029
- }
1030
- });
1031
- return {
1032
- hasVariants: query.data?.hasVariants ?? false,
1033
- hasBatches: query.data?.hasBatches ?? false,
1034
- isLoading: query.isLoading,
1035
- error: query.error ?? null
1036
- };
1037
- };
1038
- var LOG = "[RecordsAdmin/useFacetBrowse]";
1039
- var QK = ["records-admin", "facet-browse"];
1040
- var toScaffoldSummary = (facet, value) => {
1041
- const facetKey = facet.key ?? "";
1042
- const valueKey = value.key ?? "";
1043
- const ref = buildRef({ facetId: facetKey, facetValue: valueKey });
1044
- return {
1045
- id: null,
1046
- ref,
1047
- scope: parseRef(ref),
1048
- data: null,
1049
- status: "empty",
1050
- label: value.name ?? valueKey ?? "Untitled value",
1051
- subtitle: facet.name ?? facetKey ?? void 0
1052
- };
1053
- };
1054
- var useFacetBrowse = ({
1055
- SL,
1056
- collectionId,
1057
- existing,
1058
- search = "",
1059
- filter = "all",
1060
- enabled = true
1061
- }) => {
1062
- const queryClient = useQueryClient();
1063
- const hasAdminList = !!SL?.facets?.list;
1064
- const hasPublicList = !!SL?.facets?.publicList;
1065
- const hasAnyList = hasAdminList || hasPublicList;
1066
- const queryEnabled = enabled && !!collectionId && hasAnyList;
1067
- const query = useQuery({
1068
- queryKey: [...QK, collectionId],
1069
- enabled: queryEnabled,
1070
- staleTime: 3e4,
1071
- queryFn: async () => {
1072
- const t0 = performance.now();
1073
- try {
1074
- if (SL?.facets?.list) {
1075
- console.info(`${LOG} \u2192 SL.facets.list("${collectionId}", { includeValues: true })`);
1076
- const res = await SL.facets.list(collectionId, { includeValues: true });
1077
- const items = res?.items ?? [];
1078
- console.info(
1079
- `${LOG} \u2190 SL.facets.list ok in ${Math.round(performance.now() - t0)}ms \u2014 ${items.length} facet(s)`,
1080
- items
1081
- );
1082
- return items;
1083
- }
1084
- if (SL?.facets?.publicList) {
1085
- console.info(`${LOG} \u2192 SL.facets.publicList("${collectionId}", { includeValues: true })`);
1086
- const res = await SL.facets.publicList(collectionId, { includeValues: true });
1087
- const items = res?.items ?? [];
1088
- console.info(
1089
- `${LOG} \u2190 SL.facets.publicList ok in ${Math.round(performance.now() - t0)}ms \u2014 ${items.length} facet(s)`,
1090
- items
1091
- );
1092
- return items;
1093
- }
1094
- console.warn(`${LOG} queryFn ran but no facets API is available on SL`);
1095
- return [];
1096
- } catch (err) {
1097
- console.error(`${LOG} \u2716 facets fetch failed`, err);
1098
- throw err;
1099
- }
1100
- }
1101
- });
1102
- const lastLoggedRef = useRef("");
1103
- useEffect(() => {
1104
- const signature = `${enabled}|${collectionId}|${hasAdminList}|${hasPublicList}`;
1105
- if (signature === lastLoggedRef.current) return;
1106
- lastLoggedRef.current = signature;
1107
- if (!enabled) {
1108
- console.info(`${LOG} skipped \u2014 enabled=false (shell not on facet tab yet)`);
1109
- return;
1110
- }
1111
- if (!collectionId) {
1112
- console.warn(`${LOG} skipped \u2014 no collectionId provided`);
674
+ if (!collectionId) {
675
+ console.warn(`${LOG} skipped \u2014 no collectionId provided`);
1113
676
  return;
1114
677
  }
1115
678
  if (!hasAnyList) {
@@ -1277,49 +840,229 @@ var useProductChildren = (args) => {
1277
840
  refetch
1278
841
  };
1279
842
  };
1280
- var MESSAGE_TYPE = "smartlinks:unsaved-changes";
1281
- function useUnsavedGuard({
1282
- isDirty,
1283
- label,
1284
- confirm,
1285
- disableBeforeUnload = false,
1286
- disableParentMessage = false
1287
- }) {
1288
- useEffect(() => {
1289
- if (disableBeforeUnload || !isDirty || typeof window === "undefined") return;
1290
- const handler = (e) => {
1291
- e.preventDefault();
1292
- e.returnValue = "";
1293
- return "";
1294
- };
1295
- window.addEventListener("beforeunload", handler);
1296
- return () => window.removeEventListener("beforeunload", handler);
1297
- }, [isDirty, disableBeforeUnload]);
1298
- useEffect(() => {
1299
- if (disableParentMessage || typeof window === "undefined") return;
1300
- if (window.parent === window) return;
1301
- try {
1302
- window.parent.postMessage({
1303
- type: MESSAGE_TYPE,
1304
- isDirty,
1305
- label: label ?? null
1306
- }, "*");
1307
- } catch {
1308
- }
1309
- }, [isDirty, label, disableParentMessage]);
1310
- useEffect(() => {
1311
- if (disableParentMessage || typeof window === "undefined") return;
1312
- if (window.parent === window) return;
1313
- return () => {
1314
- try {
1315
- window.parent.postMessage({ type: MESSAGE_TYPE, isDirty: false, label: label ?? null }, "*");
1316
- } catch {
843
+ var EMPTY_RULE_FILTERS = { facetKeys: [], minClauses: null };
844
+ var COMPLEXITY_THRESHOLDS = [3, 5, 10];
845
+ var ruleOf = (r) => r.facetRule ?? null;
846
+ function RuleFilterChips({ source, value, onChange }) {
847
+ const facetKeyEntries = useMemo(() => {
848
+ const counts = /* @__PURE__ */ new Map();
849
+ for (const r of source) {
850
+ const rule = ruleOf(r);
851
+ if (!rule) continue;
852
+ for (const c of rule.all ?? []) {
853
+ counts.set(c.facetKey, (counts.get(c.facetKey) ?? 0) + 1);
1317
854
  }
1318
- };
1319
- }, [disableParentMessage, label]);
1320
- const confirmDiscard = useCallback(
1321
- async (message) => {
1322
- if (!isDirty) return true;
855
+ }
856
+ return Array.from(counts.entries()).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
857
+ }, [source]);
858
+ const maxClauses = useMemo(() => {
859
+ let max = 0;
860
+ for (const r of source) {
861
+ const rule = ruleOf(r);
862
+ if (!rule) continue;
863
+ max = Math.max(max, rule.all?.length ?? 0);
864
+ }
865
+ return max;
866
+ }, [source]);
867
+ if (facetKeyEntries.length === 0 && maxClauses < 2) return null;
868
+ const toggleKey = (key) => {
869
+ const has = value.facetKeys.includes(key);
870
+ const next = has ? value.facetKeys.filter((k) => k !== key) : [...value.facetKeys, key];
871
+ onChange({ ...value, facetKeys: next });
872
+ };
873
+ const setMin = (n) => onChange({ ...value, minClauses: n });
874
+ const hasAny = value.facetKeys.length > 0 || value.minClauses != null;
875
+ return /* @__PURE__ */ jsxs("div", { className: "ra-rule-filters", children: [
876
+ /* @__PURE__ */ jsx("div", { className: "ra-rule-filters-row", role: "group", "aria-label": "Filter rules by facet", children: facetKeyEntries.map(([key, count]) => {
877
+ const active = value.facetKeys.includes(key);
878
+ return /* @__PURE__ */ jsxs(
879
+ "button",
880
+ {
881
+ type: "button",
882
+ onClick: () => toggleKey(key),
883
+ className: "ra-rule-filter-chip",
884
+ "data-active": active ? "true" : "false",
885
+ "aria-pressed": active,
886
+ title: `Show rules using ${key}`,
887
+ children: [
888
+ /* @__PURE__ */ jsx("span", { className: "ra-rule-filter-chip-label", children: key }),
889
+ /* @__PURE__ */ jsx("span", { className: "ra-rule-filter-chip-count", children: count })
890
+ ]
891
+ },
892
+ key
893
+ );
894
+ }) }),
895
+ maxClauses >= 2 && /* @__PURE__ */ jsx("div", { className: "ra-rule-filters-row", role: "group", "aria-label": "Filter by clause count", children: COMPLEXITY_THRESHOLDS.filter((n) => maxClauses >= n).map((n) => {
896
+ const active = value.minClauses === n;
897
+ return /* @__PURE__ */ jsxs(
898
+ "button",
899
+ {
900
+ type: "button",
901
+ onClick: () => setMin(active ? null : n),
902
+ className: "ra-rule-filter-chip",
903
+ "data-tone": "complexity",
904
+ "data-active": active ? "true" : "false",
905
+ "aria-pressed": active,
906
+ title: `Only rules with \u2265 ${n} facets`,
907
+ children: [
908
+ "\u2265 ",
909
+ n,
910
+ " facets"
911
+ ]
912
+ },
913
+ n
914
+ );
915
+ }) }),
916
+ hasAny && /* @__PURE__ */ jsx(
917
+ "button",
918
+ {
919
+ type: "button",
920
+ onClick: () => onChange(EMPTY_RULE_FILTERS),
921
+ className: "ra-rule-filter-clear",
922
+ "aria-label": "Clear rule filters",
923
+ children: "Clear filters"
924
+ }
925
+ )
926
+ ] });
927
+ }
928
+ function applyRuleFilters(items, filters) {
929
+ if (filters.facetKeys.length === 0 && filters.minClauses == null) return items;
930
+ return items.filter((r) => {
931
+ const rule = ruleOf(r);
932
+ if (!rule) return false;
933
+ if (filters.minClauses != null && (rule.all?.length ?? 0) < filters.minClauses) return false;
934
+ if (filters.facetKeys.length > 0) {
935
+ const keys = new Set((rule.all ?? []).map((c) => c.facetKey));
936
+ if (!filters.facetKeys.some((k) => keys.has(k))) return false;
937
+ }
938
+ return true;
939
+ });
940
+ }
941
+
942
+ // src/components/RecordsAdmin/hooks/useShellBrowser.ts
943
+ function useShellBrowser(opts) {
944
+ const {
945
+ ctx,
946
+ SL,
947
+ collectionId,
948
+ activeScope,
949
+ contextScope,
950
+ probeIsLoading,
951
+ selectedProductId,
952
+ drillTab,
953
+ classify: classify2
954
+ } = opts;
955
+ const [search, setSearch] = useState("");
956
+ const [filter, setFilter] = useState("all");
957
+ const [ruleFilters, setRuleFilters] = useState(EMPTY_RULE_FILTERS);
958
+ const [facetBrowseFilter, setFacetBrowseFilter] = useState(null);
959
+ useEffect(() => {
960
+ setSearch("");
961
+ setFilter("all");
962
+ setRuleFilters(EMPTY_RULE_FILTERS);
963
+ setFacetBrowseFilter(null);
964
+ }, [activeScope]);
965
+ const productBrowse = useProductBrowse({
966
+ SL,
967
+ collectionId,
968
+ search: activeScope === "product" ? search : "",
969
+ enabled: activeScope === "product" && !contextScope?.productId
970
+ });
971
+ const recordListEnabled = (activeScope === "rule" || activeScope === "collection") && !probeIsLoading;
972
+ const recordList = useRecordList({
973
+ ctx,
974
+ scopeKind: activeScope,
975
+ search,
976
+ filter,
977
+ classify: classify2,
978
+ contextScope,
979
+ enabled: recordListEnabled
980
+ });
981
+ const facetBrowse = useFacetBrowse({
982
+ SL,
983
+ collectionId,
984
+ existing: [],
985
+ enabled: (activeScope === "rule" || activeScope === "collection") && !probeIsLoading
986
+ });
987
+ const variantChildren = useProductChildren({
988
+ SL,
989
+ collectionId,
990
+ productId: selectedProductId,
991
+ kind: drillTab === "variant" ? "variant" : null
992
+ });
993
+ const batchChildren = useProductChildren({
994
+ SL,
995
+ collectionId,
996
+ productId: selectedProductId,
997
+ kind: drillTab === "batch" ? "batch" : null
998
+ });
999
+ const refetchAll = useCallback(() => {
1000
+ productBrowse.refetch();
1001
+ recordList.refetch();
1002
+ facetBrowse.refetch();
1003
+ variantChildren.refetch?.();
1004
+ batchChildren.refetch?.();
1005
+ }, [productBrowse, recordList, facetBrowse, variantChildren, batchChildren]);
1006
+ return {
1007
+ search,
1008
+ setSearch,
1009
+ filter,
1010
+ setFilter,
1011
+ ruleFilters,
1012
+ setRuleFilters,
1013
+ facetBrowseFilter,
1014
+ setFacetBrowseFilter,
1015
+ productBrowse,
1016
+ recordList,
1017
+ facetBrowse,
1018
+ variantChildren,
1019
+ batchChildren,
1020
+ refetchAll
1021
+ };
1022
+ }
1023
+ var MESSAGE_TYPE = "smartlinks:unsaved-changes";
1024
+ function useUnsavedGuard({
1025
+ isDirty,
1026
+ label,
1027
+ confirm,
1028
+ disableBeforeUnload = false,
1029
+ disableParentMessage = false
1030
+ }) {
1031
+ useEffect(() => {
1032
+ if (disableBeforeUnload || !isDirty || typeof window === "undefined") return;
1033
+ const handler = (e) => {
1034
+ e.preventDefault();
1035
+ e.returnValue = "";
1036
+ return "";
1037
+ };
1038
+ window.addEventListener("beforeunload", handler);
1039
+ return () => window.removeEventListener("beforeunload", handler);
1040
+ }, [isDirty, disableBeforeUnload]);
1041
+ useEffect(() => {
1042
+ if (disableParentMessage || typeof window === "undefined") return;
1043
+ if (window.parent === window) return;
1044
+ try {
1045
+ window.parent.postMessage({
1046
+ type: MESSAGE_TYPE,
1047
+ isDirty,
1048
+ label: label ?? null
1049
+ }, "*");
1050
+ } catch {
1051
+ }
1052
+ }, [isDirty, label, disableParentMessage]);
1053
+ useEffect(() => {
1054
+ if (disableParentMessage || typeof window === "undefined") return;
1055
+ if (window.parent === window) return;
1056
+ return () => {
1057
+ try {
1058
+ window.parent.postMessage({ type: MESSAGE_TYPE, isDirty: false, label: label ?? null }, "*");
1059
+ } catch {
1060
+ }
1061
+ };
1062
+ }, [disableParentMessage, label]);
1063
+ const confirmDiscard = useCallback(
1064
+ async (message) => {
1065
+ if (!isDirty) return true;
1323
1066
  const msg = message ?? "You have unsaved changes. Discard them?";
1324
1067
  if (confirm) {
1325
1068
  try {
@@ -1523,45 +1266,6 @@ var useConfirmDialog = () => {
1523
1266
  )
1524
1267
  };
1525
1268
  };
1526
- var DEFAULTS2 = {
1527
- title: "Replace existing values?",
1528
- body: "This destination already has saved values. Replace them?",
1529
- confirmLabel: "Replace",
1530
- cancelLabel: "Cancel"
1531
- };
1532
- var usePasteConfirm = () => {
1533
- const [open, setOpen] = useState(false);
1534
- const [state, setState] = useState(DEFAULTS2);
1535
- const resolverRef = useRef(null);
1536
- const confirm = useCallback((i18n) => {
1537
- setState({ ...DEFAULTS2, ...i18n ?? {} });
1538
- setOpen(true);
1539
- return new Promise((resolve) => {
1540
- resolverRef.current = resolve;
1541
- });
1542
- }, []);
1543
- const handleChoice = useCallback((choice) => {
1544
- setOpen(false);
1545
- const r = resolverRef.current;
1546
- resolverRef.current = null;
1547
- r?.(choice === "discard");
1548
- }, []);
1549
- return {
1550
- confirm,
1551
- dialog: /* @__PURE__ */ jsx(
1552
- ConfirmDialog,
1553
- {
1554
- open,
1555
- title: state.title,
1556
- body: state.body,
1557
- saveLabel: "",
1558
- discardLabel: state.confirmLabel,
1559
- cancelLabel: state.cancelLabel,
1560
- onChoice: handleChoice
1561
- }
1562
- )
1563
- };
1564
- };
1565
1269
  var stores = /* @__PURE__ */ new Map();
1566
1270
  var storageKey = (key) => `ra:clipboard:${key}`;
1567
1271
  var getStore = (key) => {
@@ -1639,33 +1343,283 @@ function cloneValue(value) {
1639
1343
  }
1640
1344
  }
1641
1345
  }
1642
- var LABELS = {
1643
- collection: "Global",
1644
- product: "Products",
1645
- facet: "Shared",
1646
- variant: "Variants",
1647
- batch: "Batches",
1648
- rule: "Rules"
1346
+ var DEFAULTS2 = {
1347
+ title: "Replace existing values?",
1348
+ body: "This destination already has saved values. Replace them?",
1349
+ confirmLabel: "Replace",
1350
+ cancelLabel: "Cancel"
1649
1351
  };
1650
- var ScopeTabs = ({
1651
- scopes,
1652
- active,
1653
- onChange,
1654
- loading = false,
1655
- counts,
1656
- icons
1657
- }) => {
1658
- const iconMap = icons ?? DEFAULT_ICONS.scope;
1659
- return /* @__PURE__ */ jsx("div", { role: "tablist", className: "ra-tabs", "aria-label": "Record scope", children: scopes.map((s) => {
1660
- const Icon = iconMap[s] ?? DEFAULT_ICONS.scope[s];
1661
- const isActive = active === s;
1662
- const count = counts?.[s];
1663
- return /* @__PURE__ */ jsxs(
1664
- "button",
1665
- {
1666
- type: "button",
1667
- role: "tab",
1668
- "aria-selected": isActive,
1352
+ var usePasteConfirm = () => {
1353
+ const [open, setOpen] = useState(false);
1354
+ const [state, setState] = useState(DEFAULTS2);
1355
+ const resolverRef = useRef(null);
1356
+ const confirm = useCallback((i18n) => {
1357
+ setState({ ...DEFAULTS2, ...i18n ?? {} });
1358
+ setOpen(true);
1359
+ return new Promise((resolve) => {
1360
+ resolverRef.current = resolve;
1361
+ });
1362
+ }, []);
1363
+ const handleChoice = useCallback((choice) => {
1364
+ setOpen(false);
1365
+ const r = resolverRef.current;
1366
+ resolverRef.current = null;
1367
+ r?.(choice === "discard");
1368
+ }, []);
1369
+ return {
1370
+ confirm,
1371
+ dialog: /* @__PURE__ */ jsx(
1372
+ ConfirmDialog,
1373
+ {
1374
+ open,
1375
+ title: state.title,
1376
+ body: state.body,
1377
+ saveLabel: "",
1378
+ discardLabel: state.confirmLabel,
1379
+ cancelLabel: state.cancelLabel,
1380
+ onChoice: handleChoice
1381
+ }
1382
+ )
1383
+ };
1384
+ };
1385
+ var ClipboardToast = ({ message, variant = "copy", onDismiss }) => {
1386
+ useEffect(() => {
1387
+ const t = window.setTimeout(onDismiss, 2500);
1388
+ return () => window.clearTimeout(t);
1389
+ }, [message, onDismiss]);
1390
+ const Icon = variant === "paste" ? ClipboardPaste : Copy;
1391
+ return /* @__PURE__ */ jsxs(
1392
+ "div",
1393
+ {
1394
+ role: "status",
1395
+ "aria-live": "polite",
1396
+ className: "ra-clipboard-toast",
1397
+ children: [
1398
+ /* @__PURE__ */ jsx(Icon, { className: "w-3.5 h-3.5 shrink-0", "aria-hidden": "true" }),
1399
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: message })
1400
+ ]
1401
+ }
1402
+ );
1403
+ };
1404
+ function useShellClipboard(args) {
1405
+ const {
1406
+ enabled,
1407
+ appId,
1408
+ recordType,
1409
+ i18n,
1410
+ editorCtx,
1411
+ editingScope,
1412
+ editorHeaderLabel,
1413
+ isCollection,
1414
+ selectedItemId,
1415
+ selectedRecordId,
1416
+ resolved,
1417
+ onTelemetry,
1418
+ onCopyOverride,
1419
+ onPasteOverride,
1420
+ onLeftSelectRef
1421
+ } = args;
1422
+ const clipboard = useRecordClipboard({
1423
+ appId,
1424
+ recordType: recordType ?? "__default"
1425
+ });
1426
+ const pasteConfirm = usePasteConfirm();
1427
+ const [notice, setNotice] = useState(null);
1428
+ const copyCurrent = useCallback(() => {
1429
+ if (!enabled || !editingScope) return;
1430
+ const sourceValue = onCopyOverride ? onCopyOverride({ value: editorCtx.value, scope: editingScope }) : cloneValue(editorCtx.value);
1431
+ clipboard.set({
1432
+ value: sourceValue,
1433
+ sourceScope: editingScope,
1434
+ sourceRecordId: resolved.recordId,
1435
+ sourceLabel: editorHeaderLabel
1436
+ });
1437
+ onTelemetry?.({ type: "clipboard.copy", recordType, sourceRef: editingScope.raw });
1438
+ setNotice({
1439
+ message: i18n.copyToast.replace("{name}", editorHeaderLabel ?? editingScope.raw),
1440
+ variant: "copy"
1441
+ });
1442
+ }, [
1443
+ enabled,
1444
+ editingScope,
1445
+ onCopyOverride,
1446
+ editorCtx.value,
1447
+ clipboard,
1448
+ editorHeaderLabel,
1449
+ onTelemetry,
1450
+ recordType,
1451
+ i18n.copyToast,
1452
+ resolved.recordId
1453
+ ]);
1454
+ const pasteCurrent = useCallback(async () => {
1455
+ if (!enabled || !editingScope || !clipboard.entry) return;
1456
+ const compat = checkPasteCompatibility(clipboard.entry.sourceScope, editingScope);
1457
+ if (compat.status === "denied") {
1458
+ setNotice({ message: compat.reason ?? "Paste not allowed here", variant: "paste" });
1459
+ return;
1460
+ }
1461
+ if (compat.status === "warn") {
1462
+ const ok = await pasteConfirm.confirm({
1463
+ title: i18n.pasteWarnTitle,
1464
+ body: compat.reason ?? "",
1465
+ confirmLabel: i18n.pasteWarnContinue,
1466
+ cancelLabel: i18n.pasteConfirmCancel
1467
+ });
1468
+ if (!ok) return;
1469
+ }
1470
+ const willReplace = resolved.source === "self";
1471
+ if (willReplace) {
1472
+ const ok = await pasteConfirm.confirm({
1473
+ title: i18n.pasteConfirmTitle,
1474
+ body: i18n.pasteConfirmBody.replace(
1475
+ "{destination}",
1476
+ editorHeaderLabel ?? editingScope.raw
1477
+ ),
1478
+ confirmLabel: i18n.pasteConfirmReplace,
1479
+ cancelLabel: i18n.pasteConfirmCancel
1480
+ });
1481
+ if (!ok) return;
1482
+ }
1483
+ const sourceParsed = clipboard.entry.sourceScope;
1484
+ const transformed = onPasteOverride ? onPasteOverride(
1485
+ { value: clipboard.entry.value, sourceScope: sourceParsed },
1486
+ { scope: editingScope, currentValue: editorCtx.value ?? null }
1487
+ ) : cloneValue(clipboard.entry.value);
1488
+ if (transformed === null) return;
1489
+ editorCtx.onChange(transformed);
1490
+ onTelemetry?.({
1491
+ type: "clipboard.paste",
1492
+ recordType,
1493
+ sourceRef: clipboard.entry.sourceScope.raw,
1494
+ destinationRef: editingScope.raw,
1495
+ replaced: willReplace
1496
+ });
1497
+ setNotice({
1498
+ message: i18n.pasteToast.replace("{name}", clipboard.entry.sourceLabel ?? clipboard.entry.sourceScope.raw),
1499
+ variant: "paste"
1500
+ });
1501
+ }, [
1502
+ enabled,
1503
+ editingScope,
1504
+ clipboard.entry,
1505
+ pasteConfirm,
1506
+ resolved.source,
1507
+ onPasteOverride,
1508
+ editorCtx,
1509
+ onTelemetry,
1510
+ recordType,
1511
+ editorHeaderLabel,
1512
+ i18n.pasteWarnTitle,
1513
+ i18n.pasteWarnContinue,
1514
+ i18n.pasteConfirmCancel,
1515
+ i18n.pasteConfirmTitle,
1516
+ i18n.pasteConfirmBody,
1517
+ i18n.pasteConfirmReplace,
1518
+ i18n.pasteToast
1519
+ ]);
1520
+ const editorPasteCompat = useMemo(() => {
1521
+ if (!enabled || !clipboard.entry || !editingScope) return null;
1522
+ const sameRecord = clipboard.entry.sourceRecordId && resolved.recordId && clipboard.entry.sourceRecordId === resolved.recordId;
1523
+ const sameAnchor = !clipboard.entry.sourceRecordId && clipboard.entry.sourceScope.raw === editingScope.raw;
1524
+ if (sameRecord || sameAnchor) return { status: "denied" };
1525
+ return checkPasteCompatibility(clipboard.entry.sourceScope, editingScope);
1526
+ }, [enabled, clipboard.entry, editingScope, resolved.recordId]);
1527
+ const editorClipboard = enabled && editingScope ? {
1528
+ onCopy: copyCurrent,
1529
+ onPaste: () => {
1530
+ void pasteCurrent();
1531
+ },
1532
+ canCopy: true,
1533
+ canPaste: !!clipboard.entry && editorPasteCompat?.status !== "denied",
1534
+ pasteSourceLabel: clipboard.entry?.sourceLabel,
1535
+ pasteWillReplace: resolved.source === "self" && !!clipboard.entry
1536
+ } : void 0;
1537
+ const [pendingPasteTarget, setPendingPasteTarget] = useState(null);
1538
+ useEffect(() => {
1539
+ if (!pendingPasteTarget) return;
1540
+ if (!editingScope) return;
1541
+ const matched = pendingPasteTarget.kind === "record" ? (isCollection ? selectedItemId : selectedRecordId) === pendingPasteTarget.recordId : editingScope.raw === pendingPasteTarget.ref;
1542
+ if (!matched) return;
1543
+ const t = window.setTimeout(() => {
1544
+ setPendingPasteTarget(null);
1545
+ void pasteCurrent();
1546
+ }, 0);
1547
+ return () => window.clearTimeout(t);
1548
+ }, [pendingPasteTarget, editingScope, isCollection, selectedItemId, selectedRecordId, pasteCurrent]);
1549
+ const rowClipboard = enabled ? (record) => {
1550
+ const summaryHasData = record.data != null;
1551
+ const sourceParsed = record.scope;
1552
+ const compat = clipboard.entry ? checkPasteCompatibility(clipboard.entry.sourceScope, sourceParsed) : null;
1553
+ const sameTarget = clipboard.entry ? clipboard.entry.sourceRecordId && record.id ? clipboard.entry.sourceRecordId === record.id : clipboard.entry.sourceScope.raw === record.ref : false;
1554
+ return {
1555
+ onCopy: summaryHasData ? () => {
1556
+ const value = onCopyOverride ? onCopyOverride({ value: record.data, scope: sourceParsed }) : cloneValue(record.data);
1557
+ clipboard.set({
1558
+ value,
1559
+ sourceScope: sourceParsed,
1560
+ sourceRecordId: record.id ?? void 0,
1561
+ sourceLabel: record.label
1562
+ });
1563
+ onTelemetry?.({ type: "clipboard.copy", recordType, sourceRef: record.ref });
1564
+ setNotice({
1565
+ message: i18n.copyToast.replace("{name}", record.label),
1566
+ variant: "copy"
1567
+ });
1568
+ } : void 0,
1569
+ onPaste: () => {
1570
+ onLeftSelectRef.current?.(record);
1571
+ setPendingPasteTarget(
1572
+ record.id ? { kind: "record", recordId: record.id } : { kind: "anchor", ref: record.ref }
1573
+ );
1574
+ },
1575
+ canPaste: !!clipboard.entry && !sameTarget && compat?.status !== "denied",
1576
+ pasteWillReplace: record.status === "configured",
1577
+ clipboardSourceLabel: clipboard.entry?.sourceLabel
1578
+ };
1579
+ } : void 0;
1580
+ const toast = notice ? /* @__PURE__ */ jsx(
1581
+ ClipboardToast,
1582
+ {
1583
+ message: notice.message,
1584
+ variant: notice.variant,
1585
+ onDismiss: () => setNotice(null)
1586
+ }
1587
+ ) : null;
1588
+ return {
1589
+ editorClipboard,
1590
+ rowClipboard,
1591
+ confirmDialog: pasteConfirm.dialog,
1592
+ toast,
1593
+ store: clipboard
1594
+ };
1595
+ }
1596
+ var LABELS = {
1597
+ collection: "Global",
1598
+ product: "Products",
1599
+ facet: "Shared",
1600
+ variant: "Variants",
1601
+ batch: "Batches",
1602
+ rule: "Rules"
1603
+ };
1604
+ var ScopeTabs = ({
1605
+ scopes,
1606
+ active,
1607
+ onChange,
1608
+ loading = false,
1609
+ counts,
1610
+ icons
1611
+ }) => {
1612
+ const iconMap = icons ?? DEFAULT_ICONS.scope;
1613
+ return /* @__PURE__ */ jsx("div", { role: "tablist", className: "ra-tabs", "aria-label": "Record scope", children: scopes.map((s) => {
1614
+ const Icon = iconMap[s] ?? DEFAULT_ICONS.scope[s];
1615
+ const isActive = active === s;
1616
+ const count = counts?.[s];
1617
+ return /* @__PURE__ */ jsxs(
1618
+ "button",
1619
+ {
1620
+ type: "button",
1621
+ role: "tab",
1622
+ "aria-selected": isActive,
1669
1623
  onClick: () => onChange(s),
1670
1624
  disabled: loading,
1671
1625
  className: "ra-tab",
@@ -2030,104 +1984,6 @@ var ProductList = RecordList;
2030
1984
  var FacetList = RecordList;
2031
1985
  var VariantList = RecordList;
2032
1986
  var BatchList = RecordList;
2033
- var EMPTY_RULE_FILTERS = { facetKeys: [], minClauses: null };
2034
- var COMPLEXITY_THRESHOLDS = [3, 5, 10];
2035
- var ruleOf = (r) => r.facetRule ?? null;
2036
- function RuleFilterChips({ source, value, onChange }) {
2037
- const facetKeyEntries = useMemo(() => {
2038
- const counts = /* @__PURE__ */ new Map();
2039
- for (const r of source) {
2040
- const rule = ruleOf(r);
2041
- if (!rule) continue;
2042
- for (const c of rule.all ?? []) {
2043
- counts.set(c.facetKey, (counts.get(c.facetKey) ?? 0) + 1);
2044
- }
2045
- }
2046
- return Array.from(counts.entries()).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
2047
- }, [source]);
2048
- const maxClauses = useMemo(() => {
2049
- let max = 0;
2050
- for (const r of source) {
2051
- const rule = ruleOf(r);
2052
- if (!rule) continue;
2053
- max = Math.max(max, rule.all?.length ?? 0);
2054
- }
2055
- return max;
2056
- }, [source]);
2057
- if (facetKeyEntries.length === 0 && maxClauses < 2) return null;
2058
- const toggleKey = (key) => {
2059
- const has = value.facetKeys.includes(key);
2060
- const next = has ? value.facetKeys.filter((k) => k !== key) : [...value.facetKeys, key];
2061
- onChange({ ...value, facetKeys: next });
2062
- };
2063
- const setMin = (n) => onChange({ ...value, minClauses: n });
2064
- const hasAny = value.facetKeys.length > 0 || value.minClauses != null;
2065
- return /* @__PURE__ */ jsxs("div", { className: "ra-rule-filters", children: [
2066
- /* @__PURE__ */ jsx("div", { className: "ra-rule-filters-row", role: "group", "aria-label": "Filter rules by facet", children: facetKeyEntries.map(([key, count]) => {
2067
- const active = value.facetKeys.includes(key);
2068
- return /* @__PURE__ */ jsxs(
2069
- "button",
2070
- {
2071
- type: "button",
2072
- onClick: () => toggleKey(key),
2073
- className: "ra-rule-filter-chip",
2074
- "data-active": active ? "true" : "false",
2075
- "aria-pressed": active,
2076
- title: `Show rules using ${key}`,
2077
- children: [
2078
- /* @__PURE__ */ jsx("span", { className: "ra-rule-filter-chip-label", children: key }),
2079
- /* @__PURE__ */ jsx("span", { className: "ra-rule-filter-chip-count", children: count })
2080
- ]
2081
- },
2082
- key
2083
- );
2084
- }) }),
2085
- maxClauses >= 2 && /* @__PURE__ */ jsx("div", { className: "ra-rule-filters-row", role: "group", "aria-label": "Filter by clause count", children: COMPLEXITY_THRESHOLDS.filter((n) => maxClauses >= n).map((n) => {
2086
- const active = value.minClauses === n;
2087
- return /* @__PURE__ */ jsxs(
2088
- "button",
2089
- {
2090
- type: "button",
2091
- onClick: () => setMin(active ? null : n),
2092
- className: "ra-rule-filter-chip",
2093
- "data-tone": "complexity",
2094
- "data-active": active ? "true" : "false",
2095
- "aria-pressed": active,
2096
- title: `Only rules with \u2265 ${n} facets`,
2097
- children: [
2098
- "\u2265 ",
2099
- n,
2100
- " facets"
2101
- ]
2102
- },
2103
- n
2104
- );
2105
- }) }),
2106
- hasAny && /* @__PURE__ */ jsx(
2107
- "button",
2108
- {
2109
- type: "button",
2110
- onClick: () => onChange(EMPTY_RULE_FILTERS),
2111
- className: "ra-rule-filter-clear",
2112
- "aria-label": "Clear rule filters",
2113
- children: "Clear filters"
2114
- }
2115
- )
2116
- ] });
2117
- }
2118
- function applyRuleFilters(items, filters) {
2119
- if (filters.facetKeys.length === 0 && filters.minClauses == null) return items;
2120
- return items.filter((r) => {
2121
- const rule = ruleOf(r);
2122
- if (!rule) return false;
2123
- if (filters.minClauses != null && (rule.all?.length ?? 0) < filters.minClauses) return false;
2124
- if (filters.facetKeys.length > 0) {
2125
- const keys = new Set((rule.all ?? []).map((c) => c.facetKey));
2126
- if (!filters.facetKeys.some((k) => keys.has(k))) return false;
2127
- }
2128
- return true;
2129
- });
2130
- }
2131
1987
  var COLLAPSED_FACET_CAP = 6;
2132
1988
  function FacetBrowseFilter({
2133
1989
  facets,
@@ -2509,6 +2365,103 @@ var createDefaultDeepLinkAdapter = (paramNames) => ({
2509
2365
  }
2510
2366
  });
2511
2367
 
2368
+ // src/components/RecordsAdmin/data/postMessageDeepLinkAdapter.ts
2369
+ var CONTEXT_KEYS = [
2370
+ "dark",
2371
+ "appId",
2372
+ "collectionId",
2373
+ "productId",
2374
+ "proofId",
2375
+ "lang",
2376
+ "theme"
2377
+ ];
2378
+ var findQueryString = (loc) => {
2379
+ if (loc.search && loc.search.length > 1) {
2380
+ return loc.search.startsWith("?") ? loc.search.slice(1) : loc.search;
2381
+ }
2382
+ if (loc.hash && loc.hash.includes("?")) {
2383
+ return loc.hash.slice(loc.hash.indexOf("?") + 1);
2384
+ }
2385
+ return "";
2386
+ };
2387
+ var findPath = (loc) => {
2388
+ if (loc.hash && loc.hash.startsWith("#")) {
2389
+ const hash = loc.hash.slice(1);
2390
+ const qIdx = hash.indexOf("?");
2391
+ const hashPath = qIdx >= 0 ? hash.slice(0, qIdx) : hash;
2392
+ if (hashPath) return hashPath;
2393
+ }
2394
+ return loc.pathname || "/";
2395
+ };
2396
+ var isInSmartLinksIframe = () => {
2397
+ if (typeof window === "undefined") return false;
2398
+ try {
2399
+ return window.parent != null && window.parent !== window;
2400
+ } catch {
2401
+ return true;
2402
+ }
2403
+ };
2404
+ var createPostMessageDeepLinkAdapter = (paramNames) => {
2405
+ const lastShellState = {};
2406
+ const post = (path) => {
2407
+ if (typeof window === "undefined") return;
2408
+ const params = new URLSearchParams(findQueryString(window.location));
2409
+ const context = {};
2410
+ const state = {};
2411
+ params.forEach((value, key) => {
2412
+ if (key === "theme") return;
2413
+ if (CONTEXT_KEYS.includes(key)) context[key] = value;
2414
+ else state[key] = value;
2415
+ });
2416
+ const overlay = (key, paramKey) => {
2417
+ const v = lastShellState[key];
2418
+ if (v == null || v === "") delete state[paramKey];
2419
+ else state[paramKey] = v;
2420
+ };
2421
+ overlay("recordId", paramNames.recordId);
2422
+ overlay("scope", paramNames.scope);
2423
+ overlay("view", paramNames.view);
2424
+ try {
2425
+ window.parent.postMessage({
2426
+ type: "smartlinks-route-change",
2427
+ path,
2428
+ context,
2429
+ state,
2430
+ appId: context.appId
2431
+ }, "*");
2432
+ } catch {
2433
+ }
2434
+ };
2435
+ return {
2436
+ read() {
2437
+ if (typeof window === "undefined") return {};
2438
+ const params = new URLSearchParams(findQueryString(window.location));
2439
+ return {
2440
+ recordId: params.get(paramNames.recordId),
2441
+ scope: params.get(paramNames.scope),
2442
+ view: params.get(paramNames.view)
2443
+ };
2444
+ },
2445
+ write(partial) {
2446
+ if (typeof window === "undefined") return;
2447
+ if ("recordId" in partial) lastShellState.recordId = partial.recordId ?? null;
2448
+ if ("scope" in partial) lastShellState.scope = partial.scope ?? null;
2449
+ if ("view" in partial) lastShellState.view = partial.view ?? null;
2450
+ post(findPath(window.location));
2451
+ },
2452
+ subscribe(listener) {
2453
+ if (typeof window === "undefined") return () => {
2454
+ };
2455
+ window.addEventListener("popstate", listener);
2456
+ window.addEventListener("hashchange", listener);
2457
+ return () => {
2458
+ window.removeEventListener("popstate", listener);
2459
+ window.removeEventListener("hashchange", listener);
2460
+ };
2461
+ }
2462
+ };
2463
+ };
2464
+
2512
2465
  // src/components/RecordsAdmin/hooks/useDeepLinkState.ts
2513
2466
  var SMART_PUSH = ["record.open", "record.close", "scope"];
2514
2467
  var classify = (mode, kind) => {
@@ -2528,7 +2481,7 @@ function useDeepLinkState(options) {
2528
2481
  if (!enabled) return null;
2529
2482
  if (options?.adapter) return options.adapter;
2530
2483
  if (!defaultAdapterRef.current) {
2531
- defaultAdapterRef.current = createDefaultDeepLinkAdapter(paramNames);
2484
+ defaultAdapterRef.current = isInSmartLinksIframe() ? createPostMessageDeepLinkAdapter(paramNames) : createDefaultDeepLinkAdapter(paramNames);
2532
2485
  }
2533
2486
  return defaultAdapterRef.current;
2534
2487
  }, [enabled, options?.adapter, paramNames]);
@@ -2902,6 +2855,49 @@ function RecordEditor({
2902
2855
  )
2903
2856
  ] });
2904
2857
  }
2858
+ var isFacetRuleValid = (rule) => {
2859
+ if (!rule || !Array.isArray(rule.all) || rule.all.length === 0) return false;
2860
+ return rule.all.every(
2861
+ (c) => !!c.facetKey && Array.isArray(c.anyOf) && c.anyOf.length > 0
2862
+ );
2863
+ };
2864
+ function useRulePreview(args) {
2865
+ const {
2866
+ SL,
2867
+ collectionId,
2868
+ appId,
2869
+ rule,
2870
+ limit = 20,
2871
+ debounceMs = 350,
2872
+ enabled = true
2873
+ } = args;
2874
+ const [debouncedRule, setDebouncedRule] = useState(rule);
2875
+ const timer = useRef(null);
2876
+ useEffect(() => {
2877
+ if (timer.current) clearTimeout(timer.current);
2878
+ timer.current = setTimeout(() => setDebouncedRule(rule), debounceMs);
2879
+ return () => {
2880
+ if (timer.current) clearTimeout(timer.current);
2881
+ };
2882
+ }, [rule, debounceMs]);
2883
+ const valid = isFacetRuleValid(debouncedRule);
2884
+ const query = useQuery({
2885
+ queryKey: ["records-admin", "preview-rule", collectionId, appId, debouncedRule, limit],
2886
+ enabled: enabled && !!collectionId && !!appId && valid,
2887
+ staleTime: 3e4,
2888
+ queryFn: () => SL.app.records.previewRule(collectionId, appId, {
2889
+ facetRule: debouncedRule,
2890
+ limit
2891
+ })
2892
+ });
2893
+ return {
2894
+ totalMatches: query.data?.total ?? null,
2895
+ sampleProductIds: (query.data?.matchingProducts ?? []).map((p) => p.productId),
2896
+ isLoading: query.isFetching,
2897
+ isStale: rule !== debouncedRule,
2898
+ error: query.error ?? null
2899
+ };
2900
+ }
2905
2901
  function summariseRule(clauses, facets) {
2906
2902
  let valueCount = 0;
2907
2903
  const parts = clauses.map((c) => {
@@ -4298,11 +4294,11 @@ var statusDot = (status) => {
4298
4294
  }
4299
4295
  };
4300
4296
  var UnsavedTray = ({
4301
- drafts,
4297
+ items,
4302
4298
  isSaving,
4303
4299
  onSaveAll,
4304
4300
  onDiscardAll,
4305
- onOpenDraft,
4301
+ onOpenItem,
4306
4302
  onJumpToError,
4307
4303
  saveLabel,
4308
4304
  discardLabel,
@@ -4318,27 +4314,8 @@ var UnsavedTray = ({
4318
4314
  const wrapRef = useRef(null);
4319
4315
  const SI = SaveIcon ?? Save;
4320
4316
  const DI = DiscardIcon ?? Undo2;
4321
- const uniqueDrafts = useMemo(() => {
4322
- const byBucket = /* @__PURE__ */ new Map();
4323
- for (const d of drafts) {
4324
- const bucket = d.recordId || d.scopeRaw || d.key;
4325
- const existing = byBucket.get(bucket);
4326
- if (!existing) {
4327
- byBucket.set(bucket, d);
4328
- continue;
4329
- }
4330
- const existingSynthetic = existing.key.startsWith("draft:");
4331
- const incomingSynthetic = d.key.startsWith("draft:");
4332
- if (existingSynthetic && !incomingSynthetic) byBucket.set(bucket, d);
4333
- }
4334
- return Array.from(byBucket.values()).sort((a, b) => a.order - b.order);
4335
- }, [drafts]);
4336
- const liveDrafts = useMemo(
4337
- () => uniqueDrafts.filter((d) => d.status !== "saved"),
4338
- [uniqueDrafts]
4339
- );
4340
- const total = liveDrafts.length;
4341
- const errors = liveDrafts.filter((d) => d.status === "error").length;
4317
+ const total = items.length;
4318
+ const errors = items.filter((i) => i.status === "error").length;
4342
4319
  const isSingle = total === 1;
4343
4320
  useEffect(() => {
4344
4321
  if (!open) return;
@@ -4349,7 +4326,7 @@ var UnsavedTray = ({
4349
4326
  return () => window.removeEventListener("mousedown", onDoc);
4350
4327
  }, [open]);
4351
4328
  if (total === 0) return null;
4352
- const countLabel = isSingle ? liveDrafts[0].label || "this record" : countTemplate.replace("{n}", String(total));
4329
+ const countLabel = isSingle ? items[0].label || "this record" : countTemplate.replace("{n}", String(total));
4353
4330
  return /* @__PURE__ */ jsxs(
4354
4331
  "div",
4355
4332
  {
@@ -4419,14 +4396,14 @@ var UnsavedTray = ({
4419
4396
  }
4420
4397
  )
4421
4398
  ] }),
4422
- open && !isSingle && /* @__PURE__ */ jsx("div", { className: "ra-unsaved-popover", role: "menu", children: liveDrafts.map((d) => /* @__PURE__ */ jsxs(
4399
+ open && !isSingle && /* @__PURE__ */ jsx("div", { className: "ra-unsaved-popover", role: "menu", children: items.map((it) => /* @__PURE__ */ jsxs(
4423
4400
  "button",
4424
4401
  {
4425
4402
  type: "button",
4426
4403
  className: "ra-unsaved-popover-row",
4427
4404
  onClick: () => {
4428
4405
  setOpen(false);
4429
- onOpenDraft?.(d);
4406
+ onOpenItem?.(it);
4430
4407
  },
4431
4408
  role: "menuitem",
4432
4409
  children: [
@@ -4434,31 +4411,29 @@ var UnsavedTray = ({
4434
4411
  "span",
4435
4412
  {
4436
4413
  className: "ra-unsaved-popover-dot",
4437
- style: { background: statusDot(d.status) },
4414
+ style: { background: statusDot(it.status) },
4438
4415
  "aria-hidden": "true"
4439
4416
  }
4440
4417
  ),
4441
- /* @__PURE__ */ jsx("span", { className: "ra-unsaved-popover-label", children: d.label || "Default" }),
4442
- d.context && /* @__PURE__ */ jsx("span", { className: "ra-unsaved-popover-ctx", children: d.context }),
4443
- d.status === "error" && /* @__PURE__ */ jsx("span", { className: "ra-unsaved-popover-err", children: "failed" })
4418
+ /* @__PURE__ */ jsx("span", { className: "ra-unsaved-popover-label", children: it.label || "Default" }),
4419
+ it.status === "error" && /* @__PURE__ */ jsx("span", { className: "ra-unsaved-popover-err", children: "failed" })
4444
4420
  ]
4445
4421
  },
4446
- d.key
4422
+ it.editorId
4447
4423
  )) })
4448
4424
  ]
4449
4425
  }
4450
4426
  );
4451
4427
  };
4452
4428
  var HEADER_PILL_OVERFLOW_THRESHOLD = 5;
4453
- function canRenderInHeader(drafts) {
4454
- const live = drafts.filter((d) => d.status !== "saved");
4455
- if (live.length === 0) return false;
4456
- if (live.length >= HEADER_PILL_OVERFLOW_THRESHOLD) return false;
4457
- if (live.some((d) => d.status === "error")) return false;
4429
+ function canRenderInHeader(items) {
4430
+ if (items.length === 0) return false;
4431
+ if (items.length >= HEADER_PILL_OVERFLOW_THRESHOLD) return false;
4432
+ if (items.some((i) => i.status === "error")) return false;
4458
4433
  return true;
4459
4434
  }
4460
4435
  var HeaderUnsavedPill = ({
4461
- drafts,
4436
+ items,
4462
4437
  isSaving,
4463
4438
  onSaveAll,
4464
4439
  onDiscardAll,
@@ -4472,24 +4447,8 @@ var HeaderUnsavedPill = ({
4472
4447
  }) => {
4473
4448
  const SI = SaveIcon ?? Save;
4474
4449
  const DI = DiscardIcon ?? Undo2;
4475
- const liveDrafts = useMemo(() => {
4476
- const byBucket = /* @__PURE__ */ new Map();
4477
- for (const d of drafts) {
4478
- if (d.status === "saved") continue;
4479
- const bucket = d.recordId || d.scopeRaw || d.key;
4480
- const existing = byBucket.get(bucket);
4481
- if (!existing) {
4482
- byBucket.set(bucket, d);
4483
- continue;
4484
- }
4485
- const existingSynthetic = existing.key.startsWith("draft:");
4486
- const incomingSynthetic = d.key.startsWith("draft:");
4487
- if (existingSynthetic && !incomingSynthetic) byBucket.set(bucket, d);
4488
- }
4489
- return Array.from(byBucket.values());
4490
- }, [drafts]);
4491
- if (liveDrafts.length === 0) return null;
4492
- const total = liveDrafts.length;
4450
+ if (items.length === 0) return null;
4451
+ const total = items.length;
4493
4452
  const isSingle = total === 1;
4494
4453
  return /* @__PURE__ */ jsxs(
4495
4454
  "div",
@@ -4537,8 +4496,8 @@ var HeaderUnsavedPill = ({
4537
4496
  };
4538
4497
  var SaveAllProgress = ({
4539
4498
  open,
4540
- drafts,
4541
- store,
4499
+ items,
4500
+ saveOne,
4542
4501
  onClose,
4543
4502
  onJumpToError,
4544
4503
  i18n
@@ -4546,42 +4505,44 @@ var SaveAllProgress = ({
4546
4505
  const [running, setRunning] = useState(false);
4547
4506
  const [done, setDone] = useState(false);
4548
4507
  const stopRef = useRef(false);
4549
- const [batch, setBatch] = useState([]);
4508
+ const [batchIds, setBatchIds] = useState([]);
4550
4509
  useEffect(() => {
4551
4510
  if (!open) {
4552
4511
  setRunning(false);
4553
4512
  setDone(false);
4554
4513
  stopRef.current = false;
4555
- setBatch([]);
4514
+ setBatchIds([]);
4556
4515
  return;
4557
4516
  }
4558
- const initial = drafts.filter((d) => d.status !== "saved");
4559
- setBatch(initial);
4517
+ const initial = items.map((i) => i.editorId);
4518
+ setBatchIds(initial);
4560
4519
  void runBatch(initial);
4561
4520
  }, [open]);
4562
- const runBatch = async (items) => {
4521
+ const runBatch = async (ids) => {
4563
4522
  setRunning(true);
4564
4523
  setDone(false);
4565
4524
  stopRef.current = false;
4566
- for (const item of items) {
4525
+ for (const id of ids) {
4567
4526
  if (stopRef.current) break;
4568
- const live = store.get(item.key);
4569
- if (!live) continue;
4570
- if (live.status === "saved") continue;
4571
- store.setStatus(live.key, "saving");
4572
4527
  try {
4573
- await live.save();
4574
- if (store.has(live.key)) store.setStatus(live.key, "saved");
4575
- } catch (err) {
4576
- store.setStatus(live.key, "error", err);
4528
+ await saveOne(id);
4529
+ } catch {
4577
4530
  }
4578
4531
  }
4579
4532
  setRunning(false);
4580
4533
  setDone(true);
4581
4534
  };
4582
- const visible = batch.map((d) => store.get(d.key) ?? d);
4535
+ const visible = useMemo(() => batchIds.map((id) => {
4536
+ const live = items.find((i) => i.editorId === id);
4537
+ return live ?? {
4538
+ editorId: id,
4539
+ label: "",
4540
+ status: "saved",
4541
+ scope: { kind: "collection", raw: "" }
4542
+ };
4543
+ }), [batchIds, items]);
4583
4544
  const total = visible.length;
4584
- const completed = visible.filter((d) => d.status === "saved" || d.status === "error").length;
4545
+ const completed = visible.filter((d) => d.status !== "dirty" && d.status !== "saving").length;
4585
4546
  const errors = visible.filter((d) => d.status === "error");
4586
4547
  const successAll = done && errors.length === 0 && completed === total;
4587
4548
  useEffect(() => {
@@ -4630,7 +4591,7 @@ var SaveAllProgress = ({
4630
4591
  ] }),
4631
4592
  /* @__PURE__ */ jsx("span", { className: "ra-saveall-label", children: d.label || "Default" }),
4632
4593
  d.status === "error" && /* @__PURE__ */ jsx("span", { className: "ra-saveall-err", title: String(d.error?.message ?? d.error ?? ""), children: d.error?.message ?? "Save failed" })
4633
- ] }, d.key)) }),
4594
+ ] }, d.editorId)) }),
4634
4595
  /* @__PURE__ */ jsxs("div", { className: "ra-saveall-actions", children: [
4635
4596
  running && /* @__PURE__ */ jsx(
4636
4597
  "button",
@@ -4661,7 +4622,7 @@ var SaveAllProgress = ({
4661
4622
  {
4662
4623
  type: "button",
4663
4624
  className: "ra-unsaved-btn ra-unsaved-btn-primary",
4664
- onClick: () => void runBatch(errors),
4625
+ onClick: () => void runBatch(errors.map((e) => e.editorId)),
4665
4626
  children: i18n.retryFailed
4666
4627
  }
4667
4628
  )
@@ -4682,81 +4643,734 @@ var SaveAllProgress = ({
4682
4643
  }
4683
4644
  );
4684
4645
  };
4685
- var ClipboardToast = ({ message, variant = "copy", onDismiss }) => {
4686
- useEffect(() => {
4687
- const t = window.setTimeout(onDismiss, 2500);
4688
- return () => window.clearTimeout(t);
4689
- }, [message, onDismiss]);
4690
- const Icon = variant === "paste" ? ClipboardPaste : Copy;
4691
- return /* @__PURE__ */ jsxs(
4692
- "div",
4693
- {
4694
- role: "status",
4695
- "aria-live": "polite",
4696
- className: "ra-clipboard-toast",
4697
- children: [
4698
- /* @__PURE__ */ jsx(Icon, { className: "w-3.5 h-3.5 shrink-0", "aria-hidden": "true" }),
4699
- /* @__PURE__ */ jsx("span", { className: "truncate", children: message })
4700
- ]
4701
- }
4702
- );
4703
- };
4704
4646
 
4705
- // src/components/RecordsAdmin/data/csv.ts
4706
- var escapeCell = (s) => {
4707
- if (/[",\n]/.test(s)) return `"${s.replace(/"/g, '""')}"`;
4708
- return s;
4709
- };
4710
- var parseLine = (line) => {
4711
- const out = [];
4712
- let cur = "";
4713
- let inQuotes = false;
4714
- for (let i = 0; i < line.length; i += 1) {
4715
- const ch = line[i];
4716
- if (inQuotes) {
4717
- if (ch === '"' && line[i + 1] === '"') {
4718
- cur += '"';
4719
- i += 1;
4720
- } else if (ch === '"') inQuotes = false;
4721
- else cur += ch;
4722
- } else if (ch === '"') inQuotes = true;
4723
- else if (ch === ",") {
4724
- out.push(cur);
4725
- cur = "";
4726
- } else cur += ch;
4647
+ // src/components/RecordsAdmin/editor-session/editorStore.ts
4648
+ var isEqual = (a, b) => {
4649
+ if (a === b) return true;
4650
+ try {
4651
+ return JSON.stringify(a) === JSON.stringify(b);
4652
+ } catch {
4653
+ return false;
4727
4654
  }
4728
- out.push(cur);
4729
- return out;
4730
- };
4731
- var exportCsv = (records, schema) => {
4732
- const headers = ["ref", ...schema.columns.map((c) => c.header)];
4733
- const rows = records.filter((r) => r.data != null).map((r) => [r.ref, ...schema.columns.map((c) => c.toCell(r.data))]);
4734
- const csv = [headers, ...rows].map((row) => row.map((cell) => escapeCell(String(cell ?? ""))).join(",")).join("\n");
4735
- return new Blob([csv], { type: "text/csv;charset=utf-8" });
4736
4655
  };
4737
- var importCsv = async (file, schema, ctx) => {
4738
- const text = await file.text();
4739
- const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
4740
- if (lines.length === 0) {
4741
- return { total: 0, saved: 0, failed: 0, errorRows: [], annotatedCsv: "" };
4656
+ var cloneDeep = (v) => {
4657
+ if (v == null) return v;
4658
+ try {
4659
+ return structuredClone(v);
4660
+ } catch {
4661
+ return JSON.parse(JSON.stringify(v));
4742
4662
  }
4743
- const headers = parseLine(lines[0]);
4744
- const refIdx = headers.indexOf("ref");
4745
- const colMap = schema.columns.map((c) => ({ col: c, idx: headers.indexOf(c.header) }));
4746
- const errorRows = [];
4747
- let saved = 0;
4748
- for (let i = 1; i < lines.length; i += 1) {
4749
- const cells = parseLine(lines[i]);
4750
- const ref = refIdx >= 0 ? cells[refIdx] : "";
4751
- if (!ref) {
4752
- errorRows.push({ row: i, error: "Missing ref" });
4753
- continue;
4663
+ };
4664
+ var TITLE_KEYS = ["title", "name", "label", "heading", "question", "slug"];
4665
+ var TITLE_WRAPPERS = ["display", "content", "meta", "data"];
4666
+ var deriveDefaultLabel = (value, spec) => {
4667
+ const pickString = (obj) => {
4668
+ if (!obj || typeof obj !== "object") return void 0;
4669
+ for (const key of TITLE_KEYS) {
4670
+ const raw = obj[key];
4671
+ if (typeof raw === "string" && raw.trim()) return raw.trim();
4754
4672
  }
4755
- const data = {};
4756
- for (const { col, idx } of colMap) {
4757
- if (idx < 0) continue;
4758
- try {
4759
- data[col.key] = col.fromCell(cells[idx] ?? "");
4673
+ return void 0;
4674
+ };
4675
+ const v = value;
4676
+ const top = pickString(v);
4677
+ if (top) return top;
4678
+ if (v && typeof v === "object") {
4679
+ for (const wrapper of TITLE_WRAPPERS) {
4680
+ const nested = pickString(v[wrapper]);
4681
+ if (nested) return nested;
4682
+ }
4683
+ }
4684
+ if (spec.scope.raw?.startsWith("item:")) return "Untitled item";
4685
+ if (spec.scope.kind === "rule") return "Rule";
4686
+ if (spec.scope.kind && spec.scope.kind !== "collection") {
4687
+ return spec.scope.kind.charAt(0).toUpperCase() + spec.scope.kind.slice(1);
4688
+ }
4689
+ return "Default";
4690
+ };
4691
+ var editorIdCounter = 0;
4692
+ var mintEditorId = () => {
4693
+ editorIdCounter += 1;
4694
+ return `ed_${Date.now().toString(36)}_${editorIdCounter.toString(36)}`;
4695
+ };
4696
+ var recomputePinned = (e) => {
4697
+ if (e.status === "saving" || e.status === "error") return true;
4698
+ if (e.status === "dirty") return true;
4699
+ return false;
4700
+ };
4701
+ var recomputeStatus = (e) => {
4702
+ if (e.status === "saving") return "saving";
4703
+ if (e.status === "error") return "error";
4704
+ const valueDiff = !isEqual(e.value, e.baseline);
4705
+ const ruleDiff = !isEqual(e.facetRule, e.baselineFacetRule);
4706
+ if (valueDiff || ruleDiff) return "dirty";
4707
+ return e.status === "saved" ? "saved" : "idle";
4708
+ };
4709
+ var createEditorStore = () => {
4710
+ const map = /* @__PURE__ */ new Map();
4711
+ const listeners = /* @__PURE__ */ new Set();
4712
+ let cachedList = [];
4713
+ let nextOrder = 0;
4714
+ const recompute = () => {
4715
+ cachedList = Array.from(map.values()).sort((a, b) => a.order - b.order);
4716
+ };
4717
+ const emit = () => {
4718
+ recompute();
4719
+ listeners.forEach((l) => l());
4720
+ };
4721
+ const update = (editorId, mut) => {
4722
+ const prev = map.get(editorId);
4723
+ if (!prev) return false;
4724
+ const draft = { ...prev };
4725
+ const result = mut(draft);
4726
+ const next = result ?? draft;
4727
+ next.status = recomputeStatus(next);
4728
+ next.pinned = recomputePinned(next);
4729
+ map.set(editorId, next);
4730
+ emit();
4731
+ return true;
4732
+ };
4733
+ return {
4734
+ list: () => cachedList,
4735
+ get: (editorId) => map.get(editorId),
4736
+ findExistingEditorIdFor(spec) {
4737
+ if (spec.recordId) {
4738
+ for (const entry of map.values()) {
4739
+ if (entry.recordId === spec.recordId) return entry.editorId;
4740
+ }
4741
+ }
4742
+ const wantCreate = !!spec.createMode;
4743
+ for (const entry of map.values()) {
4744
+ if (entry.recordId) continue;
4745
+ if (entry.spec.scope.raw !== spec.scope.raw) continue;
4746
+ if (!!entry.spec.createMode !== wantCreate) continue;
4747
+ return entry.editorId;
4748
+ }
4749
+ return void 0;
4750
+ },
4751
+ open(input) {
4752
+ const editorId = input.editorId ?? mintEditorId();
4753
+ if (map.has(editorId)) return editorId;
4754
+ const seedValue = input.seed?.value ?? input.defaultValue;
4755
+ const seedFacetRule = input.seed?.facetRule ?? input.spec.initialFacetRule ?? null;
4756
+ const source = input.seed?.source ?? "empty";
4757
+ const value = cloneDeep(seedValue);
4758
+ const baseline = cloneDeep(seedValue);
4759
+ const entry = {
4760
+ editorId,
4761
+ spec: input.spec,
4762
+ saveSpec: input.saveSpec,
4763
+ value,
4764
+ baseline,
4765
+ facetRule: seedFacetRule,
4766
+ baselineFacetRule: seedFacetRule,
4767
+ status: "idle",
4768
+ source,
4769
+ recordId: input.seed?.recordId ?? input.spec.recordId,
4770
+ parentValue: input.seed?.parentValue ?? null,
4771
+ label: input.label ?? input.spec.label ?? deriveDefaultLabel(value, input.spec),
4772
+ order: nextOrder++,
4773
+ lastActiveAt: Date.now(),
4774
+ pinned: false,
4775
+ mounted: false
4776
+ };
4777
+ map.set(editorId, entry);
4778
+ emit();
4779
+ return editorId;
4780
+ },
4781
+ close(editorId) {
4782
+ const entry = map.get(editorId);
4783
+ if (!entry) return false;
4784
+ if (entry.pinned) return false;
4785
+ map.delete(editorId);
4786
+ emit();
4787
+ return true;
4788
+ },
4789
+ clearUnpinned() {
4790
+ let changed = false;
4791
+ for (const [id, entry] of map.entries()) {
4792
+ if (!entry.pinned) {
4793
+ map.delete(id);
4794
+ changed = true;
4795
+ }
4796
+ }
4797
+ if (changed) emit();
4798
+ },
4799
+ setValue(editorId, next) {
4800
+ update(editorId, (e) => {
4801
+ const prev = e.value;
4802
+ const resolved = typeof next === "function" ? next(prev) : next;
4803
+ e.value = resolved;
4804
+ if (e.status === "saved" || e.status === "error") {
4805
+ e.status = "idle";
4806
+ e.error = void 0;
4807
+ }
4808
+ if (!e.spec.label) {
4809
+ e.label = deriveDefaultLabel(e.value, e.spec);
4810
+ }
4811
+ });
4812
+ },
4813
+ setFacetRule(editorId, next) {
4814
+ update(editorId, (e) => {
4815
+ e.facetRule = next;
4816
+ if (e.status === "saved" || e.status === "error") {
4817
+ e.status = "idle";
4818
+ e.error = void 0;
4819
+ }
4820
+ });
4821
+ },
4822
+ hydrateFromResolver(editorId, resolved) {
4823
+ const entry = map.get(editorId);
4824
+ if (!entry) return;
4825
+ if (entry.status === "dirty" || entry.status === "saving" || entry.status === "error") return;
4826
+ if (entry.status === "saved") {
4827
+ update(editorId, (e) => {
4828
+ e.source = resolved.source;
4829
+ if (resolved.recordId) e.recordId = resolved.recordId;
4830
+ e.parentValue = resolved.parentValue ?? null;
4831
+ });
4832
+ return;
4833
+ }
4834
+ update(editorId, (e) => {
4835
+ const fresh = resolved.data == null ? e.value : cloneDeep(resolved.data);
4836
+ e.value = fresh;
4837
+ e.baseline = cloneDeep(fresh);
4838
+ const rule = resolved.facetRule ?? e.facetRule;
4839
+ e.facetRule = rule;
4840
+ e.baselineFacetRule = rule;
4841
+ e.source = resolved.source;
4842
+ if (resolved.recordId) e.recordId = resolved.recordId;
4843
+ e.parentValue = resolved.parentValue ?? null;
4844
+ if (!e.spec.label) e.label = deriveDefaultLabel(fresh, e.spec);
4845
+ });
4846
+ },
4847
+ markMounted(editorId) {
4848
+ update(editorId, (e) => {
4849
+ e.mounted = true;
4850
+ });
4851
+ },
4852
+ markActive(editorId) {
4853
+ update(editorId, (e) => {
4854
+ e.lastActiveAt = Date.now();
4855
+ });
4856
+ },
4857
+ async save(editorId) {
4858
+ const entry = map.get(editorId);
4859
+ if (!entry) return;
4860
+ if (entry.status !== "dirty" && entry.status !== "error") return;
4861
+ const persistedValue = entry.value;
4862
+ const persistedFacetRule = entry.facetRule;
4863
+ const { ctx, anchors } = entry.saveSpec;
4864
+ const spec = entry.spec;
4865
+ update(editorId, (e) => {
4866
+ e.status = "saving";
4867
+ e.error = void 0;
4868
+ });
4869
+ try {
4870
+ let nextRecordId = entry.recordId;
4871
+ if (entry.recordId && entry.source === "self") {
4872
+ await updateRecord(ctx, entry.recordId, {
4873
+ data: persistedValue,
4874
+ facetRule: persistedFacetRule
4875
+ });
4876
+ } else if (spec.createMode) {
4877
+ const created = await createRecord(ctx, {
4878
+ ref: spec.scope.kind === "rule" && spec.scope.raw ? spec.scope.raw : spec.ref,
4879
+ scope: anchors,
4880
+ data: persistedValue,
4881
+ facetRule: persistedFacetRule
4882
+ });
4883
+ nextRecordId = created?.id ?? nextRecordId;
4884
+ } else {
4885
+ const upserted = await upsertRecord(ctx, {
4886
+ ref: spec.scope.kind === "rule" && spec.scope.raw ? spec.scope.raw : spec.ref,
4887
+ scope: anchors,
4888
+ data: persistedValue,
4889
+ facetRule: persistedFacetRule
4890
+ });
4891
+ nextRecordId = upserted.record?.id ?? nextRecordId;
4892
+ }
4893
+ update(editorId, (e) => {
4894
+ e.baseline = cloneDeep(persistedValue);
4895
+ e.baselineFacetRule = persistedFacetRule;
4896
+ e.recordId = nextRecordId;
4897
+ e.source = "self";
4898
+ e.status = "saved";
4899
+ e.error = void 0;
4900
+ });
4901
+ } catch (err) {
4902
+ update(editorId, (e) => {
4903
+ e.status = "error";
4904
+ e.error = err;
4905
+ });
4906
+ throw err;
4907
+ }
4908
+ },
4909
+ reset(editorId) {
4910
+ update(editorId, (e) => {
4911
+ e.value = cloneDeep(e.baseline);
4912
+ e.facetRule = e.baselineFacetRule;
4913
+ e.status = "idle";
4914
+ e.error = void 0;
4915
+ });
4916
+ },
4917
+ async remove(editorId) {
4918
+ const entry = map.get(editorId);
4919
+ if (!entry) return;
4920
+ if (entry.source !== "self") return;
4921
+ if (!entry.recordId) return;
4922
+ await removeRecord(entry.saveSpec.ctx, entry.recordId);
4923
+ map.delete(editorId);
4924
+ emit();
4925
+ },
4926
+ enforcePoolLimit(max, exceptEditorId) {
4927
+ const alive = Array.from(map.values());
4928
+ if (alive.length <= max) return;
4929
+ const evictable = alive.filter((e) => !e.pinned && e.editorId !== exceptEditorId).sort((a, b) => a.lastActiveAt - b.lastActiveAt);
4930
+ let toEvict = alive.length - max;
4931
+ let changed = false;
4932
+ for (const entry of evictable) {
4933
+ if (toEvict <= 0) break;
4934
+ map.delete(entry.editorId);
4935
+ toEvict -= 1;
4936
+ changed = true;
4937
+ }
4938
+ if (changed) emit();
4939
+ },
4940
+ subscribe(listener) {
4941
+ listeners.add(listener);
4942
+ return () => {
4943
+ listeners.delete(listener);
4944
+ };
4945
+ }
4946
+ };
4947
+ };
4948
+ var buildSaveSpec = (ctx, spec) => ({
4949
+ ctx,
4950
+ anchors: parsedRefToScope(spec.scope)
4951
+ });
4952
+ var EditorSessionContext = createContext(null);
4953
+ var useEditorSessionContext = () => {
4954
+ const ctx = useContext(EditorSessionContext);
4955
+ if (!ctx) {
4956
+ throw new Error(
4957
+ "[smartlinks-ui] useEditorSession / useEditorSelection / useDirtyOverview must be used inside <EditorSessionProvider> (RecordsAdminShell mounts one for you)."
4958
+ );
4959
+ }
4960
+ return ctx;
4961
+ };
4962
+ var EditorSessionProvider = ({
4963
+ ctx,
4964
+ children,
4965
+ maxOpenEditors = 8,
4966
+ defaultValueFactory
4967
+ }) => {
4968
+ const storeRef = useRef(null);
4969
+ if (!storeRef.current) storeRef.current = createEditorStore();
4970
+ const currentRef = useRef(void 0);
4971
+ const listenersRef = useRef(/* @__PURE__ */ new Set());
4972
+ const subscribeCurrent = useCallback((cb) => {
4973
+ listenersRef.current.add(cb);
4974
+ return () => {
4975
+ listenersRef.current.delete(cb);
4976
+ };
4977
+ }, []);
4978
+ const getCurrent = useCallback(() => currentRef.current, []);
4979
+ const setCurrentEditorId = useCallback((id) => {
4980
+ if (currentRef.current === id) return;
4981
+ currentRef.current = id;
4982
+ listenersRef.current.forEach((l) => l());
4983
+ }, []);
4984
+ const currentEditorId = useSyncExternalStore(subscribeCurrent, getCurrent, getCurrent);
4985
+ const value = useMemo(() => ({
4986
+ store: storeRef.current,
4987
+ ctx,
4988
+ currentEditorId,
4989
+ setCurrentEditorId,
4990
+ maxOpenEditors,
4991
+ defaultValueFactory
4992
+ }), [ctx, currentEditorId, setCurrentEditorId, maxOpenEditors, defaultValueFactory]);
4993
+ return /* @__PURE__ */ jsx(EditorSessionContext.Provider, { value, children });
4994
+ };
4995
+ var useEntry = (editorId) => {
4996
+ const { store } = useEditorSessionContext();
4997
+ const subscribe = useCallback((cb) => store.subscribe(cb), [store]);
4998
+ const getSnapshot = useCallback(
4999
+ () => editorId ? store.get(editorId) : void 0,
5000
+ [store, editorId]
5001
+ );
5002
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
5003
+ };
5004
+ var useStoreList = () => {
5005
+ const { store } = useEditorSessionContext();
5006
+ const subscribe = useCallback((cb) => store.subscribe(cb), [store]);
5007
+ const getSnapshot = useCallback(() => store.list(), [store]);
5008
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
5009
+ };
5010
+ function useEditorSession(editorId) {
5011
+ const { store, currentEditorId, ctx } = useEditorSessionContext();
5012
+ const queryClient = useQueryClient();
5013
+ const entry = useEntry(editorId);
5014
+ const onChange = useCallback((next) => {
5015
+ if (!editorId) return;
5016
+ store.setValue(editorId, next);
5017
+ }, [store, editorId]);
5018
+ const onFacetRuleChange = useCallback((next) => {
5019
+ if (!editorId) return;
5020
+ store.setFacetRule(editorId, next);
5021
+ }, [store, editorId]);
5022
+ const save = useCallback(async () => {
5023
+ if (!editorId) return;
5024
+ try {
5025
+ await store.save(editorId);
5026
+ const e = store.get(editorId);
5027
+ if (e) {
5028
+ const key = resolvedRecordQueryKey({
5029
+ collectionId: ctx.collectionId,
5030
+ appId: ctx.appId,
5031
+ recordType: ctx.recordType,
5032
+ productId: e.spec.scope.productId,
5033
+ variantId: e.spec.scope.variantId,
5034
+ batchId: e.spec.scope.batchId,
5035
+ facetId: e.spec.scope.facetId,
5036
+ facetValue: e.spec.scope.facetValue,
5037
+ proofId: e.spec.scope.proofId,
5038
+ recordId: e.recordId,
5039
+ withParent: true
5040
+ });
5041
+ queryClient.invalidateQueries({ queryKey: key });
5042
+ }
5043
+ } catch {
5044
+ }
5045
+ }, [store, editorId, ctx, queryClient]);
5046
+ const reset = useCallback(() => {
5047
+ if (!editorId) return;
5048
+ store.reset(editorId);
5049
+ }, [store, editorId]);
5050
+ const remove = useCallback(async () => {
5051
+ if (!editorId) return;
5052
+ await store.remove(editorId);
5053
+ }, [store, editorId]);
5054
+ if (!editorId || !entry) return null;
5055
+ const isRuleScope = entry.spec.scope.kind === "rule";
5056
+ const ruleValid = !isRuleScope || isFacetRuleValid(entry.facetRule);
5057
+ const canSave = ruleValid;
5058
+ const cannotSaveReason = !ruleValid ? "Pick at least one value for every facet in the rule before saving." : void 0;
5059
+ return {
5060
+ editorId,
5061
+ value: entry.value,
5062
+ onChange,
5063
+ source: entry.source,
5064
+ recordId: entry.recordId,
5065
+ parentValue: entry.parentValue,
5066
+ scope: entry.spec.scope,
5067
+ isDirty: entry.status === "dirty" || entry.status === "saving" || entry.status === "error",
5068
+ status: entry.status,
5069
+ error: entry.error ?? null,
5070
+ save,
5071
+ reset,
5072
+ remove,
5073
+ canRemove: entry.source === "self" && !!entry.recordId,
5074
+ canSave,
5075
+ cannotSaveReason,
5076
+ facetRule: entry.facetRule,
5077
+ onFacetRuleChange,
5078
+ isActive: editorId === currentEditorId
5079
+ };
5080
+ }
5081
+ var useEditorSelection = () => {
5082
+ const { store, ctx, currentEditorId, setCurrentEditorId, maxOpenEditors, defaultValueFactory } = useEditorSessionContext();
5083
+ const openEditors = useStoreList();
5084
+ const selectTarget = useCallback((input) => {
5085
+ const existing = store.findExistingEditorIdFor(input.spec);
5086
+ if (existing) {
5087
+ store.markActive(existing);
5088
+ setCurrentEditorId(existing);
5089
+ store.enforcePoolLimit(maxOpenEditors, existing);
5090
+ return existing;
5091
+ }
5092
+ const editorId = store.open({
5093
+ spec: input.spec,
5094
+ saveSpec: buildSaveSpec(ctx, input.spec),
5095
+ seed: input.seed ?? null,
5096
+ defaultValue: input.defaultValue ?? defaultValueFactory?.() ?? {},
5097
+ label: input.label
5098
+ });
5099
+ setCurrentEditorId(editorId);
5100
+ store.enforcePoolLimit(maxOpenEditors, editorId);
5101
+ return editorId;
5102
+ }, [store, ctx, setCurrentEditorId, maxOpenEditors, defaultValueFactory]);
5103
+ const focus = useCallback((editorId) => {
5104
+ store.markActive(editorId);
5105
+ setCurrentEditorId(editorId);
5106
+ }, [store, setCurrentEditorId]);
5107
+ const blur = useCallback(() => setCurrentEditorId(void 0), [setCurrentEditorId]);
5108
+ const close = useCallback((editorId) => {
5109
+ const closed = store.close(editorId);
5110
+ if (closed && currentEditorId === editorId) setCurrentEditorId(void 0);
5111
+ return closed;
5112
+ }, [store, currentEditorId, setCurrentEditorId]);
5113
+ const hydrate = useCallback((editorId, resolved) => {
5114
+ store.hydrateFromResolver(editorId, resolved);
5115
+ }, [store]);
5116
+ return { currentEditorId, openEditors, selectTarget, focus, blur, close, hydrate };
5117
+ };
5118
+ var overviewItemFromEntry = (e) => ({
5119
+ editorId: e.editorId,
5120
+ label: e.label,
5121
+ status: e.status,
5122
+ error: e.error,
5123
+ scope: e.spec.scope,
5124
+ recordId: e.recordId
5125
+ });
5126
+ var useDirtyOverview = () => {
5127
+ const { store } = useEditorSessionContext();
5128
+ const list = useStoreList();
5129
+ const items = useMemo(
5130
+ () => list.filter((e) => e.status === "dirty" || e.status === "saving" || e.status === "error").map(overviewItemFromEntry),
5131
+ [list]
5132
+ );
5133
+ const count = items.length;
5134
+ const errorCount = useMemo(() => items.filter((i) => i.status === "error").length, [items]);
5135
+ const saveAll = useCallback(async () => {
5136
+ const ids = list.filter((e) => e.status === "dirty" || e.status === "error").map((e) => e.editorId);
5137
+ for (const id of ids) {
5138
+ try {
5139
+ await store.save(id);
5140
+ } catch {
5141
+ }
5142
+ }
5143
+ }, [store, list]);
5144
+ const discardAll = useCallback(() => {
5145
+ const ids = list.filter((e) => e.status === "dirty" || e.status === "error").map((e) => e.editorId);
5146
+ ids.forEach((id) => store.reset(id));
5147
+ }, [store, list]);
5148
+ const saveOne = useCallback(async (editorId) => {
5149
+ await store.save(editorId);
5150
+ }, [store]);
5151
+ return { items, count, errorCount, saveAll, discardAll, saveOne };
5152
+ };
5153
+ var EditorMountPool = ({
5154
+ renderSlot,
5155
+ keepMountedHidden = true,
5156
+ fallback = null,
5157
+ className
5158
+ }) => {
5159
+ const { openEditors, currentEditorId } = useEditorSelection();
5160
+ const ids = useMemo(
5161
+ () => openEditors.map((e) => e.editorId).sort(),
5162
+ [openEditors]
5163
+ );
5164
+ if (!currentEditorId && ids.length === 0) {
5165
+ return /* @__PURE__ */ jsx(Fragment, { children: fallback });
5166
+ }
5167
+ const visibleIds = keepMountedHidden ? ids : currentEditorId ? [currentEditorId] : [];
5168
+ return /* @__PURE__ */ jsx("div", { className, style: { display: "contents" }, children: visibleIds.map((id) => {
5169
+ const isActive = id === currentEditorId;
5170
+ return /* @__PURE__ */ jsx(EditorPoolSlot, { editorId: id, isActive, children: renderSlot(id) }, id);
5171
+ }) });
5172
+ };
5173
+ var EditorPoolSlot = ({ editorId, isActive, children }) => {
5174
+ const inertProps = isActive ? {} : { inert: "" };
5175
+ return /* @__PURE__ */ jsx(
5176
+ "div",
5177
+ {
5178
+ "data-editor-slot": editorId,
5179
+ "data-active": isActive ? "true" : "false",
5180
+ "aria-hidden": isActive ? void 0 : true,
5181
+ style: {
5182
+ display: isActive ? "contents" : "none"
5183
+ },
5184
+ ...inertProps,
5185
+ children
5186
+ }
5187
+ );
5188
+ };
5189
+ function useEditorSlotContext(editorId) {
5190
+ const session = useEditorSession(editorId);
5191
+ return useMemo(() => {
5192
+ if (!session) return null;
5193
+ return {
5194
+ value: session.value,
5195
+ onChange: session.onChange,
5196
+ source: session.source,
5197
+ recordId: session.recordId,
5198
+ parentValue: session.parentValue,
5199
+ scope: session.scope,
5200
+ isDirty: session.isDirty,
5201
+ save: session.save,
5202
+ reset: session.reset,
5203
+ remove: session.remove,
5204
+ canRemove: session.canRemove,
5205
+ canSave: session.canSave,
5206
+ cannotSaveReason: session.cannotSaveReason,
5207
+ isSaving: session.status === "saving",
5208
+ saveError: session.error,
5209
+ facetRule: session.facetRule,
5210
+ onFacetRuleChange: session.onFacetRuleChange
5211
+ };
5212
+ }, [session]);
5213
+ }
5214
+ var isEqualSpec = (a, b) => {
5215
+ if (a === b) return true;
5216
+ if (!a || !b) return false;
5217
+ if (a.scope.raw !== b.scope.raw) return false;
5218
+ if ((a.recordId ?? null) !== (b.recordId ?? null)) return false;
5219
+ if (!!a.createMode !== !!b.createMode) return false;
5220
+ return true;
5221
+ };
5222
+ function useEditorBridge(args) {
5223
+ const { target, resolved, defaultData, onSaved, onDeleted } = args;
5224
+ const selection = useEditorSelection();
5225
+ const lastTargetRef = useRef(null);
5226
+ const editorIdRef = useRef(void 0);
5227
+ useEffect(() => {
5228
+ if (!target) {
5229
+ selection.blur();
5230
+ lastTargetRef.current = null;
5231
+ editorIdRef.current = void 0;
5232
+ return;
5233
+ }
5234
+ if (isEqualSpec(target, lastTargetRef.current)) return;
5235
+ lastTargetRef.current = target;
5236
+ const id = selection.selectTarget({
5237
+ spec: target,
5238
+ seed: resolved.source === "empty" ? void 0 : {
5239
+ value: resolved.data ?? (defaultData?.() ?? null),
5240
+ facetRule: resolved.facetRule ?? null,
5241
+ source: resolved.source,
5242
+ parentValue: resolved.parentValue ?? null,
5243
+ recordId: resolved.recordId
5244
+ },
5245
+ defaultValue: defaultData?.() ?? {}
5246
+ });
5247
+ editorIdRef.current = id;
5248
+ }, [target?.scope.raw, target?.recordId, target?.createMode]);
5249
+ useEffect(() => {
5250
+ const id = editorIdRef.current;
5251
+ if (!id) return;
5252
+ if (resolved.source === "empty" && !resolved.recordId) return;
5253
+ selection.hydrate(id, resolved);
5254
+ }, [resolved.source, resolved.recordId, resolved.sourceRef, resolved.data, resolved.facetRule]);
5255
+ const editorId = selection.currentEditorId ?? editorIdRef.current;
5256
+ const session = useEditorSession(editorId);
5257
+ const prevStatusRef = useRef(null);
5258
+ useEffect(() => {
5259
+ const prev = prevStatusRef.current;
5260
+ const next = session?.status ?? null;
5261
+ prevStatusRef.current = next;
5262
+ if (next === "saved" && prev !== "saved") {
5263
+ onSaved?.();
5264
+ }
5265
+ }, [session?.status]);
5266
+ const remove = useMemo(() => async () => {
5267
+ if (!session) return;
5268
+ await session.remove();
5269
+ onDeleted?.();
5270
+ }, [session?.remove]);
5271
+ if (!session) {
5272
+ const sentinelScope = target?.scope ?? { kind: "collection", raw: "" };
5273
+ return {
5274
+ value: defaultData?.() ?? {},
5275
+ onChange: () => {
5276
+ },
5277
+ source: "empty",
5278
+ recordId: void 0,
5279
+ parentValue: null,
5280
+ scope: sentinelScope,
5281
+ isDirty: false,
5282
+ save: async () => {
5283
+ },
5284
+ reset: () => {
5285
+ },
5286
+ remove: async () => {
5287
+ },
5288
+ canRemove: false,
5289
+ canSave: false,
5290
+ cannotSaveReason: void 0,
5291
+ isSaving: false,
5292
+ saveError: null,
5293
+ facetRule: null,
5294
+ onFacetRuleChange: () => {
5295
+ }
5296
+ };
5297
+ }
5298
+ return {
5299
+ value: session.value,
5300
+ onChange: session.onChange,
5301
+ source: session.source,
5302
+ recordId: session.recordId,
5303
+ parentValue: session.parentValue,
5304
+ scope: session.scope,
5305
+ isDirty: session.isDirty,
5306
+ save: session.save,
5307
+ reset: session.reset,
5308
+ remove,
5309
+ canRemove: session.canRemove,
5310
+ canSave: session.canSave,
5311
+ cannotSaveReason: session.cannotSaveReason,
5312
+ isSaving: session.status === "saving",
5313
+ saveError: session.error,
5314
+ facetRule: session.facetRule,
5315
+ onFacetRuleChange: session.onFacetRuleChange
5316
+ };
5317
+ }
5318
+
5319
+ // src/components/RecordsAdmin/data/csv.ts
5320
+ var escapeCell = (s) => {
5321
+ if (/[",\n]/.test(s)) return `"${s.replace(/"/g, '""')}"`;
5322
+ return s;
5323
+ };
5324
+ var parseLine = (line) => {
5325
+ const out = [];
5326
+ let cur = "";
5327
+ let inQuotes = false;
5328
+ for (let i = 0; i < line.length; i += 1) {
5329
+ const ch = line[i];
5330
+ if (inQuotes) {
5331
+ if (ch === '"' && line[i + 1] === '"') {
5332
+ cur += '"';
5333
+ i += 1;
5334
+ } else if (ch === '"') inQuotes = false;
5335
+ else cur += ch;
5336
+ } else if (ch === '"') inQuotes = true;
5337
+ else if (ch === ",") {
5338
+ out.push(cur);
5339
+ cur = "";
5340
+ } else cur += ch;
5341
+ }
5342
+ out.push(cur);
5343
+ return out;
5344
+ };
5345
+ var exportCsv = (records, schema) => {
5346
+ const headers = ["ref", ...schema.columns.map((c) => c.header)];
5347
+ const rows = records.filter((r) => r.data != null).map((r) => [r.ref, ...schema.columns.map((c) => c.toCell(r.data))]);
5348
+ const csv = [headers, ...rows].map((row) => row.map((cell) => escapeCell(String(cell ?? ""))).join(",")).join("\n");
5349
+ return new Blob([csv], { type: "text/csv;charset=utf-8" });
5350
+ };
5351
+ var importCsv = async (file, schema, ctx) => {
5352
+ const text = await file.text();
5353
+ const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
5354
+ if (lines.length === 0) {
5355
+ return { total: 0, saved: 0, failed: 0, errorRows: [], annotatedCsv: "" };
5356
+ }
5357
+ const headers = parseLine(lines[0]);
5358
+ const refIdx = headers.indexOf("ref");
5359
+ const colMap = schema.columns.map((c) => ({ col: c, idx: headers.indexOf(c.header) }));
5360
+ const errorRows = [];
5361
+ let saved = 0;
5362
+ for (let i = 1; i < lines.length; i += 1) {
5363
+ const cells = parseLine(lines[i]);
5364
+ const ref = refIdx >= 0 ? cells[refIdx] : "";
5365
+ if (!ref) {
5366
+ errorRows.push({ row: i, error: "Missing ref" });
5367
+ continue;
5368
+ }
5369
+ const data = {};
5370
+ for (const { col, idx } of colMap) {
5371
+ if (idx < 0) continue;
5372
+ try {
5373
+ data[col.key] = col.fromCell(cells[idx] ?? "");
4760
5374
  } catch (e) {
4761
5375
  errorRows.push({ row: i, error: e.message });
4762
5376
  continue;
@@ -4806,11 +5420,58 @@ var downloadBlob = (blob, filename) => {
4806
5420
  URL.revokeObjectURL(url);
4807
5421
  };
4808
5422
 
5423
+ // src/components/RecordsAdmin/hooks/useShellCsvBulk.ts
5424
+ function useShellCsvBulk(args) {
5425
+ const { csvSchema, recordType, label, items, ctx, refetchAll, onTelemetry } = args;
5426
+ const fileBase = useMemo(
5427
+ () => recordType ?? (label.toLowerCase().replace(/\s+/g, "-") || "records"),
5428
+ [recordType, label]
5429
+ );
5430
+ const handleExport = useCallback(() => {
5431
+ if (!csvSchema) return;
5432
+ const blob = exportCsv(items, csvSchema);
5433
+ downloadBlob(blob, `${fileBase}.csv`);
5434
+ onTelemetry?.({ type: "csv.export", recordType, rows: items.length });
5435
+ }, [csvSchema, items, fileBase, onTelemetry, recordType]);
5436
+ const handleImport = useCallback(() => {
5437
+ if (!csvSchema) return;
5438
+ const inp = document.createElement("input");
5439
+ inp.type = "file";
5440
+ inp.accept = ".csv,text/csv";
5441
+ inp.onchange = async () => {
5442
+ const file = inp.files?.[0];
5443
+ if (!file) return;
5444
+ const report = await importCsv(file, csvSchema, ctx);
5445
+ onTelemetry?.({ type: "csv.import", recordType, rows: report.total, errors: report.failed });
5446
+ if (report.failed > 0) {
5447
+ downloadBlob(
5448
+ new Blob([report.annotatedCsv], { type: "text/csv" }),
5449
+ `${fileBase}-errors.csv`
5450
+ );
5451
+ }
5452
+ refetchAll();
5453
+ };
5454
+ inp.click();
5455
+ }, [csvSchema, ctx, fileBase, onTelemetry, recordType, refetchAll]);
5456
+ return useMemo(
5457
+ () => csvSchema ? { onImportCsv: handleImport, onExportCsv: handleExport } : {},
5458
+ [csvSchema, handleImport, handleExport]
5459
+ );
5460
+ }
5461
+
4809
5462
  // src/components/RecordsAdmin/shell/tokens.css
4810
5463
  styleInject(':root {\n --ra-status-own: var(--ra-emerald, 142 71% 45%);\n --ra-status-shared: var(--ra-amber, 38 92% 50%);\n --ra-status-missing: var(--muted-foreground, 220 9% 46%);\n --ra-accent: var(--primary, 222 47% 11%);\n --ra-surface: var(--card, 0 0% 100%);\n --ra-border: var(--border, 220 13% 91%);\n --ra-text: var(--foreground, 222 47% 11%);\n --ra-muted: var(--muted, 220 14% 96%);\n --ra-muted-text: var(--muted-foreground, 220 9% 46%);\n --ra-radius: var(--radius, 0.625rem);\n --ra-dot-size: 0.5rem;\n --ra-page-bg: var(--background, 220 14% 98%);\n --ra-card-shadow: 0 1px 2px hsl(var(--ra-accent) / 0.04), 0 4px 12px hsl(var(--ra-accent) / 0.05);\n --ra-card-shadow-hover: 0 2px 4px hsl(var(--ra-accent) / 0.06), 0 8px 24px hsl(var(--ra-accent) / 0.08);\n --ra-row-hover: hsl(var(--ra-accent) / 0.05);\n --ra-row-active-bg: hsl(var(--ra-accent) / 0.10);\n --ra-row-active-bd: hsl(var(--ra-accent) / 0.45);\n --ra-focus-ring: hsl(var(--ra-accent) / 0.35);\n --ra-font-display: var(--font-display, var(--font-sans, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif));\n --ra-font-ui: var(--font-sans, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);\n --ra-title-weight: 600;\n --ra-display-weight: 700;\n --ra-info: var(--ra-blue, 214 95% 55%);\n --ra-success: var(--ra-emerald, 142 71% 45%);\n --ra-warning: var(--ra-amber, 38 92% 50%);\n --ra-danger: var(--destructive, 0 72% 51%);\n}\n:root {\n --sl-control-bg: var(--muted, 220 14% 96%);\n --sl-control-fg: var(--muted-foreground, 220 9% 40%);\n --sl-control-border: var(--border, 220 13% 88%);\n --sl-control-active-bg: var(--primary, 222 47% 11%);\n --sl-control-active-fg: var(--primary-foreground, 0 0% 100%);\n --sl-control-active-bd: var(--primary, 222 47% 11%);\n --sl-control-hover-bg: var(--sl-control-active-bg, 222 47% 11%);\n --sl-control-hover-fg: var(--foreground, 222 47% 11%);\n --sl-control-focus-ring: var(--sl-control-active-bg, 222 47% 11%);\n --sl-control-radius: var(--radius, 0.5rem);\n --sl-control-weight: 500;\n --sl-control-active-weight: 600;\n}\n.ra-status-dot {\n display: inline-block;\n width: var(--ra-dot-size);\n height: var(--ra-dot-size);\n border-radius: 9999px;\n flex-shrink: 0;\n}\n.ra-status-own {\n background: hsl(var(--ra-status-own));\n}\n.ra-status-shared {\n background: hsl(var(--ra-status-shared));\n}\n.ra-status-missing {\n background: hsl(var(--ra-status-missing) / 0.4);\n border: 1px solid hsl(var(--ra-status-missing) / 0.6);\n}\n.ra-row-active {\n background: var(--ra-row-active-bg);\n border-color: var(--ra-row-active-bd) !important;\n}\n');
4811
5464
 
4812
5465
  // src/components/RecordsAdmin/shell/shell.css
4813
5466
  styleInject(".ra-shell {\n color: hsl(var(--ra-text));\n background: hsl(var(--ra-page-bg));\n font-family: var(--ra-font-ui);\n}\n.ra-shell *,\n.ra-shell *::before,\n.ra-shell *::after {\n box-sizing: border-box;\n}\n.ra-shell .ra-card {\n background: hsl(var(--ra-surface));\n border: 1px solid hsl(var(--ra-border));\n border-radius: var(--ra-radius);\n box-shadow: var(--ra-card-shadow);\n}\n.ra-shell .ra-card-hover {\n transition:\n box-shadow .18s ease,\n transform .18s ease,\n border-color .18s ease;\n}\n.ra-shell .ra-card-hover:hover {\n box-shadow: var(--ra-card-shadow-hover);\n}\n.ra-shell .ra-display {\n font-family: var(--ra-font-display);\n font-weight: var(--ra-display-weight);\n letter-spacing: -0.01em;\n}\n.ra-shell .ra-title {\n font-weight: var(--ra-title-weight);\n}\n.ra-shell :where(button, [role=button], input, select, textarea, a):focus-visible {\n outline: none;\n box-shadow: 0 0 0 3px var(--ra-focus-ring);\n border-radius: calc(var(--ra-radius) * 0.6);\n}\n.ra-shell .ra-header {\n display: block;\n width: 100%;\n}\n.ra-shell .ra-header__main {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: flex-start;\n gap: 0.55rem;\n}\n.ra-shell .ra-header-aside {\n display: flex;\n align-items: flex-start;\n gap: 0.5rem;\n flex-shrink: 0;\n}\n.ra-shell .ra-header-icon {\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n justify-content: flex-start;\n background: transparent;\n color: hsl(var(--ra-text));\n border: 0;\n padding: 0;\n margin-top: 0.1rem;\n}\n.ra-shell .ra-header-icon > svg {\n width: 1.05rem;\n height: 1.05rem;\n}\n.ra-shell .ra-header-text {\n flex: 1;\n min-width: 0;\n}\n.ra-shell .ra-header-title {\n font-family: var(--ra-font-display);\n font-weight: 700;\n font-size: 1.2rem;\n line-height: 1.2;\n color: hsl(var(--ra-text));\n letter-spacing: -0.015em;\n margin: 0;\n display: inline-flex;\n align-items: center;\n gap: 0.5rem;\n}\n.ra-shell .ra-header-subtitle {\n font-size: 0.78rem;\n color: hsl(var(--ra-muted-text));\n margin-top: 0.1rem;\n line-height: 1.3;\n}\n.ra-shell .ra-header-stats {\n display: flex;\n align-items: stretch;\n gap: 0.15rem;\n padding: 0.15rem 0.4rem;\n border-radius: calc(var(--ra-radius) * 0.75);\n background: hsl(var(--ra-surface) / 0.7);\n border: 1px solid hsl(var(--ra-border));\n}\n.ra-shell .ra-header-stats--titled {\n flex-direction: column;\n align-items: stretch;\n padding: 0.4rem 0.55rem;\n gap: 0.3rem;\n}\n.ra-shell .ra-header-stats .ra-stats-items {\n display: flex;\n align-items: stretch;\n gap: 0.15rem;\n}\n.ra-shell .ra-header-stats .ra-stats-heading {\n display: flex;\n align-items: center;\n gap: 0.35rem;\n color: hsl(var(--ra-muted-text));\n font-size: 0.65rem;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n}\n.ra-shell .ra-header-stats .ra-stats-heading-icon {\n display: inline-flex;\n align-items: center;\n color: hsl(var(--ra-text));\n opacity: 0.75;\n}\n.ra-shell .ra-header-stats .ra-stats-heading-icon > svg {\n width: 0.85rem;\n height: 0.85rem;\n}\n.ra-shell .ra-stat {\n display: flex;\n flex-direction: column;\n align-items: center;\n padding: 0.15rem 0.45rem;\n min-width: 2.5rem;\n}\n.ra-shell .ra-stat-value {\n font-family: var(--ra-font-display);\n font-weight: var(--ra-display-weight);\n font-size: 0.85rem;\n color: hsl(var(--ra-text));\n line-height: 1;\n}\n.ra-shell .ra-stat-label {\n font-size: 0.6rem;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: hsl(var(--ra-muted-text));\n margin-top: 0.15rem;\n}\n.ra-shell .ra-stat-divider {\n width: 1px;\n background: hsl(var(--ra-border));\n margin: 0.25rem 0;\n}\n.ra-shell .ra-header-actions {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n.ra-shell .ra-tabs {\n display: flex;\n gap: 0.25rem;\n padding: 0.25rem;\n background: hsl(var(--sl-control-bg));\n border-radius: var(--sl-control-radius);\n border: 1px solid hsl(var(--sl-control-border));\n}\n.ra-shell .ra-tab {\n display: inline-flex;\n align-items: center;\n gap: 0.4rem;\n padding: 0.4rem 0.7rem;\n border-radius: calc(var(--sl-control-radius) - 2px);\n font-size: 0.78rem;\n font-weight: var(--sl-control-weight);\n color: hsl(var(--sl-control-fg));\n background: transparent;\n border: 0;\n cursor: pointer;\n transition:\n background .15s ease,\n color .15s ease,\n transform .15s ease;\n white-space: nowrap;\n}\n.ra-shell .ra-tab:hover {\n background: hsl(var(--sl-control-hover-bg) / 0.10);\n color: hsl(var(--sl-control-hover-fg));\n}\n.ra-shell .ra-tab:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px hsl(var(--sl-control-focus-ring) / 0.45);\n}\n.ra-shell .ra-tab[aria-selected=true] {\n background: hsl(var(--sl-control-active-bg));\n color: hsl(var(--sl-control-active-fg));\n border-color: hsl(var(--sl-control-active-bd));\n font-weight: var(--sl-control-active-weight);\n box-shadow: 0 1px 2px hsl(var(--sl-control-active-bg) / 0.25);\n}\n.ra-shell .ra-tab[aria-selected=true]:hover {\n background: hsl(var(--sl-control-active-bg) / 0.92);\n color: hsl(var(--sl-control-active-fg));\n}\n.ra-shell .ra-tab[aria-selected=true] .ra-tab-icon {\n color: hsl(var(--sl-control-active-fg));\n}\n.ra-shell .ra-tab[disabled] {\n opacity: .5;\n cursor: not-allowed;\n}\n.ra-shell .ra-tab-count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 1.25rem;\n padding: 0 0.35rem;\n height: 1.1rem;\n border-radius: 999px;\n background: hsl(var(--sl-control-active-fg) / 0.20);\n color: hsl(var(--sl-control-active-fg));\n font-size: 0.625rem;\n font-weight: 600;\n line-height: 1;\n}\n.ra-shell .ra-tab[aria-selected=false] .ra-tab-count {\n background: hsl(var(--sl-control-fg) / 0.15);\n color: hsl(var(--sl-control-fg));\n}\n.ra-shell[data-density=compact] .ra-row {\n padding-block: 0.4rem;\n}\n.ra-shell .ra-row {\n display: flex;\n align-items: center;\n gap: 0.55rem;\n width: 100%;\n text-align: left;\n padding: 0.45rem 0.75rem;\n border-left: 3px solid transparent;\n background: transparent;\n border-bottom: 1px solid transparent;\n transition: background .12s ease, border-color .12s ease;\n cursor: pointer;\n color: hsl(var(--ra-text));\n font-family: inherit;\n}\n.ra-shell .ra-row + .ra-row {\n border-top: 1px solid hsl(var(--ra-border) / 0.6);\n}\n.ra-shell .ra-row:hover {\n background: var(--ra-row-hover);\n}\n.ra-shell .ra-row[data-selected=true] {\n background: var(--ra-row-active-bg);\n border-left-color: var(--ra-row-active-bd);\n}\n.ra-shell .ra-row-compact {\n padding-block: 0.3rem;\n}\n.ra-shell .ra-row-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.5rem;\n height: 1.5rem;\n border-radius: calc(var(--ra-radius) * 0.6);\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-muted-text));\n flex-shrink: 0;\n}\n.ra-shell .ra-row[data-selected=true] .ra-row-icon {\n background: hsl(var(--ra-accent) / 0.15);\n color: hsl(var(--ra-accent));\n}\n.ra-shell .ra-row-body {\n flex: 1;\n min-width: 0;\n}\n.ra-shell .ra-row-title {\n font-weight: var(--ra-title-weight);\n font-size: 0.8125rem;\n line-height: 1.2;\n color: hsl(var(--ra-text));\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ra-shell .ra-row-sub {\n font-size: 0.6875rem;\n color: hsl(var(--ra-muted-text));\n margin-top: 0.05rem;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ra-shell .ra-row-rule-chips {\n display: flex;\n flex-wrap: wrap;\n gap: 0.2rem;\n margin-top: 0.2rem;\n}\n.ra-shell .ra-rule-chip {\n display: inline-flex;\n align-items: center;\n max-width: 100%;\n padding: 0.05rem 0.4rem;\n border-radius: 999px;\n font-size: 0.625rem;\n font-weight: 500;\n line-height: 1.4;\n background: hsl(var(--ra-accent) / 0.10);\n color: hsl(var(--ra-accent));\n border: 1px solid hsl(var(--ra-accent) / 0.20);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ra-shell .ra-rule-chip-more {\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-muted-text));\n border-color: hsl(var(--ra-border));\n}\n.ra-shell[data-density=compact] .ra-row-rule-chips {\n margin-top: 0.15rem;\n gap: 0.15rem;\n}\n.ra-shell[data-density=compact] .ra-rule-chip {\n font-size: 0.6rem;\n padding: 0.02rem 0.35rem;\n}\n.ra-shell .ra-rule-filters {\n display: flex;\n flex-direction: column;\n gap: 0.3rem;\n}\n.ra-shell .ra-rule-filters-row {\n display: flex;\n flex-wrap: wrap;\n gap: 0.25rem;\n}\n.ra-shell .ra-rule-filter-chip {\n display: inline-flex;\n align-items: center;\n gap: 0.3rem;\n padding: 0.15rem 0.5rem;\n border-radius: 999px;\n font-size: 0.65rem;\n font-weight: 500;\n line-height: 1.4;\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-muted-text));\n border: 1px solid hsl(var(--ra-border));\n cursor: pointer;\n transition:\n background .12s ease,\n color .12s ease,\n border-color .12s ease;\n max-width: 100%;\n}\n.ra-shell .ra-rule-filter-chip:hover {\n background: hsl(var(--ra-accent) / 0.10);\n color: hsl(var(--ra-text));\n border-color: hsl(var(--ra-accent) / 0.25);\n}\n.ra-shell .ra-rule-filter-chip[data-active=true] {\n background: hsl(var(--ra-accent) / 0.15);\n color: hsl(var(--ra-accent));\n border-color: hsl(var(--ra-accent) / 0.40);\n}\n.ra-shell .ra-rule-filter-chip[data-tone=complexity][data-active=true] {\n background: hsl(var(--ra-info) / 0.15);\n color: hsl(var(--ra-info));\n border-color: hsl(var(--ra-info) / 0.40);\n}\n.ra-shell .ra-rule-filter-chip-label {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 9rem;\n}\n.ra-shell .ra-rule-filter-chip-count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 1.1rem;\n height: 1rem;\n padding: 0 0.3rem;\n border-radius: 999px;\n background: hsl(var(--ra-surface));\n color: hsl(var(--ra-muted-text));\n font-size: 0.6rem;\n font-weight: 600;\n}\n.ra-shell .ra-rule-filter-chip[data-active=true] .ra-rule-filter-chip-count {\n background: hsl(var(--ra-accent) / 0.18);\n color: hsl(var(--ra-accent));\n}\n.ra-shell .ra-rule-filter-clear {\n align-self: flex-start;\n background: transparent;\n border: 0;\n padding: 0;\n color: hsl(var(--ra-muted-text));\n font-size: 0.65rem;\n cursor: pointer;\n text-decoration: underline;\n text-decoration-style: dotted;\n}\n.ra-shell .ra-rule-filter-clear:hover {\n color: hsl(var(--ra-text));\n}\n.ra-shell[data-density=compact] .ra-row {\n padding-block: 0.3rem;\n gap: 0.45rem;\n}\n.ra-shell[data-density=compact] .ra-row-title {\n font-size: 0.78125rem;\n}\n.ra-shell .ra-row-actions {\n display: inline-flex;\n align-items: center;\n gap: 0.15rem;\n margin-left: auto;\n opacity: 0;\n transition: opacity .15s ease;\n}\n.ra-shell .ra-row:hover .ra-row-actions,\n.ra-shell .ra-row:focus-within .ra-row-actions {\n opacity: 1;\n}\n.ra-shell .ra-row-action {\n width: 1.6rem;\n height: 1.6rem;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 999px;\n background: transparent;\n color: hsl(var(--ra-muted-text));\n border: 0;\n cursor: pointer;\n transition: background .15s ease, color .15s ease;\n}\n.ra-shell .ra-row-action:hover {\n background: hsl(var(--ra-accent) / 0.10);\n color: hsl(var(--ra-text));\n}\n.ra-shell .ra-row-action[data-tone=danger]:hover {\n background: hsl(var(--ra-danger) / 0.12);\n color: hsl(var(--ra-danger));\n}\n.ra-shell .ra-chip {\n display: inline-flex;\n align-items: center;\n gap: 0.3rem;\n padding: 0.15rem 0.5rem;\n border-radius: 999px;\n font-size: 0.6875rem;\n font-weight: 500;\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-muted-text));\n border: 1px solid hsl(var(--ra-border));\n white-space: nowrap;\n max-width: 14rem;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ra-shell .ra-chip[data-tone=success] {\n background: hsl(var(--ra-success) / 0.12);\n color: hsl(var(--ra-success));\n border-color: hsl(var(--ra-success) / 0.30);\n}\n.ra-shell .ra-chip[data-tone=warning] {\n background: hsl(var(--ra-warning) / 0.14);\n color: hsl(var(--ra-warning));\n border-color: hsl(var(--ra-warning) / 0.35);\n}\n.ra-shell .ra-chip[data-tone=info] {\n background: hsl(var(--ra-info) / 0.10);\n color: hsl(var(--ra-info));\n border-color: hsl(var(--ra-info) / 0.30);\n}\n.ra-shell .ra-chip[data-tone=danger] {\n background: hsl(var(--ra-danger) / 0.10);\n color: hsl(var(--ra-danger));\n border-color: hsl(var(--ra-danger) / 0.30);\n}\n.ra-shell .ra-chip[data-tone=muted] {\n background: transparent;\n color: hsl(var(--ra-muted-text));\n border-style: dashed;\n}\n.ra-shell .ra-group {\n border-bottom: 1px solid hsl(var(--ra-border));\n}\n.ra-shell .ra-group:last-child {\n border-bottom: 0;\n}\n.ra-shell .ra-group-summary {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n width: 100%;\n padding: 0.5rem 0.85rem;\n background: hsl(var(--ra-muted) / 0.6);\n font-size: 0.7rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: hsl(var(--ra-muted-text));\n border: 0;\n cursor: pointer;\n transition: background .12s ease;\n}\n.ra-shell .ra-group-summary:hover {\n background: hsl(var(--ra-muted));\n}\n.ra-shell .ra-group-summary .ra-group-chevron {\n transition: transform .15s ease;\n}\n.ra-shell .ra-group[data-open=false] .ra-group-chevron {\n transform: rotate(-90deg);\n}\n.ra-shell .ra-group-name {\n flex: 1;\n text-align: left;\n}\n.ra-shell .ra-group-count {\n font-size: 0.65rem;\n font-weight: 600;\n color: hsl(var(--ra-muted-text));\n background: hsl(var(--ra-surface));\n border: 1px solid hsl(var(--ra-border));\n border-radius: 999px;\n padding: 0.05rem 0.4rem;\n}\n.ra-shell .ra-group[data-open=false] .ra-group-body {\n display: none;\n}\n.ra-shell .ra-empty {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: center;\n padding: 2.5rem 1.5rem;\n gap: 0.75rem;\n}\n.ra-shell .ra-empty-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 3.25rem;\n height: 3.25rem;\n border-radius: 999px;\n background: hsl(var(--ra-accent) / 0.08);\n color: hsl(var(--ra-accent));\n margin-bottom: 0.25rem;\n}\n.ra-shell .ra-empty-title {\n font-family: var(--ra-font-display);\n font-weight: var(--ra-display-weight);\n font-size: 1rem;\n color: hsl(var(--ra-text));\n margin: 0;\n letter-spacing: -0.01em;\n}\n.ra-shell .ra-empty-body {\n font-size: 0.8125rem;\n color: hsl(var(--ra-muted-text));\n max-width: 22rem;\n line-height: 1.45;\n}\n.ra-shell .ra-empty-actions {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n margin-top: 0.25rem;\n flex-wrap: wrap;\n justify-content: center;\n}\n.ra-shell .ra-btn {\n display: inline-flex;\n align-items: center;\n gap: 0.4rem;\n padding: 0.45rem 0.85rem;\n border-radius: calc(var(--ra-radius) * 0.7);\n font-size: 0.8125rem;\n font-weight: 500;\n border: 1px solid hsl(var(--ra-border));\n background: hsl(var(--ra-surface));\n color: hsl(var(--ra-text));\n cursor: pointer;\n transition:\n background .15s ease,\n border-color .15s ease,\n box-shadow .15s ease,\n transform .1s ease;\n}\n.ra-shell .ra-btn:hover {\n background: hsl(var(--ra-muted));\n box-shadow: var(--ra-card-shadow);\n}\n.ra-shell .ra-btn:active {\n transform: translateY(1px);\n}\n.ra-shell .ra-btn[data-variant=primary] {\n background: hsl(var(--ra-accent));\n color: hsl(var(--ra-surface));\n border-color: hsl(var(--ra-accent));\n}\n.ra-shell .ra-btn[data-variant=primary]:hover {\n background: hsl(var(--ra-accent) / 0.92);\n}\n.ra-shell .ra-btn[data-variant=ghost] {\n background: transparent;\n border-color: transparent;\n color: hsl(var(--ra-muted-text));\n}\n.ra-shell .ra-btn[data-variant=ghost]:hover {\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-text));\n}\n.ra-shell .ra-btn[data-variant=danger] {\n color: hsl(var(--ra-danger));\n}\n.ra-shell .ra-btn[data-variant=danger]:hover {\n background: hsl(var(--ra-danger) / 0.10);\n border-color: hsl(var(--ra-danger) / 0.40);\n}\n.ra-shell .ra-intro {\n position: relative;\n display: flex;\n align-items: center;\n gap: 0.55rem;\n padding: 0.4rem 2rem 0.4rem 0.5rem;\n border-radius: var(--ra-radius);\n border: 1px solid hsl(var(--ra-info) / 0.30);\n background: hsl(var(--ra-info) / 0.08);\n}\n.ra-shell .ra-intro[data-tone=success] {\n border-color: hsl(var(--ra-success) / 0.30);\n background: hsl(var(--ra-success) / 0.08);\n}\n.ra-shell .ra-intro[data-tone=warning] {\n border-color: hsl(var(--ra-warning) / 0.35);\n background: hsl(var(--ra-warning) / 0.10);\n}\n.ra-shell .ra-intro-icon {\n flex-shrink: 0;\n width: 1.5rem;\n height: 1.5rem;\n border-radius: 999px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n background: hsl(var(--ra-info) / 0.18);\n color: hsl(var(--ra-info));\n}\n.ra-shell .ra-intro[data-tone=success] .ra-intro-icon {\n background: hsl(var(--ra-success) / 0.18);\n color: hsl(var(--ra-success));\n}\n.ra-shell .ra-intro[data-tone=warning] .ra-intro-icon {\n background: hsl(var(--ra-warning) / 0.20);\n color: hsl(var(--ra-warning));\n}\n.ra-shell .ra-intro-body {\n flex: 1;\n min-width: 0;\n}\n.ra-shell .ra-intro-title {\n font-family: var(--ra-font-display);\n font-weight: var(--ra-title-weight);\n font-size: 0.8rem;\n color: hsl(var(--ra-text));\n margin: 0;\n line-height: 1.2;\n display: inline;\n}\n.ra-shell .ra-intro-text {\n font-size: 0.78rem;\n color: hsl(var(--ra-text) / 0.85);\n line-height: 1.35;\n display: inline;\n margin-left: 0.4rem;\n}\n.ra-shell .ra-intro-dismiss {\n position: absolute;\n top: 50%;\n right: 0.35rem;\n transform: translateY(-50%);\n width: 1.4rem;\n height: 1.4rem;\n border-radius: 999px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n background: transparent;\n border: 0;\n color: hsl(var(--ra-muted-text));\n cursor: pointer;\n padding: 0;\n flex-shrink: 0;\n}\n.ra-shell .ra-intro-dismiss:hover {\n background: hsl(var(--ra-text) / 0.06);\n color: hsl(var(--ra-text));\n}\n.ra-shell .ra-bulk-menu {\n min-width: 12rem;\n background: hsl(var(--ra-surface));\n border: 1px solid hsl(var(--ra-border));\n border-radius: calc(var(--ra-radius) * 0.85);\n box-shadow: var(--ra-card-shadow-hover);\n padding: 0.3rem;\n z-index: 60;\n}\n.ra-shell .ra-bulk-item {\n display: flex;\n align-items: center;\n gap: 0.55rem;\n width: 100%;\n padding: 0.45rem 0.6rem;\n border-radius: calc(var(--ra-radius) * 0.6);\n font-size: 0.8125rem;\n color: hsl(var(--ra-text));\n background: transparent;\n border: 0;\n cursor: pointer;\n text-align: left;\n transition: background .12s ease, color .12s ease;\n}\n.ra-shell .ra-bulk-item:hover {\n background: hsl(var(--ra-muted));\n}\n.ra-shell .ra-bulk-item[data-tone=danger] {\n color: hsl(var(--ra-danger));\n}\n.ra-shell .ra-bulk-item[data-tone=danger]:hover {\n background: hsl(var(--ra-danger) / 0.10);\n}\n.ra-shell .ra-bulk-divider {\n height: 1px;\n background: hsl(var(--ra-border));\n margin: 0.25rem 0;\n}\n.ra-shell .ra-preview-rail {\n background: hsl(var(--ra-surface));\n border-left: 1px solid hsl(var(--ra-border));\n box-shadow: -4px 0 16px hsl(var(--ra-accent) / 0.04);\n display: flex;\n flex-direction: column;\n height: 100%;\n overflow: hidden;\n}\n.ra-shell .ra-preview-rail-header {\n position: sticky;\n top: 0;\n z-index: 1;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.75rem 1rem;\n background:\n linear-gradient(\n 180deg,\n hsl(var(--ra-surface)) 0%,\n hsl(var(--ra-surface) / 0.92) 100%);\n border-bottom: 1px solid hsl(var(--ra-border));\n backdrop-filter: blur(6px);\n}\n.ra-shell .ra-preview-rail-title {\n display: inline-flex;\n align-items: center;\n gap: 0.4rem;\n font-size: 0.7rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: hsl(var(--ra-muted-text));\n}\n.ra-shell .ra-preview-rail-body {\n flex: 1;\n overflow-y: auto;\n padding: 1rem;\n}\n.ra-confirm-root {\n position: fixed;\n inset: 0;\n z-index: 2147483000;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 1rem;\n background: transparent;\n}\n.ra-confirm-root .ra-confirm-backdrop {\n position: absolute;\n inset: 0;\n background: hsl(0 0% 0% / 0.45);\n backdrop-filter: blur(2px);\n animation: ra-confirm-fade .12s ease-out;\n}\n.ra-confirm-root .ra-confirm-card {\n position: relative;\n width: min(440px, 100%);\n background: hsl(var(--ra-surface));\n color: hsl(var(--ra-text));\n border: 1px solid hsl(var(--ra-border));\n border-radius: var(--ra-radius);\n box-shadow: 0 1px 2px hsl(0 0% 0% / 0.08), 0 24px 48px -12px hsl(0 0% 0% / 0.32);\n padding: 1.25rem;\n animation: ra-confirm-pop .14s ease-out;\n}\n.ra-confirm-root .ra-confirm-header {\n display: flex;\n align-items: center;\n gap: 0.6rem;\n margin-bottom: 0.5rem;\n}\n.ra-confirm-root .ra-confirm-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.75rem;\n height: 1.75rem;\n border-radius: 999px;\n background: hsl(var(--ra-warning, 38 92% 50%) / 0.12);\n color: hsl(var(--ra-warning, 38 92% 50%));\n}\n.ra-confirm-root .ra-confirm-title {\n font-family: var(--ra-font-display);\n font-weight: 600;\n font-size: 1rem;\n margin: 0;\n}\n.ra-confirm-root .ra-confirm-body {\n font-size: 0.875rem;\n color: hsl(var(--ra-muted-text));\n margin: 0 0 1.1rem;\n line-height: 1.45;\n}\n.ra-confirm-root .ra-confirm-actions {\n display: flex;\n justify-content: flex-end;\n gap: 0.5rem;\n flex-wrap: wrap;\n}\n.ra-confirm-root .ra-confirm-btn {\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n border: 1px solid transparent;\n border-radius: calc(var(--ra-radius) - 2px);\n padding: 0.45rem 0.85rem;\n font-size: 0.8125rem;\n font-weight: 500;\n cursor: pointer;\n transition:\n background-color .12s ease,\n border-color .12s ease,\n color .12s ease;\n}\n.ra-confirm-root .ra-confirm-btn:focus-visible {\n outline: none;\n box-shadow: 0 0 0 3px var(--ra-focus-ring);\n}\n.ra-confirm-root .ra-confirm-btn-ghost {\n background: transparent;\n color: hsl(var(--ra-muted-text));\n border-color: hsl(var(--ra-border));\n}\n.ra-confirm-root .ra-confirm-btn-ghost:hover {\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-text));\n}\n.ra-confirm-root .ra-confirm-btn-danger {\n background: transparent;\n color: hsl(var(--ra-danger, 0 72% 51%));\n border-color: hsl(var(--ra-danger, 0 72% 51%) / 0.45);\n}\n.ra-confirm-root .ra-confirm-btn-danger:hover {\n background: hsl(var(--ra-danger, 0 72% 51%) / 0.08);\n border-color: hsl(var(--ra-danger, 0 72% 51%));\n}\n.ra-confirm-root .ra-confirm-btn-primary {\n background: hsl(var(--ra-accent));\n color: hsl(var(--ra-accent-fg, 0 0% 100%));\n border-color: hsl(var(--ra-accent));\n}\n.ra-confirm-root .ra-confirm-btn-primary:hover {\n filter: brightness(0.95);\n}\n@keyframes ra-confirm-fade {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n@keyframes ra-confirm-pop {\n from {\n opacity: 0;\n transform: translateY(4px) scale(.98);\n }\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n.ra-shell .ra-unsaved-banner {\n display: flex;\n align-items: center;\n gap: 0.6rem;\n padding: 0.5rem 0.75rem;\n border: 1px solid hsl(var(--ra-warning, 38 92% 50%) / 0.35);\n background: hsl(var(--ra-warning, 38 92% 50%) / 0.08);\n border-radius: var(--ra-radius);\n font-size: 0.8125rem;\n color: hsl(var(--ra-text));\n animation: ra-unsaved-slide .14s ease-out;\n}\n.ra-shell .ra-unsaved-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: hsl(var(--ra-warning, 38 92% 50%));\n flex-shrink: 0;\n}\n.ra-shell .ra-unsaved-text {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.ra-shell .ra-unsaved-context {\n color: hsl(var(--ra-muted-text));\n font-weight: 400;\n}\n.ra-shell .ra-unsaved-error {\n color: hsl(var(--ra-danger, 0 72% 51%));\n font-size: 0.75rem;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n font-weight: 500;\n}\n.ra-shell .ra-unsaved-actions {\n display: inline-flex;\n gap: 0.4rem;\n flex-shrink: 0;\n}\n.ra-shell .ra-unsaved-btn {\n display: inline-flex;\n align-items: center;\n gap: 0.3rem;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n border: 1px solid transparent;\n border-radius: calc(var(--ra-radius) - 2px);\n padding: 0.3rem 0.6rem;\n font-size: 0.75rem;\n font-weight: 500;\n cursor: pointer;\n transition:\n background-color .12s ease,\n border-color .12s ease,\n color .12s ease,\n opacity .12s ease;\n}\n.ra-shell .ra-unsaved-btn:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.ra-shell .ra-unsaved-btn:focus-visible {\n outline: none;\n box-shadow: 0 0 0 3px var(--ra-focus-ring);\n}\n.ra-shell .ra-unsaved-btn-ghost {\n background: transparent;\n color: hsl(var(--ra-muted-text));\n border-color: hsl(var(--ra-border));\n}\n.ra-shell .ra-unsaved-btn-ghost:hover:not(:disabled) {\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-text));\n}\n.ra-shell .ra-unsaved-btn-primary {\n background: hsl(var(--ra-accent));\n color: hsl(var(--ra-accent-fg, 0 0% 100%));\n border-color: hsl(var(--ra-accent));\n}\n.ra-shell .ra-unsaved-btn-primary:hover:not(:disabled) {\n filter: brightness(0.95);\n}\n.sl-aph .ra-unsaved-btn {\n display: inline-flex;\n align-items: center;\n gap: 0.3rem;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n border: 1px solid transparent;\n border-radius: calc(var(--ra-radius, 8px) - 2px);\n padding: 0.3rem 0.6rem;\n font-size: 0.75rem;\n font-weight: 500;\n cursor: pointer;\n transition:\n background-color .12s ease,\n border-color .12s ease,\n color .12s ease,\n opacity .12s ease;\n}\n.sl-aph .ra-unsaved-btn:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.sl-aph .ra-unsaved-btn:focus-visible {\n outline: none;\n box-shadow: 0 0 0 3px var(--ra-focus-ring, 0 0 0 3px hsl(var(--ra-accent) / 0.35));\n}\n.sl-aph .ra-unsaved-btn-ghost {\n background: transparent;\n color: hsl(var(--ra-muted-text));\n border-color: hsl(var(--ra-border));\n}\n.sl-aph .ra-unsaved-btn-ghost:hover:not(:disabled) {\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-text));\n}\n.sl-aph .ra-unsaved-btn-primary {\n background: hsl(var(--ra-accent));\n color: hsl(var(--ra-accent-fg, 0 0% 100%));\n border-color: hsl(var(--ra-accent));\n}\n.sl-aph .ra-unsaved-btn-primary:hover:not(:disabled) {\n filter: brightness(0.95);\n}\n.sl-aph .ra-unsaved-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: hsl(var(--ra-warning, 38 92% 50%));\n flex-shrink: 0;\n}\n@keyframes ra-unsaved-slide {\n from {\n opacity: 0;\n transform: translateY(-3px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n.ra-shell .ra-unsaved-pill,\n.sl-aph .ra-unsaved-pill {\n display: inline-flex;\n align-items: center;\n gap: 0.4rem;\n padding: 0.25rem 0.5rem 0.25rem 0.6rem;\n border: 1px solid hsl(var(--ra-warning, 38 92% 50%) / 0.35);\n background: hsl(var(--ra-warning, 38 92% 50%) / 0.08);\n border-radius: 999px;\n animation: ra-unsaved-slide .14s ease-out;\n}\n.ra-shell .ra-unsaved-pill .ra-unsaved-pill-text,\n.sl-aph .ra-unsaved-pill .ra-unsaved-pill-text {\n font-size: 0.75rem;\n font-weight: 500;\n color: hsl(var(--ra-text));\n white-space: nowrap;\n margin-right: 0.15rem;\n}\n.ra-shell .ra-clipboard-toast {\n position: fixed;\n bottom: 1.25rem;\n left: 50%;\n transform: translateX(-50%);\n z-index: 90;\n display: inline-flex;\n align-items: center;\n gap: 0.5rem;\n max-width: min(28rem, calc(100vw - 2rem));\n padding: 0.55rem 0.85rem;\n border-radius: 999px;\n background: hsl(var(--ra-text));\n color: hsl(var(--ra-surface));\n font-size: 0.75rem;\n line-height: 1;\n box-shadow: 0 8px 24px -10px hsl(0 0% 0% / 0.45);\n animation: ra-clipboard-pop 0.18s ease-out both;\n pointer-events: none;\n}\n@keyframes ra-clipboard-pop {\n from {\n opacity: 0;\n transform: translate(-50%, 6px);\n }\n to {\n opacity: 1;\n transform: translate(-50%, 0);\n }\n}\n.ra-shell .ra-row-menu-wrap {\n display: inline-flex;\n align-items: center;\n margin-left: 0.25rem;\n}\n.ra-shell .ra-row-menu-trigger {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.5rem;\n height: 1.5rem;\n border-radius: 0.35rem;\n background: transparent;\n color: hsl(var(--ra-muted-text));\n opacity: 0;\n transition:\n opacity .15s ease,\n background .15s ease,\n color .15s ease;\n border: 1px solid transparent;\n}\n.ra-shell .ra-row:hover .ra-row-menu-trigger,\n.ra-shell .ra-card-hover:hover .ra-row-menu-trigger,\n.ra-shell .ra-row-menu-trigger:focus-visible,\n.ra-shell .ra-row-menu-trigger[aria-expanded=true] {\n opacity: 1;\n}\n.ra-shell .ra-row-menu-trigger:hover {\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-text));\n}\n.ra-shell .ra-row-menu {\n position: absolute;\n right: 0;\n top: calc(100% + 4px);\n z-index: 50;\n min-width: 11rem;\n padding: 0.25rem;\n border-radius: 0.5rem;\n background: hsl(var(--ra-surface));\n border: 1px solid hsl(var(--ra-border));\n box-shadow: 0 12px 28px -10px hsl(0 0% 0% / 0.25);\n display: flex;\n flex-direction: column;\n gap: 0.125rem;\n}\n.ra-shell .ra-row-menu-item {\n display: inline-flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.4rem 0.55rem;\n border-radius: 0.35rem;\n font-size: 0.75rem;\n color: hsl(var(--ra-text));\n background: transparent;\n border: 0;\n text-align: left;\n width: 100%;\n cursor: pointer;\n}\n.ra-shell .ra-row-menu-item:hover:not(:disabled) {\n background: hsl(var(--ra-muted));\n}\n.ra-shell .ra-row-menu-item:disabled {\n opacity: 0.45;\n cursor: not-allowed;\n}\n.ra-shell .ra-item-list {\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 0;\n}\n.ra-shell .ra-item-list-body {\n flex: 1;\n min-height: 0;\n overflow: auto;\n padding: 1rem 1.25rem 1.5rem;\n}\n.ra-shell .ra-item-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 0.75rem;\n padding: 0.75rem 1.25rem;\n border-bottom: 1px solid hsl(var(--ra-border));\n background: hsl(var(--ra-surface));\n}\n.ra-shell .ra-item-toolbar-title {\n display: flex;\n align-items: baseline;\n gap: 0.5rem;\n min-width: 0;\n}\n.ra-shell .ra-item-toolbar-count {\n font-size: 0.7rem;\n font-weight: 600;\n color: hsl(var(--ra-muted-text));\n background: hsl(var(--ra-muted));\n border: 1px solid hsl(var(--ra-border));\n border-radius: 999px;\n padding: 0.05rem 0.45rem;\n}\n.ra-shell .ra-item-toolbar-actions {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n flex-shrink: 0;\n}\n.ra-shell .ra-item-table-wrap {\n border: 1px solid hsl(var(--ra-border));\n border-radius: var(--ra-radius);\n background: hsl(var(--ra-surface));\n overflow: hidden;\n box-shadow: var(--ra-card-shadow);\n}\n.ra-shell .ra-item-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 0.85rem;\n color: hsl(var(--ra-text));\n}\n.ra-shell .ra-item-table thead th {\n text-align: left;\n font-size: 0.7rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: hsl(var(--ra-muted-text));\n padding: 0.65rem 0.85rem;\n background: hsl(var(--ra-muted) / 0.55);\n border-bottom: 1px solid hsl(var(--ra-border));\n}\n.ra-shell .ra-item-table tbody td {\n padding: 0.65rem 0.85rem;\n border-bottom: 1px solid hsl(var(--ra-border) / 0.7);\n vertical-align: middle;\n}\n.ra-shell .ra-item-table tbody tr:last-child td {\n border-bottom: 0;\n}\n.ra-shell .ra-item-row {\n cursor: pointer;\n transition: background .12s ease;\n}\n.ra-shell .ra-item-row:hover {\n background: var(--ra-row-hover);\n}\n.ra-shell .ra-item-row[data-selected=true] {\n background: var(--ra-row-active-bg);\n}\n.ra-shell .ra-item-row-title {\n font-weight: var(--ra-title-weight);\n color: hsl(var(--ra-text));\n}\n.ra-shell .ra-item-row-sub {\n font-size: 0.75rem;\n color: hsl(var(--ra-muted-text));\n margin-top: 0.15rem;\n}\n.ra-shell .ra-item-row-meta {\n font-size: 0.78rem;\n color: hsl(var(--ra-muted-text));\n}\n.ra-shell .ra-item-row-actions {\n text-align: right;\n white-space: nowrap;\n}\n.ra-shell .ra-item-row-actions .ra-row-action + .ra-row-action {\n margin-left: 0.15rem;\n}\n.ra-shell .ra-item-cards {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(var(--ra-item-card-min, 240px), 1fr));\n gap: 0.85rem;\n}\n.ra-shell .ra-item-gallery {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(var(--ra-item-gallery-min, 320px), 1fr));\n gap: 1rem;\n}\n.ra-shell .ra-item-cards[data-card-size=sm] {\n --ra-item-card-min: 180px;\n}\n.ra-shell .ra-item-cards[data-card-size=md] {\n --ra-item-card-min: 240px;\n}\n.ra-shell .ra-item-cards[data-card-size=lg] {\n --ra-item-card-min: 320px;\n gap: 1rem;\n}\n.ra-shell .ra-item-gallery[data-card-size=sm] {\n --ra-item-gallery-min: 240px;\n}\n.ra-shell .ra-item-gallery[data-card-size=md] {\n --ra-item-gallery-min: 320px;\n}\n.ra-shell .ra-item-gallery[data-card-size=lg] {\n --ra-item-gallery-min: 420px;\n gap: 1.25rem;\n}\n.ra-shell .ra-item-cards[data-card-size=lg] .ra-item-card-title,\n.ra-shell .ra-item-gallery[data-card-size=lg] .ra-item-card-title {\n font-size: 0.95rem;\n white-space: normal;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n}\n.ra-shell .ra-item-cards[data-card-size=lg] .ra-item-card-body,\n.ra-shell .ra-item-gallery[data-card-size=lg] .ra-item-card-body {\n padding: 0.85rem 1rem 1rem;\n}\n.ra-shell .ra-item-card {\n position: relative;\n display: flex;\n flex-direction: column;\n align-items: stretch;\n text-align: left;\n padding: 0;\n background: hsl(var(--ra-surface));\n border: 1px solid hsl(var(--ra-border));\n border-radius: var(--ra-radius);\n overflow: hidden;\n cursor: pointer;\n transition:\n box-shadow .18s ease,\n transform .12s ease,\n border-color .15s ease;\n box-shadow: var(--ra-card-shadow);\n font-family: inherit;\n color: inherit;\n}\n.ra-shell .ra-item-card:hover {\n box-shadow: var(--ra-card-shadow-hover);\n border-color: hsl(var(--ra-accent) / 0.30);\n}\n.ra-shell .ra-item-card[data-selected=true] {\n border-color: hsl(var(--ra-accent) / 0.55);\n box-shadow: var(--ra-card-shadow-hover);\n}\n.ra-shell .ra-item-card-thumb {\n width: 100%;\n aspect-ratio: 1 / 1;\n background:\n linear-gradient(\n 135deg,\n hsl(var(--ra-accent) / 0.12),\n hsl(var(--ra-accent) / 0.04));\n display: flex;\n align-items: center;\n justify-content: center;\n color: hsl(var(--ra-accent));\n overflow: hidden;\n}\n.ra-shell .ra-item-card-thumb--gallery {\n aspect-ratio: 16 / 9;\n}\n.ra-shell .ra-item-card-thumb img {\n width: 100%;\n height: 100%;\n -o-object-fit: cover;\n object-fit: cover;\n}\n.ra-shell .ra-item-card-initials {\n font-family: var(--ra-font-display);\n font-weight: var(--ra-display-weight);\n font-size: 1.5rem;\n letter-spacing: 0.02em;\n}\n.ra-shell .ra-item-card-body {\n padding: 0.65rem 0.8rem 0.85rem;\n min-width: 0;\n}\n.ra-shell .ra-item-card-title {\n font-weight: var(--ra-title-weight);\n font-size: 0.85rem;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ra-shell .ra-item-card-sub {\n font-size: 0.75rem;\n color: hsl(var(--ra-muted-text));\n margin-top: 0.15rem;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ra-shell .ra-item-card-delete {\n position: absolute;\n top: 0.4rem;\n right: 0.4rem;\n background: hsl(var(--ra-surface) / 0.85);\n backdrop-filter: blur(4px);\n opacity: 0;\n transition: opacity .15s ease;\n}\n.ra-shell .ra-item-card:hover .ra-item-card-delete,\n.ra-shell .ra-item-card:focus-within .ra-item-card-delete {\n opacity: 1;\n}\n.ra-shell .ra-item-nav {\n display: flex;\n align-items: center;\n gap: 0.6rem;\n padding: 0.5rem 1.25rem;\n border-bottom: 1px solid hsl(var(--ra-border));\n background: hsl(var(--ra-surface));\n}\n.ra-shell .ra-item-nav-position {\n font-size: 0.72rem;\n color: hsl(var(--ra-muted-text));\n font-variant-numeric: tabular-nums;\n}\n.ra-shell .ra-item-nav-arrows {\n margin-left: auto;\n display: inline-flex;\n align-items: center;\n gap: 0.15rem;\n}\n.ra-shell .ra-item-nav-arrows .ra-row-action[disabled] {\n opacity: 0.35;\n cursor: not-allowed;\n}\n.ra-shell .ra-sibling-rail {\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 0;\n}\n.ra-shell .ra-sibling-back {\n display: inline-flex;\n align-items: center;\n gap: 0.4rem;\n padding: 0.6rem 0.85rem;\n font-size: 0.75rem;\n font-weight: 500;\n color: hsl(var(--ra-muted-text));\n background: hsl(var(--ra-muted) / 0.5);\n border: 0;\n border-bottom: 1px solid hsl(var(--ra-border));\n cursor: pointer;\n text-align: left;\n transition: background .12s ease, color .12s ease;\n}\n.ra-shell .ra-sibling-back:hover {\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-text));\n}\n.ra-shell .ra-sibling-heading {\n padding: 0.6rem 0.85rem 0.4rem;\n font-size: 0.65rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: hsl(var(--ra-muted-text));\n}\n.ra-shell .ra-sibling-body {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n}\n.ra-shell .ra-sibling-list {\n list-style: none;\n margin: 0;\n padding: 0;\n}\n.ra-shell .ra-status-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n border-radius: 9999px;\n}\n.ra-shell .ra-status-icon > svg {\n width: 100%;\n height: 100%;\n display: block;\n}\n.ra-shell .ra-status-icon--own {\n color: hsl(var(--ra-status-own));\n}\n.ra-shell .ra-status-icon--shared {\n color: hsl(var(--ra-status-shared));\n}\n.ra-shell .ra-status-icon--missing {\n color: hsl(var(--ra-status-missing) / 0.7);\n}\n.ra-shell .ra-row-status {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.5rem;\n height: 1.5rem;\n flex-shrink: 0;\n}\n.ra-shell .ra-row-scope {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.25rem;\n height: 1.25rem;\n border-radius: calc(var(--ra-radius) * 0.5);\n background: hsl(var(--ra-muted));\n color: hsl(var(--ra-muted-text));\n flex-shrink: 0;\n margin-left: auto;\n opacity: 0.55;\n transition:\n opacity .12s ease,\n color .12s ease,\n background .12s ease;\n}\n.ra-shell .ra-row:hover .ra-row-scope {\n opacity: 0.85;\n}\n.ra-shell .ra-row[data-selected=true] .ra-row-scope {\n opacity: 1;\n background: hsl(var(--ra-accent) / 0.12);\n color: hsl(var(--ra-accent));\n}\n.ra-shell .ra-row[data-tone=own] .ra-row-sub {\n color: hsl(var(--ra-status-own));\n}\n.ra-shell .ra-row[data-tone=shared] .ra-row-sub {\n color: hsl(var(--ra-status-shared));\n}\n.ra-shell .ra-row[data-selected=true] {\n background:\n linear-gradient(\n 90deg,\n hsl(var(--ra-accent) / 0.10) 0%,\n hsl(var(--ra-accent) / 0.04) 100%);\n border-left-width: 3px;\n border-left-color: hsl(var(--ra-accent));\n}\n.ra-shell .ra-dirty-pip {\n display: inline-block;\n width: 0.45rem;\n height: 0.45rem;\n border-radius: 9999px;\n background: hsl(var(--ra-warning));\n box-shadow: 0 0 0 2px hsl(var(--ra-warning) / 0.18);\n flex-shrink: 0;\n}\n.ra-shell .ra-error-pip {\n display: inline-block;\n width: 0.45rem;\n height: 0.45rem;\n border-radius: 9999px;\n background: hsl(var(--ra-danger, 0 72% 51%));\n box-shadow: 0 0 0 2px hsl(var(--ra-danger, 0 72% 51%) / 0.22);\n flex-shrink: 0;\n}\n.ra-shell .ra-group-summary {\n background: transparent;\n}\n.ra-shell {\n position: relative;\n}\n.ra-shell .ra-help-float {\n position: absolute;\n top: 0.65rem;\n right: 0.85rem;\n z-index: 5;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.6rem;\n height: 1.6rem;\n padding: 0;\n color: hsl(var(--ra-muted-text));\n background: hsl(var(--ra-surface) / 0.85);\n backdrop-filter: blur(6px);\n border: 1px solid hsl(var(--ra-border));\n border-radius: 999px;\n cursor: pointer;\n transition:\n color .12s ease,\n background .12s ease,\n border-color .12s ease;\n}\n.ra-shell .ra-help-float:hover {\n color: hsl(var(--ra-accent));\n border-color: hsl(var(--ra-accent) / 0.4);\n background: hsl(var(--ra-surface));\n}\n.ra-shell .ra-help-float svg {\n width: 0.95rem;\n height: 0.95rem;\n}\n.ra-shell .ra-help-float > span {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n}\n.ra-shell .ra-preview-reopen {\n position: absolute;\n top: 50%;\n right: 0;\n transform: translateY(-50%);\n z-index: 4;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 0.4rem;\n padding: 0.65rem 0.45rem;\n background: hsl(var(--ra-surface));\n color: hsl(var(--ra-muted-text));\n border: 1px solid hsl(var(--ra-border));\n border-right: 0;\n border-radius: calc(var(--ra-radius) * 0.85) 0 0 calc(var(--ra-radius) * 0.85);\n box-shadow: var(--ra-card-shadow);\n cursor: pointer;\n transition:\n color .12s ease,\n background .12s ease,\n padding-right .15s ease;\n writing-mode: vertical-rl;\n font-size: 0.7rem;\n font-weight: 600;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n}\n.ra-shell .ra-preview-reopen:hover {\n color: hsl(var(--ra-accent));\n background: hsl(var(--ra-accent) / 0.04);\n padding-right: 0.6rem;\n}\n.ra-shell .ra-preview-reopen svg {\n width: 0.85rem;\n height: 0.85rem;\n writing-mode: horizontal-tb;\n}\n.ra-shell .ra-unsaved-tray {\n position: relative;\n display: flex;\n align-items: center;\n gap: 0.6rem;\n padding: 0.5rem 0.75rem;\n border: 1px solid hsl(var(--ra-warning, 38 92% 50%) / 0.35);\n background: hsl(var(--ra-warning, 38 92% 50%) / 0.08);\n border-radius: var(--ra-radius);\n font-size: 0.8125rem;\n color: hsl(var(--ra-text));\n animation: ra-unsaved-slide .14s ease-out;\n}\n.ra-shell .ra-unsaved-count {\n flex: 1;\n min-width: 0;\n display: inline-flex;\n align-items: center;\n gap: 0.3rem;\n padding: 0.15rem 0.4rem;\n margin: -0.15rem -0.4rem;\n background: transparent;\n border: 0;\n color: inherit;\n font: inherit;\n font-weight: 500;\n text-align: left;\n cursor: pointer;\n border-radius: calc(var(--ra-radius) - 4px);\n}\n.ra-shell .ra-unsaved-count:hover {\n background: hsl(var(--ra-muted) / 0.6);\n}\n.ra-shell .ra-unsaved-error-chip {\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n border: 1px solid hsl(var(--ra-danger, 0 72% 51%) / 0.35);\n background: hsl(var(--ra-danger, 0 72% 51%) / 0.08);\n color: hsl(var(--ra-danger, 0 72% 51%));\n border-radius: 999px;\n padding: 0.15rem 0.55rem;\n font-size: 0.7rem;\n font-weight: 500;\n cursor: pointer;\n}\n.ra-shell .ra-unsaved-error-chip:hover {\n filter: brightness(0.97);\n}\n.ra-shell .ra-unsaved-popover {\n position: absolute;\n top: calc(100% + 6px);\n left: 0;\n z-index: 60;\n min-width: 18rem;\n max-height: 18rem;\n overflow: auto;\n background: hsl(var(--ra-surface));\n border: 1px solid hsl(var(--ra-border));\n border-radius: var(--ra-radius);\n box-shadow: 0 12px 30px -10px hsl(0 0% 0% / 0.25);\n padding: 0.3rem;\n display: flex;\n flex-direction: column;\n gap: 0.15rem;\n}\n.ra-shell .ra-unsaved-popover-row {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.4rem 0.55rem;\n background: transparent;\n border: 0;\n border-radius: calc(var(--ra-radius) - 2px);\n cursor: pointer;\n text-align: left;\n font: inherit;\n color: hsl(var(--ra-text));\n}\n.ra-shell .ra-unsaved-popover-row:hover {\n background: hsl(var(--ra-muted));\n}\n.ra-shell .ra-unsaved-popover-dot {\n width: 0.5rem;\n height: 0.5rem;\n border-radius: 999px;\n flex-shrink: 0;\n}\n.ra-shell .ra-unsaved-popover-label {\n flex: 1;\n min-width: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n font-size: 0.8125rem;\n}\n.ra-shell .ra-unsaved-popover-ctx {\n color: hsl(var(--ra-muted-text));\n font-size: 0.7rem;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n}\n.ra-shell .ra-unsaved-popover-err {\n color: hsl(var(--ra-danger, 0 72% 51%));\n font-size: 0.7rem;\n font-weight: 500;\n}\n.ra-saveall-overlay {\n position: fixed;\n inset: 0;\n z-index: 100;\n background: hsl(0 0% 0% / 0.45);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 1rem;\n animation: ra-confirm-fade .12s ease-out;\n}\n.ra-saveall-card {\n background: hsl(var(--ra-surface));\n color: hsl(var(--ra-text));\n border: 1px solid hsl(var(--ra-border));\n border-radius: var(--ra-radius);\n box-shadow: 0 24px 48px -16px hsl(0 0% 0% / 0.45);\n width: min(28rem, 100%);\n max-height: min(80vh, 36rem);\n display: flex;\n flex-direction: column;\n animation: ra-confirm-pop .14s ease-out;\n}\n.ra-saveall-header {\n padding: 1rem 1rem 0.5rem;\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n.ra-saveall-title {\n font-weight: 600;\n font-size: 0.95rem;\n}\n.ra-saveall-progress {\n height: 4px;\n background: hsl(var(--ra-muted));\n border-radius: 999px;\n overflow: hidden;\n}\n.ra-saveall-progress-bar {\n height: 100%;\n background: hsl(var(--ra-accent));\n transition: width .2s ease;\n}\n.ra-saveall-counter {\n color: hsl(var(--ra-muted-text));\n font-size: 0.75rem;\n font-variant-numeric: tabular-nums;\n}\n.ra-saveall-list {\n list-style: none;\n margin: 0;\n padding: 0.25rem 0.5rem;\n overflow: auto;\n flex: 1;\n}\n.ra-saveall-row {\n display: flex;\n align-items: center;\n gap: 0.6rem;\n padding: 0.45rem 0.5rem;\n border-radius: calc(var(--ra-radius) - 4px);\n font-size: 0.8125rem;\n}\n.ra-saveall-row[data-status=saving] {\n background: hsl(var(--ra-accent) / 0.06);\n}\n.ra-saveall-row[data-status=saved] {\n color: hsl(var(--ra-muted-text));\n}\n.ra-saveall-row[data-status=error] {\n background: hsl(var(--ra-danger, 0 72% 51%) / 0.06);\n}\n.ra-saveall-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n color: hsl(var(--ra-muted-text));\n}\n.ra-saveall-row[data-status=saved] .ra-saveall-icon {\n color: hsl(var(--ra-success, 142 71% 45%));\n}\n.ra-saveall-row[data-status=saving] .ra-saveall-icon {\n color: hsl(var(--ra-accent));\n}\n.ra-saveall-row[data-status=error] .ra-saveall-icon {\n color: hsl(var(--ra-danger, 0 72% 51%));\n}\n.ra-saveall-label {\n flex: 1;\n min-width: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ra-saveall-err {\n color: hsl(var(--ra-danger, 0 72% 51%));\n font-size: 0.7rem;\n max-width: 12rem;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ra-saveall-actions {\n padding: 0.75rem 1rem 1rem;\n display: flex;\n justify-content: flex-end;\n gap: 0.4rem;\n border-top: 1px solid hsl(var(--ra-border));\n}\n.ra-spin {\n animation: ra-spin 1s linear infinite;\n}\n@keyframes ra-spin {\n to {\n transform: rotate(360deg);\n }\n}\n");
5467
+ var EditorPoolBody = ({
5468
+ editorId,
5469
+ renderEditor
5470
+ }) => {
5471
+ const ctx = useEditorSlotContext(editorId);
5472
+ if (!ctx) return null;
5473
+ return renderEditor(ctx);
5474
+ };
4814
5475
  var TOP_LEVEL_SCOPES = ["collection", "rule", "product"];
4815
5476
  var WARNED_FACET_DEPRECATED = false;
4816
5477
  var DRAFT_ID = "__draft__";
@@ -4836,12 +5497,28 @@ var productItemToSummary = (p) => {
4836
5497
  };
4837
5498
  };
4838
5499
  function RecordsAdminShell(props) {
4839
- return /* @__PURE__ */ jsx(DirtyDraftProvider, { children: /* @__PURE__ */ jsx(RecordsAdminShellInner, { ...props }) });
4840
- }
4841
- function RecordsAdminShellInner(props) {
4842
- const {
4843
- SL,
4844
- appId,
5500
+ const ctx = useMemo(
5501
+ () => ({
5502
+ SL: props.SL,
5503
+ collectionId: props.collectionId,
5504
+ appId: props.appId,
5505
+ recordType: props.recordType
5506
+ }),
5507
+ [props.SL, props.collectionId, props.appId, props.recordType]
5508
+ );
5509
+ return /* @__PURE__ */ jsx(
5510
+ EditorSessionProvider,
5511
+ {
5512
+ ctx,
5513
+ defaultValueFactory: props.defaultData,
5514
+ children: /* @__PURE__ */ jsx(RecordsAdminShellInner, { ...props })
5515
+ }
5516
+ );
5517
+ }
5518
+ function RecordsAdminShellInner(props) {
5519
+ const {
5520
+ SL,
5521
+ appId,
4845
5522
  collectionId,
4846
5523
  recordType,
4847
5524
  label,
@@ -4995,10 +5672,6 @@ function RecordsAdminShellInner(props) {
4995
5672
  useEffect(() => {
4996
5673
  setActiveScope(initialScope);
4997
5674
  }, [initialScope]);
4998
- const [search, setSearch] = useState("");
4999
- const [filter, setFilter] = useState("all");
5000
- const [ruleFilters, setRuleFilters] = useState(EMPTY_RULE_FILTERS);
5001
- const [facetBrowseFilter, setFacetBrowseFilter] = useState(null);
5002
5675
  const [selectedRecordId, setSelectedRecordId] = useState(null);
5003
5676
  const [draftKind, setDraftKind] = useState(null);
5004
5677
  const [ruleWizardStep, setRuleWizardStep] = useState(null);
@@ -5040,40 +5713,32 @@ function RecordsAdminShellInner(props) {
5040
5713
  if (requested === "header" && !headerWillRender) return "footer";
5041
5714
  return requested;
5042
5715
  }, [intro?.reopenAffordance, headerWillRender]);
5043
- const productBrowse = useProductBrowse({
5716
+ const browser = useShellBrowser({
5717
+ ctx,
5044
5718
  SL,
5045
5719
  collectionId,
5046
- search: activeScope === "product" ? search : "",
5047
- enabled: activeScope === "product" && !contextScope?.productId
5720
+ activeScope,
5721
+ contextScope,
5722
+ probeIsLoading: probe.isLoading,
5723
+ selectedProductId,
5724
+ drillTab,
5725
+ classify: classify2
5048
5726
  });
5049
- const recordListEnabled = (activeScope === "rule" || activeScope === "collection") && !probe.isLoading;
5050
- const recordList = useRecordList({
5051
- ctx,
5052
- scopeKind: activeScope,
5727
+ const {
5053
5728
  search,
5729
+ setSearch,
5054
5730
  filter,
5055
- classify: classify2,
5056
- contextScope,
5057
- enabled: recordListEnabled
5058
- });
5059
- const facetBrowse = useFacetBrowse({
5060
- SL,
5061
- collectionId,
5062
- existing: [],
5063
- enabled: (activeScope === "rule" || activeScope === "collection") && !probe.isLoading
5064
- });
5065
- const variantChildren = useProductChildren({
5066
- SL,
5067
- collectionId,
5068
- productId: selectedProductId,
5069
- kind: drillTab === "variant" ? "variant" : null
5070
- });
5071
- const batchChildren = useProductChildren({
5072
- SL,
5073
- collectionId,
5074
- productId: selectedProductId,
5075
- kind: drillTab === "batch" ? "batch" : null
5076
- });
5731
+ setFilter,
5732
+ ruleFilters,
5733
+ setRuleFilters,
5734
+ facetBrowseFilter,
5735
+ setFacetBrowseFilter,
5736
+ productBrowse,
5737
+ recordList,
5738
+ facetBrowse,
5739
+ variantChildren,
5740
+ batchChildren
5741
+ } = browser;
5077
5742
  useEffect(() => {
5078
5743
  if (activeScope !== "product") return;
5079
5744
  if (selectedProductId) return;
@@ -5090,10 +5755,6 @@ function RecordsAdminShellInner(props) {
5090
5755
  if (first?.id) setSelectedRecordId(first.id);
5091
5756
  }, [activeScope, selectedRecordId, recordList.items, cardinality]);
5092
5757
  useEffect(() => {
5093
- setSearch("");
5094
- setFilter("all");
5095
- setRuleFilters(EMPTY_RULE_FILTERS);
5096
- setFacetBrowseFilter(null);
5097
5758
  setSelectedRecordId(null);
5098
5759
  setDraftKind(null);
5099
5760
  }, [activeScope]);
@@ -5230,9 +5891,35 @@ function RecordsAdminShellInner(props) {
5230
5891
  batchChildren.refetch();
5231
5892
  if (isCollection) collectionItems.refetch();
5232
5893
  }, [productBrowse, recordList, facetBrowse, variantChildren, batchChildren, isCollection, collectionItems]);
5233
- const editorCtx = useRecordEditor({
5234
- ctx,
5235
- scope: editingTargetScope ?? parseRef(""),
5894
+ const editorTargetSpec = useMemo(() => {
5895
+ if (!editingTargetScope) return null;
5896
+ const initialFacetRule = editingTargetScope.kind === "rule" ? ruleWizardStep === 2 && ruleWizardRule ? ruleWizardRule : resolved.facetRule ?? { all: [] } : ruleWizardStep === 2 && ruleWizardRule && isCollection && !!selectedItemId ? ruleWizardRule : null;
5897
+ const label2 = deriveDraftLabel ? deriveDraftLabel(resolved.data, editingTargetScope) : void 0;
5898
+ return {
5899
+ scope: editingTargetScope,
5900
+ recordId: resolved.recordId,
5901
+ // Collection-cardinality item drafts must `create` (insert a new
5902
+ // row) instead of upsert — multiple items can share the same scope
5903
+ // + recordType, so an upsert would collide on the server's dedupe
5904
+ // key. Flip on whenever the right pane shows an item that hasn't
5905
+ // been saved yet (no resolved.recordId).
5906
+ createMode: isCollection && !!selectedItemId && !resolved.recordId,
5907
+ ref: editingTargetScope.kind === "rule" ? editingTargetScope.raw : void 0,
5908
+ initialFacetRule,
5909
+ label: label2
5910
+ };
5911
+ }, [
5912
+ editingTargetScope?.raw,
5913
+ editingTargetScope?.kind,
5914
+ resolved.recordId,
5915
+ resolved.facetRule,
5916
+ isCollection,
5917
+ selectedItemId,
5918
+ ruleWizardStep,
5919
+ ruleWizardRule
5920
+ ]);
5921
+ const editorCtx = useEditorBridge({
5922
+ target: editorTargetSpec,
5236
5923
  resolved: {
5237
5924
  data: resolved.data,
5238
5925
  source: resolved.source,
@@ -5242,18 +5929,6 @@ function RecordsAdminShellInner(props) {
5242
5929
  facetRule: resolved.facetRule
5243
5930
  },
5244
5931
  defaultData,
5245
- reseed: dirtyStrategy === "keep" ? "preserve-dirty" : "always",
5246
- // Collection-cardinality item drafts must `create` (insert a new row)
5247
- // instead of `upsert` — multiple items can share the same scope +
5248
- // recordType, so an upsert would collide on the server's dedupe key.
5249
- // Flip on whenever the right pane shows an item that hasn't been
5250
- // saved yet (no resolved.recordId).
5251
- createMode: isCollection && !!selectedItemId && !resolved.recordId,
5252
- // Seed an empty rule for freshly-minted `rule:{id}` refs so the Targeting
5253
- // section can render its empty-state picker. For existing rule records
5254
- // pull the saved rule off the resolved record. Pinned scopes get `null`
5255
- // and the Targeting section stays hidden.
5256
- initialFacetRule: editingTargetScope?.kind === "rule" ? ruleWizardStep === 2 && ruleWizardRule ? ruleWizardRule : resolved.facetRule ?? { all: [] } : ruleWizardStep === 2 && ruleWizardRule && isCollection && !!selectedItemId ? ruleWizardRule : null,
5257
5932
  onSaved: () => {
5258
5933
  onTelemetry?.({ type: "record.save", recordType, ref: editingTargetScope?.raw ?? "", isCreate: resolved.source !== "self" });
5259
5934
  if (ruleWizardStep !== null) {
@@ -5273,8 +5948,7 @@ function RecordsAdminShellInner(props) {
5273
5948
  setSelectedBatchId(void 0);
5274
5949
  }
5275
5950
  refetchAll();
5276
- },
5277
- deriveDraftLabel
5951
+ }
5278
5952
  });
5279
5953
  useUnsavedGuard({
5280
5954
  isDirty: editorCtx.isDirty,
@@ -5286,32 +5960,41 @@ function RecordsAdminShellInner(props) {
5286
5960
  disableParentMessage: dirtyStrategy === "keep"
5287
5961
  });
5288
5962
  const dirtyConfirm = useConfirmDialog();
5289
- const pasteConfirm = usePasteConfirm();
5290
- const draftStore = useDirtyDraftStore();
5291
- const dirtyDrafts = useDirtyDrafts();
5292
- const dirtyKeys = useMemo(
5293
- () => new Set(dirtyDrafts.filter((d) => d.status !== "saved").map((d) => d.key)),
5294
- [dirtyDrafts]
5295
- );
5296
- const errorKeys = useMemo(
5297
- () => new Set(dirtyDrafts.filter((d) => d.status === "error").map((d) => d.key)),
5298
- [dirtyDrafts]
5299
- );
5963
+ const dirtyOverview = useDirtyOverview();
5964
+ const editorSelection = useEditorSelection();
5965
+ const dirtyItems = dirtyOverview.items;
5966
+ const dirtyKeys = useMemo(() => {
5967
+ const s = /* @__PURE__ */ new Set();
5968
+ for (const it of dirtyItems) {
5969
+ if (it.status === "saved") continue;
5970
+ if (it.recordId) s.add(it.recordId);
5971
+ if (it.scope?.raw) s.add(it.scope.raw);
5972
+ }
5973
+ return s;
5974
+ }, [dirtyItems]);
5975
+ const errorKeys = useMemo(() => {
5976
+ const s = /* @__PURE__ */ new Set();
5977
+ for (const it of dirtyItems) {
5978
+ if (it.status !== "error") continue;
5979
+ if (it.recordId) s.add(it.recordId);
5980
+ if (it.scope?.raw) s.add(it.scope.raw);
5981
+ }
5982
+ return s;
5983
+ }, [dirtyItems]);
5300
5984
  const [saveAllOpen, setSaveAllOpen] = useState(false);
5301
5985
  const handleSaveAll = () => setSaveAllOpen(true);
5302
5986
  const handleDiscardAll = () => {
5303
- if (editorCtx.isDirty) editorCtx.reset();
5304
- draftStore.clearAll();
5987
+ dirtyOverview.discardAll();
5305
5988
  };
5306
- const showHeaderUnsavedPill = dirtyStrategy === "keep" && (canRenderInHeader(dirtyDrafts) || dirtyDrafts.filter((d) => d.status !== "saved").length === 0 && editorCtx.isDirty);
5307
- const showTrayBanner = dirtyStrategy === "keep" && !showHeaderUnsavedPill && (dirtyDrafts.some((d) => d.status !== "saved") || editorCtx.isDirty);
5989
+ const showHeaderUnsavedPill = dirtyStrategy === "keep" && canRenderInHeader(dirtyItems);
5990
+ const showTrayBanner = dirtyStrategy === "keep" && !showHeaderUnsavedPill && dirtyItems.length > 0;
5308
5991
  const composedHeaderActions = useMemo(() => {
5309
5992
  if (!showHeaderUnsavedPill) return headerActions;
5310
5993
  return /* @__PURE__ */ jsxs(Fragment, { children: [
5311
5994
  /* @__PURE__ */ jsx(
5312
5995
  HeaderUnsavedPill,
5313
5996
  {
5314
- drafts: dirtyDrafts,
5997
+ items: dirtyItems,
5315
5998
  isSaving: saveAllOpen,
5316
5999
  onSaveAll: handleSaveAll,
5317
6000
  onDiscardAll: handleDiscardAll,
@@ -5329,17 +6012,13 @@ function RecordsAdminShellInner(props) {
5329
6012
  }, [
5330
6013
  showHeaderUnsavedPill,
5331
6014
  headerActions,
5332
- dirtyDrafts,
6015
+ dirtyItems,
5333
6016
  saveAllOpen,
5334
6017
  actionLabels,
5335
6018
  actionIcons,
5336
6019
  i18n
5337
6020
  ]);
5338
- const clipboard = useRecordClipboard({
5339
- appId,
5340
- recordType: recordType ?? "__default"
5341
- });
5342
- const [clipboardNotice, setClipboardNotice] = useState(null);
6021
+ const onLeftSelectRef = useRef(null);
5343
6022
  const { runWithGuard } = useDirtyNavigation({
5344
6023
  strategy: dirtyStrategy,
5345
6024
  isDirty: editorCtx.isDirty,
@@ -5354,32 +6033,15 @@ function RecordsAdminShellInner(props) {
5354
6033
  cancel: i18n.unsavedPromptCancel
5355
6034
  }
5356
6035
  });
5357
- const handleExport = () => {
5358
- if (!csvSchema) return;
5359
- const fileBase = recordType ?? (label.toLowerCase().replace(/\s+/g, "-") || "records");
5360
- const blob = exportCsv(recordList.items, csvSchema);
5361
- downloadBlob(blob, `${fileBase}.csv`);
5362
- onTelemetry?.({ type: "csv.export", recordType, rows: recordList.items.length });
5363
- };
5364
- const handleImport = () => {
5365
- if (!csvSchema) return;
5366
- const inp = document.createElement("input");
5367
- inp.type = "file";
5368
- inp.accept = ".csv,text/csv";
5369
- inp.onchange = async () => {
5370
- const file = inp.files?.[0];
5371
- if (!file) return;
5372
- const report = await importCsv(file, csvSchema, ctx);
5373
- onTelemetry?.({ type: "csv.import", recordType, rows: report.total, errors: report.failed });
5374
- if (report.failed > 0) {
5375
- const fileBase = recordType ?? (label.toLowerCase().replace(/\s+/g, "-") || "records");
5376
- downloadBlob(new Blob([report.annotatedCsv], { type: "text/csv" }), `${fileBase}-errors.csv`);
5377
- }
5378
- refetchAll();
5379
- };
5380
- inp.click();
5381
- };
5382
- const csvBulk = csvSchema ? { onImportCsv: handleImport, onExportCsv: handleExport } : {};
6036
+ const csvBulk = useShellCsvBulk({
6037
+ csvSchema,
6038
+ recordType,
6039
+ label,
6040
+ items: recordList.items,
6041
+ ctx,
6042
+ refetchAll,
6043
+ onTelemetry
6044
+ });
5383
6045
  const [previewScope, setPreviewScope] = useState(null);
5384
6046
  const [drawerOpen, setDrawerOpen] = useState(false);
5385
6047
  const [sidePreviewOpen, setSidePreviewOpen] = useState(true);
@@ -5408,158 +6070,25 @@ function RecordsAdminShellInner(props) {
5408
6070
  }
5409
6071
  return void 0;
5410
6072
  }, [activeScope, editingScope, selectedProductId, productBrowse.items]);
5411
- const copyCurrent = useCallback(() => {
5412
- if (!enableClipboard || !editingScope) return;
5413
- const sourceValue = onCopyOverride ? onCopyOverride({ value: editorCtx.value, scope: editingScope }) : cloneValue(editorCtx.value);
5414
- clipboard.set({
5415
- value: sourceValue,
5416
- sourceScope: editingScope,
5417
- sourceRecordId: resolved.recordId,
5418
- sourceLabel: editorHeaderLabel
5419
- });
5420
- onTelemetry?.({ type: "clipboard.copy", recordType, sourceRef: editingScope.raw });
5421
- setClipboardNotice({
5422
- message: i18n.copyToast.replace("{name}", editorHeaderLabel ?? editingScope.raw),
5423
- variant: "copy"
5424
- });
5425
- }, [
5426
- enableClipboard,
6073
+ const shellClipboard = useShellClipboard({
6074
+ enabled: !!enableClipboard,
6075
+ appId,
6076
+ recordType,
6077
+ i18n,
6078
+ editorCtx,
5427
6079
  editingScope,
5428
- onCopyOverride,
5429
- editorCtx.value,
5430
- clipboard,
5431
6080
  editorHeaderLabel,
6081
+ isCollection,
6082
+ selectedItemId,
6083
+ selectedRecordId,
6084
+ resolved: { source: resolved.source, recordId: resolved.recordId },
5432
6085
  onTelemetry,
5433
- recordType,
5434
- i18n.copyToast
5435
- ]);
5436
- const pasteCurrent = useCallback(async () => {
5437
- if (!enableClipboard || !editingScope || !clipboard.entry) return;
5438
- const compat = checkPasteCompatibility(clipboard.entry.sourceScope, editingScope);
5439
- if (compat.status === "denied") {
5440
- setClipboardNotice({ message: compat.reason ?? "Paste not allowed here", variant: "paste" });
5441
- return;
5442
- }
5443
- if (compat.status === "warn") {
5444
- const ok = await pasteConfirm.confirm({
5445
- title: i18n.pasteWarnTitle,
5446
- body: compat.reason ?? "",
5447
- confirmLabel: i18n.pasteWarnContinue,
5448
- cancelLabel: i18n.pasteConfirmCancel
5449
- });
5450
- if (!ok) return;
5451
- }
5452
- const willReplace = resolved.source === "self";
5453
- if (willReplace) {
5454
- const ok = await pasteConfirm.confirm({
5455
- title: i18n.pasteConfirmTitle,
5456
- body: i18n.pasteConfirmBody.replace(
5457
- "{destination}",
5458
- editorHeaderLabel ?? editingScope.raw
5459
- ),
5460
- confirmLabel: i18n.pasteConfirmReplace,
5461
- cancelLabel: i18n.pasteConfirmCancel
5462
- });
5463
- if (!ok) return;
5464
- }
5465
- const sourceParsed = clipboard.entry.sourceScope;
5466
- const transformed = onPasteOverride ? onPasteOverride(
5467
- { value: clipboard.entry.value, sourceScope: sourceParsed },
5468
- { scope: editingScope, currentValue: editorCtx.value ?? null }
5469
- ) : cloneValue(clipboard.entry.value);
5470
- if (transformed === null) return;
5471
- editorCtx.onChange(transformed);
5472
- onTelemetry?.({
5473
- type: "clipboard.paste",
5474
- recordType,
5475
- sourceRef: clipboard.entry.sourceScope.raw,
5476
- destinationRef: editingScope.raw,
5477
- replaced: willReplace
5478
- });
5479
- setClipboardNotice({
5480
- message: i18n.pasteToast.replace("{name}", clipboard.entry.sourceLabel ?? clipboard.entry.sourceScope.raw),
5481
- variant: "paste"
5482
- });
5483
- }, [
5484
- enableClipboard,
5485
- editingScope,
5486
- clipboard.entry,
5487
- pasteConfirm,
5488
- resolved.source,
6086
+ onCopyOverride,
5489
6087
  onPasteOverride,
5490
- editorCtx,
5491
- onTelemetry,
5492
- recordType,
5493
- editorHeaderLabel,
5494
- i18n.pasteWarnTitle,
5495
- i18n.pasteWarnContinue,
5496
- i18n.pasteConfirmCancel,
5497
- i18n.pasteConfirmTitle,
5498
- i18n.pasteConfirmBody,
5499
- i18n.pasteConfirmReplace,
5500
- i18n.pasteToast
5501
- ]);
5502
- const editorPasteCompat = useMemo(() => {
5503
- if (!enableClipboard || !clipboard.entry || !editingScope) return null;
5504
- const sameRecord = clipboard.entry.sourceRecordId && resolved.recordId && clipboard.entry.sourceRecordId === resolved.recordId;
5505
- const sameAnchor = !clipboard.entry.sourceRecordId && clipboard.entry.sourceScope.raw === editingScope.raw;
5506
- if (sameRecord || sameAnchor) return { status: "denied" };
5507
- return checkPasteCompatibility(clipboard.entry.sourceScope, editingScope);
5508
- }, [enableClipboard, clipboard.entry, editingScope]);
5509
- const editorClipboard = enableClipboard && editingScope ? {
5510
- onCopy: copyCurrent,
5511
- onPaste: () => {
5512
- void pasteCurrent();
5513
- },
5514
- canCopy: true,
5515
- canPaste: !!clipboard.entry && editorPasteCompat?.status !== "denied",
5516
- pasteSourceLabel: clipboard.entry?.sourceLabel,
5517
- pasteWillReplace: resolved.source === "self" && !!clipboard.entry
5518
- } : void 0;
5519
- const [pendingPasteTarget, setPendingPasteTarget] = useState(null);
5520
- useEffect(() => {
5521
- if (!pendingPasteTarget) return;
5522
- if (!editingScope) return;
5523
- const matched = pendingPasteTarget.kind === "record" ? (isCollection ? selectedItemId : selectedRecordId) === pendingPasteTarget.recordId : editingScope.raw === pendingPasteTarget.ref;
5524
- if (!matched) return;
5525
- const t = window.setTimeout(() => {
5526
- setPendingPasteTarget(null);
5527
- void pasteCurrent();
5528
- }, 0);
5529
- return () => window.clearTimeout(t);
5530
- }, [pendingPasteTarget, editingScope, isCollection, selectedItemId, selectedRecordId, pasteCurrent]);
5531
- const rowClipboard = enableClipboard ? (record) => {
5532
- const summaryHasData = record.data != null;
5533
- const sourceParsed = record.scope;
5534
- const compat = clipboard.entry ? checkPasteCompatibility(clipboard.entry.sourceScope, sourceParsed) : null;
5535
- const sameTarget = clipboard.entry ? clipboard.entry.sourceRecordId && record.id ? clipboard.entry.sourceRecordId === record.id : clipboard.entry.sourceScope.raw === record.ref : false;
5536
- return {
5537
- onCopy: summaryHasData ? () => {
5538
- const value = onCopyOverride ? onCopyOverride({ value: record.data, scope: sourceParsed }) : cloneValue(record.data);
5539
- clipboard.set({
5540
- value,
5541
- sourceScope: sourceParsed,
5542
- sourceRecordId: record.id ?? void 0,
5543
- sourceLabel: record.label
5544
- });
5545
- onTelemetry?.({ type: "clipboard.copy", recordType, sourceRef: record.ref });
5546
- setClipboardNotice({
5547
- message: i18n.copyToast.replace("{name}", record.label),
5548
- variant: "copy"
5549
- });
5550
- } : void 0,
5551
- onPaste: () => {
5552
- onLeftSelectRef.current?.(record);
5553
- setPendingPasteTarget(
5554
- record.id ? { kind: "record", recordId: record.id } : { kind: "anchor", ref: record.ref }
5555
- );
5556
- },
5557
- canPaste: !!clipboard.entry && !sameTarget && compat?.status !== "denied",
5558
- pasteWillReplace: record.status === "configured",
5559
- clipboardSourceLabel: clipboard.entry?.sourceLabel
5560
- };
5561
- } : void 0;
5562
- const onLeftSelectRef = useRef(null);
6088
+ onLeftSelectRef
6089
+ });
6090
+ const editorClipboard = shellClipboard.editorClipboard;
6091
+ const rowClipboard = shellClipboard.rowClipboard;
5563
6092
  const baseScopeRef = editingScope?.raw ?? "";
5564
6093
  const itemNounLabel = itemNoun || "item";
5565
6094
  const buildItemUrlValue = useCallback((id) => {
@@ -5750,7 +6279,19 @@ function RecordsAdminShellInner(props) {
5750
6279
  clipboard: editorClipboard,
5751
6280
  actionLabels,
5752
6281
  actionIcons,
5753
- children: renderEditor(editorCtx)
6282
+ children: /* @__PURE__ */ jsx(
6283
+ EditorMountPool,
6284
+ {
6285
+ renderSlot: (slotId) => /* @__PURE__ */ jsx(
6286
+ EditorPoolBody,
6287
+ {
6288
+ editorId: slotId,
6289
+ renderEditor
6290
+ }
6291
+ ),
6292
+ fallback: renderEditor(editorCtx)
6293
+ }
6294
+ )
5754
6295
  }
5755
6296
  );
5756
6297
  const withNav = (node) => itemNav ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full min-h-0", children: [
@@ -5984,15 +6525,8 @@ function RecordsAdminShellInner(props) {
5984
6525
  "data-density": density,
5985
6526
  children: [
5986
6527
  dirtyConfirm.dialog,
5987
- pasteConfirm.dialog,
5988
- clipboardNotice && /* @__PURE__ */ jsx(
5989
- ClipboardToast,
5990
- {
5991
- message: clipboardNotice.message,
5992
- variant: clipboardNotice.variant,
5993
- onDismiss: () => setClipboardNotice(null)
5994
- }
5995
- ),
6528
+ shellClipboard.confirmDialog,
6529
+ shellClipboard.toast,
5996
6530
  (() => {
5997
6531
  const showFloatHelp = !!intro && dismissed && resolvedReopenAffordance === "footer" && !headerWillRender;
5998
6532
  if (!showFloatHelp) return null;
@@ -6074,19 +6608,22 @@ function RecordsAdminShellInner(props) {
6074
6608
  showTrayBanner && /* @__PURE__ */ jsx(
6075
6609
  UnsavedTray,
6076
6610
  {
6077
- drafts: dirtyDrafts,
6611
+ items: dirtyItems,
6078
6612
  isSaving: saveAllOpen,
6079
6613
  onSaveAll: handleSaveAll,
6080
6614
  onDiscardAll: handleDiscardAll,
6081
- onOpenDraft: (d) => {
6082
- if (d.recordId) {
6083
- setSelectedRecordId(d.recordId);
6615
+ onOpenItem: (it) => {
6616
+ editorSelection.focus(it.editorId);
6617
+ if (it.recordId) {
6618
+ setSelectedRecordId(it.recordId);
6084
6619
  setDraftKind(null);
6085
6620
  }
6086
6621
  },
6087
6622
  onJumpToError: () => {
6088
- const first = dirtyDrafts.find((d) => d.status === "error");
6089
- if (first?.recordId) {
6623
+ const first = dirtyItems.find((it) => it.status === "error");
6624
+ if (!first) return;
6625
+ editorSelection.focus(first.editorId);
6626
+ if (first.recordId) {
6090
6627
  setSelectedRecordId(first.recordId);
6091
6628
  setDraftKind(null);
6092
6629
  }
@@ -6106,12 +6643,13 @@ function RecordsAdminShellInner(props) {
6106
6643
  SaveAllProgress,
6107
6644
  {
6108
6645
  open: saveAllOpen,
6109
- drafts: dirtyDrafts,
6110
- store: draftStore,
6646
+ items: dirtyItems,
6647
+ saveOne: dirtyOverview.saveOne,
6111
6648
  onClose: () => setSaveAllOpen(false),
6112
- onJumpToError: (d) => {
6113
- if (d.recordId) {
6114
- setSelectedRecordId(d.recordId);
6649
+ onJumpToError: (it) => {
6650
+ editorSelection.focus(it.editorId);
6651
+ if (it.recordId) {
6652
+ setSelectedRecordId(it.recordId);
6115
6653
  setDraftKind(null);
6116
6654
  }
6117
6655
  },
@@ -6621,6 +7159,414 @@ var ResolvedPreview = ({ children }) => /* @__PURE__ */ jsxs("div", { className:
6621
7159
  /* @__PURE__ */ jsx("div", { className: "text-[10px] uppercase tracking-wide mb-2", style: { color: "hsl(var(--ra-muted-text))" }, children: "Public preview" }),
6622
7160
  children
6623
7161
  ] });
7162
+ var createStore = () => {
7163
+ const map = /* @__PURE__ */ new Map();
7164
+ const listeners = /* @__PURE__ */ new Set();
7165
+ let nextOrder = 0;
7166
+ let cachedList = [];
7167
+ const recompute = () => {
7168
+ cachedList = Array.from(map.values()).sort((a, b) => a.order - b.order);
7169
+ };
7170
+ const emit = () => {
7171
+ recompute();
7172
+ listeners.forEach((l) => l());
7173
+ };
7174
+ return {
7175
+ list: () => cachedList,
7176
+ get: (key) => map.get(key),
7177
+ has: (key) => map.has(key),
7178
+ upsertDraft(input) {
7179
+ const existing = map.get(input.key);
7180
+ const order = existing?.order ?? input.order ?? nextOrder++;
7181
+ const status = input.status ?? existing?.status ?? "dirty";
7182
+ map.set(input.key, {
7183
+ ...input,
7184
+ order,
7185
+ status
7186
+ });
7187
+ emit();
7188
+ },
7189
+ setStatus(key, status, error) {
7190
+ const existing = map.get(key);
7191
+ if (!existing) return;
7192
+ map.set(key, { ...existing, status, error });
7193
+ emit();
7194
+ },
7195
+ clearDraft(key) {
7196
+ if (map.delete(key)) emit();
7197
+ },
7198
+ clearAll() {
7199
+ if (map.size === 0) return;
7200
+ map.clear();
7201
+ emit();
7202
+ },
7203
+ subscribe(listener) {
7204
+ listeners.add(listener);
7205
+ return () => {
7206
+ listeners.delete(listener);
7207
+ };
7208
+ }
7209
+ };
7210
+ };
7211
+ var DirtyDraftContext = createContext(null);
7212
+ var DirtyDraftProvider = ({ children }) => {
7213
+ const storeRef = useRef(null);
7214
+ if (!storeRef.current) storeRef.current = createStore();
7215
+ return /* @__PURE__ */ jsx(DirtyDraftContext.Provider, { value: storeRef.current, children });
7216
+ };
7217
+ var useDirtyDraftStore = () => {
7218
+ const ctx = useContext(DirtyDraftContext);
7219
+ const fallbackRef = useRef(null);
7220
+ if (!ctx && !fallbackRef.current) fallbackRef.current = createStore();
7221
+ return ctx ?? fallbackRef.current;
7222
+ };
7223
+ var useDirtyDrafts = () => {
7224
+ const store = useDirtyDraftStore();
7225
+ return useSyncExternalStore(
7226
+ useCallback((cb) => store.subscribe(cb), [store]),
7227
+ useCallback(() => store.list(), [store]),
7228
+ useCallback(() => store.list(), [store])
7229
+ );
7230
+ };
7231
+ var useDirtyDraft = (key) => {
7232
+ const store = useDirtyDraftStore();
7233
+ const getSnapshot = useCallback(
7234
+ () => key ? store.get(key) : void 0,
7235
+ [store, key]
7236
+ );
7237
+ return useSyncExternalStore(
7238
+ useCallback((cb) => store.subscribe(cb), [store]),
7239
+ getSnapshot,
7240
+ getSnapshot
7241
+ );
7242
+ };
7243
+ var buildDraftKey = (opts) => {
7244
+ if (opts.recordId) return opts.recordId;
7245
+ const kind = opts.draftKind ?? "rec";
7246
+ return `draft:${kind}:${opts.scopeRaw}`;
7247
+ };
7248
+ var useDirtyDraftActions = () => {
7249
+ const store = useDirtyDraftStore();
7250
+ const drafts = useDirtyDrafts();
7251
+ const count = useMemo(() => drafts.filter((d) => d.status !== "saved").length, [drafts]);
7252
+ const errorCount = useMemo(() => drafts.filter((d) => d.status === "error").length, [drafts]);
7253
+ return { drafts, count, errorCount, store };
7254
+ };
7255
+
7256
+ // src/components/RecordsAdmin/hooks/useRecordEditor.ts
7257
+ var isEqual2 = (a, b) => {
7258
+ try {
7259
+ return JSON.stringify(a) === JSON.stringify(b);
7260
+ } catch {
7261
+ return false;
7262
+ }
7263
+ };
7264
+ var cloneSeed = (resolved, defaultData) => {
7265
+ if (resolved.source === "self") return resolved.data;
7266
+ if (resolved.source === "inherited" && resolved.data != null) {
7267
+ try {
7268
+ return structuredClone(resolved.data);
7269
+ } catch {
7270
+ return JSON.parse(JSON.stringify(resolved.data));
7271
+ }
7272
+ }
7273
+ return defaultData?.() ?? {};
7274
+ };
7275
+ var getScopedResolvedRecordId = (scope, resolved) => {
7276
+ if (!resolved.recordId) return void 0;
7277
+ if (scope.itemId && scope.itemId === resolved.recordId) return resolved.recordId;
7278
+ const scopeRaw = scope.raw || "";
7279
+ const sourceRef = resolved.sourceRef || "";
7280
+ if (scopeRaw === sourceRef) return resolved.recordId;
7281
+ return void 0;
7282
+ };
7283
+ function useRecordEditor(args) {
7284
+ const {
7285
+ ctx,
7286
+ scope,
7287
+ resolved,
7288
+ defaultData,
7289
+ onSaved,
7290
+ onDeleted,
7291
+ onSaveError,
7292
+ reseed = "always",
7293
+ initialFacetRule = null,
7294
+ createMode = false,
7295
+ deriveDraftLabel
7296
+ } = args;
7297
+ const queryClient = useQueryClient();
7298
+ const draftStore = useDirtyDraftStore();
7299
+ const scopedResolvedRecordId = getScopedResolvedRecordId(scope, resolved);
7300
+ const draftKey = buildDraftKey({
7301
+ // Scope identity must win during record switches. `resolved.recordId`
7302
+ // can briefly lag behind the newly-selected target while the resolver is
7303
+ // catching up, and keying drafts off that stale id lets one edited item
7304
+ // borrow another item's draft. Once the resolver confirms the matching
7305
+ // record for this scope, we transparently migrate onto the concrete UUID.
7306
+ recordId: scopedResolvedRecordId,
7307
+ scopeRaw: scope.raw,
7308
+ draftKind: scope.kind
7309
+ });
7310
+ const initialDraft = draftStore.get(draftKey);
7311
+ const cleanSeed = cloneSeed(resolved, defaultData);
7312
+ const seed = initialDraft && reseed === "preserve-dirty" ? initialDraft.value : cleanSeed;
7313
+ const seedFacetRule = initialDraft && reseed === "preserve-dirty" ? initialDraft.facetRule : initialFacetRule;
7314
+ const [value, setValue] = useState(seed);
7315
+ const [savedSnapshot, setSavedSnapshot] = useState(
7316
+ initialDraft ? initialDraft.baseline : cleanSeed
7317
+ );
7318
+ const [facetRule, setFacetRule] = useState(seedFacetRule);
7319
+ const [savedFacetRule, setSavedFacetRule] = useState(
7320
+ initialDraft ? initialDraft.baselineFacetRule : initialFacetRule
7321
+ );
7322
+ const [userInteracted, setUserInteracted] = useState(!!initialDraft);
7323
+ const [optimisticSource, setOptimisticSource] = useState(null);
7324
+ const [isSaving, setIsSaving] = useState(false);
7325
+ const [saveError, setSaveError] = useState(null);
7326
+ const valueRef = useRef(seed);
7327
+ useEffect(() => {
7328
+ valueRef.current = value;
7329
+ }, [value]);
7330
+ const prevTargetRef = useRef({
7331
+ scopeRaw: scope.raw,
7332
+ recordId: resolved.recordId
7333
+ });
7334
+ useEffect(() => {
7335
+ const prev = prevTargetRef.current;
7336
+ const targetChanged = prev.scopeRaw !== scope.raw || (prev.recordId ?? null) !== (resolved.recordId ?? null);
7337
+ prevTargetRef.current = { scopeRaw: scope.raw, recordId: resolved.recordId };
7338
+ const fresh = cloneSeed(resolved, defaultData);
7339
+ if (targetChanged) {
7340
+ const incomingKey = buildDraftKey({
7341
+ recordId: getScopedResolvedRecordId(scope, resolved),
7342
+ scopeRaw: scope.raw,
7343
+ draftKind: scope.kind
7344
+ });
7345
+ const incomingDraft = draftStore.get(incomingKey);
7346
+ if (incomingDraft && reseed === "preserve-dirty") {
7347
+ setValue(incomingDraft.value);
7348
+ setSavedSnapshot(incomingDraft.baseline);
7349
+ setFacetRule(incomingDraft.facetRule);
7350
+ setSavedFacetRule(incomingDraft.baselineFacetRule);
7351
+ setUserInteracted(true);
7352
+ } else {
7353
+ setValue(fresh);
7354
+ setSavedSnapshot(fresh);
7355
+ setFacetRule(initialFacetRule);
7356
+ setSavedFacetRule(initialFacetRule);
7357
+ setUserInteracted(false);
7358
+ }
7359
+ } else {
7360
+ if (reseed === "preserve-dirty") {
7361
+ const hasUnsaved = !isEqual2(valueRef.current, savedSnapshot);
7362
+ if (!hasUnsaved) setValue(fresh);
7363
+ } else {
7364
+ setValue(fresh);
7365
+ }
7366
+ setSavedSnapshot(fresh);
7367
+ setFacetRule(initialFacetRule);
7368
+ setSavedFacetRule(initialFacetRule);
7369
+ }
7370
+ setOptimisticSource(null);
7371
+ }, [scope.raw, resolved.recordId, resolved.source, resolved.sourceRef]);
7372
+ const valueDiff = !isEqual2(value, savedSnapshot) || !isEqual2(facetRule, savedFacetRule);
7373
+ const isDirty = userInteracted && valueDiff;
7374
+ const handleChange = useCallback((next) => {
7375
+ setUserInteracted(true);
7376
+ setValue(next);
7377
+ }, []);
7378
+ const handleFacetRuleChange = useCallback((next) => {
7379
+ setUserInteracted(true);
7380
+ setFacetRule(next);
7381
+ }, []);
7382
+ const save = useCallback(async () => {
7383
+ const anchors = parsedRefToScope(scope);
7384
+ const hasAnchors = !!(anchors.productId || anchors.variantId || anchors.batchId || anchors.proofId);
7385
+ const hasRule = !!(facetRule && facetRule.all && facetRule.all.length > 0);
7386
+ const isRuleScope2 = scope.kind === "rule";
7387
+ if (isRuleScope2 && !hasAnchors && !hasRule && !resolved.recordId && !createMode) {
7388
+ console.warn("[useRecordEditor] save() bailed \u2014 rule scope with no clauses, no recordId, not in createMode");
7389
+ return;
7390
+ }
7391
+ const previousSnapshot = savedSnapshot;
7392
+ const previousRuleSnapshot = savedFacetRule;
7393
+ resolved.source;
7394
+ const cacheKey = resolvedRecordQueryKey({
7395
+ collectionId: ctx.collectionId,
7396
+ appId: ctx.appId,
7397
+ recordType: ctx.recordType,
7398
+ productId: scope.productId,
7399
+ variantId: scope.variantId,
7400
+ batchId: scope.batchId,
7401
+ facetId: scope.facetId,
7402
+ facetValue: scope.facetValue,
7403
+ proofId: scope.proofId,
7404
+ recordId: resolved.recordId,
7405
+ withParent: true
7406
+ });
7407
+ const previousCache = queryClient.getQueryData(cacheKey);
7408
+ setSaveError(null);
7409
+ setIsSaving(true);
7410
+ setSavedSnapshot(value);
7411
+ setSavedFacetRule(facetRule);
7412
+ setOptimisticSource("self");
7413
+ queryClient.setQueryData(cacheKey, {
7414
+ data: value,
7415
+ source: "self",
7416
+ sourceRef: scope.raw,
7417
+ recordId: resolved.recordId,
7418
+ parentValue: previousCache?.parentValue ?? resolved.parentValue
7419
+ });
7420
+ try {
7421
+ if (resolved.recordId && resolved.source === "self") {
7422
+ await updateRecord(ctx, resolved.recordId, {
7423
+ data: value,
7424
+ facetRule
7425
+ });
7426
+ } else if (createMode) {
7427
+ await createRecord(ctx, {
7428
+ ref: scope.kind === "rule" && scope.raw ? scope.raw : void 0,
7429
+ scope: anchors,
7430
+ data: value,
7431
+ facetRule
7432
+ });
7433
+ } else {
7434
+ await upsertRecord(ctx, {
7435
+ // External ref only when the underlying scope already carries
7436
+ // one (e.g. rule:{id} for an existing rule record). Empty for
7437
+ // anchor-only writes — server addresses by anchors.
7438
+ ref: scope.kind === "rule" && scope.raw ? scope.raw : void 0,
7439
+ scope: anchors,
7440
+ data: value,
7441
+ facetRule
7442
+ });
7443
+ }
7444
+ draftStore.clearDraft(draftKey);
7445
+ onSaved?.();
7446
+ } catch (err) {
7447
+ setSavedSnapshot(previousSnapshot);
7448
+ setSavedFacetRule(previousRuleSnapshot);
7449
+ setOptimisticSource(null);
7450
+ if (previousCache !== void 0) {
7451
+ queryClient.setQueryData(cacheKey, previousCache);
7452
+ } else {
7453
+ queryClient.invalidateQueries({ queryKey: cacheKey });
7454
+ }
7455
+ setSaveError(err);
7456
+ onSaveError?.(err);
7457
+ throw err;
7458
+ } finally {
7459
+ setIsSaving(false);
7460
+ }
7461
+ }, [scope.raw, value, savedSnapshot, facetRule, savedFacetRule, resolved.source, resolved.parentValue, resolved.recordId, createMode, draftKey]);
7462
+ const reset = useCallback(() => {
7463
+ setValue(savedSnapshot);
7464
+ setFacetRule(savedFacetRule);
7465
+ setUserInteracted(false);
7466
+ draftStore.clearDraft(draftKey);
7467
+ }, [savedSnapshot, savedFacetRule, draftKey]);
7468
+ const remove = useCallback(async () => {
7469
+ if (resolved.source !== "self") return;
7470
+ if (!resolved.recordId) return;
7471
+ await removeRecord(ctx, resolved.recordId);
7472
+ draftStore.clearDraft(draftKey);
7473
+ onDeleted?.();
7474
+ }, [resolved.source, resolved.recordId, draftKey]);
7475
+ const prevDraftKeyRef = useRef(draftKey);
7476
+ const prevScopeRawRef = useRef(scope.raw);
7477
+ useEffect(() => {
7478
+ const prevKey = prevDraftKeyRef.current;
7479
+ const prevScopeRaw = prevScopeRawRef.current;
7480
+ if (prevKey && prevKey !== draftKey && prevScopeRaw === scope.raw) {
7481
+ const stale = draftStore.get(prevKey);
7482
+ if (stale) draftStore.clearDraft(prevKey);
7483
+ }
7484
+ prevDraftKeyRef.current = draftKey;
7485
+ prevScopeRawRef.current = scope.raw;
7486
+ }, [draftKey, scope.raw]);
7487
+ useEffect(() => {
7488
+ if (!isDirty) {
7489
+ return;
7490
+ }
7491
+ const anchors = parsedRefToScope(scope);
7492
+ const saveKind = resolved.recordId && resolved.source === "self" ? "update" : createMode ? "create" : "upsert";
7493
+ const deriveLabel = () => {
7494
+ if (deriveDraftLabel) {
7495
+ try {
7496
+ const custom = deriveDraftLabel(value, scope);
7497
+ if (typeof custom === "string" && custom.trim()) return custom.trim();
7498
+ } catch {
7499
+ }
7500
+ }
7501
+ const KEYS = ["title", "name", "label", "heading", "question", "slug"];
7502
+ const pickString = (obj) => {
7503
+ if (!obj || typeof obj !== "object") return void 0;
7504
+ for (const key of KEYS) {
7505
+ const raw = obj[key];
7506
+ if (typeof raw === "string" && raw.trim()) return raw.trim();
7507
+ }
7508
+ return void 0;
7509
+ };
7510
+ const v = value;
7511
+ const top = pickString(v);
7512
+ if (top) return top;
7513
+ if (v && typeof v === "object") {
7514
+ for (const wrapper of ["display", "content", "meta", "data"]) {
7515
+ const nested = pickString(v[wrapper]);
7516
+ if (nested) return nested;
7517
+ }
7518
+ }
7519
+ if (scope.raw?.startsWith("item:")) return "Untitled item";
7520
+ if (scope.kind === "rule") return "Rule";
7521
+ if (scope.kind && scope.kind !== "collection") {
7522
+ return scope.kind.charAt(0).toUpperCase() + scope.kind.slice(1);
7523
+ }
7524
+ return "Default";
7525
+ };
7526
+ draftStore.upsertDraft({
7527
+ key: draftKey,
7528
+ label: deriveLabel(),
7529
+ context: scope.kind,
7530
+ scopeRaw: scope.raw,
7531
+ recordId: resolved.recordId,
7532
+ value,
7533
+ facetRule,
7534
+ baseline: savedSnapshot,
7535
+ baselineFacetRule: savedFacetRule,
7536
+ createMode,
7537
+ scopeAnchors: anchors,
7538
+ ref: scope.kind === "rule" && scope.raw ? scope.raw : void 0,
7539
+ saveKind,
7540
+ save: async () => {
7541
+ await save();
7542
+ }
7543
+ });
7544
+ }, [isDirty, value, facetRule, savedSnapshot, savedFacetRule, scope.raw, resolved.recordId, resolved.source, createMode, save]);
7545
+ const effectiveSource = optimisticSource ?? resolved.source;
7546
+ const isRuleScope = scope.kind === "rule";
7547
+ const ruleValid = !isRuleScope || isFacetRuleValid(facetRule);
7548
+ const canSave = ruleValid;
7549
+ const cannotSaveReason = !ruleValid ? "Pick at least one value for every facet in the rule before saving." : void 0;
7550
+ return {
7551
+ value,
7552
+ onChange: handleChange,
7553
+ source: effectiveSource,
7554
+ recordId: resolved.recordId,
7555
+ parentValue: resolved.parentValue,
7556
+ scope,
7557
+ isDirty,
7558
+ save,
7559
+ reset,
7560
+ remove,
7561
+ canRemove: effectiveSource === "self",
7562
+ canSave,
7563
+ cannotSaveReason,
7564
+ isSaving,
7565
+ saveError,
7566
+ facetRule,
7567
+ onFacetRuleChange: handleFacetRuleChange
7568
+ };
7569
+ }
6624
7570
  var resolveAllQueryKey = (args) => [
6625
7571
  "records-admin",
6626
7572
  "resolve-all",
@@ -6861,6 +7807,6 @@ function useMergedRecord(args) {
6861
7807
  };
6862
7808
  }
6863
7809
 
6864
- 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, downloadBlob, exportCsv, importCsv, mergeIcons, parseRef, pickHeaderIcon, resolutionChain, resolveRecord, statusToneLabel, useCollectedRecords, useCollectionItems, useDeepLinkState, useDirtyDraft, useDirtyDraftActions, useDirtyDraftStore, useDirtyDrafts, useDirtyNavigation, useFacetBrowse, useIntroDismissed, useItemViewPref, useMergedRecord, usePresentationPref, useProductBrowse, useProductChildren, useRecordClipboard, useRecordEditor, useRecordList, useResolveAllRecords, useResolvedRecord, useRulePreview, useScopeProbe, useUnsavedGuard };
7810
+ 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, parseRef, pickHeaderIcon, resolutionChain, resolveRecord, statusToneLabel, useCollectedRecords, useCollectionItems, useDeepLinkState, useDirtyDraft, useDirtyDraftActions, useDirtyDraftStore, useDirtyDrafts, useDirtyNavigation, useFacetBrowse, useIntroDismissed, useItemViewPref, useMergedRecord, usePresentationPref, useProductBrowse, useProductChildren, useRecordClipboard, useRecordEditor, useRecordList, useResolveAllRecords, useResolvedRecord, useRulePreview, useScopeProbe, useUnsavedGuard };
6865
7811
  //# sourceMappingURL=index.js.map
6866
7812
  //# sourceMappingURL=index.js.map