@smartnet360/svelte-components 0.0.55 → 0.0.56

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.
@@ -43,7 +43,7 @@
43
43
 
44
44
  if (store.allSites.length > 0) {
45
45
  console.log('SiteFilterControl: Building tree...');
46
- const treeNodes = buildSiteTree(store.allSites);
46
+ const treeNodes = buildSiteTree(store.allSites, store.groupColorMap);
47
47
 
48
48
  treeStore = createTreeStore({
49
49
  nodes: [treeNodes],
@@ -71,12 +71,30 @@
71
71
  }
72
72
  }
73
73
  });
74
+
75
+ function handleColorChange(groupKey: string, color: string) {
76
+ store.setGroupColor(groupKey, color);
77
+ }
74
78
  </script>
75
79
 
76
80
  <MapControl {position} {title} collapsible={true} {initiallyCollapsed}>
77
81
  {#if treeStore && $treeStore}
78
82
  <div class="site-filter-tree">
79
- <TreeView store={$treeStore} showControls={false} />
83
+ <TreeView store={$treeStore} showControls={false}>
84
+ {#snippet children({ node, state })}
85
+ <!-- Custom node rendering with color picker for leaf nodes -->
86
+ {#if node.metadata?.type === 'featureGroup'}
87
+ <input
88
+ type="color"
89
+ class="color-picker"
90
+ value={node.metadata.color || store.color}
91
+ oninput={(e) => handleColorChange(node.id, e.currentTarget.value)}
92
+ onclick={(e) => e.stopPropagation()}
93
+ title="Choose color for this group"
94
+ />
95
+ {/if}
96
+ {/snippet}
97
+ </TreeView>
80
98
 
81
99
  <div class="site-filter-stats">
82
100
  <small class="text-muted">
@@ -102,8 +120,25 @@
102
120
  text-align: center;
103
121
  }
104
122
 
123
+ .color-picker {
124
+ width: 24px;
125
+ height: 24px;
126
+ padding: 0;
127
+ border: 1px solid #ccc;
128
+ border-radius: 4px;
129
+ cursor: pointer;
130
+ flex-shrink: 0;
131
+ }
132
+
133
+ .color-picker:hover {
134
+ border-color: #0d6efd;
135
+ }
136
+
105
137
  :global(.site-filter-tree .tree-node-label) {
106
138
  font-size: 13px;
139
+ display: flex;
140
+ align-items: center;
141
+ flex: 1;
107
142
  }
108
143
 
109
144
  :global(.site-filter-tree .tree-node-checkbox) {
@@ -43,7 +43,7 @@
43
43
  <!-- Size slider -->
44
44
  <div class="control-row">
45
45
  <label for="site-size-slider" class="control-label">
46
- Circle Radius: <strong>{store.size}px</strong>
46
+ Site Radius: <strong>{store.size}px</strong>
47
47
  </label>
48
48
  <input
49
49
  id="site-size-slider"
@@ -75,7 +75,7 @@
75
75
  <!-- Circle Color -->
76
76
  <div class="control-row">
77
77
  <label for="site-color-picker" class="control-label">
78
- Circle Color
78
+ Base Color
79
79
  </label>
80
80
  <input
81
81
  id="site-color-picker"
@@ -129,6 +129,40 @@
129
129
  bind:value={store.labelColor}
130
130
  />
131
131
  </div>
132
+
133
+ <!-- Label offset slider -->
134
+ <div class="control-row">
135
+ <label for="label-offset-slider" class="control-label">
136
+ Label Offset: <strong>{store.labelOffset.toFixed(1)}</strong>
137
+ </label>
138
+ <input
139
+ id="label-offset-slider"
140
+ type="range"
141
+ class="form-range"
142
+ min="-3"
143
+ max="3"
144
+ step="0.1"
145
+ bind:value={store.labelOffset}
146
+ />
147
+ </div>
148
+
149
+ <!-- Label property dropdown -->
150
+ <div class="control-row">
151
+ <label for="label-property-select" class="control-label">
152
+ Label Field
153
+ </label>
154
+ <select
155
+ id="label-property-select"
156
+ class="form-select"
157
+ bind:value={store.labelProperty}
158
+ >
159
+ <option value="name">Name</option>
160
+ <option value="id">ID</option>
161
+ <option value="provider">Provider</option>
162
+ <option value="technology">Technology</option>
163
+ <option value="featureGroup">Feature Group</option>
164
+ </select>
165
+ </div>
132
166
  {/if}
133
167
  </div>
134
168
  </MapControl>
@@ -182,4 +216,21 @@
182
216
  border-radius: 4px;
183
217
  cursor: pointer;
184
218
  }
219
+
220
+ .form-select {
221
+ flex: 1;
222
+ min-width: 0;
223
+ font-size: 12px;
224
+ padding: 4px 8px;
225
+ border: 1px solid #dee2e6;
226
+ border-radius: 4px;
227
+ background-color: white;
228
+ cursor: pointer;
229
+ }
230
+
231
+ .form-select:focus {
232
+ border-color: #0d6efd;
233
+ outline: none;
234
+ box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
235
+ }
185
236
  </style>
@@ -60,6 +60,9 @@
60
60
  const showLabels = store.showLabels;
61
61
  const labelSize = store.labelSize;
62
62
  const labelColor = store.labelColor;
63
+ const labelOffset = store.labelOffset;
64
+ const labelProperty = store.labelProperty;
65
+ const groupColorMap = store.groupColorMap; // Track color map changes
63
66
 
64
67
  if (map) {
65
68
  updateLayer();
@@ -107,10 +110,18 @@
107
110
  source: sourceId,
108
111
  paint: {
109
112
  'circle-radius': store.size,
110
- 'circle-color': store.color,
113
+ 'circle-color': [
114
+ 'coalesce',
115
+ ['get', 'groupColor'], // Use group-specific color if available
116
+ store.color // Fallback to global color
117
+ ],
111
118
  'circle-opacity': store.opacity,
112
119
  'circle-stroke-width': 2,
113
- 'circle-stroke-color': '#ffffff',
120
+ 'circle-stroke-color': [
121
+ 'coalesce',
122
+ ['get', 'groupColor'], // Use group-specific color if available
123
+ store.color // Fallback to global color
124
+ ],
114
125
  'circle-stroke-opacity': store.opacity
115
126
  }
116
127
  });
@@ -121,9 +132,9 @@
121
132
  type: 'symbol',
122
133
  source: sourceId,
123
134
  layout: {
124
- 'text-field': ['get', 'name'],
135
+ 'text-field': ['get', store.labelProperty],
125
136
  'text-size': store.labelSize,
126
- 'text-offset': [0, 1.5],
137
+ 'text-offset': [0, store.labelOffset],
127
138
  'text-anchor': 'top',
128
139
  'text-font': ['Open Sans Regular', 'Arial Unicode MS Regular']
129
140
  },
@@ -151,18 +162,24 @@
151
162
  function updateLayer(): void {
152
163
  if (!map) return;
153
164
 
154
- // Update data
155
- const geojson = sitesToGeoJSON(store.filteredSites);
165
+ // Update data with color information
166
+ const geojson = sitesToGeoJSON(store.filteredSites, store.groupColorMap);
156
167
  updateGeoJSONSource(map, sourceId, geojson);
157
168
 
158
169
  // Update circle visual properties
159
170
  map.setPaintProperty(layerId, 'circle-radius', store.size);
160
- map.setPaintProperty(layerId, 'circle-color', store.color);
171
+ map.setPaintProperty(layerId, 'circle-color', [
172
+ 'coalesce',
173
+ ['get', 'groupColor'],
174
+ store.color
175
+ ]);
161
176
  map.setPaintProperty(layerId, 'circle-opacity', store.opacity);
162
177
  map.setPaintProperty(layerId, 'circle-stroke-opacity', store.opacity);
163
178
 
164
179
  // Update label properties
180
+ map.setLayoutProperty(labelLayerId, 'text-field', ['get', store.labelProperty]);
165
181
  map.setLayoutProperty(labelLayerId, 'text-size', store.labelSize);
182
+ map.setLayoutProperty(labelLayerId, 'text-offset', [0, store.labelOffset]);
166
183
  map.setPaintProperty(labelLayerId, 'text-color', store.labelColor);
167
184
  map.setLayoutProperty(labelLayerId, 'visibility', store.showLabels ? 'visible' : 'none');
168
185
  }
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Uses Svelte 5 runes ($state) to create a directly bindable reactive object.
5
5
  * This allows components to use bind:value instead of manual event handlers.
6
+ * Persists visual settings to localStorage.
6
7
  */
7
8
  import type { Site, SiteStoreValue } from '../types';
8
9
  export declare function createSiteStoreContext(initialSites?: Site[]): {
@@ -15,14 +16,20 @@ export declare function createSiteStoreContext(initialSites?: Site[]): {
15
16
  showOnHover: boolean;
16
17
  labelSize: number;
17
18
  labelColor: string;
19
+ labelOffset: number;
20
+ labelProperty: string;
18
21
  strokeWidth: number;
19
22
  strokeColor: string;
23
+ groupColorMap: Map<string, string>;
20
24
  setAllSites(sites: Site[]): void;
21
25
  setFilteredSites(sites: Site[]): void;
22
26
  setSize(size: number): void;
23
27
  setColor(color: string): void;
24
28
  setOpacity(opacity: number): void;
25
29
  setShowLabels(show: boolean): void;
30
+ getGroupColor(groupKey: string): string | undefined;
31
+ setGroupColor(groupKey: string, color: string): void;
32
+ clearGroupColor(groupKey: string): void;
26
33
  reset(): void;
27
34
  getState(): SiteStoreValue;
28
35
  };
@@ -3,21 +3,81 @@
3
3
  *
4
4
  * Uses Svelte 5 runes ($state) to create a directly bindable reactive object.
5
5
  * This allows components to use bind:value instead of manual event handlers.
6
+ * Persists visual settings to localStorage.
6
7
  */
8
+ const STORAGE_KEY = 'siteVisualSettings';
9
+ function loadSettings() {
10
+ if (typeof window === 'undefined')
11
+ return {};
12
+ try {
13
+ const saved = localStorage.getItem(STORAGE_KEY);
14
+ if (saved) {
15
+ return JSON.parse(saved);
16
+ }
17
+ }
18
+ catch (error) {
19
+ console.warn('Failed to load site settings from localStorage:', error);
20
+ }
21
+ return {};
22
+ }
23
+ function saveSettings(settings) {
24
+ if (typeof window === 'undefined')
25
+ return;
26
+ try {
27
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
28
+ }
29
+ catch (error) {
30
+ console.warn('Failed to save site settings to localStorage:', error);
31
+ }
32
+ }
7
33
  export function createSiteStoreContext(initialSites = []) {
34
+ // Load persisted settings
35
+ const persistedSettings = loadSettings();
36
+ // Convert persisted groupColors object back to Map
37
+ const initialColorMap = new Map();
38
+ if (persistedSettings.groupColors) {
39
+ Object.entries(persistedSettings.groupColors).forEach(([key, value]) => {
40
+ initialColorMap.set(key, value);
41
+ });
42
+ }
8
43
  // Internal reactive state
9
44
  let state = $state({
10
45
  allSites: initialSites,
11
46
  filteredSites: initialSites,
12
- size: 10,
13
- color: '#3b82f6',
14
- opacity: 1.0,
15
- showLabels: false,
47
+ size: persistedSettings.size ?? 10,
48
+ color: persistedSettings.color ?? '#3b82f6',
49
+ opacity: persistedSettings.opacity ?? 1.0,
50
+ showLabels: persistedSettings.showLabels ?? false,
16
51
  showOnHover: true,
17
- labelSize: 12,
18
- labelColor: '#000000',
19
- strokeWidth: 2,
20
- strokeColor: '#ffffff'
52
+ labelSize: persistedSettings.labelSize ?? 12,
53
+ labelColor: persistedSettings.labelColor ?? '#000000',
54
+ labelOffset: persistedSettings.labelOffset ?? 1.5,
55
+ labelProperty: persistedSettings.labelProperty ?? 'name',
56
+ groupColorMap: initialColorMap,
57
+ strokeWidth: persistedSettings.strokeWidth ?? 2,
58
+ strokeColor: persistedSettings.strokeColor ?? '#ffffff'
59
+ });
60
+ // Auto-save settings when they change
61
+ $effect(() => {
62
+ // Convert Map to plain object for serialization
63
+ const groupColorsObj = {};
64
+ state.groupColorMap.forEach((color, key) => {
65
+ groupColorsObj[key] = color;
66
+ });
67
+ const settings = {
68
+ size: state.size,
69
+ color: state.color,
70
+ opacity: state.opacity,
71
+ showLabels: state.showLabels,
72
+ labelSize: state.labelSize,
73
+ labelColor: state.labelColor,
74
+ labelOffset: state.labelOffset,
75
+ labelProperty: state.labelProperty,
76
+ strokeWidth: state.strokeWidth ?? 2,
77
+ strokeColor: state.strokeColor ?? '#ffffff',
78
+ groupColors: groupColorsObj
79
+ };
80
+ saveSettings(settings);
21
81
  });
22
82
  // Return object with getters/setters for direct binding
23
83
  return {
@@ -40,10 +100,16 @@ export function createSiteStoreContext(initialSites = []) {
40
100
  set labelSize(value) { state.labelSize = value; },
41
101
  get labelColor() { return state.labelColor; },
42
102
  set labelColor(value) { state.labelColor = value; },
103
+ get labelOffset() { return state.labelOffset; },
104
+ set labelOffset(value) { state.labelOffset = value; },
105
+ get labelProperty() { return state.labelProperty; },
106
+ set labelProperty(value) { state.labelProperty = value; },
43
107
  get strokeWidth() { return state.strokeWidth ?? 2; },
44
108
  set strokeWidth(value) { state.strokeWidth = value; },
45
109
  get strokeColor() { return state.strokeColor ?? '#ffffff'; },
46
110
  set strokeColor(value) { state.strokeColor = value; },
111
+ get groupColorMap() { return state.groupColorMap; },
112
+ set groupColorMap(value) { state.groupColorMap = value; },
47
113
  // Convenience methods (optional, but nice to have)
48
114
  setAllSites(sites) { state.allSites = sites; },
49
115
  setFilteredSites(sites) { state.filteredSites = sites; },
@@ -51,6 +117,19 @@ export function createSiteStoreContext(initialSites = []) {
51
117
  setColor(color) { state.color = color; },
52
118
  setOpacity(opacity) { state.opacity = opacity; },
53
119
  setShowLabels(show) { state.showLabels = show; },
120
+ // Group color methods
121
+ getGroupColor(groupKey) {
122
+ return state.groupColorMap.get(groupKey);
123
+ },
124
+ setGroupColor(groupKey, color) {
125
+ state.groupColorMap.set(groupKey, color);
126
+ // Trigger reactivity by reassigning
127
+ state.groupColorMap = new Map(state.groupColorMap);
128
+ },
129
+ clearGroupColor(groupKey) {
130
+ state.groupColorMap.delete(groupKey);
131
+ state.groupColorMap = new Map(state.groupColorMap);
132
+ },
54
133
  // Reset to defaults
55
134
  reset() {
56
135
  state.allSites = initialSites;
@@ -62,8 +141,11 @@ export function createSiteStoreContext(initialSites = []) {
62
141
  state.showOnHover = true;
63
142
  state.labelSize = 12;
64
143
  state.labelColor = '#000000';
144
+ state.labelOffset = 1.5;
145
+ state.labelProperty = 'name';
65
146
  state.strokeWidth = 2;
66
147
  state.strokeColor = '#ffffff';
148
+ state.groupColorMap = new Map();
67
149
  },
68
150
  // Get snapshot of current state (useful for debugging)
69
151
  getState() {
@@ -29,6 +29,9 @@ export interface SiteStoreValue {
29
29
  showOnHover: boolean;
30
30
  labelSize: number;
31
31
  labelColor: string;
32
+ labelOffset: number;
33
+ labelProperty: string;
34
+ groupColorMap: Map<string, string>;
32
35
  hoverColor?: string;
33
36
  selectedColor?: string;
34
37
  strokeWidth?: number;
@@ -23,8 +23,10 @@ export interface FeatureCollection<T> {
23
23
  /**
24
24
  * Converts an array of sites to a GeoJSON FeatureCollection
25
25
  * Sites are represented as Point features (circles will be drawn by Mapbox)
26
+ * @param sites - Array of sites to convert
27
+ * @param colorMap - Optional map of group keys (provider:featureGroup) to colors
26
28
  */
27
- export declare function sitesToGeoJSON(sites: Site[]): FeatureCollection<SiteFeature>;
29
+ export declare function sitesToGeoJSON(sites: Site[], colorMap?: Map<string, string>): FeatureCollection<SiteFeature>;
28
30
  /**
29
31
  * Converts a single site to a GeoJSON Feature
30
32
  */
@@ -4,16 +4,25 @@
4
4
  /**
5
5
  * Converts an array of sites to a GeoJSON FeatureCollection
6
6
  * Sites are represented as Point features (circles will be drawn by Mapbox)
7
+ * @param sites - Array of sites to convert
8
+ * @param colorMap - Optional map of group keys (provider:featureGroup) to colors
7
9
  */
8
- export function sitesToGeoJSON(sites) {
9
- const features = sites.map((site) => ({
10
- type: 'Feature',
11
- geometry: {
12
- type: 'Point',
13
- coordinates: [site.longitude, site.latitude]
14
- },
15
- properties: site
16
- }));
10
+ export function sitesToGeoJSON(sites, colorMap) {
11
+ const features = sites.map((site) => {
12
+ const groupKey = `${site.provider}:${site.featureGroup}`;
13
+ const groupColor = colorMap?.get(groupKey);
14
+ return {
15
+ type: 'Feature',
16
+ geometry: {
17
+ type: 'Point',
18
+ coordinates: [site.longitude, site.latitude]
19
+ },
20
+ properties: {
21
+ ...site,
22
+ groupColor // Add groupColor property for Mapbox styling
23
+ }
24
+ };
25
+ });
17
26
  return {
18
27
  type: 'FeatureCollection',
19
28
  features
@@ -6,8 +6,10 @@ import type { TreeNode } from '../../../../core/TreeView/tree.model';
6
6
  /**
7
7
  * Builds a hierarchical tree from flat site array
8
8
  * Structure: All Sites -> Provider -> Feature Group
9
+ * @param sites - Array of sites to build tree from
10
+ * @param colorMap - Optional map of group keys (provider:featureGroup) to colors
9
11
  */
10
- export declare function buildSiteTree(sites: Site[]): TreeNode;
12
+ export declare function buildSiteTree(sites: Site[], colorMap?: Map<string, string>): TreeNode;
11
13
  /**
12
14
  * Filters sites based on checked tree paths
13
15
  */
@@ -4,8 +4,10 @@
4
4
  /**
5
5
  * Builds a hierarchical tree from flat site array
6
6
  * Structure: All Sites -> Provider -> Feature Group
7
+ * @param sites - Array of sites to build tree from
8
+ * @param colorMap - Optional map of group keys (provider:featureGroup) to colors
7
9
  */
8
- export function buildSiteTree(sites) {
10
+ export function buildSiteTree(sites, colorMap) {
9
11
  // Group sites by provider, then by feature group
10
12
  const providerGroups = new Map();
11
13
  sites.forEach((site) => {
@@ -30,6 +32,7 @@ export function buildSiteTree(sites) {
30
32
  sortedFeatureGroups.forEach((featureGroup) => {
31
33
  const groupSites = featureGroups.get(featureGroup);
32
34
  const nodeId = `${provider}:${featureGroup}`;
35
+ const groupKey = nodeId; // Use same key format for color lookup
33
36
  providerChildren.push({
34
37
  id: nodeId,
35
38
  label: `${featureGroup} (${groupSites.length})`,
@@ -39,7 +42,8 @@ export function buildSiteTree(sites) {
39
42
  type: 'featureGroup',
40
43
  provider,
41
44
  featureGroup,
42
- siteIds: groupSites.map((s) => s.id)
45
+ siteIds: groupSites.map((s) => s.id),
46
+ color: colorMap?.get(groupKey) // Add color from map if available
43
47
  }
44
48
  });
45
49
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartnet360/svelte-components",
3
- "version": "0.0.55",
3
+ "version": "0.0.56",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",