@proveanything/smartlinks-utils-ui 0.1.13 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2833 @@
1
+ import { cn } from '../../chunk-L7FQ52F5.js';
2
+ import { createContext, useMemo, useState, useEffect, useCallback, useRef, useContext } from 'react';
3
+ import { Package, Layers, Tag, Box, Rows3, Image, LayoutGrid, List, ChevronRight, ChevronDown, Trash2, Eye, X, HelpCircle, Search, Plus, CornerDownLeft, Circle } from 'lucide-react';
4
+ import { useQueryClient, useInfiniteQuery, useQuery } from '@tanstack/react-query';
5
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
+
7
+ // src/components/RecordsAdmin/types/i18n.ts
8
+ var DEFAULT_I18N = {
9
+ emptyTitle: "Nothing here yet",
10
+ emptyBody: "Pick a record from the left to start editing.",
11
+ noResults: "No records match your search.",
12
+ searchPlaceholder: "Search\u2026",
13
+ filterAll: "All",
14
+ filterConfigured: "Configured",
15
+ filterPartial: "Partial",
16
+ filterEmpty: "Empty",
17
+ save: "Save",
18
+ discard: "Discard",
19
+ delete: "Delete record",
20
+ saving: "Saving\u2026",
21
+ saveError: "Save failed",
22
+ inherited: "Inherited",
23
+ override: "Override",
24
+ reset: "Reset to inherited",
25
+ bulkActions: "Bulk actions",
26
+ applyToMany: "Apply to many\u2026",
27
+ copyFrom: "Copy from\u2026",
28
+ clear: "Clear records\u2026",
29
+ importCsv: "Import CSV",
30
+ exportCsv: "Export CSV",
31
+ preview: "Preview",
32
+ editor: "Editor",
33
+ closePreview: "Close preview",
34
+ openPreview: "Open preview",
35
+ previewAs: "Preview as",
36
+ previewAsDefault: "Same as edited",
37
+ confirmDelete: "Confirm delete",
38
+ unsavedBadge: "Unsaved",
39
+ unsavedPromptTitle: "Unsaved changes",
40
+ unsavedPromptBody: "You have unsaved changes. What would you like to do?",
41
+ unsavedPromptDiscard: "Discard changes",
42
+ unsavedPromptCancel: "Stay here",
43
+ unsavedPromptSave: "Save & continue",
44
+ presentationList: "List",
45
+ presentationGrid: "Grid",
46
+ presentationGallery: "Gallery",
47
+ presentationCompact: "Compact",
48
+ newItem: "New item",
49
+ noItemsTitle: "No items yet",
50
+ noItemsBody: "Create your first item to get started."
51
+ };
52
+
53
+ // src/components/RecordsAdmin/types/presentation.ts
54
+ var ALL_PRESENTATIONS = ["list", "grid", "gallery", "compact"];
55
+
56
+ // src/components/RecordsAdmin/data/refs.ts
57
+ var KIND_KEYS = {
58
+ product: "productId",
59
+ facet: "facetId",
60
+ variant: "variantId",
61
+ batch: "batchId",
62
+ proof: "proofId",
63
+ collection: "collectionId"
64
+ };
65
+ var parseRef = (raw) => {
66
+ const parsed = { kind: "collection", raw };
67
+ if (!raw) return parsed;
68
+ const segments = raw.split("/").filter(Boolean);
69
+ let kind = "collection";
70
+ for (const seg of segments) {
71
+ const idx = seg.indexOf(":");
72
+ if (idx < 0) continue;
73
+ const k = seg.slice(0, idx);
74
+ const id = seg.slice(idx + 1);
75
+ if (k === "facet") {
76
+ const eq2 = id.indexOf("=");
77
+ if (eq2 >= 0) {
78
+ parsed.facetId = id.slice(0, eq2);
79
+ parsed.facetValue = id.slice(eq2 + 1);
80
+ } else {
81
+ parsed.facetId = id;
82
+ }
83
+ } else if (k in KIND_KEYS) {
84
+ parsed[KIND_KEYS[k]] = id;
85
+ }
86
+ if (k !== "collection") kind = k;
87
+ }
88
+ parsed.kind = kind;
89
+ return parsed;
90
+ };
91
+ var buildRef = (a) => {
92
+ const parts = [];
93
+ if (a.facetId) {
94
+ parts.push(a.facetValue ? `facet:${a.facetId}=${a.facetValue}` : `facet:${a.facetId}`);
95
+ }
96
+ if (a.productId) parts.push(`product:${a.productId}`);
97
+ if (a.variantId) parts.push(`variant:${a.variantId}`);
98
+ if (a.batchId) parts.push(`batch:${a.batchId}`);
99
+ if (a.proofId) parts.push(`proof:${a.proofId}`);
100
+ return parts.join("/");
101
+ };
102
+ var resolutionChain = (target, supportedScopes) => {
103
+ const chain = [];
104
+ if (supportedScopes.includes("batch") && target.batchId && target.productId) {
105
+ chain.push(buildRef({ productId: target.productId, batchId: target.batchId }));
106
+ }
107
+ if (supportedScopes.includes("variant") && target.variantId && target.productId) {
108
+ chain.push(buildRef({ productId: target.productId, variantId: target.variantId }));
109
+ }
110
+ if (supportedScopes.includes("product") && target.productId) {
111
+ chain.push(buildRef({ productId: target.productId }));
112
+ }
113
+ if (supportedScopes.includes("facet") && target.facetId) {
114
+ chain.push(buildRef({ facetId: target.facetId, facetValue: target.facetValue }));
115
+ }
116
+ return Array.from(new Set(chain));
117
+ };
118
+
119
+ // src/components/RecordsAdmin/data/scopeBridge.ts
120
+ var parsedRefToScope = (ref) => {
121
+ const scope = {};
122
+ if (ref.productId) scope.productId = ref.productId;
123
+ if (ref.variantId) scope.variantId = ref.variantId;
124
+ if (ref.batchId) scope.batchId = ref.batchId;
125
+ if (ref.proofId) scope.proofId = ref.proofId;
126
+ if (ref.facetId) {
127
+ scope.facets = [{
128
+ key: ref.facetId,
129
+ valueKeys: ref.facetValue ? [ref.facetValue] : []
130
+ }];
131
+ }
132
+ return scope;
133
+ };
134
+ var parsedRefToTarget = (ref) => {
135
+ const target = {};
136
+ if (ref.productId) target.productId = ref.productId;
137
+ if (ref.variantId) target.variantId = ref.variantId;
138
+ if (ref.batchId) target.batchId = ref.batchId;
139
+ if (ref.proofId) target.proofId = ref.proofId;
140
+ if (ref.facetId && ref.facetValue) {
141
+ target.facets = { [ref.facetId]: [ref.facetValue] };
142
+ }
143
+ return target;
144
+ };
145
+ var scopesEqual = (a, b) => {
146
+ if ((a.productId ?? null) !== (b.productId ?? null)) return false;
147
+ if ((a.variantId ?? null) !== (b.variantId ?? null)) return false;
148
+ if ((a.batchId ?? null) !== (b.batchId ?? null)) return false;
149
+ if ((a.proofId ?? null) !== (b.proofId ?? null)) return false;
150
+ const af = a.facets ?? [];
151
+ const bf = b.facets ?? [];
152
+ if (af.length !== bf.length) return false;
153
+ const norm = (xs) => xs.map((f) => `${f.key}:${[...f.valueKeys].sort().join(",")}`).sort().join("|");
154
+ return norm(af) === norm(bf);
155
+ };
156
+
157
+ // src/components/RecordsAdmin/data/records.ts
158
+ var listRecords = async (ctx, params = {}) => {
159
+ const { limit = 100, offset, ref, refPrefix, q, sort } = params;
160
+ const res = await ctx.SL.app.records.list(
161
+ ctx.collectionId,
162
+ ctx.appId,
163
+ { recordType: ctx.recordType, limit, offset, ref, refPrefix, q, sort },
164
+ true
165
+ );
166
+ return {
167
+ data: res?.data ?? [],
168
+ total: res?.pagination?.total ?? (res?.data?.length ?? 0),
169
+ hasMore: res?.pagination?.hasMore ?? false
170
+ };
171
+ };
172
+ var getRecordByRef = async (ctx, ref) => {
173
+ const res = await ctx.SL.app.records.list(
174
+ ctx.collectionId,
175
+ ctx.appId,
176
+ { recordType: ctx.recordType, ref, limit: 1 },
177
+ true
178
+ );
179
+ return res?.data?.[0] ?? null;
180
+ };
181
+ var upsertRecord = async (ctx, write) => {
182
+ const ref = write.ref ?? deriveRefFromScope(write.scope);
183
+ const res = await ctx.SL.app.records.upsert(ctx.collectionId, ctx.appId, {
184
+ ref,
185
+ recordType: ctx.recordType,
186
+ scope: write.scope,
187
+ data: write.data,
188
+ status: write.status,
189
+ startsAt: write.startsAt,
190
+ expiresAt: write.expiresAt,
191
+ customId: write.customId,
192
+ sourceSystem: write.sourceSystem
193
+ });
194
+ return { record: res, isCreate: !!res?.created };
195
+ };
196
+ var deleteRecord = async (ctx, ref) => {
197
+ const existing = await getRecordByRef(ctx, ref).catch(() => null);
198
+ if (!existing) return false;
199
+ await ctx.SL.app.records.remove(ctx.collectionId, ctx.appId, existing.id, true);
200
+ return true;
201
+ };
202
+ var restoreRecord = async (ctx, recordId) => ctx.SL.app.records.restore(ctx.collectionId, ctx.appId, recordId);
203
+ var matchRecords = async (ctx, target, opts = {}) => ctx.SL.app.records.match(
204
+ ctx.collectionId,
205
+ ctx.appId,
206
+ {
207
+ target,
208
+ recordType: ctx.recordType,
209
+ strategy: opts.strategy ?? "all",
210
+ at: opts.at,
211
+ includeScheduled: opts.includeScheduled,
212
+ includeExpired: opts.includeExpired,
213
+ limit: opts.limit
214
+ },
215
+ true
216
+ );
217
+ var bulkUpsert = async (ctx, entries) => {
218
+ const CHUNK = 500;
219
+ let saved = 0;
220
+ let failed = 0;
221
+ for (let i = 0; i < entries.length; i += CHUNK) {
222
+ const slice = entries.slice(i, i + CHUNK);
223
+ const items = slice.map((e) => ({
224
+ ref: e.ref ?? deriveRefFromScope(e.scope),
225
+ recordType: ctx.recordType,
226
+ scope: e.scope,
227
+ data: e.data,
228
+ status: e.status
229
+ }));
230
+ const res = await ctx.SL.app.records.bulkUpsert(ctx.collectionId, ctx.appId, items).catch(() => ({ saved: 0, failed: items.length }));
231
+ saved += res.saved ?? 0;
232
+ failed += res.failed ?? 0;
233
+ }
234
+ return { saved, failed };
235
+ };
236
+ var bulkDelete = async (ctx, input) => {
237
+ if ("scope" in input) {
238
+ const res = await ctx.SL.app.records.bulkDelete(
239
+ ctx.collectionId,
240
+ ctx.appId,
241
+ { scope: input.scope, recordType: ctx.recordType }
242
+ );
243
+ return { removed: res.deleted ?? 0 };
244
+ }
245
+ const CHUNK = 1e3;
246
+ let removed = 0;
247
+ for (let i = 0; i < input.refs.length; i += CHUNK) {
248
+ const slice = input.refs.slice(i, i + CHUNK);
249
+ const res = await ctx.SL.app.records.bulkDelete(
250
+ ctx.collectionId,
251
+ ctx.appId,
252
+ { refs: slice, recordType: ctx.recordType }
253
+ );
254
+ removed += res.deleted ?? 0;
255
+ }
256
+ return { removed };
257
+ };
258
+ var deriveRefFromScope = (scope) => {
259
+ const parts = [];
260
+ if (scope.facets && scope.facets.length > 0) {
261
+ const sorted = [...scope.facets].sort((a, b) => a.key.localeCompare(b.key));
262
+ for (const f of sorted) {
263
+ const vals = [...f.valueKeys].sort().join(",");
264
+ parts.push(vals ? `facet:${f.key}=${vals}` : `facet:${f.key}`);
265
+ }
266
+ }
267
+ if (scope.productId) parts.push(`product:${scope.productId}`);
268
+ if (scope.variantId) parts.push(`variant:${scope.variantId}`);
269
+ if (scope.batchId) parts.push(`batch:${scope.batchId}`);
270
+ if (scope.proofId) parts.push(`proof:${scope.proofId}`);
271
+ return parts.join("/");
272
+ };
273
+
274
+ // src/components/RecordsAdmin/hooks/useRecordList.ts
275
+ var defaultClassify = (r) => {
276
+ if (!r.data) return "empty";
277
+ const keys = Object.keys(r.data);
278
+ if (keys.length === 0) return "empty";
279
+ return "configured";
280
+ };
281
+ var matchesScope = (kind, p) => {
282
+ if (kind === "product") return !!p.productId && !p.variantId && !p.batchId;
283
+ if (kind === "facet") return !!p.facetId;
284
+ if (kind === "variant") return !!p.variantId;
285
+ if (kind === "batch") return !!p.batchId;
286
+ return false;
287
+ };
288
+ var matchesContext = (p, ctx) => {
289
+ if (!ctx) return true;
290
+ if (ctx.productId && p.productId !== ctx.productId) return false;
291
+ if (ctx.variantId && p.variantId !== ctx.variantId) return false;
292
+ if (ctx.batchId && p.batchId !== ctx.batchId) return false;
293
+ return true;
294
+ };
295
+ var toSummary = (rec) => {
296
+ const ref = rec.ref ?? "";
297
+ const scope = parseRef(ref);
298
+ return {
299
+ id: rec.id,
300
+ ref,
301
+ scope,
302
+ data: rec.data ?? null,
303
+ status: "configured",
304
+ label: scope.batchId ?? scope.variantId ?? scope.productId ?? scope.facetValue ?? scope.facetId ?? ref,
305
+ updatedAt: rec.updatedAt
306
+ };
307
+ };
308
+ var QK_BASE = ["records-admin", "list"];
309
+ var useRecordList = (args) => {
310
+ const {
311
+ ctx,
312
+ scopeKind,
313
+ search = "",
314
+ filter = "all",
315
+ classify,
316
+ enabled = true,
317
+ scaffolder,
318
+ contextScope,
319
+ pageSize = 100
320
+ } = args;
321
+ const queryClient = useQueryClient();
322
+ const queryKey = useMemo(
323
+ () => [
324
+ ...QK_BASE,
325
+ ctx.collectionId,
326
+ ctx.appId,
327
+ ctx.recordType,
328
+ scopeKind,
329
+ contextScope?.productId ?? null,
330
+ contextScope?.variantId ?? null,
331
+ contextScope?.batchId ?? null
332
+ ],
333
+ [ctx.collectionId, ctx.appId, ctx.recordType, scopeKind, contextScope?.productId, contextScope?.variantId, contextScope?.batchId]
334
+ );
335
+ const query = useInfiniteQuery({
336
+ queryKey,
337
+ enabled,
338
+ initialPageParam: 0,
339
+ queryFn: async ({ pageParam }) => {
340
+ const offset = pageParam;
341
+ const { data, total: total2, hasMore } = await listRecords(ctx, { limit: pageSize, offset });
342
+ return { data, total: total2, hasMore, nextOffset: offset + data.length };
343
+ },
344
+ getNextPageParam: (last) => last.hasMore ? last.nextOffset : void 0,
345
+ staleTime: 3e4
346
+ });
347
+ const [scaffolded, setScaffolded] = useState(null);
348
+ const rawItems = useMemo(() => {
349
+ const all = query.data?.pages.flatMap((p) => p.data) ?? [];
350
+ const cls = classify ?? defaultClassify;
351
+ return all.map(toSummary).filter((s) => matchesScope(scopeKind, s.scope)).filter((s) => matchesContext(s.scope, contextScope)).map((s) => ({ ...s, status: cls(s) }));
352
+ }, [query.data, scopeKind, classify, contextScope]);
353
+ useEffect(() => {
354
+ if (!scaffolder) {
355
+ setScaffolded(null);
356
+ return;
357
+ }
358
+ let cancelled = false;
359
+ Promise.resolve(scaffolder(rawItems)).then((next) => {
360
+ if (!cancelled) setScaffolded(next);
361
+ });
362
+ return () => {
363
+ cancelled = true;
364
+ };
365
+ }, [rawItems, scaffolder]);
366
+ const items = scaffolded ?? rawItems;
367
+ const filtered = useMemo(() => {
368
+ let out = items;
369
+ if (filter !== "all") out = out.filter((r) => r.status === filter);
370
+ if (search.trim()) {
371
+ const q = search.trim().toLowerCase();
372
+ out = out.filter((r) => `${r.label} ${r.subtitle ?? ""} ${r.ref}`.toLowerCase().includes(q));
373
+ }
374
+ return out;
375
+ }, [items, filter, search]);
376
+ const counts = useMemo(() => ({
377
+ all: items.length,
378
+ configured: items.filter((r) => r.status === "configured").length,
379
+ partial: items.filter((r) => r.status === "partial").length,
380
+ empty: items.filter((r) => r.status === "empty").length
381
+ }), [items]);
382
+ const refetch = useCallback(() => {
383
+ queryClient.invalidateQueries({ queryKey: [...QK_BASE, ctx.collectionId, ctx.appId, ctx.recordType] });
384
+ }, [queryClient, ctx.collectionId, ctx.appId, ctx.recordType]);
385
+ const total = query.data?.pages[query.data.pages.length - 1]?.total ?? items.length;
386
+ return {
387
+ items: filtered,
388
+ total,
389
+ counts,
390
+ isLoading: query.isLoading,
391
+ error: query.error ?? null,
392
+ refetch,
393
+ hasNextPage: query.hasNextPage,
394
+ isFetchingNextPage: query.isFetchingNextPage,
395
+ fetchNextPage: query.fetchNextPage
396
+ };
397
+ };
398
+
399
+ // src/components/RecordsAdmin/data/resolveRecord.ts
400
+ var resolveRecord = async (args) => {
401
+ const target = parsedRefToTarget(args.target);
402
+ const editingScope = parsedRefToScope(args.target);
403
+ const result = await matchRecords(args.ctx, target, { strategy: "all" }).catch(() => null);
404
+ const records = result?.records ?? [];
405
+ if (records.length === 0) {
406
+ return { data: null, source: "empty" };
407
+ }
408
+ const winner = records[0];
409
+ const winnerIsSelf = scopesEqual(winner.scope, editingScope);
410
+ if (winnerIsSelf) {
411
+ const parent = records[1];
412
+ return {
413
+ data: winner.data,
414
+ source: "self",
415
+ sourceRef: winner.ref ?? void 0,
416
+ parentValue: args.withParent && parent ? parent.data : void 0
417
+ };
418
+ }
419
+ return {
420
+ data: winner.data,
421
+ source: "inherited",
422
+ sourceRef: winner.ref ?? void 0,
423
+ parentValue: args.withParent ? winner.data : void 0
424
+ };
425
+ };
426
+
427
+ // src/components/RecordsAdmin/hooks/useResolvedRecord.ts
428
+ var DEFAULT_SCOPES = ["batch", "variant", "product", "facet"];
429
+ var resolvedRecordQueryKey = (args) => [
430
+ "records-admin",
431
+ "resolved",
432
+ args.collectionId,
433
+ args.appId,
434
+ args.recordType,
435
+ args.productId ?? null,
436
+ args.variantId ?? null,
437
+ args.batchId ?? null,
438
+ args.facetId ?? null,
439
+ args.facetValue ?? null,
440
+ args.proofId ?? null,
441
+ args.withParent ?? true
442
+ ];
443
+ function useResolvedRecord(args) {
444
+ const {
445
+ SL,
446
+ appId,
447
+ recordType,
448
+ collectionId,
449
+ productId,
450
+ variantId,
451
+ batchId,
452
+ facetId,
453
+ facetValue,
454
+ proofId,
455
+ supportedScopes = DEFAULT_SCOPES,
456
+ enabled = true,
457
+ withParent = true
458
+ } = args;
459
+ const query = useQuery({
460
+ queryKey: resolvedRecordQueryKey({
461
+ collectionId,
462
+ appId,
463
+ recordType,
464
+ productId,
465
+ variantId,
466
+ batchId,
467
+ facetId,
468
+ facetValue,
469
+ proofId,
470
+ withParent
471
+ }),
472
+ enabled: enabled && !!collectionId && !!appId,
473
+ staleTime: 15e3,
474
+ queryFn: async () => {
475
+ const target = parseRef("");
476
+ target.productId = productId;
477
+ target.variantId = variantId;
478
+ target.batchId = batchId;
479
+ target.facetId = facetId;
480
+ target.facetValue = facetValue;
481
+ target.proofId = proofId;
482
+ return resolveRecord({
483
+ ctx: { SL, collectionId, appId, recordType },
484
+ target,
485
+ withParent
486
+ });
487
+ }
488
+ });
489
+ const data = query.data ?? { data: null, source: "empty" };
490
+ return {
491
+ ...data,
492
+ isLoading: query.isLoading,
493
+ error: query.error ?? null
494
+ };
495
+ }
496
+
497
+ // src/components/RecordsAdmin/hooks/useRecordEditor.ts
498
+ var isEqual = (a, b) => {
499
+ try {
500
+ return JSON.stringify(a) === JSON.stringify(b);
501
+ } catch {
502
+ return false;
503
+ }
504
+ };
505
+ var cloneSeed = (resolved, defaultData) => {
506
+ if (resolved.source === "self") return resolved.data;
507
+ if (resolved.source === "inherited" && resolved.data != null) {
508
+ try {
509
+ return structuredClone(resolved.data);
510
+ } catch {
511
+ return JSON.parse(JSON.stringify(resolved.data));
512
+ }
513
+ }
514
+ return defaultData?.() ?? {};
515
+ };
516
+ function useRecordEditor(args) {
517
+ const {
518
+ ctx,
519
+ scope,
520
+ resolved,
521
+ defaultData,
522
+ onSaved,
523
+ onDeleted,
524
+ onSaveError,
525
+ reseed = "always"
526
+ } = args;
527
+ const queryClient = useQueryClient();
528
+ const seed = cloneSeed(resolved, defaultData);
529
+ const [value, setValue] = useState(seed);
530
+ const [savedSnapshot, setSavedSnapshot] = useState(seed);
531
+ const [optimisticSource, setOptimisticSource] = useState(null);
532
+ const [isSaving, setIsSaving] = useState(false);
533
+ const [saveError, setSaveError] = useState(null);
534
+ const valueRef = useRef(seed);
535
+ useEffect(() => {
536
+ valueRef.current = value;
537
+ }, [value]);
538
+ useEffect(() => {
539
+ const fresh = cloneSeed(resolved, defaultData);
540
+ if (reseed === "preserve-dirty") {
541
+ const hasUnsaved = !isEqual(valueRef.current, savedSnapshot);
542
+ if (!hasUnsaved) setValue(fresh);
543
+ } else {
544
+ setValue(fresh);
545
+ }
546
+ setSavedSnapshot(fresh);
547
+ setOptimisticSource(null);
548
+ }, [scope.raw, resolved.source, resolved.sourceRef]);
549
+ const isDirty = !isEqual(value, savedSnapshot);
550
+ const save = useCallback(async () => {
551
+ if (!scope.raw) return;
552
+ const previousSnapshot = savedSnapshot;
553
+ resolved.source;
554
+ const cacheKey = resolvedRecordQueryKey({
555
+ collectionId: ctx.collectionId,
556
+ appId: ctx.appId,
557
+ recordType: ctx.recordType,
558
+ productId: scope.productId,
559
+ variantId: scope.variantId,
560
+ batchId: scope.batchId,
561
+ facetId: scope.facetId,
562
+ facetValue: scope.facetValue,
563
+ proofId: scope.proofId,
564
+ withParent: true
565
+ });
566
+ const previousCache = queryClient.getQueryData(cacheKey);
567
+ setSaveError(null);
568
+ setIsSaving(true);
569
+ setSavedSnapshot(value);
570
+ setOptimisticSource("self");
571
+ queryClient.setQueryData(cacheKey, {
572
+ data: value,
573
+ source: "self",
574
+ sourceRef: scope.raw,
575
+ parentValue: previousCache?.parentValue ?? resolved.parentValue
576
+ });
577
+ try {
578
+ await upsertRecord(ctx, {
579
+ ref: scope.raw,
580
+ scope: parsedRefToScope(scope),
581
+ data: value
582
+ });
583
+ onSaved?.();
584
+ } catch (err) {
585
+ setSavedSnapshot(previousSnapshot);
586
+ setOptimisticSource(null);
587
+ if (previousCache !== void 0) {
588
+ queryClient.setQueryData(cacheKey, previousCache);
589
+ } else {
590
+ queryClient.invalidateQueries({ queryKey: cacheKey });
591
+ }
592
+ setSaveError(err);
593
+ onSaveError?.(err);
594
+ throw err;
595
+ } finally {
596
+ setIsSaving(false);
597
+ }
598
+ }, [scope.raw, value, savedSnapshot, resolved.source, resolved.parentValue]);
599
+ const reset = useCallback(() => {
600
+ setValue(savedSnapshot);
601
+ }, [savedSnapshot]);
602
+ const remove = useCallback(async () => {
603
+ if (resolved.source !== "self" || !scope.raw) return;
604
+ await deleteRecord(ctx, scope.raw);
605
+ onDeleted?.();
606
+ }, [resolved.source, scope.raw]);
607
+ const effectiveSource = optimisticSource ?? resolved.source;
608
+ return {
609
+ value,
610
+ onChange: setValue,
611
+ source: effectiveSource,
612
+ parentValue: resolved.parentValue,
613
+ scope,
614
+ isDirty,
615
+ save,
616
+ reset,
617
+ remove,
618
+ canRemove: effectiveSource === "self",
619
+ isSaving,
620
+ saveError
621
+ };
622
+ }
623
+ var lsKey = (appId, recordType) => `ra:intro:${appId}:${recordType}`;
624
+ var useIntroDismissed = (SL, collectionId, appId, recordType) => {
625
+ const [dismissed, setDismissed] = useState(() => {
626
+ try {
627
+ return localStorage.getItem(lsKey(appId, recordType)) === "1";
628
+ } catch {
629
+ return false;
630
+ }
631
+ });
632
+ useEffect(() => {
633
+ let cancelled = false;
634
+ (async () => {
635
+ try {
636
+ const cfg = await SL?.appConfiguration?.getConfig?.({ collectionId, appId, admin: true });
637
+ if (cancelled) return;
638
+ const flag = cfg?._meta?.introDismissed?.[recordType];
639
+ if (flag) setDismissed(true);
640
+ } catch {
641
+ }
642
+ })();
643
+ return () => {
644
+ cancelled = true;
645
+ };
646
+ }, [SL, collectionId, appId, recordType]);
647
+ const dismiss = useCallback(async () => {
648
+ setDismissed(true);
649
+ try {
650
+ localStorage.setItem(lsKey(appId, recordType), "1");
651
+ } catch {
652
+ }
653
+ try {
654
+ const cfg = await SL?.appConfiguration?.getConfig?.({ collectionId, appId, admin: true }).catch(() => ({}));
655
+ const next = {
656
+ ...cfg ?? {},
657
+ _meta: {
658
+ ...cfg?._meta ?? {},
659
+ introDismissed: { ...cfg?._meta?.introDismissed ?? {}, [recordType]: true }
660
+ }
661
+ };
662
+ await SL?.appConfiguration?.setConfig?.({ collectionId, appId, admin: true, config: next });
663
+ } catch {
664
+ }
665
+ }, [SL, collectionId, appId, recordType]);
666
+ const undismiss = useCallback(() => {
667
+ setDismissed(false);
668
+ try {
669
+ localStorage.removeItem(lsKey(appId, recordType));
670
+ } catch {
671
+ }
672
+ }, [appId, recordType]);
673
+ return { dismissed, dismiss, undismiss };
674
+ };
675
+ var useScopeProbe = ({ SL, collectionId, admin = true, enabled = true }) => {
676
+ const query = useQuery({
677
+ queryKey: ["records-admin", "scope-probe", collectionId, admin],
678
+ enabled: enabled && !!collectionId && !!SL?.collection?.get,
679
+ staleTime: 5 * 6e4,
680
+ queryFn: async () => {
681
+ const c = await SL.collection.get(collectionId, admin);
682
+ return {
683
+ hasVariants: !!c?.variants,
684
+ hasBatches: !!c?.batches
685
+ };
686
+ }
687
+ });
688
+ return {
689
+ hasVariants: query.data?.hasVariants ?? false,
690
+ hasBatches: query.data?.hasBatches ?? false,
691
+ isLoading: query.isLoading,
692
+ error: query.error ?? null
693
+ };
694
+ };
695
+ var QK = ["records-admin", "product-browse"];
696
+ var toBrowseItem = (p) => ({
697
+ id: p.id ?? p.productId ?? "",
698
+ name: p.name ?? p.id ?? "Untitled",
699
+ sku: p.sku ?? null,
700
+ sortOrder: p.sortOrder ?? null
701
+ });
702
+ var useProductBrowse = (args) => {
703
+ const { SL, collectionId, search = "", pageSize = 50, enabled = true } = args;
704
+ const queryClient = useQueryClient();
705
+ const queryKey = useMemo(
706
+ () => [...QK, collectionId, search.trim(), pageSize],
707
+ [collectionId, search, pageSize]
708
+ );
709
+ const query = useInfiniteQuery({
710
+ queryKey,
711
+ enabled: enabled && !!collectionId && !!SL?.product?.query,
712
+ initialPageParam: { offset: 0, cursor: null },
713
+ queryFn: async ({ pageParam }) => {
714
+ const body = {
715
+ page: { limit: pageSize, ...pageParam.cursor ? { cursor: pageParam.cursor } : { offset: pageParam.offset } },
716
+ sort: [{ field: "sortOrder", direction: "asc" }, { field: "name", direction: "asc" }]
717
+ };
718
+ if (search.trim()) body.query = { search: search.trim() };
719
+ const res = await SL.product.query(collectionId, body);
720
+ const items2 = (res?.items ?? []).map(toBrowseItem);
721
+ const page = res?.page ?? {};
722
+ return {
723
+ items: items2,
724
+ nextOffset: pageParam.offset + items2.length,
725
+ nextCursor: page.nextCursor ?? null,
726
+ hasMore: page.hasMore ?? items2.length >= pageSize,
727
+ total: page.total
728
+ };
729
+ },
730
+ getNextPageParam: (last) => {
731
+ if (!last.hasMore) return void 0;
732
+ if (last.nextCursor) return { offset: last.nextOffset, cursor: last.nextCursor };
733
+ return { offset: last.nextOffset, cursor: null };
734
+ },
735
+ staleTime: 3e4
736
+ });
737
+ const items = useMemo(
738
+ () => query.data?.pages.flatMap((p) => p.items) ?? [],
739
+ [query.data]
740
+ );
741
+ const refetch = () => queryClient.invalidateQueries({ queryKey: [...QK, collectionId] });
742
+ return {
743
+ items,
744
+ total: query.data?.pages[query.data.pages.length - 1]?.total,
745
+ isLoading: query.isLoading,
746
+ error: query.error ?? null,
747
+ hasNextPage: !!query.hasNextPage,
748
+ isFetchingNextPage: query.isFetchingNextPage,
749
+ fetchNextPage: query.fetchNextPage,
750
+ refetch
751
+ };
752
+ };
753
+ var QK2 = ["records-admin", "product-children"];
754
+ var variantToItem = (v) => ({
755
+ id: v.id ?? v.variantId ?? "",
756
+ name: v.name ?? v.label ?? v.id ?? "Untitled variant",
757
+ subtitle: v.sku ?? v.size ?? void 0
758
+ });
759
+ var batchToItem = (b) => ({
760
+ id: b.id ?? b.batchId ?? "",
761
+ name: b.name ?? b.id ?? "Untitled batch",
762
+ subtitle: b.expiryDate?.iso ?? b.expiry ?? b.lotCode ?? void 0
763
+ });
764
+ var useProductChildren = (args) => {
765
+ const { SL, collectionId, productId, kind, enabled = true } = args;
766
+ const queryClient = useQueryClient();
767
+ const queryKey = useMemo(
768
+ () => [...QK2, collectionId, productId ?? null, kind],
769
+ [collectionId, productId, kind]
770
+ );
771
+ const query = useQuery({
772
+ queryKey,
773
+ enabled: enabled && !!collectionId && !!productId && !!kind,
774
+ staleTime: 3e4,
775
+ queryFn: async () => {
776
+ if (!productId || !kind) return [];
777
+ if (kind === "variant") {
778
+ const res2 = await SL.variant.list(collectionId, productId);
779
+ return (res2 ?? []).map(variantToItem);
780
+ }
781
+ const res = await SL.batch.list(collectionId, productId);
782
+ return (res ?? []).map(batchToItem);
783
+ }
784
+ });
785
+ const refetch = () => queryClient.invalidateQueries({
786
+ queryKey: [...QK2, collectionId, productId ?? null]
787
+ });
788
+ return {
789
+ items: query.data ?? [],
790
+ isLoading: query.isLoading,
791
+ error: query.error ?? null,
792
+ refetch
793
+ };
794
+ };
795
+ var MESSAGE_TYPE = "smartlinks:unsaved-changes";
796
+ function useUnsavedGuard({
797
+ isDirty,
798
+ label,
799
+ confirm,
800
+ disableBeforeUnload = false,
801
+ disableParentMessage = false
802
+ }) {
803
+ useEffect(() => {
804
+ if (disableBeforeUnload || !isDirty || typeof window === "undefined") return;
805
+ const handler = (e) => {
806
+ e.preventDefault();
807
+ e.returnValue = "";
808
+ return "";
809
+ };
810
+ window.addEventListener("beforeunload", handler);
811
+ return () => window.removeEventListener("beforeunload", handler);
812
+ }, [isDirty, disableBeforeUnload]);
813
+ useEffect(() => {
814
+ if (disableParentMessage || typeof window === "undefined") return;
815
+ if (window.parent === window) return;
816
+ try {
817
+ window.parent.postMessage({
818
+ type: MESSAGE_TYPE,
819
+ isDirty,
820
+ label: label ?? null
821
+ }, "*");
822
+ } catch {
823
+ }
824
+ }, [isDirty, label, disableParentMessage]);
825
+ useEffect(() => {
826
+ if (disableParentMessage || typeof window === "undefined") return;
827
+ if (window.parent === window) return;
828
+ return () => {
829
+ try {
830
+ window.parent.postMessage({ type: MESSAGE_TYPE, isDirty: false, label: label ?? null }, "*");
831
+ } catch {
832
+ }
833
+ };
834
+ }, [disableParentMessage, label]);
835
+ const confirmDiscard = useCallback(
836
+ async (message) => {
837
+ if (!isDirty) return true;
838
+ const msg = message ?? "You have unsaved changes. Discard them?";
839
+ if (confirm) {
840
+ try {
841
+ return !!await confirm(msg);
842
+ } catch {
843
+ return false;
844
+ }
845
+ }
846
+ if (typeof window === "undefined") return true;
847
+ return window.confirm(msg);
848
+ },
849
+ [isDirty, confirm]
850
+ );
851
+ return { confirmDiscard };
852
+ }
853
+ var fallbackConfirm = async (i18n) => {
854
+ if (typeof window === "undefined") return "discard";
855
+ const msg = `${i18n?.title ?? "Unsaved changes"}
856
+
857
+ ${i18n?.body ?? "OK to save & continue, Cancel to discard."}`;
858
+ return window.confirm(msg) ? "save" : "discard";
859
+ };
860
+ var useDirtyNavigation = ({
861
+ strategy,
862
+ isDirty,
863
+ save,
864
+ reset,
865
+ confirm,
866
+ i18n
867
+ }) => {
868
+ const runWithGuard = useCallback(
869
+ async (action) => {
870
+ if (!isDirty || strategy === "keep") {
871
+ action();
872
+ return true;
873
+ }
874
+ if (strategy === "autosave") {
875
+ try {
876
+ await save();
877
+ } catch {
878
+ const fallback = await (confirm ?? fallbackConfirm)(i18n);
879
+ if (fallback === "cancel") return false;
880
+ if (fallback === "discard") reset();
881
+ }
882
+ action();
883
+ return true;
884
+ }
885
+ const choice = await (confirm ?? fallbackConfirm)(i18n);
886
+ if (choice === "cancel") return false;
887
+ if (choice === "save") {
888
+ try {
889
+ await save();
890
+ } catch {
891
+ return false;
892
+ }
893
+ } else {
894
+ reset();
895
+ }
896
+ action();
897
+ return true;
898
+ },
899
+ [strategy, isDirty, save, reset, confirm, i18n]
900
+ );
901
+ return { runWithGuard };
902
+ };
903
+ var ICONS = {
904
+ product: Box,
905
+ facet: Tag,
906
+ variant: Layers,
907
+ batch: Package
908
+ };
909
+ var LABELS = {
910
+ product: "Products",
911
+ facet: "Shared",
912
+ // Variant/Batch are drill-downs under a product, but kept here for hosts
913
+ // that still pass them as top-level scopes. The shell omits them by default.
914
+ variant: "Variants",
915
+ batch: "Batches"
916
+ };
917
+ var ScopeTabs = ({ scopes, active, onChange, loading = false }) => /* @__PURE__ */ jsx("div", { role: "tablist", className: "flex gap-1 border-b", style: { borderColor: "hsl(var(--ra-border))" }, children: scopes.map((s) => {
918
+ const Icon = ICONS[s];
919
+ const isActive = active === s;
920
+ return /* @__PURE__ */ jsxs(
921
+ "button",
922
+ {
923
+ type: "button",
924
+ role: "tab",
925
+ "aria-selected": isActive,
926
+ onClick: () => onChange(s),
927
+ disabled: loading,
928
+ className: cn(
929
+ "flex items-center gap-1.5 px-3 py-2 text-xs border-b-2 -mb-px transition-colors",
930
+ isActive ? "font-medium" : "opacity-60 hover:opacity-100",
931
+ loading && "cursor-wait opacity-40"
932
+ ),
933
+ style: {
934
+ borderColor: isActive ? "hsl(var(--ra-accent))" : "transparent",
935
+ color: "hsl(var(--ra-text))"
936
+ },
937
+ children: [
938
+ /* @__PURE__ */ jsx(Icon, { className: cn("w-3.5 h-3.5", loading && "animate-pulse") }),
939
+ LABELS[s]
940
+ ]
941
+ },
942
+ s
943
+ );
944
+ }) });
945
+ var StatusFilterPills = ({ value, onChange, counts, i18n }) => {
946
+ const opts = [
947
+ { key: "all", label: i18n.filterAll, count: counts.all },
948
+ { key: "configured", label: i18n.filterConfigured, count: counts.configured },
949
+ { key: "partial", label: i18n.filterPartial, count: counts.partial },
950
+ { key: "empty", label: i18n.filterEmpty, count: counts.empty }
951
+ ];
952
+ return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: opts.map((o) => {
953
+ const active = value === o.key;
954
+ return /* @__PURE__ */ jsxs(
955
+ "button",
956
+ {
957
+ type: "button",
958
+ onClick: () => onChange(o.key),
959
+ className: cn(
960
+ "text-xs px-2.5 py-1 rounded-full border transition-colors",
961
+ active ? "font-medium" : "opacity-80 hover:opacity-100"
962
+ ),
963
+ style: {
964
+ background: active ? "hsl(var(--ra-accent) / 0.12)" : "hsl(var(--ra-muted))",
965
+ borderColor: active ? "hsl(var(--ra-accent) / 0.5)" : "hsl(var(--ra-border))",
966
+ color: "hsl(var(--ra-text))"
967
+ },
968
+ children: [
969
+ o.label,
970
+ /* @__PURE__ */ jsx("span", { className: "ml-1.5 opacity-60", children: o.count })
971
+ ]
972
+ },
973
+ o.key
974
+ );
975
+ }) });
976
+ };
977
+ var StatusDot = ({ source, status, className }) => {
978
+ let cls = "ra-status-missing";
979
+ if (source === "self" || status === "configured") cls = "ra-status-own";
980
+ else if (source === "inherited" || status === "partial") cls = "ra-status-shared";
981
+ return /* @__PURE__ */ jsx("span", { className: cn("ra-status-dot", cls, className), "aria-hidden": "true" });
982
+ };
983
+ var DefaultRecordRow = ({ record, ctx, compact = false }) => {
984
+ const { selected, onSelect, isDirty } = ctx;
985
+ return /* @__PURE__ */ jsxs(
986
+ "button",
987
+ {
988
+ type: "button",
989
+ onClick: onSelect,
990
+ className: cn(
991
+ "w-full text-left flex items-center gap-2.5 px-3 transition-colors hover:bg-[hsl(var(--ra-muted))]",
992
+ compact ? "py-1.5" : "py-2.5",
993
+ selected && "ra-row-active"
994
+ ),
995
+ children: [
996
+ !compact && /* @__PURE__ */ jsx(StatusDot, { status: record.status }),
997
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
998
+ /* @__PURE__ */ jsx("div", { className: "text-sm truncate", style: { color: "hsl(var(--ra-text))" }, children: record.label }),
999
+ !compact && record.subtitle && /* @__PURE__ */ jsx("div", { className: "text-xs truncate", style: { color: "hsl(var(--ra-muted-text))" }, children: record.subtitle })
1000
+ ] }),
1001
+ record.badges?.slice(0, 2).map((b, i) => /* @__PURE__ */ jsx(
1002
+ "span",
1003
+ {
1004
+ className: "text-[10px] px-1.5 py-0.5 rounded-sm shrink-0",
1005
+ style: {
1006
+ background: "hsl(var(--ra-muted))",
1007
+ color: "hsl(var(--ra-muted-text))"
1008
+ },
1009
+ children: b.label
1010
+ },
1011
+ `${b.label}-${i}`
1012
+ )),
1013
+ isDirty && /* @__PURE__ */ jsx(
1014
+ "span",
1015
+ {
1016
+ title: "Unsaved changes",
1017
+ "aria-label": "Unsaved changes",
1018
+ className: "ra-status-dot ra-status-shared shrink-0"
1019
+ }
1020
+ )
1021
+ ]
1022
+ }
1023
+ );
1024
+ };
1025
+ var initials = (s) => s.split(/\s+/).filter(Boolean).slice(0, 2).map((p) => p[0]?.toUpperCase() ?? "").join("") || "?";
1026
+ var DefaultRecordCard = ({ record, ctx, variant = "grid" }) => {
1027
+ const { selected, onSelect, isDirty } = ctx;
1028
+ const aspect = variant === "gallery" ? "aspect-video" : "aspect-square";
1029
+ return /* @__PURE__ */ jsxs(
1030
+ "button",
1031
+ {
1032
+ type: "button",
1033
+ onClick: onSelect,
1034
+ className: cn(
1035
+ "group flex flex-col text-left rounded-md overflow-hidden border transition-colors",
1036
+ "hover:bg-[hsl(var(--ra-muted))]",
1037
+ selected && "ring-2"
1038
+ ),
1039
+ style: {
1040
+ borderColor: "hsl(var(--ra-border))",
1041
+ // selected ring
1042
+ ...selected ? { boxShadow: "0 0 0 2px hsl(var(--ra-accent, var(--ra-text)))" } : {}
1043
+ },
1044
+ children: [
1045
+ /* @__PURE__ */ jsxs(
1046
+ "div",
1047
+ {
1048
+ className: cn(aspect, "relative w-full flex items-center justify-center overflow-hidden"),
1049
+ style: { background: "hsl(var(--ra-muted))" },
1050
+ children: [
1051
+ record.thumbnail ? /* @__PURE__ */ jsx(
1052
+ "img",
1053
+ {
1054
+ src: record.thumbnail,
1055
+ alt: "",
1056
+ loading: "lazy",
1057
+ className: "w-full h-full object-cover"
1058
+ }
1059
+ ) : /* @__PURE__ */ jsx(
1060
+ "span",
1061
+ {
1062
+ className: "text-2xl font-medium opacity-60",
1063
+ style: { color: "hsl(var(--ra-text))" },
1064
+ children: initials(record.label)
1065
+ }
1066
+ ),
1067
+ /* @__PURE__ */ jsx("div", { className: "absolute top-1.5 left-1.5", children: /* @__PURE__ */ jsx(StatusDot, { status: record.status }) }),
1068
+ isDirty && /* @__PURE__ */ jsx(
1069
+ "span",
1070
+ {
1071
+ title: "Unsaved changes",
1072
+ "aria-label": "Unsaved changes",
1073
+ className: "ra-status-dot ra-status-shared absolute top-1.5 right-1.5"
1074
+ }
1075
+ )
1076
+ ]
1077
+ }
1078
+ ),
1079
+ /* @__PURE__ */ jsxs("div", { className: "p-2 min-w-0", children: [
1080
+ /* @__PURE__ */ jsx("div", { className: "text-sm truncate", style: { color: "hsl(var(--ra-text))" }, children: record.label }),
1081
+ variant === "gallery" && record.subtitle && /* @__PURE__ */ jsx("div", { className: "text-xs truncate", style: { color: "hsl(var(--ra-muted-text))" }, children: record.subtitle }),
1082
+ record.badges && record.badges.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex gap-1 mt-1.5 flex-wrap", children: record.badges.slice(0, 3).map((b, i) => /* @__PURE__ */ jsx(
1083
+ "span",
1084
+ {
1085
+ className: "text-[10px] px-1.5 py-0.5 rounded-sm",
1086
+ style: {
1087
+ background: "hsl(var(--ra-surface))",
1088
+ border: "1px solid hsl(var(--ra-border))",
1089
+ color: "hsl(var(--ra-muted-text))"
1090
+ },
1091
+ children: b.label
1092
+ },
1093
+ `${b.label}-${i}`
1094
+ )) })
1095
+ ] })
1096
+ ]
1097
+ }
1098
+ );
1099
+ };
1100
+ var RecordList = ({
1101
+ items,
1102
+ selectedRef,
1103
+ onSelect,
1104
+ dirtyRef,
1105
+ presentation = "list",
1106
+ renderListRow,
1107
+ renderCard
1108
+ }) => {
1109
+ const buildCtx = (item) => ({
1110
+ selected: item.ref === selectedRef,
1111
+ onSelect: () => onSelect(item),
1112
+ isDirty: !!dirtyRef && item.ref === dirtyRef
1113
+ });
1114
+ if (presentation === "grid" || presentation === "gallery") {
1115
+ const minColPx = presentation === "gallery" ? 200 : 120;
1116
+ return /* @__PURE__ */ jsx(
1117
+ "div",
1118
+ {
1119
+ className: "grid gap-2 p-2",
1120
+ style: { gridTemplateColumns: `repeat(auto-fill, minmax(${minColPx}px, 1fr))` },
1121
+ children: items.map((item) => {
1122
+ const ctx = buildCtx(item);
1123
+ return /* @__PURE__ */ jsx("div", { children: renderCard ? renderCard(item, ctx) : /* @__PURE__ */ jsx(DefaultRecordCard, { record: item, ctx, variant: presentation }) }, item.ref);
1124
+ })
1125
+ }
1126
+ );
1127
+ }
1128
+ const compact = presentation === "compact";
1129
+ return /* @__PURE__ */ jsx("ul", { className: "divide-y", style: { borderColor: "hsl(var(--ra-border))" }, children: items.map((item) => {
1130
+ const ctx = buildCtx(item);
1131
+ return /* @__PURE__ */ jsx("li", { children: renderListRow ? renderListRow(item, ctx) : /* @__PURE__ */ jsx(DefaultRecordRow, { record: item, ctx, compact }) }, item.ref);
1132
+ }) });
1133
+ };
1134
+ var ProductList = RecordList;
1135
+ var FacetList = RecordList;
1136
+ var VariantList = RecordList;
1137
+ var BatchList = RecordList;
1138
+ var EmptyState = ({ title, body, action }) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center text-center p-8 gap-2", children: [
1139
+ /* @__PURE__ */ jsx("div", { className: "text-sm font-medium", style: { color: "hsl(var(--ra-text))" }, children: title }),
1140
+ body && /* @__PURE__ */ jsx("div", { className: "text-xs", style: { color: "hsl(var(--ra-muted-text))" }, children: body }),
1141
+ action
1142
+ ] });
1143
+ var LoadingState = () => /* @__PURE__ */ jsx("div", { className: "p-4 space-y-2", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsx("div", { className: "h-10 rounded animate-pulse", style: { background: "hsl(var(--ra-muted))" } }, i)) });
1144
+ var ErrorState = ({ error }) => /* @__PURE__ */ jsx("div", { className: "p-4 text-xs", style: { color: "hsl(0 70% 45%)" }, children: error.message || "Something went wrong." });
1145
+ var ICONS2 = {
1146
+ list: List,
1147
+ grid: LayoutGrid,
1148
+ gallery: Image,
1149
+ compact: Rows3
1150
+ };
1151
+ var labelFor = (p, i18n) => {
1152
+ switch (p) {
1153
+ case "list":
1154
+ return i18n.presentationList;
1155
+ case "grid":
1156
+ return i18n.presentationGrid;
1157
+ case "gallery":
1158
+ return i18n.presentationGallery;
1159
+ case "compact":
1160
+ return i18n.presentationCompact;
1161
+ }
1162
+ };
1163
+ var PresentationSwitcher = ({ options, value, onChange, i18n }) => {
1164
+ if (options.length < 2) return null;
1165
+ return /* @__PURE__ */ jsx(
1166
+ "div",
1167
+ {
1168
+ className: "inline-flex rounded-md border overflow-hidden",
1169
+ style: { borderColor: "hsl(var(--ra-border))" },
1170
+ role: "tablist",
1171
+ "aria-label": "View",
1172
+ children: options.map((opt) => {
1173
+ const Icon = ICONS2[opt];
1174
+ const active = opt === value;
1175
+ return /* @__PURE__ */ jsx(
1176
+ "button",
1177
+ {
1178
+ type: "button",
1179
+ role: "tab",
1180
+ "aria-selected": active,
1181
+ title: labelFor(opt, i18n),
1182
+ "aria-label": labelFor(opt, i18n),
1183
+ onClick: () => onChange(opt),
1184
+ className: cn(
1185
+ "flex items-center justify-center w-7 h-7 transition-colors",
1186
+ active ? "bg-[hsl(var(--ra-text))] text-[hsl(var(--ra-surface))]" : "hover:bg-[hsl(var(--ra-muted))]"
1187
+ ),
1188
+ children: /* @__PURE__ */ jsx(Icon, { className: "w-3.5 h-3.5" })
1189
+ },
1190
+ opt
1191
+ );
1192
+ })
1193
+ }
1194
+ );
1195
+ };
1196
+ var KEY_PREFIX = "smartlinks-ui:records-admin:presentation";
1197
+ var safeRead = (key) => {
1198
+ try {
1199
+ return typeof window === "undefined" ? null : window.localStorage.getItem(key);
1200
+ } catch {
1201
+ return null;
1202
+ }
1203
+ };
1204
+ var safeWrite = (key, val) => {
1205
+ try {
1206
+ if (typeof window !== "undefined") window.localStorage.setItem(key, val);
1207
+ } catch {
1208
+ }
1209
+ };
1210
+ function usePresentationPref(args) {
1211
+ const { appId, recordType, options, defaultValue } = args;
1212
+ const key = `${KEY_PREFIX}:${appId}:${recordType}`;
1213
+ const initial = () => {
1214
+ const stored = safeRead(key);
1215
+ if (stored && options.includes(stored)) return stored;
1216
+ return options.includes(defaultValue) ? defaultValue : options[0];
1217
+ };
1218
+ const [value, setValue] = useState(initial);
1219
+ useEffect(() => {
1220
+ if (!options.includes(value)) setValue(options[0]);
1221
+ }, [options, value]);
1222
+ const set = useCallback((next) => {
1223
+ setValue(next);
1224
+ safeWrite(key, next);
1225
+ }, [key]);
1226
+ return [value, set];
1227
+ }
1228
+ var segmentsFor = (s) => {
1229
+ const out = [];
1230
+ if (s.facetId) out.push({ label: s.facetValue ? `${s.facetId}: ${s.facetValue}` : `Facet: ${s.facetId}`, key: "facet" });
1231
+ if (s.productId) out.push({ label: `Product: ${s.productId}`, key: "product" });
1232
+ if (s.variantId) out.push({ label: `Variant: ${s.variantId}`, key: "variant" });
1233
+ if (s.batchId) out.push({ label: `Batch: ${s.batchId}`, key: "batch" });
1234
+ return out;
1235
+ };
1236
+ var ScopeBreadcrumb = ({ scope }) => {
1237
+ const segs = segmentsFor(scope);
1238
+ if (segs.length === 0) return null;
1239
+ return /* @__PURE__ */ jsx("nav", { "aria-label": "Scope", className: "flex items-center gap-1 text-xs flex-wrap", style: { color: "hsl(var(--ra-muted-text))" }, children: segs.map((s, i) => /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1", children: [
1240
+ i > 0 && /* @__PURE__ */ jsx(ChevronRight, { className: "w-3 h-3 opacity-50" }),
1241
+ /* @__PURE__ */ jsx("span", { style: { color: "hsl(var(--ra-text))" }, children: s.label })
1242
+ ] }, s.key)) });
1243
+ };
1244
+ var BulkActionsMenu = ({
1245
+ i18n,
1246
+ onApplyToMany,
1247
+ onCopyFrom,
1248
+ onClearMany,
1249
+ onImportCsv,
1250
+ onExportCsv
1251
+ }) => {
1252
+ const [open, setOpen] = useState(false);
1253
+ const ref = useRef(null);
1254
+ useEffect(() => {
1255
+ if (!open) return;
1256
+ const onDocClick = (e) => {
1257
+ if (ref.current && !ref.current.contains(e.target)) setOpen(false);
1258
+ };
1259
+ document.addEventListener("mousedown", onDocClick);
1260
+ return () => document.removeEventListener("mousedown", onDocClick);
1261
+ }, [open]);
1262
+ const items = [
1263
+ onApplyToMany && { label: i18n.applyToMany, onClick: onApplyToMany },
1264
+ onCopyFrom && { label: i18n.copyFrom, onClick: onCopyFrom },
1265
+ onClearMany && { label: i18n.clear, onClick: onClearMany },
1266
+ onImportCsv && { label: i18n.importCsv, onClick: onImportCsv },
1267
+ onExportCsv && { label: i18n.exportCsv, onClick: onExportCsv }
1268
+ ].filter(Boolean);
1269
+ if (items.length === 0) return null;
1270
+ return /* @__PURE__ */ jsxs("div", { className: "relative", ref, children: [
1271
+ /* @__PURE__ */ jsxs(
1272
+ "button",
1273
+ {
1274
+ type: "button",
1275
+ onClick: () => setOpen((v) => !v),
1276
+ className: "flex items-center gap-1 px-3 py-1.5 text-xs rounded-md border hover:bg-[hsl(var(--ra-muted))]",
1277
+ style: { borderColor: "hsl(var(--ra-border))", color: "hsl(var(--ra-text))" },
1278
+ children: [
1279
+ i18n.bulkActions,
1280
+ /* @__PURE__ */ jsx(ChevronDown, { className: "w-3 h-3" })
1281
+ ]
1282
+ }
1283
+ ),
1284
+ open && /* @__PURE__ */ jsx(
1285
+ "div",
1286
+ {
1287
+ className: "absolute right-0 mt-1 min-w-[180px] rounded-md border shadow-md py-1 z-10",
1288
+ style: { background: "hsl(var(--ra-surface))", borderColor: "hsl(var(--ra-border))" },
1289
+ children: items.map((it) => /* @__PURE__ */ jsx(
1290
+ "button",
1291
+ {
1292
+ type: "button",
1293
+ onClick: () => {
1294
+ setOpen(false);
1295
+ it.onClick();
1296
+ },
1297
+ className: "w-full text-left px-3 py-1.5 text-xs hover:bg-[hsl(var(--ra-muted))]",
1298
+ style: { color: "hsl(var(--ra-text))" },
1299
+ children: it.label
1300
+ },
1301
+ it.label
1302
+ ))
1303
+ }
1304
+ )
1305
+ ] });
1306
+ };
1307
+ var DeleteButton = ({
1308
+ onConfirm,
1309
+ onBeforeDelete,
1310
+ label = "Delete record",
1311
+ confirmLabel = "Confirm delete",
1312
+ revertMs = 3e3,
1313
+ disabled
1314
+ }) => {
1315
+ const [armed, setArmed] = useState(false);
1316
+ const [busy, setBusy] = useState(false);
1317
+ const timerRef = useRef(null);
1318
+ const clearTimer = () => {
1319
+ if (timerRef.current) {
1320
+ clearTimeout(timerRef.current);
1321
+ timerRef.current = null;
1322
+ }
1323
+ };
1324
+ useEffect(() => () => clearTimer(), []);
1325
+ const arm = async () => {
1326
+ if (onBeforeDelete) {
1327
+ const ok = await onBeforeDelete();
1328
+ if (!ok) return;
1329
+ }
1330
+ setArmed(true);
1331
+ clearTimer();
1332
+ timerRef.current = setTimeout(() => setArmed(false), revertMs);
1333
+ };
1334
+ const fire = async () => {
1335
+ clearTimer();
1336
+ setBusy(true);
1337
+ try {
1338
+ await onConfirm();
1339
+ } finally {
1340
+ setBusy(false);
1341
+ setArmed(false);
1342
+ }
1343
+ };
1344
+ if (armed) {
1345
+ return /* @__PURE__ */ jsxs(
1346
+ "button",
1347
+ {
1348
+ type: "button",
1349
+ onClick: fire,
1350
+ disabled: busy,
1351
+ className: "text-xs px-3 py-1.5 rounded-md font-medium transition-colors flex items-center gap-1.5 disabled:opacity-60",
1352
+ style: {
1353
+ background: "hsl(var(--ra-danger, 0 70% 45%))",
1354
+ color: "hsl(0 0% 100%)"
1355
+ },
1356
+ children: [
1357
+ /* @__PURE__ */ jsx(Trash2, { className: "w-3.5 h-3.5" }),
1358
+ busy ? "\u2026" : confirmLabel
1359
+ ]
1360
+ }
1361
+ );
1362
+ }
1363
+ return /* @__PURE__ */ jsxs(
1364
+ "button",
1365
+ {
1366
+ type: "button",
1367
+ onClick: arm,
1368
+ disabled,
1369
+ className: "text-xs px-3 py-1.5 rounded-md border transition-colors hover:bg-[hsl(var(--ra-muted))] disabled:opacity-40 flex items-center gap-1.5",
1370
+ style: {
1371
+ borderColor: "hsl(var(--ra-border))",
1372
+ color: "hsl(var(--ra-danger, var(--ra-text)))"
1373
+ },
1374
+ children: [
1375
+ /* @__PURE__ */ jsx(Trash2, { className: "w-3.5 h-3.5" }),
1376
+ label
1377
+ ]
1378
+ }
1379
+ );
1380
+ };
1381
+ function RecordEditor({
1382
+ ctx,
1383
+ i18n,
1384
+ children,
1385
+ preview,
1386
+ bulkActions,
1387
+ footerExtra,
1388
+ onBeforeDelete
1389
+ }) {
1390
+ const sourceLabel = ctx.source === "self" ? "Own" : ctx.source === "inherited" ? "Inherited" : "New";
1391
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", children: [
1392
+ /* @__PURE__ */ jsxs(
1393
+ "header",
1394
+ {
1395
+ className: "sticky top-0 z-10 px-5 py-3 border-b flex items-center justify-between gap-3",
1396
+ style: { borderColor: "hsl(var(--ra-border))", background: "hsl(var(--ra-surface))" },
1397
+ children: [
1398
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
1399
+ /* @__PURE__ */ jsx(ScopeBreadcrumb, { scope: ctx.scope }),
1400
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 mt-1", children: [
1401
+ /* @__PURE__ */ jsx(StatusDot, { source: ctx.source }),
1402
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] uppercase tracking-wide", style: { color: "hsl(var(--ra-muted-text))" }, children: sourceLabel }),
1403
+ ctx.isDirty && /* @__PURE__ */ jsxs(
1404
+ "span",
1405
+ {
1406
+ className: "ml-2 flex items-center gap-1 text-[10px] uppercase tracking-wide",
1407
+ style: { color: "hsl(var(--ra-accent))" },
1408
+ children: [
1409
+ /* @__PURE__ */ jsx("span", { className: "ra-status-dot ra-status-shared", "aria-hidden": "true" }),
1410
+ i18n.unsavedBadge ?? "Unsaved"
1411
+ ]
1412
+ }
1413
+ )
1414
+ ] })
1415
+ ] }),
1416
+ bulkActions && /* @__PURE__ */ jsx(BulkActionsMenu, { ...bulkActions, i18n })
1417
+ ]
1418
+ }
1419
+ ),
1420
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto px-5 py-4", children: [
1421
+ children,
1422
+ preview
1423
+ ] }),
1424
+ /* @__PURE__ */ jsxs(
1425
+ "footer",
1426
+ {
1427
+ className: "sticky bottom-0 px-5 py-3 border-t flex items-center justify-between gap-2",
1428
+ style: { borderColor: "hsl(var(--ra-border))", background: "hsl(var(--ra-surface))" },
1429
+ children: [
1430
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1431
+ /* @__PURE__ */ jsx(
1432
+ "button",
1433
+ {
1434
+ type: "button",
1435
+ onClick: ctx.reset,
1436
+ disabled: !ctx.isDirty || !!ctx.isSaving,
1437
+ className: "text-xs px-3 py-1.5 rounded-md border transition-opacity disabled:opacity-40 hover:bg-[hsl(var(--ra-muted))]",
1438
+ style: { borderColor: "hsl(var(--ra-border))", color: "hsl(var(--ra-text))" },
1439
+ children: i18n.discard
1440
+ }
1441
+ ),
1442
+ ctx.canRemove && /* @__PURE__ */ jsx(
1443
+ DeleteButton,
1444
+ {
1445
+ onConfirm: () => ctx.remove(),
1446
+ onBeforeDelete,
1447
+ label: i18n.delete,
1448
+ confirmLabel: i18n.confirmDelete
1449
+ }
1450
+ ),
1451
+ footerExtra,
1452
+ ctx.saveError != null && !ctx.isSaving && /* @__PURE__ */ jsx(
1453
+ "span",
1454
+ {
1455
+ className: "text-[10px] uppercase tracking-wide",
1456
+ style: { color: "hsl(var(--ra-danger, 0 70% 45%))" },
1457
+ role: "alert",
1458
+ children: i18n.saveError ?? "Save failed"
1459
+ }
1460
+ )
1461
+ ] }),
1462
+ /* @__PURE__ */ jsx(
1463
+ "button",
1464
+ {
1465
+ type: "button",
1466
+ onClick: () => {
1467
+ void ctx.save().catch(() => {
1468
+ });
1469
+ },
1470
+ disabled: !ctx.isDirty || !!ctx.isSaving,
1471
+ className: "text-xs px-3 py-1.5 rounded-md font-medium transition-opacity disabled:opacity-40",
1472
+ style: { background: "hsl(var(--ra-accent))", color: "hsl(var(--ra-surface))" },
1473
+ children: ctx.isSaving ? i18n.saving ?? "Saving\u2026" : i18n.save
1474
+ }
1475
+ )
1476
+ ]
1477
+ }
1478
+ )
1479
+ ] });
1480
+ }
1481
+ var TAB_META = {
1482
+ product: { icon: Box, label: "Product" },
1483
+ variant: { icon: Layers, label: "Variants" },
1484
+ batch: { icon: Package, label: "Batches" }
1485
+ };
1486
+ var ProductDrillDown = ({
1487
+ productLabel,
1488
+ showVariants,
1489
+ showBatches,
1490
+ active,
1491
+ onChange,
1492
+ selectedChildId,
1493
+ onSelectChild,
1494
+ variants,
1495
+ batches,
1496
+ variantsLoading,
1497
+ batchesLoading,
1498
+ children
1499
+ }) => {
1500
+ const tabs = ["product"];
1501
+ if (showVariants) tabs.push("variant");
1502
+ if (showBatches) tabs.push("batch");
1503
+ const childList = active === "variant" ? variants : active === "batch" ? batches : [];
1504
+ const childLoading = active === "variant" ? variantsLoading : active === "batch" ? batchesLoading : false;
1505
+ const childEmptyLabel = active === "variant" ? "No variants" : "No batches";
1506
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", children: [
1507
+ /* @__PURE__ */ jsx(
1508
+ "div",
1509
+ {
1510
+ className: "flex items-center gap-1 px-4 pt-3 border-b",
1511
+ style: { borderColor: "hsl(var(--ra-border))" },
1512
+ children: tabs.map((t) => {
1513
+ const meta = TAB_META[t];
1514
+ const Icon = meta.icon;
1515
+ const isActive = active === t;
1516
+ const label = t === "product" ? productLabel : meta.label;
1517
+ return /* @__PURE__ */ jsxs(
1518
+ "button",
1519
+ {
1520
+ type: "button",
1521
+ role: "tab",
1522
+ "aria-selected": isActive,
1523
+ onClick: () => onChange(t),
1524
+ className: cn(
1525
+ "flex items-center gap-1.5 px-3 py-2 text-xs border-b-2 -mb-px transition-colors",
1526
+ isActive ? "font-medium" : "opacity-60 hover:opacity-100"
1527
+ ),
1528
+ style: {
1529
+ borderColor: isActive ? "hsl(var(--ra-accent))" : "transparent",
1530
+ color: "hsl(var(--ra-text))"
1531
+ },
1532
+ children: [
1533
+ /* @__PURE__ */ jsx(Icon, { className: "w-3.5 h-3.5" }),
1534
+ /* @__PURE__ */ jsx("span", { className: "truncate max-w-[160px]", children: label })
1535
+ ]
1536
+ },
1537
+ t
1538
+ );
1539
+ })
1540
+ }
1541
+ ),
1542
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-h-0 grid", style: {
1543
+ gridTemplateColumns: active === "product" ? "1fr" : "minmax(200px, 240px) 1fr"
1544
+ }, children: [
1545
+ active !== "product" && /* @__PURE__ */ jsxs(
1546
+ "aside",
1547
+ {
1548
+ className: "border-r overflow-y-auto",
1549
+ style: { borderColor: "hsl(var(--ra-border))", background: "hsl(var(--ra-surface))" },
1550
+ children: [
1551
+ childLoading && /* @__PURE__ */ jsx("div", { className: "p-3 space-y-2", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsx("div", { className: "h-9 rounded animate-pulse", style: { background: "hsl(var(--ra-muted))" } }, i)) }),
1552
+ !childLoading && childList.length === 0 && /* @__PURE__ */ jsx("div", { className: "p-4 text-xs", style: { color: "hsl(var(--ra-muted-text))" }, children: childEmptyLabel }),
1553
+ !childLoading && childList.length > 0 && /* @__PURE__ */ jsx("ul", { className: "divide-y", style: { borderColor: "hsl(var(--ra-border))" }, children: childList.map((c) => {
1554
+ const isActive = c.id === selectedChildId;
1555
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
1556
+ "button",
1557
+ {
1558
+ type: "button",
1559
+ onClick: () => onSelectChild(c.id),
1560
+ className: cn(
1561
+ "w-full text-left px-3 py-2 transition-colors hover:bg-[hsl(var(--ra-muted))]",
1562
+ isActive && "ra-row-active"
1563
+ ),
1564
+ children: [
1565
+ /* @__PURE__ */ jsx("div", { className: "text-sm truncate", style: { color: "hsl(var(--ra-text))" }, children: c.name }),
1566
+ c.subtitle && /* @__PURE__ */ jsx("div", { className: "text-xs truncate", style: { color: "hsl(var(--ra-muted-text))" }, children: c.subtitle })
1567
+ ]
1568
+ }
1569
+ ) }, c.id);
1570
+ }) })
1571
+ ]
1572
+ }
1573
+ ),
1574
+ /* @__PURE__ */ jsx("div", { className: "overflow-hidden", children })
1575
+ ] })
1576
+ ] });
1577
+ };
1578
+ var PreviewHeader = ({
1579
+ label,
1580
+ scopePicker,
1581
+ onClose
1582
+ }) => /* @__PURE__ */ jsxs(
1583
+ "header",
1584
+ {
1585
+ className: "px-4 py-2.5 border-b flex items-center justify-between gap-3 flex-wrap",
1586
+ style: {
1587
+ borderColor: "hsl(var(--ra-border))",
1588
+ background: "hsl(var(--ra-surface))"
1589
+ },
1590
+ children: [
1591
+ /* @__PURE__ */ jsxs(
1592
+ "div",
1593
+ {
1594
+ className: "flex items-center gap-1.5 text-[10px] uppercase tracking-wide",
1595
+ style: { color: "hsl(var(--ra-muted-text))" },
1596
+ children: [
1597
+ /* @__PURE__ */ jsx(Eye, { className: "w-3 h-3" }),
1598
+ label
1599
+ ]
1600
+ }
1601
+ ),
1602
+ scopePicker,
1603
+ onClose && /* @__PURE__ */ jsx(
1604
+ "button",
1605
+ {
1606
+ type: "button",
1607
+ onClick: onClose,
1608
+ "aria-label": "Close preview",
1609
+ className: "p-1 rounded hover:bg-[hsl(var(--ra-muted))]",
1610
+ style: { color: "hsl(var(--ra-muted-text))" },
1611
+ children: /* @__PURE__ */ jsx(X, { className: "w-3.5 h-3.5" })
1612
+ }
1613
+ )
1614
+ ]
1615
+ }
1616
+ );
1617
+ var InlinePreview = ({ children, scopePicker, label = "Preview" }) => /* @__PURE__ */ jsxs("div", { className: "mt-4 pt-4 border-t", style: { borderColor: "hsl(var(--ra-border))" }, children: [
1618
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 mb-2 flex-wrap", children: [
1619
+ /* @__PURE__ */ jsx(
1620
+ "div",
1621
+ {
1622
+ className: "text-[10px] uppercase tracking-wide",
1623
+ style: { color: "hsl(var(--ra-muted-text))" },
1624
+ children: label
1625
+ }
1626
+ ),
1627
+ scopePicker
1628
+ ] }),
1629
+ children
1630
+ ] });
1631
+ var SidePreview = ({ children, scopePicker, label = "Preview" }) => /* @__PURE__ */ jsxs(
1632
+ "div",
1633
+ {
1634
+ className: "flex flex-col h-full border-l overflow-hidden",
1635
+ style: { borderColor: "hsl(var(--ra-border))" },
1636
+ children: [
1637
+ /* @__PURE__ */ jsx(PreviewHeader, { label, scopePicker }),
1638
+ /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto p-4", children })
1639
+ ]
1640
+ }
1641
+ );
1642
+ var TabbedPreview = ({
1643
+ editor,
1644
+ preview,
1645
+ scopePicker,
1646
+ defaultTab = "editor",
1647
+ i18n
1648
+ }) => {
1649
+ const [tab, setTab] = useState(defaultTab);
1650
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", children: [
1651
+ /* @__PURE__ */ jsxs(
1652
+ "div",
1653
+ {
1654
+ className: "flex items-center gap-1 px-4 pt-3 border-b",
1655
+ style: { borderColor: "hsl(var(--ra-border))" },
1656
+ children: [
1657
+ ["editor", "preview"].map((t) => {
1658
+ const isActive = tab === t;
1659
+ const lbl = t === "editor" ? i18n?.editor ?? "Editor" : i18n?.preview ?? "Preview";
1660
+ return /* @__PURE__ */ jsx(
1661
+ "button",
1662
+ {
1663
+ type: "button",
1664
+ role: "tab",
1665
+ "aria-selected": isActive,
1666
+ onClick: () => setTab(t),
1667
+ className: cn(
1668
+ "px-3 py-2 text-xs border-b-2 -mb-px transition-colors",
1669
+ isActive ? "font-medium" : "opacity-60 hover:opacity-100"
1670
+ ),
1671
+ style: {
1672
+ borderColor: isActive ? "hsl(var(--ra-accent))" : "transparent",
1673
+ color: "hsl(var(--ra-text))"
1674
+ },
1675
+ children: lbl
1676
+ },
1677
+ t
1678
+ );
1679
+ }),
1680
+ tab === "preview" && scopePicker && /* @__PURE__ */ jsx("div", { className: "ml-auto pb-1", children: scopePicker })
1681
+ ]
1682
+ }
1683
+ ),
1684
+ /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0", children: tab === "editor" ? editor : /* @__PURE__ */ jsx("div", { className: "h-full overflow-y-auto p-4", children: preview }) })
1685
+ ] });
1686
+ };
1687
+ var DrawerPreview = ({
1688
+ open,
1689
+ onClose,
1690
+ children,
1691
+ scopePicker,
1692
+ label = "Preview",
1693
+ width = 420
1694
+ }) => {
1695
+ if (!open) return null;
1696
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1697
+ /* @__PURE__ */ jsx(
1698
+ "div",
1699
+ {
1700
+ className: "absolute inset-0 z-10 transition-opacity",
1701
+ style: { background: "hsl(0 0% 0% / 0.25)" },
1702
+ onClick: onClose,
1703
+ "aria-hidden": "true"
1704
+ }
1705
+ ),
1706
+ /* @__PURE__ */ jsxs(
1707
+ "aside",
1708
+ {
1709
+ className: "absolute top-0 right-0 bottom-0 z-20 flex flex-col shadow-xl",
1710
+ style: {
1711
+ width,
1712
+ background: "hsl(var(--ra-bg, var(--ra-surface)))",
1713
+ borderLeft: "1px solid hsl(var(--ra-border))"
1714
+ },
1715
+ role: "dialog",
1716
+ "aria-label": label,
1717
+ children: [
1718
+ /* @__PURE__ */ jsx(PreviewHeader, { label, scopePicker, onClose }),
1719
+ /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto p-4", children })
1720
+ ]
1721
+ }
1722
+ )
1723
+ ] });
1724
+ };
1725
+ var PreviewToggleButton = ({
1726
+ onClick,
1727
+ label = "Open preview"
1728
+ }) => /* @__PURE__ */ jsxs(
1729
+ "button",
1730
+ {
1731
+ type: "button",
1732
+ onClick,
1733
+ className: "text-xs px-3 py-1.5 rounded-md border transition-colors hover:bg-[hsl(var(--ra-muted))] flex items-center gap-1.5",
1734
+ style: { borderColor: "hsl(var(--ra-border))", color: "hsl(var(--ra-text))" },
1735
+ children: [
1736
+ /* @__PURE__ */ jsx(Eye, { className: "w-3.5 h-3.5" }),
1737
+ label
1738
+ ]
1739
+ }
1740
+ );
1741
+ var SELECT_CLS = "text-xs px-2 py-1 rounded-md border bg-transparent focus:outline-none focus:ring-1";
1742
+ var PreviewScopePicker = ({
1743
+ SL,
1744
+ collectionId,
1745
+ editingScope,
1746
+ value,
1747
+ onChange,
1748
+ showVariants,
1749
+ showBatches,
1750
+ i18n
1751
+ }) => {
1752
+ const products = useProductBrowse({ SL, collectionId, pageSize: 100 });
1753
+ const variants = useProductChildren({
1754
+ SL,
1755
+ collectionId,
1756
+ productId: value.productId,
1757
+ kind: showVariants ? "variant" : null
1758
+ });
1759
+ const batches = useProductChildren({
1760
+ SL,
1761
+ collectionId,
1762
+ productId: value.productId,
1763
+ kind: showBatches ? "batch" : null
1764
+ });
1765
+ const isDefault = useMemo(
1766
+ () => value.raw === editingScope.raw,
1767
+ [value.raw, editingScope.raw]
1768
+ );
1769
+ const selectStyle = {
1770
+ borderColor: "hsl(var(--ra-border))",
1771
+ color: "hsl(var(--ra-text))"
1772
+ };
1773
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [
1774
+ /* @__PURE__ */ jsxs(
1775
+ "span",
1776
+ {
1777
+ className: "flex items-center gap-1 text-[10px] uppercase tracking-wide",
1778
+ style: { color: "hsl(var(--ra-muted-text))" },
1779
+ children: [
1780
+ /* @__PURE__ */ jsx(Eye, { className: "w-3 h-3" }),
1781
+ i18n?.previewAs ?? "Preview as"
1782
+ ]
1783
+ }
1784
+ ),
1785
+ !isDefault && /* @__PURE__ */ jsxs(
1786
+ "button",
1787
+ {
1788
+ type: "button",
1789
+ onClick: () => onChange(editingScope),
1790
+ className: "text-[10px] px-2 py-1 rounded-md border hover:bg-[hsl(var(--ra-muted))]",
1791
+ style: selectStyle,
1792
+ children: [
1793
+ "\u21BA ",
1794
+ i18n?.previewAsDefault ?? "Same as edited"
1795
+ ]
1796
+ }
1797
+ ),
1798
+ /* @__PURE__ */ jsxs(
1799
+ "select",
1800
+ {
1801
+ className: SELECT_CLS,
1802
+ style: selectStyle,
1803
+ value: value.productId ?? "",
1804
+ onChange: (e) => {
1805
+ const productId = e.target.value || void 0;
1806
+ onChange({
1807
+ ...value,
1808
+ productId,
1809
+ // Clear variant/batch when switching products.
1810
+ variantId: void 0,
1811
+ batchId: void 0,
1812
+ raw: productId ? `product:${productId}` : ""
1813
+ });
1814
+ },
1815
+ children: [
1816
+ /* @__PURE__ */ jsx("option", { value: "", children: "\u2014 Any product \u2014" }),
1817
+ products.items.map((p) => /* @__PURE__ */ jsx("option", { value: p.id, children: p.name }, p.id))
1818
+ ]
1819
+ }
1820
+ ),
1821
+ showVariants && value.productId && variants.items.length > 0 && /* @__PURE__ */ jsxs(
1822
+ "select",
1823
+ {
1824
+ className: SELECT_CLS,
1825
+ style: selectStyle,
1826
+ value: value.variantId ?? "",
1827
+ onChange: (e) => {
1828
+ const variantId = e.target.value || void 0;
1829
+ onChange({
1830
+ ...value,
1831
+ variantId,
1832
+ batchId: void 0,
1833
+ raw: variantId ? `product:${value.productId}/variant:${variantId}` : `product:${value.productId}`
1834
+ });
1835
+ },
1836
+ children: [
1837
+ /* @__PURE__ */ jsx("option", { value: "", children: "\u2014 Any variant \u2014" }),
1838
+ variants.items.map((v) => /* @__PURE__ */ jsx("option", { value: v.id, children: v.name }, v.id))
1839
+ ]
1840
+ }
1841
+ ),
1842
+ showBatches && value.productId && batches.items.length > 0 && /* @__PURE__ */ jsxs(
1843
+ "select",
1844
+ {
1845
+ className: SELECT_CLS,
1846
+ style: selectStyle,
1847
+ value: value.batchId ?? "",
1848
+ onChange: (e) => {
1849
+ const batchId = e.target.value || void 0;
1850
+ onChange({
1851
+ ...value,
1852
+ batchId,
1853
+ raw: batchId ? `product:${value.productId}/batch:${batchId}` : `product:${value.productId}`
1854
+ });
1855
+ },
1856
+ children: [
1857
+ /* @__PURE__ */ jsx("option", { value: "", children: "\u2014 Any batch \u2014" }),
1858
+ batches.items.map((b) => /* @__PURE__ */ jsx("option", { value: b.id, children: b.name }, b.id))
1859
+ ]
1860
+ }
1861
+ )
1862
+ ] });
1863
+ };
1864
+ var IntroCard = ({ title, body, onDismiss }) => /* @__PURE__ */ jsxs(
1865
+ "div",
1866
+ {
1867
+ className: "relative rounded-lg border p-4 mb-4",
1868
+ style: {
1869
+ borderColor: "hsl(var(--ra-accent) / 0.3)",
1870
+ background: "hsl(var(--ra-accent) / 0.04)"
1871
+ },
1872
+ children: [
1873
+ /* @__PURE__ */ jsx(
1874
+ "button",
1875
+ {
1876
+ type: "button",
1877
+ onClick: onDismiss,
1878
+ "aria-label": "Dismiss",
1879
+ className: "absolute top-2 right-2 p-1 rounded hover:bg-black/5",
1880
+ children: /* @__PURE__ */ jsx(X, { className: "w-3.5 h-3.5 opacity-60" })
1881
+ }
1882
+ ),
1883
+ /* @__PURE__ */ jsx("div", { className: "text-sm font-medium mb-1.5", style: { color: "hsl(var(--ra-text))" }, children: title }),
1884
+ /* @__PURE__ */ jsx("div", { className: "text-xs", style: { color: "hsl(var(--ra-muted-text))" }, children: body })
1885
+ ]
1886
+ }
1887
+ );
1888
+ var UtilityRow = ({ label, recordType, introHidden, onShowIntro }) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 mb-3 text-xs", style: { color: "hsl(var(--ra-muted-text))" }, children: [
1889
+ /* @__PURE__ */ jsx("span", { className: "font-mono opacity-70", children: recordType }),
1890
+ introHidden && onShowIntro && /* @__PURE__ */ jsxs(
1891
+ "button",
1892
+ {
1893
+ type: "button",
1894
+ onClick: onShowIntro,
1895
+ className: "flex items-center gap-1 px-2 py-1 rounded-full border hover:bg-[hsl(var(--ra-muted))]",
1896
+ style: { borderColor: "hsl(var(--ra-border))" },
1897
+ children: [
1898
+ /* @__PURE__ */ jsx(HelpCircle, { className: "w-3 h-3" }),
1899
+ "How ",
1900
+ label.toLowerCase(),
1901
+ " works"
1902
+ ]
1903
+ }
1904
+ )
1905
+ ] });
1906
+
1907
+ // src/components/RecordsAdmin/data/csv.ts
1908
+ var escapeCell = (s) => {
1909
+ if (/[",\n]/.test(s)) return `"${s.replace(/"/g, '""')}"`;
1910
+ return s;
1911
+ };
1912
+ var parseLine = (line) => {
1913
+ const out = [];
1914
+ let cur = "";
1915
+ let inQuotes = false;
1916
+ for (let i = 0; i < line.length; i += 1) {
1917
+ const ch = line[i];
1918
+ if (inQuotes) {
1919
+ if (ch === '"' && line[i + 1] === '"') {
1920
+ cur += '"';
1921
+ i += 1;
1922
+ } else if (ch === '"') inQuotes = false;
1923
+ else cur += ch;
1924
+ } else if (ch === '"') inQuotes = true;
1925
+ else if (ch === ",") {
1926
+ out.push(cur);
1927
+ cur = "";
1928
+ } else cur += ch;
1929
+ }
1930
+ out.push(cur);
1931
+ return out;
1932
+ };
1933
+ var exportCsv = (records, schema) => {
1934
+ const headers = ["ref", ...schema.columns.map((c) => c.header)];
1935
+ const rows = records.filter((r) => r.data != null).map((r) => [r.ref, ...schema.columns.map((c) => c.toCell(r.data))]);
1936
+ const csv = [headers, ...rows].map((row) => row.map((cell) => escapeCell(String(cell ?? ""))).join(",")).join("\n");
1937
+ return new Blob([csv], { type: "text/csv;charset=utf-8" });
1938
+ };
1939
+ var importCsv = async (file, schema, ctx) => {
1940
+ const text = await file.text();
1941
+ const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
1942
+ if (lines.length === 0) {
1943
+ return { total: 0, saved: 0, failed: 0, errorRows: [], annotatedCsv: "" };
1944
+ }
1945
+ const headers = parseLine(lines[0]);
1946
+ const refIdx = headers.indexOf("ref");
1947
+ const colMap = schema.columns.map((c) => ({ col: c, idx: headers.indexOf(c.header) }));
1948
+ const errorRows = [];
1949
+ let saved = 0;
1950
+ for (let i = 1; i < lines.length; i += 1) {
1951
+ const cells = parseLine(lines[i]);
1952
+ const ref = refIdx >= 0 ? cells[refIdx] : "";
1953
+ if (!ref) {
1954
+ errorRows.push({ row: i, error: "Missing ref" });
1955
+ continue;
1956
+ }
1957
+ const data = {};
1958
+ for (const { col, idx } of colMap) {
1959
+ if (idx < 0) continue;
1960
+ try {
1961
+ data[col.key] = col.fromCell(cells[idx] ?? "");
1962
+ } catch (e) {
1963
+ errorRows.push({ row: i, error: e.message });
1964
+ continue;
1965
+ }
1966
+ }
1967
+ const validation = schema.validate?.(data);
1968
+ if (validation) {
1969
+ errorRows.push({ row: i, error: validation });
1970
+ continue;
1971
+ }
1972
+ try {
1973
+ await upsertRecord(ctx, {
1974
+ ref,
1975
+ scope: parsedRefToScope(parseRef(ref)),
1976
+ data
1977
+ });
1978
+ saved += 1;
1979
+ } catch (e) {
1980
+ errorRows.push({ row: i, error: e.message });
1981
+ }
1982
+ }
1983
+ const annotatedHeaders = [...headers, "error"];
1984
+ const annotatedRows = lines.slice(1).map((line, idx) => {
1985
+ const err = errorRows.find((er) => er.row === idx + 1);
1986
+ const cells = parseLine(line);
1987
+ while (cells.length < headers.length) cells.push("");
1988
+ cells.push(err?.error ?? "");
1989
+ return cells.map(escapeCell).join(",");
1990
+ });
1991
+ const annotatedCsv = [annotatedHeaders.join(","), ...annotatedRows].join("\n");
1992
+ return {
1993
+ total: lines.length - 1,
1994
+ saved,
1995
+ failed: errorRows.length,
1996
+ errorRows,
1997
+ annotatedCsv
1998
+ };
1999
+ };
2000
+ var downloadBlob = (blob, filename) => {
2001
+ const url = URL.createObjectURL(blob);
2002
+ const a = document.createElement("a");
2003
+ a.href = url;
2004
+ a.download = filename;
2005
+ document.body.appendChild(a);
2006
+ a.click();
2007
+ document.body.removeChild(a);
2008
+ URL.revokeObjectURL(url);
2009
+ };
2010
+ var TOP_LEVEL_SCOPES = ["product", "facet"];
2011
+ var defaultItemId = () => {
2012
+ const time = Date.now().toString(36);
2013
+ const rand = Math.random().toString(36).slice(2, 8);
2014
+ return `${time}${rand}`;
2015
+ };
2016
+ var productItemToSummary = (p) => {
2017
+ const ref = buildRef({ productId: p.id });
2018
+ return {
2019
+ id: null,
2020
+ ref,
2021
+ scope: parseRef(ref),
2022
+ data: null,
2023
+ status: "empty",
2024
+ label: p.name,
2025
+ subtitle: p.sku ?? void 0
2026
+ };
2027
+ };
2028
+ function RecordsAdminShell(props) {
2029
+ const {
2030
+ SL,
2031
+ appId,
2032
+ collectionId,
2033
+ recordType,
2034
+ label,
2035
+ scopes: requestedScopes,
2036
+ defaultScope,
2037
+ contextScope,
2038
+ renderEditor,
2039
+ renderPreview,
2040
+ intro,
2041
+ csvSchema,
2042
+ classify,
2043
+ defaultData,
2044
+ i18n: i18nOverride,
2045
+ onTelemetry,
2046
+ className,
2047
+ previewMode = "inline",
2048
+ previewScopePicker = false,
2049
+ dirtyStrategy = "prompt",
2050
+ onBeforeDelete,
2051
+ disableBeforeUnload = false,
2052
+ presentations = ["list"],
2053
+ defaultPresentation,
2054
+ renderCard,
2055
+ renderListRow,
2056
+ renderEmpty,
2057
+ cardinality = "singleton",
2058
+ itemNoun = "item",
2059
+ generateItemId
2060
+ } = props;
2061
+ const i18n = { ...DEFAULT_I18N, ...i18nOverride ?? {} };
2062
+ const [presentation, setPresentation] = usePresentationPref({
2063
+ appId,
2064
+ recordType,
2065
+ options: presentations,
2066
+ defaultValue: defaultPresentation ?? presentations[0] ?? "list"
2067
+ });
2068
+ const onPresentationChange = useCallback((next) => {
2069
+ onTelemetry?.({ type: "presentation.change", recordType, from: presentation, to: next });
2070
+ setPresentation(next);
2071
+ }, [onTelemetry, recordType, presentation, setPresentation]);
2072
+ const ctx = useMemo(
2073
+ () => ({ SL, collectionId, appId, recordType }),
2074
+ [SL, collectionId, appId, recordType]
2075
+ );
2076
+ const probe = useScopeProbe({ SL, collectionId });
2077
+ const topLevelScopes = useMemo(
2078
+ () => requestedScopes.filter((s) => TOP_LEVEL_SCOPES.includes(s)),
2079
+ [requestedScopes]
2080
+ );
2081
+ const drillVariantsAllowed = useMemo(
2082
+ () => requestedScopes.includes("variant") && (probe.isLoading || probe.hasVariants),
2083
+ [requestedScopes, probe.isLoading, probe.hasVariants]
2084
+ );
2085
+ const drillBatchesAllowed = useMemo(
2086
+ () => requestedScopes.includes("batch") && (probe.isLoading || probe.hasBatches),
2087
+ [requestedScopes, probe.isLoading, probe.hasBatches]
2088
+ );
2089
+ const initialScope = useMemo(() => {
2090
+ if (contextScope?.productId && topLevelScopes.includes("product")) return "product";
2091
+ if (defaultScope && topLevelScopes.includes(defaultScope)) return defaultScope;
2092
+ return topLevelScopes[0] ?? "product";
2093
+ }, [contextScope?.productId, defaultScope, topLevelScopes]);
2094
+ const [activeScope, setActiveScope] = useState(initialScope);
2095
+ useEffect(() => {
2096
+ setActiveScope(initialScope);
2097
+ }, [initialScope]);
2098
+ const [search, setSearch] = useState("");
2099
+ const [filter, setFilter] = useState("all");
2100
+ const [selectedFacetRef, setSelectedFacetRef] = useState();
2101
+ const [selectedProductId, setSelectedProductId] = useState(
2102
+ contextScope?.productId
2103
+ );
2104
+ const [drillTab, setDrillTab] = useState(
2105
+ contextScope?.batchId ? "batch" : contextScope?.variantId ? "variant" : "product"
2106
+ );
2107
+ const [selectedVariantId, setSelectedVariantId] = useState(
2108
+ contextScope?.variantId
2109
+ );
2110
+ const [selectedBatchId, setSelectedBatchId] = useState(
2111
+ contextScope?.batchId
2112
+ );
2113
+ useEffect(() => {
2114
+ if (contextScope?.productId) setSelectedProductId(contextScope.productId);
2115
+ }, [contextScope?.productId]);
2116
+ useEffect(() => {
2117
+ if (contextScope?.variantId) {
2118
+ setSelectedVariantId(contextScope.variantId);
2119
+ setDrillTab("variant");
2120
+ }
2121
+ }, [contextScope?.variantId]);
2122
+ useEffect(() => {
2123
+ if (contextScope?.batchId) {
2124
+ setSelectedBatchId(contextScope.batchId);
2125
+ setDrillTab("batch");
2126
+ }
2127
+ }, [contextScope?.batchId]);
2128
+ const { dismissed, dismiss, undismiss } = useIntroDismissed(SL, collectionId, appId, recordType);
2129
+ const productBrowse = useProductBrowse({
2130
+ SL,
2131
+ collectionId,
2132
+ search: activeScope === "product" ? search : "",
2133
+ enabled: activeScope === "product" && !contextScope?.productId
2134
+ });
2135
+ const recordList = useRecordList({
2136
+ ctx,
2137
+ scopeKind: activeScope,
2138
+ search,
2139
+ filter,
2140
+ classify,
2141
+ contextScope,
2142
+ enabled: activeScope === "facet" && !probe.isLoading
2143
+ });
2144
+ const variantChildren = useProductChildren({
2145
+ SL,
2146
+ collectionId,
2147
+ productId: selectedProductId,
2148
+ kind: drillTab === "variant" ? "variant" : null
2149
+ });
2150
+ const batchChildren = useProductChildren({
2151
+ SL,
2152
+ collectionId,
2153
+ productId: selectedProductId,
2154
+ kind: drillTab === "batch" ? "batch" : null
2155
+ });
2156
+ useEffect(() => {
2157
+ if (activeScope !== "product") return;
2158
+ if (selectedProductId) return;
2159
+ const first = productBrowse.items[0];
2160
+ if (first) setSelectedProductId(first.id);
2161
+ }, [activeScope, selectedProductId, productBrowse.items]);
2162
+ useEffect(() => {
2163
+ if (activeScope !== "facet") return;
2164
+ if (selectedFacetRef) return;
2165
+ const first = recordList.items[0];
2166
+ if (first) setSelectedFacetRef(first.ref);
2167
+ }, [activeScope, selectedFacetRef, recordList.items]);
2168
+ useEffect(() => {
2169
+ setSearch("");
2170
+ setFilter("all");
2171
+ }, [activeScope]);
2172
+ const editingScope = useMemo(() => {
2173
+ if (activeScope === "facet") {
2174
+ return selectedFacetRef ? parseRef(selectedFacetRef) : null;
2175
+ }
2176
+ if (!selectedProductId) return null;
2177
+ if (drillTab === "variant") {
2178
+ if (!selectedVariantId) return null;
2179
+ return parseRef(buildRef({ productId: selectedProductId, variantId: selectedVariantId }));
2180
+ }
2181
+ if (drillTab === "batch") {
2182
+ if (!selectedBatchId) return null;
2183
+ return parseRef(buildRef({ productId: selectedProductId, batchId: selectedBatchId }));
2184
+ }
2185
+ return parseRef(buildRef({ productId: selectedProductId }));
2186
+ }, [activeScope, selectedFacetRef, selectedProductId, drillTab, selectedVariantId, selectedBatchId]);
2187
+ const supportedForResolution = useMemo(() => requestedScopes, [requestedScopes]);
2188
+ const resolved = useResolvedRecord({
2189
+ SL,
2190
+ appId,
2191
+ recordType,
2192
+ collectionId,
2193
+ productId: editingScope?.productId,
2194
+ variantId: editingScope?.variantId,
2195
+ batchId: editingScope?.batchId,
2196
+ facetId: editingScope?.facetId,
2197
+ facetValue: editingScope?.facetValue,
2198
+ supportedScopes: supportedForResolution,
2199
+ enabled: !!editingScope
2200
+ });
2201
+ const refetchAll = useCallback(() => {
2202
+ productBrowse.refetch();
2203
+ recordList.refetch();
2204
+ variantChildren.refetch();
2205
+ batchChildren.refetch();
2206
+ }, [productBrowse, recordList, variantChildren, batchChildren]);
2207
+ const editorCtx = useRecordEditor({
2208
+ ctx,
2209
+ scope: editingScope ?? parseRef(""),
2210
+ resolved: { data: resolved.data, source: resolved.source, sourceRef: resolved.sourceRef, parentValue: resolved.parentValue },
2211
+ defaultData,
2212
+ reseed: dirtyStrategy === "keep" ? "preserve-dirty" : "always",
2213
+ onSaved: () => {
2214
+ onTelemetry?.({ type: "record.save", recordType, ref: editingScope?.raw ?? "", isCreate: resolved.source !== "self" });
2215
+ refetchAll();
2216
+ },
2217
+ onDeleted: () => {
2218
+ onTelemetry?.({ type: "record.delete", recordType, ref: editingScope?.raw ?? "" });
2219
+ if (activeScope === "facet") {
2220
+ setSelectedFacetRef(void 0);
2221
+ } else if (drillTab === "variant") {
2222
+ setSelectedVariantId(void 0);
2223
+ } else if (drillTab === "batch") {
2224
+ setSelectedBatchId(void 0);
2225
+ }
2226
+ refetchAll();
2227
+ }
2228
+ });
2229
+ useUnsavedGuard({
2230
+ isDirty: editorCtx.isDirty,
2231
+ label: recordType,
2232
+ disableBeforeUnload
2233
+ });
2234
+ const { runWithGuard } = useDirtyNavigation({
2235
+ strategy: dirtyStrategy,
2236
+ isDirty: editorCtx.isDirty,
2237
+ save: editorCtx.save,
2238
+ reset: editorCtx.reset,
2239
+ i18n: {
2240
+ title: i18n.unsavedPromptTitle,
2241
+ body: i18n.unsavedPromptBody,
2242
+ save: i18n.unsavedPromptSave,
2243
+ discard: i18n.unsavedPromptDiscard,
2244
+ cancel: i18n.unsavedPromptCancel
2245
+ }
2246
+ });
2247
+ const handleExport = () => {
2248
+ if (!csvSchema) return;
2249
+ const blob = exportCsv(recordList.items, csvSchema);
2250
+ downloadBlob(blob, `${recordType}.csv`);
2251
+ onTelemetry?.({ type: "csv.export", recordType, rows: recordList.items.length });
2252
+ };
2253
+ const handleImport = () => {
2254
+ if (!csvSchema) return;
2255
+ const inp = document.createElement("input");
2256
+ inp.type = "file";
2257
+ inp.accept = ".csv,text/csv";
2258
+ inp.onchange = async () => {
2259
+ const file = inp.files?.[0];
2260
+ if (!file) return;
2261
+ const report = await importCsv(file, csvSchema, ctx);
2262
+ onTelemetry?.({ type: "csv.import", recordType, rows: report.total, errors: report.failed });
2263
+ if (report.failed > 0) {
2264
+ downloadBlob(new Blob([report.annotatedCsv], { type: "text/csv" }), `${recordType}-errors.csv`);
2265
+ }
2266
+ refetchAll();
2267
+ };
2268
+ inp.click();
2269
+ };
2270
+ const csvBulk = csvSchema ? { onImportCsv: handleImport, onExportCsv: handleExport } : {};
2271
+ const [previewScope, setPreviewScope] = useState(null);
2272
+ const [drawerOpen, setDrawerOpen] = useState(false);
2273
+ useEffect(() => {
2274
+ if (!editingScope) return;
2275
+ setPreviewScope((cur) => cur === null ? editingScope : cur);
2276
+ }, [editingScope]);
2277
+ const effectivePreviewScope = previewScope ?? editingScope;
2278
+ const renderEditorWithPreview = () => {
2279
+ if (!editingScope) return null;
2280
+ const previewBody = renderPreview && effectivePreviewScope ? renderPreview({ resolved: editorCtx.value, previewScope: effectivePreviewScope }) : null;
2281
+ const scopePicker = previewScopePicker && effectivePreviewScope ? /* @__PURE__ */ jsx(
2282
+ PreviewScopePicker,
2283
+ {
2284
+ SL,
2285
+ collectionId,
2286
+ editingScope,
2287
+ value: effectivePreviewScope,
2288
+ onChange: setPreviewScope,
2289
+ showVariants: drillVariantsAllowed,
2290
+ showBatches: drillBatchesAllowed,
2291
+ i18n: { previewAs: i18n.previewAs, previewAsDefault: i18n.previewAsDefault }
2292
+ }
2293
+ ) : null;
2294
+ const baseEditor = (extraFooter, inlinePreviewBody) => /* @__PURE__ */ jsx(
2295
+ RecordEditor,
2296
+ {
2297
+ ctx: editorCtx,
2298
+ i18n,
2299
+ bulkActions: { ...csvBulk, i18n },
2300
+ preview: inlinePreviewBody,
2301
+ footerExtra: extraFooter,
2302
+ onBeforeDelete: onBeforeDelete && editingScope ? () => onBeforeDelete(editingScope) : void 0,
2303
+ children: renderEditor(editorCtx)
2304
+ }
2305
+ );
2306
+ if (!previewBody) return baseEditor();
2307
+ if (previewMode === "inline") {
2308
+ return baseEditor(
2309
+ void 0,
2310
+ /* @__PURE__ */ jsx(InlinePreview, { label: i18n.preview, scopePicker, children: previewBody })
2311
+ );
2312
+ }
2313
+ if (previewMode === "side") {
2314
+ return /* @__PURE__ */ jsxs("div", { className: "grid h-full", style: { gridTemplateColumns: "minmax(0, 1fr) minmax(280px, 420px)" }, children: [
2315
+ /* @__PURE__ */ jsx("div", { className: "overflow-hidden", children: baseEditor() }),
2316
+ /* @__PURE__ */ jsx(SidePreview, { label: i18n.preview, scopePicker, children: previewBody })
2317
+ ] });
2318
+ }
2319
+ if (previewMode === "tab") {
2320
+ return /* @__PURE__ */ jsx(
2321
+ TabbedPreview,
2322
+ {
2323
+ editor: baseEditor(),
2324
+ preview: previewBody,
2325
+ scopePicker,
2326
+ i18n: { editor: i18n.editor, preview: i18n.preview }
2327
+ }
2328
+ );
2329
+ }
2330
+ return /* @__PURE__ */ jsxs("div", { className: "relative h-full", children: [
2331
+ baseEditor(
2332
+ /* @__PURE__ */ jsx(PreviewToggleButton, { onClick: () => setDrawerOpen(true), label: i18n.openPreview })
2333
+ ),
2334
+ /* @__PURE__ */ jsx(
2335
+ DrawerPreview,
2336
+ {
2337
+ open: drawerOpen,
2338
+ onClose: () => setDrawerOpen(false),
2339
+ label: i18n.preview,
2340
+ scopePicker,
2341
+ children: previewBody
2342
+ }
2343
+ )
2344
+ ] });
2345
+ };
2346
+ const isProductTab = activeScope === "product";
2347
+ const productPinned = !!contextScope?.productId;
2348
+ const productListItems = useMemo(() => {
2349
+ if (productPinned) {
2350
+ return [{
2351
+ id: null,
2352
+ ref: buildRef({ productId: contextScope.productId }),
2353
+ scope: parseRef(buildRef({ productId: contextScope.productId })),
2354
+ data: null,
2355
+ status: "empty",
2356
+ label: contextScope.productId,
2357
+ subtitle: void 0
2358
+ }];
2359
+ }
2360
+ return productBrowse.items.map(productItemToSummary);
2361
+ }, [productPinned, contextScope, productBrowse.items]);
2362
+ const leftItems = isProductTab ? productListItems : recordList.items;
2363
+ const leftLoading = isProductTab ? !productPinned && productBrowse.isLoading : recordList.isLoading || probe.isLoading;
2364
+ const leftError = isProductTab ? productBrowse.error : recordList.error;
2365
+ const leftSelectedRef = isProductTab ? selectedProductId ? buildRef({ productId: selectedProductId }) : void 0 : selectedFacetRef;
2366
+ const onLeftSelect = (item) => {
2367
+ void runWithGuard(() => {
2368
+ if (isProductTab) {
2369
+ setSelectedProductId(item.scope.productId);
2370
+ setSelectedVariantId(void 0);
2371
+ setSelectedBatchId(void 0);
2372
+ setDrillTab("product");
2373
+ } else {
2374
+ setSelectedFacetRef(item.ref);
2375
+ }
2376
+ });
2377
+ };
2378
+ return /* @__PURE__ */ jsxs("div", { className: `ra-shell flex flex-col h-full ${className ?? ""}`, children: [
2379
+ /* @__PURE__ */ jsxs("div", { className: "px-4 pt-4", children: [
2380
+ intro && !dismissed && /* @__PURE__ */ jsx(
2381
+ IntroCard,
2382
+ {
2383
+ title: intro.title,
2384
+ body: intro.body,
2385
+ onDismiss: () => {
2386
+ dismiss();
2387
+ onTelemetry?.({ type: "intro.dismiss", recordType });
2388
+ }
2389
+ }
2390
+ ),
2391
+ /* @__PURE__ */ jsx(
2392
+ UtilityRow,
2393
+ {
2394
+ label,
2395
+ recordType,
2396
+ introHidden: dismissed && !!intro,
2397
+ onShowIntro: undismiss
2398
+ }
2399
+ )
2400
+ ] }),
2401
+ /* @__PURE__ */ jsxs(
2402
+ "div",
2403
+ {
2404
+ className: "flex-1 grid border-t overflow-hidden",
2405
+ style: { gridTemplateColumns: "minmax(260px, 320px) 1fr", borderColor: "hsl(var(--ra-border))" },
2406
+ children: [
2407
+ /* @__PURE__ */ jsxs("aside", { className: "border-r overflow-hidden flex flex-col", style: { borderColor: "hsl(var(--ra-border))", background: "hsl(var(--ra-surface))" }, children: [
2408
+ /* @__PURE__ */ jsx(
2409
+ ScopeTabs,
2410
+ {
2411
+ scopes: topLevelScopes,
2412
+ active: activeScope,
2413
+ onChange: (s) => {
2414
+ void runWithGuard(() => {
2415
+ onTelemetry?.({ type: "scope.change", recordType, from: activeScope, to: s });
2416
+ setActiveScope(s);
2417
+ });
2418
+ },
2419
+ loading: probe.isLoading
2420
+ }
2421
+ ),
2422
+ /* @__PURE__ */ jsxs("div", { className: "p-3 space-y-2.5 border-b", style: { borderColor: "hsl(var(--ra-border))" }, children: [
2423
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2424
+ /* @__PURE__ */ jsxs("div", { className: "relative flex-1 min-w-0", children: [
2425
+ /* @__PURE__ */ jsx(Search, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 opacity-50" }),
2426
+ /* @__PURE__ */ jsx(
2427
+ "input",
2428
+ {
2429
+ type: "text",
2430
+ value: search,
2431
+ onChange: (e) => setSearch(e.target.value),
2432
+ placeholder: i18n.searchPlaceholder,
2433
+ className: "w-full pl-8 pr-3 py-1.5 text-xs rounded-md border bg-transparent focus:outline-none focus:ring-1",
2434
+ style: { borderColor: "hsl(var(--ra-border))", color: "hsl(var(--ra-text))" }
2435
+ }
2436
+ )
2437
+ ] }),
2438
+ /* @__PURE__ */ jsx(
2439
+ PresentationSwitcher,
2440
+ {
2441
+ options: presentations,
2442
+ value: presentation,
2443
+ onChange: onPresentationChange,
2444
+ i18n
2445
+ }
2446
+ )
2447
+ ] }),
2448
+ !isProductTab && /* @__PURE__ */ jsx(StatusFilterPills, { value: filter, onChange: setFilter, counts: recordList.counts, i18n }),
2449
+ cardinality === "collection" && !isProductTab && editingScope && /* @__PURE__ */ jsxs(
2450
+ "button",
2451
+ {
2452
+ type: "button",
2453
+ onClick: () => {
2454
+ void runWithGuard(() => {
2455
+ const id = generateItemId ? generateItemId() : defaultItemId();
2456
+ const baseRef = editingScope.raw;
2457
+ const itemRef = baseRef ? `${baseRef}/item:${id}` : `item:${id}`;
2458
+ setSelectedFacetRef(itemRef);
2459
+ onTelemetry?.({ type: "item.create", recordType, scopeRef: baseRef });
2460
+ });
2461
+ },
2462
+ className: "w-full inline-flex items-center justify-center gap-1.5 text-xs py-1.5 rounded-md border transition-colors hover:bg-[hsl(var(--ra-muted))]",
2463
+ style: { borderColor: "hsl(var(--ra-border))", color: "hsl(var(--ra-text))" },
2464
+ children: [
2465
+ /* @__PURE__ */ jsx(Plus, { className: "w-3.5 h-3.5" }),
2466
+ i18n.newItem || `New ${itemNoun}`
2467
+ ]
2468
+ }
2469
+ )
2470
+ ] }),
2471
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto", children: [
2472
+ leftLoading && /* @__PURE__ */ jsx(LoadingState, {}),
2473
+ !leftLoading && leftError && /* @__PURE__ */ jsx(ErrorState, { error: leftError }),
2474
+ !leftLoading && !leftError && leftItems.length === 0 && (renderEmpty ? renderEmpty({ scope: editingScope }) : /* @__PURE__ */ jsx(EmptyState, { title: i18n.noResults, body: search ? void 0 : i18n.emptyBody })),
2475
+ !leftLoading && !leftError && leftItems.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
2476
+ /* @__PURE__ */ jsx(
2477
+ RecordList,
2478
+ {
2479
+ items: leftItems,
2480
+ selectedRef: leftSelectedRef,
2481
+ onSelect: onLeftSelect,
2482
+ dirtyRef: editorCtx.isDirty ? editingScope?.raw : void 0,
2483
+ presentation,
2484
+ renderListRow,
2485
+ renderCard
2486
+ }
2487
+ ),
2488
+ isProductTab && !productPinned && productBrowse.hasNextPage && /* @__PURE__ */ jsx("div", { className: "p-2", children: /* @__PURE__ */ jsx(
2489
+ "button",
2490
+ {
2491
+ type: "button",
2492
+ onClick: () => {
2493
+ void productBrowse.fetchNextPage();
2494
+ },
2495
+ disabled: productBrowse.isFetchingNextPage,
2496
+ className: "w-full text-xs py-2 rounded-md border transition-opacity disabled:opacity-50 hover:bg-[hsl(var(--ra-muted))]",
2497
+ style: { borderColor: "hsl(var(--ra-border))", color: "hsl(var(--ra-text))" },
2498
+ children: productBrowse.isFetchingNextPage ? "Loading\u2026" : `Load more (${leftItems.length} shown)`
2499
+ }
2500
+ ) })
2501
+ ] })
2502
+ ] })
2503
+ ] }),
2504
+ /* @__PURE__ */ jsxs("main", { className: "overflow-hidden", children: [
2505
+ !editingScope && activeScope === "product" && !selectedProductId && /* @__PURE__ */ jsx(EmptyState, { title: i18n.emptyTitle, body: i18n.emptyBody }),
2506
+ !editingScope && activeScope === "facet" && /* @__PURE__ */ jsx(EmptyState, { title: i18n.emptyTitle, body: i18n.emptyBody }),
2507
+ isProductTab && selectedProductId && /* @__PURE__ */ jsx(
2508
+ ProductDrillDown,
2509
+ {
2510
+ productLabel: productBrowse.items.find((p) => p.id === selectedProductId)?.name ?? selectedProductId,
2511
+ showVariants: drillVariantsAllowed,
2512
+ showBatches: drillBatchesAllowed,
2513
+ active: drillTab,
2514
+ onChange: (t) => {
2515
+ void runWithGuard(() => {
2516
+ setDrillTab(t);
2517
+ if (t === "product") {
2518
+ setSelectedVariantId(void 0);
2519
+ setSelectedBatchId(void 0);
2520
+ }
2521
+ });
2522
+ },
2523
+ selectedChildId: drillTab === "variant" ? selectedVariantId : drillTab === "batch" ? selectedBatchId : void 0,
2524
+ onSelectChild: (id) => {
2525
+ void runWithGuard(() => {
2526
+ if (drillTab === "variant") setSelectedVariantId(id);
2527
+ else if (drillTab === "batch") setSelectedBatchId(id);
2528
+ });
2529
+ },
2530
+ variants: variantChildren.items,
2531
+ batches: batchChildren.items,
2532
+ variantsLoading: variantChildren.isLoading,
2533
+ batchesLoading: batchChildren.isLoading,
2534
+ children: editingScope ? renderEditorWithPreview() : /* @__PURE__ */ jsx(
2535
+ EmptyState,
2536
+ {
2537
+ title: drillTab === "variant" ? "Pick a variant" : "Pick a batch",
2538
+ body: `Select a ${drillTab} on the left to edit its ${recordType}.`
2539
+ }
2540
+ )
2541
+ }
2542
+ ),
2543
+ !isProductTab && editingScope && renderEditorWithPreview()
2544
+ ] })
2545
+ ]
2546
+ }
2547
+ )
2548
+ ] });
2549
+ }
2550
+ var RecordBrowser = ({
2551
+ scopes,
2552
+ activeScope,
2553
+ onActiveScopeChange,
2554
+ selectedRef,
2555
+ onSelectRef,
2556
+ items,
2557
+ counts,
2558
+ isLoading,
2559
+ error,
2560
+ filter,
2561
+ onFilterChange,
2562
+ search,
2563
+ onSearchChange,
2564
+ hasNextPage,
2565
+ isFetchingNextPage,
2566
+ onLoadMore,
2567
+ scopesLoading,
2568
+ i18n
2569
+ }) => {
2570
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", style: { background: "hsl(var(--ra-surface))" }, children: [
2571
+ /* @__PURE__ */ jsx(ScopeTabs, { scopes, active: activeScope, onChange: onActiveScopeChange, loading: scopesLoading }),
2572
+ /* @__PURE__ */ jsxs("div", { className: "p-3 space-y-2.5 border-b", style: { borderColor: "hsl(var(--ra-border))" }, children: [
2573
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
2574
+ /* @__PURE__ */ jsx(Search, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 opacity-50" }),
2575
+ /* @__PURE__ */ jsx(
2576
+ "input",
2577
+ {
2578
+ type: "text",
2579
+ value: search,
2580
+ onChange: (e) => onSearchChange(e.target.value),
2581
+ placeholder: i18n.searchPlaceholder,
2582
+ className: "w-full pl-8 pr-3 py-1.5 text-xs rounded-md border bg-transparent focus:outline-none focus:ring-1",
2583
+ style: {
2584
+ borderColor: "hsl(var(--ra-border))",
2585
+ color: "hsl(var(--ra-text))"
2586
+ }
2587
+ }
2588
+ )
2589
+ ] }),
2590
+ /* @__PURE__ */ jsx(StatusFilterPills, { value: filter, onChange: onFilterChange, counts, i18n })
2591
+ ] }),
2592
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto", children: [
2593
+ isLoading && /* @__PURE__ */ jsx(LoadingState, {}),
2594
+ !isLoading && error && /* @__PURE__ */ jsx(ErrorState, { error }),
2595
+ !isLoading && !error && items.length === 0 && /* @__PURE__ */ jsx(EmptyState, { title: i18n.noResults, body: search ? void 0 : i18n.emptyBody }),
2596
+ !isLoading && !error && items.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
2597
+ /* @__PURE__ */ jsx(RecordList, { items, selectedRef, onSelect: onSelectRef }),
2598
+ hasNextPage && /* @__PURE__ */ jsx("div", { className: "p-2", children: /* @__PURE__ */ jsx(
2599
+ "button",
2600
+ {
2601
+ type: "button",
2602
+ onClick: onLoadMore,
2603
+ disabled: isFetchingNextPage,
2604
+ className: "w-full text-xs py-2 rounded-md border transition-opacity disabled:opacity-50 hover:bg-[hsl(var(--ra-muted))]",
2605
+ style: { borderColor: "hsl(var(--ra-border))", color: "hsl(var(--ra-text))" },
2606
+ children: isFetchingNextPage ? "Loading\u2026" : `Load more (${items.length} shown)`
2607
+ }
2608
+ ) })
2609
+ ] })
2610
+ ] })
2611
+ ] });
2612
+ };
2613
+ var Ctx = createContext({});
2614
+ var InheritanceProvider = ({
2615
+ parentValue,
2616
+ ownValue,
2617
+ onResetField,
2618
+ children
2619
+ }) => /* @__PURE__ */ jsx(Ctx.Provider, { value: { parentValue, ownValue, onResetField }, children });
2620
+ var eq = (a, b) => {
2621
+ try {
2622
+ return JSON.stringify(a) === JSON.stringify(b);
2623
+ } catch {
2624
+ return false;
2625
+ }
2626
+ };
2627
+ var InheritanceMarker = ({ field, inheritedValue, value, children }) => {
2628
+ const ctx = useContext(Ctx);
2629
+ const inh = inheritedValue !== void 0 ? inheritedValue : ctx.parentValue?.[field];
2630
+ const cur = value !== void 0 ? value : ctx.ownValue?.[field];
2631
+ const isInherited = inh !== void 0 && eq(inh, cur);
2632
+ if (inh === void 0) return /* @__PURE__ */ jsx(Fragment, { children });
2633
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
2634
+ children,
2635
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1 text-[10px]", style: { color: "hsl(var(--ra-muted-text))" }, children: isInherited ? /* @__PURE__ */ jsxs(Fragment, { children: [
2636
+ /* @__PURE__ */ jsx(CornerDownLeft, { className: "w-2.5 h-2.5" }),
2637
+ /* @__PURE__ */ jsx("span", { children: "Inherited" })
2638
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2639
+ /* @__PURE__ */ jsx(Circle, { className: "w-2 h-2 fill-current", style: { color: "hsl(var(--ra-status-shared))" } }),
2640
+ /* @__PURE__ */ jsx("span", { children: "Override" }),
2641
+ ctx.onResetField && /* @__PURE__ */ jsx(
2642
+ "button",
2643
+ {
2644
+ type: "button",
2645
+ onClick: () => ctx.onResetField?.(field),
2646
+ className: "underline opacity-70 hover:opacity-100 ml-1",
2647
+ children: "Reset"
2648
+ }
2649
+ )
2650
+ ] }) })
2651
+ ] });
2652
+ };
2653
+ var ResolvedPreview = ({ children }) => /* @__PURE__ */ jsxs("div", { className: "mt-4 pt-4 border-t", style: { borderColor: "hsl(var(--ra-border))" }, children: [
2654
+ /* @__PURE__ */ jsx("div", { className: "text-[10px] uppercase tracking-wide mb-2", style: { color: "hsl(var(--ra-muted-text))" }, children: "Public preview" }),
2655
+ children
2656
+ ] });
2657
+ var DEFAULT_SCOPES2 = ["batch", "variant", "product", "facet"];
2658
+ var readField = (data, path) => {
2659
+ if (data == null || typeof data !== "object") return void 0;
2660
+ return path.split(".").reduce((acc, key) => {
2661
+ if (acc == null || typeof acc !== "object") return void 0;
2662
+ return acc[key];
2663
+ }, data);
2664
+ };
2665
+ var compareValues = (a, b) => {
2666
+ const aNull = a === void 0 || a === null;
2667
+ const bNull = b === void 0 || b === null;
2668
+ if (aNull && bNull) return 0;
2669
+ if (aNull) return 1;
2670
+ if (bNull) return -1;
2671
+ if (typeof a === "number" && typeof b === "number") return a - b;
2672
+ return String(a).localeCompare(String(b), void 0, { numeric: true, sensitivity: "base" });
2673
+ };
2674
+ function useCollectedRecords(args) {
2675
+ const {
2676
+ SL,
2677
+ appId,
2678
+ recordType,
2679
+ collectionId,
2680
+ productId,
2681
+ variantId,
2682
+ batchId,
2683
+ facetId,
2684
+ facetValue,
2685
+ supportedScopes = DEFAULT_SCOPES2,
2686
+ filterEmpty = true,
2687
+ sort,
2688
+ enabled = true
2689
+ } = args;
2690
+ const sortKey = sort ? sort.kind === "field" ? `field:${sort.field}:${sort.direction ?? "asc"}` : `specificity:${sort.direction ?? "desc"}` : "specificity:desc";
2691
+ const query = useQuery({
2692
+ queryKey: [
2693
+ "records-admin",
2694
+ "collected",
2695
+ collectionId,
2696
+ appId,
2697
+ recordType,
2698
+ productId ?? null,
2699
+ variantId ?? null,
2700
+ batchId ?? null,
2701
+ facetId ?? null,
2702
+ facetValue ?? null,
2703
+ supportedScopes.join(","),
2704
+ filterEmpty,
2705
+ sortKey
2706
+ ],
2707
+ enabled: enabled && !!collectionId && !!appId,
2708
+ staleTime: 15e3,
2709
+ queryFn: async () => {
2710
+ const target = parseRef("");
2711
+ target.productId = productId;
2712
+ target.variantId = variantId;
2713
+ target.batchId = batchId;
2714
+ target.facetId = facetId;
2715
+ target.facetValue = facetValue;
2716
+ const ctx = { SL, collectionId, appId, recordType };
2717
+ const result = await matchRecords(ctx, parsedRefToTarget(target), { strategy: "all" }).catch(() => null);
2718
+ const records = result?.records ?? [];
2719
+ const baseList = records.map((rec, i) => ({
2720
+ ref: rec.ref ?? "",
2721
+ scope: parseRef(rec.ref ?? ""),
2722
+ data: rec.data,
2723
+ depth: i
2724
+ }));
2725
+ const sortKind = sort?.kind ?? "specificity";
2726
+ const direction = sort?.direction ?? (sortKind === "specificity" ? "desc" : "asc");
2727
+ const sign = direction === "desc" ? -1 : 1;
2728
+ const fieldPath = sort?.kind === "field" ? sort.field : "";
2729
+ const sorted = [...baseList].sort((a, b) => {
2730
+ if (sortKind === "field") {
2731
+ const cmp = compareValues(readField(a.data, fieldPath), readField(b.data, fieldPath));
2732
+ if (cmp !== 0) return sign * cmp;
2733
+ return a.depth - b.depth;
2734
+ }
2735
+ return direction === "desc" ? a.depth - b.depth : b.depth - a.depth;
2736
+ });
2737
+ return sorted;
2738
+ }
2739
+ });
2740
+ return {
2741
+ items: query.data ?? [],
2742
+ isLoading: query.isLoading,
2743
+ error: query.error ?? null,
2744
+ refetch: query.refetch
2745
+ };
2746
+ }
2747
+ var DEFAULT_SCOPES3 = ["batch", "variant", "product", "facet"];
2748
+ var isPlainObject = (v) => !!v && typeof v === "object" && !Array.isArray(v) && v.constructor === Object;
2749
+ var deepMerge = (parent, child) => {
2750
+ if (!isPlainObject(parent) || !isPlainObject(child)) return child;
2751
+ const out = { ...parent };
2752
+ for (const [k, v] of Object.entries(child)) {
2753
+ out[k] = isPlainObject(v) && isPlainObject(out[k]) ? deepMerge(out[k], v) : v;
2754
+ }
2755
+ return out;
2756
+ };
2757
+ var shallowMerge = (parent, child) => {
2758
+ if (!isPlainObject(parent) || !isPlainObject(child)) return child;
2759
+ return { ...parent, ...child };
2760
+ };
2761
+ function useMergedRecord(args) {
2762
+ const {
2763
+ SL,
2764
+ appId,
2765
+ recordType,
2766
+ collectionId,
2767
+ productId,
2768
+ variantId,
2769
+ batchId,
2770
+ facetId,
2771
+ facetValue,
2772
+ supportedScopes = DEFAULT_SCOPES3,
2773
+ strategy = "deep",
2774
+ merge,
2775
+ enabled = true
2776
+ } = args;
2777
+ const mergeFn = useMemo(() => {
2778
+ if (merge) return merge;
2779
+ return strategy === "shallow" ? shallowMerge : deepMerge;
2780
+ }, [merge, strategy]);
2781
+ const query = useQuery({
2782
+ queryKey: [
2783
+ "records-admin",
2784
+ "merged",
2785
+ collectionId,
2786
+ appId,
2787
+ recordType,
2788
+ productId ?? null,
2789
+ variantId ?? null,
2790
+ batchId ?? null,
2791
+ facetId ?? null,
2792
+ facetValue ?? null,
2793
+ supportedScopes.join(","),
2794
+ strategy
2795
+ ],
2796
+ enabled: enabled && !!collectionId && !!appId,
2797
+ staleTime: 15e3,
2798
+ queryFn: async () => {
2799
+ const target = parseRef("");
2800
+ target.productId = productId;
2801
+ target.variantId = variantId;
2802
+ target.batchId = batchId;
2803
+ target.facetId = facetId;
2804
+ target.facetValue = facetValue;
2805
+ const ctx = { SL, collectionId, appId, recordType };
2806
+ const result = await matchRecords(ctx, parsedRefToTarget(target), { strategy: "all" }).catch(() => null);
2807
+ const records = result?.records ?? [];
2808
+ if (records.length === 0) return { data: null, provenance: {}, layers: [] };
2809
+ const present = records.map((rec) => ({ ref: rec.ref ?? "", data: rec.data }));
2810
+ const ordered = [...present].reverse();
2811
+ let merged = null;
2812
+ const provenance = {};
2813
+ for (const layer of ordered) {
2814
+ merged = mergeFn(merged, layer.data);
2815
+ if (isPlainObject(layer.data)) {
2816
+ for (const k of Object.keys(layer.data)) provenance[k] = layer.ref;
2817
+ }
2818
+ }
2819
+ return { data: merged, provenance, layers: present.map((p) => p.ref) };
2820
+ }
2821
+ });
2822
+ const data = query.data ?? { data: null, provenance: {}, layers: [] };
2823
+ return {
2824
+ ...data,
2825
+ isLoading: query.isLoading,
2826
+ error: query.error ?? null,
2827
+ refetch: query.refetch
2828
+ };
2829
+ }
2830
+
2831
+ export { ALL_PRESENTATIONS, BatchList, BulkActionsMenu, DEFAULT_I18N, DefaultRecordCard, DefaultRecordRow, DeleteButton, DrawerPreview, EmptyState, ErrorState, FacetList, InheritanceMarker, InheritanceProvider, InlinePreview, IntroCard, LoadingState, PresentationSwitcher, PreviewScopePicker, PreviewToggleButton, ProductDrillDown, ProductList, RecordBrowser, RecordEditor, RecordList, RecordsAdminShell, ResolvedPreview, ScopeBreadcrumb, ScopeTabs, SidePreview, StatusDot, StatusFilterPills, TabbedPreview, UtilityRow, VariantList, buildRef, bulkDelete, bulkUpsert, deleteRecord, downloadBlob, exportCsv, getRecordByRef, importCsv, listRecords, matchRecords, parseRef, parsedRefToScope, parsedRefToTarget, resolutionChain, resolveRecord, restoreRecord, scopesEqual, upsertRecord, useCollectedRecords, useDirtyNavigation, useIntroDismissed, useMergedRecord, usePresentationPref, useProductBrowse, useProductChildren, useRecordEditor, useRecordList, useResolvedRecord, useScopeProbe, useUnsavedGuard };
2832
+ //# sourceMappingURL=index.js.map
2833
+ //# sourceMappingURL=index.js.map