Repository navigation
Releases: madnh/puredashboard
Release list
v0.5.0
Changed — BREAKING
-
The router no longer sets
aria-currenton links. Until v0.4.2, after every
navigationRouterscanned EVERYa[href]in the document and set
aria-current="page"on links whose path equalled the route exactly, removing it from
all others. That (a) dropped the parent section on a child route (#/scanslost it on
#/scans/5), and an app could not fix it inafterEachbecause the router ran after and
again in a microtask; (b) marked content links that happen to point at the current page
(a header status link, a link in a table); (c) fought<puredashboard-nav>/
<puredashboard-tabs>over the same attribute and could leave their current link
without it. Reported from ribeto-sync.Migrate — pick what renders your links:
<puredashboard-nav>: add thefollow-routeattribute and stop settingcurrentfrom
afterEach(or keep settingcurrentyourself; both work).<puredashboard-tabs>in link mode: addfollow-route, or keep settingvalue.- Plain
<a>links (e.g. the theme shell's<nav class="tabs">), which got
aria-currentfrom the router for free before:import { Router, markActive } from "LIB/router.js"; document.addEventListener("routechange", () => markActive(document.querySelector("nav.tabs"))); router.start(); // add the listener before start() so the first navigation is caught
- CSS that styles
[aria-current="page"]needs no change. - Code that relied on the exact-match rule:
markActive(container, { exact: true })or
follow-route="exact".
Added
routechangeevent ondocumentafter every committed navigation (guards passed,
modules loaded, before the page mounts).detail = { path, params, query, route, mode, base }.
A navigation cancelled or redirected by a guard publishes only its final route.src/active-link.js:currentRoute()(the last committed route, for code that starts
after a navigation) andmarkActive(container, { exact? }), which marks the best-matching
link INSIDEcontaineronly. Both re-exported byrouter.js. Matching is by path
segments: equal, or a segment prefix (#/scanson#/scans/5, never#/scansx);
longest match wins within the container; the root matches only itself; queries ignored;
history mode stripsbase.follow-routeon<puredashboard-nav>and<puredashboard-tabs>(link mode): the
component sets its owncurrent/valuefrom the router on connect, on every
routechange, and whenitems/tabschange;follow-route="exact"turns prefix
matching off. No match clears it (no item current). Survives detach + re-attach.
v0.4.2
Fixed
- A host
input/changelistener reads the new value. In input, textarea, number,
select, date, time, datetime, color, slider, checkbox and switch, a listener the app added
on the element BEFORE it connected ran ahead of the element's own value sync, so.value
(or.checked) was one edit behind. The sync now runs in the capture phase
(Reactive.on(type, sel, fn, { capture: true })). Reported from ribeto-sync. errorno longer blocks the form for good. Once set,errorstayed a custom validity
until the app cleared it, so a native<form>never firedsubmitagain and the app had
no event to clear it on. Input, textarea, number, select, date, time, datetime, checkbox,
switch, radio-group and combobox now clearerrorwhen the user changes the value, before
the app'sinput/changehandler runs (which may set it again).theme/shell.css: cards in a grid/flex line up..card + .cardadded a top margin to
every later card, on top of the container'sgap. The rule is now.page > .card + .card
(cards stacked directly in the page). Cards stacked inside another wrapper no longer get
that margin; space them with the wrapper'sgap.
[0.4.1] - 2026-10-22 - Re-attach fixes, json-view follows app theme
Fixed
- Detach + re-attach no longer leaves an element dead.
<puredashboard-popconfirm>and
<puredashboard-popover>stopped opening on a trigger click,<puredashboard-form>let the
browser submit natively (navigation, nosubmitevent),<puredashboard-splitter>gutters
stopped resizing (pointer and keyboard), and<puredashboard-gallery>lost its/
shortcut, once the element was removed and inserted again (a parent re-rendering by
replacing children).disconnectedCallbackremoved the listeners; the run-once wiring never
added them back.connectedCallbacknow re-adds them. <puredashboard-json-view theme="auto">follows the app theme. It read only
prefers-color-scheme, so a page forced to<html data-theme="light">on a dark OS (or the
reverse) showed a tree in the opposite mode to everything around it.autonow uses
<html data-theme="light|dark">when set, else the OS, and tracks both live.
[0.4.0] - 2026-10-12 - Dialog sizes, gallery, agent API
Added
dialog()/drawer()size:"sm"400 ·"md"560 ·"lg"800 ·"xl"1140px ·
"full"· a number (px) · a CSS length ·"auto". A modal is now a FIXED width capped
to the viewport; drawers take the same names for their width (320 / 400 / 560 / 720px)
or, at top/bottom, height (30 / 45 / 65 / 80vh). Presets are themable
(--puredashboard-dialog-w-<name>,--puredashboard-dialog-drawer-w-<name>/-h-<name>),
and the height cap is--puredashboard-dialog-max-h(85vh). Gallery story "Sizes"._components.jsonllists the imperative functions —dialog,drawer,alert
(withconfirm/prompt),menu,toast— one line each with summary, params,
returns and example (grep '"function":"dialog"'). The generator reads their
@functionJSDoc from the source; before, they were missing and agents guessed._agents.md: "Always passsize" with a content → size table.<puredashboard-gallery>index page. With no component selected (the new default;
previously the first one) the canvas lists every component as a card — name, title,
story count, and the def'sdescriptionor its first story'snotes— grouped like
the sidebar. Cards are links (?c=<tag>).<puredashboard-gallery>filter. A filter box in the sidebar (filterproperty,
URLq=,/focuses it) narrows the sidebar and the index by short name or title.
Changed
dialog()defaults tosize: "md"(560px, fixed). It used to shrink to its content
(up to 520px), so a dialog holding a form or a table came out cramped — the usual
result when nobody set a width. Passsize: "auto"for the old behaviour.
alert/confirm/promptstill fit their message unless given asize.<puredashboard-gallery>is built from the library's own layout: a
<puredashboard-sider>(filter in its header slot, a<puredashboard-nav>with Index /
Overview rows and one section per title prefix; collapses off-canvas, a drawer below
md, remembered inlocalStorage), a<puredashboard-header>toolbar split into start
(sidebar toggle + title) and end (controls), and a<puredashboard-content>canvas. Nav
rows are real links, so Cmd/Ctrl-click opens a component in a new tab. Stories are no
longer remounted by unrelated re-renders (filtering, theme). On small screens the
toolbar controls wrap and the per-card viewport switch is icon-only. The old
__side/__item/__group/__mainclasses are gone.
Fixed
- Theme: a forced
data-theme="dark"now setscolor-scheme: dark. It inherited
light dark, which lets the browser follow the OS — so on a light-mode OS every native
scrollbar and control of a dark page was drawn light (a white scrollbar gutter beside
dark panels in dialogs, drawers, popovers, lists). dialog()/drawer()body scrollbar matches the sider's: thin, transparent track,
thumb while the pointer is over the dialog or focus is inside;
--puredashboard-dialog-scrollbarsets its colour._agents.md: theprompt()example used{ value }; the option is
defaultValue.reactive.js: a child position that switched template →repeat()→ the same
template rendered nothing. The nested instance was kept after its DOM was removed, so
the switch back updated the detached instance. Any component rendering a list in one
state and a fixed template in another at the same position was affected.<puredashboard-sider>drawer no longer reflows the content. Closing it put the
sider back in the flow while its width was still animating to 0, so the text beside it
rewrapped through every step; with the icon rail below the breakpoint, opening it also
shifted the content by the rail width. The drawer now keeps exactly its collapsed
footprint in the flow (--pd-sider-cw, set by the element) and overlaps the rest with a
negative end margin that animates with the width, so the content width never changes.<puredashboard-sider>scrollbar. When the nav is taller than the space between the
sticky header and footer, the scroll region showed the browser's default scrollbar (a
light gutter in dark themes). It is now thin, on a transparent track, and its thumb only
shows while the pointer is over the sider or focus is inside;--pd-sider-scrollbar
sets its colour.
[0.3.2] - 2026-10-12 - Rail hides on phones
Changed
<puredashboard-sider>railhides while the sider isclosedbelow its breakpoint.
On a phone the 16px edge strip sat invisibly over the content's left edge and the way
back in is the app's hamburger anyway; above the breakpoint (desktop offcanvas) the rail
stays as the pointer handle. (Observation from model-gateway's GW-231 measurements.)
[0.3.1] - 2026-10-11 - Sidebar follow-ups
Follow-ups to the 0.3.0 sidebar from the first app that adopted it (model-gateway):
the "icon rail on desktop, hamburger + drawer on phones" shell, and three small
contracts the app had been bridging on its side. Defaults unchanged; the one visible
change is that a sider collapsed to width 0 now really is gone (see Fixed).
Added
<puredashboard-sider>breakpoint-collapse="offcanvas"(property
breakpointCollapse, default"icon"= today's behaviour): below thebreakpointthe
sider collapses to width0instead of the icon rail, andcollapseModereads
"offcanvas"there; above it thecollapsiblemode applies.toggle()from a header
hamburger opens the existing drawer. Follows media-query flips both ways and attribute
changes.<puredashboard-header>sections. Wrap children inpuredashboard-header__start,
puredashboard-header__centerandpuredashboard-header__endto split the bar into a
left-hugging start (sider toggle, breadcrumb), a centred middle that takes the slack
(search, tabs) and a right-hugging end (actions, user menu). Any may be omitted.<puredashboard-sider>trigger="bar"|"button": the collapse trigger as today's
full-width strip, or a small centred icon button with no strip. Unset,variant="inset"
now uses the button (the strip read as a stray bar beside the content card); everything
else keeps the bar.<puredashboard-sider>breakpointMatches(read-only) and abreakpointevent
{ matches, collapsed, collapseMode }on every media-query flip — whether or not
collapsedchanged — so an app can show or hide desktop-only UI without its own
matchMedia.<puredashboard-sider>closedstate attribute (set by the element) whenever it is
collapsed to width0—collapsible="offcanvas",breakpoint-collapse="offcanvas"
below the breakpoint, orcollapsed-width="0". Its header, scroll region, footer and
trigger are madeinertand hidden, the edge border is dropped; only therail(a
pointer handle) and theshortcutremain as ways back in.RouterafterEach(ctx, hit)option — called once the guards have passed and the
page module has loaded, i.e. the navigation is committed, just before the page mounts
(before the view-transition swap);router.currentalready points at the new hit. A
throwing hook is reported throughonError(or the console) and never blocks the mount.<puredashboard-gallery>viewport switch. Every story card gets an Auto · Phone ·
Tablet · Desktop switch (a<puredashboard-segmented>with icons), and the toolbar a
"Viewport" select (URLvp=) that frames every card at once. A framed story is staged in an<iframe>of that size (390 / 820 / 1280px
wide, or"WxH") that loads the gallery inonlymode, so the story gets a real viewport
of its own — breakpoints, media queries and100vhbehave as on that device. A story may
declare its default withviewport: "phone"(or{ width, height }); propsviewport
/frames,.puredashboard-gallery__frame/__vp.<puredashboard-layout>plainfor the nested layout beside an inset sider: it stays
on the page background, its header becomes a bare toolbar (no panel, no border) and the
content — or the nested layout with its own header — inside is the inset card: the
"toolbar above the card" arrangement.- Gallery: Nav story "Inset" now carries an app header split into the three sections (sider
toggle + breadcrumb | search | inbox badge + user), and "Inset · top header" shows the
same header as a bare toolbar ABOVE the inset card (plain). - Gallery: Nav stories "Hamburger" (breakpoint
md+breakpoint-collapse="offcanvas",
header button →toggle(), follows the window width) and "Hamburger · phone" (the same
shell framed as a phone by default, so the drawer shows on a desktop screen), and Menu
story "Toggle from its button" (a counter shows that the re-click closes without
re-opening).
Changed
<puredashboard-sider>persistis desktop-only. Opening or closing the drawer
below the breakpoint (trigger, rail, shortcut,toggle(), backdrop, Escape, link) no
longer writes the stored value — a desktop "collapsed" survives a phone-width detour.<puredashboard-sider>growing past its breakpoint restores the state held before it
matched (the persisted choice whenpersistis set; otherwise the author-set or
previous value) instead of always expanding.<puredashboard-sider>drawer is modal: while it is open the sider's layout siblings
areinert(restored on close), and an Escape that something else already handled
(defaultPrevented— an openmenu()) no longer closes the drawer as well.
Fixed
<puredashboard-sider>collapsed to width 0 kept its contents in the Tab order (11
stops measured through a hidden sider), left a 1px edge line, and let the collapse
trigger's chevron spill out of the 0-wide offcanvas sider. All three are theclosed
state above.menu(): clicking the anchor of the open menu re-opened it. The press light-dismissed
the menu and the same click's handler opened a fresh one. Now a press on the anchor
closes it, the promise resolvesnull, andmenu()called from that click's handler
returns an already-resolvednullinstead of opening again — no change to the usual
button.onclick = () => menu(button, …)wiring.
[0.3.0] - 2026-10-11 - Sidebar
A stronger sidebar, modelled on the shadcn Sidebar and adapted to PureDashboard's
zero-dependency, light-DOM conventions. Defaults keep their look; everything new is
opt-in except two small visual corrections called out under Changed.
Added
<puredashboard-nav>sections. A node{ heading, children, collapsible? }renders
a titled block (small muted heading over a flat list,puredashboard-nav__section/
__heading/__list--section);collapsible: truemakes the heading a
<button aria-expanded>and thetoggleevent reports it by itsheading.<puredashboard-nav>row actions. A node'saction: { icon, label }adds a secondary
button beside the link (puredashboard-nav__action, shown on hover / focus / on the
active row, always on touch screens) that firesactionwith{ item, label, href }.<puredashboard-nav>icon-onlyattribute. The rail look on the nav itself: centred
icons, labels clipped (still the links' names) and copied to each link'stitle, badges /
chevrons / actions / sub-lists / section headings hidden (a section keeps a separator).<puredashboard-nav>loading.true(5 rows) or a number renders skeleton rows and
marks the navaria-busy.<puredashboard-nav>palette knobs--pd-nav-text/-muted/-hover/-active-bg
/-border/-radius, so a container (the sider) can recolour a nav without touching
the page tokens.<puredashboard-sider>header and footer slots. Children withslot="header"/
slot="footer"sit in sticky regions above / below the scrolling middle (brand or
workspace switcher on top, user menu at the bottom);puredashboard-sider__expanded-only
on any element inside hides it while collapsed.<puredashboard-sider>variant="floating"(a rounded, bordered, shadowed card inset
from the layout) andvariant="inset"(the sider sits on the page background and the
content beside it becomes the raised card).<puredashboard-sider>collapsible="offcanvas"slides the sider out completely
(width 0) instead of leaving the icon rail;collapseModereads back"icon"/
"offcanvas".<puredashboard-sider>rail— a thin toggle handle along the outer edge (hover
shows a line, click toggles;labels.rail).<puredashboard-sider>shortcut— Cmd (macOS) / Ctrl + the given key toggles; the
bare attribute meansb.<puredashboard-sider>persist— remembers the collapsed state inlocalStorage
underpuredashboard-sider:<name>and restores it on connect.<puredashboard-sider>drawer below itsbreakpoint. Expanding a breakpoint-collapsed
sider (trigger, rail, shortcut,toggle()) opens it over the content as a drawer
(overlaystate attribute, backdrop on the layout); backdrop click, Escape or a link
inside closes it.- Theme tokens
--sidebar-bg/-text/-muted/-border/-hover/-activein
tokens.css, defaulting to the page panel tokens; the sider and the nav inside it paint
with them, so one override gives every sidebar its own palette. - Gallery: Nav stories "App shell", "Icon rail", "Floating", "Inset", "Offcanvas",
"Loading", "Sidebar palette".
Changed
- Nav rows are square. Top-level rows were rounded (
--radius, 10px) while nested rows
were squared off next to the guide line, so the active row changed shape with its depth;
now every row (and section heading / action button) has no radius. Opt back in with
--pd-nav-radius. - A
<puredashboard-sider>mirrorscollapsedonto its navs asicon-only(a
MutationObservercovers navs added later); the icon-only CSS moved fromlayout.css
intonav.cssunder that attribute. Visually unchanged. - A sider's
breakpointonly collapses on a match. Above the breakpoint an author-set
or persistedcollapsednow stands; it used to be forced open on connect. - The sider no longer clips its own overflow (the header, middle and footer regions do),
so the edge rail can straddle the border.
[0.2.0] - 2026-10-07 - Full component set
From three components to a full admin-UI set. 0.1.0 shipped the table, the uploader, the
Markdown viewer, the overlays, the router and the optional theme; 0.2.0 adds about sixty
components across forms, navigation, data display, feedback and layout, a desktop-native
layer, a component gallery, a landing page and machine-readable docs for AI agents — still
zero-dependency, no-build and CSP-safe. It also takes in the 19 local patches a production
consumer (model-gateway) was carrying, plus one option it asked for, and fixes the UI
defects a full review of the gallery found in both themes.
Upgrade notes (from 0.1.0)
- Light theme colours are darker.
--accentis#2563eb(hover#1d4ed8, active
#1e40af) and--redis#dc2626in the light palette, for readable white-on-accent
text. Pages pinned to the old hues should override these tokens. - The optional theme's
button/a/ form-control rules have zero specificity
(:where()). Any class in your app now beats them; if you relied on the theme winning
over a single class of yours, raise that rule. <puredashboard-table>markup: every<th>carriesscope="col", and the
rows-per-page select always lists the currentpageSize. Tests that snapshot the table
header may need updating.components.cssis the new one-link bundle for every component stylesheet; linking
files one by one still works.
Added
-
Design tokens in three tiers (
theme/tokens.css): primitive--pd-ref-*, semantic
app tokens, per-component--pd-<name>-*knobs — plus focus ring, elevation, z-index,
motion (honours reduced motion), control sizing with a compact density
([data-density="compact"]), a type scale and status backgrounds. Every 0.1.0 token
name is kept, and every value except the light palette's accent and red (see the
upgrade notes).components.css@imports every component stylesheet. -
Forms —
<puredashboard-input>,-textarea,-number(± steppers),-select,
-combobox(searchable, APG),-checkbox,-switch,-radio-group,-slider,-date,
-time,-datetime,-color,-rate, the<puredashboard-form>wrapper (real
<form>, values + validity) and the<puredashboard-field>label/hint/error wrapper.
All form-associated throughElementInternals. -
Navigation —
<puredashboard-tabs>(APG tabs, or a nav of real links),
-breadcrumb,-pagination(windowed; cursor paging withhasMore),-steps,
-nav(collapsible sidebar tree),-menubar(desktop application menu bar). -
Data display —
<puredashboard-card>,-descriptions,-statistic,-tag,
-badge,-avatar,-list,-tree,-collapse,-timeline,-empty,-json-view,
-lazy,-copy,-toggle,-toggle-group,-segmented. -
Feedback and overlays —
<puredashboard-alert>,-progress(line / circle),
-meter,-spinner,-skeleton,-result,-tooltip,-popover,-popconfirm. -
General and layout —
<puredashboard-button>(variants, sizes,status
success/warning/danger,shaperound/circle, loading, link mode),-divider,-space,
-flex,-row/-colgrid,<puredashboard-layout>with-header/-content/
-footer/-sider(collapsible),-splitter(resizable panes, APG keyboard). -
Desktop-native layer —
<puredashboard-titlebar>for frameless Tauri / Wails /
Electron windows (mac / windows / linux control layouts, drag region), an opt-in macOS
skintheme/native.css([data-skin="macos"]: vibrancy, hairlines, compact radii) and a
frameless-window recipe indocs/DESKTOP.md. -
dialog()layout — a flex column whose header and newfooteroption stay put while
the body scrolls;alert/confirm/promptput their buttons in that footer. -
<puredashboard-gallery>— a reusable, zero-dependency component explorer (sidebar,
theme / lang / density toolbar, overview grid, URL state) with a story for every
component (test/gallery.html), a demo dashboard (test/dashboard.html) and a desktop
demo (test/desktop.html). -
Landing page (
index.html) for GitHub Pages, built with the library itself, with a
live demo section. -
Docs for AI agents that consume the library —
src/_agents.md(rules, recipes,
component index) andsrc/_components.jsonl(one JSON line per component, generated
from the JSDoc bytools/api-reference). -
<puredashboard-copy>(copy.js): a copy-to-clipboard button — one click writes a
value to the system clipboard and the button reports the result (the Lucidecopy
icon swaps to a check, or a cross on failure, forfeedbackms, while an off-screen
live region announces it). The value does not have to be text: a string, rich
HTML (type="html"writestext/htmland a plain-text flattening), or an
image — a URL, aBlob/File, an<img>or a<canvas>, normalised to PNG
through a canvas because that is the one format clipboards reliably accept. The value
comes fromvalue(also a possibly-async function, so it stays late-bound),src
(an image URL, fetched on click) orfrom(a CSS selector — an<input>contributes
its.value, anything else itstextContent). Text degrades to the legacy
<textarea>+execCommandpath when the async Clipboard API is unavailable; images
and HTML need a secure context, and a failure is never silent — it shows the error
state and emitscopyerror. Renders a real<button>(platform keyboard, focus,
disabled) that is named "Copy" by default, so an icon-only one needs no
aria-label.variant="text", three sizes and the--pd-copy-*knobs match
<puredashboard-toggle>, so they line up in one toolbar. The success event is
copied, notcopy, so it can't be confused with the platform's own bubbling
Ctrl+C event.
Tables paste into a spreadsheet as real cells: an element source contributes its
outerHTML(sofrom="#report table"copies the grid, not a run-on string), a
<table>is inferred ashtmlwithout settingtype, and thetext/plainhalf is a
structured flattening — TSV for tables (tab per cell, newline per row), line breaks
for block elements and<br>— so Excel's "Paste Special → Text" and any plain-text
field still get one cell per column instead of everything in one. -
<puredashboard-lazy>(lazy.js): defers building expensive content until it is
needed —<img loading="lazy">, but for components. A page holding dozens of
<puredashboard-json-view>/<puredashboard-markdown>/ tables pays for all of them
up front; wrapping each one defers the work until it scrolls into view. Measured in
Chromium with 200<puredashboard-json-view>s: 1005 ms → 53 ms to build,
13 000 → 656 DOM nodes, 200 → 4 components upgraded.
Three content sources: a<template>child (zero JS — a template's content is
inert, so the elements inside are never even upgraded), arender(host)function, or
load: () => import(…)whose default export is a tag name or a mount function (the
same contract as arouter.jspage). Triggers:visible(default,
IntersectionObserver+rootMargin),idle,eager,manual+renderNow().
While pending it shows the author's[data-lazy-fallback]child or a built-in shimmer
of the reservedheight, so nothing jumps on swap;unrenderalso tears content down
when it scrolls far away (long lists). State is reflected asdata-state
(pending/rendering/rendered/error), it emitsrender/unrender/
loaderror, materialises everything pending onbeforeprint, and renders immediately
whereIntersectionObserveris unavailable so content is never lost. -
<puredashboard-toggle-group>(toggle-group.js): a set of<puredashboard-toggle>
buttons sharing one selection — text alignment, a view mode, a formatting set. The
toggles are the host's real light-DOM children (adopted likesplitter.js's panels,
and re-synced live by aMutationObserverwhen they are added or removed), so any
toggle feature — icons, labels, sizes,variant— works inside a group. Single-select
by default (valueis a string,nullwhen empty);multiplemakes it an array.
deselectable=falsekeeps one always selected,attached(default) joins the buttons
into one control,orientation="vertical"stacks them,disableddisables the set
(and restores only what it disabled). Rendersrole="group"with a roving tabindex
— the whole group is ONE tab stop and Arrow/Home/End move between the toggles
(loopwraps). The children's ownchangeevents are swallowed; the group emits a
singlechange{value}, so a caller listens in one place. -
<puredashboard-toggle>(toggle.js): a two-state button — it stays visibly
pressed until pressed again — for a setting that applies IMMEDIATELY (bold/italic in a
toolbar, mute, pin, "show archived"). Renders a native<button>witharia-pressed,
so keyboard (Space/Enter), focus anddisabledcome from the platform. Deliberately
not form-associated:<puredashboard-switch>(role=switch) and
<puredashboard-checkbox>remain the form inputs — a toggle is an action button and
submits nothing. Props:pressed,disabled,value(its identity inside a toggle
group),label(string or node),icon(trusted SVG),size,variant
(default/textfor toolbars), plustabbable+focus()so a group can run a
roving tabindex. Emitschange{pressed, value}on user action only — setting
.pressedin JS stays silent.pressedis reflected as an attribute; an icon-only
toggle takes its name fromaria-label(mirrored onto the inner button). -
<puredashboard-meter>(meter.js): a gauge for a MEASUREMENT inside a known
range (disk used, memory, quota, a score) —role="meter"with
aria-valuenow/min/max+aria-valuetext,...
[0.1.0] - 2026-06-26 - First public release
First public release — extracted into a standalone, zero-dependency, no-build library.
Added
- Template engine (
reactive.js): a lit-html-style parts engine that diffs the DOM
in place (so<input>focus/caret/scroll survive re-renders), therepeat()keyed-list
directive,renderResult, and theReactivecustom-element base. Noeval/new Function. - String templating (
html.js): escapedhtml, plusraw,icon,escapeHTML. - Components
<puredashboard-table>— sortable columns, filter, pagination + rows-per-page,
row selection with a bulk-action bar, per-row actions.<puredashboard-upload>— drag-and-drop, thumbnails, per-file progress, native
multipart form submit; plusuploadFile().<puredashboard-markdown>— XSS-safe Markdown renderedtextContent-only with an
href whitelist; plusparseMarkdown/renderMarkdown.
- Imperative overlays —
dialog/drawer/alert/confirm/prompt,
menu(), andtoast(+.success/.error/.warn/.info), all on the native top layer. - Router (
router.js) — hash or History API modes, params, catch-all 404,
lazy-loaded pages, layouts, and guards. - Optional theme (
src/theme/) —tokens.css(light/dark palette),base.css
(form controls),shell.css(dashboard frame), anddashboard.css(all-in-one).
Dark by default, light viaprefers-color-scheme, forceable withdata-theme. - Docs & tooling — README,
docs/ARCHITECTURE.md,docs/USAGE.md,
docs/DEVELOPMENT.md; jsdom test suite run in Docker (make -C test); a browser
showcase/demo harness undertest/.
Changed
- Toasts now use the elevated
--panelsurface (consistent withmenu/dialog), so
they read as crisp cards instead of relying on a low-contrast background.