Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Document viewer — layout scaffolding

Structural scaffolding for a panel-based document viewing desktop app: every shell area is present as a labelled placeholder with its real behaviour (resize, collapse, overflow, scroll) already implemented. No document engine, no business logic — the frame the components will later be dropped into.

Plain HTML/CSS/JS, no build step, no dependencies.

python3 -m http.server 8777      # then open http://localhost:8777/index.html

Everything was verified over http://. Opening index.html straight off disk is built for as well — classic scripts rather than ES modules, an inline SVG sprite, and BroadcastChannel/localStorage behind try/catch so the postMessage transport carries the cross-window sync on its own — but that path is untested. Serve over HTTP if you want the tested configuration.

The two windows

File Purpose
index.html the viewer shell
control.html area on/off switches, theme switcher, live geometry readout

Open the control window from the viewer's header (the right-hand button, next to the branding) or from the application menu → Layout controls…. It can also be opened on its own; it will find the viewer either way.

The two windows share one state object over three transports at once — BroadcastChannel, postMessage between opener/child, and localStorage — so they stay in sync over http:// and file:// alike, and the layout is remembered between sessions.

Layout

Taken from the Figma Scaffolding Layout frame, including its deliberate asymmetries: the header spans the full width, the left rail and left sidebar are full-height columns (so the main toolbar starts to their right), and the status bar spans only the thumbnail + canvas columns.

┌──────────────────────────────────────────────────────────────┐
│ header                                        full width 32  │
├────┬─────────────┬───────────────────────────────────────────┤
│    │ infobox     │ main toolbar                          48  │
│rail│ ─────────── ├──────────┬────────────────┬──────┬───────┤
│ L  │ tools       │ thumbs   │ canvas         │ side │ rail  │
│ 48 │ ─────────── │ 180      │ (flex)         │ bar  │  R    │
│    │ sidebar L   │          │                │ R    │  48   │
│    │ 240         │          │                │ 240  │       │
│    │             ├──────────┴────────────────┤      │       │
│    │             │ status bar            24   │      │       │
└────┴─────────────┴───────────────────────────┴──────┴───────┘

All dimensions are multiples of 4px.

Area Size Scrolling Overflow
Header 32px, full width never ⋯ menu
Main toolbar 48px never ⋯ menu
Rails (left/right) 48px never ⋯ menu
Left / right sidebar 240px, drag 160–480 vertical only ⋯ menu in the tool strip
Thumbnail view 180px, drag 120–400 vertical only —
Viewer canvas fills, min 320px both axes —
Status bar 24px never —

Scrollbars are custom overlay bars: they float over the content, fade out ~1s after the last scroll, and take no layout width — a panel is exactly as wide whether or not its content overflows.

Resizing and collapsing

Splitters are 4px wide with a 12px pointer target.

  • Drag to resize.
  • Drag past the minimum to collapse. The splitter stays behind as an 8px handle with a chevron.
  • Drag the handle outwards to re-open.
  • Double-click to toggle.
  • Rail tabs switch the sidebar's content; clicking the active tab collapses the sidebar (the Acrobat / VS Code convention).
  • The main toolbar has toggles for the thumbnail view and the right sidebar.

Keyboard, with focus on a splitter (role="separator", full aria-value*):

Key Action
← / → resize by 8px
Shift + arrows resize by 32px
Home collapse
End expand as far as fits
Enter / Space toggle collapse

Responsive behaviour

One solver in js/resize.js owns every horizontal dimension. When the window gets too narrow it first shrinks panels to their minimum, then collapses them, both in the specified priority order:

right sidebar → left sidebar → thumbnail view

The canvas always keeps its 320px minimum. Because the solver derives everything from scratch on each pass — only the user's desired widths and manual collapses are stored — panels come back on their own when the window grows again, with no undo bookkeeping.

Switching an area off in the control window is the same code path: the area gets hidden, its splitter with it, and the flex rows close the gap, so nothing empty is ever left behind. Special cases are handled too — turning the canvas off lets the thumbnail column take over the freed width rather than leaving a hole.

Themes

Three themes, switched from the control window or the application menu.

Theme Chrome Panels Canvas Accent
Dark (default) #30363F #434952 #595F67 #3170EB
Light #E7E9EB #F7F9FB #808080 #264D97
Test one distinct hue per area, plus name badges on every region

The palette is derived from the supplied sources rather than invented:

  • css/tokens.css carries the --nfwc-* primitives verbatim from the MoCla2 / Fusion OO colour-token Figma file.
  • The dark mapping matches the JWT-NG predecessor pixel for pixel (sampled from the Figma render: chrome #30363F, panels #434952, selection #3170EB).
  • The light mapping matches the live jadice web viewer, which paints its header in exactly --nfwc-primary-background #E7E9EB — confirming the token file is the same one the app uses.
  • Type is Source Sans 3 / Source Sans Pro, the face the live viewer uses, with a system fallback if the webfont is unavailable.

A semantic layer (--bg-*, --text-*, --border-*) sits on top of the primitives; components only ever consume the semantic names, so re-theming means editing one block.

Accessibility

Targeting WCAG 2.1 level AA. What was actually verified in the browser, in all three themes:

  • Text contrast — every rendered text node audited against its composited background: 154 nodes (166 in the test theme), all ≥ 4.5:1, zero failures.
  • Non-text contrast (1.4.11) — 23 checks per theme covering the focus ring, scrollbar thumbs, control borders and the active-tab indicator against every surface they can land on (chrome, panels, canvas, popovers, the accent-filled selected state, the white document sheet). All pass.

Two findings from that audit shaped the design:

  • A single-colour focus ring cannot pass everywhere — brand blue on a brand-blue selected control measured 1.00:1. The ring is therefore two-tone (a light band against a dark band, 21:1 with each other), drawn inside the control so the chrome's overflow: hidden cannot clip it.
  • A translucent scrollbar thumb cannot pass over both a white document page and an accent-filled row, so the thumb carries a hairline of the opposite luminance. Whichever side loses contrast, the other keeps it.

Also implemented:

  • Landmarks (banner, navigation, complementary, main, status), a skip link, and aria-labelledby on every scrollable region.
  • Rails are real tablists; toolbars are real toolbars, both with roving tabindex and Arrow/Home/End navigation (ARIA APG).
  • Splitters are role="separator" with live aria-valuenow / aria-valuetext ("Left sidebar collapsed" / "…248 pixels wide").
  • Menus are role="menu" with arrow-key navigation, Escape to close, and focus returned to the trigger.
  • Collapse/expand and panel switches are announced through a polite live region.
  • Overlay scrollbars are pointer sugar only — the scroll containers keep native keyboard scrolling and are focusable.
  • prefers-reduced-motion and forced-colors (Windows High Contrast) honoured.

Not covered: no screen-reader pass with an actual AT, and no automated axe/Lighthouse run — the contrast and ARIA claims above come from the in-page audit and from reading the markup, not from a third-party tool.

Files

index.html            the shell
control.html          the second window
css/tokens.css        primitives, semantic tokens, metrics, the three themes
css/base.css          reset, focus ring, overlay-scrollbar chrome, a11y utils
css/layout.css        shell geometry, splitters, collapse/disable rules
css/components.css    toolbars, rails, panels, slots, menus, toasts, canvas
css/control.css       the control window
js/config.js          area registry + panel limits — the single source of truth
js/bus.js             cross-window state (BroadcastChannel + postMessage + storage)
js/resize.js          the layout solver, drag/keyboard resize, responsive cascade
js/overflow.js        popup menus and the ⋯ overflow controllers
js/overlay-scroll.js  auto-hiding overlay scrollbars
js/app.js             wiring for the shell
js/control.js         wiring for the control window

Adding an area

  1. Add an entry to AREAS in js/config.js (id, group, label, behaviour note).
  2. Put data-area="<id>" on the matching element in index.html.

The control window's checkbox, the show/hide plumbing and the parent/child blocking all follow from that one entry.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages