From f215133997231e2e638836c1f056fb21e01ce8e9 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Thu, 5 Feb 2026 15:49:07 +0000 Subject: [PATCH] feat(map): Introduce CSS Grid sidebar layout and integrated navigation + draw panels, add directions view and step-to-map interaction --- changelog.md | 10 + readme.hints.md | 54 ++-- ts_web/00_commitinfo_data.ts | 2 +- ts_web/elements/00componentstyles.ts | 271 +++++++++++++++++- .../00group-map/dees-geo-map/dees-geo-map.ts | 136 ++++++--- .../00group-map/dees-geo-map/geo-map.icons.ts | 3 + .../dees-geo-map/geo-map.navigation.ts | 112 ++++++-- 7 files changed, 492 insertions(+), 96 deletions(-) diff --git a/changelog.md b/changelog.md index 48e1e36..faac312 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,15 @@ # Changelog +## 2026-02-05 - 1.2.0 - feat(map) +Introduce CSS Grid sidebar layout and integrated navigation + draw panels, add directions view and step-to-map interaction + +- Replace overlay/header layout with a CSS Grid: left and right sidebars (controlled via --left-panel-width/--right-panel-width) that push the map area instead of overlaying it. +- Move navigation panel from map overlay into a left sidebar and add NavigationController viewMode with a new directions view (summary, back button) and planning view separation. +- Add a right-side draw tools panel (draw panel UI, tool grid, actions) with isDrawPanelOpen state and toolbar toggle to show/hide draw tools. +- Add interactivity: clicking a turn-by-turn step flies the map to that step (flyToStep), and directions->planning switching via setViewMode/back button. +- Numerous style additions and adjustments: header toolbar grid area, sidebar/draw panel styling, nav directions styles, z-index tweaks, cursor and hover states. +- Add new arrowLeft icon and reorganize toolbar controls (navigation/draw toggles, traffic, zoom) to accommodate sidebar layout + ## 2026-02-05 - 1.1.1 - fix(assets) remove header toolbar PNG assets (header-toolbar-fixed, header-toolbar-full, header-toolbar-layout, header-toolbar-v3) diff --git a/readme.hints.md b/readme.hints.md index 92f1bb3..ac6fe9b 100644 --- a/readme.hints.md +++ b/readme.hints.md @@ -82,6 +82,7 @@ The navigation panel (`showNavigation={true}`) provides A-to-B routing using OSR - **Transport modes**: Driving, Walking, Cycling - **Point selection**: Type an address or click on the map - **Route display**: Blue line overlay with turn-by-turn directions +- **Click on step**: Click any turn-by-turn step to fly/pan the map to that location - **API**: Uses free OSRM API (https://router.project-osrm.org) with fair-use rate limit - **Traffic-aware routing**: When a traffic provider is configured, shows duration with/without traffic @@ -209,34 +210,55 @@ const nav = new NavigationController({ ### UI Layout -The component uses a header toolbar above the map for a cleaner layout: +The component uses a CSS Grid layout with sidebars that **push** the map (not overlay): ``` ┌──────────────────────────────────────────────────────────────────────┐ │ HEADER TOOLBAR │ -│ [Draw Tools] | [Search Bar] | [Nav Toggle] [Traffic] [Zoom +/-] │ -├──────────────────────────────────────────────────────────────────────┤ -│ │ -│ [Navigation] MAP │ -│ (toggleable) │ -│ │ -│ [Traffic Legend] [Feature Count] │ -└──────────────────────────────────────────────────────────────────────┘ +│ [Nav Toggle] | [Search Bar] | [Draw Toggle] [Traffic] [+/-] │ +├───────────────┬──────────────────────────────┬───────────────────────┤ +│ LEFT SIDEBAR │ MAP │ RIGHT SIDEBAR │ +│ (Navigation) │ │ (Draw Tools) │ +│ │ │ │ +│ - Mode select │ │ [Point] [Line] │ +│ - Start input │ │ [Polygon][Rect] │ +│ - End input │ │ [Circle] [Free] │ +│ - Route steps │ │ ───────────────── │ +│ │ [Traffic Legend] │ [Select & Edit] │ +│ │ [Feature Count] │ [Clear All] │ +└───────────────┴──────────────────────────────┴───────────────────────┘ ``` -**Header Toolbar Sections:** -- **Left**: Draw tools (Point, Line, Polygon, Rectangle, Circle, Freehand, Select, Clear) -- **Center**: Search bar (expandable width) -- **Right**: Navigation toggle, Traffic toggle, Zoom in/out buttons +**CSS Grid Layout:** +- Grid columns: `var(--left-panel-width) 1fr var(--right-panel-width)` +- Grid rows: `auto 1fr` (header + content) +- Sidebars push the map area, not overlay it -**Map Overlays:** -- Navigation panel: Toggleable overlay on top-left of map +**Header Toolbar Sections:** +- **Left**: Navigation panel toggle button +- **Center**: Search bar (expandable width) +- **Right**: Draw panel toggle, Traffic toggle, Zoom in/out buttons + +**Left Sidebar (300px when open):** +- Contains NavigationController render output +- Slides in/out with 0.25s ease transition +- Default open when `showNavigation={true}` + +**Right Sidebar (180px when open):** +- Draw tools panel with 2-column grid layout +- Tool buttons with icons AND labels +- Select & Edit and Clear All actions +- Slides in/out with 0.25s ease transition +- Default open when `showToolbar={true}` + +**Map Overlays (remaining):** - Traffic legend: Bottom-left overlay (when traffic enabled) - Feature count: Bottom-left overlay (when features exist) **Z-index hierarchy:** - `z-index: 20` - Dropdowns (search results, nav search results) -- `z-index: 5` - Map overlays (navigation panel, traffic legend, feature count) +- `z-index: 10` - Header toolbar +- `z-index: 5` - Map overlays (traffic legend, feature count) ### Shadow DOM & Terra-Draw Drawing Fix Terra-draw's event listeners normally intercept map events through MapLibre's canvas element. In Shadow DOM contexts, these events are scoped locally and don't propagate correctly, causing terra-draw handlers to fail while MapLibre's drag handlers continue working. diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index 0bbd6a9..f36a8d2 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@design.estate/dees-catalog-geo', - version: '1.1.1', + version: '1.2.0', description: 'A geospatial web components library with MapLibre GL JS maps and terra-draw drawing tools' } diff --git a/ts_web/elements/00componentstyles.ts b/ts_web/elements/00componentstyles.ts index b67c0d7..e77ba9b 100644 --- a/ts_web/elements/00componentstyles.ts +++ b/ts_web/elements/00componentstyles.ts @@ -652,6 +652,7 @@ export const navigationStyles = css` padding: 10px 12px; border-bottom: 1px solid rgba(255, 255, 255, 0.05); transition: background-color 0.15s ease; + cursor: pointer; } .nav-step:last-child { @@ -732,6 +733,73 @@ export const navigationStyles = css` font-size: 12px; } + /* Directions view styles */ + .nav-directions-header { + display: flex; + align-items: center; + gap: 12px; + padding: 12px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + } + + .nav-back-btn { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: none; + border-radius: 6px; + background: rgba(255, 255, 255, 0.1); + color: var(--geo-text, #fff); + cursor: pointer; + transition: background-color 0.15s ease; + flex-shrink: 0; + } + + .nav-back-btn:hover { + background: rgba(255, 255, 255, 0.2); + } + + .nav-back-btn svg { + width: 18px; + height: 18px; + } + + .nav-directions-summary { + flex: 1; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 6px; + font-size: 13px; + font-weight: 600; + color: var(--geo-text, #fff); + } + + .nav-directions-summary svg { + width: 14px; + height: 14px; + color: var(--geo-tool-active, #0084ff); + } + + .nav-directions-separator { + color: rgba(255, 255, 255, 0.4); + margin: 0 4px; + } + + /* Make steps section taller when it's the main content in directions view */ + .nav-directions-view .nav-steps { + max-height: none; + flex: 1; + overflow-y: auto; + } + + .nav-directions-view { + display: flex; + flex-direction: column; + } + /* Traffic-aware route info */ .nav-traffic-info { display: flex; @@ -801,21 +869,27 @@ export const navigationStyles = css` * Header toolbar styles for toolbar above map */ export const headerToolbarStyles = css` + /* CSS Grid layout for sidebars that push the map */ .geo-component { - display: flex; - flex-direction: column; + display: grid; + grid-template-columns: var(--left-panel-width, 0px) 1fr var(--right-panel-width, 0px); + grid-template-rows: auto 1fr; + grid-template-areas: + "header header header" + "left-panel map-area right-panel"; height: 100%; width: 100%; + transition: grid-template-columns 0.25s ease; } .header-toolbar { + grid-area: header; display: flex; align-items: center; gap: 12px; padding: 8px 12px; background: var(--geo-toolbar-bg, rgba(30, 30, 30, 0.95)); border-bottom: 1px solid var(--geo-toolbar-border, rgba(255, 255, 255, 0.1)); - flex-shrink: 0; min-height: 52px; position: relative; z-index: 10; @@ -824,7 +898,7 @@ export const headerToolbarStyles = css` .toolbar-left { display: flex; align-items: center; - gap: 4px; + gap: 8px; flex-shrink: 0; } @@ -890,11 +964,196 @@ export const headerToolbarStyles = css` max-width: 300px; } - /* Map container takes remaining space */ + /* Left Sidebar - Navigation Panel */ + .left-sidebar { + grid-area: left-panel; + background: var(--geo-toolbar-bg, rgba(30, 30, 30, 0.95)); + border-right: 1px solid var(--geo-toolbar-border, rgba(255, 255, 255, 0.1)); + overflow: hidden; + display: flex; + flex-direction: column; + min-width: 0; + } + + .left-sidebar.collapsed { + border-right: none; + } + + .left-sidebar .navigation-panel { + width: 300px; + min-width: 300px; + border: none; + border-radius: 0; + background: transparent; + backdrop-filter: none; + -webkit-backdrop-filter: none; + height: 100%; + overflow-y: auto; + } + + /* Right Sidebar - Draw Panel */ + .right-sidebar { + grid-area: right-panel; + background: var(--geo-toolbar-bg, rgba(30, 30, 30, 0.95)); + border-left: 1px solid var(--geo-toolbar-border, rgba(255, 255, 255, 0.1)); + overflow: hidden; + display: flex; + flex-direction: column; + min-width: 0; + } + + .right-sidebar.collapsed { + border-left: none; + } + + /* Draw Panel Styles */ + .draw-panel { + width: 180px; + min-width: 180px; + display: flex; + flex-direction: column; + height: 100%; + overflow-y: auto; + } + + .draw-panel-header { + display: flex; + align-items: center; + gap: 8px; + padding: 12px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + } + + .draw-panel-header-icon { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + color: var(--geo-tool-active, #0084ff); + } + + .draw-panel-header-icon svg { + width: 18px; + height: 18px; + } + + .draw-panel-header-title { + font-size: 13px; + font-weight: 600; + color: var(--geo-text, #fff); + } + + .draw-tools-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 8px; + padding: 12px; + } + + .draw-tool-button { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 6px; + padding: 12px 8px; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + background: transparent; + color: var(--geo-text, #fff); + cursor: pointer; + transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease; + } + + .draw-tool-button:hover { + background: var(--geo-tool-hover, rgba(255, 255, 255, 0.1)); + border-color: rgba(255, 255, 255, 0.2); + } + + .draw-tool-button.active { + background: var(--geo-tool-active, #0084ff); + border-color: var(--geo-tool-active, #0084ff); + color: #fff; + } + + .draw-tool-button:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + .draw-tool-button svg { + width: 22px; + height: 22px; + } + + .draw-tool-button-label { + font-size: 11px; + font-weight: 500; + white-space: nowrap; + } + + .draw-panel-divider { + height: 1px; + margin: 4px 12px; + background: rgba(255, 255, 255, 0.1); + } + + .draw-panel-actions { + display: flex; + flex-direction: column; + gap: 8px; + padding: 12px; + } + + .draw-action-button { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 10px 12px; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + background: transparent; + color: var(--geo-text, #fff); + cursor: pointer; + font-size: 12px; + font-weight: 500; + transition: background-color 0.15s ease, border-color 0.15s ease; + } + + .draw-action-button:hover { + background: var(--geo-tool-hover, rgba(255, 255, 255, 0.1)); + border-color: rgba(255, 255, 255, 0.2); + } + + .draw-action-button.active { + background: var(--geo-tool-active, #0084ff); + border-color: var(--geo-tool-active, #0084ff); + } + + .draw-action-button:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + .draw-action-button svg { + width: 16px; + height: 16px; + } + + .draw-action-button.danger:hover { + background: rgba(239, 68, 68, 0.2); + border-color: rgba(239, 68, 68, 0.4); + color: #fca5a5; + } + + /* Map container takes grid area */ .geo-component .map-container { - flex: 1; + grid-area: map-area; position: relative; min-height: 0; + min-width: 0; } `; diff --git a/ts_web/elements/00group-map/dees-geo-map/dees-geo-map.ts b/ts_web/elements/00group-map/dees-geo-map/dees-geo-map.ts index 7692430..69500fd 100644 --- a/ts_web/elements/00group-map/dees-geo-map/dees-geo-map.ts +++ b/ts_web/elements/00group-map/dees-geo-map/dees-geo-map.ts @@ -141,6 +141,9 @@ export class DeesGeoMap extends DeesElement { @state() private accessor isNavigationOpen: boolean = true; + @state() + private accessor isDrawPanelOpen: boolean = true; + // Controllers private searchController: SearchController | null = null; private navigationController: NavigationController | null = null; @@ -836,6 +839,10 @@ export class DeesGeoMap extends DeesElement { this.isNavigationOpen = !this.isNavigationOpen; } + private toggleDrawPanel(): void { + this.isDrawPanelOpen = !this.isDrawPanelOpen; + } + // ─── Render ───────────────────────────────────────────────────────────────── public render(): TemplateResult { @@ -843,24 +850,31 @@ export class DeesGeoMap extends DeesElement { const hasTrafficProvider = this.trafficController?.provider?.isConfigured ?? false; const showTrafficControls = Boolean(hasTrafficProvider || this.trafficApiKey || this.trafficProvider); + // Calculate panel widths for CSS Grid + const leftPanelWidth = this.showNavigation && this.isNavigationOpen ? '300px' : '0px'; + const rightPanelWidth = this.showToolbar && this.isDrawPanelOpen ? '180px' : '0px'; + return html` -
+
${this.renderHeaderToolbar(showTrafficControls)} + + +
this.handleMapContextMenu(e)}>
- -
- ${this.showNavigation && this.isNavigationOpen && this.navigationController - ? this.navigationController.render() - : ''} -
+ +
- +
@@ -873,10 +887,15 @@ export class DeesGeoMap extends DeesElement { ` : ''}
- +
+ + +
`; } @@ -884,11 +903,16 @@ export class DeesGeoMap extends DeesElement { private renderHeaderToolbar(showTrafficControls: boolean): TemplateResult { return html`
- +
- ${this.showToolbar ? html` - ${this.renderDrawTools()} -
+ ${this.showNavigation ? html` + ` : ''}
@@ -899,15 +923,15 @@ export class DeesGeoMap extends DeesElement { : ''}
- +
- ${this.showNavigation ? html` + ${this.showToolbar ? html` ` : ''} ${showTrafficControls && this.trafficController @@ -933,7 +957,7 @@ export class DeesGeoMap extends DeesElement { `; } - private renderDrawTools(): TemplateResult { + private renderDrawPanel(): TemplateResult { const tools: { id: TDrawTool; icon: string; label: string }[] = [ { id: 'point', icon: 'point', label: 'Point' }, { id: 'linestring', icon: 'line', label: 'Line' }, @@ -944,33 +968,51 @@ export class DeesGeoMap extends DeesElement { ]; return html` - ${tools.map(tool => html` - - `)} -
- - +
+
+
+ ${renderIcon('polygon')} +
+
Draw Tools
+
+ +
+ ${tools.map(tool => html` + + `)} +
+ +
+ +
+ + +
+
`; } diff --git a/ts_web/elements/00group-map/dees-geo-map/geo-map.icons.ts b/ts_web/elements/00group-map/dees-geo-map/geo-map.icons.ts index 48e25fe..3f79bf1 100644 --- a/ts_web/elements/00group-map/dees-geo-map/geo-map.icons.ts +++ b/ts_web/elements/00group-map/dees-geo-map/geo-map.icons.ts @@ -37,6 +37,9 @@ export const GEO_MAP_ICONS: Record = { ruler: html``, error: html``, + // Arrows + arrowLeft: html``, + // Traffic traffic: html``, trafficLight: html``, diff --git a/ts_web/elements/00group-map/dees-geo-map/geo-map.navigation.ts b/ts_web/elements/00group-map/dees-geo-map/geo-map.navigation.ts index 6785214..f1c87ec 100644 --- a/ts_web/elements/00group-map/dees-geo-map/geo-map.navigation.ts +++ b/ts_web/elements/00group-map/dees-geo-map/geo-map.navigation.ts @@ -96,6 +96,9 @@ export class NavigationController { // Mode public navigationMode: TNavigationMode = 'driving'; + // View mode: 'planning' for route input, 'directions' for turn-by-turn + public viewMode: 'planning' | 'directions' = 'planning'; + // Internal private callbacks: INavigationControllerCallbacks; private navSearchDebounceTimer: ReturnType | null = null; @@ -198,6 +201,9 @@ export class NavigationController { // Fit map to route bounds this.fitToRoute(routeToRender); + + // Switch to directions view after successful route calculation + this.viewMode = 'directions'; } } catch (error) { this.navigationState = { @@ -272,6 +278,7 @@ export class NavigationController { this.navStartSearchResults = []; this.navEndSearchResults = []; this.navClickMode = null; + this.viewMode = 'planning'; // Remove markers if (this.startMarker) { @@ -516,6 +523,20 @@ export class NavigationController { map.fitBounds(bounds, { padding: 80 }); } + /** + * Fly to a specific navigation step location + */ + public flyToStep(step: IOSRMStep): void { + const map = this.callbacks.getMap(); + if (!map) return; + + map.flyTo({ + center: step.maneuver.location, + zoom: 17, + duration: 1000, + }); + } + // ─── Search within Navigation ─────────────────────────────────────────────── /** @@ -763,6 +784,14 @@ export class NavigationController { } } + /** + * Switch between planning and directions view + */ + public setViewMode(mode: 'planning' | 'directions'): void { + this.viewMode = mode; + this.callbacks.onRequestUpdate(); + } + // ─── Cleanup ──────────────────────────────────────────────────────────────── /** @@ -790,7 +819,18 @@ export class NavigationController { * @param extraClass - Optional CSS class to add to the panel for positioning */ public render(extraClass?: string): TemplateResult { - const { route, isLoading, error, startPoint, endPoint } = this.navigationState; + // If we have a route and we're in directions view, show that + if (this.viewMode === 'directions' && this.navigationState.route) { + return this.renderDirectionsView(extraClass); + } + return this.renderPlanningView(extraClass); + } + + /** + * Render the route planning view (inputs, mode selector, actions) + */ + private renderPlanningView(extraClass?: string): TemplateResult { + const { isLoading, error, startPoint, endPoint } = this.navigationState; const canCalculate = startPoint && endPoint && !isLoading; return html` @@ -858,35 +898,55 @@ export class NavigationController { Calculating route...
` : ''} + + `; + } - ${route && !isLoading ? html` -