@inkindcards/semantic-layer 2.3.0 → 2.4.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.
@@ -809,6 +809,7 @@ function InspectableRegistry({ children }) {
809
809
  const [entries, setEntries] = useState(/* @__PURE__ */ new Map());
810
810
  const [inspectActive, setInspectActive] = useState(false);
811
811
  const [selectedId, setSelectedId] = useState(null);
812
+ const [modalTab, setModalTab] = useState(null);
812
813
  const register = useCallback((entry) => {
813
814
  setEntries((prev) => {
814
815
  const next = new Map(prev);
@@ -831,7 +832,9 @@ function InspectableRegistry({ children }) {
831
832
  inspectActive,
832
833
  setInspectActive,
833
834
  selectedId,
834
- setSelectedId
835
+ setSelectedId,
836
+ modalTab,
837
+ setModalTab
835
838
  };
836
839
  return /* @__PURE__ */ jsx(InspectableRegistryContext.Provider, { value, children });
837
840
  }
@@ -1076,6 +1079,255 @@ function generateMigrationPrompt(config) {
1076
1079
  );
1077
1080
  return parts.join("\n");
1078
1081
  }
1082
+
1083
+ // src/components/report-prompt-generator.ts
1084
+ var TIME_PRESET_LABELS = {
1085
+ "12m": "12 months",
1086
+ "6m": "6 months",
1087
+ "3m": "3 months",
1088
+ "1m": "1 month",
1089
+ "1w": "1 week",
1090
+ "1d": "1 day"
1091
+ };
1092
+ var PALETTE = [
1093
+ "#3b82f6",
1094
+ "#10b981",
1095
+ "#f59e0b",
1096
+ "#ef4444",
1097
+ "#8b5cf6",
1098
+ "#ec4899",
1099
+ "#06b6d4",
1100
+ "#84cc16",
1101
+ "#f97316",
1102
+ "#6366f1"
1103
+ ];
1104
+ function generateReportPrompt(config) {
1105
+ const { dashboardTitle, layout, widgets, sharedFilters } = config;
1106
+ if (widgets.length === 0) {
1107
+ return `Create a new dashboard page called "${dashboardTitle}". Use @inkindcards/semantic-layer to connect to live data. Start by adding a DataCatalog so the user can browse available metrics and dimensions.`;
1108
+ }
1109
+ const parts = [];
1110
+ parts.push(`Create a dashboard page called "${dashboardTitle}" using the @inkindcards/semantic-layer SDK to query live data from the dbt Semantic Layer.`);
1111
+ parts.push("");
1112
+ parts.push("## Setup");
1113
+ parts.push("");
1114
+ parts.push("Install dependencies if not already present:");
1115
+ parts.push(" npm install @inkindcards/semantic-layer recharts");
1116
+ parts.push("");
1117
+ parts.push("Imports needed:");
1118
+ parts.push("```tsx");
1119
+ parts.push(`import { useSemanticQuery } from "@inkindcards/semantic-layer/react";`);
1120
+ const chartTypes = new Set(widgets.map((w) => w.type));
1121
+ const rechartsImports = ["ResponsiveContainer", "Tooltip", "Legend"];
1122
+ if (chartTypes.has("line")) rechartsImports.push("LineChart", "Line", "XAxis", "YAxis", "CartesianGrid");
1123
+ if (chartTypes.has("bar")) rechartsImports.push("BarChart", "Bar", "XAxis", "YAxis", "CartesianGrid");
1124
+ if (chartTypes.has("area")) rechartsImports.push("AreaChart", "Area", "XAxis", "YAxis", "CartesianGrid");
1125
+ if (chartTypes.has("pie")) rechartsImports.push("PieChart", "Pie", "Cell");
1126
+ const unique = [...new Set(rechartsImports)];
1127
+ if (unique.length > 3) {
1128
+ parts.push(`import { ${unique.join(", ")} } from "recharts";`);
1129
+ }
1130
+ parts.push("```");
1131
+ parts.push("");
1132
+ if (sharedFilters.length > 0) {
1133
+ parts.push("## Shared Filters");
1134
+ parts.push("");
1135
+ parts.push("Create state variables for cross-widget filtering. Render a filter bar at the top of the dashboard with a select/dropdown for each filter. All useSemanticQuery calls below should include the `filters` object so every widget responds to filter changes.");
1136
+ parts.push("");
1137
+ parts.push("```tsx");
1138
+ for (const f of sharedFilters) {
1139
+ parts.push(`const [filter_${sanitize(f.dimension.name)}, setFilter_${sanitize(f.dimension.name)}] = useState<string[]>([]);`);
1140
+ }
1141
+ parts.push("");
1142
+ parts.push("const filters: Record<string, string[]> = {");
1143
+ for (const f of sharedFilters) {
1144
+ parts.push(` "${f.dimension.name}": filter_${sanitize(f.dimension.name)},`);
1145
+ }
1146
+ parts.push("};");
1147
+ parts.push("```");
1148
+ parts.push("");
1149
+ const timeFilters = sharedFilters.filter((f) => f.dimension.type === "time_dimension" && f.timePreset);
1150
+ const dimFilters = sharedFilters.filter((f) => f.dimension.type !== "time_dimension" || !f.timePreset);
1151
+ if (timeFilters.length > 0) {
1152
+ parts.push("For time filters, compute a date range relative to today:");
1153
+ for (const f of timeFilters) {
1154
+ parts.push(` - "${f.dimension.name}": default to the last ${TIME_PRESET_LABELS[f.timePreset]}. Compute a start date (e.g. \`new Date(); startDate.setMonth(startDate.getMonth() - N)\`) and pass it as a filter value. Include preset buttons (${Object.values(TIME_PRESET_LABELS).join(", ")}) so the user can switch ranges.`);
1155
+ }
1156
+ parts.push("");
1157
+ }
1158
+ if (dimFilters.length > 0) {
1159
+ parts.push("For each dimension filter dropdown, query the distinct values of the dimension using useSemanticQuery with that dimension as a groupBy and any single metric, then populate the dropdown options from the returned rows.");
1160
+ parts.push("");
1161
+ }
1162
+ }
1163
+ parts.push("## Layout");
1164
+ parts.push("");
1165
+ parts.push(layoutInstructions(layout, widgets));
1166
+ parts.push("");
1167
+ parts.push("## Color Palette");
1168
+ parts.push("");
1169
+ parts.push(`Use this consistent color palette for all charts: ${PALETTE.slice(0, 6).map((c) => `\`${c}\``).join(", ")}. Assign colors to metrics in order so the same metric has the same color everywhere.`);
1170
+ parts.push("");
1171
+ parts.push("## Widgets");
1172
+ parts.push("");
1173
+ for (let i = 0; i < widgets.length; i++) {
1174
+ const w = widgets[i];
1175
+ parts.push(`### ${i + 1}. ${w.title} (${widgetLabel(w.type)})`);
1176
+ parts.push("");
1177
+ parts.push(widgetPrompt(w, sharedFilters.length > 0));
1178
+ parts.push("");
1179
+ }
1180
+ parts.push("## Styling");
1181
+ parts.push("");
1182
+ parts.push("- Use a clean, modern design with rounded corners (border-radius: 12px) and subtle shadows on each card.");
1183
+ parts.push("- Background: white cards on a light gray page (#f9fafb).");
1184
+ parts.push("- Use system-ui / -apple-system font stack, 14px base size.");
1185
+ parts.push("- Each widget should be wrapped in a card with a title header, padding 20px, and a subtle 1px border (#e5e7eb).");
1186
+ parts.push("- Show a shimmer/skeleton loading state while isLoading is true.");
1187
+ parts.push("- Show a styled error banner if error is not null.");
1188
+ parts.push("- Format numbers with Intl.NumberFormat: use commas for large numbers, currency format for revenue/dollar metrics, percentage format for rate metrics.");
1189
+ parts.push("- Charts should fill their container width using ResponsiveContainer with height 300px.");
1190
+ parts.push("- On mobile (< 768px), stack everything in a single column.");
1191
+ parts.push("");
1192
+ return parts.join("\n");
1193
+ }
1194
+ function widgetPrompt(w, hasFilters, index) {
1195
+ const lines = [];
1196
+ const metricsArr = w.metrics.map((m) => `'${m.name}'`).join(", ");
1197
+ const groupByArr = w.groupBy.map((d) => `'${d.name}'`).join(", ");
1198
+ lines.push("Query:");
1199
+ lines.push("```tsx");
1200
+ lines.push(`const { data, isLoading, error } = useSemanticQuery({`);
1201
+ if (w.metrics.length > 0) lines.push(` metrics: [${metricsArr}],`);
1202
+ if (w.groupBy.length > 0) lines.push(` groupBy: [${groupByArr}],`);
1203
+ if (w.grain) lines.push(` grain: '${w.grain}',`);
1204
+ if (hasFilters) lines.push(` filters,`);
1205
+ lines.push(`});`);
1206
+ lines.push("```");
1207
+ lines.push("");
1208
+ switch (w.type) {
1209
+ case "line":
1210
+ case "area": {
1211
+ const ChartTag = w.type === "line" ? "LineChart" : "AreaChart";
1212
+ const SeriesTag = w.type === "line" ? "Line" : "Area";
1213
+ const xField = w.groupBy[0];
1214
+ lines.push(`Render a ${ChartTag} inside a ResponsiveContainer (height 300).`);
1215
+ if (xField) lines.push(`X-axis: "${xField.name}" (label: "${xField.displayName}").`);
1216
+ lines.push(`Add a ${SeriesTag} for each metric:`);
1217
+ for (let i = 0; i < w.metrics.length; i++) {
1218
+ const m = w.metrics[i];
1219
+ lines.push(` - dataKey="${m.name}" name="${m.displayName}" stroke="${PALETTE[i % PALETTE.length]}"${w.type === "area" ? ` fill="${PALETTE[i % PALETTE.length]}" fillOpacity={0.15}` : ""}`);
1220
+ }
1221
+ lines.push("Include CartesianGrid (strokeDasharray='3 3'), Tooltip, and Legend.");
1222
+ break;
1223
+ }
1224
+ case "bar": {
1225
+ const xField = w.groupBy[0];
1226
+ lines.push("Render a BarChart inside a ResponsiveContainer (height 300).");
1227
+ if (xField) lines.push(`X-axis: "${xField.name}" (label: "${xField.displayName}").`);
1228
+ lines.push("Add a Bar for each metric:");
1229
+ for (let i = 0; i < w.metrics.length; i++) {
1230
+ const m = w.metrics[i];
1231
+ lines.push(` - dataKey="${m.name}" name="${m.displayName}" fill="${PALETTE[i % PALETTE.length]}" radius={[4,4,0,0]}`);
1232
+ }
1233
+ lines.push("Include CartesianGrid (strokeDasharray='3 3'), Tooltip, and Legend.");
1234
+ break;
1235
+ }
1236
+ case "pie": {
1237
+ const m = w.metrics[0];
1238
+ const dim = w.groupBy[0];
1239
+ lines.push("Render a PieChart inside a ResponsiveContainer (height 300).");
1240
+ if (m && dim) {
1241
+ lines.push(`Pie dataKey="${m.name}" nameKey="${dim.name}". Label each slice with the dimension value and percentage.`);
1242
+ }
1243
+ lines.push(`Use Cell components to apply these fill colors in order: ${PALETTE.slice(0, 6).map((c) => `"${c}"`).join(", ")}.`);
1244
+ lines.push("Include Tooltip and Legend.");
1245
+ break;
1246
+ }
1247
+ case "table": {
1248
+ lines.push("Render a styled data table with:");
1249
+ lines.push(" - Sticky header row with a light gray background.");
1250
+ lines.push(" - Sortable columns (click header to toggle asc/desc).");
1251
+ lines.push(" - Zebra-striped rows for readability.");
1252
+ const allFields = [...w.groupBy, ...w.metrics];
1253
+ if (allFields.length > 0) {
1254
+ lines.push("Columns:");
1255
+ for (const f of allFields) {
1256
+ const align = f.type === "metric" ? "right-aligned, number-formatted" : "left-aligned";
1257
+ lines.push(` - "${f.displayName}" (key: "${f.name}") \u2014 ${align}`);
1258
+ }
1259
+ }
1260
+ break;
1261
+ }
1262
+ case "kpi": {
1263
+ const m = w.metrics[0];
1264
+ lines.push("Render a KPI card showing:");
1265
+ if (m) {
1266
+ lines.push(` - Large formatted number for "${m.displayName}" (key: "${m.name}").`);
1267
+ lines.push(` - Use Intl.NumberFormat with appropriate style (currency for revenue/dollar fields, decimal otherwise).`);
1268
+ lines.push(` - Subtitle label: "${m.displayName}".`);
1269
+ }
1270
+ if (w.comparisonMetric) {
1271
+ lines.push(` - Below the main number, show a comparison badge for "${w.comparisonMetric.displayName}" with a green up-arrow or red down-arrow indicating positive/negative change.`);
1272
+ }
1273
+ if (w.grain && w.groupBy.length > 0) {
1274
+ lines.push(` - Query with grain '${w.grain}' and take the most recent period's value. Optionally show a sparkline of the trend.`);
1275
+ } else {
1276
+ lines.push(" - Query without groupBy to get the aggregate total.");
1277
+ }
1278
+ break;
1279
+ }
1280
+ }
1281
+ return lines.join("\n");
1282
+ }
1283
+ function layoutInstructions(layout, widgets) {
1284
+ const kpiCount = widgets.filter((w) => w.type === "kpi").length;
1285
+ const chartCount = widgets.filter((w) => w.type !== "kpi" && w.type !== "table").length;
1286
+ const tableCount = widgets.filter((w) => w.type === "table").length;
1287
+ switch (layout) {
1288
+ case "single":
1289
+ return "Stack all widgets in a single column, full width. Use a flex column with gap 24px.";
1290
+ case "two-column":
1291
+ return "Use a CSS grid with `grid-template-columns: repeat(2, 1fr)` and gap 24px. KPI cards can be smaller \u2014 place up to 3 per row using `grid-template-columns: repeat(auto-fill, minmax(200px, 1fr))` in a sub-grid. Tables span full width (`grid-column: 1 / -1`).";
1292
+ case "dashboard":
1293
+ return [
1294
+ "Use a dashboard layout with three zones:",
1295
+ kpiCount > 0 ? ` 1. **KPI row** at the top: ${kpiCount} KPI card(s) in a horizontal flex row with gap 16px, equal width.` : " 1. **KPI row**: (none configured \u2014 skip this zone).",
1296
+ chartCount > 0 ? ` 2. **Charts zone**: ${chartCount} chart(s) in a 2-column CSS grid (gap 24px). If only 1 chart, span full width.` : " 2. **Charts zone**: (none configured).",
1297
+ tableCount > 0 ? ` 3. **Table zone** at the bottom: full-width data table(s), stacked.` : " 3. **Table zone**: (none configured)."
1298
+ ].join("\n");
1299
+ }
1300
+ }
1301
+ function widgetLabel(type) {
1302
+ const labels = {
1303
+ line: "Line Chart",
1304
+ bar: "Bar Chart",
1305
+ area: "Area Chart",
1306
+ pie: "Pie / Donut Chart",
1307
+ table: "Data Table",
1308
+ kpi: "KPI Card"
1309
+ };
1310
+ return labels[type];
1311
+ }
1312
+ function sanitize(name) {
1313
+ return name.replace(/[^a-zA-Z0-9_]/g, "_");
1314
+ }
1315
+ var LOVABLE_CHAR_LIMIT = 5e4;
1316
+ function isRunningInIframe() {
1317
+ try {
1318
+ return window.self !== window.top;
1319
+ } catch {
1320
+ return true;
1321
+ }
1322
+ }
1323
+ function canOpenInLovable(prompt) {
1324
+ return prompt.length <= LOVABLE_CHAR_LIMIT;
1325
+ }
1326
+ function openInLovable(prompt) {
1327
+ const encoded = encodeURIComponent(prompt);
1328
+ const url = `https://lovable.dev/?autosubmit=true#prompt=${encoded}`;
1329
+ window.open(url, "_blank", "noopener");
1330
+ }
1079
1331
  function DataInspector({ children }) {
1080
1332
  return /* @__PURE__ */ jsxs(InspectableRegistry, { children: [
1081
1333
  /* @__PURE__ */ jsx(InspectorOverlay, {}),
@@ -1085,7 +1337,15 @@ function DataInspector({ children }) {
1085
1337
  function InspectorOverlay() {
1086
1338
  const registry = useContext(InspectableRegistryContext);
1087
1339
  if (!registry) return null;
1088
- const { inspectActive, setInspectActive, selectedId, setSelectedId, entries } = registry;
1340
+ const {
1341
+ inspectActive,
1342
+ setInspectActive,
1343
+ selectedId,
1344
+ setSelectedId,
1345
+ entries,
1346
+ modalTab,
1347
+ setModalTab
1348
+ } = registry;
1089
1349
  useEffect(() => {
1090
1350
  function handleKeyDown(e) {
1091
1351
  if (e.ctrlKey && e.shiftKey && e.key === "I") {
@@ -1096,38 +1356,60 @@ function InspectorOverlay() {
1096
1356
  window.addEventListener("keydown", handleKeyDown);
1097
1357
  return () => window.removeEventListener("keydown", handleKeyDown);
1098
1358
  }, [inspectActive, setInspectActive]);
1359
+ useEffect(() => {
1360
+ if (selectedId) setModalTab("migrate");
1361
+ }, [selectedId, setModalTab]);
1099
1362
  const selectedEntry = selectedId ? entries.get(selectedId) ?? null : null;
1363
+ const handleClose = useCallback(() => {
1364
+ setModalTab(null);
1365
+ setSelectedId(null);
1366
+ }, [setModalTab, setSelectedId]);
1367
+ const handleToggle = useCallback(() => {
1368
+ if (modalTab) {
1369
+ handleClose();
1370
+ setInspectActive(false);
1371
+ } else {
1372
+ setModalTab("build");
1373
+ setInspectActive(true);
1374
+ }
1375
+ }, [modalTab, handleClose, setModalTab, setInspectActive]);
1376
+ const handleTabChange = useCallback((tab) => {
1377
+ setModalTab(tab);
1378
+ if (tab === "build") setSelectedId(null);
1379
+ }, [setModalTab, setSelectedId]);
1100
1380
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1101
1381
  /* @__PURE__ */ jsx(
1102
1382
  ToggleButton,
1103
1383
  {
1104
1384
  active: inspectActive,
1385
+ modalOpen: modalTab !== null,
1105
1386
  count: entries.size,
1106
- onToggle: () => {
1107
- setInspectActive(!inspectActive);
1108
- if (inspectActive) setSelectedId(null);
1109
- }
1387
+ onToggle: handleToggle
1110
1388
  }
1111
1389
  ),
1112
- selectedEntry && /* @__PURE__ */ jsx(
1113
- InspectorModal,
1390
+ modalTab && /* @__PURE__ */ jsx(
1391
+ UnifiedModal,
1114
1392
  {
1393
+ tab: modalTab,
1394
+ onTabChange: handleTabChange,
1115
1395
  entry: selectedEntry,
1116
- onClose: () => setSelectedId(null)
1396
+ onClose: handleClose
1117
1397
  }
1118
1398
  )
1119
1399
  ] });
1120
1400
  }
1121
1401
  function ToggleButton({
1122
1402
  active,
1403
+ modalOpen,
1123
1404
  count,
1124
1405
  onToggle
1125
1406
  }) {
1407
+ const lit = active || modalOpen;
1126
1408
  return /* @__PURE__ */ jsxs(
1127
1409
  "button",
1128
1410
  {
1129
1411
  onClick: onToggle,
1130
- title: active ? "Exit inspect mode (Ctrl+Shift+I)" : "Enter inspect mode (Ctrl+Shift+I)",
1412
+ title: lit ? "Close data tools (Ctrl+Shift+I)" : "Open data tools (Ctrl+Shift+I)",
1131
1413
  style: {
1132
1414
  position: "fixed",
1133
1415
  bottom: 20,
@@ -1140,9 +1422,9 @@ function ToggleButton({
1140
1422
  fontSize: 13,
1141
1423
  fontWeight: 600,
1142
1424
  fontFamily: "system-ui, -apple-system, sans-serif",
1143
- color: active ? "#fff" : "#374151",
1144
- backgroundColor: active ? "#3b82f6" : "#fff",
1145
- border: `1px solid ${active ? "#2563eb" : "#d1d5db"}`,
1425
+ color: lit ? "#fff" : "#374151",
1426
+ backgroundColor: lit ? "#3b82f6" : "#fff",
1427
+ border: `1px solid ${lit ? "#2563eb" : "#d1d5db"}`,
1146
1428
  borderRadius: 999,
1147
1429
  cursor: "pointer",
1148
1430
  boxShadow: "0 2px 8px rgba(0,0,0,0.12)",
@@ -1153,8 +1435,8 @@ function ToggleButton({
1153
1435
  /* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "8" }),
1154
1436
  /* @__PURE__ */ jsx("line", { x1: "21", y1: "21", x2: "16.65", y2: "16.65" })
1155
1437
  ] }),
1156
- active ? "Inspecting" : "Inspect Data",
1157
- /* @__PURE__ */ jsx(
1438
+ lit ? "Active" : "Data Tools",
1439
+ count > 0 && /* @__PURE__ */ jsx(
1158
1440
  "span",
1159
1441
  {
1160
1442
  style: {
@@ -1166,8 +1448,8 @@ function ToggleButton({
1166
1448
  fontSize: 11,
1167
1449
  fontWeight: 700,
1168
1450
  borderRadius: 999,
1169
- backgroundColor: active ? "rgba(255,255,255,0.25)" : "#e5e7eb",
1170
- color: active ? "#fff" : "#6b7280",
1451
+ backgroundColor: lit ? "rgba(255,255,255,0.25)" : "#e5e7eb",
1452
+ color: lit ? "#fff" : "#6b7280",
1171
1453
  padding: "0 5px"
1172
1454
  },
1173
1455
  children: count
@@ -1177,11 +1459,98 @@ function ToggleButton({
1177
1459
  }
1178
1460
  );
1179
1461
  }
1180
- function InspectorModal({
1462
+ function UnifiedModal({
1463
+ tab,
1464
+ onTabChange,
1181
1465
  entry,
1182
1466
  onClose
1467
+ }) {
1468
+ const [dragPos, setDragPos] = useState(null);
1469
+ const dragRef = useRef(null);
1470
+ const modalRef = useRef(null);
1471
+ const handleHeaderMouseDown = useCallback((e) => {
1472
+ if (e.target.closest("button")) return;
1473
+ e.preventDefault();
1474
+ const modal = modalRef.current;
1475
+ if (!modal) return;
1476
+ const rect = modal.getBoundingClientRect();
1477
+ dragRef.current = { startX: e.clientX, startY: e.clientY, origX: rect.left, origY: rect.top };
1478
+ const onMove = (ev) => {
1479
+ if (!dragRef.current) return;
1480
+ const dx = ev.clientX - dragRef.current.startX;
1481
+ const dy = ev.clientY - dragRef.current.startY;
1482
+ setDragPos({ x: dragRef.current.origX + dx, y: dragRef.current.origY + dy });
1483
+ };
1484
+ const onUp = () => {
1485
+ dragRef.current = null;
1486
+ document.removeEventListener("mousemove", onMove);
1487
+ document.removeEventListener("mouseup", onUp);
1488
+ };
1489
+ document.addEventListener("mousemove", onMove);
1490
+ document.addEventListener("mouseup", onUp);
1491
+ }, []);
1492
+ const positionStyle = dragPos ? { position: "fixed", top: dragPos.y, left: dragPos.x, transform: "none" } : { position: "fixed", top: "50%", left: "50%", transform: "translate(-50%, -50%)" };
1493
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1494
+ /* @__PURE__ */ jsx("div", { onClick: onClose, style: backdropStyle }),
1495
+ /* @__PURE__ */ jsxs("div", { ref: modalRef, style: { ...modalStyle, ...positionStyle }, children: [
1496
+ /* @__PURE__ */ jsxs(
1497
+ "div",
1498
+ {
1499
+ onMouseDown: handleHeaderMouseDown,
1500
+ style: { ...modalHeaderStyle, cursor: dragRef.current ? "grabbing" : "grab" },
1501
+ children: [
1502
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 0 }, children: [
1503
+ /* @__PURE__ */ jsx(TabButton, { label: "Migrate", active: tab === "migrate", onClick: () => onTabChange("migrate") }),
1504
+ /* @__PURE__ */ jsx(TabButton, { label: "Build", active: tab === "build", onClick: () => onTabChange("build") })
1505
+ ] }),
1506
+ /* @__PURE__ */ jsx("button", { onClick: onClose, style: closeBtnStyle, children: "\xD7" })
1507
+ ]
1508
+ }
1509
+ ),
1510
+ tab === "migrate" ? /* @__PURE__ */ jsx(MigrateTab, { entry, onClose }) : /* @__PURE__ */ jsx(BuildTab, {})
1511
+ ] })
1512
+ ] });
1513
+ }
1514
+ function TabButton({ label, active, onClick }) {
1515
+ return /* @__PURE__ */ jsx(
1516
+ "button",
1517
+ {
1518
+ onClick,
1519
+ style: {
1520
+ padding: "8px 20px",
1521
+ fontSize: 13,
1522
+ fontWeight: 600,
1523
+ fontFamily: "system-ui, -apple-system, sans-serif",
1524
+ color: active ? "#3b82f6" : "#6b7280",
1525
+ backgroundColor: "transparent",
1526
+ border: "none",
1527
+ borderBottom: active ? "2px solid #3b82f6" : "2px solid transparent",
1528
+ cursor: "pointer",
1529
+ transition: "all 0.15s ease"
1530
+ },
1531
+ children: label
1532
+ }
1533
+ );
1534
+ }
1535
+ function MigrateTab({
1536
+ entry,
1537
+ onClose: _onClose
1183
1538
  }) {
1184
1539
  const { fields: catalog, error: catalogError, isLoading: catalogLoading } = useMetrics();
1540
+ if (!entry) {
1541
+ return /* @__PURE__ */ jsx("div", { style: { flex: 1, display: "flex", alignItems: "center", justifyContent: "center", padding: 40 }, children: /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", color: "#9ca3af" }, children: [
1542
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 14, fontWeight: 500, marginBottom: 8 }, children: "No component selected" }),
1543
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 12 }, children: "Enable inspect mode (Ctrl+Shift+I) then click on a data component to map it to the semantic layer." })
1544
+ ] }) });
1545
+ }
1546
+ return /* @__PURE__ */ jsx(MigrateTabContent, { entry, catalog, catalogError, catalogLoading });
1547
+ }
1548
+ function MigrateTabContent({
1549
+ entry,
1550
+ catalog,
1551
+ catalogError,
1552
+ catalogLoading
1553
+ }) {
1185
1554
  const analysis = useMemo(
1186
1555
  () => analyzeChartData(entry.columns, entry.sampleValues),
1187
1556
  [entry.columns, entry.sampleValues]
@@ -1229,204 +1598,717 @@ function InspectorModal({
1229
1598
  originalMetricColumns: analysis.metricColumns
1230
1599
  };
1231
1600
  const prompt = generateMigrationPrompt(migrationConfig);
1232
- try {
1233
- await navigator.clipboard.writeText(prompt);
1234
- setCopied(true);
1235
- setTimeout(() => setCopied(false), 2e3);
1236
- } catch {
1237
- const ta = document.createElement("textarea");
1238
- ta.value = prompt;
1239
- document.body.appendChild(ta);
1240
- ta.select();
1241
- document.execCommand("copy");
1242
- document.body.removeChild(ta);
1243
- setCopied(true);
1244
- setTimeout(() => setCopied(false), 2e3);
1245
- }
1601
+ await copyToClipboard(prompt);
1602
+ setCopied(true);
1603
+ setTimeout(() => setCopied(false), 2e3);
1246
1604
  }, [entry, dimMappings, selectedMetrics, grain, hasTimeDim, analysis.metricColumns]);
1247
1605
  const canGenerate = selectedMetrics.length > 0 || Array.from(dimMappings.values()).some(Boolean);
1248
1606
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1249
- /* @__PURE__ */ jsx("div", { onClick: onClose, style: backdropStyle }),
1250
- /* @__PURE__ */ jsxs("div", { style: modalStyle, children: [
1251
- /* @__PURE__ */ jsxs("div", { style: modalHeaderStyle, children: [
1252
- /* @__PURE__ */ jsxs("div", { children: [
1253
- /* @__PURE__ */ jsx("div", { style: { fontSize: 16, fontWeight: 700, color: "#111827" }, children: entry.label }),
1254
- /* @__PURE__ */ jsxs("div", { style: { fontSize: 12, color: "#6b7280", marginTop: 2 }, children: [
1255
- "Source: ",
1256
- entry.currentSource
1257
- ] })
1258
- ] }),
1259
- /* @__PURE__ */ jsx("button", { onClick: onClose, style: closeBtnStyle, children: "\xD7" })
1607
+ /* @__PURE__ */ jsxs("div", { style: { overflow: "auto", flex: 1 }, children: [
1608
+ /* @__PURE__ */ jsxs("div", { style: sectionStyle, children: [
1609
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 15, fontWeight: 700, color: "#111827" }, children: entry.label }),
1610
+ /* @__PURE__ */ jsxs("div", { style: { fontSize: 12, color: "#6b7280", marginTop: 2 }, children: [
1611
+ "Source: ",
1612
+ entry.currentSource
1613
+ ] })
1260
1614
  ] }),
1261
- /* @__PURE__ */ jsxs("div", { style: { overflow: "auto", flex: 1 }, children: [
1262
- entry.sampleValues.length > 0 && /* @__PURE__ */ jsxs("div", { style: sectionStyle, children: [
1263
- /* @__PURE__ */ jsx("div", { style: sectionTitleStyle, children: "Current Data Preview" }),
1264
- /* @__PURE__ */ jsx("div", { style: { overflow: "auto" }, children: /* @__PURE__ */ jsxs("table", { style: previewTableStyle, children: [
1265
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: entry.columns.map((col) => /* @__PURE__ */ jsx("th", { style: previewThStyle, children: col }, col)) }) }),
1266
- /* @__PURE__ */ jsx("tbody", { children: entry.sampleValues.map((row, i) => /* @__PURE__ */ jsx("tr", { children: entry.columns.map((col) => /* @__PURE__ */ jsx("td", { style: previewTdStyle, children: row[col] === null || row[col] === void 0 ? "\u2014" : String(row[col]) }, col)) }, i)) })
1615
+ entry.sampleValues.length > 0 && /* @__PURE__ */ jsxs("div", { style: sectionStyle, children: [
1616
+ /* @__PURE__ */ jsx("div", { style: sectionTitleStyle, children: "Current Data Preview" }),
1617
+ /* @__PURE__ */ jsx("div", { style: { overflow: "auto" }, children: /* @__PURE__ */ jsxs("table", { style: previewTableStyle, children: [
1618
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: entry.columns.map((col) => /* @__PURE__ */ jsx("th", { style: previewThStyle, children: col }, col)) }) }),
1619
+ /* @__PURE__ */ jsx("tbody", { children: entry.sampleValues.map((row, i) => /* @__PURE__ */ jsx("tr", { children: entry.columns.map((col) => /* @__PURE__ */ jsx("td", { style: previewTdStyle, children: row[col] === null || row[col] === void 0 ? "\u2014" : String(row[col]) }, col)) }, i)) })
1620
+ ] }) }),
1621
+ /* @__PURE__ */ jsxs("div", { style: { marginTop: 8, fontSize: 11, color: "#9ca3af" }, children: [
1622
+ "Detected: ",
1623
+ analysis.dimensionColumns.length,
1624
+ " dimension column",
1625
+ analysis.dimensionColumns.length !== 1 ? "s" : "",
1626
+ " (",
1627
+ analysis.dimensionColumns.join(", ") || "none",
1628
+ ")",
1629
+ " \xB7 ",
1630
+ analysis.metricColumns.length,
1631
+ " value column",
1632
+ analysis.metricColumns.length !== 1 ? "s" : "",
1633
+ " (",
1634
+ analysis.metricColumns.join(", ") || "none",
1635
+ ")"
1636
+ ] })
1637
+ ] }),
1638
+ catalog.length === 0 && /* @__PURE__ */ jsx("div", { style: { ...sectionStyle, textAlign: "center" }, children: /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "#9ca3af" }, children: catalogLoading ? "Loading catalog..." : catalogError ? `Error loading catalog: ${catalogError}` : "No semantic layer catalog available. Connect to the gateway to enable smart matching." }) }),
1639
+ catalog.length > 0 && /* @__PURE__ */ jsxs("div", { style: sectionStyle, children: [
1640
+ /* @__PURE__ */ jsx("div", { style: sectionTitleStyle, children: "Dimensions (Group By)" }),
1641
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 11, color: "#9ca3af", marginBottom: 8 }, children: "What categories define the rows?" }),
1642
+ analysis.dimensionColumns.length === 0 ? /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "#9ca3af", fontStyle: "italic" }, children: "No dimension columns detected." }) : /* @__PURE__ */ jsxs("table", { style: { width: "100%", borderCollapse: "collapse", fontSize: 13 }, children: [
1643
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
1644
+ /* @__PURE__ */ jsx("th", { style: mappingThStyle, children: "Current Column" }),
1645
+ /* @__PURE__ */ jsx("th", { style: mappingThStyle, children: "Sample Values" }),
1646
+ /* @__PURE__ */ jsx("th", { style: mappingThStyle, children: "Semantic Layer Dimension" })
1267
1647
  ] }) }),
1268
- /* @__PURE__ */ jsxs("div", { style: { marginTop: 8, fontSize: 11, color: "#9ca3af" }, children: [
1269
- "Detected: ",
1270
- analysis.dimensionColumns.length,
1271
- " dimension column",
1272
- analysis.dimensionColumns.length !== 1 ? "s" : "",
1273
- " (",
1274
- analysis.dimensionColumns.join(", ") || "none",
1275
- ")",
1276
- " \xB7 ",
1277
- analysis.metricColumns.length,
1278
- " value column",
1279
- analysis.metricColumns.length !== 1 ? "s" : "",
1280
- " (",
1281
- analysis.metricColumns.join(", ") || "none",
1648
+ /* @__PURE__ */ jsx("tbody", { children: analysis.dimensionColumns.map((col) => {
1649
+ const sampleVals = entry.sampleValues.map((row) => row[col]).filter((v) => v !== null && v !== void 0).slice(0, 4).map(String);
1650
+ return /* @__PURE__ */ jsxs("tr", { children: [
1651
+ /* @__PURE__ */ jsx("td", { style: mappingTdStyle, children: /* @__PURE__ */ jsx("code", { style: { fontSize: 12, color: "#6366f1", fontFamily: "monospace" }, children: col }) }),
1652
+ /* @__PURE__ */ jsxs("td", { style: { ...mappingTdStyle, fontSize: 11, color: "#9ca3af" }, children: [
1653
+ sampleVals.join(", "),
1654
+ sampleVals.length >= 4 ? ", ..." : ""
1655
+ ] }),
1656
+ /* @__PURE__ */ jsx("td", { style: mappingTdStyle, children: /* @__PURE__ */ jsxs(
1657
+ "select",
1658
+ {
1659
+ value: dimMappings.get(col)?.name ?? "",
1660
+ onChange: (e) => {
1661
+ const f = dimensions.find((d) => d.name === e.target.value) ?? null;
1662
+ handleDimChange(col, f);
1663
+ },
1664
+ style: selectStyle2,
1665
+ children: [
1666
+ /* @__PURE__ */ jsx("option", { value: "", children: "-- select dimension --" }),
1667
+ dimensions.map((f) => /* @__PURE__ */ jsxs("option", { value: f.name, children: [
1668
+ f.displayName,
1669
+ " (",
1670
+ f.type,
1671
+ ") \\u2014 ",
1672
+ f.name
1673
+ ] }, f.name))
1674
+ ]
1675
+ }
1676
+ ) })
1677
+ ] }, col);
1678
+ }) })
1679
+ ] }),
1680
+ hasTimeDim && /* @__PURE__ */ jsxs("div", { style: { marginTop: 10, display: "flex", alignItems: "center", gap: 8 }, children: [
1681
+ /* @__PURE__ */ jsx("label", { style: { fontSize: 12, fontWeight: 600, color: "#374151" }, children: "Time Grain:" }),
1682
+ /* @__PURE__ */ jsxs("select", { value: grain, onChange: (e) => setGrain(e.target.value), style: { ...selectStyle2, width: "auto" }, children: [
1683
+ /* @__PURE__ */ jsx("option", { value: "day", children: "Day" }),
1684
+ /* @__PURE__ */ jsx("option", { value: "week", children: "Week" }),
1685
+ /* @__PURE__ */ jsx("option", { value: "month", children: "Month" }),
1686
+ /* @__PURE__ */ jsx("option", { value: "quarter", children: "Quarter" }),
1687
+ /* @__PURE__ */ jsx("option", { value: "year", children: "Year" })
1688
+ ] })
1689
+ ] })
1690
+ ] }),
1691
+ catalog.length > 0 && /* @__PURE__ */ jsxs("div", { style: sectionStyle, children: [
1692
+ /* @__PURE__ */ jsx("div", { style: sectionTitleStyle, children: "Metrics (Values)" }),
1693
+ /* @__PURE__ */ jsxs("div", { style: { fontSize: 11, color: "#9ca3af", marginBottom: 8 }, children: [
1694
+ "Which metrics should be queried?",
1695
+ analysis.metricColumns.length > 0 && /* @__PURE__ */ jsxs("span", { children: [
1696
+ " (replaces: ",
1697
+ analysis.metricColumns.join(", "),
1282
1698
  ")"
1283
1699
  ] })
1284
1700
  ] }),
1285
- catalog.length === 0 && /* @__PURE__ */ jsx("div", { style: { ...sectionStyle, textAlign: "center" }, children: /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "#9ca3af" }, children: catalogLoading ? "Loading catalog..." : catalogError ? `Error loading catalog: ${catalogError}` : "No semantic layer catalog available. Connect to the gateway to enable smart matching." }) }),
1286
- catalog.length > 0 && /* @__PURE__ */ jsxs("div", { style: sectionStyle, children: [
1287
- /* @__PURE__ */ jsx("div", { style: sectionTitleStyle, children: "Dimensions (Group By)" }),
1288
- /* @__PURE__ */ jsx("div", { style: { fontSize: 11, color: "#9ca3af", marginBottom: 8 }, children: "What categories define the rows? (X-axis, grouping)" }),
1289
- analysis.dimensionColumns.length === 0 ? /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "#9ca3af", fontStyle: "italic" }, children: "No dimension columns detected in the data." }) : /* @__PURE__ */ jsxs("table", { style: { width: "100%", borderCollapse: "collapse", fontSize: 13 }, children: [
1290
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
1291
- /* @__PURE__ */ jsx("th", { style: mappingThStyle, children: "Current Column" }),
1292
- /* @__PURE__ */ jsx("th", { style: mappingThStyle, children: "Sample Values" }),
1293
- /* @__PURE__ */ jsx("th", { style: mappingThStyle, children: "Semantic Layer Dimension" })
1294
- ] }) }),
1295
- /* @__PURE__ */ jsx("tbody", { children: analysis.dimensionColumns.map((col) => {
1296
- const sampleVals = entry.sampleValues.map((row) => row[col]).filter((v) => v !== null && v !== void 0).slice(0, 4).map(String);
1297
- return /* @__PURE__ */ jsxs("tr", { children: [
1298
- /* @__PURE__ */ jsx("td", { style: mappingTdStyle, children: /* @__PURE__ */ jsx("code", { style: { fontSize: 12, color: "#6366f1", fontFamily: "monospace" }, children: col }) }),
1299
- /* @__PURE__ */ jsxs("td", { style: { ...mappingTdStyle, fontSize: 11, color: "#9ca3af" }, children: [
1300
- sampleVals.join(", "),
1301
- sampleVals.length >= 4 ? ", ..." : ""
1302
- ] }),
1303
- /* @__PURE__ */ jsx("td", { style: mappingTdStyle, children: /* @__PURE__ */ jsxs(
1304
- "select",
1305
- {
1306
- value: dimMappings.get(col)?.name ?? "",
1307
- onChange: (e) => {
1308
- const field = dimensions.find((f) => f.name === e.target.value) ?? null;
1309
- handleDimChange(col, field);
1310
- },
1311
- style: selectStyle2,
1312
- children: [
1313
- /* @__PURE__ */ jsx("option", { value: "", children: "-- select dimension --" }),
1314
- dimensions.map((f) => /* @__PURE__ */ jsxs("option", { value: f.name, children: [
1315
- f.displayName,
1316
- " (",
1317
- f.type,
1318
- ") \u2014 ",
1319
- f.name
1320
- ] }, f.name))
1321
- ]
1322
- }
1323
- ) })
1324
- ] }, col);
1325
- }) })
1326
- ] }),
1327
- hasTimeDim && /* @__PURE__ */ jsxs("div", { style: { marginTop: 10, display: "flex", alignItems: "center", gap: 8 }, children: [
1328
- /* @__PURE__ */ jsx("label", { style: { fontSize: 12, fontWeight: 600, color: "#374151" }, children: "Time Grain:" }),
1329
- /* @__PURE__ */ jsxs(
1330
- "select",
1331
- {
1332
- value: grain,
1333
- onChange: (e) => setGrain(e.target.value),
1334
- style: { ...selectStyle2, width: "auto" },
1335
- children: [
1336
- /* @__PURE__ */ jsx("option", { value: "day", children: "Day" }),
1337
- /* @__PURE__ */ jsx("option", { value: "week", children: "Week" }),
1338
- /* @__PURE__ */ jsx("option", { value: "month", children: "Month" }),
1339
- /* @__PURE__ */ jsx("option", { value: "quarter", children: "Quarter" }),
1340
- /* @__PURE__ */ jsx("option", { value: "year", children: "Year" })
1341
- ]
1342
- }
1343
- )
1701
+ metrics.length === 0 ? /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "#9ca3af", fontStyle: "italic" }, children: "No metrics available." }) : /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", gap: 4, maxHeight: 200, overflow: "auto" }, children: metrics.map((m) => {
1702
+ const isSel = selectedMetrics.some((s) => s.name === m.name);
1703
+ return /* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: 8, padding: "4px 8px", borderRadius: 4, cursor: "pointer", backgroundColor: isSel ? "#eff6ff" : "transparent", border: isSel ? "1px solid #bfdbfe" : "1px solid transparent", fontSize: 12 }, children: [
1704
+ /* @__PURE__ */ jsx("input", { type: "checkbox", checked: isSel, onChange: () => handleToggleMetric(m), style: { margin: 0 } }),
1705
+ /* @__PURE__ */ jsx("span", { style: { fontWeight: 500, color: "#111827" }, children: m.displayName }),
1706
+ /* @__PURE__ */ jsx("code", { style: { fontSize: 10, color: "#6366f1", fontFamily: "monospace" }, children: m.name }),
1707
+ m.category && /* @__PURE__ */ jsx("span", { style: { marginLeft: "auto", fontSize: 10, color: "#9ca3af" }, children: m.category })
1708
+ ] }, m.name);
1709
+ }) }),
1710
+ selectedMetrics.length > 0 && /* @__PURE__ */ jsxs("div", { style: { marginTop: 8, fontSize: 11, color: "#6b7280" }, children: [
1711
+ "Selected: ",
1712
+ selectedMetrics.map((m) => m.displayName).join(", ")
1713
+ ] })
1714
+ ] }),
1715
+ canGenerate && /* @__PURE__ */ jsxs("div", { style: sectionStyle, children: [
1716
+ /* @__PURE__ */ jsx("div", { style: sectionTitleStyle, children: "Query Preview" }),
1717
+ /* @__PURE__ */ jsx("pre", { style: codePreviewStyle, children: `useSemanticQuery({${selectedMetrics.length > 0 ? `
1718
+ metrics: [${selectedMetrics.map((m) => `'${m.name}'`).join(", ")}],` : ""}${Array.from(dimMappings.values()).some(Boolean) ? `
1719
+ groupBy: [${Array.from(dimMappings.values()).filter(Boolean).map((f) => `'${f.name}'`).join(", ")}],` : ""}${hasTimeDim && grain ? `
1720
+ grain: '${grain}',` : ""}
1721
+ })` })
1722
+ ] })
1723
+ ] }),
1724
+ /* @__PURE__ */ jsx("div", { style: { padding: "12px 20px", borderTop: "1px solid #e5e7eb" }, children: /* @__PURE__ */ jsx("button", { onClick: handleGenerate, disabled: !canGenerate, style: { ...primaryBtnStyle, width: "100%", opacity: canGenerate ? 1 : 0.5, cursor: canGenerate ? "pointer" : "not-allowed" }, children: copied ? "Copied to clipboard!" : "Generate Migration Prompt" }) })
1725
+ ] });
1726
+ }
1727
+ var _nextWidgetId = 1;
1728
+ function nextWidgetId() {
1729
+ return `w${_nextWidgetId++}`;
1730
+ }
1731
+ var WIDGET_TYPES = [
1732
+ { type: "kpi", label: "KPI", icon: "#" },
1733
+ { type: "line", label: "Line", icon: "\u2571" },
1734
+ { type: "bar", label: "Bar", icon: "\u2581\u2583\u2585" },
1735
+ { type: "area", label: "Area", icon: "\u25E2" },
1736
+ { type: "pie", label: "Pie", icon: "\u25D4" },
1737
+ { type: "table", label: "Table", icon: "\u2637" }
1738
+ ];
1739
+ function BuildTab() {
1740
+ const { fields: catalog, isLoading: catalogLoading } = useMetrics();
1741
+ const allMetrics = useMemo(() => catalog.filter((f) => f.type === "metric"), [catalog]);
1742
+ const allDimensions = useMemo(() => {
1743
+ const dims = catalog.filter((f) => f.type === "dimension" || f.type === "time_dimension");
1744
+ return dims.sort((a, b) => {
1745
+ if (a.name === "metric_time") return -1;
1746
+ if (b.name === "metric_time") return 1;
1747
+ return 0;
1748
+ });
1749
+ }, [catalog]);
1750
+ const [dashboardTitle, setDashboardTitle] = useState("Dashboard");
1751
+ const [layout, setLayout] = useState("dashboard");
1752
+ const [widgets, setWidgets] = useState([]);
1753
+ const [sharedFilters, setSharedFilters] = useState([]);
1754
+ const [catalogSearch, setCatalogSearch] = useState("");
1755
+ const [copied, setCopied] = useState(false);
1756
+ const [lovableSent, setLovableSent] = useState(false);
1757
+ const [previewOpen, setPreviewOpen] = useState(false);
1758
+ const [catalogWidth, setCatalogWidth] = useState(220);
1759
+ const catalogDragRef = useRef(null);
1760
+ const inIframe = useMemo(() => isRunningInIframe(), []);
1761
+ const handleCatalogDragStart = useCallback((e) => {
1762
+ e.preventDefault();
1763
+ catalogDragRef.current = { startX: e.clientX, startW: catalogWidth };
1764
+ const onMove = (ev) => {
1765
+ if (!catalogDragRef.current) return;
1766
+ const delta = ev.clientX - catalogDragRef.current.startX;
1767
+ setCatalogWidth(Math.max(160, Math.min(400, catalogDragRef.current.startW + delta)));
1768
+ };
1769
+ const onUp = () => {
1770
+ catalogDragRef.current = null;
1771
+ document.removeEventListener("mousemove", onMove);
1772
+ document.removeEventListener("mouseup", onUp);
1773
+ };
1774
+ document.addEventListener("mousemove", onMove);
1775
+ document.addEventListener("mouseup", onUp);
1776
+ }, [catalogWidth]);
1777
+ const addWidget = useCallback((type) => {
1778
+ const isTimeSeries = type === "line" || type === "area" || type === "kpi";
1779
+ const metricTime = allDimensions.find((d) => d.name === "metric_time");
1780
+ setWidgets((prev) => [...prev, {
1781
+ id: nextWidgetId(),
1782
+ title: `${WIDGET_TYPES.find((t) => t.type === type)?.label ?? "Widget"} ${prev.length + 1}`,
1783
+ type,
1784
+ metrics: [],
1785
+ groupBy: isTimeSeries && metricTime ? [metricTime] : [],
1786
+ grain: isTimeSeries && metricTime ? "month" : null
1787
+ }]);
1788
+ }, [allDimensions]);
1789
+ const removeWidget = useCallback((id) => {
1790
+ setWidgets((prev) => prev.filter((w) => w.id !== id));
1791
+ }, []);
1792
+ const updateWidget = useCallback((id, patch) => {
1793
+ setWidgets((prev) => prev.map((w) => w.id === id ? { ...w, ...patch } : w));
1794
+ }, []);
1795
+ const duplicateWidget = useCallback((id) => {
1796
+ setWidgets((prev) => {
1797
+ const source = prev.find((w) => w.id === id);
1798
+ if (!source) return prev;
1799
+ const copy = {
1800
+ ...source,
1801
+ id: nextWidgetId(),
1802
+ title: `${source.title} (2)`,
1803
+ metrics: [...source.metrics],
1804
+ groupBy: [...source.groupBy]
1805
+ };
1806
+ const idx = prev.findIndex((w) => w.id === id);
1807
+ return [...prev.slice(0, idx + 1), copy, ...prev.slice(idx + 1)];
1808
+ });
1809
+ }, []);
1810
+ const addFilter = useCallback((dim) => {
1811
+ if (sharedFilters.some((f) => f.dimension.name === dim.name)) return;
1812
+ setSharedFilters((prev) => [...prev, {
1813
+ dimension: dim,
1814
+ label: dim.displayName,
1815
+ timePreset: dim.type === "time_dimension" ? "12m" : null
1816
+ }]);
1817
+ }, [sharedFilters]);
1818
+ const removeFilter = useCallback((name) => {
1819
+ setSharedFilters((prev) => prev.filter((f) => f.dimension.name !== name));
1820
+ }, []);
1821
+ const updateFilterPreset = useCallback((name, preset) => {
1822
+ setSharedFilters((prev) => prev.map(
1823
+ (f) => f.dimension.name === name ? { ...f, timePreset: preset } : f
1824
+ ));
1825
+ }, []);
1826
+ const addFieldToWidget = useCallback((widgetId, field) => {
1827
+ setWidgets((prev) => prev.map((w) => {
1828
+ if (w.id !== widgetId) return w;
1829
+ if (field.type === "metric") {
1830
+ if (w.metrics.some((m) => m.name === field.name)) return w;
1831
+ return { ...w, metrics: [...w.metrics, field] };
1832
+ } else {
1833
+ if (w.groupBy.some((d) => d.name === field.name)) return w;
1834
+ return { ...w, groupBy: [...w.groupBy, field] };
1835
+ }
1836
+ }));
1837
+ }, []);
1838
+ const removeFieldFromWidget = useCallback((widgetId, fieldName, role) => {
1839
+ setWidgets((prev) => prev.map((w) => {
1840
+ if (w.id !== widgetId) return w;
1841
+ if (role === "metric") return { ...w, metrics: w.metrics.filter((m) => m.name !== fieldName) };
1842
+ return { ...w, groupBy: w.groupBy.filter((d) => d.name !== fieldName) };
1843
+ }));
1844
+ }, []);
1845
+ const reportConfig = useMemo(() => ({
1846
+ dashboardTitle,
1847
+ layout,
1848
+ widgets,
1849
+ sharedFilters
1850
+ }), [dashboardTitle, layout, widgets, sharedFilters]);
1851
+ const prompt = useMemo(() => generateReportPrompt(reportConfig), [reportConfig]);
1852
+ const canGenerate = widgets.length > 0 && widgets.some((w) => w.metrics.length > 0);
1853
+ const canLovable = canGenerate && canOpenInLovable(prompt);
1854
+ const handleCopy = useCallback(async () => {
1855
+ await copyToClipboard(prompt);
1856
+ setCopied(true);
1857
+ setTimeout(() => setCopied(false), 2e3);
1858
+ }, [prompt]);
1859
+ const handleLovable = useCallback(async () => {
1860
+ if (inIframe) {
1861
+ await copyToClipboard(prompt);
1862
+ setLovableSent(true);
1863
+ setTimeout(() => setLovableSent(false), 3e3);
1864
+ } else {
1865
+ openInLovable(prompt);
1866
+ setLovableSent(true);
1867
+ setTimeout(() => setLovableSent(false), 3e3);
1868
+ }
1869
+ }, [prompt, inIframe]);
1870
+ const filteredMetrics = useMemo(() => {
1871
+ const q = catalogSearch.toLowerCase();
1872
+ if (!q) return allMetrics;
1873
+ return allMetrics.filter((f) => f.displayName.toLowerCase().includes(q) || f.name.toLowerCase().includes(q) || f.description.toLowerCase().includes(q));
1874
+ }, [allMetrics, catalogSearch]);
1875
+ const filteredDimensions = useMemo(() => {
1876
+ const q = catalogSearch.toLowerCase();
1877
+ if (!q) return allDimensions;
1878
+ return allDimensions.filter((f) => f.displayName.toLowerCase().includes(q) || f.name.toLowerCase().includes(q) || f.description.toLowerCase().includes(q));
1879
+ }, [allDimensions, catalogSearch]);
1880
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1881
+ /* @__PURE__ */ jsxs("div", { style: { overflow: "auto", flex: 1, display: "flex", flexDirection: "column" }, children: [
1882
+ /* @__PURE__ */ jsxs("div", { style: { ...sectionStyle, display: "flex", gap: 12, alignItems: "center", flexWrap: "wrap" }, children: [
1883
+ /* @__PURE__ */ jsx(
1884
+ "input",
1885
+ {
1886
+ value: dashboardTitle,
1887
+ onChange: (e) => setDashboardTitle(e.target.value),
1888
+ placeholder: "Dashboard title",
1889
+ style: { ...inputStyle2, flex: 1, minWidth: 160, fontSize: 15, fontWeight: 700 }
1890
+ }
1891
+ ),
1892
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 4 }, children: ["single", "two-column", "dashboard"].map((l) => /* @__PURE__ */ jsx(
1893
+ "button",
1894
+ {
1895
+ onClick: () => setLayout(l),
1896
+ style: {
1897
+ padding: "4px 10px",
1898
+ fontSize: 11,
1899
+ fontWeight: 600,
1900
+ fontFamily: "system-ui, -apple-system, sans-serif",
1901
+ borderRadius: 4,
1902
+ border: layout === l ? "1px solid #3b82f6" : "1px solid #d1d5db",
1903
+ backgroundColor: layout === l ? "#eff6ff" : "#fff",
1904
+ color: layout === l ? "#3b82f6" : "#6b7280",
1905
+ cursor: "pointer"
1906
+ },
1907
+ children: l === "single" ? "1-Col" : l === "two-column" ? "2-Col" : "Dashboard"
1908
+ },
1909
+ l
1910
+ )) })
1911
+ ] }),
1912
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", flex: 1, minHeight: 0 }, children: [
1913
+ /* @__PURE__ */ jsxs("div", { style: { width: catalogWidth, borderRight: "1px solid #e5e7eb", overflow: "auto", flexShrink: 0 }, children: [
1914
+ /* @__PURE__ */ jsx("div", { style: { padding: "8px 12px" }, children: /* @__PURE__ */ jsx(
1915
+ "input",
1916
+ {
1917
+ value: catalogSearch,
1918
+ onChange: (e) => setCatalogSearch(e.target.value),
1919
+ placeholder: "Search fields...",
1920
+ style: { ...inputStyle2, width: "100%", fontSize: 12 }
1921
+ }
1922
+ ) }),
1923
+ catalogLoading ? /* @__PURE__ */ jsx("div", { style: { padding: 12, fontSize: 12, color: "#9ca3af" }, children: "Loading catalog..." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1924
+ /* @__PURE__ */ jsxs("div", { style: { padding: "4px 12px" }, children: [
1925
+ /* @__PURE__ */ jsxs("div", { style: { ...sectionTitleStyle, marginBottom: 4 }, children: [
1926
+ "Metrics (",
1927
+ filteredMetrics.length,
1928
+ ")"
1929
+ ] }),
1930
+ filteredMetrics.map((f) => /* @__PURE__ */ jsx(CatalogChip, { field: f, widgets, onAddToWidget: addFieldToWidget }, f.name)),
1931
+ filteredMetrics.length === 0 && /* @__PURE__ */ jsx("div", { style: { fontSize: 11, color: "#9ca3af", padding: "4px 0" }, children: "None found" })
1932
+ ] }),
1933
+ /* @__PURE__ */ jsxs("div", { style: { padding: "4px 12px" }, children: [
1934
+ /* @__PURE__ */ jsxs("div", { style: { ...sectionTitleStyle, marginBottom: 4 }, children: [
1935
+ "Dimensions (",
1936
+ filteredDimensions.length,
1937
+ ")"
1938
+ ] }),
1939
+ filteredDimensions.map((f) => /* @__PURE__ */ jsx(
1940
+ CatalogChip,
1941
+ {
1942
+ field: f,
1943
+ widgets,
1944
+ onAddToWidget: addFieldToWidget,
1945
+ onAddAsFilter: addFilter,
1946
+ suffix: f.name === "metric_time" ? " (recommended)" : void 0
1947
+ },
1948
+ f.name
1949
+ )),
1950
+ filteredDimensions.length === 0 && /* @__PURE__ */ jsx("div", { style: { fontSize: 11, color: "#9ca3af", padding: "4px 0" }, children: "None found" })
1951
+ ] })
1344
1952
  ] })
1345
1953
  ] }),
1346
- catalog.length > 0 && /* @__PURE__ */ jsxs("div", { style: sectionStyle, children: [
1347
- /* @__PURE__ */ jsx("div", { style: sectionTitleStyle, children: "Metrics (Values)" }),
1348
- /* @__PURE__ */ jsxs("div", { style: { fontSize: 11, color: "#9ca3af", marginBottom: 8 }, children: [
1349
- "Which metrics should be queried? These become the series/values in the chart.",
1350
- analysis.metricColumns.length > 0 && /* @__PURE__ */ jsxs("span", { children: [
1351
- " (replaces: ",
1352
- analysis.metricColumns.join(", "),
1353
- ")"
1354
- ] })
1954
+ /* @__PURE__ */ jsx(
1955
+ "div",
1956
+ {
1957
+ onMouseDown: handleCatalogDragStart,
1958
+ style: {
1959
+ width: 4,
1960
+ cursor: "col-resize",
1961
+ backgroundColor: "transparent",
1962
+ flexShrink: 0,
1963
+ position: "relative"
1964
+ },
1965
+ onMouseEnter: (e) => {
1966
+ e.currentTarget.style.backgroundColor = "#d1d5db";
1967
+ },
1968
+ onMouseLeave: (e) => {
1969
+ e.currentTarget.style.backgroundColor = "transparent";
1970
+ }
1971
+ }
1972
+ ),
1973
+ /* @__PURE__ */ jsx("div", { style: { flex: 1, overflow: "auto", padding: "12px 16px" }, children: widgets.length === 0 ? /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", padding: "40px 20px", color: "#9ca3af" }, children: [
1974
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 14, fontWeight: 500, marginBottom: 12 }, children: "Compose your dashboard" }),
1975
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 12, marginBottom: 20 }, children: "Add widgets below, then pick metrics and dimensions from the catalog on the left." }),
1976
+ /* @__PURE__ */ jsx(AddWidgetBar, { onAdd: addWidget })
1977
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1978
+ widgets.map((w) => /* @__PURE__ */ jsx(
1979
+ WidgetCard,
1980
+ {
1981
+ widget: w,
1982
+ onUpdate: updateWidget,
1983
+ onRemove: removeWidget,
1984
+ onDuplicate: duplicateWidget,
1985
+ onRemoveField: removeFieldFromWidget,
1986
+ allDimensions
1987
+ },
1988
+ w.id
1989
+ )),
1990
+ /* @__PURE__ */ jsx(AddWidgetBar, { onAdd: addWidget }),
1991
+ /* @__PURE__ */ jsxs("div", { style: { marginTop: 16, padding: "12px 0", borderTop: "1px solid #f3f4f6" }, children: [
1992
+ /* @__PURE__ */ jsx("div", { style: sectionTitleStyle, children: "Shared Filters" }),
1993
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 11, color: "#9ca3af", marginBottom: 8 }, children: "Dimensions that filter all widgets together. Add from the catalog sidebar." }),
1994
+ sharedFilters.length === 0 ? /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "#d1d5db", fontStyle: "italic" }, children: "None added yet" }) : /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: sharedFilters.map((f) => /* @__PURE__ */ jsxs("div", { children: [
1995
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: /* @__PURE__ */ jsxs("span", { style: chipStyle, children: [
1996
+ f.label,
1997
+ /* @__PURE__ */ jsx("button", { onClick: () => removeFilter(f.dimension.name), style: chipRemoveStyle, children: "\xD7" })
1998
+ ] }) }),
1999
+ f.dimension.type === "time_dimension" && /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 4, marginTop: 4, marginLeft: 4 }, children: Object.keys(TIME_PRESET_LABELS).map((p) => /* @__PURE__ */ jsx(
2000
+ "button",
2001
+ {
2002
+ onClick: () => updateFilterPreset(f.dimension.name, p),
2003
+ style: {
2004
+ padding: "2px 8px",
2005
+ fontSize: 10,
2006
+ fontWeight: 600,
2007
+ fontFamily: "system-ui, -apple-system, sans-serif",
2008
+ borderRadius: 999,
2009
+ border: f.timePreset === p ? "1px solid #3b82f6" : "1px solid #e5e7eb",
2010
+ backgroundColor: f.timePreset === p ? "#eff6ff" : "#fff",
2011
+ color: f.timePreset === p ? "#3b82f6" : "#9ca3af",
2012
+ cursor: "pointer",
2013
+ transition: "all 0.1s"
2014
+ },
2015
+ children: TIME_PRESET_LABELS[p]
2016
+ },
2017
+ p
2018
+ )) })
2019
+ ] }, f.dimension.name)) })
1355
2020
  ] }),
1356
- metrics.length === 0 ? /* @__PURE__ */ jsx("div", { style: { fontSize: 12, color: "#9ca3af", fontStyle: "italic" }, children: "No metrics available in the catalog." }) : /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", gap: 4, maxHeight: 200, overflow: "auto" }, children: metrics.map((m) => {
1357
- const isSelected = selectedMetrics.some((s) => s.name === m.name);
1358
- return /* @__PURE__ */ jsxs(
1359
- "label",
2021
+ canGenerate && /* @__PURE__ */ jsxs("div", { style: { marginTop: 12 }, children: [
2022
+ /* @__PURE__ */ jsxs(
2023
+ "button",
1360
2024
  {
1361
- style: {
1362
- display: "flex",
1363
- alignItems: "center",
1364
- gap: 8,
1365
- padding: "4px 8px",
1366
- borderRadius: 4,
1367
- cursor: "pointer",
1368
- backgroundColor: isSelected ? "#eff6ff" : "transparent",
1369
- border: isSelected ? "1px solid #bfdbfe" : "1px solid transparent",
1370
- fontSize: 12
1371
- },
2025
+ onClick: () => setPreviewOpen(!previewOpen),
2026
+ style: { fontSize: 11, fontWeight: 600, color: "#6b7280", background: "none", border: "none", cursor: "pointer", padding: 0, fontFamily: "system-ui, -apple-system, sans-serif" },
1372
2027
  children: [
1373
- /* @__PURE__ */ jsx(
1374
- "input",
1375
- {
1376
- type: "checkbox",
1377
- checked: isSelected,
1378
- onChange: () => handleToggleMetric(m),
1379
- style: { margin: 0 }
1380
- }
1381
- ),
1382
- /* @__PURE__ */ jsx("span", { style: { fontWeight: 500, color: "#111827" }, children: m.displayName }),
1383
- /* @__PURE__ */ jsx("code", { style: { fontSize: 10, color: "#6366f1", fontFamily: "monospace" }, children: m.name }),
1384
- m.category && /* @__PURE__ */ jsx("span", { style: { marginLeft: "auto", fontSize: 10, color: "#9ca3af" }, children: m.category })
2028
+ previewOpen ? "\u25BC" : "\u25B6",
2029
+ " Prompt Preview"
1385
2030
  ]
1386
- },
1387
- m.name
1388
- );
1389
- }) }),
1390
- selectedMetrics.length > 0 && /* @__PURE__ */ jsxs("div", { style: { marginTop: 8, fontSize: 11, color: "#6b7280" }, children: [
1391
- "Selected: ",
1392
- selectedMetrics.map((m) => m.displayName).join(", ")
2031
+ }
2032
+ ),
2033
+ previewOpen && /* @__PURE__ */ jsx("pre", { style: { ...codePreviewStyle, marginTop: 8, maxHeight: 200, overflow: "auto", fontSize: 10 }, children: prompt })
1393
2034
  ] })
1394
- ] }),
1395
- canGenerate && /* @__PURE__ */ jsxs("div", { style: sectionStyle, children: [
1396
- /* @__PURE__ */ jsx("div", { style: sectionTitleStyle, children: "Query Preview" }),
1397
- /* @__PURE__ */ jsx("pre", { style: {
1398
- fontSize: 11,
1399
- fontFamily: "monospace",
1400
- backgroundColor: "#f9fafb",
1401
- border: "1px solid #e5e7eb",
1402
- borderRadius: 6,
1403
- padding: "8px 12px",
1404
- whiteSpace: "pre-wrap",
1405
- color: "#374151",
1406
- margin: 0
1407
- }, children: `useSemanticQuery({${selectedMetrics.length > 0 ? `
1408
- metrics: [${selectedMetrics.map((m) => `'${m.name}'`).join(", ")}],` : ""}${Array.from(dimMappings.values()).some(Boolean) ? `
1409
- groupBy: [${Array.from(dimMappings.values()).filter(Boolean).map((f) => `'${f.name}'`).join(", ")}],` : ""}${hasTimeDim && grain ? `
1410
- grain: '${grain}',` : ""}
1411
- })` })
1412
- ] })
1413
- ] }),
1414
- /* @__PURE__ */ jsx("div", { style: { padding: "12px 20px", borderTop: "1px solid #e5e7eb" }, children: /* @__PURE__ */ jsx(
2035
+ ] }) })
2036
+ ] })
2037
+ ] }),
2038
+ /* @__PURE__ */ jsxs("div", { style: { padding: "12px 20px", borderTop: "1px solid #e5e7eb", display: "flex", gap: 10 }, children: [
2039
+ /* @__PURE__ */ jsx(
1415
2040
  "button",
1416
2041
  {
1417
- onClick: handleGenerate,
2042
+ onClick: handleCopy,
1418
2043
  disabled: !canGenerate,
1419
2044
  style: {
1420
- ...generateBtnStyle,
2045
+ ...secondaryBtnStyle,
2046
+ flex: 1,
1421
2047
  opacity: canGenerate ? 1 : 0.5,
1422
2048
  cursor: canGenerate ? "pointer" : "not-allowed"
1423
2049
  },
1424
- children: copied ? "Copied to clipboard!" : "Generate Migration Prompt"
2050
+ children: copied ? "Copied!" : "Copy Prompt"
1425
2051
  }
1426
- ) })
2052
+ ),
2053
+ /* @__PURE__ */ jsxs(
2054
+ "button",
2055
+ {
2056
+ onClick: handleLovable,
2057
+ disabled: !canLovable,
2058
+ title: canGenerate && !canLovable ? "Prompt exceeds 50,000 character limit for direct send" : void 0,
2059
+ style: {
2060
+ ...primaryBtnStyle,
2061
+ flex: 1.5,
2062
+ opacity: canLovable ? 1 : 0.4,
2063
+ cursor: canLovable ? "pointer" : "not-allowed",
2064
+ display: "flex",
2065
+ alignItems: "center",
2066
+ justifyContent: "center",
2067
+ gap: 6
2068
+ },
2069
+ children: [
2070
+ lovableSent ? inIframe ? "Copied! Paste into Lovable chat" : "Opening Lovable..." : inIframe ? "Send to Lovable \u2764\uFE0F" : "Build in Lovable \u2764\uFE0F",
2071
+ !lovableSent && !inIframe && /* @__PURE__ */ jsxs("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
2072
+ /* @__PURE__ */ jsx("path", { d: "M7 17L17 7" }),
2073
+ /* @__PURE__ */ jsx("path", { d: "M7 7h10v10" })
2074
+ ] })
2075
+ ]
2076
+ }
2077
+ )
1427
2078
  ] })
1428
2079
  ] });
1429
2080
  }
2081
+ function CatalogChip({
2082
+ field,
2083
+ widgets,
2084
+ onAddToWidget,
2085
+ onAddAsFilter,
2086
+ suffix
2087
+ }) {
2088
+ const [menuOpen, setMenuOpen] = useState(false);
2089
+ const typeIcon = field.type === "metric" ? "\u2581" : field.type === "time_dimension" ? "\u25F7" : "\u2022";
2090
+ const typeColor = field.type === "metric" ? "#10b981" : field.type === "time_dimension" ? "#f59e0b" : "#6366f1";
2091
+ return /* @__PURE__ */ jsxs("div", { style: { position: "relative" }, children: [
2092
+ /* @__PURE__ */ jsxs(
2093
+ "button",
2094
+ {
2095
+ onClick: () => {
2096
+ if (widgets.length === 1) {
2097
+ onAddToWidget(widgets[0].id, field);
2098
+ } else if (widgets.length > 1) {
2099
+ setMenuOpen(!menuOpen);
2100
+ }
2101
+ },
2102
+ title: `${field.displayName}
2103
+ ${field.description}`,
2104
+ style: {
2105
+ display: "flex",
2106
+ alignItems: "center",
2107
+ gap: 4,
2108
+ width: "100%",
2109
+ textAlign: "left",
2110
+ padding: "3px 6px",
2111
+ fontSize: 11,
2112
+ fontFamily: "system-ui, -apple-system, sans-serif",
2113
+ color: "#374151",
2114
+ backgroundColor: "transparent",
2115
+ border: "none",
2116
+ borderRadius: 3,
2117
+ cursor: widgets.length > 0 ? "pointer" : "default",
2118
+ opacity: widgets.length > 0 ? 1 : 0.6
2119
+ },
2120
+ onMouseEnter: (e) => {
2121
+ e.currentTarget.style.backgroundColor = "#f3f4f6";
2122
+ },
2123
+ onMouseLeave: (e) => {
2124
+ e.currentTarget.style.backgroundColor = "transparent";
2125
+ },
2126
+ children: [
2127
+ /* @__PURE__ */ jsx("span", { style: { color: typeColor, fontSize: 10, flexShrink: 0 }, children: typeIcon }),
2128
+ /* @__PURE__ */ jsxs("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: [
2129
+ field.displayName,
2130
+ suffix && /* @__PURE__ */ jsx("span", { style: { color: "#9ca3af", fontWeight: 400 }, children: suffix })
2131
+ ] })
2132
+ ]
2133
+ }
2134
+ ),
2135
+ menuOpen && widgets.length > 1 && /* @__PURE__ */ jsxs("div", { style: { position: "absolute", left: 0, top: "100%", zIndex: 10, backgroundColor: "#fff", border: "1px solid #e5e7eb", borderRadius: 6, boxShadow: "0 4px 12px rgba(0,0,0,0.1)", minWidth: 160, padding: "4px 0" }, children: [
2136
+ widgets.map((w) => /* @__PURE__ */ jsx(
2137
+ "button",
2138
+ {
2139
+ onClick: () => {
2140
+ onAddToWidget(w.id, field);
2141
+ setMenuOpen(false);
2142
+ },
2143
+ style: { display: "block", width: "100%", textAlign: "left", padding: "4px 10px", fontSize: 11, fontFamily: "system-ui, -apple-system, sans-serif", border: "none", backgroundColor: "transparent", cursor: "pointer", color: "#374151" },
2144
+ onMouseEnter: (e) => {
2145
+ e.currentTarget.style.backgroundColor = "#f3f4f6";
2146
+ },
2147
+ onMouseLeave: (e) => {
2148
+ e.currentTarget.style.backgroundColor = "transparent";
2149
+ },
2150
+ children: w.title
2151
+ },
2152
+ w.id
2153
+ )),
2154
+ onAddAsFilter && /* @__PURE__ */ jsxs(Fragment, { children: [
2155
+ /* @__PURE__ */ jsx("div", { style: { borderTop: "1px solid #e5e7eb", margin: "4px 0" } }),
2156
+ /* @__PURE__ */ jsx(
2157
+ "button",
2158
+ {
2159
+ onClick: () => {
2160
+ onAddAsFilter(field);
2161
+ setMenuOpen(false);
2162
+ },
2163
+ style: { display: "block", width: "100%", textAlign: "left", padding: "4px 10px", fontSize: 11, fontFamily: "system-ui, -apple-system, sans-serif", border: "none", backgroundColor: "transparent", cursor: "pointer", color: "#6b7280", fontStyle: "italic" },
2164
+ onMouseEnter: (e) => {
2165
+ e.currentTarget.style.backgroundColor = "#f3f4f6";
2166
+ },
2167
+ onMouseLeave: (e) => {
2168
+ e.currentTarget.style.backgroundColor = "transparent";
2169
+ },
2170
+ children: "+ Add as shared filter"
2171
+ }
2172
+ )
2173
+ ] })
2174
+ ] })
2175
+ ] });
2176
+ }
2177
+ function AddWidgetBar({ onAdd }) {
2178
+ return /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 6, justifyContent: "center", padding: "8px 0", flexWrap: "wrap" }, children: WIDGET_TYPES.map((wt) => /* @__PURE__ */ jsxs(
2179
+ "button",
2180
+ {
2181
+ onClick: () => onAdd(wt.type),
2182
+ style: {
2183
+ display: "flex",
2184
+ flexDirection: "column",
2185
+ alignItems: "center",
2186
+ gap: 2,
2187
+ padding: "6px 12px",
2188
+ fontSize: 10,
2189
+ fontWeight: 600,
2190
+ fontFamily: "system-ui, -apple-system, sans-serif",
2191
+ color: "#6b7280",
2192
+ backgroundColor: "#fff",
2193
+ border: "1px dashed #d1d5db",
2194
+ borderRadius: 6,
2195
+ cursor: "pointer",
2196
+ transition: "all 0.1s",
2197
+ minWidth: 56
2198
+ },
2199
+ onMouseEnter: (e) => {
2200
+ const el = e.currentTarget;
2201
+ el.style.borderColor = "#3b82f6";
2202
+ el.style.color = "#3b82f6";
2203
+ },
2204
+ onMouseLeave: (e) => {
2205
+ const el = e.currentTarget;
2206
+ el.style.borderColor = "#d1d5db";
2207
+ el.style.color = "#6b7280";
2208
+ },
2209
+ children: [
2210
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 16, lineHeight: 1 }, children: wt.icon }),
2211
+ wt.label
2212
+ ]
2213
+ },
2214
+ wt.type
2215
+ )) });
2216
+ }
2217
+ function WidgetCard({
2218
+ widget,
2219
+ onUpdate,
2220
+ onRemove,
2221
+ onDuplicate,
2222
+ onRemoveField,
2223
+ allDimensions
2224
+ }) {
2225
+ const [collapsed, setCollapsed] = useState(false);
2226
+ const hasTimeDim = widget.groupBy.some((d) => d.type === "time_dimension");
2227
+ return /* @__PURE__ */ jsxs("div", { style: { border: "1px solid #e5e7eb", borderRadius: 8, marginBottom: 10, backgroundColor: "#fff" }, children: [
2228
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8, padding: "8px 12px", borderBottom: collapsed ? "none" : "1px solid #f3f4f6" }, children: [
2229
+ /* @__PURE__ */ jsx("button", { onClick: () => setCollapsed(!collapsed), style: { background: "none", border: "none", cursor: "pointer", padding: 0, fontSize: 10, color: "#9ca3af" }, children: collapsed ? "\u25B6" : "\u25BC" }),
2230
+ /* @__PURE__ */ jsx(
2231
+ "input",
2232
+ {
2233
+ value: widget.title,
2234
+ onChange: (e) => onUpdate(widget.id, { title: e.target.value }),
2235
+ style: { ...inputStyle2, flex: 1, fontSize: 13, fontWeight: 600, padding: "2px 4px" }
2236
+ }
2237
+ ),
2238
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 2 }, children: WIDGET_TYPES.map((wt) => /* @__PURE__ */ jsx(
2239
+ "button",
2240
+ {
2241
+ onClick: () => onUpdate(widget.id, { type: wt.type }),
2242
+ title: wt.label,
2243
+ style: {
2244
+ width: 24,
2245
+ height: 24,
2246
+ display: "flex",
2247
+ alignItems: "center",
2248
+ justifyContent: "center",
2249
+ fontSize: 12,
2250
+ borderRadius: 4,
2251
+ border: widget.type === wt.type ? "1px solid #3b82f6" : "1px solid transparent",
2252
+ backgroundColor: widget.type === wt.type ? "#eff6ff" : "transparent",
2253
+ color: widget.type === wt.type ? "#3b82f6" : "#9ca3af",
2254
+ cursor: "pointer"
2255
+ },
2256
+ children: wt.icon
2257
+ },
2258
+ wt.type
2259
+ )) }),
2260
+ /* @__PURE__ */ jsx("button", { onClick: () => onDuplicate(widget.id), style: { background: "none", border: "none", cursor: "pointer", fontSize: 12, color: "#d1d5db", padding: "0 2px" }, title: "Duplicate widget", children: "\u2398" }),
2261
+ /* @__PURE__ */ jsx("button", { onClick: () => onRemove(widget.id), style: { background: "none", border: "none", cursor: "pointer", fontSize: 14, color: "#d1d5db", padding: "0 2px" }, title: "Remove widget", children: "\xD7" })
2262
+ ] }),
2263
+ !collapsed && /* @__PURE__ */ jsxs("div", { style: { padding: "8px 12px" }, children: [
2264
+ /* @__PURE__ */ jsxs("div", { style: { marginBottom: 8 }, children: [
2265
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 10, fontWeight: 600, color: "#6b7280", textTransform: "uppercase", letterSpacing: "0.05em", marginBottom: 4 }, children: "Metrics" }),
2266
+ widget.metrics.length === 0 ? /* @__PURE__ */ jsx("div", { style: { fontSize: 11, color: "#d1d5db", fontStyle: "italic" }, children: "Click a metric in the catalog to add" }) : /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 4, flexWrap: "wrap" }, children: widget.metrics.map((m) => /* @__PURE__ */ jsxs("span", { style: { ...chipStyle, backgroundColor: "#ecfdf5", borderColor: "#a7f3d0", color: "#065f46" }, children: [
2267
+ m.displayName,
2268
+ /* @__PURE__ */ jsx("button", { onClick: () => onRemoveField(widget.id, m.name, "metric"), style: chipRemoveStyle, children: "\xD7" })
2269
+ ] }, m.name)) })
2270
+ ] }),
2271
+ /* @__PURE__ */ jsxs("div", { style: { marginBottom: 8 }, children: [
2272
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 10, fontWeight: 600, color: "#6b7280", textTransform: "uppercase", letterSpacing: "0.05em", marginBottom: 4 }, children: "Group By" }),
2273
+ widget.groupBy.length === 0 ? /* @__PURE__ */ jsx("div", { style: { fontSize: 11, color: "#d1d5db", fontStyle: "italic" }, children: "Click a dimension in the catalog to add" }) : /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 4, flexWrap: "wrap" }, children: widget.groupBy.map((d) => /* @__PURE__ */ jsxs("span", { style: { ...chipStyle, backgroundColor: "#eef2ff", borderColor: "#c7d2fe", color: "#3730a3" }, children: [
2274
+ d.displayName,
2275
+ /* @__PURE__ */ jsx("button", { onClick: () => onRemoveField(widget.id, d.name, "groupBy"), style: chipRemoveStyle, children: "\xD7" })
2276
+ ] }, d.name)) })
2277
+ ] }),
2278
+ hasTimeDim && /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [
2279
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 10, fontWeight: 600, color: "#6b7280", textTransform: "uppercase" }, children: "Grain" }),
2280
+ /* @__PURE__ */ jsxs(
2281
+ "select",
2282
+ {
2283
+ value: widget.grain ?? "",
2284
+ onChange: (e) => onUpdate(widget.id, { grain: e.target.value || null }),
2285
+ style: { ...selectStyle2, width: "auto", fontSize: 11 },
2286
+ children: [
2287
+ /* @__PURE__ */ jsx("option", { value: "", children: "None" }),
2288
+ /* @__PURE__ */ jsx("option", { value: "day", children: "Day" }),
2289
+ /* @__PURE__ */ jsx("option", { value: "week", children: "Week" }),
2290
+ /* @__PURE__ */ jsx("option", { value: "month", children: "Month" }),
2291
+ /* @__PURE__ */ jsx("option", { value: "quarter", children: "Quarter" }),
2292
+ /* @__PURE__ */ jsx("option", { value: "year", children: "Year" })
2293
+ ]
2294
+ }
2295
+ )
2296
+ ] })
2297
+ ] })
2298
+ ] });
2299
+ }
2300
+ async function copyToClipboard(text) {
2301
+ try {
2302
+ await navigator.clipboard.writeText(text);
2303
+ } catch {
2304
+ const ta = document.createElement("textarea");
2305
+ ta.value = text;
2306
+ document.body.appendChild(ta);
2307
+ ta.select();
2308
+ document.execCommand("copy");
2309
+ document.body.removeChild(ta);
2310
+ }
2311
+ }
1430
2312
  var backdropStyle = {
1431
2313
  position: "fixed",
1432
2314
  inset: 0,
@@ -1434,12 +2316,9 @@ var backdropStyle = {
1434
2316
  zIndex: 1e5
1435
2317
  };
1436
2318
  var modalStyle = {
1437
- position: "fixed",
1438
- top: "50%",
1439
- left: "50%",
1440
- transform: "translate(-50%, -50%)",
1441
2319
  zIndex: 100001,
1442
- width: "min(700px, 90vw)",
2320
+ width: "min(900px, 95vw)",
2321
+ height: "85vh",
1443
2322
  maxHeight: "85vh",
1444
2323
  backgroundColor: "#fff",
1445
2324
  borderRadius: 12,
@@ -1452,8 +2331,8 @@ var modalStyle = {
1452
2331
  var modalHeaderStyle = {
1453
2332
  display: "flex",
1454
2333
  justifyContent: "space-between",
1455
- alignItems: "flex-start",
1456
- padding: "16px 20px",
2334
+ alignItems: "center",
2335
+ padding: "0 20px",
1457
2336
  borderBottom: "1px solid #e5e7eb"
1458
2337
  };
1459
2338
  var closeBtnStyle = {
@@ -1469,7 +2348,7 @@ var sectionStyle = {
1469
2348
  padding: "12px 20px"
1470
2349
  };
1471
2350
  var sectionTitleStyle = {
1472
- fontSize: 11,
2351
+ fontSize: 10,
1473
2352
  fontWeight: 700,
1474
2353
  textTransform: "uppercase",
1475
2354
  letterSpacing: "0.05em",
@@ -1513,7 +2392,6 @@ var mappingTdStyle = {
1513
2392
  verticalAlign: "middle"
1514
2393
  };
1515
2394
  var selectStyle2 = {
1516
- width: "100%",
1517
2395
  padding: "4px 6px",
1518
2396
  fontSize: 12,
1519
2397
  border: "1px solid #d1d5db",
@@ -1521,17 +2399,69 @@ var selectStyle2 = {
1521
2399
  backgroundColor: "#fff",
1522
2400
  color: "#111827"
1523
2401
  };
1524
- var generateBtnStyle = {
1525
- width: "100%",
2402
+ var inputStyle2 = {
2403
+ padding: "4px 8px",
2404
+ fontSize: 12,
2405
+ border: "1px solid #d1d5db",
2406
+ borderRadius: 4,
2407
+ backgroundColor: "#fff",
2408
+ color: "#111827",
2409
+ outline: "none",
2410
+ fontFamily: "system-ui, -apple-system, sans-serif"
2411
+ };
2412
+ var codePreviewStyle = {
2413
+ fontSize: 11,
2414
+ fontFamily: "monospace",
2415
+ backgroundColor: "#f9fafb",
2416
+ border: "1px solid #e5e7eb",
2417
+ borderRadius: 6,
2418
+ padding: "8px 12px",
2419
+ whiteSpace: "pre-wrap",
2420
+ color: "#374151",
2421
+ margin: 0
2422
+ };
2423
+ var primaryBtnStyle = {
1526
2424
  padding: "10px 16px",
1527
2425
  fontSize: 14,
1528
2426
  fontWeight: 600,
1529
2427
  color: "#fff",
1530
2428
  backgroundColor: "#3b82f6",
1531
2429
  border: "none",
1532
- borderRadius: 8
2430
+ borderRadius: 8,
2431
+ fontFamily: "system-ui, -apple-system, sans-serif"
2432
+ };
2433
+ var secondaryBtnStyle = {
2434
+ padding: "10px 16px",
2435
+ fontSize: 14,
2436
+ fontWeight: 600,
2437
+ color: "#374151",
2438
+ backgroundColor: "#fff",
2439
+ border: "1px solid #d1d5db",
2440
+ borderRadius: 8,
2441
+ fontFamily: "system-ui, -apple-system, sans-serif"
2442
+ };
2443
+ var chipStyle = {
2444
+ display: "inline-flex",
2445
+ alignItems: "center",
2446
+ gap: 4,
2447
+ padding: "2px 8px",
2448
+ fontSize: 11,
2449
+ fontWeight: 500,
2450
+ borderRadius: 999,
2451
+ border: "1px solid #e5e7eb",
2452
+ backgroundColor: "#f9fafb",
2453
+ color: "#374151"
2454
+ };
2455
+ var chipRemoveStyle = {
2456
+ background: "none",
2457
+ border: "none",
2458
+ cursor: "pointer",
2459
+ fontSize: 12,
2460
+ color: "#9ca3af",
2461
+ padding: 0,
2462
+ lineHeight: 1
1533
2463
  };
1534
2464
 
1535
- export { AdminDashboard, DataCatalog, DataInspector, Inspectable, MetricPicker, ResultsTable, analyzeChartData, generateMigrationPrompt, matchField, matchFields };
2465
+ export { AdminDashboard, DataCatalog, DataInspector, Inspectable, MetricPicker, ResultsTable, analyzeChartData, canOpenInLovable, generateMigrationPrompt, generateReportPrompt, isRunningInIframe, matchField, matchFields, openInLovable };
1536
2466
  //# sourceMappingURL=components.js.map
1537
2467
  //# sourceMappingURL=components.js.map