Adjustable divider #35

Closed
opened 2026-03-27 23:07:40 +00:00 by maxtkc · 0 comments
Owner

Summary

Add a continuous drag-to-resize handle between the map and the right sidebar. A thin draggable bar sits on the left edge of the right panel; users can drag it to resize freely between 300px and 900px. Width is not persisted — it resets to the 650px default on page reload. The implementation swaps the hardcoded 650px grid column for a CSS custom property (--panel-width) and introduces a small new PanelResizer module (~50 lines) that owns all mouse event handling.

Relevant Context

  • src/index.html:38.app-container has grid-cols-[1fr_650px]; that class gets removed in favour of a CSS rule using --panel-width
  • src/index.html:383.right-panel div; needs relative added; resizer handle is its first child
  • src/styles/main.css — add one rule: .app-container { grid-template-columns: 1fr var(--panel-width, 650px); }
  • src/modules/map-controller.ts:623forceMapResize() debounces 350ms (designed for panel toggle); we'll also add a resizeNow() method for calling during live drag
  • src/index.ts — GTFSEditor constructor; new module instantiated here (no class property needed)

Phase 1: CSS Foundation

Replace the hardcoded 650px grid column with a CSS variable. The variable default preserves the current layout exactly — no visible change until JS touches it.

  • In src/styles/main.css, add:
    .app-container {
      grid-template-columns: 1fr var(--panel-width, 650px);
    }
    
  • In src/index.html:38, remove grid-cols-[1fr_650px] from .app-container (the CSS rule above takes over)
  • In src/index.html:383, add relative to .right-panel class list (required for the absolutely-positioned resizer child)

No gotchas — the CSS variable default (650px) is identical to the removed Tailwind class.

Phase 2: Resizer Element + Module + Wiring

Add the drag handle to HTML, implement PanelResizer, expose resizeNow() on MapController, and wire everything into GTFSEditor.

  • In src/index.html, add resizer div as the first child of .right-panel:

    <div id="panel-resizer" class="absolute left-0 top-0 bottom-0 w-2 cursor-col-resize z-10 group">
      <div class="absolute inset-y-0 left-0.5 w-px bg-base-300 group-hover:bg-primary/50 transition-colors"></div>
    </div>
    

    The 8px hit area (w-2) overlaps the existing border-l visually; the inner w-px div provides the visible highlight on hover.

  • Add public resizeNow(): void { this.map?.resize(); } to MapController (after the forceMapResize method is fine)

  • Create src/modules/panel-resizer.ts:

    import { MapController } from './map-controller';
    
    const MIN_WIDTH = 300;
    const MAX_WIDTH = 900;
    const DEFAULT_WIDTH = 650;
    
    export class PanelResizer {
      constructor(appContainer: HTMLElement, mapController: MapController) {
        const resizer = document.getElementById('panel-resizer');
        if (!resizer) return;
    
        resizer.addEventListener('mousedown', (e) => {
          e.preventDefault();
          const startX = e.clientX;
          const startWidth =
            parseInt(
              getComputedStyle(appContainer).getPropertyValue('--panel-width')
            ) || DEFAULT_WIDTH;
    
          document.body.style.userSelect = 'none';
          document.body.style.cursor = 'col-resize';
    
          const onMouseMove = (e: MouseEvent) => {
            const newWidth = Math.min(
              MAX_WIDTH,
              Math.max(MIN_WIDTH, startWidth + startX - e.clientX)
            );
            appContainer.style.setProperty('--panel-width', `${newWidth}px`);
            mapController.resizeNow();
          };
    
          const onMouseUp = () => {
            document.body.style.userSelect = '';
            document.body.style.cursor = '';
            mapController.forceMapResize();
            document.removeEventListener('mousemove', onMouseMove);
            document.removeEventListener('mouseup', onMouseUp);
          };
    
          document.addEventListener('mousemove', onMouseMove);
          document.addEventListener('mouseup', onMouseUp);
        });
      }
    }
    
  • In src/index.ts, import and instantiate PanelResizer in the GTFSEditor constructor after mapController is set up:

    import { PanelResizer } from './modules/panel-resizer';
    // ...
    const appContainer = document.querySelector<HTMLElement>('.app-container')!;
    new PanelResizer(appContainer, this.mapController);
    

    No need to store as a class property — the module registers its own listeners.

Gotchas:

  • document.body.style.userSelect = 'none' during drag prevents the browser from selecting text across the rest of the UI as the mouse moves
  • mapController.resizeNow() calls map.resize() directly on every mousemove — this is safe for Mapbox GL JS and gives a live update of map tiles as the panel resizes
  • getComputedStyle(...).getPropertyValue('--panel-width') returns an empty string if unset (first drag), so the || DEFAULT_WIDTH fallback is needed
  • The overflow-hidden on .right-panel clips children, but since the resizer uses absolute positioning it works fine within the panel bounds — it just won't bleed outside

Original Issue

It should be possible to expand or collapse the sidebar and map. Should it be discrete steps or continuous?

## Summary Add a continuous drag-to-resize handle between the map and the right sidebar. A thin draggable bar sits on the left edge of the right panel; users can drag it to resize freely between 300px and 900px. Width is not persisted — it resets to the 650px default on page reload. The implementation swaps the hardcoded `650px` grid column for a CSS custom property (`--panel-width`) and introduces a small new `PanelResizer` module (~50 lines) that owns all mouse event handling. ## Relevant Context - `src/index.html:38` — `.app-container` has `grid-cols-[1fr_650px]`; that class gets removed in favour of a CSS rule using `--panel-width` - `src/index.html:383` — `.right-panel` div; needs `relative` added; resizer handle is its first child - `src/styles/main.css` — add one rule: `.app-container { grid-template-columns: 1fr var(--panel-width, 650px); }` - `src/modules/map-controller.ts:623` — `forceMapResize()` debounces 350ms (designed for panel toggle); we'll also add a `resizeNow()` method for calling during live drag - `src/index.ts` — GTFSEditor constructor; new module instantiated here (no class property needed) ## Phase 1: CSS Foundation Replace the hardcoded 650px grid column with a CSS variable. The variable default preserves the current layout exactly — no visible change until JS touches it. - [x] In `src/styles/main.css`, add: ```css .app-container { grid-template-columns: 1fr var(--panel-width, 650px); } ``` - [x] In `src/index.html:38`, remove `grid-cols-[1fr_650px]` from `.app-container` (the CSS rule above takes over) - [x] In `src/index.html:383`, add `relative` to `.right-panel` class list (required for the absolutely-positioned resizer child) No gotchas — the CSS variable default (650px) is identical to the removed Tailwind class. ## Phase 2: Resizer Element + Module + Wiring Add the drag handle to HTML, implement `PanelResizer`, expose `resizeNow()` on `MapController`, and wire everything into `GTFSEditor`. - [x] In `src/index.html`, add resizer div as the **first child** of `.right-panel`: ```html <div id="panel-resizer" class="absolute left-0 top-0 bottom-0 w-2 cursor-col-resize z-10 group"> <div class="absolute inset-y-0 left-0.5 w-px bg-base-300 group-hover:bg-primary/50 transition-colors"></div> </div> ``` The 8px hit area (`w-2`) overlaps the existing `border-l` visually; the inner `w-px` div provides the visible highlight on hover. - [x] Add `public resizeNow(): void { this.map?.resize(); }` to `MapController` (after the `forceMapResize` method is fine) - [x] Create `src/modules/panel-resizer.ts`: ```ts import { MapController } from './map-controller'; const MIN_WIDTH = 300; const MAX_WIDTH = 900; const DEFAULT_WIDTH = 650; export class PanelResizer { constructor(appContainer: HTMLElement, mapController: MapController) { const resizer = document.getElementById('panel-resizer'); if (!resizer) return; resizer.addEventListener('mousedown', (e) => { e.preventDefault(); const startX = e.clientX; const startWidth = parseInt( getComputedStyle(appContainer).getPropertyValue('--panel-width') ) || DEFAULT_WIDTH; document.body.style.userSelect = 'none'; document.body.style.cursor = 'col-resize'; const onMouseMove = (e: MouseEvent) => { const newWidth = Math.min( MAX_WIDTH, Math.max(MIN_WIDTH, startWidth + startX - e.clientX) ); appContainer.style.setProperty('--panel-width', `${newWidth}px`); mapController.resizeNow(); }; const onMouseUp = () => { document.body.style.userSelect = ''; document.body.style.cursor = ''; mapController.forceMapResize(); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); }; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }); } } ``` - [x] In `src/index.ts`, import and instantiate `PanelResizer` in the `GTFSEditor` constructor after `mapController` is set up: ```ts import { PanelResizer } from './modules/panel-resizer'; // ... const appContainer = document.querySelector<HTMLElement>('.app-container')!; new PanelResizer(appContainer, this.mapController); ``` No need to store as a class property — the module registers its own listeners. **Gotchas:** - `document.body.style.userSelect = 'none'` during drag prevents the browser from selecting text across the rest of the UI as the mouse moves - `mapController.resizeNow()` calls `map.resize()` directly on every `mousemove` — this is safe for Mapbox GL JS and gives a live update of map tiles as the panel resizes - `getComputedStyle(...).getPropertyValue('--panel-width')` returns an empty string if unset (first drag), so the `|| DEFAULT_WIDTH` fallback is needed - The `overflow-hidden` on `.right-panel` clips children, but since the resizer uses `absolute` positioning it works fine within the panel bounds — it just won't bleed outside --- ## Original Issue It should be possible to expand or collapse the sidebar and map. Should it be discrete steps or continuous?
maxtkc self-assigned this 2026-03-27 23:07:40 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
gtfs.zone/coloring-book#35
No description provided.