@campfire-interactive/volume-intelligence-ui 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +38 -9
  2. package/dist/index.js +248 -403
  3. package/package.json +43 -43
package/dist/index.d.ts CHANGED
@@ -124,7 +124,7 @@ interface TenantVolumeConfig {
124
124
  sourceId: string;
125
125
  source: SourceWithSegments;
126
126
  }
127
- type SegmentMappingCategory = 'hierarchy_level' | 'automotive_field' | 'period' | 'value';
127
+ type SegmentMappingCategory = 'hierarchy_level' | 'automotive_field' | 'period' | 'value' | 'dimension';
128
128
  interface SegmentMappingRow {
129
129
  id: string;
130
130
  sourceId: string;
@@ -308,6 +308,19 @@ interface SourceAdminTransport {
308
308
  listUploads(params?: UploadListParams): Promise<UploadListResponse>;
309
309
  clearSourceData(sourceId: string, confirm: string): Promise<ClearSourceDataResult>;
310
310
  getRequiredMappingStatus?(sourceId: string): Promise<RequiredMappingStatus>;
311
+ getMappableDimensions?(): Promise<MappableDimension[]>;
312
+ }
313
+ /** One dimension the operator can map a source column to. The category+targetKey
314
+ * say where the mapping is stored (a hierarchy level, an automotive field, or a
315
+ * dim slot); `label` is what the operator sees; `required` drives ordering +
316
+ * validation. The storage detail is deliberately hidden from the UI — the
317
+ * operator just picks a column per dimension. */
318
+ interface MappableDimension {
319
+ key: string;
320
+ label: string;
321
+ required: boolean;
322
+ category: 'hierarchy_level' | 'automotive_field' | 'dimension';
323
+ targetKey: string;
311
324
  }
312
325
 
313
326
  interface VolumeFilesTransport {
@@ -368,6 +381,22 @@ interface VolumeSourcesListProps {
368
381
  */
369
382
  declare function VolumeSourcesList({ transport, onOpenSource, renderActiveSourceExtras, className }: VolumeSourcesListProps): react.JSX.Element;
370
383
 
384
+ /**
385
+ * Segment mapper — a catalog-driven, required-first assignment surface built for
386
+ * large column pools (100+). For each dimension the operator gives a source
387
+ * column; the storage detail (a hierarchy level / automotive field / dim slot)
388
+ * rides on the MappableDimension and is routed on save, never shown.
389
+ *
390
+ * Three ways to fill it, so a big pool stays manageable:
391
+ * 1. AUTO-MATCH on load — fuzzy-matches column names to the still-unmapped
392
+ * dimensions and pre-fills strong hits (tagged "auto", editable).
393
+ * 2. CLICK — click a dimension to focus it, then click a pool column.
394
+ * 3. DRAG — drag a pool column chip onto a dimension.
395
+ * The pool is searchable and hides already-used columns.
396
+ *
397
+ * When the host doesn't supply the tenant catalog, falls back to
398
+ * CANONICAL_MAPPABLE (level1..6 + required automotive) so the mapper still works.
399
+ */
371
400
  interface AutomotiveSlot {
372
401
  key: string;
373
402
  label: string;
@@ -377,11 +406,10 @@ interface AutomotiveSlot {
377
406
  }
378
407
  interface UnifiedSegmentMapperProps {
379
408
  segments: VolumeSourceSegment[];
380
- hierarchyInitial: SegmentMappingRow[];
381
- automotiveInitial: SegmentMappingRow[];
382
- automotiveSlots: AutomotiveSlot[];
383
- transformRules: readonly string[];
384
- transformRuleLabels: Record<string, string>;
409
+ /** The tenant's mappable dimensions; falls back to CANONICAL_MAPPABLE. */
410
+ mappableDimensions?: MappableDimension[];
411
+ /** Existing mappings across all categories, used to prefill the pickers. */
412
+ initialMappings: SegmentMappingRow[];
385
413
  onSave: (hierarchy: Array<{
386
414
  targetKey: string;
387
415
  segmentId: string;
@@ -389,12 +417,13 @@ interface UnifiedSegmentMapperProps {
389
417
  targetKey: string;
390
418
  segmentId: string;
391
419
  transformRule?: string | null;
420
+ }>, dimensions: Array<{
421
+ targetKey: string;
422
+ segmentId: string;
392
423
  }>) => Promise<void>;
393
424
  saving: boolean;
394
- hierarchyMode?: 'tree' | 'sparse';
395
- baseHierarchyHint?: Partial<Record<number, string>>;
396
425
  }
397
- declare function UnifiedSegmentMapper({ segments, hierarchyInitial, automotiveInitial, automotiveSlots, transformRules, transformRuleLabels, onSave, saving, hierarchyMode, baseHierarchyHint, }: UnifiedSegmentMapperProps): react.JSX.Element;
426
+ declare function UnifiedSegmentMapper({ segments, mappableDimensions, initialMappings, onSave, saving, }: UnifiedSegmentMapperProps): react.JSX.Element;
398
427
 
399
428
  interface VolumeSourceDetailProps {
400
429
  transport: SourceAdminTransport;
package/dist/index.js CHANGED
@@ -997,7 +997,7 @@ function VolumeSourcesList({ transport, onOpenSource, renderActiveSourceExtras,
997
997
 
998
998
  // src/UnifiedSegmentMapper.tsx
999
999
  import { useEffect as useEffect4, useMemo as useMemo3, useState as useState4 } from "react";
1000
- import { Search, CheckCircle2 as CheckCircle22 } from "lucide-react";
1000
+ import { Search, Wand2 } from "lucide-react";
1001
1001
 
1002
1002
  // src/constants.ts
1003
1003
  var HIERARCHY_LEVEL_COUNT = 6;
@@ -1025,259 +1025,255 @@ var AUTOMOTIVE_TRANSFORM_LABELS = {
1025
1025
  normalise_country: "Convert country code (e.g. USA \u2192 US)",
1026
1026
  parse_iso_date: "Parse to ISO date (YYYY-MM-DD)"
1027
1027
  };
1028
+ var CANONICAL_MAPPABLE = [
1029
+ { key: "oem", label: "OEM", required: true, category: "hierarchy_level", targetKey: "level1" },
1030
+ { key: "platform", label: "Platform", required: true, category: "hierarchy_level", targetKey: "level2" },
1031
+ { key: "program", label: "Program", required: true, category: "hierarchy_level", targetKey: "level3" },
1032
+ { key: "plant", label: "Plant", required: true, category: "hierarchy_level", targetKey: "level4" },
1033
+ { key: "nameplate", label: "Nameplate", required: true, category: "hierarchy_level", targetKey: "level5" },
1034
+ { key: "trim", label: "Trim Level", required: false, category: "hierarchy_level", targetKey: "level6" },
1035
+ { key: "region", label: "Region", required: true, category: "automotive_field", targetKey: "region" },
1036
+ { key: "plant_country", label: "Plant Country", required: true, category: "automotive_field", targetKey: "plant_country" },
1037
+ { key: "sop_date", label: "SOP (Start of Production)", required: true, category: "automotive_field", targetKey: "sop_date" },
1038
+ { key: "eop_date", label: "EOP (End of Production)", required: true, category: "automotive_field", targetKey: "eop_date" },
1039
+ { key: "brand", label: "Brand", required: false, category: "automotive_field", targetKey: "brand" }
1040
+ ];
1028
1041
 
1029
1042
  // src/UnifiedSegmentMapper.tsx
1030
1043
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1031
- var LEVELS = [1, 2, 3, 4, 5, 6];
1044
+ var autoTransform = (targetKey) => AUTOMOTIVE_SLOTS.find((s) => s.key === targetKey)?.defaultTransform ?? null;
1045
+ var norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
1046
+ var toks = (s) => s.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
1047
+ function score(dim, colLabel) {
1048
+ const c = norm(colLabel);
1049
+ if (!c) return 0;
1050
+ let best = 0;
1051
+ for (const name of [dim.label, dim.key, dim.targetKey]) {
1052
+ const d = norm(name);
1053
+ if (!d) continue;
1054
+ if (d === c) best = Math.max(best, 100);
1055
+ else if (c.includes(d) || d.includes(c)) best = Math.max(best, 75);
1056
+ }
1057
+ const ct = new Set(toks(colLabel));
1058
+ const dt = toks(dim.label);
1059
+ if (dt.length) {
1060
+ const overlap = dt.filter((t) => ct.has(t)).length / dt.length;
1061
+ best = Math.max(best, Math.round(overlap * 60));
1062
+ }
1063
+ return best;
1064
+ }
1065
+ var AUTO_THRESHOLD = 70;
1066
+ function autoMatch(dims, segments, existing) {
1067
+ const used = new Set(Object.values(existing).filter(Boolean));
1068
+ const picks = {};
1069
+ const autoKeys = /* @__PURE__ */ new Set();
1070
+ const empty = dims.filter((d) => !existing[d.key]).sort((a, b) => a.required === b.required ? 0 : a.required ? -1 : 1);
1071
+ for (const d of empty) {
1072
+ let bestId = null;
1073
+ let bestScore = AUTO_THRESHOLD - 1;
1074
+ for (const s of segments) {
1075
+ if (used.has(s.id)) continue;
1076
+ const sc = score(d, s.label);
1077
+ if (sc > bestScore) {
1078
+ bestScore = sc;
1079
+ bestId = s.id;
1080
+ }
1081
+ }
1082
+ if (bestId) {
1083
+ picks[d.key] = bestId;
1084
+ used.add(bestId);
1085
+ autoKeys.add(d.key);
1086
+ }
1087
+ }
1088
+ return { picks, autoKeys };
1089
+ }
1032
1090
  function UnifiedSegmentMapper({
1033
1091
  segments,
1034
- hierarchyInitial,
1035
- automotiveInitial,
1036
- automotiveSlots,
1037
- transformRules,
1038
- transformRuleLabels,
1092
+ mappableDimensions,
1093
+ initialMappings,
1039
1094
  onSave,
1040
- saving,
1041
- hierarchyMode = "tree",
1042
- baseHierarchyHint
1095
+ saving
1043
1096
  }) {
1044
- const isSparse = hierarchyMode === "sparse";
1045
- const [hierAssign, setHierAssign] = useState4({});
1046
- const [autoAssign, setAutoAssign] = useState4({});
1047
- const [searchQuery, setSearchQuery] = useState4("");
1097
+ const dims = useMemo3(
1098
+ () => mappableDimensions?.length ? mappableDimensions : CANONICAL_MAPPABLE,
1099
+ [mappableDimensions]
1100
+ );
1101
+ const [assign, setAssign] = useState4({});
1102
+ const [savedAssign, setSavedAssign] = useState4({});
1103
+ const [autoKeys, setAutoKeys] = useState4(/* @__PURE__ */ new Set());
1104
+ const [activeDim, setActiveDim] = useState4(null);
1105
+ const [query, setQuery] = useState4("");
1048
1106
  const [dragging, setDragging] = useState4(null);
1049
- const [dropTarget, setDropTarget] = useState4(null);
1107
+ const [dropDim, setDropDim] = useState4(null);
1050
1108
  useEffect4(() => {
1051
- const initHier = {};
1052
- for (const lv of LEVELS) {
1053
- initHier[lv] = hierarchyInitial.find((m) => m.targetKey === `level${lv}`)?.segmentId ?? null;
1109
+ const existing = {};
1110
+ const claimed = /* @__PURE__ */ new Set();
1111
+ for (const d of dims) {
1112
+ const segId = initialMappings.find((m) => m.targetCategory === d.category && m.targetKey === d.targetKey)?.segmentId ?? "";
1113
+ existing[d.key] = segId && !claimed.has(segId) ? segId : "";
1114
+ if (existing[d.key]) claimed.add(segId);
1054
1115
  }
1055
- setHierAssign(initHier);
1056
- const initAuto = {};
1057
- for (const slot of automotiveSlots) {
1058
- const existing = automotiveInitial.find((m) => m.targetKey === slot.key);
1059
- initAuto[slot.key] = {
1060
- segmentId: existing?.segmentId ?? null,
1061
- transformRule: existing?.transformRule ?? slot.defaultTransform ?? null
1062
- };
1063
- }
1064
- setAutoAssign(initAuto);
1065
- }, [hierarchyInitial, automotiveInitial, automotiveSlots]);
1116
+ const { picks, autoKeys: keys } = autoMatch(dims, segments, existing);
1117
+ setAssign({ ...existing, ...picks });
1118
+ setSavedAssign(existing);
1119
+ setAutoKeys(keys);
1120
+ setActiveDim(null);
1121
+ }, [initialMappings, mappableDimensions, segments]);
1066
1122
  const segmentById = useMemo3(() => new Map(segments.map((s) => [s.id, s])), [segments]);
1067
- const usedIds = useMemo3(() => {
1068
- const s = /* @__PURE__ */ new Set();
1069
- for (const v of Object.values(hierAssign)) if (v) s.add(v);
1070
- for (const a of Object.values(autoAssign)) if (a.segmentId) s.add(a.segmentId);
1071
- return s;
1072
- }, [hierAssign, autoAssign]);
1073
- const poolSegments = useMemo3(() => {
1074
- const q = searchQuery.trim().toLowerCase();
1123
+ const usedIds = useMemo3(() => new Set(Object.values(assign).filter(Boolean)), [assign]);
1124
+ const pool = useMemo3(() => {
1125
+ const q = query.trim().toLowerCase();
1075
1126
  return segments.filter((s) => !usedIds.has(s.id)).filter((s) => !q || s.label.toLowerCase().includes(q)).sort((a, b) => a.label.localeCompare(b.label));
1076
- }, [segments, usedIds, searchQuery]);
1077
- const hierarchyCoveredKeys = useMemo3(() => {
1078
- const covered = /* @__PURE__ */ new Set();
1079
- for (const slot of automotiveSlots) {
1080
- if (slot.hierarchyCoveredByLevel == null) continue;
1081
- const lvSegId = hierAssign[slot.hierarchyCoveredByLevel];
1082
- if (lvSegId && !autoAssign[slot.key]?.segmentId) covered.add(slot.key);
1083
- }
1084
- return covered;
1085
- }, [automotiveSlots, hierAssign, autoAssign]);
1086
- const hierarchyErrors = useMemo3(() => {
1087
- if (isSparse) return [];
1088
- const errors = [];
1089
- for (let n = 2; n <= 6; n++) {
1090
- if (hierAssign[n] && !hierAssign[n - 1]) {
1091
- errors.push(`${DEFAULT_LEVEL_NAMES[n]} requires ${DEFAULT_LEVEL_NAMES[n - 1]} to be set first`);
1092
- }
1093
- }
1094
- return errors;
1095
- }, [hierAssign, isSparse]);
1096
- const missingAutomotive = useMemo3(() => {
1097
- const out = [];
1098
- for (const slot of automotiveSlots) {
1099
- if (hierarchyCoveredKeys.has(slot.key)) continue;
1100
- const explicitRequired = slot.required || slot.hierarchyCoveredByLevel != null && hierAssign[slot.hierarchyCoveredByLevel] == null;
1101
- if (explicitRequired && !autoAssign[slot.key]?.segmentId) out.push(slot.label);
1102
- }
1103
- return out;
1104
- }, [automotiveSlots, hierarchyCoveredKeys, hierAssign, autoAssign]);
1105
- const isDirty = useMemo3(() => {
1106
- for (const lv of LEVELS) {
1107
- const cur = hierAssign[lv] ?? null;
1108
- const saved = hierarchyInitial.find((m) => m.targetKey === `level${lv}`)?.segmentId ?? null;
1109
- if (cur !== saved) return true;
1110
- }
1111
- for (const slot of automotiveSlots) {
1112
- const cur = autoAssign[slot.key] ?? { segmentId: null, transformRule: null };
1113
- const saved = automotiveInitial.find((m) => m.targetKey === slot.key);
1114
- const savedSeg = saved?.segmentId ?? null;
1115
- const savedRule = saved?.transformRule ?? slot.defaultTransform ?? null;
1116
- if (cur.segmentId !== savedSeg || cur.transformRule !== savedRule) return true;
1117
- }
1118
- return false;
1119
- }, [hierAssign, autoAssign, hierarchyInitial, automotiveInitial, automotiveSlots]);
1120
- const canSave = isDirty && hierarchyErrors.length === 0 && missingAutomotive.length === 0 && !saving;
1121
- function startDrag(payload) {
1122
- setDragging(payload);
1123
- }
1124
- function endDrag() {
1125
- setDragging(null);
1126
- setDropTarget(null);
1127
- }
1128
- function dropOnHier(level, payload) {
1129
- const parts = payload.split(":");
1130
- const kind = parts[0];
1131
- const segId = parts[2] ?? parts[1];
1132
- setHierAssign((prev) => {
1133
- const next = { ...prev };
1134
- const displaced = next[level] ?? null;
1135
- next[level] = segId;
1136
- if (kind === "hier") {
1137
- const fromLevel = parseInt(parts[1], 10);
1138
- next[fromLevel] = displaced;
1127
+ }, [segments, usedIds, query]);
1128
+ const required = useMemo3(() => dims.filter((d) => d.required), [dims]);
1129
+ const optional = useMemo3(() => dims.filter((d) => !d.required), [dims]);
1130
+ const missingRequired = required.filter((d) => !assign[d.key]).map((d) => d.label);
1131
+ const autoCount = useMemo3(() => [...autoKeys].filter((k) => !!assign[k]).length, [autoKeys, assign]);
1132
+ const dirty = dims.some((d) => (assign[d.key] ?? "") !== (savedAssign[d.key] ?? ""));
1133
+ const canSave = dirty && missingRequired.length === 0 && !saving;
1134
+ function setOne(dimKey, segId) {
1135
+ setAssign((p) => {
1136
+ const next = { ...p, [dimKey]: segId };
1137
+ if (segId) {
1138
+ for (const k of Object.keys(next)) if (k !== dimKey && next[k] === segId) next[k] = "";
1139
1139
  }
1140
1140
  return next;
1141
1141
  });
1142
- if (kind === "auto") {
1143
- const slotKey = parts[1];
1144
- setAutoAssign((p) => ({ ...p, [slotKey]: { segmentId: null, transformRule: p[slotKey]?.transformRule ?? null } }));
1145
- }
1146
- endDrag();
1147
- }
1148
- function dropOnAuto(slotKey, payload) {
1149
- const parts = payload.split(":");
1150
- const kind = parts[0];
1151
- const segId = parts[2] ?? parts[1];
1152
- setAutoAssign((prev) => {
1153
- const slot = automotiveSlots.find((s) => s.key === slotKey);
1154
- return {
1155
- ...prev,
1156
- [slotKey]: { segmentId: segId, transformRule: prev[slotKey]?.transformRule ?? slot?.defaultTransform ?? null }
1157
- };
1142
+ setAutoKeys((prev) => {
1143
+ if (!prev.has(dimKey)) return prev;
1144
+ const next = new Set(prev);
1145
+ next.delete(dimKey);
1146
+ return next;
1158
1147
  });
1159
- if (kind === "hier") {
1160
- const fromLevel = parseInt(parts[1], 10);
1161
- setHierAssign((p) => ({ ...p, [fromLevel]: null }));
1162
- } else if (kind === "auto") {
1163
- const fromKey = parts[1];
1164
- if (fromKey !== slotKey) {
1165
- setAutoAssign((p) => ({ ...p, [fromKey]: { segmentId: null, transformRule: p[fromKey]?.transformRule ?? null } }));
1166
- }
1167
- }
1168
- endDrag();
1169
1148
  }
1170
- function dropOnPool(payload) {
1171
- const parts = payload.split(":");
1172
- const kind = parts[0];
1173
- if (kind === "hier") {
1174
- const lv = parseInt(parts[1], 10);
1175
- setHierAssign((p) => ({ ...p, [lv]: null }));
1176
- } else if (kind === "auto") {
1177
- const slotKey = parts[1];
1178
- setAutoAssign((p) => ({ ...p, [slotKey]: { segmentId: null, transformRule: p[slotKey]?.transformRule ?? null } }));
1179
- }
1180
- endDrag();
1181
- }
1182
- function clearAuto(slotKey) {
1183
- setAutoAssign((p) => ({ ...p, [slotKey]: { segmentId: null, transformRule: p[slotKey]?.transformRule ?? null } }));
1184
- }
1185
- function setAutoTransform(slotKey, rule) {
1186
- setAutoAssign((p) => ({ ...p, [slotKey]: { ...p[slotKey], transformRule: rule } }));
1149
+ function clickColumn(segId) {
1150
+ if (!activeDim) return;
1151
+ setOne(activeDim, segId);
1152
+ const nextEmpty = [...required, ...optional].find((d) => d.key !== activeDim && !assign[d.key] && d.required);
1153
+ setActiveDim(nextEmpty?.key ?? null);
1187
1154
  }
1188
1155
  async function handleSave() {
1189
- const hierEntries = [];
1190
- for (const lv of LEVELS) {
1191
- const segId = hierAssign[lv];
1192
- if (segId) hierEntries.push({ targetKey: `level${lv}`, segmentId: segId });
1156
+ const hierarchy = [];
1157
+ const automotive = [];
1158
+ const dimensions = [];
1159
+ const managedH = new Set(dims.filter((d) => d.category === "hierarchy_level").map((d) => d.targetKey));
1160
+ const managedA = new Set(dims.filter((d) => d.category === "automotive_field").map((d) => d.targetKey));
1161
+ const managedD = new Set(dims.filter((d) => d.category === "dimension").map((d) => d.targetKey));
1162
+ const existingTransform = /* @__PURE__ */ new Map();
1163
+ for (const m of initialMappings) {
1164
+ if (m.targetCategory === "automotive_field") existingTransform.set(m.targetKey, m.transformRule);
1165
+ if (m.targetCategory === "hierarchy_level" && !managedH.has(m.targetKey))
1166
+ hierarchy.push({ targetKey: m.targetKey, segmentId: m.segmentId });
1167
+ else if (m.targetCategory === "automotive_field" && !managedA.has(m.targetKey))
1168
+ automotive.push({ targetKey: m.targetKey, segmentId: m.segmentId, transformRule: m.transformRule });
1169
+ else if (m.targetCategory === "dimension" && !managedD.has(m.targetKey))
1170
+ dimensions.push({ targetKey: m.targetKey, segmentId: m.segmentId });
1193
1171
  }
1194
- const autoEntries = [];
1195
- for (const slot of automotiveSlots) {
1196
- const a = autoAssign[slot.key];
1197
- if (a?.segmentId) {
1198
- autoEntries.push({ targetKey: slot.key, segmentId: a.segmentId, transformRule: a.transformRule ?? slot.defaultTransform ?? null });
1199
- }
1172
+ for (const d of dims) {
1173
+ const segmentId = assign[d.key];
1174
+ if (!segmentId) continue;
1175
+ if (d.category === "hierarchy_level") hierarchy.push({ targetKey: d.targetKey, segmentId });
1176
+ else if (d.category === "automotive_field")
1177
+ automotive.push({
1178
+ targetKey: d.targetKey,
1179
+ segmentId,
1180
+ transformRule: existingTransform.has(d.targetKey) ? existingTransform.get(d.targetKey) : autoTransform(d.targetKey)
1181
+ });
1182
+ else dimensions.push({ targetKey: d.targetKey, segmentId });
1200
1183
  }
1201
- await onSave(hierEntries, autoEntries);
1184
+ await onSave(hierarchy, automotive, dimensions);
1185
+ setSavedAssign({ ...assign });
1186
+ setAutoKeys(/* @__PURE__ */ new Set());
1187
+ }
1188
+ function renderRow(d) {
1189
+ const segId = assign[d.key];
1190
+ const seg = segId ? segmentById.get(segId) : void 0;
1191
+ const isActive = activeDim === d.key;
1192
+ const isDrop = dropDim === d.key;
1193
+ return /* @__PURE__ */ jsxs5(
1194
+ "div",
1195
+ {
1196
+ className: `cfi-vi-hier-row${isActive ? " cfi-vi-dropzone--active" : ""}`,
1197
+ style: { cursor: "pointer", borderRadius: 6 },
1198
+ onClick: () => setActiveDim(isActive ? null : d.key),
1199
+ onDragOver: (e) => {
1200
+ e.preventDefault();
1201
+ setDropDim(d.key);
1202
+ },
1203
+ onDragLeave: (e) => {
1204
+ if (!e.currentTarget.contains(e.relatedTarget)) setDropDim(null);
1205
+ },
1206
+ onDrop: (e) => {
1207
+ e.preventDefault();
1208
+ if (dragging) {
1209
+ setOne(d.key, dragging);
1210
+ setDragging(null);
1211
+ }
1212
+ setDropDim(null);
1213
+ },
1214
+ children: [
1215
+ /* @__PURE__ */ jsx5("div", { className: `cfi-vi-hier-dot ${seg ? "cfi-vi-hier-dot--filled" : ""}` }),
1216
+ /* @__PURE__ */ jsx5("span", { className: "cfi-vi-hier-name", style: { minWidth: 150 }, children: d.label }),
1217
+ d.required && !seg && /* @__PURE__ */ jsx5(Badge, { variant: "outline", children: "required" }),
1218
+ /* @__PURE__ */ jsx5("div", { className: `cfi-vi-dropzone ${isDrop ? "cfi-vi-dropzone--active" : seg ? "cfi-vi-dropzone--filled" : "cfi-vi-dropzone--empty"}`, style: { flex: 1 }, children: seg ? /* @__PURE__ */ jsxs5("div", { className: "cfi-vi-chip--assigned", children: [
1219
+ /* @__PURE__ */ jsx5("div", { className: "cfi-vi-chip-dot cfi-vi-chip-dot--on" }),
1220
+ /* @__PURE__ */ jsx5("span", { className: "cfi-vi-chip-label", children: seg.label }),
1221
+ autoKeys.has(d.key) && /* @__PURE__ */ jsx5("span", { className: "cfi-vi-auto-auto-tag", children: "auto" }),
1222
+ /* @__PURE__ */ jsx5(
1223
+ "button",
1224
+ {
1225
+ type: "button",
1226
+ className: "cfi-vi-chip-x",
1227
+ title: "Unassign \u2014 returns to pool",
1228
+ onClick: (e) => {
1229
+ e.stopPropagation();
1230
+ setOne(d.key, "");
1231
+ },
1232
+ children: "\xD7"
1233
+ }
1234
+ )
1235
+ ] }) : /* @__PURE__ */ jsx5("span", { className: "cfi-vi-dropzone-ph", children: isActive ? "Click a column \u2192" : isDrop ? "Drop here" : "Drag or click a column\u2026" }) })
1236
+ ]
1237
+ },
1238
+ d.key
1239
+ );
1240
+ }
1241
+ if (segments.length === 0) {
1242
+ return /* @__PURE__ */ jsx5("div", { className: "cfi-vi", children: /* @__PURE__ */ jsx5("p", { className: "cfi-vi-pool-empty", children: "No columns detected. Run \u201CDetect segments\u201D from the Structure tab first." }) });
1202
1243
  }
1203
- void transformRules;
1204
1244
  return /* @__PURE__ */ jsxs5("div", { className: "cfi-vi cfi-vi-mapper", children: [
1205
1245
  /* @__PURE__ */ jsxs5("div", { className: "cfi-vi-mapper-col", children: [
1246
+ /* @__PURE__ */ jsx5("div", { className: "cfi-vi-mapper-section-head", children: /* @__PURE__ */ jsxs5("span", { className: "cfi-vi-mapper-note", style: { display: "inline-flex", alignItems: "center", gap: 6 }, children: [
1247
+ /* @__PURE__ */ jsx5(Wand2, { size: 13, "aria-hidden": true }),
1248
+ " Auto-matched ",
1249
+ autoCount,
1250
+ " \xB7 ",
1251
+ missingRequired.length,
1252
+ " required still need a column"
1253
+ ] }) }),
1206
1254
  /* @__PURE__ */ jsxs5("section", { className: "cfi-vi-mapper-section", children: [
1207
- /* @__PURE__ */ jsxs5("div", { className: "cfi-vi-mapper-section-head", children: [
1208
- /* @__PURE__ */ jsx5("h3", { className: "cfi-vi-mapper-h3", children: "Hierarchy Tree" }),
1209
- /* @__PURE__ */ jsx5("span", { className: "cfi-vi-mapper-note", children: isSparse ? "Drag chips onto whichever levels your file actually has. Empty levels are fine." : "Drag a chip onto a level. L(n) needs L(n\u22121) first." })
1210
- ] }),
1211
- /* @__PURE__ */ jsx5("div", { className: "cfi-vi-mapper-box", children: LEVELS.map((level) => /* @__PURE__ */ jsx5(
1212
- HierarchyRow,
1213
- {
1214
- level,
1215
- segId: hierAssign[level] ?? null,
1216
- segmentById,
1217
- isDropTarget: dropTarget === `hier:${level}`,
1218
- onDragOver: () => setDropTarget(`hier:${level}`),
1219
- onDragLeave: () => setDropTarget(null),
1220
- onDrop: () => dragging && dropOnHier(level, dragging),
1221
- onChipDragStart: (segId) => startDrag(`hier:${level}:${segId}`),
1222
- onChipDragEnd: endDrag,
1223
- onChipRemove: () => setHierAssign((p) => ({ ...p, [level]: null })),
1224
- sparse: isSparse,
1225
- baseHint: baseHierarchyHint?.[level]
1226
- },
1227
- level
1228
- )) }),
1229
- hierarchyErrors.length > 0 && /* @__PURE__ */ jsx5("div", { children: hierarchyErrors.map((err) => /* @__PURE__ */ jsxs5("p", { className: "cfi-vi-mapper-err", children: [
1230
- "\u26A0 ",
1231
- err
1232
- ] }, err)) })
1233
- ] }),
1234
- automotiveSlots.length > 0 && /* @__PURE__ */ jsxs5("section", { className: "cfi-vi-mapper-section", children: [
1235
- /* @__PURE__ */ jsxs5("div", { className: "cfi-vi-mapper-section-head", children: [
1236
- /* @__PURE__ */ jsx5("h3", { className: "cfi-vi-mapper-h3", children: "Automotive Fields" }),
1237
- /* @__PURE__ */ jsx5("span", { className: "cfi-vi-mapper-note", children: "Brand / Nameplate / Plant Name come from the hierarchy unless overridden." })
1238
- ] }),
1239
- /* @__PURE__ */ jsx5("div", { className: "cfi-vi-mapper-box", children: automotiveSlots.map((slot) => {
1240
- const covered = hierarchyCoveredKeys.has(slot.key);
1241
- const hierarchySegId = slot.hierarchyCoveredByLevel != null ? hierAssign[slot.hierarchyCoveredByLevel] : null;
1242
- const assignment = autoAssign[slot.key];
1243
- return /* @__PURE__ */ jsx5(
1244
- AutomotiveRow,
1245
- {
1246
- slot,
1247
- covered,
1248
- hierarchySegment: covered && hierarchySegId ? segmentById.get(hierarchySegId) : void 0,
1249
- assignment,
1250
- segmentById,
1251
- transformRuleLabels,
1252
- hierarchyEmpty: slot.hierarchyCoveredByLevel != null && !hierAssign[slot.hierarchyCoveredByLevel],
1253
- isDropTarget: dropTarget === `auto:${slot.key}`,
1254
- onDragOver: () => setDropTarget(`auto:${slot.key}`),
1255
- onDragLeave: () => setDropTarget(null),
1256
- onDrop: () => dragging && dropOnAuto(slot.key, dragging),
1257
- onChipDragStart: (segId) => startDrag(`auto:${slot.key}:${segId}`),
1258
- onChipDragEnd: endDrag,
1259
- onChipRemove: () => clearAuto(slot.key),
1260
- onTransformChange: (rule) => setAutoTransform(slot.key, rule)
1261
- },
1262
- slot.key
1263
- );
1264
- }) }),
1265
- missingAutomotive.length > 0 && /* @__PURE__ */ jsxs5("p", { className: "cfi-vi-mapper-err", children: [
1266
- "\u26A0 Required: ",
1267
- missingAutomotive.join(", ")
1255
+ /* @__PURE__ */ jsx5("div", { className: "cfi-vi-mapper-section-head", children: /* @__PURE__ */ jsx5("h3", { className: "cfi-vi-mapper-h3", children: "Required" }) }),
1256
+ /* @__PURE__ */ jsx5("div", { className: "cfi-vi-mapper-box", children: required.map(renderRow) }),
1257
+ missingRequired.length > 0 && /* @__PURE__ */ jsxs5("p", { className: "cfi-vi-mapper-err", children: [
1258
+ "\u26A0 Still needed: ",
1259
+ missingRequired.join(", ")
1268
1260
  ] })
1269
1261
  ] }),
1262
+ optional.length > 0 && /* @__PURE__ */ jsxs5("section", { className: "cfi-vi-mapper-section", children: [
1263
+ /* @__PURE__ */ jsx5("div", { className: "cfi-vi-mapper-section-head", children: /* @__PURE__ */ jsx5("h3", { className: "cfi-vi-mapper-h3", children: "Optional" }) }),
1264
+ /* @__PURE__ */ jsx5("div", { className: "cfi-vi-mapper-box", children: optional.map(renderRow) })
1265
+ ] }),
1270
1266
  /* @__PURE__ */ jsxs5("div", { className: "cfi-vi-mapper-save", children: [
1271
1267
  /* @__PURE__ */ jsx5(Btn, { size: "sm", onClick: handleSave, disabled: !canSave, children: saving ? "Saving\u2026" : "Save Mapping" }),
1272
- isDirty && hierarchyErrors.length === 0 && missingAutomotive.length === 0 && !saving && /* @__PURE__ */ jsx5("span", { className: "cfi-vi-mapper-dirty", children: "Unsaved changes" }),
1273
- !isDirty && /* @__PURE__ */ jsx5("span", { className: "cfi-vi-muted", children: "Up to date" })
1268
+ dirty && missingRequired.length === 0 && !saving && /* @__PURE__ */ jsx5("span", { className: "cfi-vi-mapper-dirty", children: "Unsaved changes" }),
1269
+ !dirty && /* @__PURE__ */ jsx5("span", { className: "cfi-vi-muted", children: "Up to date" })
1274
1270
  ] })
1275
1271
  ] }),
1276
1272
  /* @__PURE__ */ jsxs5("aside", { className: "cfi-vi-pool-col", children: [
1277
1273
  /* @__PURE__ */ jsxs5("div", { className: "cfi-vi-mapper-section-head", children: [
1278
- /* @__PURE__ */ jsx5("h3", { className: "cfi-vi-mapper-h3", children: "Segment Pool" }),
1274
+ /* @__PURE__ */ jsx5("h3", { className: "cfi-vi-mapper-h3", children: "Columns" }),
1279
1275
  /* @__PURE__ */ jsxs5("span", { className: "cfi-vi-mapper-note", children: [
1280
- poolSegments.length,
1276
+ pool.length,
1281
1277
  " of ",
1282
1278
  segments.length,
1283
1279
  " unused"
@@ -1285,158 +1281,29 @@ function UnifiedSegmentMapper({
1285
1281
  ] }),
1286
1282
  /* @__PURE__ */ jsxs5("div", { className: "cfi-vi-search", children: [
1287
1283
  /* @__PURE__ */ jsx5(Search, { size: 14, className: "cfi-vi-search-icon", "aria-hidden": true }),
1288
- /* @__PURE__ */ jsx5(TextInput, { type: "search", placeholder: "Filter segments\u2026", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value) })
1284
+ /* @__PURE__ */ jsx5(TextInput, { type: "search", placeholder: "Filter columns\u2026", value: query, onChange: (e) => setQuery(e.target.value) })
1289
1285
  ] }),
1290
- /* @__PURE__ */ jsx5(
1286
+ /* @__PURE__ */ jsx5("p", { className: "cfi-vi-mapper-note", children: activeDim ? `Click a column to assign it to \u201C${dims.find((d) => d.key === activeDim)?.label}\u201D.` : "Click a dimension to select it, then click a column \u2014 or drag a column onto a dimension." }),
1287
+ /* @__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(
1291
1288
  "div",
1292
1289
  {
1293
- className: `cfi-vi-pool ${dropTarget === "pool" ? "cfi-vi-pool--active" : ""}`,
1294
- onDragOver: (e) => {
1295
- e.preventDefault();
1296
- setDropTarget("pool");
1297
- },
1298
- onDragLeave: (e) => {
1299
- if (!e.currentTarget.contains(e.relatedTarget)) setDropTarget(null);
1300
- },
1301
- onDrop: (e) => {
1302
- e.preventDefault();
1303
- if (dragging) dropOnPool(dragging);
1304
- },
1305
- children: segments.length === 0 ? /* @__PURE__ */ jsx5("p", { className: "cfi-vi-pool-empty", children: "No segments detected. Run \u201CDetect segments\u201D from the source page first." }) : poolSegments.length === 0 ? /* @__PURE__ */ jsx5("p", { className: "cfi-vi-pool-empty", children: searchQuery ? "No segments match the filter." : "All segments assigned \u2014 drop a chip here to return it." }) : /* @__PURE__ */ jsx5("div", { className: "cfi-vi-chips", children: poolSegments.map((seg) => /* @__PURE__ */ jsx5(SegmentChip, { label: seg.label, onDragStart: () => startDrag(`pool:${seg.id}`), onDragEnd: endDrag }, seg.id)) })
1306
- }
1307
- ),
1308
- /* @__PURE__ */ jsx5("p", { className: "cfi-vi-mapper-note", children: "Drag chips onto a hierarchy level or an automotive field. Drop a chip back here to unassign." })
1309
- ] })
1310
- ] });
1311
- }
1312
- function HierarchyRow({
1313
- level,
1314
- segId,
1315
- segmentById,
1316
- isDropTarget,
1317
- onDragOver,
1318
- onDragLeave,
1319
- onDrop,
1320
- onChipDragStart,
1321
- onChipDragEnd,
1322
- onChipRemove,
1323
- sparse,
1324
- baseHint
1325
- }) {
1326
- const seg = segId ? segmentById.get(segId) : void 0;
1327
- const isLeaf = level === 6;
1328
- const indent = sparse ? 0 : (level - 1) * 22;
1329
- const levelName = DEFAULT_LEVEL_NAMES[level];
1330
- return /* @__PURE__ */ jsxs5("div", { className: "cfi-vi-hier-row", style: { paddingLeft: `${indent}px` }, children: [
1331
- !sparse && level > 1 && /* @__PURE__ */ jsx5("div", { className: "cfi-vi-hier-elbow" }),
1332
- /* @__PURE__ */ jsx5("div", { className: `cfi-vi-hier-dot ${seg ? "cfi-vi-hier-dot--filled" : ""}` }),
1333
- /* @__PURE__ */ jsx5("span", { className: "cfi-vi-hier-name", children: levelName }),
1334
- isLeaf && !sparse && /* @__PURE__ */ jsx5("span", { className: "cfi-vi-leaf", children: "Leaf" }),
1335
- /* @__PURE__ */ jsx5(
1336
- "div",
1337
- {
1338
- className: `cfi-vi-dropzone ${isDropTarget ? "cfi-vi-dropzone--active" : seg ? "cfi-vi-dropzone--filled" : "cfi-vi-dropzone--empty"}`,
1339
- onDragOver: (e) => {
1340
- e.preventDefault();
1341
- onDragOver();
1342
- },
1343
- onDragLeave: (e) => {
1344
- if (!e.currentTarget.contains(e.relatedTarget)) onDragLeave();
1345
- },
1346
- onDrop: (e) => {
1347
- e.preventDefault();
1348
- onDrop();
1349
- },
1350
- children: seg ? /* @__PURE__ */ jsx5(AssignedChip, { seg, onDragStart: () => onChipDragStart(seg.id), onDragEnd: onChipDragEnd, onRemove: onChipRemove }) : /* @__PURE__ */ jsx5("span", { className: "cfi-vi-dropzone-ph", children: isDropTarget ? "Drop here" : "Drop a segment\u2026" })
1351
- }
1352
- ),
1353
- baseHint && /* @__PURE__ */ jsxs5("span", { className: "cfi-vi-base-hint", title: `Base source uses "${baseHint}" on ${levelName}. Match here for the overlay to join.`, children: [
1354
- "base: ",
1355
- baseHint
1356
- ] })
1357
- ] });
1358
- }
1359
- function AutomotiveRow({
1360
- slot,
1361
- covered,
1362
- hierarchySegment,
1363
- assignment,
1364
- segmentById,
1365
- transformRuleLabels,
1366
- hierarchyEmpty,
1367
- isDropTarget,
1368
- onDragOver,
1369
- onDragLeave,
1370
- onDrop,
1371
- onChipDragStart,
1372
- onChipDragEnd,
1373
- onChipRemove
1374
- }) {
1375
- const assignedSeg = assignment?.segmentId ? segmentById.get(assignment.segmentId) : void 0;
1376
- const requiredBadge = (slot.required || hierarchyEmpty) && !covered && !assignedSeg;
1377
- return /* @__PURE__ */ jsxs5("div", { className: "cfi-vi-auto-row", children: [
1378
- /* @__PURE__ */ jsxs5("div", { className: "cfi-vi-auto-label", children: [
1379
- /* @__PURE__ */ jsxs5("div", { className: "cfi-vi-row", children: [
1380
- /* @__PURE__ */ jsx5("span", { className: "cfi-vi-auto-label-text", children: slot.label }),
1381
- requiredBadge && /* @__PURE__ */ jsx5(Badge, { variant: "outline", children: "required" })
1382
- ] }),
1383
- slot.hierarchyCoveredByLevel != null && /* @__PURE__ */ jsxs5("p", { className: `cfi-vi-auto-cover ${covered ? "cfi-vi-auto-cover--on" : ""}`, children: [
1384
- covered ? "\u2713 " : "",
1385
- "From L",
1386
- slot.hierarchyCoveredByLevel,
1387
- " hierarchy"
1388
- ] })
1389
- ] }),
1390
- /* @__PURE__ */ jsx5(
1391
- "div",
1392
- {
1393
- className: `cfi-vi-dropzone ${isDropTarget ? "cfi-vi-dropzone--active" : assignedSeg ? "cfi-vi-dropzone--filled" : covered ? "cfi-vi-dropzone--covered" : "cfi-vi-dropzone--empty"}`,
1394
- onDragOver: (e) => {
1395
- e.preventDefault();
1396
- onDragOver();
1397
- },
1398
- onDragLeave: (e) => {
1399
- if (!e.currentTarget.contains(e.relatedTarget)) onDragLeave();
1400
- },
1401
- onDrop: (e) => {
1402
- e.preventDefault();
1403
- onDrop();
1290
+ draggable: true,
1291
+ onDragStart: () => setDragging(s.id),
1292
+ onDragEnd: () => setDragging(null),
1293
+ onClick: () => clickColumn(s.id),
1294
+ className: "cfi-vi-chip",
1295
+ style: { cursor: activeDim ? "pointer" : "grab" },
1296
+ title: activeDim ? "Click to assign" : "Drag onto a dimension, or select a dimension first",
1297
+ children: [
1298
+ /* @__PURE__ */ jsx5("div", { className: "cfi-vi-chip-dot" }),
1299
+ s.label
1300
+ ]
1404
1301
  },
1405
- children: assignedSeg ? /* @__PURE__ */ jsx5(AssignedChip, { seg: assignedSeg, onDragStart: () => onChipDragStart(assignedSeg.id), onDragEnd: onChipDragEnd, onRemove: onChipRemove }) : covered && hierarchySegment ? /* @__PURE__ */ jsxs5("div", { className: "cfi-vi-auto-auto", title: "Auto-supplied from hierarchy. Drop a different chip here to override.", children: [
1406
- /* @__PURE__ */ jsx5(CheckCircle22, { size: 14, "aria-hidden": true }),
1407
- /* @__PURE__ */ jsx5("span", { className: "cfi-vi-auto-auto-label", children: hierarchySegment.label }),
1408
- /* @__PURE__ */ jsx5("span", { className: "cfi-vi-auto-auto-tag", children: "auto" })
1409
- ] }) : /* @__PURE__ */ jsx5("span", { className: "cfi-vi-dropzone-ph", children: isDropTarget ? "Drop here" : "Drop a segment\u2026" })
1410
- }
1411
- ),
1412
- slot.defaultTransform && !covered && /* @__PURE__ */ jsxs5("div", { className: "cfi-vi-auto-transform", children: [
1413
- /* @__PURE__ */ jsx5("span", { className: "cfi-vi-auto-transform-label", children: "Transform:" }),
1414
- " ",
1415
- transformRuleLabels[slot.defaultTransform] ?? slot.defaultTransform
1302
+ s.id
1303
+ )) }) })
1416
1304
  ] })
1417
1305
  ] });
1418
1306
  }
1419
- function AssignedChip({
1420
- seg,
1421
- onDragStart,
1422
- onDragEnd,
1423
- onRemove
1424
- }) {
1425
- return /* @__PURE__ */ jsxs5("div", { draggable: true, onDragStart, onDragEnd, className: "cfi-vi-chip--assigned", children: [
1426
- /* @__PURE__ */ jsx5("div", { className: "cfi-vi-chip-dot cfi-vi-chip-dot--on" }),
1427
- /* @__PURE__ */ jsx5("span", { className: "cfi-vi-chip-label", children: seg.label }),
1428
- /* @__PURE__ */ jsx5("button", { type: "button", onClick: (e) => {
1429
- e.stopPropagation();
1430
- onRemove();
1431
- }, className: "cfi-vi-chip-x", title: "Remove \u2014 returns to pool", children: "\xD7" })
1432
- ] });
1433
- }
1434
- function SegmentChip({ label, onDragStart, onDragEnd }) {
1435
- return /* @__PURE__ */ jsxs5("div", { draggable: true, onDragStart, onDragEnd, className: "cfi-vi-chip", children: [
1436
- /* @__PURE__ */ jsx5("div", { className: "cfi-vi-chip-dot" }),
1437
- label
1438
- ] });
1439
- }
1440
1307
 
1441
1308
  // src/VolumeSourceDetail.tsx
1442
1309
  import { useEffect as useEffect7, useRef as useRef3, useState as useState7 } from "react";
@@ -1730,8 +1597,8 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, classNa
1730
1597
  const [mappingKeys, setMappingKeys] = useState7(null);
1731
1598
  const [mappingRows, setMappingRows] = useState7([]);
1732
1599
  const [savingMapping, setSavingMapping] = useState7(false);
1733
- const [baseHierarchyHint, setBaseHierarchyHint] = useState7({});
1734
1600
  const [requiredStatus, setRequiredStatus] = useState7(null);
1601
+ const [mappableDimensions, setMappableDimensions] = useState7([]);
1735
1602
  async function loadRequiredStatus() {
1736
1603
  if (!id || !transport.getRequiredMappingStatus) {
1737
1604
  setRequiredStatus(null);
@@ -1762,34 +1629,12 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, classNa
1762
1629
  setHasData(uploads.items.some((u) => u.status === "done" || u.status === "done_with_errors"));
1763
1630
  setMappingKeys(keys);
1764
1631
  setMappingRows(mapping.items);
1765
- if (src.kind === "overlay") {
1766
- try {
1767
- const tenantConfig = await transport.getTenantConfig();
1768
- if (tenantConfig?.sourceId && tenantConfig.sourceId !== id) {
1769
- const [baseSrc, baseMapping] = await Promise.all([
1770
- transport.getSource(tenantConfig.sourceId),
1771
- transport.listSegmentMapping(tenantConfig.sourceId)
1772
- ]);
1773
- const baseSegLabel = new Map(baseSrc.segments.map((s) => [s.id, s.label]));
1774
- const hint = {};
1775
- for (const row of baseMapping.items) {
1776
- if (row.targetCategory !== "hierarchy_level") continue;
1777
- const m = /^level(\d)$/.exec(row.targetKey);
1778
- if (!m) continue;
1779
- const label = baseSegLabel.get(row.segmentId);
1780
- if (label) hint[parseInt(m[1], 10)] = label;
1781
- }
1782
- setBaseHierarchyHint(hint);
1783
- } else {
1784
- setBaseHierarchyHint({});
1785
- }
1786
- } catch {
1787
- setBaseHierarchyHint({});
1788
- }
1632
+ void loadRequiredStatus();
1633
+ if (transport.getMappableDimensions) {
1634
+ transport.getMappableDimensions().then(setMappableDimensions).catch(() => setMappableDimensions([]));
1789
1635
  } else {
1790
- setBaseHierarchyHint({});
1636
+ setMappableDimensions([]);
1791
1637
  }
1792
- void loadRequiredStatus();
1793
1638
  } catch (e) {
1794
1639
  setError(e instanceof Error ? e.message : "Failed to load source");
1795
1640
  } finally {
@@ -1822,14 +1667,19 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, classNa
1822
1667
  setError(e instanceof Error ? e.message : "Delete failed");
1823
1668
  }
1824
1669
  }
1825
- async function handleSaveMapping(hierarchy, automotive) {
1670
+ async function handleSaveMapping(hierarchy, automotive, dimensions) {
1826
1671
  if (!id) return;
1827
1672
  setSavingMapping(true);
1828
1673
  setError(null);
1829
1674
  try {
1830
1675
  const hResp = await transport.replaceSegmentMappingCategory(id, "hierarchy_level", hierarchy);
1831
1676
  const aResp = await transport.replaceSegmentMappingCategory(id, "automotive_field", automotive);
1832
- setMappingRows([...hResp.items, ...aResp.items]);
1677
+ const rows = [...hResp.items, ...aResp.items];
1678
+ if (transport.getMappableDimensions) {
1679
+ const dResp = await transport.replaceSegmentMappingCategory(id, "dimension", dimensions);
1680
+ rows.push(...dResp.items);
1681
+ }
1682
+ setMappingRows(rows);
1833
1683
  void loadRequiredStatus();
1834
1684
  } catch (e) {
1835
1685
  setError(e instanceof Error ? e.message : "Failed to save mapping");
@@ -2181,16 +2031,11 @@ function VolumeSourceDetail({ transport, sourceId, onOpenSource, onBack, classNa
2181
2031
  source.segments.length === 0 ? /* @__PURE__ */ jsx8("p", { className: "cfi-vi-muted", children: "No segments yet. Add segments on the Structure tab (Detect from file or Add manually) before mapping." }) : /* @__PURE__ */ jsx8(
2182
2032
  UnifiedSegmentMapper,
2183
2033
  {
2184
- segments: source.segments.slice().sort((a, b) => a.label.localeCompare(b.label)),
2185
- hierarchyInitial: mappingRows.filter((m) => m.targetCategory === "hierarchy_level"),
2186
- automotiveInitial: mappingRows.filter((m) => m.targetCategory === "automotive_field"),
2187
- automotiveSlots: isOverlay ? [] : AUTOMOTIVE_SLOTS,
2188
- transformRules: mappingKeys?.transformRules ?? [],
2189
- transformRuleLabels: AUTOMOTIVE_TRANSFORM_LABELS,
2034
+ segments: source.segments,
2035
+ mappableDimensions,
2036
+ initialMappings: mappingRows,
2190
2037
  onSave: handleSaveMapping,
2191
- saving: savingMapping,
2192
- hierarchyMode: isOverlay ? "sparse" : "tree",
2193
- baseHierarchyHint: isOverlay ? baseHierarchyHint : void 0
2038
+ saving: savingMapping
2194
2039
  }
2195
2040
  )
2196
2041
  ] })
package/package.json CHANGED
@@ -1,43 +1,43 @@
1
- {
2
- "name": "@campfire-interactive/volume-intelligence-ui",
3
- "version": "0.5.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.6.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
+ }