@proveanything/smartlinks-utils-ui 0.2.13 → 0.3.1

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