@campfire-interactive/volume-intelligence-ui 0.7.0 → 0.9.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +60 -10
  2. package/dist/index.js +143 -62
  3. package/package.json +43 -43
package/dist/index.d.ts CHANGED
@@ -150,12 +150,21 @@ interface SegmentMappingEntry {
150
150
  segmentId: string;
151
151
  transformRule?: string | null;
152
152
  }
153
+ type AliasStatus = 'suggested' | 'approved' | 'rejected';
153
154
  interface SourceValueAlias {
154
155
  id: string;
155
156
  sourceId: string;
156
- level: number;
157
+ /** Segment identity (replaces the old hierarchy-only `level`).
158
+ * targetCategory: 'hierarchy_level' | 'automotive_field' | 'dimension';
159
+ * targetKey: 'level1'..'level6' | 'plant_name'|… | 'dim1'..'dimN'. */
160
+ targetCategory: string;
161
+ targetKey: string;
157
162
  rawValue: string;
158
163
  canonicalValue: string;
164
+ /** Two-stage lifecycle — only 'approved' aliases are used downstream. */
165
+ status: AliasStatus;
166
+ confidence?: number | null;
167
+ suggestedBy?: string | null;
159
168
  createdAt: string;
160
169
  updatedAt: string;
161
170
  }
@@ -163,6 +172,13 @@ interface ValueAliasEntry {
163
172
  rawValue: string;
164
173
  canonicalValue: string;
165
174
  }
175
+ /** A mapped segment an alias can be defined for (built by the host from the
176
+ * source's segment mappings). */
177
+ interface AliasTarget {
178
+ targetCategory: string;
179
+ targetKey: string;
180
+ label: string;
181
+ }
166
182
  interface RelinkResult {
167
183
  totalOverlay: number;
168
184
  linked: number;
@@ -300,8 +316,16 @@ interface SourceAdminTransport {
300
316
  items: SegmentMappingRow[];
301
317
  }>;
302
318
  listValueAliases(sourceId: string): Promise<SourceValueAlias[]>;
319
+ /** Bulk-replace the APPROVED aliases for a segment. OPTIONAL/newer shape
320
+ * keyed by (targetCategory, targetKey); falls back to replaceLevelValueAliases
321
+ * for hosts still on the hierarchy-level shape. */
322
+ replaceValueAliases?(sourceId: string, targetCategory: string, targetKey: string, entries: ValueAliasEntry[]): Promise<SourceValueAlias[]>;
303
323
  replaceLevelValueAliases(sourceId: string, level: number, entries: ValueAliasEntry[]): Promise<SourceValueAlias[]>;
304
324
  deleteValueAlias(aliasId: string): Promise<void>;
325
+ /** Approve/reject a suggested alias. OPTIONAL: when omitted the card hides the
326
+ * suggested-review controls (a host that predates the workflow is unaffected). */
327
+ approveValueAlias?(aliasId: string): Promise<SourceValueAlias>;
328
+ rejectValueAlias?(aliasId: string): Promise<SourceValueAlias>;
305
329
  relinkOverlaySource(sourceId: string): Promise<RelinkResult>;
306
330
  getTenantConfig(): Promise<TenantVolumeConfig | null>;
307
331
  setTenantSource(sourceId: string): Promise<TenantVolumeConfig>;
@@ -369,6 +393,14 @@ interface VolumeSourcesListProps {
369
393
  * (ADR §6) while the list itself stays host-agnostic.
370
394
  */
371
395
  renderActiveSourceExtras?: (source: VolumeSource) => ReactNode;
396
+ /**
397
+ * When true, the list is view-only: the create / add-from-template / set-active
398
+ * affordances are hidden so a non-admin host (e.g. forecast members) can show
399
+ * sources without editing them. Defaults to false — existing embedders are
400
+ * unaffected. Mutations are never reachable in this mode; the host should
401
+ * still enforce the boundary server-side.
402
+ */
403
+ readOnly?: boolean;
372
404
  className?: string;
373
405
  }
374
406
  /**
@@ -379,7 +411,7 @@ interface VolumeSourcesListProps {
379
411
  * The VI-webapp's active-source analytical sub-cards (enrichment toggle,
380
412
  * backtests) are intentionally NOT here — per ADR §6 they stay VI-webapp-only.
381
413
  */
382
- declare function VolumeSourcesList({ transport, onOpenSource, renderActiveSourceExtras, className }: VolumeSourcesListProps): react.JSX.Element;
414
+ declare function VolumeSourcesList({ transport, onOpenSource, renderActiveSourceExtras, readOnly, className }: VolumeSourcesListProps): react.JSX.Element;
383
415
 
384
416
  /**
385
417
  * Segment mapper — a catalog-driven, required-first assignment surface built for
@@ -422,8 +454,11 @@ interface UnifiedSegmentMapperProps {
422
454
  segmentId: string;
423
455
  }>) => Promise<void>;
424
456
  saving: boolean;
457
+ /** View-only: assignments can't be changed and Save is disabled. Follows the
458
+ * `locked` convention used by TallLayoutMappings / ValueAliasesCard. */
459
+ locked?: boolean;
425
460
  }
426
- declare function UnifiedSegmentMapper({ segments, mappableDimensions, initialMappings, onSave, saving, }: UnifiedSegmentMapperProps): react.JSX.Element;
461
+ declare function UnifiedSegmentMapper({ segments, mappableDimensions, initialMappings, onSave, saving, locked, }: UnifiedSegmentMapperProps): react.JSX.Element;
427
462
 
428
463
  interface VolumeSourceDetailProps {
429
464
  transport: SourceAdminTransport;
@@ -432,6 +467,14 @@ interface VolumeSourceDetailProps {
432
467
  onOpenSource: (sourceId: string) => void;
433
468
  /** Optional "All sources →" affordance; hidden when omitted. */
434
469
  onBack?: () => void;
470
+ /**
471
+ * View-only mode: hides the Danger-zone tab and disables every mutating
472
+ * control (layout, unique key, segment detect/add/delete, mapper save,
473
+ * value aliases). For non-admin hosts (e.g. forecast members). Defaults to
474
+ * false — existing embedders unaffected. The host must still enforce the
475
+ * boundary server-side.
476
+ */
477
+ readOnly?: boolean;
435
478
  className?: string;
436
479
  }
437
480
  /**
@@ -440,7 +483,7 @@ interface VolumeSourceDetailProps {
440
483
  * Ported from forecast's SourceDetailPage; adminApi → injected transport,
441
484
  * react-router → onOpenSource/onBack callbacks, shadcn/Tailwind → plain CSS.
442
485
  */
443
- declare function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, className }: VolumeSourceDetailProps): react.JSX.Element;
486
+ declare function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, readOnly, className }: VolumeSourceDetailProps): react.JSX.Element;
444
487
 
445
488
  interface TallLayoutMappingsProps {
446
489
  transport: SourceAdminTransport;
@@ -450,18 +493,25 @@ interface TallLayoutMappingsProps {
450
493
  declare function TallLayoutMappings({ transport, source, locked }: TallLayoutMappingsProps): react.JSX.Element;
451
494
 
452
495
  /**
453
- * Per-source, per-level value alias editor. Ported from forecast; adminApi
454
- * calls lifted to the injected transport. The matcher applies aliases to both
455
- * sides of the overlay→base comparison, so an operator can fix vocabulary
456
- * drift (e.g. "MICHIGAN" "MICHIGAN ASSEMBLY") without re-uploading.
496
+ * Per-source value alias editor. Aliases normalise a raw source value to a
497
+ * canonical one for a mapped segment (targetCategory/targetKey) the matcher
498
+ * applies approved hierarchy aliases to both sides of the overlay→base compare;
499
+ * automotive aliases are consumed by downstream repos via the seam API.
500
+ *
501
+ * Two stages: `suggested` aliases (populated externally, e.g. cfi-ai-hub) are
502
+ * shown in a review list with Approve/Reject; `approved` aliases are the
503
+ * editable set. `targets` (mapped segments) comes from the host; when omitted we
504
+ * fall back to hierarchy levels 1..6 so older hosts keep working.
457
505
  */
458
506
  interface ValueAliasesCardProps {
459
507
  transport: SourceAdminTransport;
460
508
  sourceId: string;
461
509
  locked: boolean;
462
510
  isOverlay: boolean;
511
+ /** Mapped segments aliases can be defined for. Falls back to level1..6. */
512
+ targets?: AliasTarget[];
463
513
  }
464
- declare function ValueAliasesCard({ transport, sourceId, locked, isOverlay }: ValueAliasesCardProps): react.JSX.Element;
514
+ declare function ValueAliasesCard({ transport, sourceId, locked, isOverlay, targets }: ValueAliasesCardProps): react.JSX.Element;
465
515
 
466
516
  declare const HIERARCHY_LEVEL_COUNT = 6;
467
517
  /** Display labels for hierarchy levels 1..6 (the mapping of source columns to
@@ -474,4 +524,4 @@ declare const DEFAULT_LEVEL_NAMES: Record<number, string>;
474
524
  declare const AUTOMOTIVE_SLOTS: AutomotiveSlot[];
475
525
  declare const AUTOMOTIVE_TRANSFORM_LABELS: Record<string, string>;
476
526
 
477
- export { AUTOMOTIVE_SLOTS, AUTOMOTIVE_TRANSFORM_LABELS, type AutomotiveSlot, type ClearSourceDataResult, type CreateSegmentInput, type CreateSourceInput, DEFAULT_LEVEL_NAMES, type DetectSegmentsResult, HIERARCHY_LEVEL_COUNT, type ListErrorsResult, type MappableDimension, type RelinkResult, type RequiredMappingStatus, type SegmentMappingCategory, type SegmentMappingCategoryKeys, type SegmentMappingEntry, type SegmentMappingKeys, type SegmentMappingRow, type SourceAdminTransport, type SourceLayout, type SourceTemplateSummary, type SourceValueAlias, type SourceWithSegments, TallLayoutMappings, type TallLayoutMappingsProps, type TenantVolumeConfig, UnifiedSegmentMapper, type UnifiedSegmentMapperProps, type UpdateSourceInput, type Upload, type UploadError, type UploadErrorRow, type UploadInput, type UploadListParams, type UploadListResponse, type UploadRecord, type UploadStats, type UploadStatus, type ValueAliasEntry, ValueAliasesCard, type ValueAliasesCardProps, VolumeFiles, type VolumeFilesProps, type VolumeFilesTransport, VolumeImport, type VolumeImportProps, type VolumeSource$1 as VolumeImportSource, type VolumeImportTransport, type UploadStatus$1 as VolumeImportUploadStatus, type VolumeSource, VolumeSourceDetail, type VolumeSourceDetailProps, type VolumeSourceKind, type VolumeSourceLayout, type VolumeSourceSegment, VolumeSourcesList, type VolumeSourcesListProps };
527
+ export { AUTOMOTIVE_SLOTS, AUTOMOTIVE_TRANSFORM_LABELS, type AliasStatus, type AliasTarget, type AutomotiveSlot, type ClearSourceDataResult, type CreateSegmentInput, type CreateSourceInput, DEFAULT_LEVEL_NAMES, type DetectSegmentsResult, HIERARCHY_LEVEL_COUNT, type ListErrorsResult, type MappableDimension, type RelinkResult, type RequiredMappingStatus, type SegmentMappingCategory, type SegmentMappingCategoryKeys, type SegmentMappingEntry, type SegmentMappingKeys, type SegmentMappingRow, type SourceAdminTransport, type SourceLayout, type SourceTemplateSummary, type SourceValueAlias, type SourceWithSegments, TallLayoutMappings, type TallLayoutMappingsProps, type TenantVolumeConfig, UnifiedSegmentMapper, type UnifiedSegmentMapperProps, type UpdateSourceInput, type Upload, type UploadError, type UploadErrorRow, type UploadInput, type UploadListParams, type UploadListResponse, type UploadRecord, type UploadStats, type UploadStatus, type ValueAliasEntry, ValueAliasesCard, type ValueAliasesCardProps, VolumeFiles, type VolumeFilesProps, type VolumeFilesTransport, VolumeImport, type VolumeImportProps, type VolumeSource$1 as VolumeImportSource, type VolumeImportTransport, type UploadStatus$1 as VolumeImportUploadStatus, type VolumeSource, VolumeSourceDetail, type VolumeSourceDetailProps, type VolumeSourceKind, type VolumeSourceLayout, type VolumeSourceSegment, VolumeSourcesList, type VolumeSourcesListProps };
package/dist/index.js CHANGED
@@ -786,7 +786,7 @@ import { useCallback as useCallback3, useEffect as useEffect3, useState as useSt
786
786
  import { CheckCircle2, Sparkles } from "lucide-react";
787
787
  import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
788
788
  var EMPTY_FORM = { code: "", name: "", description: "", headerRowIndex: 0, volumeHeaderFormat: "MMM yyyy" };
789
- function VolumeSourcesList({ transport, onOpenSource, renderActiveSourceExtras, className }) {
789
+ function VolumeSourcesList({ transport, onOpenSource, renderActiveSourceExtras, readOnly = false, className }) {
790
790
  const [sources, setSources] = useState3([]);
791
791
  const [activeSourceId, setActiveSourceId] = useState3(null);
792
792
  const [templates, setTemplates] = useState3([]);
@@ -883,7 +883,7 @@ function VolumeSourcesList({ transport, onOpenSource, renderActiveSourceExtras,
883
883
  ] })
884
884
  ] })
885
885
  ] }),
886
- /* @__PURE__ */ jsxs4("div", { className: "cfi-vi-row", children: [
886
+ !readOnly && /* @__PURE__ */ jsxs4("div", { className: "cfi-vi-row", children: [
887
887
  availableTemplates.length > 0 && /* @__PURE__ */ jsxs4(Btn, { variant: "outline", size: "sm", onClick: () => setShowTemplates(true), children: [
888
888
  /* @__PURE__ */ jsx4(Sparkles, { size: 14, "aria-hidden": true }),
889
889
  " Add from template"
@@ -906,7 +906,7 @@ function VolumeSourcesList({ transport, onOpenSource, renderActiveSourceExtras,
906
906
  /* @__PURE__ */ jsxs4("span", { className: "cfi-vi-row", children: [
907
907
  /* @__PURE__ */ jsx4(Badge, { variant: "outline", children: src.code }),
908
908
  !src.isActive && /* @__PURE__ */ jsx4(Badge, { variant: "secondary", children: "Disabled" }),
909
- !isActive && src.kind === "base" && /* @__PURE__ */ jsx4(Btn, { variant: "outline", size: "sm", disabled: activating === src.id, onClick: () => handleActivate(src), children: activating === src.id ? "Activating\u2026" : "Set Active" })
909
+ !readOnly && !isActive && src.kind === "base" && /* @__PURE__ */ jsx4(Btn, { variant: "outline", size: "sm", disabled: activating === src.id, onClick: () => handleActivate(src), children: activating === src.id ? "Activating\u2026" : "Set Active" })
910
910
  ] })
911
911
  ] }) }),
912
912
  /* @__PURE__ */ jsxs4(CardBody, { children: [
@@ -1092,7 +1092,8 @@ function UnifiedSegmentMapper({
1092
1092
  mappableDimensions,
1093
1093
  initialMappings,
1094
1094
  onSave,
1095
- saving
1095
+ saving,
1096
+ locked = false
1096
1097
  }) {
1097
1098
  const dims = useMemo3(
1098
1099
  () => mappableDimensions?.length ? mappableDimensions : CANONICAL_MAPPABLE,
@@ -1132,6 +1133,7 @@ function UnifiedSegmentMapper({
1132
1133
  const dirty = dims.some((d) => (assign[d.key] ?? "") !== (savedAssign[d.key] ?? ""));
1133
1134
  const canSave = dirty && missingRequired.length === 0 && !saving;
1134
1135
  function setOne(dimKey, segId) {
1136
+ if (locked) return;
1135
1137
  setAssign((p) => {
1136
1138
  const next = { ...p, [dimKey]: segId };
1137
1139
  if (segId) {
@@ -1147,12 +1149,13 @@ function UnifiedSegmentMapper({
1147
1149
  });
1148
1150
  }
1149
1151
  function clickColumn(segId) {
1150
- if (!activeDim) return;
1152
+ if (!activeDim || locked) return;
1151
1153
  setOne(activeDim, segId);
1152
1154
  const nextEmpty = [...required, ...optional].find((d) => d.key !== activeDim && !assign[d.key] && d.required);
1153
1155
  setActiveDim(nextEmpty?.key ?? null);
1154
1156
  }
1155
1157
  async function handleSave() {
1158
+ if (locked) return;
1156
1159
  const hierarchy = [];
1157
1160
  const automotive = [];
1158
1161
  const dimensions = [];
@@ -1194,8 +1197,10 @@ function UnifiedSegmentMapper({
1194
1197
  "div",
1195
1198
  {
1196
1199
  className: `cfi-vi-hier-row${isActive ? " cfi-vi-dropzone--active" : ""}`,
1197
- style: { cursor: "pointer", borderRadius: 6 },
1198
- onClick: () => setActiveDim(isActive ? null : d.key),
1200
+ style: { cursor: locked ? "default" : "pointer", borderRadius: 6 },
1201
+ onClick: () => {
1202
+ if (!locked) setActiveDim(isActive ? null : d.key);
1203
+ },
1199
1204
  onDragOver: (e) => {
1200
1205
  e.preventDefault();
1201
1206
  setDropDim(d.key);
@@ -1264,7 +1269,7 @@ function UnifiedSegmentMapper({
1264
1269
  /* @__PURE__ */ jsx5("div", { className: "cfi-vi-mapper-box", children: optional.map(renderRow) })
1265
1270
  ] }),
1266
1271
  /* @__PURE__ */ jsxs5("div", { className: "cfi-vi-mapper-save", children: [
1267
- /* @__PURE__ */ jsx5(Btn, { size: "sm", onClick: handleSave, disabled: !canSave, children: saving ? "Saving\u2026" : "Save Mapping" }),
1272
+ /* @__PURE__ */ jsx5(Btn, { size: "sm", onClick: handleSave, disabled: !canSave || locked, children: saving ? "Saving\u2026" : "Save Mapping" }),
1268
1273
  dirty && missingRequired.length === 0 && !saving && /* @__PURE__ */ jsx5("span", { className: "cfi-vi-mapper-dirty", children: "Unsaved changes" }),
1269
1274
  !dirty && /* @__PURE__ */ jsx5("span", { className: "cfi-vi-muted", children: "Up to date" })
1270
1275
  ] })
@@ -1287,12 +1292,14 @@ function UnifiedSegmentMapper({
1287
1292
  /* @__PURE__ */ jsx5("div", { className: "cfi-vi-pool", children: pool.length === 0 ? /* @__PURE__ */ jsx5("p", { className: "cfi-vi-pool-empty", children: query ? "No columns match the filter." : "All columns assigned." }) : /* @__PURE__ */ jsx5("div", { className: "cfi-vi-chips", children: pool.map((s) => /* @__PURE__ */ jsxs5(
1288
1293
  "div",
1289
1294
  {
1290
- draggable: true,
1291
- onDragStart: () => setDragging(s.id),
1295
+ draggable: !locked,
1296
+ onDragStart: () => {
1297
+ if (!locked) setDragging(s.id);
1298
+ },
1292
1299
  onDragEnd: () => setDragging(null),
1293
1300
  onClick: () => clickColumn(s.id),
1294
1301
  className: "cfi-vi-chip",
1295
- style: { cursor: activeDim ? "pointer" : "grab" },
1302
+ style: { cursor: locked ? "default" : activeDim ? "pointer" : "grab" },
1296
1303
  title: activeDim ? "Click to assign" : "Drag onto a dimension, or select a dimension first",
1297
1304
  children: [
1298
1305
  /* @__PURE__ */ jsx5("div", { className: "cfi-vi-chip-dot" }),
@@ -1306,7 +1313,7 @@ function UnifiedSegmentMapper({
1306
1313
  }
1307
1314
 
1308
1315
  // src/VolumeSourceDetail.tsx
1309
- import { useEffect as useEffect7, useRef as useRef3, useState as useState7 } from "react";
1316
+ import { useEffect as useEffect7, useMemo as useMemo6, useRef as useRef4, useState as useState7 } from "react";
1310
1317
  import { KeyRound, Lock, Rows, Columns, AlertTriangle as AlertTriangle3, Layers, SlidersHorizontal, Tag } from "lucide-react";
1311
1318
 
1312
1319
  // src/TallLayoutMappings.tsx
@@ -1410,18 +1417,32 @@ function TallLayoutMappings({ transport, source, locked }) {
1410
1417
  }
1411
1418
 
1412
1419
  // src/ValueAliasesCard.tsx
1413
- import { useEffect as useEffect6, useMemo as useMemo5, useState as useState6 } from "react";
1414
- import { Plus, Trash2, ArrowRight, Save, RotateCw } from "lucide-react";
1420
+ import { useEffect as useEffect6, useMemo as useMemo5, useRef as useRef3, useState as useState6 } from "react";
1421
+ import { Plus, Trash2, ArrowRight, Save, RotateCw, Check, X as X3 } from "lucide-react";
1415
1422
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1416
- function ValueAliasesCard({ transport, sourceId, locked, isOverlay }) {
1417
- const [selectedLevel, setSelectedLevel] = useState6(4);
1423
+ var HIERARCHY_FALLBACK = [1, 2, 3, 4, 5, 6].map((n) => ({
1424
+ targetCategory: "hierarchy_level",
1425
+ targetKey: `level${n}`,
1426
+ label: `L${n} \u2014 ${DEFAULT_LEVEL_NAMES[n]}`
1427
+ }));
1428
+ var keyOf = (t) => `${t.targetCategory}:${t.targetKey}`;
1429
+ function ValueAliasesCard({ transport, sourceId, locked, isOverlay, targets }) {
1430
+ const targetList = targets && targets.length > 0 ? targets : HIERARCHY_FALLBACK;
1431
+ const [selectedKey, setSelectedKey] = useState6(keyOf(targetList[0]));
1418
1432
  const [aliases, setAliases] = useState6([]);
1419
1433
  const [draft, setDraft] = useState6([]);
1420
1434
  const [saving, setSaving] = useState6(false);
1435
+ const [busyId, setBusyId] = useState6(null);
1421
1436
  const [error, setError] = useState6(null);
1422
1437
  const [loading, setLoading] = useState6(true);
1423
1438
  const [relinking, setRelinking] = useState6(false);
1424
1439
  const [relinkResult, setRelinkResult] = useState6(null);
1440
+ const editedRef = useRef3(false);
1441
+ const selected = useMemo5(
1442
+ () => targetList.find((t) => keyOf(t) === selectedKey) ?? targetList[0],
1443
+ [targetList, selectedKey]
1444
+ );
1445
+ const matchesSelected = (a) => a.targetCategory === selected.targetCategory && a.targetKey === selected.targetKey;
1425
1446
  useEffect6(() => {
1426
1447
  let cancelled = false;
1427
1448
  setLoading(true);
@@ -1439,21 +1460,39 @@ function ValueAliasesCard({ transport, sourceId, locked, isOverlay }) {
1439
1460
  cancelled = true;
1440
1461
  };
1441
1462
  }, [sourceId, transport]);
1463
+ const approved = useMemo5(() => aliases.filter((a) => matchesSelected(a) && a.status === "approved"), [aliases, selected]);
1464
+ const suggested = useMemo5(() => aliases.filter((a) => matchesSelected(a) && a.status === "suggested"), [aliases, selected]);
1442
1465
  useEffect6(() => {
1443
- setDraft(
1444
- aliases.filter((a) => a.level === selectedLevel).map((a) => ({ id: a.id, rawValue: a.rawValue, canonicalValue: a.canonicalValue }))
1445
- );
1446
- }, [aliases, selectedLevel]);
1466
+ editedRef.current = false;
1467
+ }, [selectedKey]);
1468
+ useEffect6(() => {
1469
+ if (!editedRef.current) {
1470
+ setDraft(approved.map((a) => ({ id: a.id, rawValue: a.rawValue, canonicalValue: a.canonicalValue })));
1471
+ }
1472
+ }, [approved]);
1473
+ useEffect6(() => {
1474
+ if (!targetList.some((t) => keyOf(t) === selectedKey)) {
1475
+ setSelectedKey(keyOf(targetList[0]));
1476
+ }
1477
+ }, [targetList, selectedKey]);
1447
1478
  const isDirty = useMemo5(() => {
1448
- const saved = aliases.filter((a) => a.level === selectedLevel).map((a) => `${a.rawValue}${a.canonicalValue}`).sort();
1479
+ const saved = approved.map((a) => `${a.rawValue.trim().toUpperCase()}${a.canonicalValue.trim().toUpperCase()}`).sort();
1449
1480
  const current = draft.filter((d) => d.rawValue.trim() && d.canonicalValue.trim()).map((d) => `${d.rawValue.trim().toUpperCase()}${d.canonicalValue.trim().toUpperCase()}`).sort();
1450
1481
  if (saved.length !== current.length) return true;
1451
- for (let i = 0; i < saved.length; i++) if (saved[i] !== current[i]) return true;
1452
- return false;
1453
- }, [aliases, draft, selectedLevel]);
1454
- const addRow = () => setDraft((p) => [...p, { id: null, rawValue: "", canonicalValue: "" }]);
1455
- const updateRow = (i, patch) => setDraft((p) => p.map((r, idx) => idx === i ? { ...r, ...patch } : r));
1456
- const removeRow = (i) => setDraft((p) => p.filter((_, idx) => idx !== i));
1482
+ return saved.some((s, i) => s !== current[i]);
1483
+ }, [approved, draft]);
1484
+ const addRow = () => {
1485
+ editedRef.current = true;
1486
+ setDraft((p) => [...p, { id: null, rawValue: "", canonicalValue: "" }]);
1487
+ };
1488
+ const updateRow = (i, patch) => {
1489
+ editedRef.current = true;
1490
+ setDraft((p) => p.map((r, idx) => idx === i ? { ...r, ...patch } : r));
1491
+ };
1492
+ const removeRow = (i) => {
1493
+ editedRef.current = true;
1494
+ setDraft((p) => p.filter((_, idx) => idx !== i));
1495
+ };
1457
1496
  async function handleRelink() {
1458
1497
  setRelinking(true);
1459
1498
  setError(null);
@@ -1472,42 +1511,79 @@ function ValueAliasesCard({ transport, sourceId, locked, isOverlay }) {
1472
1511
  setError(null);
1473
1512
  try {
1474
1513
  const entries = draft.filter((d) => d.rawValue.trim() && d.canonicalValue.trim()).map((d) => ({ rawValue: d.rawValue.trim().toUpperCase(), canonicalValue: d.canonicalValue.trim().toUpperCase() }));
1475
- const updated = await transport.replaceLevelValueAliases(sourceId, selectedLevel, entries);
1476
- setAliases((prev) => [...prev.filter((a) => a.level !== selectedLevel), ...updated]);
1514
+ let updated;
1515
+ if (transport.replaceValueAliases) {
1516
+ updated = await transport.replaceValueAliases(sourceId, selected.targetCategory, selected.targetKey, entries);
1517
+ } else {
1518
+ const level = parseInt(selected.targetKey.replace("level", ""), 10);
1519
+ updated = await transport.replaceLevelValueAliases(sourceId, level, entries);
1520
+ }
1521
+ setAliases((prev) => [...prev.filter((a) => !(matchesSelected(a) && a.status === "approved")), ...updated.filter((a) => a.status === "approved")]);
1522
+ editedRef.current = false;
1477
1523
  } catch (e) {
1478
1524
  setError(e instanceof Error ? e.message : "Failed to save aliases");
1479
1525
  } finally {
1480
1526
  setSaving(false);
1481
1527
  }
1482
1528
  }
1529
+ async function review(aliasId, action) {
1530
+ const fn = action === "approve" ? transport.approveValueAlias : transport.rejectValueAlias;
1531
+ if (!fn) return;
1532
+ setBusyId(aliasId);
1533
+ setError(null);
1534
+ try {
1535
+ const updated = await fn(aliasId);
1536
+ setAliases((prev) => prev.map((a) => a.id === aliasId ? updated : a));
1537
+ } catch (e) {
1538
+ setError(e instanceof Error ? e.message : `Failed to ${action}`);
1539
+ } finally {
1540
+ setBusyId(null);
1541
+ }
1542
+ }
1543
+ const canReview = !!transport.approveValueAlias && !!transport.rejectValueAlias;
1483
1544
  if (loading) return /* @__PURE__ */ jsx7("p", { className: "cfi-vi-muted", children: "Loading aliases\u2026" });
1484
1545
  return /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-aliases", children: [
1485
1546
  /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-row cfi-vi-row--between", children: [
1486
1547
  /* @__PURE__ */ jsxs7("div", { children: [
1487
1548
  /* @__PURE__ */ jsx7("h2", { className: "cfi-vi-h2", children: "Value Aliases" }),
1488
1549
  /* @__PURE__ */ jsxs7("p", { className: "cfi-vi-muted", style: { fontSize: "var(--cfi-font-size-xs, 11px)" }, children: [
1489
- "Normalise level values when this source uses different vocabulary than the base/overlay it joins to (e.g. ",
1550
+ "Normalise segment values when this source uses different vocabulary than what it joins to (e.g. ",
1490
1551
  /* @__PURE__ */ jsx7("span", { className: "cfi-vi-mono", children: '"MICHIGAN" \u2192 "MICHIGAN ASSEMBLY"' }),
1491
- "). Applied at match time \u2014 no re-upload."
1552
+ "). Only approved aliases take effect."
1492
1553
  ] })
1493
1554
  ] }),
1494
- /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-field", style: { minWidth: 140 }, children: [
1495
- /* @__PURE__ */ jsx7("label", { className: "cfi-vi-label", children: "Level" }),
1496
- /* @__PURE__ */ jsx7("select", { className: "cfi-vi-input", value: String(selectedLevel), onChange: (e) => setSelectedLevel(parseInt(e.target.value, 10)), children: [1, 2, 3, 4, 5, 6].map((n) => /* @__PURE__ */ jsxs7("option", { value: n, children: [
1497
- "L",
1498
- n,
1499
- " \u2014 ",
1500
- DEFAULT_LEVEL_NAMES[n]
1501
- ] }, n)) })
1555
+ /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-field", style: { minWidth: 200 }, children: [
1556
+ /* @__PURE__ */ jsx7("label", { className: "cfi-vi-label", children: "Segment" }),
1557
+ /* @__PURE__ */ jsx7("select", { className: "cfi-vi-input", value: selectedKey, onChange: (e) => setSelectedKey(e.target.value), children: targetList.map((t) => /* @__PURE__ */ jsx7("option", { value: keyOf(t), children: t.label }, keyOf(t))) })
1502
1558
  ] })
1503
1559
  ] }),
1560
+ canReview && suggested.length > 0 && /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-panel", style: { borderColor: "var(--cfi-color-warning, #f59e0b)" }, children: [
1561
+ /* @__PURE__ */ jsxs7("h3", { className: "cfi-vi-mapper-h3", style: { margin: 0 }, children: [
1562
+ "Suggested (",
1563
+ suggested.length,
1564
+ ")"
1565
+ ] }),
1566
+ /* @__PURE__ */ jsx7("p", { className: "cfi-vi-muted", style: { fontSize: "var(--cfi-font-size-xs, 11px)" }, children: "Populated by an upstream system. Approve to make them take effect, or reject to dismiss." }),
1567
+ /* @__PURE__ */ jsx7("div", { className: "cfi-vi-alias-rows", children: suggested.map((s) => /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-row", children: [
1568
+ /* @__PURE__ */ jsx7("span", { className: "cfi-vi-mono", children: s.rawValue }),
1569
+ /* @__PURE__ */ jsx7(ArrowRight, { size: 14, className: "cfi-vi-muted", "aria-hidden": true }),
1570
+ /* @__PURE__ */ jsx7("span", { className: "cfi-vi-mono", style: { flex: 1 }, children: s.canonicalValue }),
1571
+ s.confidence != null && /* @__PURE__ */ jsxs7("span", { className: "cfi-vi-muted", style: { fontSize: "var(--cfi-font-size-xs, 11px)" }, children: [
1572
+ Math.round(s.confidence * 100),
1573
+ "%"
1574
+ ] }),
1575
+ /* @__PURE__ */ jsxs7(Btn, { variant: "outline", size: "sm", onClick: () => void review(s.id, "approve"), disabled: locked || busyId === s.id, children: [
1576
+ /* @__PURE__ */ jsx7(Check, { size: 14, "aria-hidden": true }),
1577
+ " Approve"
1578
+ ] }),
1579
+ /* @__PURE__ */ jsx7("button", { type: "button", className: "cfi-vi-icon-btn", onClick: () => void review(s.id, "reject"), disabled: locked || busyId === s.id, title: "Reject", children: /* @__PURE__ */ jsx7(X3, { size: 14, "aria-hidden": true }) })
1580
+ ] }, s.id)) })
1581
+ ] }),
1504
1582
  /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-panel", children: [
1505
1583
  draft.length === 0 ? /* @__PURE__ */ jsxs7("p", { className: "cfi-vi-muted", style: { fontStyle: "italic", fontSize: "var(--cfi-font-size-xs, 12px)" }, children: [
1506
- "No aliases at L",
1507
- selectedLevel,
1508
- " (",
1509
- DEFAULT_LEVEL_NAMES[selectedLevel],
1510
- "). Click ",
1584
+ "No approved aliases for ",
1585
+ /* @__PURE__ */ jsx7("strong", { children: selected.label }),
1586
+ ". Click ",
1511
1587
  /* @__PURE__ */ jsx7("strong", { children: "+ Add alias" }),
1512
1588
  " to create one."
1513
1589
  ] }) : /* @__PURE__ */ jsx7("div", { className: "cfi-vi-alias-rows", children: draft.map((row, i) => /* @__PURE__ */ jsxs7("div", { className: "cfi-vi-row", children: [
@@ -1554,7 +1630,7 @@ function ValueAliasesCard({ transport, sourceId, locked, isOverlay }) {
1554
1630
  variant: "outline",
1555
1631
  size: "sm",
1556
1632
  onClick: () => void handleRelink(),
1557
- disabled: relinking || isDirty,
1633
+ disabled: relinking || isDirty || locked,
1558
1634
  title: isDirty ? "Save aliases first, then re-link" : "Re-run overlay \u2192 base resolution against current aliases",
1559
1635
  children: [
1560
1636
  /* @__PURE__ */ jsx7(RotateCw, { size: 14, className: relinking ? "cfi-vi-spin" : "", "aria-hidden": true }),
@@ -1572,7 +1648,7 @@ function ValueAliasesCard({ transport, sourceId, locked, isOverlay }) {
1572
1648
 
1573
1649
  // src/VolumeSourceDetail.tsx
1574
1650
  import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1575
- function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, className }) {
1651
+ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, readOnly = false, className }) {
1576
1652
  const id = sourceId;
1577
1653
  const [source, setSource] = useState7(null);
1578
1654
  const [allSources, setAllSources] = useState7([]);
@@ -1584,7 +1660,7 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, classNa
1584
1660
  const [savingKey, setSavingKey] = useState7(false);
1585
1661
  const [selectedKeyId, setSelectedKeyId] = useState7("__none__");
1586
1662
  const [hasData, setHasData] = useState7(false);
1587
- const fileRef = useRef3(null);
1663
+ const fileRef = useRef4(null);
1588
1664
  const [savingLayout, setSavingLayout] = useState7(false);
1589
1665
  const [editLayout, setEditLayout] = useState7("wide");
1590
1666
  const [editValueDateFormat, setEditValueDateFormat] = useState7("yyyy-MM-dd");
@@ -1746,6 +1822,10 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, classNa
1746
1822
  setSavingLayout(false);
1747
1823
  }
1748
1824
  }
1825
+ const aliasTargets = useMemo6(
1826
+ () => mappingRows.filter((m) => m.targetCategory === "hierarchy_level" || m.targetCategory === "automotive_field" || m.targetCategory === "dimension").map((m) => ({ targetCategory: m.targetCategory, targetKey: m.targetKey, label: `${m.segmentLabel} \xB7 ${m.targetKey}` })),
1827
+ [mappingRows]
1828
+ );
1749
1829
  if (loading) return /* @__PURE__ */ jsx8(Root, { className, children: /* @__PURE__ */ jsx8("p", { className: "cfi-vi-muted", children: "Loading\u2026" }) });
1750
1830
  if (!source) return /* @__PURE__ */ jsx8(Root, { className, children: /* @__PURE__ */ jsx8("p", { className: "cfi-vi-error", children: "Source not found" }) });
1751
1831
  const keySegment = source.segments.find((s) => s.id === source.uniqueKeySegmentId);
@@ -1755,7 +1835,7 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, classNa
1755
1835
  { id: "mapping", label: "Mapping", Icon: Layers },
1756
1836
  { id: "aliases", label: "Aliases", Icon: Tag },
1757
1837
  { id: "danger", label: "Danger zone", Icon: AlertTriangle3 }
1758
- ];
1838
+ ].filter((t) => !readOnly || t.id !== "danger");
1759
1839
  return /* @__PURE__ */ jsx8(Root, { className, children: /* @__PURE__ */ jsxs8("div", { className: "cfi-vi-detail", children: [
1760
1840
  /* @__PURE__ */ jsxs8("div", { children: [
1761
1841
  /* @__PURE__ */ jsxs8("div", { className: "cfi-vi-row", style: { gap: 8 }, children: [
@@ -1845,7 +1925,7 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, classNa
1845
1925
  {
1846
1926
  className: "cfi-vi-input",
1847
1927
  value: editLayout,
1848
- disabled: hasData || savingLayout,
1928
+ disabled: hasData || savingLayout || readOnly,
1849
1929
  onChange: (e) => setEditLayout(e.target.value),
1850
1930
  children: [
1851
1931
  /* @__PURE__ */ jsx8("option", { value: "wide", children: "wide" }),
@@ -1862,13 +1942,13 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, classNa
1862
1942
  className: "cfi-vi-mono",
1863
1943
  value: editValueDateFormat,
1864
1944
  placeholder: "yyyy-MM-dd",
1865
- disabled: hasData || savingLayout,
1945
+ disabled: hasData || savingLayout || readOnly,
1866
1946
  onChange: (e) => setEditValueDateFormat(e.target.value)
1867
1947
  }
1868
1948
  )
1869
1949
  ] })
1870
1950
  ] }),
1871
- /* @__PURE__ */ jsx8(Btn, { size: "sm", onClick: () => void saveLayoutAndRouting(), disabled: hasData || savingLayout, children: savingLayout ? "Saving\u2026" : "Save layout" })
1951
+ /* @__PURE__ */ jsx8(Btn, { size: "sm", onClick: () => void saveLayoutAndRouting(), disabled: hasData || savingLayout || readOnly, children: savingLayout ? "Saving\u2026" : "Save layout" })
1872
1952
  ] }),
1873
1953
  /* @__PURE__ */ jsxs8("div", { className: "cfi-vi-panel", children: [
1874
1954
  /* @__PURE__ */ jsxs8("div", { className: "cfi-vi-row", children: [
@@ -1884,7 +1964,7 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, classNa
1884
1964
  {
1885
1965
  className: "cfi-vi-input",
1886
1966
  value: selectedKeyId,
1887
- disabled: hasData,
1967
+ disabled: hasData || readOnly,
1888
1968
  onChange: (e) => setSelectedKeyId(e.target.value),
1889
1969
  children: [
1890
1970
  /* @__PURE__ */ jsx8("option", { value: "__none__", children: "None \u2014 use display name" }),
@@ -1893,7 +1973,7 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, classNa
1893
1973
  }
1894
1974
  )
1895
1975
  ] }),
1896
- /* @__PURE__ */ jsx8(Btn, { size: "sm", onClick: saveUniqueKey, disabled: savingKey || hasData, children: savingKey ? "Saving\u2026" : "Save" })
1976
+ /* @__PURE__ */ jsx8(Btn, { size: "sm", onClick: saveUniqueKey, disabled: savingKey || hasData || readOnly, children: savingKey ? "Saving\u2026" : "Save" })
1897
1977
  ] })
1898
1978
  ] })
1899
1979
  ] }),
@@ -1904,7 +1984,7 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, classNa
1904
1984
  source.segments.length,
1905
1985
  ")"
1906
1986
  ] }),
1907
- /* @__PURE__ */ jsx8(Btn, { variant: "outline", size: "sm", onClick: () => fileRef.current?.click(), disabled: detecting || hasData, children: detecting ? "Detecting\u2026" : "Detect from file" }),
1987
+ /* @__PURE__ */ jsx8(Btn, { variant: "outline", size: "sm", onClick: () => fileRef.current?.click(), disabled: detecting || hasData || readOnly, children: detecting ? "Detecting\u2026" : "Detect from file" }),
1908
1988
  /* @__PURE__ */ jsx8(
1909
1989
  "input",
1910
1990
  {
@@ -1932,7 +2012,7 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, classNa
1932
2012
  className: "cfi-vi-mono",
1933
2013
  value: newSegmentLabel,
1934
2014
  placeholder: "e.g. Color",
1935
- disabled: hasData || addingSegment,
2015
+ disabled: hasData || addingSegment || readOnly,
1936
2016
  onChange: (e) => setNewSegmentLabel(e.target.value),
1937
2017
  onKeyDown: (e) => {
1938
2018
  if (e.key === "Enter" && newSegmentLabel.trim()) void handleAddSegment();
@@ -1947,7 +2027,7 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, classNa
1947
2027
  {
1948
2028
  className: "cfi-vi-input",
1949
2029
  value: newSegmentDataType,
1950
- disabled: hasData || addingSegment,
2030
+ disabled: hasData || addingSegment || readOnly,
1951
2031
  onChange: (e) => setNewSegmentDataType(e.target.value),
1952
2032
  children: [
1953
2033
  /* @__PURE__ */ jsx8("option", { value: "string", children: "string" }),
@@ -1957,7 +2037,7 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, classNa
1957
2037
  }
1958
2038
  )
1959
2039
  ] }),
1960
- /* @__PURE__ */ jsx8(Btn, { size: "sm", onClick: () => void handleAddSegment(), disabled: hasData || addingSegment || !newSegmentLabel.trim(), children: addingSegment ? "Adding\u2026" : "Add segment" })
2040
+ /* @__PURE__ */ jsx8(Btn, { size: "sm", onClick: () => void handleAddSegment(), disabled: hasData || addingSegment || readOnly || !newSegmentLabel.trim(), children: addingSegment ? "Adding\u2026" : "Add segment" })
1961
2041
  ] })
1962
2042
  ] }),
1963
2043
  source.segments.length === 0 ? /* @__PURE__ */ jsx8("p", { className: "cfi-vi-muted", children: "No segments yet. Upload a sample file to auto-detect." }) : /* @__PURE__ */ jsxs8("table", { className: "cfi-vi-table", children: [
@@ -1983,7 +2063,7 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, classNa
1983
2063
  className: "cfi-vi-icon-btn",
1984
2064
  style: { color: "var(--cfi-color-error, #dc2626)" },
1985
2065
  onClick: () => void handleDelete(seg.id),
1986
- disabled: hasData,
2066
+ disabled: hasData || readOnly,
1987
2067
  title: "Delete segment",
1988
2068
  children: "\xD7"
1989
2069
  }
@@ -1993,7 +2073,7 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, classNa
1993
2073
  ] })
1994
2074
  ] }),
1995
2075
  tab === "mapping" && /* @__PURE__ */ jsxs8("div", { className: "cfi-vi-tabpanel", children: [
1996
- source.layout === "tall" && /* @__PURE__ */ jsx8(TallLayoutMappings, { transport, source, locked: hasData }),
2076
+ source.layout === "tall" && /* @__PURE__ */ jsx8(TallLayoutMappings, { transport, source, locked: hasData || readOnly }),
1997
2077
  /* @__PURE__ */ jsxs8("div", { children: [
1998
2078
  /* @__PURE__ */ jsxs8("div", { className: "cfi-vi-row", children: [
1999
2079
  /* @__PURE__ */ jsx8(Layers, { size: 16, className: "cfi-vi-muted", "aria-hidden": true }),
@@ -2035,13 +2115,14 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, classNa
2035
2115
  mappableDimensions,
2036
2116
  initialMappings: mappingRows,
2037
2117
  onSave: handleSaveMapping,
2038
- saving: savingMapping
2118
+ saving: savingMapping,
2119
+ locked: readOnly
2039
2120
  }
2040
2121
  )
2041
2122
  ] })
2042
2123
  ] }),
2043
- tab === "aliases" && /* @__PURE__ */ jsx8("div", { className: "cfi-vi-tabpanel", children: /* @__PURE__ */ jsx8(ValueAliasesCard, { transport, sourceId: source.id, locked: false, isOverlay }) }),
2044
- tab === "danger" && /* @__PURE__ */ jsx8("div", { className: "cfi-vi-tabpanel", children: /* @__PURE__ */ jsxs8("div", { className: "cfi-vi-danger", children: [
2124
+ tab === "aliases" && /* @__PURE__ */ jsx8("div", { className: "cfi-vi-tabpanel", children: /* @__PURE__ */ jsx8(ValueAliasesCard, { transport, sourceId: source.id, locked: readOnly, isOverlay, targets: aliasTargets }) }),
2125
+ tab === "danger" && !readOnly && /* @__PURE__ */ jsx8("div", { className: "cfi-vi-tabpanel", children: /* @__PURE__ */ jsxs8("div", { className: "cfi-vi-danger", children: [
2045
2126
  /* @__PURE__ */ jsxs8("div", { className: "cfi-vi-row", style: { color: "var(--cfi-color-error, #dc2626)" }, children: [
2046
2127
  /* @__PURE__ */ jsx8(AlertTriangle3, { size: 16, "aria-hidden": true }),
2047
2128
  /* @__PURE__ */ jsx8("h2", { className: "cfi-vi-h2", children: "Danger zone \u2014 clear source data" })
package/package.json CHANGED
@@ -1,43 +1,43 @@
1
- {
2
- "name": "@campfire-interactive/volume-intelligence-ui",
3
- "version": "0.7.0",
4
- "description": "Shared Volume Intelligence UI components (volume import) for the Campfire Suite — embedded by the VI webapp and by consuming apps (OMSF) so there is one import surface, not two that drift.",
5
- "type": "module",
6
- "main": "dist/index.js",
7
- "types": "dist/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./dist/index.d.ts",
11
- "import": "./dist/index.js"
12
- }
13
- },
14
- "files": [
15
- "dist"
16
- ],
17
- "scripts": {
18
- "build": "tsup",
19
- "dev": "tsup --watch"
20
- },
21
- "peerDependencies": {
22
- "react": "^18.0.0 || ^19.0.0",
23
- "react-dom": "^18.0.0 || ^19.0.0"
24
- },
25
- "dependencies": {
26
- "@campfire-interactive/design-tokens": "*",
27
- "lucide-react": "^0.487.0"
28
- },
29
- "devDependencies": {
30
- "@types/react": "^18.2.0",
31
- "react": "^18.2.0",
32
- "react-dom": "^18.2.0",
33
- "tsup": "^8.0.0",
34
- "typescript": "^5.3.3"
35
- },
36
- "publishConfig": {
37
- "registry": "https://registry.npmjs.org",
38
- "access": "public"
39
- },
40
- "engines": {
41
- "node": ">=18.0.0"
42
- }
43
- }
1
+ {
2
+ "name": "@campfire-interactive/volume-intelligence-ui",
3
+ "version": "0.9.1",
4
+ "description": "Shared Volume Intelligence UI components (volume import) for the Campfire Suite — embedded by the VI webapp and by consuming apps (OMSF) so there is one import surface, not two that drift.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsup",
19
+ "dev": "tsup --watch"
20
+ },
21
+ "peerDependencies": {
22
+ "react": "^18.0.0 || ^19.0.0",
23
+ "react-dom": "^18.0.0 || ^19.0.0"
24
+ },
25
+ "dependencies": {
26
+ "@campfire-interactive/design-tokens": "*",
27
+ "lucide-react": "^0.487.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/react": "^18.2.0",
31
+ "react": "^18.2.0",
32
+ "react-dom": "^18.2.0",
33
+ "tsup": "^8.0.0",
34
+ "typescript": "^5.3.3"
35
+ },
36
+ "publishConfig": {
37
+ "registry": "https://registry.npmjs.org",
38
+ "access": "public"
39
+ },
40
+ "engines": {
41
+ "node": ">=18.0.0"
42
+ }
43
+ }