WDM Visual Component Library - Style Guide¶
1. Overview¶
The WDM component library provides shared UI primitives for WDM visualization views: wavelength editors, channel planners, and service path traces. It is not a full design system - it covers the specific patterns that appear across WDM canvases and panels.
Files:
- CSS: netbox_wdm/static/netbox_wdm/css/wdm-components.css
- TypeScript: netbox_wdm/static/netbox_wdm/src/
- Built output: netbox_wdm/static/netbox_wdm/dist/ (via npm run build)
2. Getting Started¶
Add the wdm-component class to any wrapper element. This activates all CSS custom properties.
Load the stylesheet in your template:
The component root reads data-bs-theme from a parent element to switch between dark and light palettes automatically - no extra JS needed.
3. Color Palette¶
All colors are defined as CSS custom properties on .wdm-component. Never use hex values directly - always reference these variables.
| Variable | Purpose |
|---|---|
--wdm-bg |
Canvas / panel background |
--wdm-bg-card |
Card / input surface (slightly lighter than bg) |
--wdm-border |
Borders, dividers, separator lines |
--wdm-text |
Primary body text |
--wdm-text-muted |
Secondary / hint text |
--wdm-text-label |
Row label text (left side of detail rows) |
--wdm-link |
Interactive links and active pill highlight |
--wdm-live |
Live / active status (green) |
--wdm-planned |
Planned status (amber) |
--wdm-pending |
Pending / in-progress status (orange) |
--wdm-danger |
Error / protected / destructive (red) |
--wdm-muted |
Disabled / draft state (grey) |
Light mode overrides are applied via [data-bs-theme="light"] .wdm-component.
4. Badge (.wdm-badge)¶
Use badges to show status inline with text. They are small (9px, uppercase) and meant for tight spaces.
Available variants:
| Class | Color | When to use |
|---|---|---|
.wdm-badge--lit |
Green | Channel is carrying traffic |
.wdm-badge--active |
Green | Service is active (alias for lit) |
.wdm-badge--planned |
Amber | Service planned but not provisioned |
.wdm-badge--reserved |
Orange | Channel reserved for future use |
.wdm-badge--available |
Grey | Channel available for assignment |
.wdm-badge--protected |
Red | Channel or service is protected; do not touch |
<span class="wdm-badge wdm-badge--lit">Lit</span>
<span class="wdm-badge wdm-badge--reserved">Reserved</span>
<span class="wdm-badge wdm-badge--protected">Protected</span>
Do not use badges for counts or non-status information - use plain text for those.
5. Legend (.wdm-legend)¶
The legend is an absolutely-positioned overlay anchored to the bottom-left of the canvas container. It collapses downward - the bottom edge stays fixed and the content shrinks into a small pill-shaped bar at the bottom corner.
Rules:
- Only include items that are currently visible on the canvas. If a filter hides all reserved channels, remove the "Reserved" entry from the legend. Rebuild on every render.
- Group related items under wdm-legend__section with a section title.
- Section ordering convention: status items first (lit, reserved, available), then structural items (grid positions, wavelength bands), then interaction hints (selected, hover).
<div class="wdm-component">
<div class="wdm-legend" id="my-legend">
<div class="wdm-legend__header">
<span class="wdm-legend__title">Legend</span>
<button class="wdm-legend__toggle" aria-label="Toggle legend">▼</button>
</div>
<div class="wdm-legend__body">
<div class="wdm-legend__section">
<div class="wdm-legend__section-title">Status</div>
<div class="wdm-legend__item">
<span class="wdm-legend__dot" style="background:#00cc66"></span>
Lit
</div>
<div class="wdm-legend__item">
<span class="wdm-legend__dot" style="background:#ffaa00"></span>
Reserved
</div>
</div>
</div>
</div>
</div>
The data-collapsed attribute is managed by JS. When collapsed, the legend shrinks to a 24px pill showing "Legend ▲". When expanded, content grows upward from the bottom edge, toggle shows "▼".
6. Detail Panel (.wdm-detail-panel)¶
A slide-in panel anchored to the right edge of the canvas container. On mobile it becomes a bottom sheet.
Structure:
<div class="wdm-detail-panel" id="detail-panel" role="complementary" aria-label="Details">
<div class="wdm-detail-panel__header">
<span class="wdm-detail-panel__title">Channel C32</span>
<button class="wdm-detail-panel__close" aria-label="Close panel">×</button>
</div>
<div class="wdm-detail-panel__body">
<!-- Card with rows -->
<div class="wdm-detail-card">
<div class="wdm-detail-card__heading">Wavelength</div>
<!-- Text row -->
<div class="wdm-detail-card__row">
<span class="wdm-detail-card__label">Grid position</span>
<span class="wdm-detail-card__value">32</span>
</div>
<!-- Link row -->
<div class="wdm-detail-card__row">
<span class="wdm-detail-card__label">Service</span>
<a class="wdm-link wdm-detail-card__value" href="/services/12/">SVC-0012</a>
</div>
<!-- Badge row -->
<div class="wdm-detail-card__row">
<span class="wdm-detail-card__label">Status</span>
<span class="wdm-badge wdm-badge--lit">Lit</span>
</div>
</div>
</div>
</div>
Escape key and close button should be wired by the constructor.
7. Toolbar (.wdm-toolbar)¶
The toolbar sits above the canvas, below any page heading. It wraps at narrow widths.
Structure rules:
1. Start with pill groups (exclusive mode selectors).
2. Add a separator between groups of unrelated controls.
3. Place wdm-toolbar__spacer to push right-side controls to the far right.
4. Search input goes last on the right.
<div class="wdm-toolbar wdm-component">
<!-- Exclusive mode group (only one active at a time) -->
<div class="wdm-pill-group" role="group" aria-label="View mode">
<button class="wdm-pill wdm-pill--active" aria-pressed="true">Grid</button>
<button class="wdm-pill" aria-pressed="false">Spectrum</button>
</div>
<!-- Separator -->
<div class="wdm-separator" role="separator"></div>
<!-- Independent filter toggles (any combination allowed) -->
<div class="wdm-pill-filter" role="group" aria-label="Filters">
<button class="wdm-pill wdm-pill--on" style="--pill-color:#00cc66" aria-pressed="true">Lit</button>
<button class="wdm-pill wdm-pill--off" style="--pill-color:#ffaa00" aria-pressed="false">Reserved</button>
</div>
<!-- Spacer pushes the following to the right -->
<div class="wdm-toolbar__spacer"></div>
<!-- Search -->
<input class="wdm-search" type="search" placeholder="Search…" aria-label="Search channels">
</div>
Pill group (exclusive): Only one pill has wdm-pill--active. Clicking another deactivates the current one. Use aria-pressed to reflect state.
Pill filter (independent): Each pill toggles on/off independently. Use wdm-pill--on / wdm-pill--off modifier and set --pill-color to the associated status color. Use aria-pressed on each button.
8. Stats Bar (.wdm-stats-bar)¶
A slim (28px) bar pinned to the bottom of the canvas. Surfaces aggregate counts.
Stat ordering: Essential counts left (total channels, lit count), secondary/contextual counts right.
Mark the most important stats with wdm-stat--essential - these remain visible on mobile when non-essential stats are hidden.
<div class="wdm-stats-bar wdm-component">
<div class="wdm-stats-bar__left">
<span class="wdm-stat wdm-stat--essential">
<span class="wdm-stat__label">Channels:</span> 44
</span>
<span class="wdm-stats-bar__dot" aria-hidden="true">·</span>
<span class="wdm-stat wdm-stat--lit">
<span class="wdm-stat__label">Lit:</span> 28
</span>
<span class="wdm-stats-bar__dot" aria-hidden="true">·</span>
<span class="wdm-stat wdm-stat--reserved">
<span class="wdm-stat__label">Reserved:</span> 4
</span>
</div>
<div class="wdm-stats-bar__right">
<span id="status-msg"></span>
</div>
</div>
Messages: Use setMessage() for status updates - they appear on the right side and auto-clear. The left side (counts) is never replaced. Do not use alerts or toasts for minor status updates from canvas actions.
9. Theme Integration¶
The library automatically responds to Bootstrap's data-bs-theme attribute. No extra work is needed if your page already sets this.
Rules:
- Always use --wdm-* variables for colors inside .wdm-component.
- Never hardcode hex values for backgrounds, text, or borders.
- For SVG fill and stroke, use Bootstrap variables (var(--bs-body-color), var(--bs-primary)) since SVG elements live inside the canvas, not inside .wdm-component.
/* Correct */
.my-element {
color: var(--wdm-text);
border-color: var(--wdm-border);
}
/* Wrong */
.my-element {
color: #ccc;
border-color: #444;
}
10. Responsive Design¶
Breakpoints¶
| Breakpoint | Width | Behavior |
|---|---|---|
| Desktop | > 991px | Full layout, panel slides in from right |
| Tablet | ≤ 991px | Panel overlays canvas (absolute, full height, shadow) |
| Mobile | ≤ 767px | Panel becomes bottom sheet (50vh max), toolbar compacts, legend hidden, non-essential stats hidden |
Component behavior at each breakpoint¶
Detail panel:
- Desktop: side-by-side with canvas, no shadow.
- Tablet: overlays canvas, box-shadow: -4px 0 16px rgba(0,0,0,0.3).
- Mobile: fixed bottom sheet, slides up from bottom, swipe-down to dismiss.
Toolbar:
- Desktop: single row, full labels.
- Mobile: smaller padding and font (10px), search wraps to its own row (order: 99).
Stats bar:
- Mobile: only wdm-stat--essential stats are visible. Mark secondary stats without this class.
Legend:
- Mobile: hidden entirely (display: none). Canvas space is too limited.
11. Touch & Accessibility¶
Touch targets¶
All interactive elements must have a minimum tap target of 44×44px, even if the visual element is smaller. Use padding to expand the hit area without changing the visual size.
Swipe-to-dismiss (mobile bottom sheet)¶
Wire touchstart / touchmove / touchend on the panel header. If the user drags down more than 60px, close the panel.
Focus states¶
Every interactive element must have a visible focus ring. The library provides :focus-visible styles on .wdm-pill, .wdm-link, .wdm-legend__toggle, and .wdm-detail-panel__close. Do not override these with outline: none.
ARIA labels¶
| Element | Required attribute |
|---|---|
.wdm-detail-panel |
role="complementary", aria-label="Details" |
.wdm-detail-panel__close |
aria-label="Close panel" |
.wdm-legend__toggle |
aria-label="Toggle legend" |
| Pill group | role="group", aria-label="<group name>" |
| Each pill | aria-pressed="true\|false" |
.wdm-separator |
role="separator" |
| Dot separators in stats bar | aria-hidden="true" |
Keyboard navigation¶
- Escape - close the open detail panel.
- Tab - move through toolbar controls, close button, and panel rows in DOM order.
- Pill groups should support Left/Right arrow keys to move between pills in the group (implement with a
keydownhandler; the CSS does not do this automatically).
12. TypeScript Component Structure¶
All components live in netbox_wdm/static/netbox_wdm/src/:
| File | Export | Purpose |
|---|---|---|
wavelength-editor-types.ts |
ChannelData, PortData, EditorConfig |
Type definitions for the wavelength editor |
wavelength-editor.ts |
WavelengthEditor class |
ROADM channel assignment editor |
channel-trace-types.ts |
TraceData, PathElement, CableSegment, etc. |
Type definitions for trace visualizations |
channel-trace.ts |
renderChannelTrace |
Single-channel vertical trace diagram |
circuit-trace.ts |
renderCircuitTrace |
Multi-path horizontal circuit trace (NetBox-style) |
Each component follows the pattern - one file per component, types in a separate -types.ts file.
Import via the entry point or barrel:
import type { ChannelData, EditorConfig } from './wavelength-editor-types';
import type { TraceData, CableSegment } from './channel-trace-types';
13. Debug Mode¶
When Django runs with DEBUG=True, templates should pass debug: true in the editor config. All editor JavaScript should use a dbg() helper that logs to console.log with a [WDM] prefix only when debug is enabled.
function dbg(...args: unknown[]): void {
if ((window as any).WAVELENGTH_EDITOR_CONFIG?.debug) {
console.log('[WDM]', ...args);
}
}
dbg('loadData() response:', { channels: response.channels.length });
// Outputs: [WDM] loadData() response: {channels: 44}
Debug logging covers: config load, DOM element discovery, API fetch/response, state after load, and errors. In production (DEBUG=False), no console output is produced.