diff --git a/GUIDELINES.md b/GUIDELINES.md index aaab68d..942b4dc 100644 --- a/GUIDELINES.md +++ b/GUIDELINES.md @@ -533,6 +533,46 @@ tune. This is a first-class goal, bounded by sections 3.1 and 11. 5. **One listener, one write per frame** for pointer-driven effects. 6. **ES5-compatible style** in `niveus.js`, to match the file. +### 14.4 Cross-engine rules + +Each of these was a real defect, found by driving Firefox with real input. + +1. **Never wrap an appearance change in a page-wide view transition.** The + snapshot cannot blur what sits behind frosted glass, so for its whole + duration every frosted bar, menu and Glaze pane goes clear and shows the page + sharply through itself. Let surfaces transition their own colours. Selection + markers still use a short view transition; keep it brief, and prefer a + per-element animation for anything inside frosted chrome. +2. **Do not use padding to make a scroll container's last item reachable.** + Firefox leaves the container's bottom padding out of its scrollable area, so + the final rows were unreachable and a short list could not scroll at all. Use + real boxes — `::before` / `::after` spacers — which every engine counts. +3. **Normalise wheel deltas.** Chromium reports a notch in pixels (~100); + Firefox reports it in lines (~3). Convert with `deltaMode` before using + `deltaY`, or an effect that works in one engine is invisible in the other. +4. **A scrollable list is not draggable on desktop.** Touch scrolls natively; a + mouse does not. Anything that is meant to feel like a phone control (a wheel, + a carousel) needs pointer-drag handling, with inertia, an edge that gives, and + a click that still works when the pointer did not move. +5. **Shadows need something to darken.** A black shadow on a near-black canvas + is invisible, however correct its computed value. The dark canvas is + deliberately lighter than the shadow colour, and soft layers use only a mild + negative spread so the blur reaches outside the element. +6. **Where a CSS feature is missing, supply the same curve in script** (scroll- + driven animation and `interpolate-size` are absent in Firefox), and keep the + plain state correct without either. +7. **State that the browser only half-supports needs an attribute.** A checkbox + can *show* a partial state but a click never *chooses* it; + `data-nv-tristate` makes the cycle explicit. +8. **Same-page links travel.** `scroll-behavior: smooth` under + `prefers-reduced-motion: no-preference`; a jump to a section is the textbook + change a user could have watched. +9. **Know your test rig.** A headless browser without a GPU does not render + `backdrop-filter`, reports no hover capability (so hover-only controls such + as scroller arrows disappear), and may not deliver scroll events reliably. + Treat those as limits of the rig, not of the system, and confirm them on a + real display. + ### 14.3 Documentation Every new class, token, attribute, function or event is added to `REFERENCE.md` @@ -604,9 +644,13 @@ Niveus code, follow it literally. python3 tools/build.py --check # dist/ matches sources ``` -7. **Look at it.** Render both appearances in a real browser at a realistic size - and inspect the result. Passing checks prove the rules; they do not prove it - looks right or works. Exercise interactive components, not just their markup. +7. **Look at it, in Firefox first.** Chromium implements nearly everything + Niveus uses, so passing there proves little; Firefox is the stricter test and + is where regressions have actually appeared. Render both appearances at a + realistic size and exercise the interactive parts with *real input* — pointer + drags, wheel notches, key presses — not only synthetic events or static + screenshots. Passing checks prove the rules; they do not prove it looks right + or works. Then confirm in Chromium. See 14.4 for what differs. 8. **Report faithfully.** State what you verified, what you could not, and what failed. A test that failed because of the test harness is still reported. @@ -651,6 +695,7 @@ A change is done when: - [ ] It is documented in `REFERENCE.md`; `README.md` and this document are updated if a principle or the component list changed. - [ ] It renders correctly in **light and dark**, with and without `niveus.js`. +- [ ] It was seen and exercised in **Firefox** with real input, then in Chromium. - [ ] Every interaction state exists and is reachable by keyboard. - [ ] Translucent surfaces have a solid fallback. - [ ] It respects reduced motion and reduced transparency. @@ -714,7 +759,7 @@ Use this for review, human or automated. Every line is a yes/no. - [ ] No `!important`; application CSS is unlayered. - [ ] `REFERENCE.md` updated; `dist/` rebuilt, not hand-edited. - [ ] `build.py`, `check.py`, `build.py --check` pass. -- [ ] Seen in a browser; interactive parts exercised. +- [ ] Seen and exercised in Firefox with real input, then Chromium. --- diff --git a/REFERENCE.md b/REFERENCE.md index 28ae7b4..89da370 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -527,6 +527,7 @@ everything still renders — state changes simply arrive instead of travelling. | `data-nv-calendar-step` | button in a calendar | `-1` or `1`: months to move. | | `data-nv-timepicker` | `.nv-timepicker` | Marks a time picker. Also `data-nv-value` (`HH:MM`), `-hours` (`24` default / `12`), `-step` (minutes), `-name` (hidden form field), `-format-toggle` (adds the 24H / AM / PM wheel). | | `data-nv-picker-label` | child of a picker trigger | Receives the chosen option's text; default is the trigger itself. | +| `data-nv-tristate` | `input[type=checkbox]` | Click cycles unchecked → checked → partial. `="partial"` starts partial. Exposes `data-nv-state` (`off`/`on`/`partial`). | | `data-nv-scroller` | `.nv-scroller-frame` | Wires its `[data-nv-scroll="prev"\|"next"]` controls. | | `data-scrolled` | `.nv-nav` | Set by the application on scroll; gives the bar its shadow. | | `data-nv-direction` | `` | Set during a transition by `nv.transition`. | diff --git a/dist/niveus.css b/dist/niveus.css index e18d6e0..b85cacc 100644 --- a/dist/niveus.css +++ b/dist/niveus.css @@ -527,6 +527,12 @@ consistent between appearances: an ambient contact shadow that keeps the edge crisp, a soft penumbra for the spread, and an umbra that only appears once a surface is genuinely far from the canvas. + + Two things keep a shadow visible, and both have failed before. The soft + layers use only a mild negative spread: a deep one tucks the blur under the + element and leaves almost nothing outside it. And the canvas has to be + lighter than the shadow colour — black cannot darken black — which is why the + dark canvas is not the darkest value it could be. ========================================================================== */ :root { @@ -535,27 +541,27 @@ --nv-elevation-1: 0 1px 1px var(--nv-color-shadow-ambient), 0 2px 4px -1px var(--nv-color-shadow-penumbra), - 0 6px 14px -8px var(--nv-color-shadow-umbra); + 0 6px 14px -5px var(--nv-color-shadow-umbra); --nv-elevation-2: 0 1px 1px var(--nv-color-shadow-ambient), 0 3px 6px -2px var(--nv-color-shadow-penumbra), - 0 10px 20px -10px var(--nv-color-shadow-umbra); + 0 10px 20px -7px var(--nv-color-shadow-umbra); --nv-elevation-3: 0 1px 2px var(--nv-color-shadow-ambient), 0 6px 12px -4px var(--nv-color-shadow-penumbra), - 0 18px 36px -12px var(--nv-color-shadow-umbra); + 0 18px 36px -9px var(--nv-color-shadow-umbra); --nv-elevation-4: 0 1px 2px var(--nv-color-shadow-ambient), 0 10px 20px -6px var(--nv-color-shadow-penumbra), - 0 30px 60px -16px var(--nv-color-shadow-umbra); + 0 30px 60px -12px var(--nv-color-shadow-umbra); --nv-elevation-5: 0 2px 3px var(--nv-color-shadow-ambient), 0 16px 32px -8px var(--nv-color-shadow-penumbra), - 0 44px 88px -20px var(--nv-color-shadow-umbra); + 0 44px 88px -16px var(--nv-color-shadow-umbra); /* --- Inset --------------------------------------------------------------- Recessed surfaces: fields, wells, tracks, pressed controls. */ @@ -1093,10 +1099,10 @@ :root, [data-nv-brand] { /* --- Canvas and surfaces ----------------------------------------------- */ - --nv-dark-background: color-mix(in oklab, #0f0f12, var(--nv-brand) var(--nv-brand-tint)); - --nv-dark-background-subtle: color-mix(in oklab, #18181b, var(--nv-brand) var(--nv-brand-tint)); - --nv-dark-surface: color-mix(in oklab, #1f1f22, var(--nv-brand) var(--nv-brand-tint)); - --nv-dark-surface-elevated: color-mix(in oklab, #2a2a2e, var(--nv-brand) var(--nv-brand-tint)); + --nv-dark-background: color-mix(in oklab, #17171b, var(--nv-brand) var(--nv-brand-tint)); + --nv-dark-background-subtle: color-mix(in oklab, #1d1d21, var(--nv-brand) var(--nv-brand-tint)); + --nv-dark-surface: color-mix(in oklab, #232327, var(--nv-brand) var(--nv-brand-tint)); + --nv-dark-surface-elevated: color-mix(in oklab, #2d2d32, var(--nv-brand) var(--nv-brand-tint)); --nv-dark-surface-inset: color-mix(in oklab, #111113, var(--nv-brand) var(--nv-brand-tint)); --nv-dark-surface-hover: color-mix(in oklab, #313136, var(--nv-brand) var(--nv-brand-tint)); --nv-dark-surface-active: color-mix(in oklab, #39393f, var(--nv-brand) var(--nv-brand-tint)); @@ -1183,9 +1189,9 @@ calc(c * 0.2) h); /* --- Depth and material -------------------------------------------------- */ - --nv-dark-shadow-ambient: rgb(0 0 0 / 0.5); - --nv-dark-shadow-penumbra: rgb(0 0 0 / 0.5); - --nv-dark-shadow-umbra: rgb(0 0 0 / 0.55); + --nv-dark-shadow-ambient: rgb(0 0 0 / 0.6); + --nv-dark-shadow-penumbra: rgb(0 0 0 / 0.62); + --nv-dark-shadow-umbra: rgb(0 0 0 / 0.72); --nv-dark-shadow-inset: rgb(0 0 0 / 0.65); --nv-dark-highlight: rgb(255 255 255 / 0.15); --nv-dark-highlight-strong: rgb(255 255 255 / 0.24); @@ -1374,6 +1380,13 @@ border-radius: var(--nv-radius-xs); } + /* A link to a place on the same page travels there instead of cutting. */ + @media (prefers-reduced-motion: no-preference) { + html { + scroll-behavior: smooth; + } + } + /* Whatever the browser draws itself — a bare checkbox, a progress bar, the text caret, a date field's glyphs — takes the brand, so no native colour survives next to the system's own. */ @@ -4119,8 +4132,11 @@ inline-size: 3.5rem; block-size: calc(var(--_row) * var(--_rows)); margin: 0; - padding: calc(var(--_row) * (var(--_rows) - 1) / 2) 0; + padding: 0; list-style: none; + cursor: grab; + user-select: none; + touch-action: pan-y; overflow-y: auto; overscroll-behavior: contain; scroll-snap-type: y mandatory; @@ -4140,6 +4156,21 @@ display: none; } + /* Spacers, not padding: they let the first and last rows reach the centre. + Firefox leaves the bottom padding of a scroll container out of its + scrollable area, which stranded the last rows (and made a three-row wheel + unscrollable); real boxes always count. */ + .nv-timepicker__column::before, + .nv-timepicker__column::after { + content: ""; + display: block; + block-size: calc(var(--_row) * (var(--_rows) - 1) / 2); + } + + .nv-timepicker__column[data-nv-dragging] { + cursor: grabbing; + } + .nv-timepicker__column:focus-visible { outline: none; border-radius: var(--nv-radius-md); diff --git a/index.html b/index.html index 757cec9..db92b1d 100644 --- a/index.html +++ b/index.html @@ -768,7 +768,7 @@ Weekly digest