A filter component for data tables, and a sharper tab strip
· Release
This release adds the filter toolbar that sits above a data table, sharpens the segmented tab control, and fixes a hover state that had never actually rendered.
New: nana-filter and nana-filter-item
Section titled “New: nana-filter and nana-filter-item”Filtering a table needs a search box, a status cut, a date window, and often a column picker — four different controls that all have to end up in one query. The new components collect them without replacing any of them.
Each filter is a nana-filter-item wrapping whichever field suits it. The values
are read by duck-typing (value, or checked for switches and checkboxes)
rather than by tag name, so a field this library doesn’t ship works too, as long
as it exposes a value and fires nana-change or change.
<nana-filter label="Filters">
<nana-filter-item name="q" label="Search" grow live debounce="250">
<nana-input placeholder="Order ref or customer" clearable></nana-input>
</nana-filter-item>
<nana-filter-item name="status" label="Status" operator="is" default-value="all">
<nana-select value="all">
<nana-option value="all">All</nana-option>
<nana-option value="open">Open</nana-option>
<nana-option value="pending">Pending</nana-option>
<nana-option value="closed">Closed</nana-option>
</nana-select>
</nana-filter-item>
<nana-filter-item name="created" label="Created" operator="between">
<nana-date-range clearable></nana-date-range>
</nana-filter-item>
</nana-filter> The aggregated value is a plain object keyed by each item’s name — active
filters only:
filter.addEventListener("nana-filter-change", (e) => { loadOrders(new URLSearchParams(e.detail.value)); // { q: "acme", status: "open" }});
// Restoring from the URL writes through to every field, without echoing an event back.filter.value = Object.fromEntries(new URLSearchParams(location.search));Three details worth knowing
Section titled “Three details worth knowing”default-value is what stops a chip appearing immediately. A select sitting
on All holds a non-empty value, so without it the filter counts as active
from first paint and shows a chip forever. Set it to whatever means “no filter”
and the item stays inactive until the user moves off it. clear() returns the
field there too.
Chips resolve option labels. A chip reads Status is Open, not
status: open — the item looks up the chosen nana-option and uses its text. A
date range’s start/end is split back into two readable ends, and a multi-select
collapses into a single removable token rather than one chip per value.
no-chip covers the things that aren’t really filters. A column picker
belongs in the query but doesn’t narrow the result set, so rendering
“Columns: Ref, Customer, Total” as a removable chip is misleading. Mark it
no-chip and it contributes its value while staying out of the chip row, out of
the active count, and out of Clear all — nobody clearing their filters means
to reset which columns they can see.
<nana-filter label="Filters" variant="panel">
<nana-filter-item name="status" label="Status" default-value="all">
<nana-select value="all">
<nana-option value="all">All</nana-option>
<nana-option value="open">Open</nana-option>
</nana-select>
</nana-filter-item>
<nana-filter-item name="columns" label="Visible columns" no-chip>
<nana-checkbox-group orientation="horizontal">
<nana-checkbox value="ref" checked>Ref</nana-checkbox>
<nana-checkbox value="customer" checked>Customer</nana-checkbox>
<nana-checkbox value="total">Total</nana-checkbox>
</nana-checkbox-group>
</nana-filter-item>
</nana-filter> Three layouts — bar for sitting directly above a table, panel for card chrome
and a responsive grid, popover for a trigger button carrying the active count.
mode="manual" buffers edits behind an Apply / Reset footer when each change
costs a round trip.
See Filter and Filter Item.
Things that change
Section titled “Things that change”Segmented tab segments now sit flush. The 0.25rem gap between them is gone.
A gap opened grey channels through the track, which read as several separate
controls rather than one strip with a moving thumb. The thumb’s corner radius is
now derived from the track’s radius minus its padding, so the two curves stay
concentric instead of the thumb looking pasted into a rounder hole. Both are
themeable via --nana-tab-group-track-radius and --nana-tab-group-track-pad.
Disabled tabs recede by opacity, not by colour. They previously used the subtle text step; they now use the inactive colour at 45% opacity, so they stay distinguishable from a merely unselected tab in both themes.
Tab labels are fractionally tighter. Tab text now carries the tight letter-spacing token. Very wide tab strips may reflow by a pixel or two.
Polish
Section titled “Polish”The segmented control got a proper raised thumb: a three-layer shadow — a tight contact shadow, a wider ambient one, and a hairline edge — that tightens as the thumb compresses under a press. The track carries an inset hairline so it reads as recessed rather than as a flat grey rectangle.
One thing we tried and backed out: pushing unselected labels further into the
background. Measured on the segmented track, the subtle text step gives
3.98:1 at the 14px label size — under the 4.5:1 AA floor. The track is what
breaks it; on a bare page the same colour scrapes 4.74:1. So unselected labels
stay on the muted step (6.55:1 on the track), and selection is carried by the gap
to the active state instead — full-strength text on an opaque thumb at 17.9:1.
--nana-tab-inactive-color is there if you want to recess it against a
background you’ve checked yourself.
nana-input’s hover border never rendered. The @media (hover: hover) guard
trailed the selector instead of wrapping the rule:
/* A selector followed by @media is not valid nesting — the whole block was dropped. */:host(:not([disabled])) @media (hover: hover) { .wrapper:hover { … } }Malformed CSS inside a Lit css “ template doesn’t fail the TypeScript
build, so this sat there silently. The field now shows its border affordance on
hover before focus.
New token
Section titled “New token”--nana-transition-easing-emphasized — cubic-bezier(0.32, 0.72, 0, 1), a
decelerating curve for movement that should settle rather than stop dead. The tab
indicator and the segmented thumb both use it.
A note on versions
Section titled “A note on versions”0.0.19 and 0.0.20 ship identical code. The per-package release script was
named publish, which is also an npm lifecycle hook — so npm publish uploaded
the package and then invoked itself against the version it had just created. The
resulting 409 Conflict made a successful release look like a failed one. The
script is renamed, and 0.0.20 is the version to depend on.