leaflet-theme-control 0.0.1 → 0.0.2

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/README.md CHANGED
@@ -219,19 +219,21 @@ See [examples/api.html](examples/api.html) for a complete example.
219
219
 
220
220
  ### Options
221
221
 
222
- | Option | Type | Default | Description |
223
- | ------------------- | -------- | ---------------------- | ------------------------------------------------------------------- |
224
- | `position` | String | `"topright"` | Position of the control |
225
- | `themes` | Object | `DEFAULT_THEMES` | Theme definitions |
226
- | `defaultTheme` | String | `"light"` | Initial theme |
227
- | `storageKey` | String | `"leaflet-theme"` | localStorage key |
228
- | `detectSystemTheme` | Boolean | `true` | Detect OS dark mode |
229
- | `cssSelector` | String | `".leaflet-tile-pane"` | Elements to apply filter to |
230
- | `addButton` | Boolean | `true` | Add UI button to map (set to `false` for programmatic control only) |
231
- | `enableEditor` | Boolean | `false` | Enable theme editor UI with customization sliders |
232
- | `onChange` | Function | `null` | Callback on theme change: `(themeKey, theme) => {}` |
233
- | `getLabel` | Function | `null` | Function to get translated theme labels: `(themeKey) => string` |
234
- | `getEditorLabels` | Function | `null` | Function to get translated editor UI labels: `(key) => string` |
222
+ | Option | Type | Default | Description |
223
+ | ------------------- | -------- | ---------------------- | ---------------------------------------------------------------------------------------------------------- |
224
+ | `position` | String | `"topright"` | Position of the control |
225
+ | `themes` | Object | `DEFAULT_THEMES` | Theme definitions |
226
+ | `defaultTheme` | String | `"light"` | Initial theme |
227
+ | `storageKey` | String | `"leaflet-theme"` | localStorage key |
228
+ | `detectSystemTheme` | Boolean | `true` | Detect OS dark mode |
229
+ | `cssSelector` | String | `".leaflet-tile-pane"` | Elements to apply filter to |
230
+ | `addButton` | Boolean | `true` | Add UI button to map (set to `false` for programmatic control only) |
231
+ | `enableEditor` | Boolean | `false` | Enable theme editor UI with customization sliders |
232
+ | `onChange` | Function | `null` | Callback on theme change AND editor changes: `(themeKey, theme) => {}` |
233
+ | `getLabel` | Function | `null` | Function to get translated theme labels: `(themeKey) => string` (optional if themes have `label` property) |
234
+ | `getEditorLabels` | Function | `null` | Function to get translated editor UI labels: `(key) => string` |
235
+ | `panelPosition` | String | `"topright"` | Position of editor panel: `"topright"`, `"topleft"`, `"bottomright"`, `"bottomleft"` |
236
+ | `panelZIndex` | Number | `1000` | Z-index for editor panel to avoid conflicts |
235
237
 
236
238
  ### Methods
237
239
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leaflet-theme-control",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "A Leaflet control for switching between visual themes (light, dark, grayscale, custom, etc.) using CSS filters",
5
5
  "main": "src/leaflet-theme-control.js",
6
6
  "module": "src/leaflet-theme-control.js",
@@ -1,7 +1,7 @@
1
1
  /* CSS Custom Properties for easy theming */
2
2
  :root {
3
- /* Primary colors */
4
- --ltc-primary: #2196f3;
3
+ /* Primary colors - fallback to Leaflet defaults if available */
4
+ --ltc-primary: var(--leaflet-control-bg-color, #2196f3);
5
5
  --ltc-primary-dark: #1976d2;
6
6
  --ltc-primary-light: #64b5f6;
7
7
  --ltc-primary-bg: #e3f2fd;
@@ -9,15 +9,15 @@
9
9
  /* Success/Custom badge */
10
10
  --ltc-success: #4caf50;
11
11
 
12
- /* Neutral colors - Light mode */
13
- --ltc-bg: white;
14
- --ltc-bg-hover: #f4f4f4;
12
+ /* Neutral colors - Light mode - with Leaflet fallbacks */
13
+ --ltc-bg: var(--leaflet-control-bg-color, white);
14
+ --ltc-bg-hover: var(--leaflet-control-hover-bg-color, #f4f4f4);
15
15
  --ltc-bg-secondary: #f5f5f5;
16
16
  --ltc-bg-tertiary: #e0e0e0;
17
- --ltc-text: #333;
17
+ --ltc-text: var(--leaflet-control-text-color, #333);
18
18
  --ltc-text-secondary: #666;
19
19
  --ltc-text-tertiary: #aaa;
20
- --ltc-border: #e0e0e0;
20
+ --ltc-border: var(--leaflet-control-border-color, #e0e0e0);
21
21
  --ltc-border-dark: #bbb;
22
22
 
23
23
  /* Component specific */
@@ -68,8 +68,6 @@
68
68
 
69
69
  .leaflet-theme-panel {
70
70
  position: absolute;
71
- top: 60px;
72
- right: 10px;
73
71
  width: 320px;
74
72
  max-width: calc(100vw - 20px);
75
73
  max-height: calc(100vh - 80px);
@@ -77,12 +75,46 @@
77
75
  background: var(--ltc-bg);
78
76
  border-radius: 8px;
79
77
  box-shadow: 0 4px 12px var(--ltc-shadow);
80
- z-index: 1000;
81
78
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
82
79
  pointer-events: all;
83
80
  color: var(--ltc-text);
84
81
  }
85
82
 
83
+ /* Position variants */
84
+ .leaflet-theme-panel[data-position="topright"],
85
+ .leaflet-theme-panel[data-position="topleft"] {
86
+ top: 60px;
87
+ }
88
+
89
+ .leaflet-theme-panel[data-position="bottomright"],
90
+ .leaflet-theme-panel[data-position="bottomleft"] {
91
+ bottom: 20px;
92
+ }
93
+
94
+ .leaflet-theme-panel[data-position="topright"],
95
+ .leaflet-theme-panel[data-position="bottomright"] {
96
+ right: 10px;
97
+ }
98
+
99
+ .leaflet-theme-panel[data-position="topleft"],
100
+ .leaflet-theme-panel[data-position="bottomleft"] {
101
+ left: 10px;
102
+ }
103
+
104
+ /* Responsive positioning for small screens */
105
+ /* Use higher specificity to override position classes without !important */
106
+ @media (max-width: 480px) {
107
+ .leaflet-theme-panel.leaflet-theme-panel {
108
+ top: 50%;
109
+ left: 50%;
110
+ right: auto;
111
+ bottom: auto;
112
+ transform: translate(-50%, -50%);
113
+ width: calc(100vw - 40px);
114
+ max-height: calc(100vh - 100px);
115
+ }
116
+ }
117
+
86
118
  /* Panel Header */
87
119
  .theme-panel-header {
88
120
  display: flex;
@@ -388,18 +420,37 @@
388
420
  /* Touch-friendly larger hit area */
389
421
  @media (hover: none) and (pointer: coarse) {
390
422
  .theme-editor-slider input[type="range"] {
391
- height: 32px;
423
+ height: 40px; /* Größere Touch-Area */
424
+ }
425
+
426
+ .theme-editor-slider input[type="range"]::-webkit-slider-runnable-track {
427
+ height: 10px; /* Größere Track */
392
428
  }
393
429
 
394
430
  .theme-editor-slider input[type="range"]::-webkit-slider-thumb {
395
- width: 28px;
396
- height: 28px;
397
- margin-top: -10px;
431
+ width: 32px; /* Größerer Thumb */
432
+ height: 32px;
433
+ margin-top: -11px;
434
+ }
435
+
436
+ .theme-editor-slider input[type="range"]::-moz-range-track {
437
+ height: 10px;
398
438
  }
399
439
 
400
440
  .theme-editor-slider input[type="range"]::-moz-range-thumb {
401
- width: 28px;
402
- height: 28px;
441
+ width: 32px;
442
+ height: 32px;
443
+ }
444
+
445
+ /* Größere Buttons für Touch */
446
+ .theme-select-btn,
447
+ .theme-edit-btn {
448
+ min-height: 48px;
449
+ }
450
+
451
+ .control-style-btn {
452
+ min-height: 44px;
453
+ font-size: 14px;
403
454
  }
404
455
  }
405
456
 
@@ -25,6 +25,8 @@ export class ThemeControl extends Control {
25
25
  onChange: null,
26
26
  getLabel: null, // Function to get translated theme labels: (themeKey) => string
27
27
  getEditorLabels: null, // Function to get translated editor UI labels: (key) => string
28
+ panelPosition: 'topright', // Position of the editor panel: 'topright', 'topleft', 'bottomright', 'bottomleft'
29
+ panelZIndex: 1000, // Z-index for editor panel
28
30
  })
29
31
  }
30
32
 
@@ -204,6 +206,23 @@ export class ThemeControl extends Control {
204
206
  this.root.classList.add(theme.className)
205
207
  }
206
208
 
209
+ // Also apply theme class to control container if map exists
210
+ if (this.map) {
211
+ const controlContainer = this.map.getContainer().querySelector('.leaflet-control-container')
212
+ if (controlContainer) {
213
+ // Remove all theme classes from control container
214
+ Object.values(this.options.themes).forEach((t) => {
215
+ if (t.className) {
216
+ controlContainer.classList.remove(t.className)
217
+ }
218
+ })
219
+ // Add current theme class
220
+ if (theme.className) {
221
+ controlContainer.classList.add(theme.className)
222
+ }
223
+ }
224
+ }
225
+
207
226
  // Apply CSS filter to map tiles
208
227
  const mapElements = document.querySelectorAll(this.options.cssSelector)
209
228
  mapElements.forEach((el) => {
@@ -106,6 +106,11 @@ export class ThemeEditor {
106
106
  panel.setAttribute('aria-modal', 'true')
107
107
  panel.setAttribute('aria-labelledby', 'theme-panel-title')
108
108
 
109
+ // Apply position and z-index from options
110
+ const position = this.themeControl.options.panelPosition || 'topright'
111
+ panel.setAttribute('data-position', position)
112
+ panel.style.zIndex = this.themeControl.options.panelZIndex || 1000
113
+
109
114
  // Prevent map interactions when using the panel
110
115
  DomEvent.disableClickPropagation(panel)
111
116
  DomEvent.disableScrollPropagation(panel)
@@ -201,6 +206,37 @@ export class ThemeEditor {
201
206
  document.removeEventListener('keydown', this._onKeyDown)
202
207
  }
203
208
 
209
+ _isThemeModified(themeKey) {
210
+ // Check if theme has custom filters saved
211
+ if (!this.customFilters[themeKey]) {
212
+ return false
213
+ }
214
+
215
+ // Get default theme from DEFAULT_THEMES
216
+ const defaultTheme = DEFAULT_THEMES[themeKey]
217
+ if (!defaultTheme) {
218
+ // Custom theme not in defaults - consider it modified
219
+ return true
220
+ }
221
+
222
+ // Parse default filter and compare with custom
223
+ const defaultValues = this._parseFilterString(defaultTheme.filter)
224
+ const customValues = this.customFilters[themeKey]
225
+
226
+ // Check if any filter value differs from default
227
+ const filterKeys = ['invert', 'hueRotate', 'saturate', 'brightness', 'contrast', 'sepia', 'grayscale']
228
+ const filtersModified = filterKeys.some((key) => {
229
+ const defaultVal = defaultValues[key]
230
+ const customVal = customValues[key] !== undefined ? customValues[key] : defaultVal
231
+ return Math.abs(customVal - defaultVal) > 0.01 // Use small epsilon for float comparison
232
+ })
233
+
234
+ // Check if control style differs
235
+ const controlStyleModified = customValues.controlStyle && customValues.controlStyle !== defaultTheme.controlStyle
236
+
237
+ return filtersModified || controlStyleModified
238
+ }
239
+
204
240
  cleanup() {
205
241
  // Close panel if open
206
242
  if (this.isOpen) {
@@ -273,7 +309,7 @@ export class ThemeEditor {
273
309
  }
274
310
 
275
311
  _createThemeRow(themeKey, theme, isActive) {
276
- const hasCustom = !!this.customFilters[themeKey]
312
+ const hasCustom = this._isThemeModified(themeKey)
277
313
  const themeLabel = this.themeControl._getThemeLabel(themeKey)
278
314
 
279
315
  // Create select button with icon, label and badges
@@ -321,7 +357,7 @@ export class ThemeEditor {
321
357
 
322
358
  // Theme select buttons
323
359
  const selectBtns = this.panel.querySelectorAll('.theme-select-btn')
324
- selectBtns.forEach((btn) => {
360
+ selectBtns.forEach((btn, index) => {
325
361
  DomEvent.on(btn, 'click', (e) => {
326
362
  const themeKey = e.currentTarget.dataset.theme
327
363
 
@@ -332,6 +368,20 @@ export class ThemeEditor {
332
368
  this.themeControl.setTheme(themeKey)
333
369
  this.close()
334
370
  })
371
+
372
+ // Keyboard navigation for theme buttons
373
+ DomEvent.on(btn, 'keydown', (e) => {
374
+ if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
375
+ e.preventDefault()
376
+ const nextIndex = (index + 1) % selectBtns.length
377
+ selectBtns[nextIndex].focus()
378
+ }
379
+ else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
380
+ e.preventDefault()
381
+ const prevIndex = (index - 1 + selectBtns.length) % selectBtns.length
382
+ selectBtns[prevIndex].focus()
383
+ }
384
+ })
335
385
  })
336
386
 
337
387
  // Theme edit buttons
@@ -532,6 +582,11 @@ export class ThemeEditor {
532
582
  if (this.themeControl.getCurrentTheme() === themeKey) {
533
583
  this.themeControl.setTheme(themeKey)
534
584
  }
585
+
586
+ // Fire onChange callback for editor changes
587
+ if (this.themeControl.options.onChange) {
588
+ this.themeControl.options.onChange(themeKey, this.themeControl.options.themes[themeKey])
589
+ }
535
590
  }
536
591
 
537
592
  _resetTheme(themeKey) {
@@ -546,6 +601,16 @@ export class ThemeEditor {
546
601
  // Copy all properties from default theme
547
602
  this.themeControl.options.themes[themeKey] = { ...defaultTheme }
548
603
  }
604
+ else {
605
+ // For custom themes not in DEFAULT_THEMES, reset to parsed filter values
606
+ // This handles user-defined themes that don't have a default
607
+ const theme = this.themeControl.options.themes[themeKey]
608
+ if (theme) {
609
+ // Keep the theme but clear any custom modifications by re-parsing
610
+ // the original filter (which may still be modified, so this is a no-op for custom themes)
611
+ console.warn(`Theme "${themeKey}" has no default in DEFAULT_THEMES, cannot fully reset`)
612
+ }
613
+ }
549
614
 
550
615
  // Reapply theme (including controlStyle) BEFORE re-rendering editor
551
616
  // This ensures the DOM attribute is set correctly first
@@ -553,6 +618,11 @@ export class ThemeEditor {
553
618
  this.themeControl._applyTheme(themeKey, false)
554
619
  }
555
620
 
621
+ // Fire onChange callback for reset
622
+ if (this.themeControl.options.onChange) {
623
+ this.themeControl.options.onChange(themeKey, this.themeControl.options.themes[themeKey])
624
+ }
625
+
556
626
  // Re-render editor panel with default values
557
627
  this._renderThemeEditor(themeKey)
558
628
  }