From 369d1cd8f79721e080868a97afc104c253678c5d Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Wed, 28 Oct 2026 09:24:24 +0100 Subject: [PATCH 1/5] Add Glaze material, pickers, slider and design guidelines Glaze is glass with a dial: --nv-glass-frost (0 clear .. 1 heavy) moves backdrop blur, tint and saturation together, read where the surface is drawn so any subtree or slider can change it. A masked rim and a specular bloom follow the pointer via niveus.js; without it the light sits at its default and the material still reads. Falls back to the elevated solid under reduced transparency or without backdrop-filter. New components: .nv-slider (binds to a token with data-nv-var), .nv-calendar, .nv-timepicker (24-hour by default, optional 24H / AM/PM switch, value always HH:MM), and .nv-picker with .nv-menu--listbox (search, type-ahead, keys). All render without niveus.js except where noted. GUIDELINES.md is the normative rulebook for people and AI agents. check.py, build.py and build.py --check pass. Co-Authored-By: Claude Sonnet 5.5 --- GUIDELINES.md | 689 +++++++++++++++++++++++++++++++ README.md | 14 +- REFERENCE.md | 49 ++- dist/niveus.css | 579 +++++++++++++++++++++++++- index.html | 148 +++++++ niveus/components/calendar.css | 131 ++++++ niveus/components/menu.css | 3 +- niveus/components/picker.css | 69 ++++ niveus/components/slider.css | 109 +++++ niveus/components/surface.css | 99 +++++ niveus/components/timepicker.css | 128 ++++++ niveus/niveus.css | 4 + niveus/niveus.js | 631 ++++++++++++++++++++++++++++ niveus/themes/dark.css | 5 + niveus/themes/light.css | 7 + niveus/tokens/color.css | 4 + niveus/tokens/materials.css | 16 + 17 files changed, 2679 insertions(+), 6 deletions(-) create mode 100644 GUIDELINES.md create mode 100644 niveus/components/calendar.css create mode 100644 niveus/components/picker.css create mode 100644 niveus/components/slider.css create mode 100644 niveus/components/timepicker.css diff --git a/GUIDELINES.md b/GUIDELINES.md new file mode 100644 index 0000000..3521410 --- /dev/null +++ b/GUIDELINES.md @@ -0,0 +1,689 @@ +# Niveus Design Guidelines + +**Status:** normative · **Version:** 1.0 · **Applies to:** every person and every +AI agent that designs with, builds on, or changes Niveus. + +This document says what Niveus is, what it decides, and what it forbids. It is +written to be followed, not admired. Where it conflicts with a habit, a +preference, or a model's prior, this document wins. + +The key words **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT** and **MAY** are +used as in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119). A **MUST** is +enforced by a tool or is a defect when broken. A **SHOULD** needs a stated +reason to break. A **MAY** is a freedom, not a recommendation. + +--- + +## Contents + +1. [How to use this document](#1-how-to-use-this-document) +2. [Identity](#2-identity) +3. [Principles and their order](#3-principles-and-their-order) +4. [Materials](#4-materials) +5. [Glaze](#5-glaze) +6. [Elevation and light](#6-elevation-and-light) +7. [Color and appearance](#7-color-and-appearance) +8. [Type, space and shape](#8-type-space-and-shape) +9. [Motion and continuity](#9-motion-and-continuity) +10. [Interaction and state](#10-interaction-and-state) +11. [Accessibility](#11-accessibility) +12. [Components](#12-components) +13. [Customization contract](#13-customization-contract) +14. [Authoring rules](#14-authoring-rules) +15. [Protocol for AI agents](#15-protocol-for-ai-agents) +16. [Changing the system](#16-changing-the-system) +17. [Anti-patterns](#17-anti-patterns) +18. [Compliance checklist](#18-compliance-checklist) + +--- + +## 1. How to use this document + +### 1.1 Sources of truth, in order of authority + +When two sources disagree, the higher one is right and the lower one has a bug. + +| # | Source | Answers | +| - | ------ | ------- | +| 1 | `tools/check.py`, `tools/build.py --check` | What is *allowed*. Mechanical, not negotiable. | +| 2 | **This document** | What is *intended*, and why. | +| 3 | [`REFERENCE.md`](REFERENCE.md) | What exists: every class, token, attribute, function, event. | +| 4 | [`README.md`](README.md) | How to install and orient. | +| 5 | The CSS and JS | How it is currently built. | + +`REFERENCE.md` is the complete API. If a class, token or attribute is not in it, +**it does not exist** and MUST NOT be used, guessed, or invented. + +### 1.2 Who this is for + +- **Designers and engineers** use it to decide, and to review each other. +- **AI agents** use it as an operating contract. Section 15 is written for them + directly; sections 2–14 apply to them as to anyone. +- **Product authors** who ship Niveus to end users use section 13 to decide what + their users may customize. + +### 1.3 What this document is not + +It is not a style preference list. Every rule here exists because breaking it +produces a specific, nameable failure: an unreadable surface, a broken theme, a +control that behaves differently in two places, a port that has drifted. Rules +state their reason so they can be applied to cases the document did not foresee. + +--- + +## 2. Identity + +Niveus is a **design-system layer**, not a component library bolted onto an +application. Applications consume it; they do not redefine it. + +**It is:** tactile (controls are bevelled keys with light falling across them), +adaptive (one brand colour drives light and dark), and continuous (state +changes can be followed). Fields are wells cut into a surface. Windows are +solid. Glass is kept for what floats. + +**It is not:** a flat system, a material-for-its-own-sake system, or a +glassmorphism kit. Skeuomorphism here is **functional**: a surface imitates a +physical property only where that property answers a question the user is +already asking. + +> Niveus is not about making interfaces look physical. It is about making +> digital surfaces feel understandable. + +The questions a surface must answer are: **What is this? Is it above or below? +Where did it come from? Did my action register?** An effect that answers none +of them is decoration, and decoration makes an interface tiring. + +### 2.1 Naming + +Names in Niveus describe a thing's role, never another vendor's product. A +feature MUST NOT be named after a third party's trademark, and documentation +MUST NOT present a Niveus feature as a clone of one. (Niveus's adjustable-frost +glass is called **Glaze** for this reason.) + +--- + +## 3. Principles and their order + +| Principle | Rule | What it costs | +| --------- | ---- | ------------- | +| **Clarity** | Every element has a purpose. Decoration never competes with content. | Fewer flourishes. | +| **Soft physicality** | Depth comes from shadow, highlight and inset, not ornament. | No gratuitous textures. | +| **Material** | Surfaces are made of something: solid, elevated, inset, frosted, glass, glaze. | Every surface must pick one. | +| **Restraint** | No effect exists because it is possible. Translucency and shadow are spent, not sprinkled. | Saying no to good-looking things. | +| **Consistency** | The same control behaves the same way everywhere, including under a pointer. | No local variations. | +| **Adaptability** | Nothing is defined in terms of one appearance or one colour. | Every value is a role. | +| **Continuity** | Nothing pops into a state the user could have watched it reach. | Motion must be earned and bounded. | +| **Customizability** | Where the author allows it, the person using the product can tune it. | A bounded, documented set of dials. | + +### 3.1 Precedence + +When principles conflict, resolve in this order: + +1. **Accessibility** (section 11) overrides everything. A beautiful surface that + cannot be read or operated is broken. +2. **Clarity** overrides material and motion. +3. **Restraint** overrides every effect. A quiet interface with one thing moving + reads clearly; a busy one does not. +4. **Consistency** overrides local preference. +5. **Customizability** operates *inside* the limits above, never through them. + +--- + +## 4. Materials + +A material answers *what light does when it hits a surface*. Elevation answers +*how far the surface is from the canvas*. They are independent and combine +freely. + +| Material | Class | Use for | MUST NOT be used for | +| -------- | ----- | ------- | -------------------- | +| Solid | `.nv-material-solid` | Content. The default. | — | +| Elevated | `.nv-material-elevated` | Surfaces that must be read before their context. | — | +| Inset | `.nv-material-inset` | Fields, wells, tracks: anything recessed. | Anything that is pressed *toward* the user. | +| Frosted | `.nv-material-frosted` | Chrome that content passes under: bars, docks, menus, toolbars. | Containers of body copy or forms. | +| Glass | `.nv-material-glass` | Contextual layers where the content behind must stay legible. | Surfaces with no content behind them. | +| Glaze | `.nv-material-glaze` | Floating, interactive panes where the visitor's control over frost is a feature. See section 5. | Whole-page backgrounds; anything the user must read at length. | + +### 4.1 Rules + +1. **Solid by default.** Anything that *holds content* — windows, cards, dialogs, + tables, forms — is solid. Translucency is reserved for what *floats over* + content. +2. **Translucency means "this floats".** Used everywhere, it stops meaning + anything, tints whatever is behind it, and forces the GPU to composite. + Spend it. +3. **Raised things are bevelled and sheened.** Lit along the top edge, shaded + along the bottom, with a contact outline. Compose with + `box-shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-2)` and + `background-image: var(--nv-sheen-raised)` over a role colour. +4. **Never invent a material.** If none fits, the right answer is a different + composition, or a proposal under section 16 — not a local `backdrop-filter`. +5. **Translucent materials MUST degrade.** `prefers-reduced-transparency` and + missing `backdrop-filter` fall back to the solid equivalent. A new + translucent surface without that fallback is incomplete. + +--- + +## 5. Glaze + +Glaze is glass with a dial. It exists to make Niveus feel *alive* — a surface +that catches light and answers to the person looking at it — without giving up +legibility. + +### 5.1 Anatomy + +| Part | Mechanism | Controlled by | +| ---- | --------- | ------------- | +| **Frost** | Backdrop blur, tint opacity and saturation, moved together | `--nv-glass-frost`, `0` (clear) … `1` (heavy frost); range set by `--nv-glaze-blur-max`, `--nv-glaze-tint-min`, `--nv-glaze-tint-max` | +| **Rim** | A 1.5px masked gradient border, bright on the side facing the light | `--nv-glaze-rim` (width), `--nv-color-glaze-rim-lit` / `-far` | +| **Specular** | A soft bloom under the pointer | `--nv-color-glaze-specular` | +| **Light** | Pointer position and angle written per surface | `niveus.js` → `--nv-light-x/-y/-angle` | + +Frost moves blur, tint and saturation **together** on purpose. A pane that +changed only one of them would stop looking like the same pane. + +### 5.2 Rules + +1. **Frost is read where the surface is drawn.** Set `--nv-glass-frost` on + `:root` for the site, or on any subtree to change just that region. Never + copy a frost value into another token; that breaks subtree overrides. +2. **Something colourful MUST be behind it.** Glaze over a flat canvas looks like + a grey film. Use it over imagery, gradients, or an `.nv-bed`. If nothing is + behind it, use `elevated`. +3. **Never place Glaze behind work.** Forms, tables and long reading belong on + solid surfaces. Glaze is for what the user passes *through*: pickers, + popovers, docks, notifications, hover cards, command surfaces. +4. **Text on Glaze MUST meet contrast at every frost the person can choose.** + Clear glass over a busy backdrop is the worst case. A pane carrying body text + SHOULD bound its slider (`min="0.2"` or higher) so the lowest setting is + still legible. Verify against the busiest content that can sit behind it. +5. **Do not stack Glaze.** Glaze over Glaze compounds blur and tint into mud. + One translucent layer between content and the eye. +6. **Do not animate the blur radius.** Blur is expensive; change frost by + discrete steps or on user input, not as ambient motion. +7. **No JavaScript, no problem.** Without `niveus.js` the light sits at its + default (top left) and the material is still correct. Light tracking MUST + NOT be required for legibility, and is skipped for touch and under reduced + motion. +8. **Position.** Glaze carries zero-specificity `position: relative`; a menu or + dialog using it keeps the positioning its own component gave it. + +--- + +## 6. Elevation and light + +Six levels, each describing one relationship between a surface and the canvas. +A shadow that is on neither scale is a shadow that means nothing. + +| Level | Class | Relationship | Typical use | +| ----- | ----- | ------------ | ----------- | +| 0 | `.nv-elevation-0` | flush | painted on the canvas | +| 1 | `.nv-elevation-1` | resting | cards, rows, panels | +| 2 | `.nv-elevation-2` | raised | anything that reacts | +| 3 | `.nv-elevation-3` | floating | popovers, menus | +| 4 | `.nv-elevation-4` | lifted | dialogs, sheets | +| 5 | `.nv-elevation-5` | overlay | topmost; used sparingly | + +Inset depth uses `.nv-elevation-inset-{sm,md,lg}`. + +1. **Light comes from above.** Highlights sit on top edges, shade on bottom + edges. A highlight on a lower edge is a bug. +2. **Dark is not light inverted.** Surfaces get *lighter* as they rise, shadows + deepen, and the top highlight thins. Elevation therefore carries a tint, not + only a shadow. +3. **Elevation tracks the user's mental model, not the visual taste.** Raise + what the user can act on or must notice; do not raise to decorate. +4. **A recess catches light on its lower lip** (`--nv-ledge`). Wells are cut into + the surface, not painted darker. + +--- + +## 7. Color and appearance + +### 7.1 The model + +The whole colour system has **one input**: `--nv-brand`. Every role, in both +appearances, is derived from it with relative colour syntax. There is no second +place to define a colour. + +- A component names a **role** (`--nv-color-*`); the appearance decides what the + role is made of, through `light-dark()`. +- `--nv-palette-*` is raw and does not adapt. **MUST NOT** be referenced from + application code. +- Appearance is `data-theme="light" | "dark"`, or absent to follow the OS. +- A subtree may rebrand with `data-nv-brand`. + +### 7.2 Rules + +1. **No literal colours** in components or examples (the sole exception is the + value of `--nv-brand`). `tools/check.py` fails on any. +2. **Every role exists in both appearances.** Adding `--nv-light-x` without + `--nv-dark-x` fails the parity check. +3. **Contrast is measured, not assumed.** The derived label on any filled colour + clears 4.5:1 for any brand. New roles that carry text MUST do the same. +4. **State is never colour alone.** Pair colour with a shape, icon, text or + attribute change (see 11). +5. **Semantic colours mean one thing.** `success`, `warning`, `danger` carry + meaning; they are not accents. Use `accent` for emphasis and brand. +6. **Dark mode is designed, not generated.** When a new surface is added, look at + it in dark. If it only works on white, it is not finished. + +--- + +## 8. Type, space and shape + +- **Type.** Instrument Sans for the interface, JetBrains Mono for code and + engraved labels, shipped in `fonts/`. Use the role classes — `.nv-display`, + `.nv-heading`, `.nv-title`, `.nv-body`, `.nv-label`, `.nv-caption`, + `.nv-overline` — not raw sizes. Sizes are in `rem` so the user's own font + size is respected. Tabular figures (`font-variant-numeric: tabular-nums`) for + any number that changes or aligns. +- **Space.** A 4px rhythm, expressed as `--nv-space-*`. Values are tokens; a + one-off `13px` is a defect. +- **Shape.** `--nv-radius-*`. Nested radii MUST be concentric: use + `--nv-radius-nested-*`, or the outer radius minus the gap. Pills + (`--nv-radius-full`) are for pills, avatars and switches. +- **Logical properties.** Use `inline`/`block` (`padding-inline`, + `inset-block-start`), never `left`/`right`, so the system works in RTL. + Where a property has no logical form (`translate`), flip it explicitly for + RTL. +- **Application-specific values** that describe *your content* — a 15rem sidebar, + an aspect ratio, a grid template — are yours. A value that restates the design + language is the system's, and hard-coding it is how a system quietly stops + being one. + +--- + +## 9. Motion and continuity + +Every interaction MUST read as a continuation of the state before it. The goal is +not to animate everything. It is that **nothing pops into a state the user could +have watched it reach.** + +### 9.1 Rules + +1. **Motion explains.** It shows where something came from, where it went, or + that an action registered. Motion with no such job is removed. +2. **Durations and easings are tokens.** `--nv-duration-*`, `--nv-ease-*`, + `--nv-transition-*`. A literal `200ms` is a defect, and it bypasses reduced + motion. +3. **Reduced motion collapses at the token level.** Components never opt in + individually. `prefers-reduced-motion` skips view transitions entirely; the + change still happens, it just arrives. +4. **Selection travels.** When emphasis moves between elements, the marker + itself moves (shared `view-transition-name`). When a panel replaces another, + it slides in the direction of travel. +5. **View transition names are unique per document.** A duplicate makes the + browser skip the transition. Give each selection group its own + `--nv-selection-name`. +6. **Origin matters.** Menus scale out of their trigger's edge; dialogs expand + away from the control that opened them. +7. **No JavaScript, no breakage.** Continuity enhances; it MUST NOT be required + for function. +8. **Restraint wins ties.** When continuity and restraint conflict, animate less. + +--- + +## 10. Interaction and state + +1. **One treatment per state, written once per component.** Hover lift, press + sink and focus ring are defined once; a new variant MUST NOT redefine them. + Variants are made by reassigning private `--_*` properties. +2. **Hover is guarded.** Hover styles live in `@media (hover: hover)`. Touch + devices never see a stuck hover. +3. **Pressed means pressed.** Raised controls sink on press. A control that does + not respond to a press does not look pressable. +4. **Targets are at least 44px**, including controls drawn smaller (the system + extends the pointer target). +5. **Every state has a visible form:** default, hover, focus-visible, active, + selected, disabled, invalid, loading. A component missing one is unfinished. +6. **Disabled is not hidden.** A disabled control keeps its place, dims, and + uses `not-allowed`. Prefer explaining *why* over disabling silently. +7. **Selection state lives in attributes with a platform meaning:** + `aria-selected`, `aria-current`, `aria-pressed`, `aria-checked`, + `aria-invalid`, `disabled`, `data-outside`. Not in ad-hoc classes. + +--- + +## 11. Accessibility + +Accessibility is a precondition, not a feature. Target **WCAG 2.2 level AA**. +Niveus provides the base; the author is responsible for the rest. + +**Provided by the system:** + +- One focus treatment: a contrast ring under an accent ring, visible on any + surface including accent-filled controls. +- A minimum 44px pointer target. +- `prefers-reduced-motion` and `prefers-reduced-transparency` honoured at the + token level. +- Filled colours with a derived on-colour that clears 4.5:1. + +**Required of the author:** + +1. **Icon-only buttons MUST have `aria-label`.** The system cannot name them. +2. **Use native elements** — ` + +
+ + + +
+ + A popup list is a menu whose items are options: the same frosted surface, + anchoring and light dismiss, with a selection that stays. A picker is its + trigger: a field-shaped button that shows the current choice and a chevron + that turns over while the list is open. + + niveus.js adds what a native . + ========================================================================== */ + + .nv-timepicker { + --_row: 2.25rem; + --_rows: 5; + + display: inline-flex; + flex-direction: column; + align-items: center; + gap: var(--nv-space-2); + padding: var(--nv-space-2) var(--nv-space-3) var(--nv-space-3); + border-radius: var(--nv-radius-xl); + color: var(--nv-color-text-primary); + } + + .nv-timepicker__wheels { + position: relative; + display: flex; + gap: var(--nv-space-1); + } + + /* The lens. It belongs to the wheels, not the picker, so the switch beneath + them never moves the centre. */ + .nv-timepicker__wheels::before { + content: ""; + position: absolute; + inset-inline: calc(var(--nv-space-2) * -1); + inset-block-start: 50%; + block-size: var(--_row); + translate: 0 -50%; + background-color: var(--nv-color-selection); + border-radius: var(--nv-radius-md); + box-shadow: var(--nv-highlight-top); + pointer-events: none; + } + + .nv-timepicker__column { + position: relative; + inline-size: 3.5rem; + block-size: calc(var(--_row) * var(--_rows)); + margin: 0; + padding: calc(var(--_row) * (var(--_rows) - 1) / 2) 0; + list-style: none; + overflow-y: auto; + overscroll-behavior: contain; + scroll-snap-type: y mandatory; + scrollbar-width: none; + mask-image: linear-gradient( + transparent, + currentcolor 28%, + currentcolor 72%, + transparent + ); + } + + .nv-timepicker__column::-webkit-scrollbar { + display: none; + } + + .nv-timepicker__column:focus-visible { + outline: none; + border-radius: var(--nv-radius-md); + box-shadow: var(--nv-focus-ring); + } + + .nv-timepicker__option { + display: grid; + place-items: center; + block-size: var(--_row); + scroll-snap-align: center; + color: var(--nv-color-text-tertiary); + font: var(--nv-font-label); + font-size: var(--nv-font-size-lg); + font-variant-numeric: tabular-nums; + cursor: pointer; + transition: var(--nv-transition-colors); + } + + .nv-timepicker__option[aria-selected="true"] { + color: var(--nv-color-text-primary); + font-weight: var(--nv-font-weight-semibold); + } + + /* 24H / AM/PM. A button group under the wheels; the pressed one is the format + in use. 24-hour is the default, so it starts pressed. */ + .nv-timepicker__format { + align-self: center; + } + + /* Pressed reads as pushed in: the format in use is a key held down. */ + .nv-timepicker__format .nv-button[aria-pressed="true"] { + background-color: var(--nv-color-accent-subtle); + background-image: none; + color: var(--nv-color-accent-text); + box-shadow: var(--nv-elevation-inset-sm), var(--nv-ledge); + } + + .nv-timepicker__separator { + display: grid; + place-items: center; + align-self: center; + color: var(--nv-color-text-tertiary); + font: var(--nv-font-label); + font-size: var(--nv-font-size-lg); + } +} @layer niveus.components { /* ========================================================================== Niveus — Disclosure diff --git a/index.html b/index.html index 3e6e30c..757cec9 100644 --- a/index.html +++ b/index.html @@ -67,6 +67,72 @@ place-items: center; } + /* Glaze specimen: color behind the pane, so the frost has something to + act on. The orbs and the line of type are only there to be blurred. */ + .glaze-demo { + position: relative; + margin-block-start: var(--nv-space-6); + min-block-size: 22rem; + display: grid; + place-items: center; + overflow: hidden; + } + + .glaze-demo__scene { + position: absolute; + inset: 0; + pointer-events: none; + } + + .glaze-demo__orb { + position: absolute; + border-radius: var(--nv-radius-full); + inline-size: var(--_size); + block-size: var(--_size); + inset-inline-start: var(--_x); + inset-block-start: var(--_y); + background: var(--_fill); + } + + .glaze-demo__type { + position: absolute; + inset-inline: 0; + inset-block-end: var(--nv-space-4); + text-align: center; + font: var(--nv-font-display); + font-size: clamp(2.5rem, 9vw, 5.5rem); + font-weight: 700; + color: var(--nv-color-text-primary); + opacity: 0.85; + white-space: nowrap; + } + + .glaze-demo__panel { + position: relative; + inline-size: min(26rem, 100%); + padding: var(--nv-space-6); + border-radius: var(--nv-radius-xl); + display: flex; + flex-direction: column; + gap: var(--nv-space-4); + } + + .picker-demo { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + justify-content: center; + gap: var(--nv-space-6); + margin-block-start: var(--nv-space-6); + } + + .picker-demo__column { + display: flex; + flex-direction: column; + gap: var(--nv-space-4); + inline-size: min(16rem, 100%); + } + .specimen__name { font: var(--nv-font-label); } @@ -297,6 +363,84 @@ + +
+ + +
+
+

Glaze

+

+ One dial controls how frosted this pane is. The rim brightens on + the side facing your pointer. +

+
+
+ + + Clear glass on the left, heavy frost on the right. +
+
+ + +
+
+
+ + + +
+
+

Pickers

+

+ A calendar, wheels for time, and a popup list. They hold content, so + they sit on solid surfaces; here the first two float on Glaze + over a bed, which is where a date field's popover would be. +

+
+ +
+
+
+ +

+ +
+
+
+ +
+
+ 09:30 + +
+ + +
+ + + + + + + +
+
+
+
@@ -1315,6 +1459,10 @@