@schandlergarcia/sf-web-components 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/dist/components/library/cards/ActionList.d.ts +10 -10
  2. package/dist/components/library/cards/ActionList.js +2 -3
  3. package/dist/components/library/cards/ActionList.js.map +1 -1
  4. package/dist/components/library/cards/ActivityCard.d.ts +18 -5
  5. package/dist/components/library/cards/ActivityCard.js +3 -4
  6. package/dist/components/library/cards/ActivityCard.js.map +1 -1
  7. package/dist/components/library/cards/BaseCard.d.ts +30 -24
  8. package/dist/components/library/cards/BaseCard.js +2 -3
  9. package/dist/components/library/cards/BaseCard.js.map +1 -1
  10. package/dist/components/library/cards/CalloutCard.d.ts +11 -9
  11. package/dist/components/library/cards/CalloutCard.js +2 -3
  12. package/dist/components/library/cards/CalloutCard.js.map +1 -1
  13. package/dist/components/library/cards/ChartCard.d.ts +29 -17
  14. package/dist/components/library/cards/ChartCard.js +13 -14
  15. package/dist/components/library/cards/ChartCard.js.map +1 -1
  16. package/dist/components/library/cards/FeedPanel.d.ts +12 -11
  17. package/dist/components/library/cards/FeedPanel.js +3 -4
  18. package/dist/components/library/cards/FeedPanel.js.map +1 -1
  19. package/dist/components/library/cards/ListCard.d.ts +33 -20
  20. package/dist/components/library/cards/ListCard.js +35 -35
  21. package/dist/components/library/cards/ListCard.js.map +1 -1
  22. package/dist/components/library/cards/MetricCard.d.ts +23 -17
  23. package/dist/components/library/cards/MetricCard.js +10 -11
  24. package/dist/components/library/cards/MetricCard.js.map +1 -1
  25. package/dist/components/library/cards/MetricsStrip.d.ts +11 -11
  26. package/dist/components/library/cards/MetricsStrip.js +1 -1
  27. package/dist/components/library/cards/MetricsStrip.js.map +1 -1
  28. package/dist/components/library/cards/SectionCard.d.ts +17 -12
  29. package/dist/components/library/cards/SectionCard.js +18 -19
  30. package/dist/components/library/cards/SectionCard.js.map +1 -1
  31. package/dist/components/library/cards/SemanticMetricCard.d.ts +15 -20
  32. package/dist/components/library/cards/SemanticMetricCardWithLoading.d.ts +8 -7
  33. package/dist/components/library/cards/SemanticTableCard.d.ts +13 -18
  34. package/dist/components/library/cards/SemanticTableCardWithLoading.d.ts +8 -7
  35. package/dist/components/library/cards/StatusCard.d.ts +29 -15
  36. package/dist/components/library/cards/StatusCard.js +16 -17
  37. package/dist/components/library/cards/StatusCard.js.map +1 -1
  38. package/dist/components/library/cards/TableCard.d.ts +40 -23
  39. package/dist/components/library/cards/TableCard.js +59 -59
  40. package/dist/components/library/cards/TableCard.js.map +1 -1
  41. package/dist/components/library/cards/WidgetCard.d.ts +19 -11
  42. package/dist/components/library/cards/WidgetCard.js.map +1 -1
  43. package/dist/components/library/charts/D3Chart.d.ts +23 -16
  44. package/dist/components/library/charts/D3Chart.js.map +1 -1
  45. package/dist/components/library/charts/D3ChartTemplates.d.ts +33 -3
  46. package/dist/components/library/charts/D3ChartTemplates.js +7 -7
  47. package/dist/components/library/charts/D3ChartTemplates.js.map +1 -1
  48. package/dist/components/library/charts/GeoMap.d.ts +81 -18
  49. package/dist/components/library/charts/GeoMap.js +28 -26
  50. package/dist/components/library/charts/GeoMap.js.map +1 -1
  51. package/dist/components/library/filters/FilterBar.d.ts +18 -8
  52. package/dist/components/library/filters/FilterBar.js +2 -3
  53. package/dist/components/library/filters/FilterBar.js.map +1 -1
  54. package/dist/components/library/filters/SearchFilter.d.ts +7 -6
  55. package/dist/components/library/filters/SearchFilter.js +2 -3
  56. package/dist/components/library/filters/SearchFilter.js.map +1 -1
  57. package/dist/components/library/filters/SelectFilter.d.ts +13 -7
  58. package/dist/components/library/filters/SelectFilter.js +2 -3
  59. package/dist/components/library/filters/SelectFilter.js.map +1 -1
  60. package/dist/components/library/filters/ToggleFilter.d.ts +7 -5
  61. package/dist/components/library/filters/ToggleFilter.js +2 -3
  62. package/dist/components/library/filters/ToggleFilter.js.map +1 -1
  63. package/dist/components/library/forms/FormField.d.ts +10 -8
  64. package/dist/components/library/forms/FormField.js +3 -4
  65. package/dist/components/library/forms/FormField.js.map +1 -1
  66. package/dist/components/library/forms/FormModal.d.ts +23 -14
  67. package/dist/components/library/forms/FormModal.js.map +1 -1
  68. package/dist/components/library/forms/FormRenderer.d.ts +29 -9
  69. package/dist/components/library/forms/FormRenderer.js +6 -7
  70. package/dist/components/library/forms/FormRenderer.js.map +1 -1
  71. package/dist/components/library/forms/FormSection.d.ts +10 -8
  72. package/dist/components/library/forms/FormSection.js +2 -3
  73. package/dist/components/library/forms/FormSection.js.map +1 -1
  74. package/dist/components/library/forms/index.d.ts +5 -0
  75. package/dist/components/library/forms/useFormState.d.ts +23 -15
  76. package/dist/components/library/forms/useFormState.js +53 -47
  77. package/dist/components/library/forms/useFormState.js.map +1 -1
  78. package/dist/components/library/heroui/Accordion.d.ts +6 -5
  79. package/dist/components/library/heroui/Accordion.js +7 -8
  80. package/dist/components/library/heroui/Accordion.js.map +1 -1
  81. package/dist/components/library/heroui/Breadcrumbs.d.ts +5 -2
  82. package/dist/components/library/heroui/Breadcrumbs.js +4 -5
  83. package/dist/components/library/heroui/Breadcrumbs.js.map +1 -1
  84. package/dist/components/library/heroui/Collapsible.d.ts +19 -30
  85. package/dist/components/library/heroui/Collapsible.js +13 -13
  86. package/dist/components/library/heroui/Collapsible.js.map +1 -1
  87. package/dist/components/library/heroui/DatePicker.d.ts +24 -52
  88. package/dist/components/library/heroui/DatePicker.js +5 -6
  89. package/dist/components/library/heroui/DatePicker.js.map +1 -1
  90. package/dist/components/library/heroui/Dialog.d.ts +18 -32
  91. package/dist/components/library/heroui/Dialog.js +6 -7
  92. package/dist/components/library/heroui/Dialog.js.map +1 -1
  93. package/dist/components/library/heroui/Drawer.d.ts +6 -2
  94. package/dist/components/library/heroui/Drawer.js +2 -3
  95. package/dist/components/library/heroui/Drawer.js.map +1 -1
  96. package/dist/components/library/heroui/Dropdown.d.ts +6 -2
  97. package/dist/components/library/heroui/Dropdown.js +2 -3
  98. package/dist/components/library/heroui/Dropdown.js.map +1 -1
  99. package/dist/components/library/heroui/Field.d.ts +19 -38
  100. package/dist/components/library/heroui/Field.js +9 -10
  101. package/dist/components/library/heroui/Field.js.map +1 -1
  102. package/dist/components/library/heroui/Meter.d.ts +7 -5
  103. package/dist/components/library/heroui/Meter.js +4 -5
  104. package/dist/components/library/heroui/Meter.js.map +1 -1
  105. package/dist/components/library/heroui/Popover.d.ts +23 -38
  106. package/dist/components/library/heroui/Popover.js +12 -12
  107. package/dist/components/library/heroui/Popover.js.map +1 -1
  108. package/dist/components/library/heroui/Select.d.ts +31 -37
  109. package/dist/components/library/heroui/Select.js +3 -4
  110. package/dist/components/library/heroui/Select.js.map +1 -1
  111. package/dist/components/library/layout/PageContainer.d.ts +6 -4
  112. package/dist/components/library/layout/PageContainer.js +4 -5
  113. package/dist/components/library/layout/PageContainer.js.map +1 -1
  114. package/package.json +4 -1
  115. package/src/components/library/cards/{ActionList.jsx → ActionList.tsx} +13 -9
  116. package/src/components/library/cards/{ActivityCard.jsx → ActivityCard.tsx} +33 -4
  117. package/src/components/library/cards/{BaseCard.jsx → BaseCard.tsx} +33 -6
  118. package/src/components/library/cards/{CalloutCard.jsx → CalloutCard.tsx} +12 -10
  119. package/src/components/library/cards/{ChartCard.jsx → ChartCard.tsx} +32 -6
  120. package/src/components/library/cards/{FeedPanel.jsx → FeedPanel.tsx} +13 -2
  121. package/src/components/library/cards/{ListCard.jsx → ListCard.tsx} +43 -7
  122. package/src/components/library/cards/{MetricCard.jsx → MetricCard.tsx} +25 -6
  123. package/src/components/library/cards/{MetricsStrip.jsx → MetricsStrip.tsx} +22 -12
  124. package/src/components/library/cards/{SectionCard.jsx → SectionCard.tsx} +27 -8
  125. package/src/components/library/cards/{SemanticMetricCard.jsx → SemanticMetricCard.tsx} +17 -5
  126. package/src/components/library/cards/{SemanticMetricCardWithLoading.jsx → SemanticMetricCardWithLoading.tsx} +9 -3
  127. package/src/components/library/cards/{SemanticTableCard.jsx → SemanticTableCard.tsx} +14 -3
  128. package/src/components/library/cards/{SemanticTableCardWithLoading.jsx → SemanticTableCardWithLoading.tsx} +9 -5
  129. package/src/components/library/cards/{StatusCard.jsx → StatusCard.tsx} +61 -12
  130. package/src/components/library/cards/{TableCard.jsx → TableCard.tsx} +51 -12
  131. package/src/components/library/cards/{WidgetCard.jsx → WidgetCard.tsx} +28 -5
  132. package/src/components/library/charts/{D3Chart.jsx → D3Chart.tsx} +27 -7
  133. package/src/components/library/charts/{D3ChartTemplates.jsx → D3ChartTemplates.tsx} +60 -28
  134. package/src/components/library/charts/{GeoMap.jsx → GeoMap.tsx} +106 -17
  135. package/src/components/library/filters/{FilterBar.jsx → FilterBar.tsx} +21 -11
  136. package/src/components/library/filters/{SearchFilter.jsx → SearchFilter.tsx} +8 -2
  137. package/src/components/library/filters/{SelectFilter.jsx → SelectFilter.tsx} +15 -8
  138. package/src/components/library/filters/{ToggleFilter.jsx → ToggleFilter.tsx} +7 -6
  139. package/src/components/library/forms/{FormField.jsx → FormField.tsx} +91 -45
  140. package/src/components/library/forms/{FormModal.jsx → FormModal.tsx} +21 -20
  141. package/src/components/library/forms/{FormRenderer.jsx → FormRenderer.tsx} +32 -10
  142. package/src/components/library/forms/{FormSection.jsx → FormSection.tsx} +13 -7
  143. package/src/components/library/forms/index.tsx +11 -0
  144. package/src/components/library/forms/{useFormState.jsx → useFormState.tsx} +43 -23
  145. package/src/components/library/heroui/{Accordion.jsx → Accordion.tsx} +8 -3
  146. package/src/components/library/heroui/{Breadcrumbs.jsx → Breadcrumbs.tsx} +5 -2
  147. package/src/components/library/heroui/Collapsible.tsx +62 -0
  148. package/src/components/library/heroui/{DatePicker.jsx → DatePicker.tsx} +28 -4
  149. package/src/components/library/heroui/Dialog.tsx +43 -0
  150. package/src/components/library/heroui/{Drawer.jsx → Drawer.tsx} +6 -2
  151. package/src/components/library/heroui/{Dropdown.jsx → Dropdown.tsx} +6 -2
  152. package/src/components/library/heroui/{Field.jsx → Field.tsx} +23 -6
  153. package/src/components/library/heroui/Meter.tsx +13 -0
  154. package/src/components/library/heroui/{Popover.jsx → Popover.tsx} +29 -8
  155. package/src/components/library/heroui/Select.tsx +73 -0
  156. package/src/components/library/layout/{PageContainer.jsx → PageContainer.tsx} +6 -3
  157. package/src/components/library/forms/index.jsx +0 -5
  158. package/src/components/library/heroui/Collapsible.jsx +0 -42
  159. package/src/components/library/heroui/Dialog.jsx +0 -37
  160. package/src/components/library/heroui/Meter.jsx +0 -8
  161. package/src/components/library/heroui/Select.jsx +0 -37
  162. /package/src/components/library/filters/{index.jsx → index.ts} +0 -0
@@ -1,10 +1,12 @@
1
1
  import React from "react";
2
- import BaseCard from "./BaseCard";
2
+ import BaseCard, { BaseCardProps } from "./BaseCard";
3
3
  import UIInput from "../ui/UIInput";
4
4
  import UIButton from "../ui/UIButton";
5
5
  import UIText from "../ui/Text";
6
6
 
7
- function defaultTypeFormat(type, value) {
7
+ type ColumnType = "currency" | "percentage" | "number" | "text";
8
+
9
+ function defaultTypeFormat(type: ColumnType | undefined, value: unknown): string {
8
10
  if (value == null) return "";
9
11
  if (!type) return String(value);
10
12
 
@@ -23,7 +25,7 @@ function defaultTypeFormat(type, value) {
23
25
  return String(value);
24
26
  }
25
27
 
26
- function stableSort(data, compare) {
28
+ function stableSort<T>(data: T[], compare: (a: T, b: T) => number): T[] {
27
29
  return data
28
30
  .map((item, idx) => ({ item, idx }))
29
31
  .sort((a, b) => {
@@ -33,6 +35,44 @@ function stableSort(data, compare) {
33
35
  .map((x) => x.item);
34
36
  }
35
37
 
38
+ export interface TableColumn {
39
+ key: string;
40
+ label: string;
41
+ type?: ColumnType;
42
+ sortable?: boolean;
43
+ mono?: boolean;
44
+ className?: string;
45
+ render?: (value: unknown, row: Record<string, unknown>) => React.ReactNode;
46
+ }
47
+
48
+ export interface SortState {
49
+ key: string;
50
+ direction: "asc" | "desc";
51
+ }
52
+
53
+ export interface TableCardProps extends Omit<BaseCardProps, "variant" | "header" | "body"> {
54
+ data?: Record<string, unknown>[];
55
+ columns?: TableColumn[];
56
+ title?: string;
57
+ subtitle?: string;
58
+ searchable?: boolean;
59
+ sortable?: boolean;
60
+ paginated?: boolean;
61
+ selectable?: boolean;
62
+ pageSize?: number;
63
+ actions?: React.ReactNode;
64
+ rowActions?: (row: Record<string, unknown>) => React.ReactNode;
65
+ onRowSelect?: (row: Record<string, unknown>) => void;
66
+ onSort?: (sort: SortState) => void;
67
+ onSearch?: (query: string) => void;
68
+ loading?: boolean;
69
+ error?: string | Error;
70
+ emptyMessage?: string;
71
+ simulateInitialLoad?: boolean;
72
+ minInitialDelayMs?: number;
73
+ maxInitialDelayMs?: number;
74
+ }
75
+
36
76
  export default function TableCard({
37
77
  data = [],
38
78
  columns = [],
@@ -55,12 +95,12 @@ export default function TableCard({
55
95
  minInitialDelayMs = 350,
56
96
  maxInitialDelayMs = 900,
57
97
  ...cardProps
58
- }) {
98
+ }: TableCardProps) {
59
99
  const [query, setQuery] = React.useState("");
60
- const [sortKey, setSortKey] = React.useState(null);
61
- const [sortDir, setSortDir] = React.useState("asc");
100
+ const [sortKey, setSortKey] = React.useState<string | null>(null);
101
+ const [sortDir, setSortDir] = React.useState<"asc" | "desc">("asc");
62
102
  const [page, setPage] = React.useState(1);
63
- const [selectedId, setSelectedId] = React.useState(null);
103
+ const [selectedId, setSelectedId] = React.useState<string | number | undefined>(undefined);
64
104
  const [simLoading, setSimLoading] = React.useState(simulateInitialLoad);
65
105
 
66
106
  React.useEffect(() => {
@@ -163,7 +203,7 @@ export default function TableCard({
163
203
  );
164
204
  }
165
205
 
166
- const canSort = (col) => sortable && (col.sortable ?? true);
206
+ const canSort = (col: TableColumn) => sortable && (col.sortable ?? true);
167
207
 
168
208
  return (
169
209
  <BaseCard
@@ -248,11 +288,12 @@ export default function TableCard({
248
288
  </tr>
249
289
  ) : (
250
290
  pageData.map((row, idx) => {
251
- const rowId = row?.id ?? idx;
291
+ const rawId = row?.id;
292
+ const rowId: string | number = typeof rawId === "string" || typeof rawId === "number" ? rawId : idx;
252
293
  const selected = selectable && selectedId === rowId;
253
294
  return (
254
295
  <tr
255
- key={rowId}
296
+ key={String(rowId)}
256
297
  className={[
257
298
  "group",
258
299
  selectable ? "cursor-pointer" : "",
@@ -333,5 +374,3 @@ export default function TableCard({
333
374
  />
334
375
  );
335
376
  }
336
-
337
-
@@ -1,8 +1,22 @@
1
1
  import React from "react";
2
- import BaseCard from "./BaseCard";
2
+ import BaseCard, { BaseCardProps } from "./BaseCard";
3
3
  import UIText from "../ui/Text";
4
4
 
5
- function Section({ title, actions, content, divided }) {
5
+ export interface WidgetSection {
6
+ id?: string | number;
7
+ title?: string;
8
+ actions?: React.ReactNode;
9
+ content: React.ReactNode;
10
+ }
11
+
12
+ interface SectionProps {
13
+ title?: string;
14
+ actions?: React.ReactNode;
15
+ content: React.ReactNode;
16
+ divided: boolean;
17
+ }
18
+
19
+ function Section({ title, actions, content, divided }: SectionProps) {
6
20
  return (
7
21
  <div className={divided ? "border-t border-slate-200 pt-4 dark:border-slate-800" : ""}>
8
22
  {(title || actions) ? (
@@ -22,6 +36,17 @@ function Section({ title, actions, content, divided }) {
22
36
  );
23
37
  }
24
38
 
39
+ export interface WidgetCardProps extends Omit<BaseCardProps, "variant" | "header" | "body"> {
40
+ header: React.ReactNode;
41
+ sections?: WidgetSection[];
42
+ footer?: React.ReactNode;
43
+ divided?: boolean;
44
+ collapsible?: boolean;
45
+ defaultExpanded?: boolean;
46
+ loading?: boolean;
47
+ emptyMessage?: string;
48
+ }
49
+
25
50
  export default function WidgetCard({
26
51
  header,
27
52
  sections = [],
@@ -32,7 +57,7 @@ export default function WidgetCard({
32
57
  loading = false,
33
58
  emptyMessage = "No sections.",
34
59
  ...cardProps
35
- }) {
60
+ }: WidgetCardProps) {
36
61
  const [expanded, setExpanded] = React.useState(defaultExpanded);
37
62
 
38
63
  const hdr = (
@@ -86,5 +111,3 @@ export default function WidgetCard({
86
111
 
87
112
  return <BaseCard variant="widget" header={hdr} body={body} {...cardProps} />;
88
113
  }
89
-
90
-
@@ -1,5 +1,27 @@
1
1
  import React from "react";
2
2
 
3
+ interface Dimensions {
4
+ width: number;
5
+ height: number;
6
+ }
7
+
8
+ export interface D3ChartProps {
9
+ data: unknown;
10
+ renderChart?: (svg: SVGSVGElement, data: unknown, dims: Dimensions, options: Record<string, unknown>) => void;
11
+ options?: Record<string, unknown>;
12
+ width?: number;
13
+ height?: number;
14
+ responsive?: boolean;
15
+ aspectRatio?: number;
16
+ className?: string;
17
+ style?: React.CSSProperties;
18
+ containerStyle?: React.CSSProperties;
19
+ svgStyle?: React.CSSProperties;
20
+ loading?: boolean;
21
+ error?: string | Error;
22
+ ariaLabel?: string;
23
+ }
24
+
3
25
  /**
4
26
  * Minimal D3 chart host:
5
27
  * - Owns the <svg> element
@@ -21,10 +43,10 @@ export default function D3Chart({
21
43
  loading = false,
22
44
  error,
23
45
  ariaLabel = "Chart"
24
- }) {
25
- const containerRef = React.useRef(null);
26
- const svgRef = React.useRef(null);
27
- const [containerWidth, setContainerWidth] = React.useState(null);
46
+ }: D3ChartProps): React.ReactElement {
47
+ const containerRef = React.useRef<HTMLDivElement>(null);
48
+ const svgRef = React.useRef<SVGSVGElement>(null);
49
+ const [containerWidth, setContainerWidth] = React.useState<number | null>(null);
28
50
 
29
51
  React.useEffect(() => {
30
52
  if (!responsive) return;
@@ -53,7 +75,7 @@ export default function D3Chart({
53
75
  const svgEl = svgRef.current;
54
76
  if (!svgEl) return;
55
77
 
56
- const dims = {
78
+ const dims: Dimensions = {
57
79
  width: computedWidth ?? 0,
58
80
  height: computedHeight ?? 0
59
81
  };
@@ -105,5 +127,3 @@ export default function D3Chart({
105
127
  </div>
106
128
  );
107
129
  }
108
-
109
-
@@ -1,11 +1,45 @@
1
1
  import * as d3 from "d3";
2
2
 
3
- function clear(svg) {
3
+ interface Dimensions {
4
+ width: number;
5
+ height: number;
6
+ }
7
+
8
+ interface Margin {
9
+ top: number;
10
+ right: number;
11
+ bottom: number;
12
+ left: number;
13
+ }
14
+
15
+ interface LineChartOptions {
16
+ xKey?: string;
17
+ yKey?: string;
18
+ margin?: Margin;
19
+ stroke?: string;
20
+ strokeWidth?: number;
21
+ showAxes?: boolean;
22
+ showGrid?: boolean;
23
+ }
24
+
25
+ interface GroupedBarChartOptions {
26
+ groups?: string[];
27
+ margin?: Margin;
28
+ xKey?: string;
29
+ colors?: string[];
30
+ barRadius?: number;
31
+ yFormat?: string;
32
+ showGrid?: boolean;
33
+ }
34
+
35
+ type DataPoint = Record<string, unknown>;
36
+
37
+ function clear(svg: SVGSVGElement): void {
4
38
  d3.select(svg).selectAll("*").remove();
5
39
  }
6
40
 
7
41
  export const D3ChartTemplates = {
8
- lineChart(svg, data, dims, opts = {}) {
42
+ lineChart(svg: SVGSVGElement, data: DataPoint[], dims: Dimensions, opts: LineChartOptions = {}): void {
9
43
  const {
10
44
  xKey = "x",
11
45
  yKey = "y",
@@ -25,11 +59,11 @@ export const D3ChartTemplates = {
25
59
 
26
60
  const g = d3.select(svg).attr("viewBox", `0 0 ${width} ${height}`).append("g").attr("transform", `translate(${margin.left},${margin.top})`);
27
61
 
28
- const xs = data.map((d) => d?.[xKey]).filter((v) => v != null);
29
- const ys = data.map((d) => d?.[yKey]).filter((v) => v != null);
62
+ const xs = data.map((d) => d?.[xKey]).filter((v) => v != null) as number[];
63
+ const ys = data.map((d) => d?.[yKey]).filter((v) => v != null) as number[];
30
64
 
31
- const xDomain = d3.extent(xs);
32
- const yDomain = d3.extent(ys);
65
+ const xDomain = d3.extent(xs) as [number, number];
66
+ const yDomain = d3.extent(ys) as [number, number];
33
67
 
34
68
  const x = d3.scaleLinear().domain(xDomain).nice().range([0, innerW]);
35
69
  const y = d3.scaleLinear().domain(yDomain).nice().range([innerH, 0]);
@@ -37,15 +71,15 @@ export const D3ChartTemplates = {
37
71
  if (showGrid) {
38
72
  g.append("g")
39
73
  .attr("class", "grid")
40
- .call(d3.axisLeft(y).ticks(5).tickSize(-innerW).tickFormat(""))
41
- .call((grid) => grid.selectAll("line").attr("stroke", "currentColor").attr("opacity", 0.12))
42
- .call((grid) => grid.selectAll("path").attr("stroke", "none"));
74
+ .call(d3.axisLeft(y).ticks(5).tickSize(-innerW).tickFormat(() => ""))
75
+ .call((grid: d3.Selection<SVGGElement, unknown, null, undefined>) => grid.selectAll("line").attr("stroke", "currentColor").attr("opacity", 0.12))
76
+ .call((grid: d3.Selection<SVGGElement, unknown, null, undefined>) => grid.selectAll("path").attr("stroke", "none"));
43
77
  }
44
78
 
45
79
  const line = d3
46
- .line()
47
- .x((d) => x(d[xKey]))
48
- .y((d) => y(d[yKey]))
80
+ .line<DataPoint>()
81
+ .x((d) => x(d[xKey] as number))
82
+ .y((d) => y(d[yKey] as number))
49
83
  .defined((d) => d?.[xKey] != null && d?.[yKey] != null);
50
84
 
51
85
  g.append("path")
@@ -59,15 +93,15 @@ export const D3ChartTemplates = {
59
93
  g.append("g")
60
94
  .attr("transform", `translate(0,${innerH})`)
61
95
  .call(d3.axisBottom(x).ticks(6))
62
- .call((ax) => ax.selectAll("text").attr("font-size", 10));
96
+ .call((ax: d3.Selection<SVGGElement, unknown, null, undefined>) => ax.selectAll("text").attr("font-size", 10));
63
97
 
64
98
  g.append("g")
65
99
  .call(d3.axisLeft(y).ticks(5))
66
- .call((ax) => ax.selectAll("text").attr("font-size", 10));
100
+ .call((ax: d3.Selection<SVGGElement, unknown, null, undefined>) => ax.selectAll("text").attr("font-size", 10));
67
101
  }
68
102
  },
69
103
 
70
- groupedBarChart(svg, data, dims, opts = {}) {
104
+ groupedBarChart(svg: SVGSVGElement, data: DataPoint[], dims: Dimensions, opts: GroupedBarChartOptions = {}): void {
71
105
  const {
72
106
  groups = [],
73
107
  margin = { top: 20, right: 20, bottom: 40, left: 55 },
@@ -90,37 +124,35 @@ export const D3ChartTemplates = {
90
124
 
91
125
  const groupKeys = groups.length ? groups : Object.keys(data[0] || {}).filter((k) => k !== xKey);
92
126
 
93
- const x0 = d3.scaleBand().domain(data.map((d) => d[xKey])).range([0, innerW]).padding(0.3);
127
+ const x0 = d3.scaleBand().domain(data.map((d: DataPoint) => String(d[xKey]))).range([0, innerW]).padding(0.3);
94
128
  const x1 = d3.scaleBand().domain(groupKeys).range([0, x0.bandwidth()]).padding(0.05);
95
- const yMax = d3.max(data, (d) => d3.max(groupKeys, (k) => d[k])) * 1.15;
129
+ const yMax = (d3.max(data, (d: DataPoint) => d3.max(groupKeys, (k: string) => d[k] as number)) ?? 0) * 1.15;
96
130
  const y = d3.scaleLinear().domain([0, yMax]).range([innerH, 0]);
97
131
 
98
132
  g.append("g")
99
133
  .attr("transform", `translate(0,${innerH})`)
100
134
  .call(d3.axisBottom(x0).tickSize(0))
101
- .call((ax) => ax.selectAll("text").attr("fill", "#94a3b8").attr("font-size", "11px"))
102
- .call((ax) => ax.select(".domain").remove());
135
+ .call((ax: d3.Selection<SVGGElement, unknown, null, undefined>) => ax.selectAll("text").attr("fill", "#94a3b8").attr("font-size", "11px"))
136
+ .call((ax: d3.Selection<SVGGElement, unknown, null, undefined>) => ax.select(".domain").remove());
103
137
 
104
138
  g.append("g")
105
139
  .call(d3.axisLeft(y).ticks(5).tickFormat(d3.format(yFormat)).tickSize(showGrid ? -innerW : 0))
106
- .call((ax) => ax.selectAll("text").attr("fill", "#94a3b8").attr("font-size", "10px"))
107
- .call((ax) => ax.selectAll(".tick line").attr("stroke", "#e2e8f0").attr("stroke-dasharray", "2,2"))
108
- .call((ax) => ax.select(".domain").remove());
140
+ .call((ax: d3.Selection<SVGGElement, unknown, null, undefined>) => ax.selectAll("text").attr("fill", "#94a3b8").attr("font-size", "10px"))
141
+ .call((ax: d3.Selection<SVGGElement, unknown, null, undefined>) => ax.selectAll(".tick line").attr("stroke", "#e2e8f0").attr("stroke-dasharray", "2,2"))
142
+ .call((ax: d3.Selection<SVGGElement, unknown, null, undefined>) => ax.select(".domain").remove());
109
143
 
110
144
  const rows = g.selectAll(".bar-group").data(data).join("g")
111
145
  .attr("class", "bar-group")
112
- .attr("transform", (d) => `translate(${x0(d[xKey])},0)`);
146
+ .attr("transform", (d: DataPoint) => `translate(${x0(String(d[xKey]))},0)`);
113
147
 
114
148
  groupKeys.forEach((key, i) => {
115
149
  rows.append("rect")
116
- .attr("x", x1(key))
117
- .attr("y", (d) => y(d[key]))
150
+ .attr("x", x1(key) ?? 0)
151
+ .attr("y", (d: DataPoint) => y(d[key] as number))
118
152
  .attr("width", x1.bandwidth())
119
- .attr("height", (d) => innerH - y(d[key]))
153
+ .attr("height", (d: DataPoint) => innerH - y(d[key] as number))
120
154
  .attr("rx", barRadius)
121
155
  .attr("fill", colors[i % colors.length]);
122
156
  });
123
157
  },
124
158
  };
125
-
126
-
@@ -1,9 +1,12 @@
1
1
  import React, { useMemo, useRef, useEffect, useState, useCallback } from "react";
2
2
  import * as d3 from "d3";
3
3
  import { feature } from "topojson-client";
4
+ import type { Topology, GeometryCollection } from "topojson-specification";
4
5
  import world from "world-atlas/land-110m.json";
5
6
 
6
- const land = feature(world, world.objects.land);
7
+ const land = feature(world as unknown as Topology<{ land: GeometryCollection }>, (world.objects.land as unknown) as GeometryCollection);
8
+
9
+ type ProjectionType = "naturalEarth" | "mercator" | "equirectangular";
7
10
 
8
11
  const PROJECTIONS = {
9
12
  naturalEarth: d3.geoNaturalEarth1,
@@ -11,7 +14,30 @@ const PROJECTIONS = {
11
14
  equirectangular: d3.geoEquirectangular,
12
15
  };
13
16
 
14
- const THEMES = {
17
+ interface Theme {
18
+ bg: string;
19
+ bgGradient: [string, string];
20
+ land: string;
21
+ landStroke: string;
22
+ sphere: string;
23
+ graticule: string;
24
+ graticuleOpacity: number;
25
+ markerActive: string;
26
+ markerInactive: string;
27
+ label: string;
28
+ labelInactive: string;
29
+ arc: string;
30
+ arcHighlight: string;
31
+ arcDanger: string;
32
+ dot: string;
33
+ dotDanger: string;
34
+ overlayFill: string;
35
+ overlayStroke: string;
36
+ }
37
+
38
+ type ThemeName = "dark" | "light";
39
+
40
+ const THEMES: Record<ThemeName, Theme> = {
15
41
  dark: {
16
42
  bg: "#050b15",
17
43
  bgGradient: ["#0a1628", "#050b15"],
@@ -54,11 +80,72 @@ const THEMES = {
54
80
  },
55
81
  };
56
82
 
57
- function buildProjection(type, width, height) {
83
+ function buildProjection(type: ProjectionType, width: number, height: number): d3.GeoProjection {
58
84
  const factory = PROJECTIONS[type] ?? PROJECTIONS.naturalEarth;
59
85
  return factory().fitSize([width, height], { type: "Sphere" });
60
86
  }
61
87
 
88
+ export interface Marker {
89
+ id: string;
90
+ lon: number;
91
+ lat: number;
92
+ label?: string | false;
93
+ active?: boolean;
94
+ }
95
+
96
+ export interface Arc {
97
+ id: string;
98
+ from: [number, number];
99
+ to: [number, number];
100
+ danger?: boolean;
101
+ color?: string;
102
+ dotColor?: string;
103
+ progress?: number;
104
+ _path?: string | null;
105
+ }
106
+
107
+ export interface Overlay {
108
+ id: string;
109
+ center: [number, number];
110
+ radius?: number;
111
+ fill?: string;
112
+ stroke?: string;
113
+ }
114
+
115
+ export interface InitialBounds {
116
+ sw: [number, number];
117
+ ne: [number, number];
118
+ padding?: number;
119
+ }
120
+
121
+ interface ChildrenFunctionProps {
122
+ proj: d3.GeoProjection;
123
+ pathGen: d3.GeoPath;
124
+ theme: Theme;
125
+ width: number;
126
+ height: number;
127
+ transform: d3.ZoomTransform;
128
+ }
129
+
130
+ export interface GeoMapProps {
131
+ width?: number;
132
+ height?: number;
133
+ projection?: ProjectionType;
134
+ theme?: ThemeName;
135
+ markers?: Marker[];
136
+ arcs?: Arc[];
137
+ overlays?: Overlay[];
138
+ selectedId?: string | null;
139
+ onArcClick?: (arc: Arc) => void;
140
+ onMarkerClick?: (marker: Marker) => void;
141
+ zoomable?: boolean;
142
+ minZoom?: number;
143
+ maxZoom?: number;
144
+ initialBounds?: InitialBounds | null;
145
+ className?: string;
146
+ children?: React.ReactNode | ((props: ChildrenFunctionProps) => React.ReactNode);
147
+ }
148
+
62
149
  export default function GeoMap({
63
150
  width = 960,
64
151
  height = 480,
@@ -76,11 +163,11 @@ export default function GeoMap({
76
163
  initialBounds = null,
77
164
  className = "",
78
165
  children,
79
- }) {
166
+ }: GeoMapProps): React.ReactElement {
80
167
  const t = THEMES[theme] ?? THEMES.dark;
81
- const svgRef = useRef(null);
82
- const [transform, setTransform] = useState(d3.zoomIdentity);
83
- const zoomRef = useRef(null);
168
+ const svgRef = useRef<SVGSVGElement>(null);
169
+ const [transform, setTransform] = useState<d3.ZoomTransform>(d3.zoomIdentity);
170
+ const zoomRef = useRef<d3.ZoomBehavior<SVGSVGElement, unknown> | null>(null);
84
171
 
85
172
  const { proj, pathGen, graticulePath, spherePath, landPath } = useMemo(() => {
86
173
  const proj = buildProjection(projType, width, height);
@@ -98,12 +185,14 @@ export default function GeoMap({
98
185
  useEffect(() => {
99
186
  if (!zoomable || !svgRef.current) return;
100
187
  const svg = d3.select(svgRef.current);
101
- const zoom = d3.zoom()
188
+ const zoom = d3.zoom<SVGSVGElement, unknown>()
102
189
  .scaleExtent([minZoom, maxZoom])
103
- .on("zoom", (e) => setTransform(e.transform));
190
+ .on("zoom", (e: d3.D3ZoomEvent<SVGSVGElement, unknown>) => setTransform(e.transform));
104
191
  zoomRef.current = zoom;
105
192
  svg.call(zoom);
106
- return () => svg.on(".zoom", null);
193
+ return () => {
194
+ svg.on(".zoom", null);
195
+ };
107
196
  }, [zoomable, minZoom, maxZoom]);
108
197
 
109
198
  // Apply initial zoom to fit bounds (markers/region) on mount
@@ -144,15 +233,15 @@ export default function GeoMap({
144
233
  const isZoomed = transform.k !== 1 || transform.x !== 0 || transform.y !== 0;
145
234
 
146
235
  const arcPaths = useMemo(() => {
147
- const cache = {};
236
+ const cache: Record<string, string | null> = {};
148
237
  arcs.forEach(a => {
149
238
  const key = `${a.from[0]},${a.from[1]}-${a.to[0]},${a.to[1]}`;
150
239
  if (cache[key]) { a._path = cache[key]; return; }
151
240
  const interp = d3.geoInterpolate(a.from, a.to);
152
- const pts = [];
241
+ const pts: [number, number][] = [];
153
242
  for (let i = 0; i <= 1; i += 0.02) {
154
243
  const p = proj(interp(i));
155
- if (p) pts.push(p);
244
+ if (p) pts.push(p as [number, number]);
156
245
  }
157
246
  const path = pts.length > 1 ? d3.line()(pts) : null;
158
247
  cache[key] = path;
@@ -162,7 +251,7 @@ export default function GeoMap({
162
251
  }, [arcs, proj]);
163
252
 
164
253
  const activeMarkerIds = useMemo(() => {
165
- const s = new Set();
254
+ const s = new Set<string>();
166
255
  markers.forEach(m => { if (m.active) s.add(m.id); });
167
256
  return s;
168
257
  }, [markers]);
@@ -194,9 +283,9 @@ export default function GeoMap({
194
283
 
195
284
  {/* Zoomable content group */}
196
285
  <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} />
286
+ <path d={spherePath ?? undefined} fill="none" stroke={t.sphere} strokeWidth={0.8 * invScale} />
287
+ <path d={landPath ?? undefined} fill={t.land} stroke={t.landStroke} strokeWidth={0.4 * invScale} />
288
+ <path d={graticulePath ?? undefined} fill="none" stroke={t.graticule} strokeWidth={0.3 * invScale} opacity={t.graticuleOpacity} />
200
289
 
201
290
  {/* Overlay zones (disruptions, weather) */}
202
291
  {overlays.map(o => {
@@ -1,19 +1,29 @@
1
- import React from "react";
2
1
  import SearchFilter from "./SearchFilter";
3
- import SelectFilter from "./SelectFilter";
2
+ import SelectFilter, { SelectOption } from "./SelectFilter";
4
3
  import ToggleFilter from "./ToggleFilter";
5
- import { FunnelIcon, XMarkIcon } from "@heroicons/react/24/outline";
4
+ import { XMarkIcon } from "@heroicons/react/24/outline";
5
+
6
+ export interface FilterDefinition {
7
+ id: string;
8
+ type: "search" | "select" | "toggle";
9
+ label?: string;
10
+ placeholder?: string;
11
+ options?: (string | SelectOption)[];
12
+ className?: string;
13
+ }
14
+
15
+ export interface FilterBarProps {
16
+ filters?: FilterDefinition[];
17
+ values?: Record<string, any>;
18
+ onChange?: (filterId: string, value: any) => void;
19
+ onReset?: () => void;
20
+ activeCount?: number;
21
+ layout?: "inline" | "stacked";
22
+ }
6
23
 
7
24
  /**
8
25
  * Renders a row of filter controls from a definitions array.
9
26
  * Pairs with usePageFilters hook for state management.
10
- *
11
- * @param {Array} filters — filter definitions [{ id, type, ... }]
12
- * @param {Object} values — current filter values keyed by filter id
13
- * @param {Function} onChange — (filterId, value) => void
14
- * @param {Function} onReset — () => void
15
- * @param {number} activeCount — number of active filters (for badge)
16
- * @param {string} layout — "inline" (default) or "stacked"
17
27
  */
18
28
  export default function FilterBar({
19
29
  filters = [],
@@ -22,7 +32,7 @@ export default function FilterBar({
22
32
  onReset,
23
33
  activeCount = 0,
24
34
  layout = "inline",
25
- }) {
35
+ }: FilterBarProps) {
26
36
  if (!filters.length) return null;
27
37
 
28
38
  const isStacked = layout === "stacked";
@@ -1,12 +1,18 @@
1
- import React from "react";
2
1
  import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline";
3
2
 
3
+ export interface SearchFilterProps {
4
+ value?: string;
5
+ onChange?: (value: string) => void;
6
+ placeholder?: string;
7
+ className?: string;
8
+ }
9
+
4
10
  export default function SearchFilter({
5
11
  value = "",
6
12
  onChange,
7
13
  placeholder = "Search…",
8
14
  className = "",
9
- }) {
15
+ }: SearchFilterProps) {
10
16
  return (
11
17
  <div className={["relative", className].filter(Boolean).join(" ")}>
12
18
  <MagnifyingGlassIcon
@@ -1,14 +1,21 @@
1
- import React from "react";
2
1
  import { ChevronDownIcon } from "@heroicons/react/24/outline";
3
2
 
3
+ export interface SelectOption {
4
+ value: string;
5
+ label: string;
6
+ }
7
+
8
+ export interface SelectFilterProps {
9
+ value?: string;
10
+ onChange?: (value: string) => void;
11
+ options?: (string | SelectOption)[];
12
+ label?: string;
13
+ placeholder?: string;
14
+ className?: string;
15
+ }
16
+
4
17
  /**
5
18
  * Dropdown select filter.
6
- *
7
- * @param {string} value — current selected value
8
- * @param {Function} onChange — (value) => void
9
- * @param {Array} options — [{ value, label }] or ["string", ...]
10
- * @param {string} label — visible label
11
- * @param {string} placeholder — placeholder when no value selected
12
19
  */
13
20
  export default function SelectFilter({
14
21
  value = "all",
@@ -17,7 +24,7 @@ export default function SelectFilter({
17
24
  label,
18
25
  placeholder,
19
26
  className = "",
20
- }) {
27
+ }: SelectFilterProps) {
21
28
  const normalizedOptions = options.map((opt) =>
22
29
  typeof opt === "string" ? { value: opt, label: opt } : opt
23
30
  );