mirror of
https://github.com/Nexoniarz/Niveus.git
synced 2026-11-02 20:24:26 +00:00
Selection markers, tab panels, brand changes and theme changes no longer use a view transition. The page snapshot could not blur what sits behind frosted glass, so every frosted surface went clear for the length of the change. nv.transition now measures the marker before and after, and animates the marker itself (FLIP); arriving content animates on its own; colours fade on their own surfaces. Verified in Firefox: no view transition is ever started. Time picker: wheels take every pointer type (mouse, pen, touch) with touch-action: none, so a phone, a browser's phone mode and a desktop behave alike. The rubber-band is capped at about 40% of a row (a whole row made the neighbour look selected). Each wheel has its own frame slot, so one wheel can no longer swallow another's last scroll event. Switching 24H / AM / PM happens when the wheel settles, and the hours animate into the new layout. Carousel arrows are frosted glass, as anything floating over content should be. At phone width the top bar keeps to one row and its link list scrolls. GUIDELINES.md records the new cross-engine rules. check.py, build.py and build.py --check pass. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
501 lines
24 KiB
Markdown
501 lines
24 KiB
Markdown
# Niveus
|
||
|
||
A tactile, adaptive interface language. Controls are bevelled keys with light
|
||
falling across them, fields are wells cut into the surface, windows are solid,
|
||
and glass is kept for the chrome that floats over everything else. Warm
|
||
graphite in dark, soft paper in light, one brand color driving both.
|
||
|
||
Niveus is a **design-system layer**, not a component library bolted onto an
|
||
application. Applications consume it; they do not redefine it. The visual rules
|
||
live here and can change without touching a line of application logic.
|
||
|
||
```html
|
||
<link rel="stylesheet" href="dist/niveus.css"> <!-- with fonts/ beside dist/ -->
|
||
```
|
||
|
||
Niveus ships its own faces in `fonts/`: Instrument Sans for the interface and
|
||
JetBrains Mono for code and engraved labels, both under the SIL Open Font
|
||
License. Copy `fonts/` alongside `dist/` (or `niveus/`) and they load by
|
||
themselves.
|
||
|
||
The stylesheet uses the variable `.woff2` files. `fonts/static/` holds the
|
||
same faces as plain TrueType files at the four weights Niveus uses (400, 500,
|
||
600, 700, plus Instrument Sans italics), for native toolkits and font
|
||
configurations that pick a weight by name rather than by variable axis.
|
||
|
||
```html
|
||
<html data-theme="dark"> <!-- omit data-theme to follow the system -->
|
||
```
|
||
|
||
Open `index.html` for the full specimen: materials, the elevation scale, every
|
||
semantic color, the type scale, and each component in both appearances.
|
||
`examples/` holds four complete products built with it.
|
||
|
||
**[GUIDELINES.md](GUIDELINES.md) is the rulebook** — what Niveus is, what it
|
||
decides, what it forbids, and a protocol for AI agents. Read it before you
|
||
design with the system or change it.
|
||
|
||
**[REFERENCE.md](REFERENCE.md) is the complete API** — every class, token,
|
||
attribute and function, each listed beside the name the rest of the industry
|
||
uses for it. It is written to be read by a person *or* handed to a model on its
|
||
own: an assistant that has only that file can write correct Niveus markup
|
||
without seeing a line of the CSS. `tools/check.py` fails if a class exists in
|
||
the CSS and is missing from it.
|
||
|
||
---
|
||
|
||
## Using it
|
||
|
||
Reference the system, never restate it:
|
||
|
||
```css
|
||
/* No */
|
||
.thing { background: #ffffff; border-radius: 12px; padding: 10px 16px; }
|
||
|
||
/* Yes */
|
||
.thing {
|
||
background: var(--nv-color-surface-elevated);
|
||
border-radius: var(--nv-radius-md);
|
||
padding: var(--nv-space-2-5) var(--nv-space-4);
|
||
box-shadow: var(--nv-highlight-top), var(--nv-elevation-2);
|
||
}
|
||
```
|
||
|
||
Application-specific values are fine when they describe *your* content and
|
||
layout — a sidebar that happens to be 15rem wide, an aspect ratio, a grid
|
||
template. They are not fine when they restate the design language.
|
||
|
||
Everything ships inside the `niveus` cascade layer, so unlayered application
|
||
styles always win. Overriding a Niveus component never requires `!important`
|
||
or a longer selector.
|
||
|
||
## Structure
|
||
|
||
```
|
||
niveus/
|
||
├── tokens/ the values, appearance-agnostic
|
||
│ ├── color.css palette + semantic roles
|
||
│ ├── typography.css families, scale, weights, composite roles
|
||
│ ├── spacing.css 4px rhythm + control sizing
|
||
│ ├── radius.css corner scale
|
||
│ ├── elevation.css the 0–5 depth scale, inset, highlight, focus ring
|
||
│ ├── motion.css durations, easings, displacement
|
||
│ └── materials.css blur, backdrop recipes, material definitions
|
||
│
|
||
├── themes/ what those values become in each appearance
|
||
│ ├── light.css
|
||
│ └── dark.css
|
||
│
|
||
├── base.css elements, before any component
|
||
├── continuity.css how one state becomes the next
|
||
├── components/ surface, button, input, card, dialog, navigation,
|
||
│ menu, picker, calendar, timepicker, slider,
|
||
│ disclosure, scroller, bed, badge, table
|
||
├── utilities.css the short list
|
||
├── niveus.css the entry point
|
||
└── niveus.js optional — continuity for changes CSS cannot animate
|
||
|
||
examples/ four complete interfaces built from the system
|
||
├── ai-landing/ marketing page
|
||
├── ai-chat/ chat web app
|
||
├── it-shop/ hardware shop
|
||
└── dashboard/ monitoring dashboard
|
||
```
|
||
|
||
`niveus/niveus.css` is the authored entry point and works directly over HTTP.
|
||
`dist/niveus.css` is the same thing with the `@import` chain flattened into one
|
||
request — use it for anything shipped.
|
||
|
||
## Theming
|
||
|
||
A component never names a color. It names a **role**, and the appearance decides
|
||
what that role is made of:
|
||
|
||
`background` · `surface` · `surface-elevated` · `surface-inset` ·
|
||
`text-primary` · `text-secondary` · `text-tertiary` · `border` · `accent` ·
|
||
`success` · `warning` · `danger`
|
||
|
||
Each role resolves through `light-dark()`, pulling from `--nv-light-*` in
|
||
`themes/light.css` or `--nv-dark-*` in `themes/dark.css`. The appearance is
|
||
selected by `color-scheme`, which the theme files bind to `[data-theme]`:
|
||
|
||
| `data-theme` | Result |
|
||
| ------------ | ----------------------------- |
|
||
| *(absent)* | follows the operating system |
|
||
| `light` | light, always |
|
||
| `dark` | dark, always |
|
||
|
||
Dark is not light inverted. Surfaces get *lighter* as they rise, shadows deepen
|
||
rather than disappear, the accent moves up its ramp to stay legible, and the top
|
||
highlight thins to a faint rim.
|
||
|
||
## Materials and depth
|
||
|
||
**Material** answers what light does when it hits a surface. **Elevation**
|
||
answers how far that surface is from the canvas. They are independent.
|
||
|
||
| Material | Use for |
|
||
| ---------- | ---------------------------------------------- |
|
||
| `solid` | content. The default. |
|
||
| `elevated` | surfaces that must be read before their context |
|
||
| `inset` | fields, wells, tracks — anything recessed |
|
||
| `frosted` | chrome that content passes under: bars, toolbars |
|
||
| `glass` | contextual layers where content behind must stay legible |
|
||
| `glaze` | glass the person looking can tune: `--nv-glass-frost` (0 clear … 1 frosted), with a rim that follows the pointer |
|
||
|
||
| Elevation | Relationship |
|
||
| --------- | ------------------------------- |
|
||
| 0 | flush — painted on the canvas |
|
||
| 1 | resting — cards, rows, panels |
|
||
| 2 | raised — anything that reacts |
|
||
| 3 | floating — popovers, menus |
|
||
| 4 | lifted — dialogs, sheets |
|
||
| 5 | overlay — topmost, used sparingly |
|
||
|
||
Applied as classes: `.nv-material-frosted`, `.nv-elevation-3`. A shadow that is
|
||
on neither scale is a shadow that means nothing.
|
||
|
||
### Light
|
||
|
||
Elevation is the shade a surface casts. **Glow** is the light it gives off —
|
||
and it is reserved for things that are *on*: a live status, the plan you are
|
||
on, a finished step, the selected item in a showcase. Glow never replaces an
|
||
elevation; a glowing surface still rests on something.
|
||
|
||
| Where | How |
|
||
| ----- | --- |
|
||
| A card that belongs to something with a color | `.nv-card--lit` — a lit top edge in `--nv-card-tint` |
|
||
| The one card that is on | `.nv-card--glow` |
|
||
| A showcase card | `.nv-card--spotlight` — light follows the pointer |
|
||
| A feature icon or product mark | `.nv-tile`, `--glow` to light it |
|
||
| A person | `.nv-avatar--glow` |
|
||
| A display line | `.nv-text-lit` — type cut from the same lit material |
|
||
|
||
Glows come in the same steps as everything else (`--nv-glow-sm/md/lg`), and a
|
||
tinted surface glows in its own tint, mixed down to `--nv-glow-strength`. Two
|
||
glowing things side by side cancel each other out: if everything is lit,
|
||
nothing is.
|
||
|
||
## Continuity
|
||
|
||
Every interaction should read as a continuation of the state before it. When
|
||
something moves, grows, collapses or changes appearance, the change itself
|
||
should be perceptible — where it came from, where it went, and why. The goal is
|
||
not to animate everything. It is that nothing pops into a new state the user
|
||
could have watched it reach.
|
||
|
||
| Change | How it stays continuous |
|
||
| ------ | ----------------------- |
|
||
| Selection moves between items | The marker itself travels. It is a pseudo-element; `niveus.js` measures it before and after the change and animates the new marker from the old box to its own. |
|
||
| Switching tabs | The incoming panel slides in from the side the user is moving towards, and the marker travels with it. |
|
||
| Content expanding | `interpolate-size` lets `block-size: auto` animate, so content grows from the row that revealed it. Nothing is measured. |
|
||
| Menus and dialogs | A menu is anchored to its trigger and scales out of its edge; a dialog expands away from the control that opened it. |
|
||
| Theme or brand change | Surfaces transition their own colors, so it glides even with no script. Nothing snapshots the page, so frosted glass stays live while it happens. |
|
||
| Hover, press, focus, select | All have a duration. Marks grow from their centre, the switch thumb travels its track. |
|
||
|
||
Most of this is plain CSS. The two cases CSS cannot do alone — a selection
|
||
moving *between two elements*, and one panel replacing another — need one line
|
||
of script. It deliberately does **not** use a view transition: a page-wide
|
||
snapshot cannot blur what sits behind frosted glass, so every frosted surface
|
||
went clear for the length of the change.
|
||
|
||
```js
|
||
nv.transition(() => {
|
||
previous.removeAttribute("aria-current");
|
||
next.setAttribute("aria-current", "page");
|
||
}, { scope: group }); // the group whose marker should travel
|
||
```
|
||
|
||
`niveus.js` is optional and dependency-free. Without it every component still
|
||
works; changes simply arrive rather than travel. It also wires
|
||
`[data-nv-theme]`, `[data-nv-dialog]`, `[data-nv-tabs]` and `[data-nv-close]`
|
||
so the common cases need no code at all.
|
||
|
||
Long pages can arrive in the order they were written: `.nv-reveal` rises into
|
||
place once as it scrolls into view, and `.nv-reveal-group` brings its children
|
||
in one after another, `--nv-stagger` apart. Only the script marks anything as
|
||
waiting, so without it — and under reduced motion — nothing is ever hidden.
|
||
|
||
### Nothing to name, nothing to keep unique
|
||
|
||
Because the marker is animated directly, several selection groups can change at
|
||
once and none of them needs a name. (`--nv-selection-name` only matters to
|
||
application code that starts its own view transition; if you do, remember that
|
||
a `view-transition-name` must be unique among all rendered elements, and that
|
||
the snapshot flattens frosted glass for as long as it runs.)
|
||
|
||
`prefers-reduced-motion` collapses every duration at the token level and skips
|
||
view transitions entirely. The change still happens; it just arrives.
|
||
|
||
## Color
|
||
|
||
The whole color system has one input:
|
||
|
||
```css
|
||
:root { --nv-brand: #d92e3f; }
|
||
```
|
||
|
||
From it the system derives the accent and its hover and press states, the text
|
||
that lands on it, the link color, the focus ring, the selection highlight, the
|
||
tint in the greys, and how far the status colors lean — for both appearances,
|
||
with nothing else to set. No component is involved: they all name roles, and
|
||
the roles re-derive.
|
||
|
||
Three things make a single input actually work rather than just recolor a
|
||
button:
|
||
|
||
**The label is derived, not chosen.** Every filled color computes its own
|
||
foreground by reading its lightness: below a threshold it resolves to
|
||
near-white, above it to near-black. A yellow brand gets dark labels and a navy
|
||
one light labels, and no one has to remember to update a second token.
|
||
|
||
**The color is moved until its label is readable.** A mid-tone is the one place
|
||
where neither white nor black reaches 4.5:1, so a brand that lands there is
|
||
darkened until white works — or, if already light, lightened until black does.
|
||
Hue and chroma are untouched; only lightness moves, and only as far as it must.
|
||
This is why a mid-tone green arrives slightly deeper than the swatch you set.
|
||
|
||
**The rest of the palette follows.** Status colors lean towards the brand by
|
||
`--nv-brand-harmony` (12%) — red still reads as danger, it just stops looking
|
||
borrowed from another system — and the greys carry `--nv-brand-tint` (4%) of
|
||
it, which is what stops an accent from looking stuck on. Set either to `0%` for
|
||
literal status colors and strictly neutral greys.
|
||
|
||
| Input | Default | What it does |
|
||
| ----- | ------- | ------------ |
|
||
| `--nv-brand` | `#4f6cff` | the one color everything derives from |
|
||
| `--nv-brand-tint` | `4%` | how much brand bleeds into the greys |
|
||
| `--nv-brand-harmony` | `12%` | how far status colors lean towards it |
|
||
| `--nv-contrast-pivot` | `0.7` | where "too mid-toned to carry a label" begins |
|
||
| `--nv-contrast-dark` / `-light` | `0.53` / `0.78` | how far either side a color is pushed |
|
||
|
||
### Branding a subtree
|
||
|
||
`data-nv-brand` re-derives the whole set for one region, so a section can carry
|
||
its own color without a second theme:
|
||
|
||
```html
|
||
<section data-nv-brand style="--nv-brand: #0d9488">
|
||
<button class="nv-button nv-button--primary">Upgrade</button>
|
||
</section>
|
||
```
|
||
|
||
The attribute is required, not decoration: `var()` inside a custom property is
|
||
substituted where that property is *declared*, so a `--nv-brand` set on a bare
|
||
element would never reach the roles. `[data-nv-brand]` is the selector that
|
||
re-runs the derivation.
|
||
|
||
### Overriding a role outright
|
||
|
||
Deriving is the default, not a requirement. Any role can be set directly, for
|
||
the whole page or for a subtree:
|
||
|
||
```css
|
||
.danger-zone { --nv-color-accent: var(--nv-color-danger); }
|
||
```
|
||
|
||
Everything inside follows — buttons, focus rings, links, switches — because
|
||
none of them ever named a color.
|
||
|
||
## Examples
|
||
|
||
`examples/` holds four complete interfaces. Open `examples/index.html`, or go
|
||
straight to one:
|
||
|
||
| | | |
|
||
| --- | --- | --- |
|
||
| **Lumen** | `examples/ai-landing/` | AI product landing page — hero, features, a scrolling row, pricing, FAQ |
|
||
| **Lumen Chat** | `examples/ai-chat/` | chat web app — sidebar, transcript, composer, settings dialog with tabs |
|
||
| **Northwire** | `examples/it-shop/` | hardware shop — filters, product grid, stock states, basket |
|
||
| **Northwire Ops** | `examples/dashboard/` | monitoring dashboard — stat tiles, a chart, a service table, incidents |
|
||
|
||
They look like four different products. None of them overrides a component,
|
||
forks a token, or writes a color. What differs between them is one declaration:
|
||
|
||
```css
|
||
:root { --nv-brand: #ea580c; } /* the shop */
|
||
```
|
||
|
||
Each links exactly two files from the system — `dist/niveus.css` and the
|
||
optional `niveus/niveus.js` — and then its own `app.css`, which contains grids,
|
||
widths and breakpoints and **not a single hex value**. `tools/check.py` enforces
|
||
that: a color anywhere in an example, other than `--nv-brand` itself, fails the
|
||
build. An example that quietly reaches for a hex value is no longer
|
||
demonstrating the system; it is demonstrating how to work around one.
|
||
|
||
Where an example needed something the system did not have, the answer was to
|
||
add it to the system: `.nv-badge` and `.nv-table` exist because these pages
|
||
asked for them.
|
||
|
||
## Philosophy
|
||
|
||
Niveus is not a folder of CSS. It is a set of decisions that have already been
|
||
made, so that building an interface stops being a series of small aesthetic
|
||
arguments.
|
||
|
||
**Niveus is not about making interfaces look physical. It is about making
|
||
digital surfaces feel understandable.** Depth, material and motion are here to
|
||
answer questions the user is already asking — what is this, is it above or
|
||
below, where did it come from, did my click register — and for nothing else. An
|
||
effect that answers no question is decoration, and decoration is what makes an
|
||
interface tiring.
|
||
|
||
### The principles, and what each one costs
|
||
|
||
| | |
|
||
| --- | --- |
|
||
| **Clarity** | Every element has a purpose. Decoration never competes with content. |
|
||
| **Soft physicality** | Depth comes from shadow, highlight and inset — not from ornament. |
|
||
| **Material** | Surfaces are made of something. Solid, elevated, inset, frosted, glass, glaze. |
|
||
| **Restraint** | No effect exists because it is possible. Translucency and shadow are spent, not sprinkled. |
|
||
| **Consistency** | The same control behaves the same way everywhere, including under a pointer. |
|
||
| **Adaptability** | Nothing is defined in terms of an appearance or a color. |
|
||
| **Continuity** | Nothing pops into a new state the user could have watched it reach. |
|
||
|
||
They constrain each other on purpose. Continuity says to animate the change;
|
||
restraint says most changes do not need it. Material says to use translucency;
|
||
restraint says only for layers that pass over content. When two pull in
|
||
different directions, restraint wins — a quiet interface with one thing moving
|
||
reads clearly, and a busy one does not.
|
||
|
||
### Where the line runs
|
||
|
||
Niveus decides **how the application looks and behaves**. The application
|
||
decides **what it does**. In practice:
|
||
|
||
- A color, a shadow, a radius, a duration, a control height → the system.
|
||
- A sidebar that happens to be 15rem wide, an aspect ratio, a grid template →
|
||
the application.
|
||
- A value that is *about* your content is yours. A value that restates the
|
||
design language is the system's, and hard-coding it is how a system quietly
|
||
stops being one.
|
||
|
||
### Adding to the system
|
||
|
||
Three questions, in order:
|
||
|
||
1. **Can it be expressed with what already exists?** Most new requirements are
|
||
an existing component with a modifier, or a pattern — a composition of
|
||
components that needs no new CSS at all. The patterns on the specimen page
|
||
introduce nothing.
|
||
2. **Is it a rule, or is it this once?** A component earns its place when it
|
||
can be stated as a reusable principle. "Selection emphasis travels" is a
|
||
rule. "This menu is 14px from its button" is not.
|
||
3. **Does it hold in both appearances, at every elevation, and under a
|
||
pointer?** If it only works on white, or only when nothing is behind it, it
|
||
is not finished.
|
||
|
||
A new token follows the same test. If a value appears three times and means the
|
||
same thing each time, it is a token. If it appears three times and means
|
||
something different each time, three different things were given the same
|
||
number by accident.
|
||
|
||
### Working on it
|
||
|
||
Every rule in this repo is checked rather than trusted:
|
||
|
||
- `tools/check.py` fails on a role defined for one appearance but not the
|
||
other, a token referenced but never defined, any hard-coded color in a
|
||
component *or an example*, and any class that exists in the CSS but is
|
||
missing from `REFERENCE.md`.
|
||
- Contrast is measured, not assumed — the derived label on every filled color
|
||
clears 4.5:1 for any brand.
|
||
- `tools/build.py --check` fails if `dist/` has drifted from the sources.
|
||
|
||
If a change cannot be made without breaking one of those, the change is
|
||
probably not the problem the system has.
|
||
|
||
## Components
|
||
|
||
| Class | Notes |
|
||
| -------------- | --------------------------------------------------------- |
|
||
| `.nv-button` | `--primary` `--subtle` `--ghost` `--danger` `--frosted` `--float`, `--sm`/`--lg`, `--icon`, `--pill`, `--block`, `.nv-button-group` |
|
||
| `.nv-input` | with `.nv-field`, `.nv-input-group`, `.nv-select`, `.nv-checkbox`, `.nv-radio`, `.nv-switch`, `.nv-choice` |
|
||
| `.nv-card` | `--elevated` `--floating` `--inset` `--frosted` `--interactive`, light: `--lit` `--glow` `--spotlight`, `.nv-card-grid` |
|
||
| `.nv-dialog` | native `<dialog>`; `--sheet` `--compact` `--wide` |
|
||
| `.nv-nav` | `--vertical` `--plain` `--inline`, plus `.nv-nav-tabs`, `.nv-tab-panel` and `.nv-breadcrumbs` |
|
||
| `.nv-menu` | anchored popover with `.nv-menu-trigger`, `__item`, `__separator`, `__label`, `__shortcut` |
|
||
| `.nv-disclosure` | native `<details>`; `.nv-disclosure-group`, plus `.nv-collapse` for other markup |
|
||
| `.nv-scroller` | snapping row inside `.nv-scroller-frame`, with edge fades and optional controls |
|
||
| `.nv-bed` | ground that is visibly not ordinary background; `--quiet` `--vivid` `--single` `--flush` |
|
||
| `.nv-badge` | state, not an action; `--accent` `--success` `--warning` `--danger`, `--solid` `--count` `--square` |
|
||
| `.nv-table` | inside `.nv-table-frame`; `__number` `__actions`, `--compact` `--interactive` |
|
||
| `.nv-tile` | a small raised key holding one glyph; `--solid` `--glow`, `--sm` `--lg` `--xl` |
|
||
| `.nv-avatar` | round raised disc; `--ring` `--glow`, sizes, `__status`, `.nv-avatar-group` |
|
||
| `.nv-stat` | readings set into one recessed well with `.nv-stat-group`; `__label` `__value` `__delta` |
|
||
| `.nv-steps` | markers set into a rail; `data-state="done"` lights the way, `--vertical` |
|
||
| `.nv-deck` | one surface in front of others of its kind, with `__layer` |
|
||
| `.nv-skeleton` | a waiting well with a slow sweep of light; `--text` `--circle` |
|
||
| `.nv-slider` | native range input as an inset track and a knob; `data-nv-var` binds it to a token |
|
||
| `.nv-calendar` | month grid with keyboard model; chosen day is a small raised key |
|
||
| `.nv-timepicker` | scroll-snap wheels, 24-hour by default, optional 24H / AM/PM switch |
|
||
| `.nv-picker` | field-shaped trigger for a popup list (`.nv-menu--listbox`) with search and type-ahead |
|
||
| `.nv-surface` | materials and elevation as utilities |
|
||
|
||
Variants are defined by reassigning private `--_*` properties. Interaction —
|
||
the lift on hover, the sink on press, the focus ring — is written once per
|
||
component, so a new variant cannot drift from how the rest of the system
|
||
responds to a pointer.
|
||
|
||
## Accessibility
|
||
|
||
- One focus treatment system-wide: a contrast ring under an accent ring, so it
|
||
stays visible on any surface including accent-filled controls.
|
||
- Controls shorter than 44px still get a 44px pointer target.
|
||
- `prefers-reduced-motion` collapses duration and displacement to zero at the
|
||
token level — no component opts in individually.
|
||
- `prefers-reduced-transparency`, and browsers without `backdrop-filter`, fall
|
||
back from frosted and glass to their solid equivalents.
|
||
|
||
## Tools
|
||
|
||
```bash
|
||
python3 tools/build.py # flatten the @import chain into dist/niveus.css
|
||
python3 tools/build.py --check # fail if dist/ is stale (for CI)
|
||
python3 tools/check.py # enforce the system's own rules
|
||
```
|
||
|
||
`tools/niveus_css.py` is not a command: it is an evaluator for the subset of
|
||
CSS the token layer is written in (`oklch(from …)`, `color-mix()`,
|
||
`light-dark()`, `calc()`), with CSS Color 4 gamut mapping, plus `read_rules()`
|
||
for the component layer (which role a button's background is in each state).
|
||
Every port that cannot read CSS — Android, Qt — imports it from here to
|
||
generate its tokens and control recipes, so no platform restates a value by
|
||
hand.
|
||
|
||
`check.py` catches the failures that do not look broken: a role defined for one
|
||
appearance but not the other, a token referenced but never defined, and a
|
||
hard-coded color in a component.
|
||
|
||
## Browser support
|
||
|
||
Niveus uses `light-dark()`, cascade layers, `:has()`, `@starting-style` and
|
||
`allow-discrete` transitions — all baseline in current Chrome, Safari, Firefox
|
||
and Edge. `backdrop-filter` degrades to a solid surface where it is missing.
|
||
|
||
The continuity layer builds on newer platform features, each of which degrades
|
||
to the state change simply happening rather than travelling:
|
||
|
||
| Feature | Used for | Without it |
|
||
| ------- | -------- | ---------- |
|
||
| Web Animations (`pseudoElement`) | travelling selection marker, tab panels | the change applies instantly |
|
||
| `interpolate-size` | disclosure height | content appears at full height (`.nv-collapse` still animates) |
|
||
| Anchor positioning | menus tied to their trigger | the menu centres itself |
|
||
| `popover` | menus, light dismiss | needs the application's own toggle |
|
||
| Scroll-driven animation | the time picker's drum effect | `niveus.js` applies the same curve from the scroll position (Firefox) |
|
||
|
||
---
|
||
|
||
> Niveus borrows from physical things only what makes a surface easier to
|
||
> understand: a key looks pressable, a well looks like a place to put
|
||
> something, and glass means something is floating.
|
||
|
||
---
|
||
|
||
## License
|
||
|
||
Niveus is licensed under the [Apache License 2.0](LICENSE). The fonts in
|
||
`fonts/` keep their own license, the SIL Open Font License 1.1
|
||
(`fonts/OFL-*.txt`), which allows bundling them with anything.
|
||
|
||
Made by **Nexoniarz**.
|