@seed-ship/mcp-ui-solid 3.0.5 → 4.0.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.
- package/CHANGELOG.md +115 -0
- package/README.md +253 -280
- package/dist/components/ChartJSRenderer.cjs +37 -15
- package/dist/components/ChartJSRenderer.cjs.map +1 -1
- package/dist/components/ChartJSRenderer.d.ts.map +1 -1
- package/dist/components/ChartJSRenderer.js +37 -15
- package/dist/components/ChartJSRenderer.js.map +1 -1
- package/dist/components/DataPreviewSection.cjs +172 -0
- package/dist/components/DataPreviewSection.cjs.map +1 -0
- package/dist/components/DataPreviewSection.d.ts +19 -0
- package/dist/components/DataPreviewSection.d.ts.map +1 -0
- package/dist/components/DataPreviewSection.js +172 -0
- package/dist/components/DataPreviewSection.js.map +1 -0
- package/dist/components/MapRenderer.cjs +168 -26
- package/dist/components/MapRenderer.cjs.map +1 -1
- package/dist/components/MapRenderer.d.ts +2 -2
- package/dist/components/MapRenderer.d.ts.map +1 -1
- package/dist/components/MapRenderer.js +169 -27
- package/dist/components/MapRenderer.js.map +1 -1
- package/dist/components/ScratchpadPanel.cjs +74 -0
- package/dist/components/ScratchpadPanel.cjs.map +1 -1
- package/dist/components/ScratchpadPanel.d.ts.map +1 -1
- package/dist/components/ScratchpadPanel.js +75 -1
- package/dist/components/ScratchpadPanel.js.map +1 -1
- package/dist/components/VerifiedText.cjs +166 -0
- package/dist/components/VerifiedText.cjs.map +1 -0
- package/dist/components/VerifiedText.d.ts +22 -0
- package/dist/components/VerifiedText.d.ts.map +1 -0
- package/dist/components/VerifiedText.js +166 -0
- package/dist/components/VerifiedText.js.map +1 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components.cjs +4 -0
- package/dist/components.cjs.map +1 -1
- package/dist/components.d.cts +4 -0
- package/dist/components.d.ts +4 -0
- package/dist/components.js +4 -0
- package/dist/components.js.map +1 -1
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/useDataValidator.cjs +31 -0
- package/dist/hooks/useDataValidator.cjs.map +1 -0
- package/dist/hooks/useDataValidator.d.ts +42 -0
- package/dist/hooks/useDataValidator.d.ts.map +1 -0
- package/dist/hooks/useDataValidator.js +31 -0
- package/dist/hooks/useDataValidator.js.map +1 -0
- package/dist/hooks.cjs +2 -0
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.d.cts +2 -0
- package/dist/hooks.d.ts +2 -0
- package/dist/hooks.js +2 -0
- package/dist/hooks.js.map +1 -1
- package/dist/index.cjs +8 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -5
- package/dist/index.d.ts +9 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/node_modules/.pnpm/@mapbox_point-geometry@1.1.0/node_modules/@mapbox/point-geometry/index.cjs +290 -0
- package/dist/node_modules/.pnpm/@mapbox_point-geometry@1.1.0/node_modules/@mapbox/point-geometry/index.cjs.map +1 -0
- package/dist/node_modules/.pnpm/@mapbox_point-geometry@1.1.0/node_modules/@mapbox/point-geometry/index.js +291 -0
- package/dist/node_modules/.pnpm/@mapbox_point-geometry@1.1.0/node_modules/@mapbox/point-geometry/index.js.map +1 -0
- package/dist/node_modules/.pnpm/@mapbox_vector-tile@2.0.4/node_modules/@mapbox/vector-tile/index.cjs +243 -0
- package/dist/node_modules/.pnpm/@mapbox_vector-tile@2.0.4/node_modules/@mapbox/vector-tile/index.cjs.map +1 -0
- package/dist/node_modules/.pnpm/@mapbox_vector-tile@2.0.4/node_modules/@mapbox/vector-tile/index.js +243 -0
- package/dist/node_modules/.pnpm/@mapbox_vector-tile@2.0.4/node_modules/@mapbox/vector-tile/index.js.map +1 -0
- package/dist/node_modules/.pnpm/color2k@2.0.3/node_modules/color2k/dist/index.exports.import.es.cjs +137 -0
- package/dist/node_modules/.pnpm/color2k@2.0.3/node_modules/color2k/dist/index.exports.import.es.cjs.map +1 -0
- package/dist/node_modules/.pnpm/color2k@2.0.3/node_modules/color2k/dist/index.exports.import.es.js +137 -0
- package/dist/node_modules/.pnpm/color2k@2.0.3/node_modules/color2k/dist/index.exports.import.es.js.map +1 -0
- package/dist/node_modules/.pnpm/pbf@4.0.1/node_modules/pbf/index.cjs +686 -0
- package/dist/node_modules/.pnpm/pbf@4.0.1/node_modules/pbf/index.cjs.map +1 -0
- package/dist/node_modules/.pnpm/pbf@4.0.1/node_modules/pbf/index.js +687 -0
- package/dist/node_modules/.pnpm/pbf@4.0.1/node_modules/pbf/index.js.map +1 -0
- package/dist/node_modules/.pnpm/pmtiles@3.2.1/node_modules/pmtiles/dist/index.cjs +1366 -0
- package/dist/node_modules/.pnpm/pmtiles@3.2.1/node_modules/pmtiles/dist/index.cjs.map +1 -0
- package/dist/node_modules/.pnpm/pmtiles@3.2.1/node_modules/pmtiles/dist/index.js +1366 -0
- package/dist/node_modules/.pnpm/pmtiles@3.2.1/node_modules/pmtiles/dist/index.js.map +1 -0
- package/dist/node_modules/.pnpm/potpack@1.0.2/node_modules/potpack/index.cjs +54 -0
- package/dist/node_modules/.pnpm/potpack@1.0.2/node_modules/potpack/index.cjs.map +1 -0
- package/dist/node_modules/.pnpm/potpack@1.0.2/node_modules/potpack/index.js +55 -0
- package/dist/node_modules/.pnpm/potpack@1.0.2/node_modules/potpack/index.js.map +1 -0
- package/dist/node_modules/.pnpm/protomaps-leaflet@4.1.1/node_modules/protomaps-leaflet/dist/esm/index.cjs +1256 -0
- package/dist/node_modules/.pnpm/protomaps-leaflet@4.1.1/node_modules/protomaps-leaflet/dist/esm/index.cjs.map +1 -0
- package/dist/node_modules/.pnpm/protomaps-leaflet@4.1.1/node_modules/protomaps-leaflet/dist/esm/index.js +1256 -0
- package/dist/node_modules/.pnpm/protomaps-leaflet@4.1.1/node_modules/protomaps-leaflet/dist/esm/index.js.map +1 -0
- package/dist/node_modules/.pnpm/quickselect@2.0.0/node_modules/quickselect/index.cjs +47 -0
- package/dist/node_modules/.pnpm/quickselect@2.0.0/node_modules/quickselect/index.cjs.map +1 -0
- package/dist/node_modules/.pnpm/quickselect@2.0.0/node_modules/quickselect/index.js +48 -0
- package/dist/node_modules/.pnpm/quickselect@2.0.0/node_modules/quickselect/index.js.map +1 -0
- package/dist/node_modules/.pnpm/rbush@3.0.1/node_modules/rbush/index.cjs +378 -0
- package/dist/node_modules/.pnpm/rbush@3.0.1/node_modules/rbush/index.cjs.map +1 -0
- package/dist/node_modules/.pnpm/rbush@3.0.1/node_modules/rbush/index.js +379 -0
- package/dist/node_modules/.pnpm/rbush@3.0.1/node_modules/rbush/index.js.map +1 -0
- package/dist/services/data-validator.cjs +85 -0
- package/dist/services/data-validator.cjs.map +1 -0
- package/dist/services/data-validator.d.ts +28 -0
- package/dist/services/data-validator.d.ts.map +1 -0
- package/dist/services/data-validator.js +85 -0
- package/dist/services/data-validator.js.map +1 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/types/chat-bus.d.ts +88 -1
- package/dist/types/chat-bus.d.ts.map +1 -1
- package/dist/types/index.d.ts +135 -6
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types.d.cts +135 -6
- package/dist/types.d.ts +135 -6
- package/package.json +5 -1
- package/src/components/ChartJSRenderer.tsx +35 -13
- package/src/components/DataPreviewSection.tsx +206 -0
- package/src/components/MapRenderer.test.tsx +94 -5
- package/src/components/MapRenderer.tsx +246 -45
- package/src/components/ScratchpadPanel.tsx +10 -2
- package/src/components/VerifiedText.tsx +187 -0
- package/src/components/index.ts +7 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/useDataValidator.ts +68 -0
- package/src/index.ts +26 -1
- package/src/services/data-validator.test.ts +151 -0
- package/src/services/data-validator.ts +149 -0
- package/src/services/index.ts +2 -0
- package/src/types/chat-bus.ts +98 -1
- package/src/types/index.ts +145 -6
- package/tsconfig.tsbuildinfo +1 -1
package/dist/types.d.ts
CHANGED
|
@@ -64,10 +64,17 @@ export interface ChartComponentParams {
|
|
|
64
64
|
labels: string[];
|
|
65
65
|
datasets: Array<{
|
|
66
66
|
label: string;
|
|
67
|
-
data: number[]
|
|
67
|
+
data: number[] | Array<{
|
|
68
|
+
x: string | number;
|
|
69
|
+
y: number;
|
|
70
|
+
}>;
|
|
68
71
|
backgroundColor?: string | string[];
|
|
69
72
|
borderColor?: string | string[];
|
|
70
73
|
borderWidth?: number;
|
|
74
|
+
/** Fill area under line (useful for time-series) */
|
|
75
|
+
fill?: boolean | string;
|
|
76
|
+
/** Line tension (0 = straight, 0.4 = smooth) */
|
|
77
|
+
tension?: number;
|
|
71
78
|
}>;
|
|
72
79
|
};
|
|
73
80
|
options?: {
|
|
@@ -88,6 +95,22 @@ export interface ChartComponentParams {
|
|
|
88
95
|
* Enable PNG export button (v2.2.0)
|
|
89
96
|
*/
|
|
90
97
|
exportable?: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Time-series axis configuration (v3.1.0).
|
|
100
|
+
* When set, x-axis labels are parsed as dates.
|
|
101
|
+
*/
|
|
102
|
+
timeAxis?: {
|
|
103
|
+
/** Date format for parsing labels (Chart.js adapter format, e.g. 'yyyy-MM-dd') */
|
|
104
|
+
parser?: string;
|
|
105
|
+
/** Display unit for x-axis ticks */
|
|
106
|
+
unit?: 'day' | 'week' | 'month' | 'quarter' | 'year';
|
|
107
|
+
/** Date format for tooltip display */
|
|
108
|
+
tooltipFormat?: string;
|
|
109
|
+
/** Min date (ISO string) */
|
|
110
|
+
min?: string;
|
|
111
|
+
/** Max date (ISO string) */
|
|
112
|
+
max?: string;
|
|
113
|
+
};
|
|
91
114
|
/**
|
|
92
115
|
* Chart container height as CSS value (v2.2.0, default '250px')
|
|
93
116
|
*/
|
|
@@ -602,9 +625,57 @@ export interface MapClusterOptions {
|
|
|
602
625
|
*/
|
|
603
626
|
animateAddingMarkers?: boolean;
|
|
604
627
|
}
|
|
628
|
+
/**
|
|
629
|
+
* GeoJSON feature popup configuration (v3.1.0)
|
|
630
|
+
*/
|
|
631
|
+
export interface MapPopupConfig {
|
|
632
|
+
/** Property key used as popup title */
|
|
633
|
+
titleField?: string;
|
|
634
|
+
/** Property keys to display in popup body */
|
|
635
|
+
fields?: string[];
|
|
636
|
+
/** Custom HTML template (use {{property}} placeholders) */
|
|
637
|
+
template?: string;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* GeoJSON style configuration (v3.1.0)
|
|
641
|
+
* Supports static styles and choropleth (data-driven) coloring.
|
|
642
|
+
*/
|
|
643
|
+
export interface MapGeoJSONStyle {
|
|
644
|
+
/** Fill color (CSS color or choropleth config) */
|
|
645
|
+
fillColor?: string;
|
|
646
|
+
/** Fill opacity (0-1, default: 0.6) */
|
|
647
|
+
fillOpacity?: number;
|
|
648
|
+
/** Stroke color (default: '#333') */
|
|
649
|
+
strokeColor?: string;
|
|
650
|
+
/** Stroke width (default: 1) */
|
|
651
|
+
strokeWeight?: number;
|
|
652
|
+
/** Stroke opacity (0-1, default: 1) */
|
|
653
|
+
strokeOpacity?: number;
|
|
654
|
+
/** Choropleth: property key for data-driven coloring */
|
|
655
|
+
choroplethField?: string;
|
|
656
|
+
/** Choropleth: color scale stops [value, color][] sorted ascending */
|
|
657
|
+
choroplethScale?: Array<[number, string]>;
|
|
658
|
+
/** Choropleth: color for features with missing/null values */
|
|
659
|
+
choroplethFallback?: string;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Named GeoJSON layer for multi-layer maps (v3.1.0)
|
|
663
|
+
*/
|
|
664
|
+
export interface MapLayer {
|
|
665
|
+
/** Layer name (shown in layer control) */
|
|
666
|
+
name: string;
|
|
667
|
+
/** Is this layer visible by default? */
|
|
668
|
+
visible?: boolean;
|
|
669
|
+
/** GeoJSON FeatureCollection (inline or from API) */
|
|
670
|
+
geojson: unknown;
|
|
671
|
+
/** Per-layer style override */
|
|
672
|
+
style?: MapGeoJSONStyle;
|
|
673
|
+
/** Per-layer popup config */
|
|
674
|
+
popup?: MapPopupConfig;
|
|
675
|
+
}
|
|
605
676
|
/**
|
|
606
677
|
* Map component parameters (Sprint 6)
|
|
607
|
-
* Updated
|
|
678
|
+
* Updated v3.1.0: GeoJSON, choropleth, popups, layers
|
|
608
679
|
*/
|
|
609
680
|
export interface MapComponentParams {
|
|
610
681
|
/**
|
|
@@ -624,7 +695,7 @@ export interface MapComponentParams {
|
|
|
624
695
|
*/
|
|
625
696
|
height?: string;
|
|
626
697
|
/**
|
|
627
|
-
* Auto-fit bounds to show all markers (default: false)
|
|
698
|
+
* Auto-fit bounds to show all markers/features (default: false)
|
|
628
699
|
*/
|
|
629
700
|
fitBounds?: boolean;
|
|
630
701
|
/**
|
|
@@ -645,15 +716,73 @@ export interface MapComponentParams {
|
|
|
645
716
|
attribution?: string;
|
|
646
717
|
/**
|
|
647
718
|
* Enable marker clustering (Sprint Ultimate U.2)
|
|
648
|
-
* - true: Enable with default options
|
|
649
|
-
* - false: Disable clustering
|
|
650
|
-
* - MapClusterOptions: Enable with custom options
|
|
651
719
|
*/
|
|
652
720
|
clustering?: boolean | MapClusterOptions;
|
|
653
721
|
/**
|
|
654
722
|
* Custom CSS class (Sprint 7)
|
|
655
723
|
*/
|
|
656
724
|
className?: string;
|
|
725
|
+
/**
|
|
726
|
+
* GeoJSON FeatureCollection to render on the map.
|
|
727
|
+
* Use this for polygons, lines, points from structured data.
|
|
728
|
+
*/
|
|
729
|
+
geojson?: unknown;
|
|
730
|
+
/**
|
|
731
|
+
* Style for the GeoJSON layer.
|
|
732
|
+
* Supports static colors and choropleth (data-driven) coloring.
|
|
733
|
+
*/
|
|
734
|
+
geojsonStyle?: MapGeoJSONStyle;
|
|
735
|
+
/**
|
|
736
|
+
* Popup configuration for GeoJSON features.
|
|
737
|
+
* Shown on feature click.
|
|
738
|
+
*/
|
|
739
|
+
popup?: MapPopupConfig;
|
|
740
|
+
/**
|
|
741
|
+
* Named layers for multi-layer maps.
|
|
742
|
+
* Each layer has its own GeoJSON, style, and popup config.
|
|
743
|
+
* A Leaflet layer control is added when layers are present.
|
|
744
|
+
*/
|
|
745
|
+
layers?: MapLayer[];
|
|
746
|
+
/**
|
|
747
|
+
* PMTiles vector tile source for large datasets (>5000 features).
|
|
748
|
+
* Requires protomaps-leaflet peer dependency.
|
|
749
|
+
* Pipeline: GeoParquet -> Tippecanoe -> PMTiles (static file on S3/CDN).
|
|
750
|
+
*/
|
|
751
|
+
pmtiles?: MapPMTilesConfig;
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* PMTiles configuration for large vector tile datasets (v3.1.0)
|
|
755
|
+
*/
|
|
756
|
+
export interface MapPMTilesConfig {
|
|
757
|
+
/** URL to the .pmtiles file (S3, CDN, local) */
|
|
758
|
+
url: string;
|
|
759
|
+
/** Attribution text for this tile source */
|
|
760
|
+
attribution?: string;
|
|
761
|
+
/** Style rules for vector features */
|
|
762
|
+
paintRules?: Array<{
|
|
763
|
+
/** Layer name in the PMTiles source */
|
|
764
|
+
dataLayer: string;
|
|
765
|
+
/** Symbol type */
|
|
766
|
+
symbolizer: 'polygon' | 'line' | 'circle';
|
|
767
|
+
/** Fill/stroke color (CSS color or function name) */
|
|
768
|
+
color?: string;
|
|
769
|
+
/** Stroke width */
|
|
770
|
+
width?: number;
|
|
771
|
+
/** Fill opacity */
|
|
772
|
+
opacity?: number;
|
|
773
|
+
}>;
|
|
774
|
+
/** Label rules for text labels */
|
|
775
|
+
labelRules?: Array<{
|
|
776
|
+
dataLayer: string;
|
|
777
|
+
/** Property key for label text */
|
|
778
|
+
textField: string;
|
|
779
|
+
/** Font size */
|
|
780
|
+
fontSize?: number;
|
|
781
|
+
}>;
|
|
782
|
+
/** Max zoom level */
|
|
783
|
+
maxZoom?: number;
|
|
784
|
+
/** Min zoom level */
|
|
785
|
+
minZoom?: number;
|
|
657
786
|
}
|
|
658
787
|
/**
|
|
659
788
|
* Grid component parameters (Phase 5.0)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@seed-ship/mcp-ui-solid",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "SolidJS components for rendering MCP-generated UI resources",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -112,6 +112,7 @@
|
|
|
112
112
|
"highlight.js": "^11.9.0",
|
|
113
113
|
"leaflet": "^1.9.4",
|
|
114
114
|
"leaflet.markercluster": "^1.5.0",
|
|
115
|
+
"protomaps-leaflet": "^4.0.0",
|
|
115
116
|
"solid-js": "^1.9.0"
|
|
116
117
|
},
|
|
117
118
|
"peerDependenciesMeta": {
|
|
@@ -132,6 +133,9 @@
|
|
|
132
133
|
},
|
|
133
134
|
"@tanstack/solid-virtual": {
|
|
134
135
|
"optional": true
|
|
136
|
+
},
|
|
137
|
+
"protomaps-leaflet": {
|
|
138
|
+
"optional": true
|
|
135
139
|
}
|
|
136
140
|
},
|
|
137
141
|
"dependencies": {
|
|
@@ -117,23 +117,45 @@ export const ChartJSRenderer: Component<ChartJSRendererProps> = (props) => {
|
|
|
117
117
|
chartInstance = null
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
// Build options, merging time-axis config if present (v3.1.0)
|
|
121
|
+
const baseOptions: any = {
|
|
122
|
+
responsive: true,
|
|
123
|
+
maintainAspectRatio: false,
|
|
124
|
+
...chartParams.options,
|
|
125
|
+
plugins: {
|
|
126
|
+
...chartParams.options?.plugins,
|
|
127
|
+
legend: {
|
|
128
|
+
display: true,
|
|
129
|
+
position: 'bottom',
|
|
130
|
+
...chartParams.options?.plugins?.legend,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Time-series axis (v3.1.0)
|
|
136
|
+
if (chartParams.timeAxis) {
|
|
137
|
+
const ta = chartParams.timeAxis
|
|
138
|
+
baseOptions.scales = {
|
|
139
|
+
...baseOptions.scales,
|
|
140
|
+
x: {
|
|
141
|
+
...baseOptions.scales?.x,
|
|
142
|
+
type: 'time',
|
|
143
|
+
time: {
|
|
144
|
+
parser: ta.parser,
|
|
145
|
+
unit: ta.unit,
|
|
146
|
+
tooltipFormat: ta.tooltipFormat,
|
|
147
|
+
},
|
|
148
|
+
...(ta.min ? { min: ta.min } : {}),
|
|
149
|
+
...(ta.max ? { max: ta.max } : {}),
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
120
154
|
// Create new chart
|
|
121
155
|
chartInstance = new Chart(canvasRef, {
|
|
122
156
|
type: chartParams.type,
|
|
123
157
|
data: chartParams.data,
|
|
124
|
-
options:
|
|
125
|
-
responsive: true,
|
|
126
|
-
maintainAspectRatio: false,
|
|
127
|
-
...chartParams.options,
|
|
128
|
-
plugins: {
|
|
129
|
-
...chartParams.options?.plugins,
|
|
130
|
-
legend: {
|
|
131
|
-
display: true,
|
|
132
|
-
position: 'bottom',
|
|
133
|
-
...chartParams.options?.plugins?.legend,
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
},
|
|
158
|
+
options: baseOptions,
|
|
137
159
|
})
|
|
138
160
|
|
|
139
161
|
setIsLoading(false)
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DataPreviewSection — paginated data table with export
|
|
3
|
+
* v3.1.0: Replaces LLM-generated markdown tables with exact source data
|
|
4
|
+
*
|
|
5
|
+
* @experimental
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Column types (number right-aligned, string left-aligned)
|
|
9
|
+
* - Pagination (configurable page size)
|
|
10
|
+
* - CSV / JSON export buttons
|
|
11
|
+
* - Source attribution + freshness label
|
|
12
|
+
* - Number formatting (FR locale)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createSignal, createMemo, For, Show } from 'solid-js'
|
|
16
|
+
import type { DataPreviewContent, DataPreviewColumn } from '../types/chat-bus'
|
|
17
|
+
|
|
18
|
+
export interface DataPreviewSectionProps {
|
|
19
|
+
content: DataPreviewContent
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Format a number for display (French locale) */
|
|
23
|
+
function formatNumber(value: unknown, format?: string): string {
|
|
24
|
+
if (typeof value !== 'number' || !isFinite(value)) return String(value ?? '')
|
|
25
|
+
// Simple formatting: use locale
|
|
26
|
+
if (format === 'percent') return `${(value * 100).toFixed(1)}%`
|
|
27
|
+
if (format === 'currency') return `${value.toLocaleString('fr-FR')} EUR`
|
|
28
|
+
if (Number.isInteger(value)) return value.toLocaleString('fr-FR')
|
|
29
|
+
return value.toLocaleString('fr-FR', { maximumFractionDigits: 2 })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Format a cell value based on column type */
|
|
33
|
+
function formatCell(value: unknown, col: DataPreviewColumn): string {
|
|
34
|
+
if (value == null) return '—'
|
|
35
|
+
if (col.type === 'number') return formatNumber(value, col.format)
|
|
36
|
+
if (col.type === 'date' && typeof value === 'string') {
|
|
37
|
+
try {
|
|
38
|
+
return new Date(value).toLocaleDateString('fr-FR')
|
|
39
|
+
} catch {
|
|
40
|
+
return value
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return String(value)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Generate CSV from columns + rows */
|
|
47
|
+
function toCSV(columns: DataPreviewColumn[], rows: Record<string, unknown>[]): string {
|
|
48
|
+
const header = columns.map(c => `"${c.label.replace(/"/g, '""')}"`).join(';')
|
|
49
|
+
const body = rows.map(row =>
|
|
50
|
+
columns.map(c => {
|
|
51
|
+
const val = row[c.key]
|
|
52
|
+
if (val == null) return ''
|
|
53
|
+
if (typeof val === 'string') return `"${val.replace(/"/g, '""')}"`
|
|
54
|
+
return String(val)
|
|
55
|
+
}).join(';')
|
|
56
|
+
).join('\n')
|
|
57
|
+
return `${header}\n${body}`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Trigger browser download */
|
|
61
|
+
function downloadFile(content: string, filename: string, mimeType: string) {
|
|
62
|
+
const blob = new Blob([content], { type: mimeType })
|
|
63
|
+
const url = URL.createObjectURL(blob)
|
|
64
|
+
const a = document.createElement('a')
|
|
65
|
+
a.href = url
|
|
66
|
+
a.download = filename
|
|
67
|
+
a.click()
|
|
68
|
+
URL.revokeObjectURL(url)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function DataPreviewSection(props: DataPreviewSectionProps) {
|
|
72
|
+
const content = () => props.content
|
|
73
|
+
const pageSize = () => content().pageSize || 25
|
|
74
|
+
const [page, setPage] = createSignal(0)
|
|
75
|
+
|
|
76
|
+
const totalRows = () => content().rows.length
|
|
77
|
+
const totalPages = () => Math.max(1, Math.ceil(totalRows() / pageSize()))
|
|
78
|
+
|
|
79
|
+
const pagedRows = createMemo(() => {
|
|
80
|
+
const start = page() * pageSize()
|
|
81
|
+
return content().rows.slice(start, start + pageSize())
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const handleExportCSV = () => {
|
|
85
|
+
const csv = toCSV(content().columns, content().rows)
|
|
86
|
+
downloadFile(csv, 'data-export.csv', 'text/csv;charset=utf-8')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const handleExportJSON = () => {
|
|
90
|
+
const json = JSON.stringify(content().rows, null, 2)
|
|
91
|
+
downloadFile(json, 'data-export.json', 'application/json')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const columnAlign = (col: DataPreviewColumn) => {
|
|
95
|
+
if (col.align) return col.align
|
|
96
|
+
if (col.type === 'number') return 'right'
|
|
97
|
+
return 'left'
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div class="data-preview-section">
|
|
102
|
+
{/* Header with source + export */}
|
|
103
|
+
<div class="flex items-center justify-between mb-2">
|
|
104
|
+
<div class="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
|
|
105
|
+
<Show when={content().source}>
|
|
106
|
+
<span class="font-medium">{content().source}</span>
|
|
107
|
+
</Show>
|
|
108
|
+
<Show when={content().freshness}>
|
|
109
|
+
<span class="px-1.5 py-0.5 rounded bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300">
|
|
110
|
+
{content().freshness}
|
|
111
|
+
</span>
|
|
112
|
+
</Show>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<Show when={content().exportable !== false}>
|
|
116
|
+
<div class="flex items-center gap-1">
|
|
117
|
+
<button
|
|
118
|
+
class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
|
119
|
+
onClick={handleExportCSV}
|
|
120
|
+
title="Export CSV"
|
|
121
|
+
>
|
|
122
|
+
CSV
|
|
123
|
+
</button>
|
|
124
|
+
<button
|
|
125
|
+
class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
|
126
|
+
onClick={handleExportJSON}
|
|
127
|
+
title="Export JSON"
|
|
128
|
+
>
|
|
129
|
+
JSON
|
|
130
|
+
</button>
|
|
131
|
+
</div>
|
|
132
|
+
</Show>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
{/* Table */}
|
|
136
|
+
<div class="overflow-x-auto rounded border border-gray-200 dark:border-gray-700">
|
|
137
|
+
<table class="w-full text-sm">
|
|
138
|
+
<thead>
|
|
139
|
+
<tr class="bg-gray-50 dark:bg-gray-800">
|
|
140
|
+
<For each={content().columns}>
|
|
141
|
+
{(col) => (
|
|
142
|
+
<th
|
|
143
|
+
class="px-3 py-2 font-medium text-xs text-gray-600 dark:text-gray-400 uppercase tracking-wider border-b border-gray-200 dark:border-gray-700"
|
|
144
|
+
style={{ "text-align": columnAlign(col) }}
|
|
145
|
+
>
|
|
146
|
+
{col.label}
|
|
147
|
+
</th>
|
|
148
|
+
)}
|
|
149
|
+
</For>
|
|
150
|
+
</tr>
|
|
151
|
+
</thead>
|
|
152
|
+
<tbody>
|
|
153
|
+
<For each={pagedRows()}>
|
|
154
|
+
{(row, i) => (
|
|
155
|
+
<tr
|
|
156
|
+
class="border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors"
|
|
157
|
+
classList={{ 'bg-gray-25 dark:bg-gray-850': i() % 2 === 1 }}
|
|
158
|
+
>
|
|
159
|
+
<For each={content().columns}>
|
|
160
|
+
{(col) => (
|
|
161
|
+
<td
|
|
162
|
+
class="px-3 py-2 text-gray-800 dark:text-gray-200"
|
|
163
|
+
style={{ "text-align": columnAlign(col) }}
|
|
164
|
+
>
|
|
165
|
+
{formatCell(row[col.key], col)}
|
|
166
|
+
</td>
|
|
167
|
+
)}
|
|
168
|
+
</For>
|
|
169
|
+
</tr>
|
|
170
|
+
)}
|
|
171
|
+
</For>
|
|
172
|
+
</tbody>
|
|
173
|
+
</table>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
{/* Footer: pagination + row count */}
|
|
177
|
+
<div class="flex items-center justify-between mt-2 text-xs text-gray-500 dark:text-gray-400">
|
|
178
|
+
<span>
|
|
179
|
+
{content().totalRows
|
|
180
|
+
? `${totalRows()} / ${content().totalRows!.toLocaleString('fr-FR')} rows`
|
|
181
|
+
: `${totalRows()} row${totalRows() !== 1 ? 's' : ''}`}
|
|
182
|
+
</span>
|
|
183
|
+
|
|
184
|
+
<Show when={totalPages() > 1}>
|
|
185
|
+
<div class="flex items-center gap-1">
|
|
186
|
+
<button
|
|
187
|
+
class="px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-40 transition-colors"
|
|
188
|
+
disabled={page() === 0}
|
|
189
|
+
onClick={() => setPage(p => p - 1)}
|
|
190
|
+
>
|
|
191
|
+
«
|
|
192
|
+
</button>
|
|
193
|
+
<span>{page() + 1} / {totalPages()}</span>
|
|
194
|
+
<button
|
|
195
|
+
class="px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-40 transition-colors"
|
|
196
|
+
disabled={page() >= totalPages() - 1}
|
|
197
|
+
onClick={() => setPage(p => p + 1)}
|
|
198
|
+
>
|
|
199
|
+
»
|
|
200
|
+
</button>
|
|
201
|
+
</div>
|
|
202
|
+
</Show>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
)
|
|
206
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MapRenderer Tests
|
|
3
|
-
* Sprint 6
|
|
3
|
+
* Sprint 6 + v3.1.0: GeoJSON, choropleth, popups
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
@@ -24,6 +24,7 @@ const mapMock = {
|
|
|
24
24
|
remove: removeMock,
|
|
25
25
|
getZoom: vi.fn(() => 13),
|
|
26
26
|
fitBounds: fitBoundsMock.mockReturnThis(),
|
|
27
|
+
addLayer: addLayerMock,
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const markerMock = {
|
|
@@ -37,11 +38,21 @@ const tileLayerMock = {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
const controlMock = {
|
|
40
|
-
attribution: vi.fn(() => ({ addTo: vi.fn() }))
|
|
41
|
+
attribution: vi.fn(() => ({ addTo: vi.fn() })),
|
|
42
|
+
layers: vi.fn(() => ({ addTo: vi.fn() }))
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
const featureGroupMock = {
|
|
44
|
-
getBounds: vi.fn(() => ({ pad: vi.fn() }))
|
|
46
|
+
getBounds: vi.fn(() => ({ pad: vi.fn(), isValid: vi.fn(() => true) }))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const geoJSONMock = {
|
|
50
|
+
addTo: vi.fn().mockReturnThis(),
|
|
51
|
+
getBounds: vi.fn(() => ({ pad: vi.fn(), isValid: vi.fn(() => true) })),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const circleMarkerMock = {
|
|
55
|
+
bindPopup: vi.fn().mockReturnThis(),
|
|
45
56
|
}
|
|
46
57
|
|
|
47
58
|
vi.mock('leaflet', () => ({
|
|
@@ -51,6 +62,11 @@ vi.mock('leaflet', () => ({
|
|
|
51
62
|
marker: vi.fn(() => markerMock),
|
|
52
63
|
featureGroup: vi.fn(() => featureGroupMock),
|
|
53
64
|
control: controlMock,
|
|
65
|
+
geoJSON: vi.fn(() => geoJSONMock),
|
|
66
|
+
circleMarker: vi.fn(() => circleMarkerMock),
|
|
67
|
+
GeoJSON: class {},
|
|
68
|
+
CircleMarker: class {},
|
|
69
|
+
Marker: class {},
|
|
54
70
|
Icon: {
|
|
55
71
|
Default: {
|
|
56
72
|
prototype: { _getIconUrl: vi.fn() },
|
|
@@ -82,8 +98,7 @@ describe('MapRenderer', () => {
|
|
|
82
98
|
expect(mapDiv).toBeTruthy()
|
|
83
99
|
})
|
|
84
100
|
|
|
85
|
-
it('renders with
|
|
86
|
-
// This test mostly verifies type check and structural validity since actual logic is mocked
|
|
101
|
+
it('renders with marker format', () => {
|
|
87
102
|
const { container } = render(() => <MapRenderer params={{
|
|
88
103
|
markers: [{ position: [10, 20], tooltip: 'Hello', popup: 'World' }]
|
|
89
104
|
}} />)
|
|
@@ -97,4 +112,78 @@ describe('MapRenderer', () => {
|
|
|
97
112
|
}} />)
|
|
98
113
|
expect(container).toBeTruthy()
|
|
99
114
|
})
|
|
115
|
+
|
|
116
|
+
// ─── GeoJSON tests (v3.1.0) ────────────────────────
|
|
117
|
+
|
|
118
|
+
const SAMPLE_GEOJSON = {
|
|
119
|
+
type: 'FeatureCollection',
|
|
120
|
+
features: [
|
|
121
|
+
{
|
|
122
|
+
type: 'Feature',
|
|
123
|
+
geometry: { type: 'Polygon', coordinates: [[[2.3, 48.8], [2.4, 48.8], [2.4, 48.9], [2.3, 48.9], [2.3, 48.8]]] },
|
|
124
|
+
properties: { name: 'Zone A', prix_m2: 3500 }
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
type: 'Feature',
|
|
128
|
+
geometry: { type: 'Polygon', coordinates: [[[2.4, 48.8], [2.5, 48.8], [2.5, 48.9], [2.4, 48.9], [2.4, 48.8]]] },
|
|
129
|
+
properties: { name: 'Zone B', prix_m2: 4200 }
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
it('renders with GeoJSON data', () => {
|
|
135
|
+
const { container } = render(() => <MapRenderer params={{
|
|
136
|
+
geojson: SAMPLE_GEOJSON,
|
|
137
|
+
fitBounds: true,
|
|
138
|
+
}} />)
|
|
139
|
+
expect(container).toBeTruthy()
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('renders with GeoJSON + popup config', () => {
|
|
143
|
+
const { container } = render(() => <MapRenderer params={{
|
|
144
|
+
geojson: SAMPLE_GEOJSON,
|
|
145
|
+
popup: { titleField: 'name', fields: ['prix_m2'] },
|
|
146
|
+
}} />)
|
|
147
|
+
expect(container).toBeTruthy()
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('renders with choropleth style', () => {
|
|
151
|
+
const { container } = render(() => <MapRenderer params={{
|
|
152
|
+
geojson: SAMPLE_GEOJSON,
|
|
153
|
+
geojsonStyle: {
|
|
154
|
+
choroplethField: 'prix_m2',
|
|
155
|
+
choroplethScale: [[2000, '#eff3ff'], [3000, '#6baed6'], [5000, '#084594']],
|
|
156
|
+
fillOpacity: 0.7,
|
|
157
|
+
},
|
|
158
|
+
}} />)
|
|
159
|
+
expect(container).toBeTruthy()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('renders with multiple named layers', () => {
|
|
163
|
+
const { container } = render(() => <MapRenderer params={{
|
|
164
|
+
layers: [
|
|
165
|
+
{ name: 'Zones', geojson: SAMPLE_GEOJSON, visible: true },
|
|
166
|
+
{ name: 'Points', geojson: { type: 'FeatureCollection', features: [] }, visible: false },
|
|
167
|
+
],
|
|
168
|
+
}} />)
|
|
169
|
+
expect(container).toBeTruthy()
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('renders with custom height', () => {
|
|
173
|
+
const { container } = render(() => <MapRenderer params={{
|
|
174
|
+
geojson: SAMPLE_GEOJSON,
|
|
175
|
+
height: '600px',
|
|
176
|
+
}} />)
|
|
177
|
+
const mapDiv = container.querySelector('div[style*="height: 600px"]')
|
|
178
|
+
expect(mapDiv).toBeTruthy()
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('renders with className', () => {
|
|
182
|
+
const { container } = render(() => <MapRenderer params={{
|
|
183
|
+
geojson: SAMPLE_GEOJSON,
|
|
184
|
+
className: 'custom-map',
|
|
185
|
+
}} />)
|
|
186
|
+
const wrapper = container.querySelector('.custom-map')
|
|
187
|
+
expect(wrapper).toBeTruthy()
|
|
188
|
+
})
|
|
100
189
|
})
|