@schandlergarcia/sf-web-components 1.2.5 → 1.2.7

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 (165) hide show
  1. package/package.json +2 -1
  2. package/scripts/postinstall.mjs +69 -93
  3. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Account.cls +196 -0
  4. package/src/components/library/.sfdx/tools/sobjects/standardObjects/AccountHistory.cls +25 -0
  5. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Asset.cls +138 -0
  6. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Attachment.cls +35 -0
  7. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Case.cls +111 -0
  8. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Contact.cls +167 -0
  9. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Contract.cls +96 -0
  10. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Domain.cls +29 -0
  11. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Lead.cls +128 -0
  12. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Note.cls +32 -0
  13. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Opportunity.cls +113 -0
  14. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Order.cls +127 -0
  15. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Pricebook2.cls +47 -0
  16. package/src/components/library/.sfdx/tools/sobjects/standardObjects/PricebookEntry.cls +47 -0
  17. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Product2.cls +91 -0
  18. package/src/components/library/.sfdx/tools/sobjects/standardObjects/RecordType.cls +35 -0
  19. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Report.cls +47 -0
  20. package/src/components/library/.sfdx/tools/sobjects/standardObjects/Task.cls +79 -0
  21. package/src/components/library/.sfdx/tools/sobjects/standardObjects/User.cls +2318 -0
  22. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Account.json +2952 -0
  23. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/AccountHistory.json +875 -0
  24. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Asset.json +1699 -0
  25. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Attachment.json +362 -0
  26. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Case.json +1371 -0
  27. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Contact.json +2309 -0
  28. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Contract.json +1304 -0
  29. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Domain.json +293 -0
  30. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Lead.json +1977 -0
  31. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Note.json +303 -0
  32. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Opportunity.json +1470 -0
  33. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Order.json +1646 -0
  34. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Pricebook2.json +482 -0
  35. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/PricebookEntry.json +433 -0
  36. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Product2.json +1039 -0
  37. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/RecordType.json +2576 -0
  38. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Report.json +486 -0
  39. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/Task.json +4296 -0
  40. package/src/components/library/.sfdx/tools/soqlMetadata/standardObjects/User.json +30415 -0
  41. package/src/components/library/.sfdx/tools/soqlMetadata/typeNames.json +78 -0
  42. package/src/components/library/.sfdx/typings/lwc/sobjects/Account.d.ts +264 -0
  43. package/src/components/library/.sfdx/typings/lwc/sobjects/AccountHistory.d.ts +44 -0
  44. package/src/components/library/.sfdx/typings/lwc/sobjects/Asset.d.ts +240 -0
  45. package/src/components/library/.sfdx/typings/lwc/sobjects/Attachment.d.ts +76 -0
  46. package/src/components/library/.sfdx/typings/lwc/sobjects/Case.d.ts +172 -0
  47. package/src/components/library/.sfdx/typings/lwc/sobjects/Contact.d.ts +264 -0
  48. package/src/components/library/.sfdx/typings/lwc/sobjects/Contract.d.ts +188 -0
  49. package/src/components/library/.sfdx/typings/lwc/sobjects/Domain.d.ts +52 -0
  50. package/src/components/library/.sfdx/typings/lwc/sobjects/Lead.d.ts +252 -0
  51. package/src/components/library/.sfdx/typings/lwc/sobjects/Note.d.ts +64 -0
  52. package/src/components/library/.sfdx/typings/lwc/sobjects/Opportunity.d.ts +200 -0
  53. package/src/components/library/.sfdx/typings/lwc/sobjects/Order.d.ts +260 -0
  54. package/src/components/library/.sfdx/typings/lwc/sobjects/Pricebook2.d.ts +64 -0
  55. package/src/components/library/.sfdx/typings/lwc/sobjects/PricebookEntry.d.ts +76 -0
  56. package/src/components/library/.sfdx/typings/lwc/sobjects/Product2.d.ts +96 -0
  57. package/src/components/library/.sfdx/typings/lwc/sobjects/RecordType.d.ts +64 -0
  58. package/src/components/library/.sfdx/typings/lwc/sobjects/Report.d.ts +80 -0
  59. package/src/components/library/.sfdx/typings/lwc/sobjects/Task.d.ts +184 -0
  60. package/src/components/library/.sfdx/typings/lwc/sobjects/User.d.ts +752 -0
  61. package/src/components/library/cards/ActionList.jsx +38 -0
  62. package/src/components/library/cards/ActivityCard.jsx +56 -0
  63. package/src/components/library/cards/BaseCard.jsx +109 -0
  64. package/src/components/library/cards/CalloutCard.jsx +37 -0
  65. package/src/components/library/cards/ChartCard.jsx +105 -0
  66. package/src/components/library/cards/FeedPanel.jsx +39 -0
  67. package/src/components/library/cards/ListCard.jsx +193 -0
  68. package/src/components/library/cards/MetricCard.jsx +109 -0
  69. package/src/components/library/cards/MetricsStrip.jsx +78 -0
  70. package/src/components/library/cards/SectionCard.jsx +83 -0
  71. package/src/components/library/cards/SemanticMetricCard.jsx +52 -0
  72. package/src/components/library/cards/SemanticMetricCardWithLoading.jsx +23 -0
  73. package/src/components/library/cards/SemanticTableCard.jsx +48 -0
  74. package/src/components/library/cards/SemanticTableCardWithLoading.jsx +22 -0
  75. package/src/components/library/cards/StatusCard.jsx +220 -0
  76. package/src/components/library/cards/TableCard.jsx +337 -0
  77. package/src/components/library/cards/WidgetCard.jsx +90 -0
  78. package/src/components/library/charts/D3Chart.jsx +109 -0
  79. package/src/components/library/charts/D3ChartTemplates.jsx +126 -0
  80. package/src/components/library/charts/GeoMap.jsx +293 -0
  81. package/src/components/library/chat/ChatBar.jsx +256 -0
  82. package/src/components/library/chat/ChatInput.jsx +89 -0
  83. package/src/components/library/chat/ChatMessage.jsx +178 -0
  84. package/src/components/library/chat/ChatMessageList.jsx +73 -0
  85. package/src/components/library/chat/ChatPanel.jsx +97 -0
  86. package/src/components/library/chat/ChatSuggestions.jsx +28 -0
  87. package/src/components/library/chat/ChatToolCall.jsx +100 -0
  88. package/src/components/library/chat/ChatTypingIndicator.jsx +23 -0
  89. package/src/components/library/chat/ChatWelcome.jsx +43 -0
  90. package/src/components/library/chat/index.jsx +10 -0
  91. package/src/components/library/chat/useChatState.jsx +130 -0
  92. package/src/components/library/data/DataModeProvider.jsx +67 -0
  93. package/src/components/library/data/DataModeToggle.jsx +36 -0
  94. package/src/components/library/data/chartDataProvider.jsx +61 -0
  95. package/src/components/library/data/filterUtils.jsx +141 -0
  96. package/src/components/library/data/useDataSource.jsx +33 -0
  97. package/src/components/library/data/usePageFilters.jsx +99 -0
  98. package/src/components/library/filters/FilterBar.jsx +95 -0
  99. package/src/components/library/filters/SearchFilter.jsx +36 -0
  100. package/src/components/library/filters/SelectFilter.jsx +55 -0
  101. package/src/components/library/filters/ToggleFilter.jsx +52 -0
  102. package/src/components/library/filters/index.jsx +4 -0
  103. package/src/components/library/forms/FormField.jsx +291 -0
  104. package/src/components/library/forms/FormModal.jsx +201 -0
  105. package/src/components/library/forms/FormRenderer.jsx +46 -0
  106. package/src/components/library/forms/FormSection.jsx +69 -0
  107. package/src/components/library/forms/index.jsx +5 -0
  108. package/src/components/library/forms/useFormState.jsx +165 -0
  109. package/src/components/library/heroui/Accordion.jsx +26 -0
  110. package/src/components/library/heroui/Alert.jsx +8 -0
  111. package/src/components/library/heroui/Badge.jsx +8 -0
  112. package/src/components/library/heroui/Breadcrumbs.jsx +22 -0
  113. package/src/components/library/heroui/Button.jsx +58 -0
  114. package/src/components/library/heroui/Card.jsx +8 -0
  115. package/src/components/library/heroui/Collapsible.jsx +42 -0
  116. package/src/components/library/heroui/DatePicker.jsx +34 -0
  117. package/src/components/library/heroui/Dialog.jsx +37 -0
  118. package/src/components/library/heroui/Drawer.jsx +32 -0
  119. package/src/components/library/heroui/Dropdown.jsx +28 -0
  120. package/src/components/library/heroui/Field.jsx +51 -0
  121. package/src/components/library/heroui/Input.jsx +6 -0
  122. package/src/components/library/heroui/Kbd.jsx +8 -0
  123. package/src/components/library/heroui/Meter.jsx +8 -0
  124. package/src/components/library/heroui/Modal.jsx +32 -0
  125. package/src/components/library/heroui/Pagination.jsx +8 -0
  126. package/src/components/library/heroui/Popover.jsx +64 -0
  127. package/src/components/library/heroui/ProgressBar.jsx +8 -0
  128. package/src/components/library/heroui/ProgressCircle.jsx +8 -0
  129. package/src/components/library/heroui/ScrollShadow.jsx +8 -0
  130. package/src/components/library/heroui/Select.jsx +37 -0
  131. package/src/components/library/heroui/Separator.jsx +8 -0
  132. package/src/components/library/heroui/Skeleton.jsx +8 -0
  133. package/src/components/library/heroui/Tabs.jsx +26 -0
  134. package/src/components/library/heroui/Toast.jsx +25 -0
  135. package/src/components/library/heroui/Toggle.jsx +14 -0
  136. package/src/components/library/heroui/Tooltip.jsx +21 -0
  137. package/src/components/library/index.jsx +149 -0
  138. package/src/components/library/layout/PageContainer.jsx +11 -0
  139. package/src/components/library/skeletons/CardSkeleton.jsx +30 -0
  140. package/src/components/library/theme/AppThemeProvider.jsx +67 -0
  141. package/src/components/library/theme/tokens.jsx +72 -0
  142. package/src/components/library/ui/Alert.jsx +80 -0
  143. package/src/components/library/ui/Avatar.jsx +44 -0
  144. package/src/components/library/ui/BreadcrumbExtras.tsx +119 -0
  145. package/src/components/library/ui/Card.jsx +117 -0
  146. package/src/components/library/ui/Checkbox.jsx +17 -0
  147. package/src/components/library/ui/Chip.jsx +38 -0
  148. package/src/components/library/ui/Collapsible.tsx +31 -0
  149. package/src/components/library/ui/Container.jsx +56 -0
  150. package/src/components/library/ui/DatePicker.tsx +34 -0
  151. package/src/components/library/ui/Dialog.tsx +141 -0
  152. package/src/components/library/ui/EmptyState.jsx +46 -0
  153. package/src/components/library/ui/Field.tsx +82 -0
  154. package/src/components/library/ui/FieldGroup.jsx +17 -0
  155. package/src/components/library/ui/Label.jsx +22 -0
  156. package/src/components/library/ui/PaginationExtras.tsx +143 -0
  157. package/src/components/library/ui/Popover.tsx +39 -0
  158. package/src/components/library/ui/Select.tsx +113 -0
  159. package/src/components/library/ui/Spinner.jsx +64 -0
  160. package/src/components/library/ui/Text.jsx +46 -0
  161. package/src/components/library/ui/UIButton.jsx +61 -0
  162. package/src/components/library/ui/UIInput.jsx +21 -0
  163. package/src/components/workspace/ComponentRegistry.jsx +297 -0
  164. package/src/templates/pages/Home.tsx.template +5 -5
  165. package/src/templates/pages/NotFound.tsx.template +2 -2
@@ -0,0 +1,293 @@
1
+ import React, { useMemo, useRef, useEffect, useState, useCallback } from "react";
2
+ import * as d3 from "d3";
3
+ import { feature } from "topojson-client";
4
+ import world from "world-atlas/land-110m.json";
5
+
6
+ const land = feature(world, world.objects.land);
7
+
8
+ const PROJECTIONS = {
9
+ naturalEarth: d3.geoNaturalEarth1,
10
+ mercator: d3.geoMercator,
11
+ equirectangular: d3.geoEquirectangular,
12
+ };
13
+
14
+ const THEMES = {
15
+ dark: {
16
+ bg: "#050b15",
17
+ bgGradient: ["#0a1628", "#050b15"],
18
+ land: "#1a2d4a",
19
+ landStroke: "#2a4060",
20
+ sphere: "#1e3a5f",
21
+ graticule: "#162a45",
22
+ graticuleOpacity: 0.35,
23
+ markerActive: "#cbd5e1",
24
+ markerInactive: "#64748b",
25
+ label: "#cbd5e1",
26
+ labelInactive: "#64748b",
27
+ arc: "#818cf8",
28
+ arcHighlight: "#c4b5fd",
29
+ arcDanger: "#f87171",
30
+ dot: "#a5b4fc",
31
+ dotDanger: "#f87171",
32
+ overlayFill: "rgba(248,113,113,0.10)",
33
+ overlayStroke: "rgba(248,113,113,0.30)",
34
+ },
35
+ light: {
36
+ bg: "#f8fafc",
37
+ bgGradient: ["#f8fafc", "#f1f5f9"],
38
+ land: "#e2e8f0",
39
+ landStroke: "#cbd5e1",
40
+ sphere: "#cbd5e1",
41
+ graticule: "#e2e8f0",
42
+ graticuleOpacity: 0.6,
43
+ markerActive: "#334155",
44
+ markerInactive: "#94a3b8",
45
+ label: "#334155",
46
+ labelInactive: "#94a3b8",
47
+ arc: "#6366f1",
48
+ arcHighlight: "#4f46e5",
49
+ arcDanger: "#ef4444",
50
+ dot: "#6366f1",
51
+ dotDanger: "#ef4444",
52
+ overlayFill: "rgba(239,68,68,0.08)",
53
+ overlayStroke: "rgba(239,68,68,0.3)",
54
+ },
55
+ };
56
+
57
+ function buildProjection(type, width, height) {
58
+ const factory = PROJECTIONS[type] ?? PROJECTIONS.naturalEarth;
59
+ return factory().fitSize([width, height], { type: "Sphere" });
60
+ }
61
+
62
+ export default function GeoMap({
63
+ width = 960,
64
+ height = 480,
65
+ projection: projType = "naturalEarth",
66
+ theme = "dark",
67
+ markers = [],
68
+ arcs = [],
69
+ overlays = [],
70
+ selectedId = null,
71
+ onArcClick,
72
+ onMarkerClick,
73
+ zoomable = true,
74
+ minZoom = 1,
75
+ maxZoom = 8,
76
+ initialBounds = null,
77
+ className = "",
78
+ children,
79
+ }) {
80
+ const t = THEMES[theme] ?? THEMES.dark;
81
+ const svgRef = useRef(null);
82
+ const [transform, setTransform] = useState(d3.zoomIdentity);
83
+ const zoomRef = useRef(null);
84
+
85
+ const { proj, pathGen, graticulePath, spherePath, landPath } = useMemo(() => {
86
+ const proj = buildProjection(projType, width, height);
87
+ const pathGen = d3.geoPath(proj);
88
+ return {
89
+ proj,
90
+ pathGen,
91
+ graticulePath: pathGen(d3.geoGraticule10()),
92
+ spherePath: pathGen({ type: "Sphere" }),
93
+ landPath: pathGen(land),
94
+ };
95
+ }, [projType, width, height]);
96
+
97
+ // D3 zoom behavior
98
+ useEffect(() => {
99
+ if (!zoomable || !svgRef.current) return;
100
+ const svg = d3.select(svgRef.current);
101
+ const zoom = d3.zoom()
102
+ .scaleExtent([minZoom, maxZoom])
103
+ .on("zoom", (e) => setTransform(e.transform));
104
+ zoomRef.current = zoom;
105
+ svg.call(zoom);
106
+ return () => svg.on(".zoom", null);
107
+ }, [zoomable, minZoom, maxZoom]);
108
+
109
+ // Apply initial zoom to fit bounds (markers/region) on mount
110
+ const initialBoundsApplied = useRef(false);
111
+ useEffect(() => {
112
+ if (!initialBounds || initialBoundsApplied.current) return;
113
+ if (!svgRef.current || !zoomRef.current || !proj) return;
114
+ const { sw, ne, padding = 40 } = initialBounds; // sw=[lonMin,latMin], ne=[lonMax,latMax]
115
+ const p0 = proj(sw);
116
+ const p1 = proj(ne);
117
+ if (!p0 || !p1) return;
118
+ const bx0 = Math.min(p0[0], p1[0]);
119
+ const by0 = Math.min(p0[1], p1[1]);
120
+ const bx1 = Math.max(p0[0], p1[0]);
121
+ const by1 = Math.max(p0[1], p1[1]);
122
+ const bw = bx1 - bx0;
123
+ const bh = by1 - by0;
124
+ if (bw < 1 || bh < 1) return;
125
+ const scale = Math.min((width - padding * 2) / bw, (height - padding * 2) / bh);
126
+ const cx = (bx0 + bx1) / 2;
127
+ const cy = (by0 + by1) / 2;
128
+ const tx = width / 2 - cx * scale;
129
+ const ty = height / 2 - cy * scale;
130
+ const clampedScale = Math.max(minZoom, Math.min(maxZoom, scale));
131
+ const t = d3.zoomIdentity.translate(tx, ty).scale(clampedScale);
132
+ d3.select(svgRef.current).call(zoomRef.current.transform, t);
133
+ initialBoundsApplied.current = true;
134
+ }, [initialBounds, proj, width, height, minZoom, maxZoom]);
135
+
136
+ const resetZoom = useCallback(() => {
137
+ if (!svgRef.current || !zoomRef.current) return;
138
+ d3.select(svgRef.current)
139
+ .transition()
140
+ .duration(400)
141
+ .call(zoomRef.current.transform, d3.zoomIdentity);
142
+ }, []);
143
+
144
+ const isZoomed = transform.k !== 1 || transform.x !== 0 || transform.y !== 0;
145
+
146
+ const arcPaths = useMemo(() => {
147
+ const cache = {};
148
+ arcs.forEach(a => {
149
+ const key = `${a.from[0]},${a.from[1]}-${a.to[0]},${a.to[1]}`;
150
+ if (cache[key]) { a._path = cache[key]; return; }
151
+ const interp = d3.geoInterpolate(a.from, a.to);
152
+ const pts = [];
153
+ for (let i = 0; i <= 1; i += 0.02) {
154
+ const p = proj(interp(i));
155
+ if (p) pts.push(p);
156
+ }
157
+ const path = pts.length > 1 ? d3.line()(pts) : null;
158
+ cache[key] = path;
159
+ a._path = path;
160
+ });
161
+ return arcs;
162
+ }, [arcs, proj]);
163
+
164
+ const activeMarkerIds = useMemo(() => {
165
+ const s = new Set();
166
+ markers.forEach(m => { if (m.active) s.add(m.id); });
167
+ return s;
168
+ }, [markers]);
169
+
170
+ const txStr = `translate(${transform.x},${transform.y}) scale(${transform.k})`;
171
+ const invScale = 1 / transform.k;
172
+
173
+ return (
174
+ <div className={`relative overflow-hidden ${className}`} style={{ background: t.bg }}>
175
+ <svg ref={svgRef} viewBox={`0 0 ${width} ${height}`} className="h-full w-full" preserveAspectRatio="xMidYMid slice" style={zoomable ? { cursor: "grab" } : undefined}>
176
+ <defs>
177
+ <radialGradient id="geo-bg" cx="50%" cy="50%" r="55%">
178
+ <stop offset="0%" stopColor={t.bgGradient[0]} />
179
+ <stop offset="100%" stopColor={t.bgGradient[1]} />
180
+ </radialGradient>
181
+ <filter id="geo-glow" x="-50%" y="-50%" width="200%" height="200%">
182
+ <feGaussianBlur in="SourceGraphic" stdDeviation="3" result="b" />
183
+ <feMerge><feMergeNode in="b" /><feMergeNode in="SourceGraphic" /></feMerge>
184
+ </filter>
185
+ </defs>
186
+ <style>{`
187
+ .geo-arc{stroke-dasharray:8,6;animation:geo-flow 1.5s linear infinite}
188
+ @keyframes geo-flow{to{stroke-dashoffset:-14}}
189
+ .geo-dot{animation:geo-pulse 2s ease-in-out infinite}
190
+ @keyframes geo-pulse{0%,100%{opacity:1}50%{opacity:.5}}
191
+ `}</style>
192
+
193
+ <rect width={width} height={height} fill="url(#geo-bg)" />
194
+
195
+ {/* Zoomable content group */}
196
+ <g transform={txStr}>
197
+ <path d={spherePath} fill="none" stroke={t.sphere} strokeWidth={0.8 * invScale} />
198
+ <path d={landPath} fill={t.land} stroke={t.landStroke} strokeWidth={0.4 * invScale} />
199
+ <path d={graticulePath} fill="none" stroke={t.graticule} strokeWidth={0.3 * invScale} opacity={t.graticuleOpacity} />
200
+
201
+ {/* Overlay zones (disruptions, weather) */}
202
+ {overlays.map(o => {
203
+ const c = proj(o.center);
204
+ if (!c) return null;
205
+ const edge = proj([o.center[0] + (o.radius ?? 5), o.center[1]]);
206
+ const r = edge ? Math.abs(edge[0] - c[0]) : 30;
207
+ return (
208
+ <circle
209
+ key={o.id}
210
+ cx={c[0]} cy={c[1]} r={r}
211
+ fill={o.fill ?? t.overlayFill}
212
+ stroke={o.stroke ?? t.overlayStroke}
213
+ strokeWidth={1 * invScale}
214
+ strokeDasharray="5,5"
215
+ className="animate-pulse"
216
+ />
217
+ );
218
+ })}
219
+
220
+ {/* Arcs */}
221
+ {arcPaths.map(a => {
222
+ if (!a._path) return null;
223
+ const sel = selectedId === a.id;
224
+ const danger = a.danger;
225
+ return (
226
+ <path
227
+ key={`arc-${a.id}`}
228
+ d={a._path}
229
+ fill="none"
230
+ stroke={danger ? t.arcDanger : sel ? t.arcHighlight : a.color ?? t.arc}
231
+ strokeWidth={(sel ? 3 : 2) * invScale}
232
+ opacity={selectedId != null && !sel ? 0.15 : danger ? 0.85 : 0.65}
233
+ className="geo-arc cursor-pointer"
234
+ onClick={() => onArcClick?.(a)}
235
+ />
236
+ );
237
+ })}
238
+
239
+ {/* Markers */}
240
+ {markers.map(m => {
241
+ const p = proj([m.lon, m.lat]);
242
+ if (!p) return null;
243
+ const active = m.active ?? activeMarkerIds.has(m.id);
244
+ return (
245
+ <g key={m.id} className={onMarkerClick ? "cursor-pointer" : ""} onClick={() => onMarkerClick?.(m)}>
246
+ <circle cx={p[0]} cy={p[1]} r={(active ? 3 : 2) * invScale} fill={active ? t.markerActive : t.markerInactive} />
247
+ {m.label !== false && (
248
+ <text x={p[0] + 6 * invScale} y={p[1] + 4 * invScale} fontSize={8 * invScale} fill={active ? t.label : t.labelInactive} fontFamily="sans-serif" fontWeight={600}>
249
+ {m.label ?? m.id}
250
+ </text>
251
+ )}
252
+ </g>
253
+ );
254
+ })}
255
+
256
+ {/* Moving dots (flight positions) */}
257
+ {arcPaths.map(a => {
258
+ if (a.progress == null || a.progress <= 0) return null;
259
+ const interp = d3.geoInterpolate(a.from, a.to);
260
+ const p = proj(interp(Math.min(a.progress, 0.99)));
261
+ if (!p) return null;
262
+ const sel = selectedId === a.id;
263
+ return (
264
+ <circle
265
+ key={`dot-${a.id}`}
266
+ cx={p[0]} cy={p[1]}
267
+ r={(sel ? 6 : 4.5) * invScale}
268
+ fill={a.danger ? t.dotDanger : a.dotColor ?? t.dot}
269
+ filter="url(#geo-glow)"
270
+ className="geo-dot cursor-pointer"
271
+ opacity={selectedId != null && !sel ? 0.3 : 1}
272
+ onClick={() => onArcClick?.(a)}
273
+ />
274
+ );
275
+ })}
276
+
277
+ {/* Custom children get access to projection */}
278
+ {typeof children === "function" ? children({ proj, pathGen, theme: t, width, height, transform }) : children}
279
+ </g>
280
+ </svg>
281
+
282
+ {/* Reset zoom button */}
283
+ {zoomable && isZoomed && (
284
+ <button
285
+ onClick={resetZoom}
286
+ className="absolute bottom-3 right-3 rounded-lg border border-white/15 bg-black/50 px-2.5 py-1.5 text-[11px] font-medium text-white/80 backdrop-blur-md transition hover:bg-black/70 hover:text-white"
287
+ >
288
+ Reset view
289
+ </button>
290
+ )}
291
+ </div>
292
+ );
293
+ }
@@ -0,0 +1,256 @@
1
+ import React, { useState, useRef, useEffect, useCallback } from "react";
2
+ import { createPortal } from "react-dom";
3
+ import { AnimatePresence, motion } from "framer-motion";
4
+ import {
5
+ SparklesIcon,
6
+ XMarkIcon,
7
+ TrashIcon,
8
+ ArrowTopRightOnSquareIcon,
9
+ } from "@heroicons/react/24/outline";
10
+ import ChatMessageList from "./ChatMessageList";
11
+ import ChatInput from "./ChatInput";
12
+ import useChatState from "./useChatState";
13
+
14
+ const BACKDROP_VARIANTS = {
15
+ hidden: { opacity: 0 },
16
+ visible: { opacity: 1, transition: { duration: 0.15 } },
17
+ exit: { opacity: 0, transition: { duration: 0.12 } },
18
+ };
19
+
20
+ const PANEL_VARIANTS = {
21
+ hidden: { opacity: 0, y: 20, scale: 0.97 },
22
+ visible: {
23
+ opacity: 1,
24
+ y: 0,
25
+ scale: 1,
26
+ transition: { type: "spring", damping: 28, stiffness: 380 },
27
+ },
28
+ exit: { opacity: 0, y: 10, scale: 0.97, transition: { duration: 0.12 } },
29
+ };
30
+
31
+ /**
32
+ * Command-palette style AI chat bar.
33
+ *
34
+ * Collapsed: a slim, clickable input strip with sparkle icon and ⌘K hint.
35
+ * Expanded: a centered floating overlay (portal) with backdrop, full chat,
36
+ * suggestions, and message history. No layout shift.
37
+ *
38
+ * Activate: click the bar, press ⌘K / Ctrl+K, or click a suggestion chip.
39
+ * Dismiss: Escape or click the backdrop.
40
+ */
41
+ export default function ChatBar({
42
+ onSend,
43
+ suggestions = [],
44
+ placeholder = "Ask a question\u2026",
45
+ title = "AI Assistant",
46
+ initialMessages = [],
47
+ className = "",
48
+ panelHeight,
49
+ renderAvatar,
50
+ onOpenInTab,
51
+ }) {
52
+ const [open, setOpen] = useState(false);
53
+ const [portalVisible, setPortalVisible] = useState(false);
54
+ const panelRef = useRef(null);
55
+ const chat = useChatState({ initialMessages, onSend });
56
+ const isEmpty = chat.messages.length === 0;
57
+
58
+ const close = useCallback(() => setOpen(false), []);
59
+
60
+ useEffect(() => {
61
+ if (open) setPortalVisible(true);
62
+ }, [open]);
63
+
64
+ useEffect(() => {
65
+ function onKey(e) {
66
+ if ((e.metaKey || e.ctrlKey) && e.key === "k") {
67
+ e.preventDefault();
68
+ setOpen((prev) => !prev);
69
+ }
70
+ if (e.key === "Escape" && open) close();
71
+ }
72
+ document.addEventListener("keydown", onKey);
73
+ return () => document.removeEventListener("keydown", onKey);
74
+ }, [open, close]);
75
+
76
+ useEffect(() => {
77
+ if (open) document.body.style.overflow = "hidden";
78
+ else document.body.style.overflow = "";
79
+ return () => { document.body.style.overflow = ""; };
80
+ }, [open]);
81
+
82
+ function handleSend(content) {
83
+ if (!open) setOpen(true);
84
+ chat.sendMessage(content);
85
+ }
86
+
87
+ const [shortcutLabel, setShortcutLabel] = useState("⌘K");
88
+ useEffect(() => {
89
+ setShortcutLabel(/Mac|iPhone|iPad/.test(navigator.userAgent) ? "⌘K" : "Ctrl K");
90
+ }, []);
91
+
92
+ const overlay =
93
+ typeof document !== "undefined" && portalVisible
94
+ ? createPortal(
95
+ <AnimatePresence onExitComplete={() => setPortalVisible(false)}>
96
+ {open && (
97
+ <>
98
+ {/* Backdrop */}
99
+ <motion.div
100
+ variants={BACKDROP_VARIANTS}
101
+ initial="hidden"
102
+ animate="visible"
103
+ exit="exit"
104
+ className="fixed inset-0 z-50 bg-black/30 backdrop-blur-sm"
105
+ onClick={close}
106
+ aria-hidden="true"
107
+ />
108
+
109
+ {/* Centered panel */}
110
+ <motion.div
111
+ ref={panelRef}
112
+ variants={PANEL_VARIANTS}
113
+ initial="hidden"
114
+ animate="visible"
115
+ exit="exit"
116
+ className="fixed inset-x-0 top-[10vh] z-50 mx-auto flex w-full max-w-4xl flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-2xl dark:border-slate-700 dark:bg-slate-900"
117
+ style={{ maxHeight: "75vh" }}
118
+ >
119
+ {/* Header */}
120
+ <div className="flex items-center justify-between border-b border-slate-100 px-4 py-2.5 dark:border-slate-800">
121
+ <div className="flex items-center gap-2">
122
+ <SparklesIcon className="h-4 w-4 text-brand-500" />
123
+ <h3 className="text-sm font-semibold text-slate-900 dark:text-slate-50">
124
+ {title}
125
+ </h3>
126
+ </div>
127
+ <div className="flex items-center gap-1">
128
+ {onOpenInTab ? (
129
+ <button
130
+ type="button"
131
+ onClick={() => {
132
+ setOpen(false);
133
+ setPortalVisible(false);
134
+ document.body.style.overflow = "";
135
+ onOpenInTab(chat.messages);
136
+ }}
137
+ className="rounded p-1 text-slate-400 transition hover:bg-slate-100 hover:text-slate-600 dark:text-slate-500 dark:hover:bg-slate-800 dark:hover:text-slate-300"
138
+ aria-label="Open in new tab"
139
+ title="Open in new tab"
140
+ >
141
+ <ArrowTopRightOnSquareIcon className="h-4 w-4" />
142
+ </button>
143
+ ) : null}
144
+ {chat.messages.length > 0 ? (
145
+ <button
146
+ type="button"
147
+ onClick={chat.clearMessages}
148
+ className="rounded p-1 text-slate-400 transition hover:bg-slate-100 hover:text-slate-600 dark:text-slate-500 dark:hover:bg-slate-800 dark:hover:text-slate-300"
149
+ aria-label="Clear chat"
150
+ >
151
+ <TrashIcon className="h-4 w-4" />
152
+ </button>
153
+ ) : null}
154
+ <button
155
+ type="button"
156
+ onClick={close}
157
+ className="rounded p-1 text-slate-400 transition hover:bg-slate-100 hover:text-slate-600 dark:text-slate-500 dark:hover:bg-slate-800 dark:hover:text-slate-300"
158
+ aria-label="Close"
159
+ >
160
+ <XMarkIcon className="h-4 w-4" />
161
+ </button>
162
+ <kbd className="ml-1 hidden rounded border border-slate-200 bg-slate-50 px-1.5 py-0.5 text-[10px] font-medium text-slate-400 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-500 sm:inline-block">
163
+ esc
164
+ </kbd>
165
+ </div>
166
+ </div>
167
+
168
+ {/* Body — messages or empty state */}
169
+ <div className="min-h-0 flex-1">
170
+ {isEmpty ? (
171
+ <div className="flex h-full min-h-[200px] flex-col items-center justify-center gap-4 px-6 py-8">
172
+ <div className="flex h-10 w-10 items-center justify-center rounded-full bg-brand-50 dark:bg-brand-950/30">
173
+ <SparklesIcon className="h-5 w-5 text-brand-500" />
174
+ </div>
175
+ <p className="text-sm text-slate-500 dark:text-slate-400">
176
+ Ask me anything about your data.
177
+ </p>
178
+ {suggestions.length > 0 ? (
179
+ <div className="flex flex-wrap justify-center gap-2">
180
+ {suggestions.map((text) => (
181
+ <button
182
+ key={text}
183
+ type="button"
184
+ onClick={() => handleSend(text)}
185
+ className="inline-flex items-center gap-1.5 rounded-full border border-slate-200 bg-white px-3 py-1.5 text-xs font-medium text-slate-600 shadow-sm transition hover:border-brand-300 hover:bg-brand-50 hover:text-brand-700 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:border-brand-700 dark:hover:bg-brand-950/30 dark:hover:text-brand-300"
186
+ >
187
+ <SparklesIcon
188
+ className="h-3 w-3"
189
+ aria-hidden="true"
190
+ />
191
+ {text}
192
+ </button>
193
+ ))}
194
+ </div>
195
+ ) : null}
196
+ </div>
197
+ ) : (
198
+ <ChatMessageList
199
+ messages={chat.messages}
200
+ isLoading={chat.isLoading}
201
+ isStreaming={chat.isStreaming}
202
+ suggestions={suggestions}
203
+ onSuggestion={(text) => chat.sendMessage(text)}
204
+ renderAvatar={renderAvatar}
205
+ />
206
+ )}
207
+ </div>
208
+
209
+ {/* Input */}
210
+ <div className="border-t border-slate-100 p-3 dark:border-slate-800">
211
+ <ChatInput
212
+ onSend={(content) => chat.sendMessage(content)}
213
+ disabled={chat.isLoading}
214
+ isLoading={chat.isLoading}
215
+ placeholder={placeholder}
216
+ />
217
+ </div>
218
+ </motion.div>
219
+ </>
220
+ )}
221
+ </AnimatePresence>,
222
+ document.body
223
+ )
224
+ : null;
225
+
226
+ return (
227
+ <>
228
+ {overlay}
229
+
230
+ {/* Trigger bar */}
231
+ <button
232
+ type="button"
233
+ onClick={() => setOpen(true)}
234
+ className={[
235
+ "group flex w-full items-center gap-3 rounded-xl border border-slate-200 bg-white px-4 py-2.5 text-left shadow-sm transition-all hover:border-brand-300 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-brand-500/20 dark:border-slate-800 dark:bg-slate-900 dark:hover:border-brand-700",
236
+ className,
237
+ ]
238
+ .filter(Boolean)
239
+ .join(" ")}
240
+ >
241
+ <SparklesIcon className="h-5 w-5 shrink-0 text-brand-500" />
242
+ <span className="flex-1 text-sm text-slate-400 dark:text-slate-500">
243
+ {placeholder}
244
+ </span>
245
+ {chat.messages.length > 0 ? (
246
+ <span className="rounded-full bg-brand-50 px-2 py-0.5 text-xs font-medium text-brand-600 dark:bg-brand-950/30 dark:text-brand-400">
247
+ {chat.messages.length} msgs
248
+ </span>
249
+ ) : null}
250
+ <kbd className="hidden rounded border border-slate-200 bg-slate-50 px-1.5 py-0.5 text-[10px] font-medium text-slate-400 group-hover:border-brand-200 group-hover:text-brand-500 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-500 sm:inline-block">
251
+ {shortcutLabel}
252
+ </kbd>
253
+ </button>
254
+ </>
255
+ );
256
+ }
@@ -0,0 +1,89 @@
1
+ import React, { useState, useRef, useEffect } from "react";
2
+ import { PaperAirplaneIcon, StopCircleIcon } from "@heroicons/react/24/solid";
3
+
4
+ /**
5
+ * Chat input with auto-resize textarea, Send button, and keyboard shortcuts.
6
+ * Enter sends, Shift+Enter inserts newline.
7
+ *
8
+ * @param {Function} onSend — (content: string) => void
9
+ * @param {boolean} disabled — disable input while agent is processing
10
+ * @param {boolean} isLoading — show stop button instead of send
11
+ * @param {Function} onStop — optional: called when stop is clicked
12
+ * @param {string} placeholder
13
+ * @param {number} maxRows — max visible rows before scroll (default 6)
14
+ */
15
+ export default function ChatInput({
16
+ onSend,
17
+ disabled = false,
18
+ isLoading = false,
19
+ onStop,
20
+ placeholder = "Type a message…",
21
+ maxRows = 6,
22
+ }) {
23
+ const [value, setValue] = useState("");
24
+ const textareaRef = useRef(null);
25
+
26
+ useEffect(() => {
27
+ const ta = textareaRef.current;
28
+ if (!ta) return;
29
+ ta.style.height = "auto";
30
+ const lineHeight = 22;
31
+ const maxHeight = lineHeight * maxRows;
32
+ ta.style.height = `${Math.min(ta.scrollHeight, maxHeight)}px`;
33
+ }, [value, maxRows]);
34
+
35
+ function handleSend() {
36
+ if (!value.trim() || disabled) return;
37
+ onSend?.(value);
38
+ setValue("");
39
+ if (textareaRef.current) {
40
+ textareaRef.current.style.height = "auto";
41
+ }
42
+ }
43
+
44
+ function handleKeyDown(e) {
45
+ if (e.key === "Enter" && !e.shiftKey) {
46
+ e.preventDefault();
47
+ handleSend();
48
+ }
49
+ }
50
+
51
+ const canSend = value.trim().length > 0 && !disabled;
52
+
53
+ return (
54
+ <div className="flex items-end gap-2 rounded-xl border border-slate-200 bg-white p-2 shadow-sm transition-colors focus-within:border-brand-300 focus-within:ring-2 focus-within:ring-brand-500/20 dark:border-slate-700 dark:bg-slate-900 dark:focus-within:border-brand-700 dark:focus-within:ring-brand-400/20">
55
+ <textarea
56
+ ref={textareaRef}
57
+ value={value}
58
+ onChange={(e) => setValue(e.target.value)}
59
+ onKeyDown={handleKeyDown}
60
+ placeholder={placeholder}
61
+ disabled={disabled}
62
+ rows={1}
63
+ className="max-h-36 min-h-[22px] flex-1 resize-none bg-transparent px-2 py-1.5 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60 dark:text-slate-50 dark:placeholder:text-slate-500"
64
+ aria-label="Chat message input"
65
+ />
66
+
67
+ {isLoading && onStop ? (
68
+ <button
69
+ type="button"
70
+ onClick={onStop}
71
+ className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg text-slate-400 transition hover:bg-slate-100 hover:text-slate-600 dark:text-slate-500 dark:hover:bg-slate-800 dark:hover:text-slate-300"
72
+ aria-label="Stop generating"
73
+ >
74
+ <StopCircleIcon className="h-5 w-5" />
75
+ </button>
76
+ ) : (
77
+ <button
78
+ type="button"
79
+ onClick={handleSend}
80
+ disabled={!canSend}
81
+ className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-brand-600 text-white transition hover:bg-brand-500 disabled:cursor-not-allowed disabled:opacity-40 dark:bg-brand-500 dark:hover:bg-brand-400"
82
+ aria-label="Send message"
83
+ >
84
+ <PaperAirplaneIcon className="h-4 w-4" />
85
+ </button>
86
+ )}
87
+ </div>
88
+ );
89
+ }