docs/Website: Niveus at upstream f340d56; fonts are binary
The vendored copy of Niveus, the site's design system, brought up to the current upstream (github.com/Nexoniarz/Niveus, 2026-10-18). The tree under docs/Website/niveus is upstream's byte for byte. .gitattributes says `* text eol=lf` and listed the binary types it knew, and woff2 was not one of them: Niveus's InstrumentSans-Italic had a CR LF pair in its compressed data, and committing it turned that into LF and the font into garbage. woff, woff2, otf, ico, gif, webp, pdf and wasm are binary now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
parent
a1c965da1e
commit
a8215149a3
56 changed files with 8139 additions and 279 deletions
1
docs/Website/niveus/.gitignore
vendored
Normal file
1
docs/Website/niveus/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
__pycache__/
|
||||
201
docs/Website/niveus/LICENSE
Normal file
201
docs/Website/niveus/LICENSE
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the
|
||||
purposes of this License, Derivative Works shall not include works
|
||||
that remain separable from, or merely link (or bind by name) to the
|
||||
interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the
|
||||
original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing
|
||||
the origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
|
@ -1,16 +1,28 @@
|
|||
# Niveus
|
||||
|
||||
A calm, adaptive interface language built around clarity, depth, material and
|
||||
consistency.
|
||||
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">
|
||||
<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 -->
|
||||
```
|
||||
|
|
@ -137,6 +149,27 @@ answers how far that surface is from the canvas. They are independent.
|
|||
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
|
||||
|
|
@ -170,6 +203,11 @@ 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.
|
||||
|
||||
### One rule worth knowing
|
||||
|
||||
A `view-transition-name` must be unique among all rendered elements, or the
|
||||
|
|
@ -379,7 +417,7 @@ probably not the problem the system has.
|
|||
| -------------- | --------------------------------------------------------- |
|
||||
| `.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`, `.nv-card-grid` |
|
||||
| `.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` |
|
||||
|
|
@ -388,6 +426,12 @@ probably not the problem the system has.
|
|||
| `.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-surface` | materials and elevation as utilities |
|
||||
|
||||
Variants are defined by reassigning private `--_*` properties. Interaction —
|
||||
|
|
@ -413,6 +457,14 @@ 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.
|
||||
|
|
@ -435,9 +487,16 @@ to the state change simply happening rather than travelling:
|
|||
|
||||
---
|
||||
|
||||
> Niveus is not about making interfaces look physical. It is about making
|
||||
> digital surfaces feel understandable.
|
||||
> 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**.
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ Everything ships inside the `niveus` cascade layer, so unlayered application
|
|||
styles win without `!important`.
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="dist/niveus.css">
|
||||
<link rel="stylesheet" href="dist/niveus.css"> <!-- keep fonts/ beside dist/ -->
|
||||
<script src="niveus/niveus.js" defer></script> <!-- optional -->
|
||||
<html data-theme="dark"> <!-- omit to follow the OS -->
|
||||
<style>:root { --nv-brand: #d92e3f; }</style> <!-- the whole identity -->
|
||||
|
|
@ -76,6 +76,9 @@ States come from the element: `disabled`, `aria-disabled`, `aria-busy="true"`
|
|||
| `.nv-card--frosted` | glass card | Translucent, for use over imagery or a bed. |
|
||||
| `.nv-card--interactive` | clickable card | Lifts on hover, sinks on press. |
|
||||
| `.nv-card--flush` | edge-to-edge card | Removes part padding, for media or tables. |
|
||||
| `.nv-card--lit` | accent-edge card, colored top border | A strip of light along the top edge in the card's tint, spilling a little down the face. For a card that belongs to something with a color. |
|
||||
| `.nv-card--glow` | glowing card, highlighted card | Gives off its tint. For the one card that is *on* — the current plan, the live item. |
|
||||
| `.nv-card--spotlight` | spotlight card, cursor glow | A soft light follows the pointer across the face (needs `niveus.js`). Showcases only. |
|
||||
| `.nv-card__header` | card header | |
|
||||
| `.nv-card__title` | card title | |
|
||||
| `.nv-card__subtitle` | card subtitle, supporting text | |
|
||||
|
|
@ -86,6 +89,19 @@ States come from the element: `disabled`, `aria-disabled`, `aria-busy="true"`
|
|||
| `.nv-card__link` | stretched link | Expands one link's hit area over the whole card. |
|
||||
| `.nv-card-grid` | responsive card grid | Auto-fitting, equal-height columns. |
|
||||
|
||||
`--lit`, `--glow` and `--spotlight` combine with every elevation and with each
|
||||
other. They take `--nv-card-tint` (the accent unless set on the card).
|
||||
|
||||
## Deck
|
||||
|
||||
| Class | Known as | Does |
|
||||
| --- | --- | --- |
|
||||
| `.nv-deck` | card stack, stacked cards | One surface in front of others of its kind. Every child shares one grid cell. |
|
||||
| `.nv-deck__layer` | stack layer | A decorative card behind the front one, stepping back and dimming with depth. Up to two; mark them `aria-hidden="true"`. |
|
||||
|
||||
Hidden `.nv-tab-panel`s can live in a deck together: the front surface is
|
||||
whichever one is showing.
|
||||
|
||||
## Dialog
|
||||
|
||||
Built on native `<dialog>`; open with `showModal()` or `nv.openDialog()`.
|
||||
|
|
@ -136,7 +152,7 @@ the same anchor through `--nv-anchor`.
|
|||
| `.nv-nav__badge` | count badge, notification badge | |
|
||||
| `.nv-nav__spacer` | flexible spacer | Pushes what follows to the end. |
|
||||
| `.nv-nav-tabs` | tab list, segmented tabs | Inset track; the selected tab lifts out of it. |
|
||||
| `.nv-tab-panel` | tab panel | Carries the shared content name so panels cross-fade. |
|
||||
| `.nv-tab-panel` | tab panel | Hands over to the next panel inside the same box when `nv.selectTab` switches tabs. |
|
||||
| `.nv-breadcrumbs` | breadcrumbs | |
|
||||
|
||||
## Disclosure
|
||||
|
|
@ -173,6 +189,61 @@ Set `--nv-scroller-item` for item width, `--nv-scroller-fade` for the edge fade.
|
|||
| `.nv-badge--square` | square badge | |
|
||||
| `.nv-badge__dot` | status dot | |
|
||||
|
||||
## Tile
|
||||
|
||||
| Class | Known as | Does |
|
||||
| --- | --- | --- |
|
||||
| `.nv-tile` | icon tile, icon badge, feature icon | A small raised key holding one glyph: tinted face, glyph in its tint. Not interactive. |
|
||||
| `.nv-tile--solid` | filled icon tile | Saturated face with a light glyph. |
|
||||
| `.nv-tile--glow` | glowing icon tile | Lit from inside. |
|
||||
| `.nv-tile--sm` `.nv-tile--lg` `.nv-tile--xl` | tile sizes | 32 / 56 / 72px; default 44px. |
|
||||
|
||||
`--nv-tile-tint` picks the color. An `<img>` inside fills the key, an `<svg>`
|
||||
takes half of it.
|
||||
|
||||
## Avatar
|
||||
|
||||
| Class | Known as | Does |
|
||||
| --- | --- | --- |
|
||||
| `.nv-avatar` | avatar, profile picture | Round raised disc holding an `<img>` or initials. |
|
||||
| `.nv-avatar--ring` | avatar ring, story ring | A ring in `--nv-avatar-tint`, set off by a gap of surface. |
|
||||
| `.nv-avatar--glow` | glowing avatar | The ring, giving off its tint. |
|
||||
| `.nv-avatar--sm` `.nv-avatar--lg` `.nv-avatar--xl` | avatar sizes | 28 / 64 / 96px; default 40px. |
|
||||
| `.nv-avatar__status` | presence dot | Grey at rest. |
|
||||
| `.nv-avatar__status--online` | online indicator | Lit green. |
|
||||
| `.nv-avatar-group` | avatar stack, facepile | Overlapping row. |
|
||||
|
||||
## Stat
|
||||
|
||||
| Class | Known as | Does |
|
||||
| --- | --- | --- |
|
||||
| `.nv-stat-group` | stats row, KPI strip, readout | One recessed well holding several readings, split by hairlines. Wraps on its own. Use a `<dl>`. |
|
||||
| `.nv-stat` | stat, metric, KPI | One reading: label, value, optional delta, in that order. |
|
||||
| `.nv-stat__label` | metric label | `<dt>` inside a group. |
|
||||
| `.nv-stat__value` | metric value, big number | `<dd>`; tabular numerals. |
|
||||
| `.nv-stat__delta` | change, trend | |
|
||||
| `.nv-stat__delta--up` `.nv-stat__delta--down` | positive / negative trend | Success and danger text. |
|
||||
|
||||
## Steps
|
||||
|
||||
| Class | Known as | Does |
|
||||
| --- | --- | --- |
|
||||
| `.nv-steps` | stepper, timeline, process, progress steps | Markers set into a rail cut into the surface. Horizontal, vertical below 40rem. Use an `<ol>`. |
|
||||
| `.nv-steps--vertical` | vertical stepper, timeline | Always vertical. |
|
||||
| `.nv-steps__item` | step | `data-state="done"` lights it and its stretch of rail; `aria-current="step"` lights the marker only. |
|
||||
| `.nv-steps__marker` | step number, step knob | |
|
||||
| `.nv-steps__body` | step content | |
|
||||
| `.nv-steps__title` | step title | |
|
||||
| `.nv-steps__text` | step description | |
|
||||
|
||||
## Skeleton
|
||||
|
||||
| Class | Known as | Does |
|
||||
| --- | --- | --- |
|
||||
| `.nv-skeleton` | skeleton, loading placeholder, shimmer | A shallow well where content will be, with a slow band of light crossing it. Still under reduced motion. |
|
||||
| `.nv-skeleton--text` | text line placeholder | The last of several lines ends short. |
|
||||
| `.nv-skeleton--circle` | avatar placeholder | |
|
||||
|
||||
## Table
|
||||
|
||||
| Class | Known as | Does |
|
||||
|
|
@ -197,6 +268,25 @@ Set `--nv-scroller-item` for item width, `--nv-scroller-fade` for the edge fade.
|
|||
|
||||
Set `--nv-bed-tint` to lead with a color other than the accent.
|
||||
|
||||
## Window
|
||||
|
||||
| Class | Known as | Does |
|
||||
| --- | --- | --- |
|
||||
| `.nv-window` | window, app frame | A framed, solid window. Content never sits on glass. |
|
||||
| `.nv-window--inactive` | unfocused window | Greys the lights and the title, drops one elevation. Hovering the lights restores their color. |
|
||||
| `.nv-window__titlebar` | titlebar, header bar | Raised strip: controls · title · actions, with the title on the window's center line. |
|
||||
| `.nv-window__controls` | traffic lights, window buttons | Group at the start of the bar. Its glyphs show while the pointer is over the group. |
|
||||
| `.nv-window__control` | window button | One light. Needs `aria-label`, plus one of the three below. |
|
||||
| `.nv-window__control--close` | close button | Red, ×. Always first. |
|
||||
| `.nv-window__control--minimize` | minimize button | Yellow, −. Always second. |
|
||||
| `.nv-window__control--zoom` | zoom, maximize button | Green, +. Always third. |
|
||||
| `.nv-window__title` | window title | Truncates. |
|
||||
| `.nv-window__actions` | titlebar actions | Trailing slot; put `.nv-button--ghost.nv-button--sm` here. |
|
||||
| `.nv-window__body` | window content | Fills the rest and scrolls. |
|
||||
|
||||
The three lights keep fixed colors on purpose: they are found by position and
|
||||
color before they are read, so they never take the brand.
|
||||
|
||||
## Surface, material and elevation
|
||||
|
||||
| Class | Known as | Does |
|
||||
|
|
@ -205,7 +295,7 @@ Set `--nv-bed-tint` to lead with a color other than the accent.
|
|||
| `.nv-material-solid` | opaque surface | Content lives here. The default. |
|
||||
| `.nv-material-elevated` | raised surface | Separated from its context. |
|
||||
| `.nv-material-inset` | recessed surface | Fields, wells, tracks. |
|
||||
| `.nv-material-frosted` | frosted glass, blur backdrop | Chrome that content passes under. |
|
||||
| `.nv-material-frosted` | frosted glass, blur backdrop | Chrome that floats over content: bars, docks, launchers, notifications, menus. Moderate blur, bevelled rim. |
|
||||
| `.nv-material-glass` | glass | Contextual layers where content behind must stay legible. |
|
||||
| `.nv-elevation-0` `.nv-elevation-1` `.nv-elevation-2` | elevation / z-depth | flush · resting · raised |
|
||||
| `.nv-elevation-3` `.nv-elevation-4` `.nv-elevation-5` | elevation / z-depth | floating · lifted · overlay |
|
||||
|
|
@ -215,6 +305,14 @@ Set `--nv-bed-tint` to lead with a color other than the accent.
|
|||
|
||||
Material and elevation are independent: combine freely.
|
||||
|
||||
Niveus is solid, with glass used sparingly: anything that holds content
|
||||
(windows, cards, dialogs) is solid, and only chrome floating over it is
|
||||
frosted. Raised things are bevelled — lit along the top edge, shaded along the
|
||||
bottom, with a contact outline — and carry a sheen across their face. When
|
||||
composing your own raised element, use `box-shadow: var(--nv-bevel),
|
||||
var(--nv-outline), var(--nv-elevation-2)` with `background-image:
|
||||
var(--nv-sheen-raised)` over its role color.
|
||||
|
||||
## Type roles
|
||||
|
||||
| Class | Known as | Size |
|
||||
|
|
@ -225,10 +323,19 @@ Material and elevation are independent: combine freely.
|
|||
| `.nv-body` | body copy | 15px |
|
||||
| `.nv-label` | label, UI text | 13px |
|
||||
| `.nv-caption` | caption, secondary text | 12px |
|
||||
| `.nv-overline` | overline, eyebrow, kicker | 12px uppercase |
|
||||
| `.nv-overline` | overline, eyebrow, engraved label | 11px mono, uppercase, wide tracking |
|
||||
| `.nv-code` | code, monospace | 13px |
|
||||
| `.nv-text-primary` `.nv-text-secondary` `.nv-text-tertiary` | text color roles | Emphasis, not size. |
|
||||
| `.nv-text-accent` `.nv-text-success` `.nv-text-warning` `.nv-text-danger` | semantic text colors | |
|
||||
| `.nv-text-lit` | gradient text, metallic text | Lit from above, settling into the accent. One display line per page. |
|
||||
| `.nv-text-lit--accent` | accent gradient text | The accent alone, lit from above — for a phrase inside a display line. |
|
||||
|
||||
## Reveal
|
||||
|
||||
| Class | Known as | Does |
|
||||
| --- | --- | --- |
|
||||
| `.nv-reveal` | scroll reveal, animate on scroll | Rises and fades in once, as it scrolls into view. Needs `niveus.js`; without it, or under reduced motion, nothing is hidden. |
|
||||
| `.nv-reveal-group` | staggered reveal | Its children arrive one after another, `--nv-stagger` apart. |
|
||||
|
||||
## Layout utilities
|
||||
|
||||
|
|
@ -243,7 +350,8 @@ Material and elevation are independent: combine freely.
|
|||
| `.nv-truncate` | ellipsis, text truncation | One line with an ellipsis. |
|
||||
| `.nv-prose` | prose, measure | Readable line length and leading. |
|
||||
| `.nv-visually-hidden` | screen-reader-only, sr-only | Available to assistive tech, invisible on screen. |
|
||||
| `.nv-continuous` | cross-fading region | Content that should cross-fade in place when it changes. |
|
||||
| `.nv-continuous` | content region | Content that hands over in place (out, then in) when it changes. |
|
||||
| `.nv-content` | view-transition class | Not an HTML class: the `view-transition-class` that gives a named element the hand-over animation. Set `view-transition-class: nv-content` on your own named element to get it. |
|
||||
| `.nv-carried` | (view-transition class) | Applied to nav items so they ride above a travelling marker. |
|
||||
| `.nv-travelling` | (view-transition class) | Applied to a selection marker so it travels below the labels. |
|
||||
|
||||
|
|
@ -274,9 +382,14 @@ danger danger-hover danger-active danger-subtle danger-border
|
|||
danger-text on-danger
|
||||
|
||||
shadow-ambient shadow-penumbra shadow-umbra shadow-inset
|
||||
highlight highlight-strong elevation-tint-1 … -5
|
||||
highlight highlight-strong lowlight outline elevation-tint-1 … -5
|
||||
sheen-top sheen-bottom sheen-strong-top sheen-strong-bottom
|
||||
knob knob-shade
|
||||
material-frosted material-glass material-border
|
||||
window-close window-minimize window-zoom window-inactive
|
||||
window-control-ring window-glyph
|
||||
focus-ring focus-ring-contrast selection
|
||||
glow
|
||||
```
|
||||
|
||||
`on-*` is the text that goes on top of that color, and is derived — never set
|
||||
|
|
@ -292,8 +405,9 @@ it by hand.
|
|||
| `--nv-contrast-pivot` | `0.7` | Lightness where a color can no longer carry a label. |
|
||||
| `--nv-contrast-dark` / `-light` | `0.53` / `0.78` | How far either side a color is pushed. |
|
||||
|
||||
`--nv-palette-{neutral|accent|success|warning|danger}-{0…1000}` are raw values.
|
||||
Components must never reference them.
|
||||
`--nv-palette-{neutral|accent|success|warning|danger}-{0…1000}` and
|
||||
`--nv-palette-window-{close|minimize|zoom}` are raw values. Components must
|
||||
never reference them.
|
||||
|
||||
### Scales
|
||||
|
||||
|
|
@ -304,24 +418,28 @@ Components must never reference them.
|
|||
| `--nv-font-size-` | `2xs xs sm md lg xl 2xl 3xl 4xl 5xl` |
|
||||
| `--nv-font-weight-` | `regular medium semibold bold` |
|
||||
| `--nv-line-height-` | `tight snug normal relaxed` |
|
||||
| `--nv-letter-spacing-` | `tighter tight normal wide wider` |
|
||||
| `--nv-font-` | `display heading title body label caption code` (composite shorthands) |
|
||||
| `--nv-font-family-` | `sans mono` |
|
||||
| `--nv-letter-spacing-` | `tighter tight normal wide wider engraved` |
|
||||
| `--nv-font-` | `display heading title body label caption code engraved` (composite shorthands) |
|
||||
| `--nv-font-family-` | `sans` (Instrument Sans) `mono` (JetBrains Mono), both shipped in `fonts/` |
|
||||
| `--nv-measure` | `-narrow` · `--nv-measure` · `-wide` |
|
||||
| `--nv-control-height-` | `sm md lg` — also `control-padding-` and `control-gap-` |
|
||||
| `--nv-elevation-` | `0 1 2 3 4 5`, `inset-sm` `inset-md` `inset-lg` |
|
||||
| `--nv-highlight-` | `top` `top-strong` `edge` |
|
||||
| bevel | `--nv-bevel` (lit top + shaded bottom) · `--nv-lowlight-bottom` · `--nv-outline` (contact line) · `--nv-ledge` (lit lower lip of a recess) |
|
||||
| `--nv-sheen-` | `raised filled surface knob` — gradients laid over a role color as `background-image` |
|
||||
| `--nv-blur-` | `sm md lg xl` — also `backdrop-frosted` `backdrop-glass` `backdrop-scrim` |
|
||||
| `--nv-material-` | `{solid,elevated,inset,frosted,glass}-{bg,border,shadow}` |
|
||||
| `--nv-glow-` | `sm md lg` (accent halos) · `blur-sm/md/lg` `spread-sm/md/lg` (geometry for a tinted glow) · `strength` (how much tint a glow carries) |
|
||||
|
||||
### Motion
|
||||
|
||||
| Family | Steps |
|
||||
| --- | --- |
|
||||
| `--nv-duration-` | `instant fast quick normal slow slower travel expand theme` |
|
||||
| `--nv-duration-` | `instant fast quick normal slow slower travel expand theme shimmer` |
|
||||
| `--nv-ease-` | `standard entrance exit spring emphasized linear` |
|
||||
| `--nv-transition-` | `colors depth control surface expand` (composites) |
|
||||
| displacement | `--nv-lift-hover` `--nv-press-depth` `--nv-travel-sm/md/lg` |
|
||||
| sequence | `--nv-stagger` — the gap between siblings arriving one after another |
|
||||
|
||||
`prefers-reduced-motion` collapses every duration and displacement at the token
|
||||
level. Components never opt in individually.
|
||||
|
|
@ -332,7 +450,9 @@ level. Components never opt in individually.
|
|||
`--nv-border-width-strong` · `--nv-target-min` · `--nv-layout-gutter` ·
|
||||
`--nv-layout-max-width` · `--nv-selection-name` · `--nv-anchor` ·
|
||||
`--nv-button-offset` · `--nv-bed-tint` · `--nv-scroller-item` ·
|
||||
`--nv-scroller-fade` · `--nv-content-name`
|
||||
`--nv-scroller-fade` · `--nv-content-name` · `--nv-card-tint` ·
|
||||
`--nv-tile-tint` · `--nv-avatar-tint` · `--nv-pointer-x` / `--nv-pointer-y`
|
||||
(set by `niveus.js`) · `--nv-reveal-index` (set by `niveus.js`)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -343,14 +463,14 @@ everything still renders — state changes simply arrive instead of travelling.
|
|||
|
||||
| Function | Signature | Does |
|
||||
| --- | --- | --- |
|
||||
| `nv.transition` | `(update, { direction, kind }) => Promise` | Runs `update` inside a view transition so the change can be followed. `direction` is `"forward"`/`"backward"`; `kind: "theme"` slows the cross-fade. Falls back to running `update` directly under reduced motion or without API support. |
|
||||
| `nv.transition` | `(update, { direction, kind, scope }) => Promise` | Runs `update` inside a view transition so the change can be followed. `direction` is `"forward"`/`"backward"`; `kind: "theme"` slows the cross-fade; `scope` is the selection group (or list of groups) whose marker should travel — pass the group whose `aria-current`/`aria-selected` the update moves. Without `scope` nothing is lifted and the change cross-fades in place. Falls back to running `update` directly under reduced motion or without API support. |
|
||||
| `nv.setTheme` | `("light" \| "dark" \| "system") => Promise` | Cross-fades the whole surface into the new appearance and fires `nv:theme` on `<html>`. |
|
||||
| `nv.syncThemeControls` | `(theme) => void` | Sets `aria-pressed` on every `[data-nv-theme]`. |
|
||||
| `nv.openDialog` | `(dialog \| id, trigger?) => dialog` | `showModal()`, plus a transform origin so the dialog expands out of its trigger. |
|
||||
| `nv.selectTab` | `(tablist, tab) => Promise` | Moves selection and its panel, with direction. |
|
||||
| `nv.initScroller` | `(frame) => void` | Wires one scroller's controls. |
|
||||
| `nv.liftLabels` | `(scope) => void` | Names navigation items so a travelling marker passes behind them. |
|
||||
| `nv.wire` | `(scope = document) => void` | Applies all of the below. Runs once on load; call again after injecting markup. |
|
||||
| `nv.liftLabels` | `(scope) => Element[]` | Names the navigation items in `scope` so a travelling marker passes behind them, and returns them. `nv.transition` does this for its `scope` and removes the names afterwards. |
|
||||
| `nv.wire` | `(scope = document) => void` | Applies all of the below, and starts watching `.nv-reveal` / `.nv-reveal-group`. Runs once on load; call again after injecting markup. |
|
||||
| `nv.prefersReducedMotion` | `() => boolean` | |
|
||||
|
||||
### Data attributes
|
||||
|
|
@ -367,19 +487,22 @@ everything still renders — state changes simply arrive instead of travelling.
|
|||
| `data-scrolled` | `.nv-nav` | Set by the application on scroll; gives the bar its shadow. |
|
||||
| `data-nv-direction` | `<html>` | Set during a transition by `nv.transition`. |
|
||||
| `data-nv-transition` | `<html>` | `"theme"` during an appearance change. |
|
||||
| `data-nv-reveal` | revealed element | `pending` while waiting to scroll into view, `shown` while arriving; removed once it has. Set by `niveus.js`. |
|
||||
| `data-nv-travelling` | selection group | Set by `nv.transition` on each `scope` for the length of the change; names its marker. |
|
||||
|
||||
---
|
||||
|
||||
## Rules that bite
|
||||
|
||||
1. **`view-transition-name` must be unique per document.** A duplicate makes
|
||||
the browser skip the transition entirely. Each selection group needs its own
|
||||
`--nv-selection-name`; `.nv-nav-tabs` defaults to `nv-tabs-selection` and
|
||||
everything else to `nv-selection`, which is enough for one of each per page.
|
||||
the browser skip the transition entirely. Markers are only named while their
|
||||
group is in `scope`, so this only bites when one transition moves two groups
|
||||
at once: give each its own `--nv-selection-name`. `.nv-nav-tabs` defaults
|
||||
to `nv-tabs-selection` and everything else to `nv-selection`.
|
||||
2. **A menu and its trigger must name the same anchor** through `--nv-anchor`,
|
||||
and the value must be unique per pair.
|
||||
3. **Only one `.nv-tab-panel` may be visible at a time** — hide the rest with
|
||||
`hidden`. They share one content name.
|
||||
3. **Hide inactive `.nv-tab-panel`s with `hidden`.** `nv.selectTab` relies
|
||||
on it to know which panel is leaving.
|
||||
4. **Never reference `--nv-palette-*` from application code.** It is raw, and
|
||||
does not adapt to the appearance.
|
||||
5. **A floating control positioned with `translate`** needs
|
||||
|
|
|
|||
4950
docs/Website/niveus/dist/niveus.css
vendored
Normal file
4950
docs/Website/niveus/dist/niveus.css
vendored
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -404,7 +404,7 @@
|
|||
.forEach((el) => el.removeAttribute("aria-current"));
|
||||
item.setAttribute("aria-current", "page");
|
||||
render(item.dataset.thread);
|
||||
});
|
||||
}, { scope: list });
|
||||
scroller.scrollTop = 0;
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -454,7 +454,7 @@
|
|||
nv.transition(() => {
|
||||
nav.querySelectorAll("[aria-current]").forEach((el) => el.removeAttribute("aria-current"));
|
||||
item.setAttribute("aria-current", "page");
|
||||
});
|
||||
}, { scope: nav });
|
||||
});
|
||||
|
||||
/* The bar earns its shadow only once content has passed under it. */
|
||||
|
|
|
|||
|
|
@ -560,7 +560,7 @@
|
|||
nv.transition(() => {
|
||||
sidebar.querySelectorAll("[aria-current]").forEach((el) => el.removeAttribute("aria-current"));
|
||||
item.setAttribute("aria-current", "page");
|
||||
});
|
||||
}, { scope: sidebar });
|
||||
});
|
||||
|
||||
document.querySelectorAll("[data-range]").forEach((option) => {
|
||||
|
|
|
|||
BIN
docs/Website/niveus/fonts/InstrumentSans-Italic.woff2
Normal file
BIN
docs/Website/niveus/fonts/InstrumentSans-Italic.woff2
Normal file
Binary file not shown.
BIN
docs/Website/niveus/fonts/InstrumentSans.woff2
Normal file
BIN
docs/Website/niveus/fonts/InstrumentSans.woff2
Normal file
Binary file not shown.
BIN
docs/Website/niveus/fonts/JetBrainsMono.woff2
Normal file
BIN
docs/Website/niveus/fonts/JetBrainsMono.woff2
Normal file
Binary file not shown.
93
docs/Website/niveus/fonts/OFL-InstrumentSans.txt
Normal file
93
docs/Website/niveus/fonts/OFL-InstrumentSans.txt
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
Copyright 2022 The Instrument Sans Project Authors (https://github.com/Instrument/instrument-sans)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
93
docs/Website/niveus/fonts/OFL-JetBrainsMono.txt
Normal file
93
docs/Website/niveus/fonts/OFL-JetBrainsMono.txt
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at: https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
BIN
docs/Website/niveus/fonts/static/InstrumentSans-Bold.ttf
Normal file
BIN
docs/Website/niveus/fonts/static/InstrumentSans-Bold.ttf
Normal file
Binary file not shown.
BIN
docs/Website/niveus/fonts/static/InstrumentSans-BoldItalic.ttf
Normal file
BIN
docs/Website/niveus/fonts/static/InstrumentSans-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
docs/Website/niveus/fonts/static/InstrumentSans-Italic.ttf
Normal file
BIN
docs/Website/niveus/fonts/static/InstrumentSans-Italic.ttf
Normal file
Binary file not shown.
BIN
docs/Website/niveus/fonts/static/InstrumentSans-Medium.ttf
Normal file
BIN
docs/Website/niveus/fonts/static/InstrumentSans-Medium.ttf
Normal file
Binary file not shown.
BIN
docs/Website/niveus/fonts/static/InstrumentSans-MediumItalic.ttf
Normal file
BIN
docs/Website/niveus/fonts/static/InstrumentSans-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
docs/Website/niveus/fonts/static/InstrumentSans-Regular.ttf
Normal file
BIN
docs/Website/niveus/fonts/static/InstrumentSans-Regular.ttf
Normal file
Binary file not shown.
BIN
docs/Website/niveus/fonts/static/InstrumentSans-SemiBold.ttf
Normal file
BIN
docs/Website/niveus/fonts/static/InstrumentSans-SemiBold.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
docs/Website/niveus/fonts/static/JetBrainsMono-Bold.ttf
Normal file
BIN
docs/Website/niveus/fonts/static/JetBrainsMono-Bold.ttf
Normal file
Binary file not shown.
BIN
docs/Website/niveus/fonts/static/JetBrainsMono-Medium.ttf
Normal file
BIN
docs/Website/niveus/fonts/static/JetBrainsMono-Medium.ttf
Normal file
Binary file not shown.
BIN
docs/Website/niveus/fonts/static/JetBrainsMono-Regular.ttf
Normal file
BIN
docs/Website/niveus/fonts/static/JetBrainsMono-Regular.ttf
Normal file
Binary file not shown.
BIN
docs/Website/niveus/fonts/static/JetBrainsMono-SemiBold.ttf
Normal file
BIN
docs/Website/niveus/fonts/static/JetBrainsMono-SemiBold.ttf
Normal file
Binary file not shown.
|
|
@ -240,12 +240,14 @@
|
|||
|
||||
<header class="hero">
|
||||
<p class="nv-overline">Design system</p>
|
||||
<h1 class="nv-display">Surfaces that feel understandable.</h1>
|
||||
<h1 class="nv-display">Surfaces you could press.</h1>
|
||||
<p class="nv-prose">
|
||||
Niveus is a calm, adaptive interface language built on clarity, depth,
|
||||
material and consistency. The same components carry their hierarchy from
|
||||
light to dark, because nothing below is defined in terms of an
|
||||
appearance — only in terms of a role.
|
||||
Niveus is a tactile, adaptive interface language. Controls are bevelled
|
||||
keys with light falling across them, fields are wells cut into the
|
||||
surface, and glass is kept for the chrome that floats over everything
|
||||
else. The same components carry their hierarchy from light to dark,
|
||||
because nothing below is defined in terms of an appearance — only in
|
||||
terms of a role.
|
||||
</p>
|
||||
<div class="demo-row">
|
||||
<button class="nv-button nv-button--primary nv-button--lg" data-nv-dialog="welcome">
|
||||
|
|
@ -513,7 +515,7 @@
|
|||
</div>
|
||||
<div class="type-row">
|
||||
<span class="nv-overline">Overline</span>
|
||||
<span class="type-row__meta">--nv-font-caption · uppercase</span>
|
||||
<span class="type-row__meta">--nv-font-engraved · mono, uppercase</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -762,6 +764,58 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3 class="nv-title" style="margin-block-end: var(--nv-space-4)">Window</h3>
|
||||
<div class="nv-bed nv-bed--quiet" style="display: grid; gap: var(--nv-space-6); grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr)); align-items: start">
|
||||
<div class="nv-window">
|
||||
<div class="nv-window__titlebar">
|
||||
<div class="nv-window__controls">
|
||||
<button class="nv-window__control nv-window__control--close" aria-label="Close"></button>
|
||||
<button class="nv-window__control nv-window__control--minimize" aria-label="Minimize"></button>
|
||||
<button class="nv-window__control nv-window__control--zoom" aria-label="Zoom"></button>
|
||||
</div>
|
||||
<span class="nv-window__title">Appearance</span>
|
||||
<div class="nv-window__actions">
|
||||
<button class="nv-button nv-button--ghost nv-button--sm nv-button--icon" aria-label="Search">
|
||||
<svg class="nv-button__icon" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="11" cy="11" r="6.5" stroke="currentColor" stroke-width="2"/><path d="m20 20-4.2-4.2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nv-window__body nv-stack" style="padding: var(--nv-space-5)">
|
||||
<p class="nv-overline">Style</p>
|
||||
<div class="nv-nav-tabs" role="tablist" aria-label="Appearance">
|
||||
<button class="nv-nav__item" role="tab" aria-selected="false">Light</button>
|
||||
<button class="nv-nav__item" role="tab" aria-selected="true">Dark</button>
|
||||
<button class="nv-nav__item" role="tab" aria-selected="false">System</button>
|
||||
</div>
|
||||
<label class="nv-choice"><input class="nv-switch" type="checkbox" checked> Translucent chrome</label>
|
||||
<label class="nv-choice"><input class="nv-switch" type="checkbox"> Reduce motion</label>
|
||||
<div class="nv-field">
|
||||
<label class="nv-field__label" for="win-wallpaper">Wallpaper</label>
|
||||
<input class="nv-input" id="win-wallpaper" value="~/Pictures/dusk-ridge.jpg">
|
||||
</div>
|
||||
<div class="nv-cluster" style="justify-content: flex-end">
|
||||
<button class="nv-button">Reset</button>
|
||||
<button class="nv-button nv-button--primary">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nv-window nv-window--inactive">
|
||||
<div class="nv-window__titlebar">
|
||||
<div class="nv-window__controls">
|
||||
<button class="nv-window__control nv-window__control--close" aria-label="Close"></button>
|
||||
<button class="nv-window__control nv-window__control--minimize" aria-label="Minimize"></button>
|
||||
<button class="nv-window__control nv-window__control--zoom" aria-label="Zoom"></button>
|
||||
</div>
|
||||
<span class="nv-window__title">Inactive — hover the lights</span>
|
||||
</div>
|
||||
<div class="nv-window__body" style="padding: var(--nv-space-5)">
|
||||
<p class="nv-caption">A window without focus greys its lights and steps back one elevation. The lights take their color back the moment the pointer reaches them.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3 class="nv-title" style="margin-block-end: var(--nv-space-4)">Dialog</h3>
|
||||
<div class="demo-row">
|
||||
|
|
@ -1315,7 +1369,7 @@
|
|||
previous.removeAttribute("aria-current");
|
||||
});
|
||||
item.setAttribute("aria-current", "page");
|
||||
});
|
||||
}, { scope: group });
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,14 @@ body {
|
|||
margin: 0;
|
||||
min-height: 100svh;
|
||||
background-color: var(--nv-color-background);
|
||||
/* The canvas is lit, not painted: a wash of the accent falls in from the top
|
||||
corner and a fainter one pools at the opposite foot. It is what gives
|
||||
translucent surfaces something to be translucent against, and keeps a
|
||||
page of solid panels from reading as one flat field. */
|
||||
background-image:
|
||||
radial-gradient(120% 80% at 0% 0%, color-mix(in oklab, var(--nv-color-accent) light-dark(10%, 16%), transparent), transparent 60%),
|
||||
radial-gradient(90% 70% at 100% 100%, color-mix(in oklab, var(--nv-color-accent) light-dark(6%, 10%), transparent), transparent 60%);
|
||||
background-attachment: fixed;
|
||||
color: var(--nv-color-text-primary);
|
||||
font: var(--nv-font-body);
|
||||
letter-spacing: var(--nv-letter-spacing-normal);
|
||||
|
|
@ -175,10 +183,11 @@ textarea {
|
|||
color: var(--nv-color-text-secondary);
|
||||
}
|
||||
|
||||
/* Engraved: small mono capitals that mark a region the way markings mark an
|
||||
instrument panel. Not for anything that has to be read as a sentence. */
|
||||
.nv-overline {
|
||||
font: var(--nv-font-caption);
|
||||
font-weight: var(--nv-font-weight-semibold);
|
||||
letter-spacing: var(--nv-letter-spacing-wider);
|
||||
font: var(--nv-font-engraved);
|
||||
letter-spacing: var(--nv-letter-spacing-engraved);
|
||||
text-transform: uppercase;
|
||||
color: var(--nv-color-text-tertiary);
|
||||
}
|
||||
|
|
@ -195,3 +204,32 @@ textarea {
|
|||
.nv-text-success { color: var(--nv-color-success-text); }
|
||||
.nv-text-warning { color: var(--nv-color-warning-text); }
|
||||
.nv-text-danger { color: var(--nv-color-danger-text); }
|
||||
|
||||
/* Lit text: type cut from the same material as the surfaces — bright along
|
||||
the top, warming towards the accent below, the way light falls across a
|
||||
raised face. Each line is lit on its own. For one display line on a page,
|
||||
not for running text. */
|
||||
.nv-text-lit {
|
||||
background-image: linear-gradient(
|
||||
180deg,
|
||||
var(--nv-color-text-primary) 30%,
|
||||
color-mix(in oklab, var(--nv-color-accent-text) 30%, var(--nv-color-text-primary))
|
||||
);
|
||||
-webkit-box-decoration-break: clone;
|
||||
box-decoration-break: clone;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
color: var(--nv-color-accent-text);
|
||||
}
|
||||
|
||||
/* The accent alone, lit from above: for an accented phrase inside a display
|
||||
line, where flat accent text would read as a link. */
|
||||
.nv-text-lit--accent {
|
||||
background-image: linear-gradient(
|
||||
180deg,
|
||||
color-mix(in oklab, var(--nv-color-accent-text) 55%, var(--nv-color-text-primary)),
|
||||
var(--nv-color-accent-text) 60%,
|
||||
var(--nv-color-accent)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
113
docs/Website/niveus/niveus/components/avatar.css
Normal file
113
docs/Website/niveus/niveus/components/avatar.css
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
/* ==========================================================================
|
||||
Niveus — Avatar
|
||||
--------------------------------------------------------------------------
|
||||
<span class="nv-avatar"><img src="…" alt=""></span>
|
||||
<span class="nv-avatar">NX</span>
|
||||
|
||||
A person, as a round raised disc: an image when there is one, initials when
|
||||
there is not. The disc is lit like a knob — highlight above, shade below —
|
||||
so a face reads as something set into the interface, not pasted onto it.
|
||||
|
||||
.nv-avatar--ring a ring in the tint, set off by a gap of surface
|
||||
.nv-avatar--glow the ring gives off its tint
|
||||
.nv-avatar--sm / --lg / --xl
|
||||
.nv-avatar__status presence dot; --online lights it
|
||||
.nv-avatar-group overlapping row of avatars
|
||||
|
||||
--nv-avatar-tint picks the ring color; the accent unless set.
|
||||
========================================================================== */
|
||||
|
||||
.nv-avatar {
|
||||
--nv-avatar-tint: var(--nv-color-accent);
|
||||
--_size: 2.5rem;
|
||||
--_ring: 0 0 transparent;
|
||||
--_glow: 0 0 transparent;
|
||||
|
||||
position: relative;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
flex: none;
|
||||
inline-size: var(--_size);
|
||||
block-size: var(--_size);
|
||||
|
||||
background-color: var(--nv-color-surface-elevated);
|
||||
background-image: var(--nv-sheen-raised);
|
||||
color: var(--nv-color-text-secondary);
|
||||
font: var(--nv-font-label);
|
||||
font-weight: var(--nv-font-weight-semibold);
|
||||
border-radius: var(--nv-radius-full);
|
||||
box-shadow: var(--_ring), var(--nv-bevel), var(--nv-outline), var(--nv-elevation-1), var(--_glow);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.nv-avatar > img {
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
border-radius: inherit;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* The gap is drawn in the surface color, so the ring reads as a separate band
|
||||
sitting around the disc rather than as a thick border painted onto it. */
|
||||
.nv-avatar--ring,
|
||||
.nv-avatar--glow {
|
||||
--_ring:
|
||||
0 0 0 2px var(--nv-color-surface),
|
||||
0 0 0 4px var(--nv-avatar-tint);
|
||||
}
|
||||
|
||||
.nv-avatar--glow {
|
||||
--_glow: 0 0 var(--nv-glow-blur-md) var(--nv-glow-spread-sm)
|
||||
color-mix(in oklab, var(--nv-avatar-tint) var(--nv-glow-strength), transparent);
|
||||
}
|
||||
|
||||
.nv-avatar--sm {
|
||||
--_size: 1.75rem;
|
||||
font: var(--nv-font-caption);
|
||||
}
|
||||
|
||||
.nv-avatar--lg {
|
||||
--_size: 4rem;
|
||||
font: var(--nv-font-title);
|
||||
}
|
||||
|
||||
.nv-avatar--xl {
|
||||
--_size: 6rem;
|
||||
font: var(--nv-font-heading);
|
||||
}
|
||||
|
||||
/* --- Status ------------------------------------------------------------------ */
|
||||
|
||||
.nv-avatar__status {
|
||||
position: absolute;
|
||||
inset-block-end: 4%;
|
||||
inset-inline-end: 4%;
|
||||
inline-size: max(0.625rem, 24%);
|
||||
block-size: max(0.625rem, 24%);
|
||||
border-radius: var(--nv-radius-full);
|
||||
background-color: var(--nv-color-text-tertiary);
|
||||
box-shadow: 0 0 0 2px var(--nv-color-surface);
|
||||
}
|
||||
|
||||
.nv-avatar__status--online {
|
||||
background-color: var(--nv-color-success);
|
||||
box-shadow:
|
||||
0 0 0 2px var(--nv-color-surface),
|
||||
0 0 var(--nv-glow-blur-sm) var(--nv-glow-spread-sm)
|
||||
color-mix(in oklab, var(--nv-color-success) var(--nv-glow-strength), transparent);
|
||||
}
|
||||
|
||||
/* --- Group ----------------------------------------------------------------------- */
|
||||
|
||||
.nv-avatar-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nv-avatar-group > .nv-avatar {
|
||||
box-shadow: 0 0 0 2px var(--nv-color-surface), var(--nv-bevel), var(--nv-elevation-1);
|
||||
}
|
||||
|
||||
.nv-avatar-group > .nv-avatar + .nv-avatar {
|
||||
margin-inline-start: calc(var(--_size) * -0.3);
|
||||
}
|
||||
|
|
@ -76,6 +76,8 @@
|
|||
--_bg: var(--_solid-bg);
|
||||
--_fg: var(--_solid-fg);
|
||||
--_border: transparent;
|
||||
background-image: var(--nv-sheen-filled);
|
||||
box-shadow: var(--nv-highlight-top);
|
||||
}
|
||||
|
||||
.nv-badge--square {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@
|
|||
.nv-button--icon square, for a lone icon (needs a label)
|
||||
.nv-button--block fills its container
|
||||
|
||||
A button is a key: a bevelled face with light falling across it, a contact
|
||||
line where it meets the surface, and a press that sinks it into an inset.
|
||||
|
||||
Every variant is defined purely by reassigning the private --_* properties
|
||||
below. Interaction — the lift on hover, the sink on press, the focus ring —
|
||||
lives once in the base rule, so a new variant can never drift from the way
|
||||
|
|
@ -23,9 +26,10 @@
|
|||
--_bg-active: var(--nv-color-surface-active);
|
||||
--_fg: var(--nv-color-text-primary);
|
||||
--_border: var(--nv-color-border);
|
||||
--_shadow: var(--nv-highlight-top), var(--nv-elevation-2);
|
||||
--_shadow-hover: var(--nv-highlight-top), var(--nv-elevation-3);
|
||||
--_shadow-active: var(--nv-elevation-inset-sm);
|
||||
--_sheen: var(--nv-sheen-raised);
|
||||
--_shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-2);
|
||||
--_shadow-hover: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-3);
|
||||
--_shadow-active: var(--nv-elevation-inset-sm), var(--nv-outline);
|
||||
|
||||
/* Size, overridden per size modifier. */
|
||||
--_height: var(--nv-control-height-md);
|
||||
|
|
@ -46,6 +50,7 @@
|
|||
border-radius: var(--_radius);
|
||||
|
||||
background-color: var(--_bg);
|
||||
background-image: var(--_sheen);
|
||||
color: var(--_fg);
|
||||
box-shadow: var(--_shadow);
|
||||
|
||||
|
|
@ -83,8 +88,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* Pressed, the face is pushed below the light: the sheen goes and the inset
|
||||
takes over. */
|
||||
.nv-button:active:not(:disabled, [aria-disabled="true"]) {
|
||||
background-color: var(--_bg-active);
|
||||
background-image: none;
|
||||
box-shadow: var(--_shadow-active);
|
||||
translate: 0 var(--nv-press-depth);
|
||||
transition-duration: var(--nv-duration-instant);
|
||||
|
|
@ -98,6 +106,7 @@
|
|||
.nv-button:disabled,
|
||||
.nv-button[aria-disabled="true"] {
|
||||
background-color: var(--nv-color-surface-inset);
|
||||
background-image: none;
|
||||
color: var(--nv-color-text-disabled);
|
||||
border-color: var(--nv-color-border-subtle);
|
||||
box-shadow: none;
|
||||
|
|
@ -113,6 +122,13 @@
|
|||
--_bg-active: var(--nv-color-accent-active);
|
||||
--_fg: var(--nv-color-on-accent);
|
||||
--_border: transparent;
|
||||
--_sheen: var(--nv-sheen-filled);
|
||||
/* A filled key gives off a little of its own color: the glow is what makes
|
||||
it read as lit rather than printed. */
|
||||
--_shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-2),
|
||||
0 6px 16px -8px color-mix(in oklab, var(--_bg) 80%, transparent);
|
||||
--_shadow-hover: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-3),
|
||||
0 8px 22px -8px color-mix(in oklab, var(--_bg) 85%, transparent);
|
||||
font-weight: var(--nv-font-weight-semibold);
|
||||
}
|
||||
|
||||
|
|
@ -122,6 +138,11 @@
|
|||
--_bg-active: var(--nv-color-danger-active);
|
||||
--_fg: var(--nv-color-on-danger);
|
||||
--_border: transparent;
|
||||
--_sheen: var(--nv-sheen-filled);
|
||||
--_shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-2),
|
||||
0 6px 16px -8px color-mix(in oklab, var(--_bg) 80%, transparent);
|
||||
--_shadow-hover: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-3),
|
||||
0 8px 22px -8px color-mix(in oklab, var(--_bg) 85%, transparent);
|
||||
--nv-color-focus-ring: var(--nv-color-danger-border);
|
||||
font-weight: var(--nv-font-weight-semibold);
|
||||
}
|
||||
|
|
@ -132,11 +153,20 @@
|
|||
--_bg-active: var(--nv-color-surface-active);
|
||||
--_fg: var(--nv-color-text-secondary);
|
||||
--_border: transparent;
|
||||
--_sheen: none;
|
||||
--_shadow: none;
|
||||
--_shadow-hover: none;
|
||||
--_shadow-active: var(--nv-elevation-inset-sm);
|
||||
}
|
||||
|
||||
/* A ghost has no material to lose, so disabling it only dims the label —
|
||||
it does not suddenly grow a well its enabled siblings never had. */
|
||||
.nv-button--ghost:disabled,
|
||||
.nv-button--ghost[aria-disabled="true"] {
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.nv-button--ghost:hover:not(:disabled, [aria-disabled="true"]) {
|
||||
color: var(--nv-color-text-primary);
|
||||
|
|
@ -150,6 +180,7 @@
|
|||
--_bg-active: var(--nv-color-accent-subtle-hover);
|
||||
--_fg: var(--nv-color-accent-text);
|
||||
--_border: var(--nv-color-accent-border);
|
||||
--_sheen: none;
|
||||
--_shadow: none;
|
||||
--_shadow-hover: var(--nv-elevation-1);
|
||||
}
|
||||
|
|
@ -159,6 +190,7 @@
|
|||
--_bg-hover: var(--nv-material-frosted-bg);
|
||||
--_bg-active: var(--nv-material-frosted-bg);
|
||||
--_border: var(--nv-material-glass-border);
|
||||
--_sheen: none;
|
||||
backdrop-filter: var(--nv-backdrop-glass);
|
||||
-webkit-backdrop-filter: var(--nv-backdrop-glass);
|
||||
}
|
||||
|
|
@ -205,9 +237,9 @@
|
|||
through every state. */
|
||||
|
||||
.nv-button--float {
|
||||
--_shadow: var(--nv-highlight-top), var(--nv-elevation-3);
|
||||
--_shadow-hover: var(--nv-highlight-top), var(--nv-elevation-4);
|
||||
--_shadow-active: var(--nv-highlight-top), var(--nv-elevation-2);
|
||||
--_shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-3);
|
||||
--_shadow-hover: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-4);
|
||||
--_shadow-active: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-2);
|
||||
|
||||
translate: var(--nv-button-offset, 0 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@
|
|||
.nv-card--inset recessed into its parent
|
||||
.nv-card--frosted over imagery or scrolling content
|
||||
.nv-card--interactive the whole card is one target
|
||||
.nv-card--lit lit along its top edge in its tint
|
||||
.nv-card--glow gives off its tint — the one card that is on
|
||||
.nv-card--spotlight catches a light that follows the pointer
|
||||
.nv-card__header / __body / __footer / __media
|
||||
|
||||
A card is a container, not a look. If a card needs a shadow that is not on
|
||||
|
|
@ -14,10 +17,20 @@
|
|||
========================================================================== */
|
||||
|
||||
.nv-card {
|
||||
--_bg: var(--nv-color-surface);
|
||||
/* Content surfaces are mostly solid: enough of the canvas comes through to
|
||||
tie a card to the light around it, never enough to compete with what it
|
||||
holds. The blur keeps anything busy behind it (a bed, an image) from
|
||||
reading through as texture. */
|
||||
--_bg: color-mix(in oklab, var(--nv-color-surface) 90%, transparent);
|
||||
--_border: var(--nv-color-border-subtle);
|
||||
--_shadow: var(--nv-highlight-top), var(--nv-elevation-1);
|
||||
--_shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-1);
|
||||
--_sheen: var(--nv-sheen-surface);
|
||||
--_padding: var(--nv-space-5);
|
||||
/* Light layers the variants switch on. Kept separate from --_shadow so a
|
||||
lit or glowing card can still be elevated, floating or interactive. */
|
||||
--_lit: 0 0 transparent;
|
||||
--_glow: 0 0 transparent;
|
||||
--_spot: linear-gradient(transparent, transparent);
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
|
@ -25,10 +38,13 @@
|
|||
min-inline-size: 0;
|
||||
|
||||
background-color: var(--_bg);
|
||||
background-image: var(--_spot), var(--_sheen);
|
||||
color: var(--nv-color-text-primary);
|
||||
border: var(--nv-border-width) solid var(--_border);
|
||||
border-radius: var(--nv-radius-lg);
|
||||
box-shadow: var(--_shadow);
|
||||
box-shadow: var(--_lit), var(--_shadow), var(--_glow);
|
||||
backdrop-filter: var(--nv-backdrop-glass);
|
||||
-webkit-backdrop-filter: var(--nv-backdrop-glass);
|
||||
|
||||
/* Children are clipped to the corner radius so media and inset regions
|
||||
cannot square off the card. */
|
||||
|
|
@ -37,18 +53,19 @@
|
|||
}
|
||||
|
||||
.nv-card--elevated {
|
||||
--_bg: var(--nv-color-surface-elevated);
|
||||
--_shadow: var(--nv-highlight-top), var(--nv-elevation-2);
|
||||
--_bg: color-mix(in oklab, var(--nv-color-surface-elevated) 90%, transparent);
|
||||
--_shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-2);
|
||||
}
|
||||
|
||||
.nv-card--floating {
|
||||
--_bg: var(--nv-color-surface-elevated);
|
||||
--_shadow: var(--nv-highlight-top), var(--nv-elevation-3);
|
||||
--_bg: color-mix(in oklab, var(--nv-color-surface-elevated) 88%, transparent);
|
||||
--_shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-3);
|
||||
}
|
||||
|
||||
.nv-card--inset {
|
||||
--_bg: var(--nv-color-surface-inset);
|
||||
--_shadow: var(--nv-elevation-inset-md);
|
||||
--_bg: color-mix(in oklab, var(--nv-color-surface-inset) 85%, transparent);
|
||||
--_shadow: var(--nv-elevation-inset-md), var(--nv-ledge);
|
||||
--_sheen: none;
|
||||
--_border: var(--nv-color-border-subtle);
|
||||
}
|
||||
|
||||
|
|
@ -56,6 +73,7 @@
|
|||
--_bg: var(--nv-material-frosted-bg);
|
||||
--_border: var(--nv-material-frosted-border);
|
||||
--_shadow: var(--nv-material-frosted-shadow);
|
||||
--_sheen: none;
|
||||
backdrop-filter: var(--nv-backdrop-frosted);
|
||||
-webkit-backdrop-filter: var(--nv-backdrop-frosted);
|
||||
}
|
||||
|
|
@ -76,20 +94,75 @@
|
|||
|
||||
@media (hover: hover) {
|
||||
.nv-card--interactive:hover {
|
||||
box-shadow: var(--nv-highlight-top), var(--nv-elevation-3);
|
||||
box-shadow: var(--_lit), var(--nv-bevel), var(--nv-outline), var(--nv-elevation-3), var(--_glow);
|
||||
translate: 0 calc(var(--nv-lift-hover) * 2);
|
||||
}
|
||||
}
|
||||
|
||||
.nv-card--interactive:active {
|
||||
box-shadow: var(--nv-highlight-top), var(--nv-elevation-1);
|
||||
box-shadow: var(--_lit), var(--nv-bevel), var(--nv-outline), var(--nv-elevation-1), var(--_glow);
|
||||
translate: 0 var(--nv-press-depth);
|
||||
transition-duration: var(--nv-duration-instant);
|
||||
}
|
||||
|
||||
.nv-card--interactive:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--_shadow), var(--nv-focus-ring);
|
||||
box-shadow: var(--_lit), var(--_shadow), var(--_glow), var(--nv-focus-ring);
|
||||
}
|
||||
|
||||
/* --- Light ------------------------------------------------------------------
|
||||
Three ways a card can carry light, each stated once and combinable with
|
||||
every elevation above. They all take the card's tint — the accent unless
|
||||
--nv-card-tint names another color, the way --nv-bed-tint does for a bed.
|
||||
|
||||
Lit: a strip of light along the top edge, spilling a little way down
|
||||
the face. For a card that belongs to something with a color: a
|
||||
project, a status, a category.
|
||||
Glow: the card gives off its tint. For the one card that is on — the
|
||||
current plan, the live item. Two glowing cards side by side
|
||||
cancel each other out.
|
||||
Spotlight: a soft light follows the pointer across the face (niveus.js
|
||||
tracks it). A showcase effect; a list of work items does not
|
||||
need one. */
|
||||
|
||||
.nv-card--lit,
|
||||
.nv-card--glow,
|
||||
.nv-card--spotlight {
|
||||
--nv-card-tint: var(--nv-color-accent);
|
||||
}
|
||||
|
||||
.nv-card--lit {
|
||||
--_lit:
|
||||
inset 0 2px 0 color-mix(in oklab, var(--nv-card-tint) 85%, transparent),
|
||||
inset 0 18px 28px -22px color-mix(in oklab, var(--nv-card-tint) 70%, transparent);
|
||||
--_sheen:
|
||||
radial-gradient(
|
||||
120% 70% at 0% 0%,
|
||||
color-mix(in oklab, var(--nv-card-tint) 12%, transparent),
|
||||
transparent 70%
|
||||
),
|
||||
var(--nv-sheen-surface);
|
||||
}
|
||||
|
||||
.nv-card--glow {
|
||||
--_border: color-mix(in oklab, var(--nv-card-tint) 40%, var(--nv-color-border-subtle));
|
||||
--_glow: 0 0 var(--nv-glow-blur-md) var(--nv-glow-spread-md)
|
||||
color-mix(in oklab, var(--nv-card-tint) var(--nv-glow-strength), transparent);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.nv-card--glow.nv-card--interactive:hover {
|
||||
--_glow: 0 0 var(--nv-glow-blur-lg) var(--nv-glow-spread-md)
|
||||
color-mix(in oklab, var(--nv-card-tint) var(--nv-glow-strength), transparent);
|
||||
}
|
||||
|
||||
.nv-card--spotlight:hover {
|
||||
--_spot: radial-gradient(
|
||||
22rem circle at var(--nv-pointer-x, 50%) var(--nv-pointer-y, 0%),
|
||||
color-mix(in oklab, var(--nv-card-tint) 14%, var(--nv-color-sheen-top)),
|
||||
transparent 70%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* Cards whose whole area is clickable via a link inside: the link's target is
|
||||
|
|
|
|||
55
docs/Website/niveus/niveus/components/deck.css
Normal file
55
docs/Website/niveus/niveus/components/deck.css
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/* ==========================================================================
|
||||
Niveus — Deck
|
||||
--------------------------------------------------------------------------
|
||||
<div class="nv-deck">
|
||||
<article class="nv-card">…</article>
|
||||
<div class="nv-deck__layer" aria-hidden="true"></div>
|
||||
<div class="nv-deck__layer" aria-hidden="true"></div>
|
||||
</div>
|
||||
|
||||
One surface in front of others of its kind: the current item of a set that
|
||||
has more behind it. The layers are cards seen edge-on, stepping back and
|
||||
dimming with distance, so the set has depth without anything having to
|
||||
move. Use it where "there are more of these" is part of the message — a
|
||||
showcase of products, a stack of notifications, a carousel at rest.
|
||||
|
||||
Everything in the deck shares one grid cell. The front surface is any
|
||||
element; the layers are decorative and hidden from assistive technology.
|
||||
Panels that swap (an .nv-tab-panel set) can live in the deck together:
|
||||
the hidden ones take no space.
|
||||
========================================================================== */
|
||||
|
||||
.nv-deck {
|
||||
--_step: var(--nv-space-3);
|
||||
|
||||
display: grid;
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
/* Room above the front surface for the layers that peek out behind it. */
|
||||
padding-block-start: calc(var(--_step) * 2);
|
||||
}
|
||||
|
||||
.nv-deck > * {
|
||||
grid-area: 1 / 1;
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
.nv-deck__layer {
|
||||
--_depth: 1;
|
||||
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
background-color: color-mix(in oklab, var(--nv-color-surface) 90%, transparent);
|
||||
background-image: var(--nv-sheen-surface);
|
||||
border: var(--nv-border-width) solid var(--nv-color-border-subtle);
|
||||
border-radius: var(--nv-radius-lg);
|
||||
box-shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-1);
|
||||
translate: 0 calc(var(--_step) * var(--_depth) * -1);
|
||||
scale: calc(1 - var(--_depth) * 0.05) 1;
|
||||
opacity: calc(1 - var(--_depth) * 0.3);
|
||||
}
|
||||
|
||||
.nv-deck__layer + .nv-deck__layer {
|
||||
--_depth: 2;
|
||||
z-index: -2;
|
||||
}
|
||||
|
|
@ -23,11 +23,17 @@
|
|||
max-block-size: min(42rem, calc(100svh - var(--nv-space-12)));
|
||||
padding: 0;
|
||||
|
||||
background-color: var(--nv-color-surface-elevated);
|
||||
/* The dialog sits over a dimmed, blurred page, so it can afford to let a
|
||||
little of it through: it reads as a pane lifted off the page rather than
|
||||
a card pasted over it. */
|
||||
background-color: color-mix(in oklab, var(--nv-color-surface-elevated) 86%, transparent);
|
||||
background-image: var(--nv-sheen-surface);
|
||||
backdrop-filter: var(--nv-backdrop-frosted);
|
||||
-webkit-backdrop-filter: var(--nv-backdrop-frosted);
|
||||
color: var(--nv-color-text-primary);
|
||||
border: var(--nv-border-width) solid var(--nv-color-border-subtle);
|
||||
border-radius: var(--nv-radius-xl);
|
||||
box-shadow: var(--nv-highlight-top), var(--nv-elevation-4);
|
||||
box-shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-4);
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
|
|
|
|||
|
|
@ -20,9 +20,10 @@
|
|||
--_padding: var(--nv-space-4);
|
||||
|
||||
background-color: var(--nv-color-surface);
|
||||
background-image: var(--nv-sheen-surface);
|
||||
border: var(--nv-border-width) solid var(--nv-color-border-subtle);
|
||||
border-radius: var(--nv-radius-lg);
|
||||
box-shadow: var(--nv-highlight-top), var(--nv-elevation-1);
|
||||
box-shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-1);
|
||||
transition: var(--nv-transition-surface);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@
|
|||
.nv-checkbox / .nv-radio custom marks on the native controls
|
||||
.nv-switch binary control with an inset track
|
||||
|
||||
Inputs are the system's inset material: they read as recessed into the
|
||||
surface that holds them, which is what distinguishes a place to type from a
|
||||
thing to press. On focus the recess stays — only the ring and border change,
|
||||
Inputs are the system's inset material: they read as wells cut into the
|
||||
surface that holds them — shaded inside, lit along the lower lip — which is
|
||||
what distinguishes a place to type from a thing to press. On focus the recess stays — only the ring and border change,
|
||||
so the field never appears to jump forward.
|
||||
========================================================================== */
|
||||
|
||||
|
|
@ -59,12 +59,14 @@
|
|||
min-inline-size: 0;
|
||||
block-size: var(--_height);
|
||||
padding-inline: var(--_padding);
|
||||
|
||||
background-color: var(--nv-color-surface-inset);
|
||||
/* A well in dark, where recess reads as shade; paper in light, where a grey
|
||||
well reads as a field that has been switched off. The inset shadow and
|
||||
ledge carry the recess either way. */
|
||||
background-color: light-dark(var(--nv-color-surface), var(--nv-color-surface-inset));
|
||||
color: var(--nv-color-text-primary);
|
||||
border: var(--nv-border-width) solid var(--_border);
|
||||
border-radius: var(--_radius);
|
||||
box-shadow: var(--nv-elevation-inset-sm);
|
||||
box-shadow: var(--nv-elevation-inset-md), var(--nv-ledge);
|
||||
|
||||
font-family: var(--nv-font-family-sans);
|
||||
font-size: var(--_font-size);
|
||||
|
|
@ -87,7 +89,7 @@
|
|||
.nv-input:focus {
|
||||
outline: none;
|
||||
--_border: var(--nv-color-accent);
|
||||
box-shadow: var(--nv-elevation-inset-sm), var(--nv-focus-ring);
|
||||
box-shadow: var(--nv-elevation-inset-md), var(--nv-focus-ring);
|
||||
}
|
||||
|
||||
.nv-input[aria-invalid="true"] {
|
||||
|
|
@ -134,7 +136,13 @@ textarea.nv-input {
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
.nv-input--color::-webkit-color-swatch,
|
||||
.nv-input--color::-webkit-color-swatch {
|
||||
border: var(--nv-border-width) solid var(--nv-color-border);
|
||||
border-radius: calc(var(--nv-radius-md) - var(--nv-space-1));
|
||||
}
|
||||
|
||||
/* Separate rules on purpose: a selector list that names another engine's
|
||||
pseudo-element is invalid as a whole, and each browser would drop both. */
|
||||
.nv-input--color::-moz-color-swatch {
|
||||
border: var(--nv-border-width) solid var(--nv-color-border);
|
||||
border-radius: calc(var(--nv-radius-md) - var(--nv-space-1));
|
||||
|
|
@ -227,13 +235,16 @@ textarea.nv-input {
|
|||
.nv-radio {
|
||||
position: relative;
|
||||
flex: none;
|
||||
/* The marks are drawn in em; pinning the size keeps them the same whatever
|
||||
the label around them is set in. */
|
||||
font-size: var(--nv-font-size-md);
|
||||
inline-size: var(--nv-space-5);
|
||||
block-size: var(--nv-space-5);
|
||||
margin: 0;
|
||||
background-color: var(--nv-color-surface-inset);
|
||||
border: var(--nv-border-width) solid var(--nv-color-border-strong);
|
||||
border-radius: var(--nv-radius-xs);
|
||||
box-shadow: var(--nv-elevation-inset-sm);
|
||||
box-shadow: var(--nv-elevation-inset-sm), var(--nv-ledge);
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
transition: var(--nv-transition-control);
|
||||
|
|
@ -247,8 +258,9 @@ textarea.nv-input {
|
|||
.nv-radio:checked,
|
||||
.nv-checkbox:indeterminate {
|
||||
background-color: var(--nv-color-accent);
|
||||
background-image: var(--nv-sheen-filled);
|
||||
border-color: transparent;
|
||||
box-shadow: var(--nv-highlight-top), var(--nv-elevation-1);
|
||||
box-shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-1);
|
||||
}
|
||||
|
||||
/* The marks are always present, at zero size and zero opacity, so checking a
|
||||
|
|
@ -323,8 +335,10 @@ textarea.nv-input {
|
|||
}
|
||||
|
||||
/* --- Switch --------------------------------------------------------------------------
|
||||
The track is inset and the thumb is elevated: the physical relationship does
|
||||
the explaining, so the control reads as on or off without a label. */
|
||||
The track is a slot cut into the surface and the thumb is a knob riding in
|
||||
it: the physical relationship does the explaining, so the control reads as
|
||||
on or off without a label. The slot stays recessed when it fills with color
|
||||
— switching on lights the slot, it does not raise it. */
|
||||
|
||||
.nv-switch {
|
||||
--_track-inline: calc(var(--nv-space-6) * 1.5);
|
||||
|
|
@ -344,7 +358,7 @@ textarea.nv-input {
|
|||
background-color: var(--nv-color-surface-inset);
|
||||
border: var(--nv-border-width) solid var(--nv-color-border);
|
||||
border-radius: var(--nv-radius-full);
|
||||
box-shadow: var(--nv-elevation-inset-sm);
|
||||
box-shadow: var(--nv-elevation-inset-md), var(--nv-ledge);
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
transition: var(--nv-transition-control);
|
||||
|
|
@ -358,9 +372,10 @@ textarea.nv-input {
|
|||
inline-size: var(--_thumb);
|
||||
block-size: var(--_thumb);
|
||||
translate: 0 -50%;
|
||||
background-color: var(--nv-color-surface-elevated);
|
||||
background-color: var(--nv-color-knob);
|
||||
background-image: var(--nv-sheen-knob);
|
||||
border-radius: var(--nv-radius-full);
|
||||
box-shadow: var(--nv-highlight-top), var(--nv-elevation-2);
|
||||
box-shadow: var(--nv-outline), var(--nv-elevation-2);
|
||||
transition:
|
||||
translate var(--nv-duration-travel) var(--nv-ease-emphasized),
|
||||
background-color var(--nv-duration-fast) var(--nv-ease-standard);
|
||||
|
|
@ -369,6 +384,7 @@ textarea.nv-input {
|
|||
.nv-switch:checked {
|
||||
background-color: var(--nv-color-accent);
|
||||
border-color: transparent;
|
||||
box-shadow: var(--nv-elevation-inset-md), var(--nv-outline);
|
||||
}
|
||||
|
||||
.nv-switch:checked::after {
|
||||
|
|
@ -386,7 +402,10 @@ textarea.nv-input {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--nv-space-2-5);
|
||||
/* Labelled at control size, so a row of switches and checkboxes sits level
|
||||
with the buttons and fields beside it. */
|
||||
font: var(--nv-font-body);
|
||||
font-size: var(--nv-font-size-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,9 +43,10 @@
|
|||
margin-block-start: var(--nv-space-2);
|
||||
|
||||
background-color: var(--nv-material-frosted-bg);
|
||||
background-image: var(--nv-sheen-surface);
|
||||
border: var(--nv-border-width) solid var(--nv-material-frosted-border);
|
||||
border-radius: var(--nv-radius-md);
|
||||
box-shadow: var(--nv-highlight-top), var(--nv-elevation-3);
|
||||
border-radius: var(--nv-radius-lg);
|
||||
box-shadow: var(--nv-material-frosted-shadow);
|
||||
backdrop-filter: var(--nv-backdrop-frosted);
|
||||
-webkit-backdrop-filter: var(--nv-backdrop-frosted);
|
||||
color: var(--nv-color-text-primary);
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
.nv-nav-tabs in-page tab strip with a sliding indicator
|
||||
|
||||
Navigation is chrome: it sits above content rather than in it. That is why
|
||||
the bar is frosted — the blur is not decoration, it is the signal that
|
||||
content is passing underneath. A sidebar that scrolls with the page is not
|
||||
the bar is frosted and bevelled — the blur is not decoration, it is the
|
||||
signal that content is passing underneath. A sidebar that scrolls with the page is not
|
||||
chrome, so .nv-nav--vertical drops the translucency by default.
|
||||
========================================================================== */
|
||||
|
||||
|
|
@ -20,18 +20,19 @@
|
|||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--nv-space-4);
|
||||
flex-wrap: wrap;
|
||||
gap: var(--nv-space-2) var(--nv-space-4);
|
||||
padding: var(--nv-space-2) var(--nv-space-4);
|
||||
min-block-size: var(--nv-control-height-lg);
|
||||
|
||||
background-color: var(--nv-material-frosted-bg);
|
||||
background-image: var(--nv-sheen-raised);
|
||||
border-block-end: var(--nv-border-width) solid var(--nv-color-border-subtle);
|
||||
|
||||
/* The travelling selection marker sits behind its item at z-index -1; the
|
||||
bar has to be its own stacking context or the marker would fall behind
|
||||
the bar's background and disappear. */
|
||||
isolation: isolate;
|
||||
box-shadow: var(--nv-highlight-top);
|
||||
box-shadow: var(--nv-bevel);
|
||||
backdrop-filter: var(--nv-backdrop-frosted);
|
||||
-webkit-backdrop-filter: var(--nv-backdrop-frosted);
|
||||
transition: var(--nv-transition-surface);
|
||||
|
|
@ -40,12 +41,13 @@
|
|||
/* The bar only earns a shadow once something is behind it. Set data-scrolled
|
||||
from the application on scroll. */
|
||||
.nv-nav[data-scrolled] {
|
||||
box-shadow: var(--nv-highlight-top), var(--nv-elevation-2);
|
||||
box-shadow: var(--nv-bevel), var(--nv-elevation-2);
|
||||
transition: box-shadow var(--nv-duration-normal) var(--nv-ease-standard);
|
||||
}
|
||||
|
||||
.nv-nav--plain {
|
||||
background-color: var(--nv-color-surface);
|
||||
background-image: var(--nv-sheen-raised);
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
}
|
||||
|
|
@ -64,6 +66,7 @@
|
|||
|
||||
background-color: transparent;
|
||||
border-block-end: 0;
|
||||
background-image: none;
|
||||
box-shadow: none;
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
|
|
@ -88,6 +91,15 @@
|
|||
padding: 0;
|
||||
list-style: none;
|
||||
min-inline-size: 0;
|
||||
/* On a narrow screen a long list scrolls inside the bar instead of pushing
|
||||
the page sideways. */
|
||||
max-inline-size: 100%;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.nv-nav__list::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nv-nav__spacer {
|
||||
|
|
@ -169,14 +181,23 @@
|
|||
z-index: -1;
|
||||
|
||||
background-color: var(--nv-color-surface-elevated);
|
||||
background-image: var(--nv-sheen-raised);
|
||||
border: var(--nv-border-width) solid var(--nv-color-border-subtle);
|
||||
border-radius: inherit;
|
||||
box-shadow: var(--nv-highlight-top), var(--nv-elevation-1);
|
||||
|
||||
view-transition-name: var(--nv-selection-name);
|
||||
box-shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-1);
|
||||
view-transition-class: nv-travelling;
|
||||
}
|
||||
|
||||
/* The marker is named only while its own group is changing (niveus.js sets
|
||||
data-nv-travelling for the length of the transition). A permanent name
|
||||
would lift every marker on the page out of the page snapshot on every
|
||||
transition, theme changes included. */
|
||||
[data-nv-travelling] .nv-nav__item[aria-current]::before,
|
||||
[data-nv-travelling] .nv-nav__item--active::before,
|
||||
[data-nv-travelling] .nv-nav__item[aria-selected="true"]::before {
|
||||
view-transition-name: var(--nv-selection-name);
|
||||
}
|
||||
|
||||
/* Hovering an already-selected item should not restate the emphasis. */
|
||||
@media (hover: hover) {
|
||||
.nv-nav__item[aria-current]:hover,
|
||||
|
|
@ -226,11 +247,15 @@
|
|||
gap: var(--nv-space-1);
|
||||
inline-size: 15rem;
|
||||
block-size: 100svh;
|
||||
flex-wrap: nowrap;
|
||||
padding: var(--nv-space-4) var(--nv-space-3);
|
||||
|
||||
background-color: var(--nv-color-background-subtle);
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
/* A sidebar is a pane of the canvas rather than chrome over content, so it
|
||||
stays mostly opaque — but it lets the light of the canvas through, which
|
||||
is what stops it reading as a grey slab beside the page. */
|
||||
background-color: color-mix(in oklab, var(--nv-color-background-subtle) 72%, transparent);
|
||||
background-image: var(--nv-sheen-surface);
|
||||
backdrop-filter: var(--nv-backdrop-frosted);
|
||||
-webkit-backdrop-filter: var(--nv-backdrop-frosted);
|
||||
border-block-end: 0;
|
||||
border-inline-end: var(--nv-border-width) solid var(--nv-color-border-subtle);
|
||||
box-shadow: none;
|
||||
|
|
@ -252,11 +277,11 @@
|
|||
margin-block-end: var(--nv-space-3);
|
||||
}
|
||||
|
||||
/* Engraved, like .nv-overline: a section marks a region, it is not read. */
|
||||
.nv-nav__section {
|
||||
padding: var(--nv-space-4) var(--nv-space-3) var(--nv-space-1-5);
|
||||
font: var(--nv-font-caption);
|
||||
font-weight: var(--nv-font-weight-semibold);
|
||||
letter-spacing: var(--nv-letter-spacing-wider);
|
||||
font: var(--nv-font-engraved);
|
||||
letter-spacing: var(--nv-letter-spacing-engraved);
|
||||
text-transform: uppercase;
|
||||
color: var(--nv-color-text-tertiary);
|
||||
}
|
||||
|
|
@ -272,13 +297,13 @@
|
|||
.nv-nav-tabs {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
max-inline-size: 100%;
|
||||
gap: var(--nv-space-0-5);
|
||||
padding: var(--nv-space-1);
|
||||
|
||||
background-color: var(--nv-color-surface-inset);
|
||||
background-color: color-mix(in oklab, var(--nv-color-surface-inset) 80%, transparent);
|
||||
border: var(--nv-border-width) solid var(--nv-color-border-subtle);
|
||||
border-radius: var(--nv-radius-md);
|
||||
box-shadow: var(--nv-elevation-inset-sm);
|
||||
box-shadow: var(--nv-elevation-inset-md), var(--nv-ledge);
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
isolation: isolate;
|
||||
|
|
@ -307,12 +332,12 @@
|
|||
|
||||
/* --- Panels ----------------------------------------------------------------------
|
||||
Switching tabs should feel like moving through one surface rather than
|
||||
swapping the interface. The panel carries the shared content name, so the
|
||||
outgoing and incoming panels cross-fade inside the same box — and slide
|
||||
with the direction of travel when the application supplies one. */
|
||||
|
||||
swapping the interface. nv.selectTab() gives the outgoing and incoming
|
||||
panels one shared name for the length of the change, so they hand over
|
||||
inside the same box — and slide with the direction of travel. The class is
|
||||
what picks up the content animation in continuity.css. */
|
||||
.nv-tab-panel {
|
||||
view-transition-name: var(--nv-content-name, nv-content);
|
||||
view-transition-class: nv-content;
|
||||
}
|
||||
|
||||
/* --- Breadcrumbs -------------------------------------------------------------------- */
|
||||
|
|
|
|||
60
docs/Website/niveus/niveus/components/skeleton.css
vendored
Normal file
60
docs/Website/niveus/niveus/components/skeleton.css
vendored
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/* ==========================================================================
|
||||
Niveus — Skeleton
|
||||
--------------------------------------------------------------------------
|
||||
<span class="nv-skeleton nv-skeleton--text"></span>
|
||||
|
||||
The shape of content that has not arrived yet. A skeleton is a shallow well
|
||||
cut where the content will sit, with a slow band of light crossing it — the
|
||||
surface is recessed and waiting, not broken.
|
||||
|
||||
.nv-skeleton a block; size it with the layout
|
||||
.nv-skeleton--text one line of body text
|
||||
.nv-skeleton--circle an avatar or a tile
|
||||
|
||||
Under reduced motion the light stops crossing and the well stays: the state
|
||||
is still legible, it just does not move.
|
||||
========================================================================== */
|
||||
|
||||
.nv-skeleton {
|
||||
display: block;
|
||||
min-block-size: var(--nv-space-4);
|
||||
border-radius: var(--nv-radius-md);
|
||||
background-color: var(--nv-color-surface-inset);
|
||||
background-image: linear-gradient(
|
||||
100deg,
|
||||
transparent 30%,
|
||||
var(--nv-color-sheen-top) 50%,
|
||||
transparent 70%
|
||||
);
|
||||
background-size: 220% 100%;
|
||||
box-shadow: var(--nv-elevation-inset-sm), var(--nv-ledge);
|
||||
animation: nv-shimmer var(--nv-duration-shimmer) var(--nv-ease-linear) infinite;
|
||||
}
|
||||
|
||||
.nv-skeleton--text {
|
||||
inline-size: 100%;
|
||||
min-block-size: 0;
|
||||
block-size: 0.75em;
|
||||
border-radius: var(--nv-radius-full);
|
||||
}
|
||||
|
||||
/* A paragraph of skeleton lines ends short, like the paragraph it stands for. */
|
||||
.nv-skeleton--text:last-child:not(:first-child) {
|
||||
inline-size: 60%;
|
||||
}
|
||||
|
||||
.nv-skeleton--circle {
|
||||
aspect-ratio: 1;
|
||||
border-radius: var(--nv-radius-full);
|
||||
}
|
||||
|
||||
@keyframes nv-shimmer {
|
||||
from { background-position: 100% 0; }
|
||||
to { background-position: -120% 0; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.nv-skeleton {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
74
docs/Website/niveus/niveus/components/stat.css
Normal file
74
docs/Website/niveus/niveus/components/stat.css
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
/* ==========================================================================
|
||||
Niveus — Stat
|
||||
--------------------------------------------------------------------------
|
||||
<dl class="nv-stat-group">
|
||||
<div class="nv-stat">
|
||||
<dt class="nv-stat__label">Servers</dt>
|
||||
<dd class="nv-stat__value">1,204</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
A number the user is meant to read at a glance. A group of stats is set into
|
||||
one shallow well, like the readout panel of an instrument: the figures sit
|
||||
behind the surface, separated by hairlines, so a row of numbers reads as one
|
||||
display instead of as four loose labels.
|
||||
|
||||
.nv-stat-group the readout well; wraps without a media query
|
||||
.nv-stat one reading
|
||||
.nv-stat__label what is measured
|
||||
.nv-stat__value the figure, in tabular numerals
|
||||
.nv-stat__delta change since last time; --up / --down
|
||||
========================================================================== */
|
||||
|
||||
.nv-stat-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(8rem, 100%), 1fr));
|
||||
/* The hairlines between readings are the well itself showing through a
|
||||
one-pixel gap, so they stay continuous however the row wraps. */
|
||||
gap: var(--nv-border-width);
|
||||
margin: 0;
|
||||
background-color: var(--nv-color-border-subtle);
|
||||
border: var(--nv-border-width) solid var(--nv-color-border-subtle);
|
||||
border-radius: var(--nv-radius-lg);
|
||||
box-shadow: var(--nv-elevation-inset-md), var(--nv-ledge);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nv-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--nv-space-1);
|
||||
min-inline-size: 0;
|
||||
padding: var(--nv-space-4) var(--nv-space-5);
|
||||
}
|
||||
|
||||
.nv-stat-group > .nv-stat {
|
||||
background-color: var(--nv-color-surface-inset);
|
||||
}
|
||||
|
||||
.nv-stat__label {
|
||||
font: var(--nv-font-caption);
|
||||
color: var(--nv-color-text-tertiary);
|
||||
}
|
||||
|
||||
.nv-stat__value {
|
||||
margin: 0;
|
||||
font: var(--nv-font-heading);
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: var(--nv-letter-spacing-tight);
|
||||
color: var(--nv-color-text-primary);
|
||||
}
|
||||
|
||||
.nv-stat__delta {
|
||||
font: var(--nv-font-caption);
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--nv-color-text-secondary);
|
||||
}
|
||||
|
||||
.nv-stat__delta--up {
|
||||
color: var(--nv-color-success);
|
||||
}
|
||||
|
||||
.nv-stat__delta--down {
|
||||
color: var(--nv-color-danger);
|
||||
}
|
||||
157
docs/Website/niveus/niveus/components/steps.css
Normal file
157
docs/Website/niveus/niveus/components/steps.css
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
/* ==========================================================================
|
||||
Niveus — Steps
|
||||
--------------------------------------------------------------------------
|
||||
<ol class="nv-steps">
|
||||
<li class="nv-steps__item" data-state="done">
|
||||
<span class="nv-steps__marker">1</span>
|
||||
<div class="nv-steps__body">
|
||||
<h3 class="nv-steps__title">Listen</h3>
|
||||
<p class="nv-steps__text">…</p>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
A sequence the user moves through: a process, an onboarding, an order's
|
||||
progress. The steps sit on a rail — a groove cut into the surface — with
|
||||
each marker a raised knob set into it. Finished steps light their stretch
|
||||
of the rail, so progress reads as something filling up rather than as a
|
||||
row of ticks.
|
||||
|
||||
.nv-steps horizontal; becomes vertical on narrow screens
|
||||
.nv-steps--vertical always vertical
|
||||
data-state="done" the step and its stretch of rail are lit
|
||||
aria-current="step" the step in progress: lit marker, rail still dark
|
||||
========================================================================== */
|
||||
|
||||
.nv-steps {
|
||||
--_marker: 2.25rem;
|
||||
--_rail: 6px;
|
||||
--_gap: var(--nv-space-4);
|
||||
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: minmax(0, 1fr);
|
||||
gap: var(--_gap);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.nv-steps__item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--nv-space-3);
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
/* Each step draws the rail from its own marker to the next one, so a finished
|
||||
step can light exactly its stretch and the rail never runs past the end. */
|
||||
.nv-steps__item:not(:last-child)::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset-block-start: calc((var(--_marker) - var(--_rail)) / 2);
|
||||
inset-inline-start: calc(var(--_marker) / 2);
|
||||
inline-size: calc(100% + var(--_gap));
|
||||
block-size: var(--_rail);
|
||||
border-radius: var(--nv-radius-full);
|
||||
background-color: var(--nv-color-surface-inset);
|
||||
box-shadow: var(--nv-elevation-inset-sm), var(--nv-ledge);
|
||||
transition: var(--nv-transition-surface);
|
||||
}
|
||||
|
||||
.nv-steps__item[data-state="done"]:not(:last-child)::before {
|
||||
background-color: var(--nv-color-accent);
|
||||
background-image: var(--nv-sheen-filled);
|
||||
box-shadow: var(--nv-glow-sm);
|
||||
}
|
||||
|
||||
.nv-steps__marker {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex: none;
|
||||
inline-size: var(--_marker);
|
||||
block-size: var(--_marker);
|
||||
|
||||
background-color: var(--nv-color-surface-elevated);
|
||||
background-image: var(--nv-sheen-raised);
|
||||
color: var(--nv-color-text-secondary);
|
||||
font: var(--nv-font-label);
|
||||
font-weight: var(--nv-font-weight-semibold);
|
||||
font-variant-numeric: tabular-nums;
|
||||
border-radius: var(--nv-radius-full);
|
||||
box-shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-2);
|
||||
transition: var(--nv-transition-surface);
|
||||
}
|
||||
|
||||
.nv-steps__item[data-state="done"] > .nv-steps__marker,
|
||||
.nv-steps__item[aria-current="step"] > .nv-steps__marker {
|
||||
background-color: var(--nv-color-accent);
|
||||
background-image: var(--nv-sheen-filled);
|
||||
color: var(--nv-color-on-accent);
|
||||
box-shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-2), var(--nv-glow-md);
|
||||
}
|
||||
|
||||
.nv-steps__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--nv-space-1);
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
.nv-steps__title {
|
||||
margin: 0;
|
||||
font: var(--nv-font-title);
|
||||
letter-spacing: var(--nv-letter-spacing-tight);
|
||||
color: var(--nv-color-text-primary);
|
||||
}
|
||||
|
||||
.nv-steps__text {
|
||||
margin: 0;
|
||||
font: var(--nv-font-caption);
|
||||
color: var(--nv-color-text-secondary);
|
||||
}
|
||||
|
||||
/* --- Vertical -------------------------------------------------------------------
|
||||
Marker beside the text and the rail running down between markers. Also what
|
||||
a horizontal sequence becomes once its columns are too narrow to read. */
|
||||
|
||||
.nv-steps--vertical {
|
||||
grid-auto-flow: row;
|
||||
grid-auto-columns: auto;
|
||||
--_gap: var(--nv-space-5);
|
||||
}
|
||||
|
||||
.nv-steps--vertical > .nv-steps__item {
|
||||
flex-direction: row;
|
||||
gap: var(--nv-space-4);
|
||||
}
|
||||
|
||||
.nv-steps--vertical > .nv-steps__item:not(:last-child)::before {
|
||||
inset-block-start: calc(var(--_marker) / 2);
|
||||
inset-inline-start: calc((var(--_marker) - var(--_rail)) / 2);
|
||||
inline-size: var(--_rail);
|
||||
block-size: calc(100% + var(--_gap));
|
||||
}
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
.nv-steps {
|
||||
grid-auto-flow: row;
|
||||
grid-auto-columns: auto;
|
||||
--_gap: var(--nv-space-5);
|
||||
}
|
||||
|
||||
.nv-steps > .nv-steps__item {
|
||||
flex-direction: row;
|
||||
gap: var(--nv-space-4);
|
||||
}
|
||||
|
||||
.nv-steps > .nv-steps__item:not(:last-child)::before {
|
||||
inset-block-start: calc(var(--_marker) / 2);
|
||||
inset-inline-start: calc((var(--_marker) - var(--_rail)) / 2);
|
||||
inline-size: var(--_rail);
|
||||
block-size: calc(100% + var(--_gap));
|
||||
}
|
||||
}
|
||||
83
docs/Website/niveus/niveus/components/tile.css
Normal file
83
docs/Website/niveus/niveus/components/tile.css
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
/* ==========================================================================
|
||||
Niveus — Tile
|
||||
--------------------------------------------------------------------------
|
||||
<span class="nv-tile"><svg …></svg></span>
|
||||
|
||||
A tile is a small raised key that holds one glyph: the icon beside a
|
||||
feature, the mark of a product, the symbol on a stat. It is the physical
|
||||
version of an icon — something with an edge you could press — which is why
|
||||
a row of features reads as a set of objects rather than as clip art.
|
||||
|
||||
.nv-tile soft key: tinted face, the glyph in its tint
|
||||
.nv-tile--solid filled key: saturated face, a light glyph
|
||||
.nv-tile--glow the key is lit from inside
|
||||
.nv-tile--sm / --lg / --xl
|
||||
|
||||
Everything derives from --nv-tile-tint, the accent unless set. A tile is
|
||||
not a button: if it does something, it is an .nv-button--icon.
|
||||
========================================================================== */
|
||||
|
||||
.nv-tile {
|
||||
--nv-tile-tint: var(--nv-color-accent);
|
||||
--_size: 2.75rem;
|
||||
--_radius: var(--nv-radius-md);
|
||||
--_bg: color-mix(in oklab, var(--nv-tile-tint) 16%, var(--nv-color-surface-elevated));
|
||||
--_ink: color-mix(in oklab, var(--nv-tile-tint) 70%, var(--nv-color-text-primary));
|
||||
--_sheen: var(--nv-sheen-raised);
|
||||
--_glow: 0 0 transparent;
|
||||
|
||||
position: relative;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
flex: none;
|
||||
inline-size: var(--_size);
|
||||
block-size: var(--_size);
|
||||
|
||||
background-color: var(--_bg);
|
||||
background-image: var(--_sheen);
|
||||
color: var(--_ink);
|
||||
border: var(--nv-border-width) solid color-mix(in oklab, var(--nv-tile-tint) 30%, var(--nv-color-border-subtle));
|
||||
border-radius: var(--_radius);
|
||||
box-shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-1), var(--_glow);
|
||||
transition: var(--nv-transition-surface);
|
||||
}
|
||||
|
||||
/* Glyphs take half the key; a mark that is itself a picture (an image, a
|
||||
logo) fills it, clipped to the key's corners. */
|
||||
.nv-tile > svg {
|
||||
inline-size: 50%;
|
||||
block-size: 50%;
|
||||
}
|
||||
|
||||
.nv-tile > img {
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
border-radius: inherit;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.nv-tile--solid {
|
||||
--_bg: var(--nv-tile-tint);
|
||||
--_ink: var(--nv-color-on-accent);
|
||||
--_sheen: var(--nv-sheen-filled);
|
||||
}
|
||||
|
||||
.nv-tile--glow {
|
||||
--_glow: 0 0 var(--nv-glow-blur-md) var(--nv-glow-spread-sm)
|
||||
color-mix(in oklab, var(--nv-tile-tint) var(--nv-glow-strength), transparent);
|
||||
}
|
||||
|
||||
.nv-tile--sm {
|
||||
--_size: 2rem;
|
||||
--_radius: var(--nv-radius-sm);
|
||||
}
|
||||
|
||||
.nv-tile--lg {
|
||||
--_size: 3.5rem;
|
||||
--_radius: var(--nv-radius-lg);
|
||||
}
|
||||
|
||||
.nv-tile--xl {
|
||||
--_size: 4.5rem;
|
||||
--_radius: var(--nv-radius-xl);
|
||||
}
|
||||
192
docs/Website/niveus/niveus/components/window.css
Normal file
192
docs/Website/niveus/niveus/components/window.css
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
/* ==========================================================================
|
||||
Niveus — Window
|
||||
--------------------------------------------------------------------------
|
||||
.nv-window a framed application window
|
||||
.nv-window--inactive a window that does not have focus
|
||||
.nv-window__titlebar controls · title · trailing actions
|
||||
.nv-window__controls the three lights, at the start of the bar
|
||||
.nv-window__control one light: --close, --minimize or --zoom
|
||||
.nv-window__title the window's name, centered on the bar
|
||||
.nv-window__actions trailing slot for a few ghost buttons
|
||||
.nv-window__body the content area
|
||||
|
||||
<div class="nv-window">
|
||||
<div class="nv-window__titlebar">
|
||||
<div class="nv-window__controls">
|
||||
<button class="nv-window__control nv-window__control--close" aria-label="Close"></button>
|
||||
<button class="nv-window__control nv-window__control--minimize" aria-label="Minimize"></button>
|
||||
<button class="nv-window__control nv-window__control--zoom" aria-label="Zoom"></button>
|
||||
</div>
|
||||
<span class="nv-window__title">Settings</span>
|
||||
</div>
|
||||
<div class="nv-window__body">…</div>
|
||||
</div>
|
||||
|
||||
A window is a solid object: the content it holds never sits on glass. Its
|
||||
titlebar is a raised strip lit like a control, and the controls are three
|
||||
lights at its start — close, minimize, zoom — always in that order and in
|
||||
those colors, because people find them by position and color before they
|
||||
read anything. Their glyphs appear only while the pointer is over the
|
||||
group. An inactive window greys its lights and steps back one elevation,
|
||||
and the lights regain their color the moment they are hovered.
|
||||
========================================================================== */
|
||||
|
||||
.nv-window {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-inline-size: 0;
|
||||
|
||||
background-color: var(--nv-color-surface);
|
||||
background-image: var(--nv-sheen-surface);
|
||||
color: var(--nv-color-text-primary);
|
||||
border: var(--nv-border-width) solid var(--nv-color-border-subtle);
|
||||
border-radius: var(--nv-radius-xl);
|
||||
box-shadow: var(--nv-highlight-top), var(--nv-outline), var(--nv-elevation-4);
|
||||
|
||||
overflow: hidden;
|
||||
transition: var(--nv-transition-surface);
|
||||
}
|
||||
|
||||
.nv-window--inactive {
|
||||
box-shadow: var(--nv-highlight-top), var(--nv-outline), var(--nv-elevation-3);
|
||||
}
|
||||
|
||||
/* --- Titlebar ---------------------------------------------------------------
|
||||
Three columns with the outer two sharing the slack, so the title stays on
|
||||
the window's center line whatever the controls and actions measure. */
|
||||
|
||||
.nv-window__titlebar {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
gap: var(--nv-space-3);
|
||||
/* Slim, as titlebars on the desktop are: tall enough for the lights and
|
||||
a label, not a toolbar's height. */
|
||||
min-block-size: calc(var(--nv-control-height-sm) + var(--nv-space-1));
|
||||
padding-inline: var(--nv-space-3);
|
||||
|
||||
background-color: var(--nv-color-surface-elevated);
|
||||
background-image: var(--nv-sheen-raised);
|
||||
border-block-end: var(--nv-border-width) solid var(--nv-color-border-subtle);
|
||||
box-shadow: var(--nv-highlight-top);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.nv-window__title {
|
||||
grid-column: 2;
|
||||
min-inline-size: 0;
|
||||
overflow: hidden;
|
||||
font: var(--nv-font-label);
|
||||
font-weight: var(--nv-font-weight-semibold);
|
||||
color: var(--nv-color-text-primary);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nv-window--inactive .nv-window__title {
|
||||
color: var(--nv-color-text-tertiary);
|
||||
}
|
||||
|
||||
.nv-window__actions {
|
||||
grid-column: 3;
|
||||
justify-self: end;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--nv-space-1);
|
||||
}
|
||||
|
||||
/* --- Controls ----------------------------------------------------------------- */
|
||||
|
||||
.nv-window__controls {
|
||||
grid-column: 1;
|
||||
justify-self: start;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--nv-space-2);
|
||||
}
|
||||
|
||||
.nv-window__control {
|
||||
--_light: var(--nv-color-window-close);
|
||||
--_c: var(--_light);
|
||||
|
||||
position: relative;
|
||||
flex: none;
|
||||
inline-size: var(--nv-space-3);
|
||||
block-size: var(--nv-space-3);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
background-color: var(--_c);
|
||||
background-image: var(--nv-sheen-filled);
|
||||
border: 0;
|
||||
border-radius: var(--nv-radius-full);
|
||||
box-shadow: inset 0 0 0 var(--nv-border-width) var(--nv-color-window-control-ring);
|
||||
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition: var(--nv-transition-colors);
|
||||
}
|
||||
|
||||
.nv-window__control--minimize { --_light: var(--nv-color-window-minimize); }
|
||||
.nv-window__control--zoom { --_light: var(--nv-color-window-zoom); }
|
||||
|
||||
/* The glyph is two bars drawn from the light's center: crossed for close, one
|
||||
for minimize, a plus for zoom. They are always present and fade in, so the
|
||||
whole group reveals at once instead of one glyph at a time. */
|
||||
.nv-window__control::before,
|
||||
.nv-window__control::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset-block-start: 50%;
|
||||
inset-inline-start: 50%;
|
||||
inline-size: 55%;
|
||||
block-size: calc(var(--nv-border-width) * 1.5);
|
||||
translate: -50% -50%;
|
||||
background-color: var(--nv-color-window-glyph);
|
||||
border-radius: var(--nv-radius-full);
|
||||
opacity: 0;
|
||||
transition: opacity var(--nv-duration-fast) var(--nv-ease-standard);
|
||||
}
|
||||
|
||||
.nv-window__control--close::before { rotate: 45deg; }
|
||||
.nv-window__control--close::after { rotate: -45deg; }
|
||||
.nv-window__control--minimize::after { display: none; }
|
||||
.nv-window__control--zoom::after { rotate: 90deg; }
|
||||
|
||||
.nv-window__controls:hover .nv-window__control::before,
|
||||
.nv-window__controls:hover .nv-window__control::after,
|
||||
.nv-window__controls:focus-within .nv-window__control::before,
|
||||
.nv-window__controls:focus-within .nv-window__control::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.nv-window__control:active {
|
||||
background-image: none;
|
||||
box-shadow:
|
||||
inset 0 0 0 var(--nv-border-width) var(--nv-color-window-control-ring),
|
||||
var(--nv-elevation-inset-sm);
|
||||
}
|
||||
|
||||
.nv-window__control:focus-visible {
|
||||
outline: none;
|
||||
box-shadow:
|
||||
inset 0 0 0 var(--nv-border-width) var(--nv-color-window-control-ring),
|
||||
var(--nv-focus-ring);
|
||||
}
|
||||
|
||||
.nv-window--inactive .nv-window__control {
|
||||
--_c: var(--nv-color-window-inactive);
|
||||
}
|
||||
|
||||
.nv-window--inactive .nv-window__controls:hover .nv-window__control {
|
||||
--_c: var(--_light);
|
||||
}
|
||||
|
||||
/* --- Body --------------------------------------------------------------------- */
|
||||
|
||||
.nv-window__body {
|
||||
flex: 1;
|
||||
min-block-size: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
|
@ -60,9 +60,9 @@
|
|||
|
||||
The fix is to capture the labels too and order the groups explicitly. The
|
||||
marker travels below; the items ride above it, exactly as they do at rest.
|
||||
niveus.js assigns each item its name — they have to be unique per document,
|
||||
which no stylesheet can guarantee — and items it has not named simply stay in
|
||||
the page snapshot.
|
||||
niveus.js names the items of the group that is changing, and only while it
|
||||
changes — names have to be unique per document, which no stylesheet can
|
||||
guarantee — and every other item simply stays in the page snapshot.
|
||||
|
||||
Because the layering is now correct, the marker needs no trick to stay out of
|
||||
the way: it travels at full opacity, like the solid object it is. */
|
||||
|
|
@ -71,6 +71,16 @@
|
|||
z-index: 0;
|
||||
}
|
||||
|
||||
/* The marker changes shape as it travels — a short label's pill becomes a
|
||||
long one's. By default the browser scales each snapshot to the group's
|
||||
width and keeps its aspect ratio, so mid-way a 40×26 pill stretched to
|
||||
80px wide is drawn 52px tall, hanging out below its strip. The marker is a
|
||||
plain surface, so its snapshots fill the box instead. */
|
||||
::view-transition-old(.nv-travelling),
|
||||
::view-transition-new(.nv-travelling) {
|
||||
block-size: 100%;
|
||||
}
|
||||
|
||||
::view-transition-group(.nv-carried) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
|
@ -101,40 +111,56 @@
|
|||
Moving forward and moving back should not look the same. The application
|
||||
sets data-nv-direction on the root for the length of the transition; content
|
||||
then leaves towards where the user came from and enters from where they are
|
||||
going. Without a direction, content simply rises into place. */
|
||||
going. Without a direction, content simply rises into place.
|
||||
|
||||
::view-transition-old(nv-content) {
|
||||
animation: nv-fade-out var(--nv-duration-quick) var(--nv-ease-exit) both;
|
||||
Content hands over; it does not cross-fade. Two blocks of text at half
|
||||
opacity on top of each other read as a rendering fault, not as motion, so
|
||||
the outgoing content is gone (fast) before the incoming arrives (delayed by
|
||||
that same beat). The box itself never empties for longer than a blink.
|
||||
|
||||
Any element whose content changes in place takes part by carrying the
|
||||
nv-content class: .nv-continuous and .nv-tab-panel both do.
|
||||
|
||||
The box clips what it holds. When a tall panel gives way to a short one,
|
||||
the page around it has already reflowed to the new height, so unclipped
|
||||
outgoing content would spill out below its own card. */
|
||||
|
||||
::view-transition-group(.nv-content) {
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
::view-transition-new(nv-content) {
|
||||
animation:
|
||||
nv-fade-in var(--nv-duration-normal) var(--nv-ease-entrance) both,
|
||||
nv-rise var(--nv-duration-normal) var(--nv-ease-entrance) both;
|
||||
::view-transition-old(.nv-content) {
|
||||
animation: nv-fade-out var(--nv-duration-fast) var(--nv-ease-exit) both;
|
||||
}
|
||||
|
||||
:root[data-nv-direction="forward"]::view-transition-old(nv-content) {
|
||||
::view-transition-new(.nv-content) {
|
||||
animation:
|
||||
nv-fade-out var(--nv-duration-quick) var(--nv-ease-exit) both,
|
||||
nv-exit-to-start var(--nv-duration-normal) var(--nv-ease-emphasized) both;
|
||||
nv-fade-in var(--nv-duration-normal) var(--nv-ease-entrance) var(--nv-duration-fast) both,
|
||||
nv-rise var(--nv-duration-normal) var(--nv-ease-entrance) var(--nv-duration-fast) both;
|
||||
}
|
||||
|
||||
:root[data-nv-direction="forward"]::view-transition-new(nv-content) {
|
||||
:root[data-nv-direction="forward"]::view-transition-old(.nv-content) {
|
||||
animation:
|
||||
nv-fade-in var(--nv-duration-normal) var(--nv-ease-entrance) both,
|
||||
nv-enter-from-end var(--nv-duration-normal) var(--nv-ease-emphasized) both;
|
||||
nv-fade-out var(--nv-duration-fast) var(--nv-ease-exit) both,
|
||||
nv-exit-to-start var(--nv-duration-fast) var(--nv-ease-exit) both;
|
||||
}
|
||||
|
||||
:root[data-nv-direction="backward"]::view-transition-old(nv-content) {
|
||||
:root[data-nv-direction="forward"]::view-transition-new(.nv-content) {
|
||||
animation:
|
||||
nv-fade-out var(--nv-duration-quick) var(--nv-ease-exit) both,
|
||||
nv-exit-to-end var(--nv-duration-normal) var(--nv-ease-emphasized) both;
|
||||
nv-fade-in var(--nv-duration-normal) var(--nv-ease-entrance) var(--nv-duration-fast) both,
|
||||
nv-enter-from-end var(--nv-duration-normal) var(--nv-ease-emphasized) var(--nv-duration-fast) both;
|
||||
}
|
||||
|
||||
:root[data-nv-direction="backward"]::view-transition-new(nv-content) {
|
||||
:root[data-nv-direction="backward"]::view-transition-old(.nv-content) {
|
||||
animation:
|
||||
nv-fade-in var(--nv-duration-normal) var(--nv-ease-entrance) both,
|
||||
nv-enter-from-start var(--nv-duration-normal) var(--nv-ease-emphasized) both;
|
||||
nv-fade-out var(--nv-duration-fast) var(--nv-ease-exit) both,
|
||||
nv-exit-to-end var(--nv-duration-fast) var(--nv-ease-exit) both;
|
||||
}
|
||||
|
||||
:root[data-nv-direction="backward"]::view-transition-new(.nv-content) {
|
||||
animation:
|
||||
nv-fade-in var(--nv-duration-normal) var(--nv-ease-entrance) var(--nv-duration-fast) both,
|
||||
nv-enter-from-start var(--nv-duration-normal) var(--nv-ease-emphasized) var(--nv-duration-fast) both;
|
||||
}
|
||||
|
||||
@keyframes nv-fade-in {
|
||||
|
|
@ -171,6 +197,34 @@
|
|||
|
||||
.nv-continuous {
|
||||
view-transition-name: var(--nv-content-name, nv-content);
|
||||
view-transition-class: nv-content;
|
||||
}
|
||||
|
||||
/* --- Reveal ------------------------------------------------------------------------
|
||||
Content arriving as it scrolls into view: it rises a short way and fades in,
|
||||
once, so a long page is read in the order it was written instead of all at
|
||||
once. .nv-reveal-group does the same for its children one after another,
|
||||
--nv-stagger apart.
|
||||
|
||||
niveus.js marks what is waiting. Without the script, and under reduced
|
||||
motion, nothing is ever marked — so nothing is ever hidden. */
|
||||
|
||||
.nv-reveal[data-nv-reveal="pending"],
|
||||
.nv-reveal-group > [data-nv-reveal="pending"] {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.nv-reveal[data-nv-reveal="shown"],
|
||||
.nv-reveal-group > [data-nv-reveal="shown"] {
|
||||
animation: nv-arrive var(--nv-duration-slower) var(--nv-ease-entrance)
|
||||
calc(var(--nv-reveal-index, 0) * var(--nv-stagger)) both;
|
||||
}
|
||||
|
||||
@keyframes nv-arrive {
|
||||
from {
|
||||
opacity: 0;
|
||||
translate: 0 var(--nv-travel-lg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Collapsible region for markup that is not a <details>. The 0fr -> 1fr grid
|
||||
|
|
|
|||
42
docs/Website/niveus/niveus/fonts.css
Normal file
42
docs/Website/niveus/niveus/fonts.css
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/* ==========================================================================
|
||||
Niveus — Fonts
|
||||
--------------------------------------------------------------------------
|
||||
Niveus ships its own faces, so it reads the same wherever it is used
|
||||
instead of depending on what a machine happens to have installed. Both are
|
||||
variable fonts under the SIL Open Font License (fonts/OFL-*.txt), free to
|
||||
bundle with anything.
|
||||
|
||||
Instrument Sans the interface. Crisp, slightly narrow, warm at small
|
||||
sizes — the voice of every label, control and heading.
|
||||
JetBrains Mono code, figures, and the engraved labels (.nv-overline)
|
||||
that mark sections the way markings mark an instrument.
|
||||
|
||||
The paths are relative to this file and to dist/niveus.css alike: both sit
|
||||
one level below the root, where fonts/ lives.
|
||||
========================================================================== */
|
||||
|
||||
@font-face {
|
||||
font-family: "Instrument Sans";
|
||||
src: url("../fonts/InstrumentSans.woff2") format("woff2");
|
||||
font-weight: 400 700;
|
||||
font-stretch: 75% 100%;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Instrument Sans";
|
||||
src: url("../fonts/InstrumentSans-Italic.woff2") format("woff2");
|
||||
font-weight: 400 700;
|
||||
font-stretch: 75% 100%;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "JetBrains Mono";
|
||||
src: url("../fonts/JetBrainsMono.woff2") format("woff2");
|
||||
font-weight: 100 800;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/* ==========================================================================
|
||||
Niveus — a calm, adaptive interface language.
|
||||
Niveus — a tactile, adaptive interface language.
|
||||
Made by Nexoniarz.
|
||||
--------------------------------------------------------------------------
|
||||
The single entry point. An application links this file and nothing else:
|
||||
|
|
@ -22,6 +22,9 @@
|
|||
@layer niveus.tokens, niveus.theme, niveus.base, niveus.continuity,
|
||||
niveus.components, niveus.utilities;
|
||||
|
||||
/* --- Fonts: the faces Niveus ships, so it reads the same everywhere ---------- */
|
||||
@import url("fonts.css");
|
||||
|
||||
/* --- Tokens: the values, appearance-agnostic ------------------------------- */
|
||||
@import url("tokens/color.css") layer(niveus.tokens);
|
||||
@import url("tokens/typography.css") layer(niveus.tokens);
|
||||
|
|
@ -54,6 +57,13 @@
|
|||
@import url("components/bed.css") layer(niveus.components);
|
||||
@import url("components/badge.css") layer(niveus.components);
|
||||
@import url("components/table.css") layer(niveus.components);
|
||||
@import url("components/window.css") layer(niveus.components);
|
||||
@import url("components/tile.css") layer(niveus.components);
|
||||
@import url("components/avatar.css") layer(niveus.components);
|
||||
@import url("components/stat.css") layer(niveus.components);
|
||||
@import url("components/steps.css") layer(niveus.components);
|
||||
@import url("components/deck.css") layer(niveus.components);
|
||||
@import url("components/skeleton.css") layer(niveus.components);
|
||||
|
||||
/* --- Utilities ----------------------------------------------------------------- */
|
||||
@import url("utilities.css") layer(niveus.utilities);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
state (a router, a framework) should call nv.transition() around its own
|
||||
update and ignore the rest.
|
||||
|
||||
nv.transition(update, { direction, kind }) run an update continuously
|
||||
nv.transition(update, { direction, kind, scope }) run an update continuously
|
||||
nv.setTheme("dark" | "light" | "system") cross-fade the appearance
|
||||
nv.openDialog(dialog, trigger) open it out of its trigger
|
||||
nv.selectTab(tablist, tab) move through the same surface
|
||||
|
|
@ -27,9 +27,16 @@
|
|||
[data-nv-close] button that closes its dialog or popover
|
||||
[data-nv-scroller] frame whose [data-nv-scroll] controls move
|
||||
its .nv-scroller a page at a time
|
||||
.nv-reveal / .nv-reveal-group content that arrives as it scrolls into
|
||||
view, once (see continuity.css)
|
||||
.nv-card--spotlight card whose light follows the pointer
|
||||
|
||||
Wiring also names every navigation item, which is what lets a travelling
|
||||
selection marker pass *behind* the labels rather than over them.
|
||||
Only what changes is lifted. A transition names the selection marker and
|
||||
the items of the groups it is given as `scope`, and only for as long as it
|
||||
runs; everything else stays in the one page snapshot. Naming every item on
|
||||
the page permanently made every transition — a theme change included —
|
||||
tear the page into dozens of layers, which is both slow and what pulled
|
||||
items out from under frosted chrome.
|
||||
|
||||
Reduced motion is honoured by skipping the view transition entirely: the
|
||||
update still runs, it simply arrives instead of travelling.
|
||||
|
|
@ -51,7 +58,10 @@
|
|||
/**
|
||||
* Run `update` so the user can follow the change.
|
||||
* `direction` ("forward" | "backward") tells content which way to travel;
|
||||
* `kind` ("theme") marks a change the whole surface takes part in.
|
||||
* `kind` ("theme") marks a change the whole surface takes part in;
|
||||
* `scope` (an element or a list of them) names the selection groups whose
|
||||
* marker should travel. Without a scope, nothing is lifted and the change
|
||||
* cross-fades in place.
|
||||
*/
|
||||
function transition(update, options) {
|
||||
options = options || {};
|
||||
|
|
@ -63,11 +73,20 @@
|
|||
if (options.direction) root.dataset.nvDirection = options.direction;
|
||||
if (options.kind) root.dataset.nvTransition = options.kind;
|
||||
|
||||
var groups = [].concat(options.scope || []).filter(Boolean);
|
||||
var lifted = [];
|
||||
groups.forEach(function (group) {
|
||||
group.setAttribute("data-nv-travelling", "");
|
||||
lifted = lifted.concat(liftLabels(group));
|
||||
});
|
||||
|
||||
var view = document.startViewTransition(update);
|
||||
|
||||
function clear() {
|
||||
delete root.dataset.nvDirection;
|
||||
delete root.dataset.nvTransition;
|
||||
groups.forEach(function (group) { group.removeAttribute("data-nv-travelling"); });
|
||||
lifted.forEach(function (item) { item.style.viewTransitionName = ""; });
|
||||
}
|
||||
view.finished.then(clear, clear);
|
||||
|
||||
|
|
@ -146,16 +165,32 @@
|
|||
}
|
||||
if (next < 0 || next === current) return Promise.resolve();
|
||||
|
||||
// The outgoing and incoming panels carry one name between them — the old
|
||||
// one in the old state, the new one in the new — so the browser treats
|
||||
// them as one surface whose content changes. Named only for the length of
|
||||
// the change, so any number of tab sets can live on one page.
|
||||
var panelOf = function (candidate) {
|
||||
return candidate ? document.getElementById(candidate.getAttribute("aria-controls")) : null;
|
||||
};
|
||||
var outgoing = panelOf(tabs[current]);
|
||||
var incoming = panelOf(tab);
|
||||
if (outgoing && canTransition()) outgoing.style.viewTransitionName = "nv-panel";
|
||||
|
||||
var done = transition(function () {
|
||||
tabs.forEach(function (candidate, index) {
|
||||
var selected = index === next;
|
||||
candidate.setAttribute("aria-selected", String(selected));
|
||||
candidate.tabIndex = selected ? 0 : -1;
|
||||
|
||||
var panel = document.getElementById(candidate.getAttribute("aria-controls"));
|
||||
var panel = panelOf(candidate);
|
||||
if (panel) panel.hidden = !selected;
|
||||
});
|
||||
}, { direction: next > current ? "forward" : "backward" });
|
||||
if (outgoing) outgoing.style.viewTransitionName = "";
|
||||
if (incoming && canTransition()) incoming.style.viewTransitionName = "nv-panel";
|
||||
}, { direction: next > current ? "forward" : "backward", scope: tablist });
|
||||
|
||||
var settle = function () { if (incoming) incoming.style.viewTransitionName = ""; };
|
||||
done.then(settle, settle);
|
||||
|
||||
tab.focus();
|
||||
return done;
|
||||
|
|
@ -240,29 +275,84 @@
|
|||
}
|
||||
|
||||
/**
|
||||
* Give every item in a selection group a view-transition name, so it is
|
||||
* captured as its own layer and rides *above* the travelling marker instead
|
||||
* of being painted over by it.
|
||||
* Give every item in `scope` a view-transition name, so it is captured as
|
||||
* its own layer and rides *above* the travelling marker instead of being
|
||||
* painted over by it. Returns the items it named, so the caller can take
|
||||
* the names back off; transition() does this for its `scope` and clears
|
||||
* them when the change is over.
|
||||
*
|
||||
* This cannot live in CSS: a view-transition-name must be unique across the
|
||||
* whole document, and a stylesheet cannot count how many navigations a page
|
||||
* happens to contain. Items already carrying a name are left alone, so an
|
||||
* application can assign its own.
|
||||
* whole document, and a stylesheet cannot count items. Items already
|
||||
* carrying a name are left alone, so an application can assign its own.
|
||||
*/
|
||||
var carried = 0;
|
||||
|
||||
function liftLabels(scope) {
|
||||
var groups = scope.querySelectorAll(".nv-nav, .nv-nav-tabs, [data-nv-travel]");
|
||||
var items = scope.querySelectorAll(".nv-nav__item");
|
||||
var named = [];
|
||||
|
||||
for (var i = 0; i < groups.length; i++) {
|
||||
var items = groups[i].querySelectorAll(".nv-nav__item");
|
||||
|
||||
for (var j = 0; j < items.length; j++) {
|
||||
if (items[j].style.viewTransitionName) continue;
|
||||
carried += 1;
|
||||
items[j].style.viewTransitionName = "nv-carried-" + carried;
|
||||
}
|
||||
for (var j = 0; j < items.length; j++) {
|
||||
if (items[j].style.viewTransitionName) continue;
|
||||
carried += 1;
|
||||
items[j].style.viewTransitionName = "nv-carried-" + carried;
|
||||
named.push(items[j]);
|
||||
}
|
||||
return named;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reveal content as it scrolls into view. Elements are only marked as
|
||||
* waiting here, so a page without this script — or under reduced motion —
|
||||
* never hides anything. Each element arrives once and is then left alone.
|
||||
*/
|
||||
var revealer = null;
|
||||
|
||||
function arrive(target) {
|
||||
var items = target.classList.contains("nv-reveal-group") ? target.children : [target];
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var item = items[i];
|
||||
item.style.setProperty("--nv-reveal-index", String(i));
|
||||
item.dataset.nvReveal = "shown";
|
||||
item.addEventListener("animationend", function settle(event) {
|
||||
if (event.target !== this || event.animationName !== "nv-arrive") return;
|
||||
this.removeEventListener("animationend", settle);
|
||||
delete this.dataset.nvReveal;
|
||||
this.style.removeProperty("--nv-reveal-index");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function initReveal(scope) {
|
||||
if (prefersReducedMotion() || typeof global.IntersectionObserver !== "function") return;
|
||||
|
||||
revealer = revealer || new IntersectionObserver(function (entries) {
|
||||
entries.forEach(function (entry) {
|
||||
if (!entry.isIntersecting) return;
|
||||
revealer.unobserve(entry.target);
|
||||
arrive(entry.target);
|
||||
});
|
||||
}, { rootMargin: "0px 0px -8% 0px" });
|
||||
|
||||
scope.querySelectorAll(".nv-reveal, .nv-reveal-group").forEach(function (target) {
|
||||
if (target.dataset.nvRevealWired) return;
|
||||
target.dataset.nvRevealWired = "";
|
||||
var items = target.classList.contains("nv-reveal-group") ? target.children : [target];
|
||||
for (var i = 0; i < items.length; i++) items[i].dataset.nvReveal = "pending";
|
||||
revealer.observe(target);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The light on a .nv-card--spotlight sits under the pointer. One listener
|
||||
* for the whole document, so cards added later need no wiring.
|
||||
*/
|
||||
function trackSpotlight(event) {
|
||||
if (event.pointerType && event.pointerType !== "mouse") return;
|
||||
var surface = event.target.closest && event.target.closest(".nv-card--spotlight");
|
||||
if (!surface) return;
|
||||
var box = surface.getBoundingClientRect();
|
||||
surface.style.setProperty("--nv-pointer-x", event.clientX - box.left + "px");
|
||||
surface.style.setProperty("--nv-pointer-y", event.clientY - box.top + "px");
|
||||
}
|
||||
|
||||
function wire(scope) {
|
||||
|
|
@ -282,7 +372,7 @@
|
|||
|
||||
scope.querySelectorAll("[data-nv-tabs]").forEach(initTabs);
|
||||
scope.querySelectorAll("[data-nv-scroller]").forEach(initScroller);
|
||||
liftLabels(scope);
|
||||
initReveal(scope);
|
||||
|
||||
scope.querySelectorAll("[data-nv-close]").forEach(function (control) {
|
||||
control.addEventListener("click", function () {
|
||||
|
|
@ -314,6 +404,8 @@
|
|||
prefersReducedMotion: prefersReducedMotion,
|
||||
};
|
||||
|
||||
document.addEventListener("pointermove", trackSpotlight, { passive: true });
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", function () { wire(); });
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@
|
|||
light-dark() in tokens/color.css. Like the light appearance, almost nothing
|
||||
here is a literal color — every role is derived from --nv-brand.
|
||||
|
||||
Dark Niveus is not the light theme inverted. Surfaces get *lighter* as they
|
||||
rise, shadows deepen rather than disappear, and the top highlight thins to a
|
||||
faint rim so elevation still reads without glare. The accent moves up its
|
||||
own ramp too: a mid-tone that reads as vivid on paper reads as muddy on a
|
||||
dark canvas.
|
||||
Dark Niveus is warm graphite, not the light theme inverted. Surfaces get
|
||||
*lighter* as they rise and each step is large enough to see; shadows are
|
||||
deep and every raised thing carries a near-black contact line, because on a
|
||||
dark canvas a shadow alone has nothing left to darken. The bevel is what
|
||||
carries depth here — a lit top edge, a shaded bottom one — and filled color
|
||||
stays saturated: a vivid accent with a light label reads as a lit key, not
|
||||
as a hole.
|
||||
========================================================================== */
|
||||
|
||||
:root[data-theme="dark"] {
|
||||
|
|
@ -19,20 +21,20 @@
|
|||
:root,
|
||||
[data-nv-brand] {
|
||||
/* --- Canvas and surfaces ----------------------------------------------- */
|
||||
--nv-dark-background: color-mix(in oklab, #0b0e14, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-background-subtle: color-mix(in oklab, #10141b, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-surface: color-mix(in oklab, #171c24, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-surface-elevated: color-mix(in oklab, #1e242e, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-surface-inset: color-mix(in oklab, #10141b, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-surface-hover: color-mix(in oklab, #222835, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-surface-active: color-mix(in oklab, #2a3140, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-scrim: rgb(3 5 9 / 0.62);
|
||||
--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-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));
|
||||
--nv-dark-scrim: rgb(6 6 8 / 0.62);
|
||||
|
||||
/* --- Text --------------------------------------------------------------- */
|
||||
--nv-dark-text-primary: #e9edf4;
|
||||
--nv-dark-text-secondary: color-mix(in oklab, #a3aebf, var(--nv-brand) 8%);
|
||||
--nv-dark-text-tertiary: color-mix(in oklab, #7a8494, var(--nv-brand) 8%);
|
||||
--nv-dark-text-disabled: #58616f;
|
||||
--nv-dark-text-primary: #ecebe8;
|
||||
--nv-dark-text-secondary: color-mix(in oklab, #a9a8a3, var(--nv-brand) 8%);
|
||||
--nv-dark-text-tertiary: color-mix(in oklab, #7d7c77, var(--nv-brand) 8%);
|
||||
--nv-dark-text-disabled: #58585c;
|
||||
--nv-dark-text-inverted: var(--nv-palette-neutral-975);
|
||||
|
||||
/* --- Borders ------------------------------------------------------------ */
|
||||
|
|
@ -41,30 +43,38 @@
|
|||
--nv-dark-border-strong: rgb(255 255 255 / 0.2);
|
||||
|
||||
/* --- Accent -------------------------------------------------------------
|
||||
Lifted up the ramp and slightly desaturated: saturated mid-tones read as
|
||||
muddy on dark canvases, and fully saturated light ones read as neon. The
|
||||
floor also puts every filled color above the pivot, so on a dark canvas
|
||||
the derived label is always the dark one — which is what makes an accent
|
||||
button read as lit rather than as a hole. */
|
||||
--nv-dark-accent: oklch(from var(--nv-brand) clamp(0.72, calc(l + 0.12), 0.86) clamp(0.02, calc(c * 0.92), 0.2) h);
|
||||
The same derivation as light: a fill is pushed out of the mid-tones until
|
||||
its label clears 4.5:1, so the brand keeps its full saturation and a light
|
||||
label. Earlier Niveus lifted the dark accent into pastel to avoid neon;
|
||||
with bevel and sheen doing the shaping, a saturated fill reads as a lit
|
||||
key instead, and the pastel was what made the whole system feel washed
|
||||
out. Only the accent *text* (links, subtle labels) moves up the ramp,
|
||||
because text on a dark canvas needs the lightness that a fill does not. */
|
||||
--nv-dark-accent: oklch(from var(--nv-brand) calc(
|
||||
clamp(0, (var(--nv-contrast-pivot) - l) * 100, 1) * min(l, var(--nv-contrast-dark)) +
|
||||
(1 - clamp(0, (var(--nv-contrast-pivot) - l) * 100, 1)) * max(l, var(--nv-contrast-light))
|
||||
) clamp(0.02, c, 0.25) h);
|
||||
--nv-dark-accent-hover: oklch(from var(--nv-dark-accent) calc(l + 0.05) c h);
|
||||
--nv-dark-accent-active: oklch(from var(--nv-dark-accent) calc(l + 0.09) c h);
|
||||
--nv-dark-accent-subtle: oklch(from var(--nv-dark-accent) l c h / 0.15);
|
||||
--nv-dark-accent-subtle-hover: oklch(from var(--nv-dark-accent) l c h / 0.22);
|
||||
--nv-dark-accent-border: oklch(from var(--nv-dark-accent) l c h / 0.38);
|
||||
--nv-dark-accent-text: oklch(from var(--nv-dark-accent) clamp(0.72, calc(l + 0.08), 0.9) calc(c * 0.9) h);
|
||||
--nv-dark-accent-subtle: oklch(from var(--nv-dark-accent) l c h / 0.2);
|
||||
--nv-dark-accent-subtle-hover: oklch(from var(--nv-dark-accent) l c h / 0.28);
|
||||
--nv-dark-accent-border: oklch(from var(--nv-dark-accent) l c h / 0.5);
|
||||
--nv-dark-accent-text: oklch(from var(--nv-dark-accent) clamp(0.74, calc(l + 0.2), 0.9) calc(c * 0.9) h);
|
||||
--nv-dark-on-accent: oklch(from var(--nv-dark-accent)
|
||||
calc(0.14 + clamp(0, (0.68 - l) * 100, 1) * 0.84)
|
||||
calc(c * 0.2) h);
|
||||
|
||||
/* --- Success ------------------------------------------------------------ */
|
||||
--nv-dark-success: oklch(from
|
||||
color-mix(in oklab, var(--nv-palette-success-400), var(--nv-brand) var(--nv-brand-harmony))
|
||||
clamp(0.72, calc(l + 0.1), 0.88) clamp(0.02, calc(c * 0.9), 0.18) h);
|
||||
color-mix(in oklab, var(--nv-palette-success-500), var(--nv-brand) var(--nv-brand-harmony))
|
||||
calc(
|
||||
clamp(0, (var(--nv-contrast-pivot) - l) * 100, 1) * min(l, var(--nv-contrast-dark)) +
|
||||
(1 - clamp(0, (var(--nv-contrast-pivot) - l) * 100, 1)) * max(l, var(--nv-contrast-light))
|
||||
) clamp(0.02, c, 0.25) h);
|
||||
--nv-dark-success-hover: oklch(from var(--nv-dark-success) calc(l + 0.05) c h);
|
||||
--nv-dark-success-subtle: oklch(from var(--nv-dark-success) l c h / 0.15);
|
||||
--nv-dark-success-subtle: oklch(from var(--nv-dark-success) l c h / 0.2);
|
||||
--nv-dark-success-border: oklch(from var(--nv-dark-success) l c h / 0.35);
|
||||
--nv-dark-success-text: oklch(from var(--nv-dark-success) clamp(0.74, calc(l + 0.08), 0.9) calc(c * 0.9) h);
|
||||
--nv-dark-success-text: oklch(from var(--nv-dark-success) clamp(0.76, calc(l + 0.2), 0.9) calc(c * 0.9) h);
|
||||
--nv-dark-on-success: oklch(from var(--nv-dark-success)
|
||||
calc(0.14 + clamp(0, (0.68 - l) * 100, 1) * 0.84)
|
||||
calc(c * 0.2) h);
|
||||
|
|
@ -72,49 +82,78 @@
|
|||
/* --- Warning ------------------------------------------------------------ */
|
||||
--nv-dark-warning: oklch(from
|
||||
color-mix(in oklab, var(--nv-palette-warning-400), var(--nv-brand) var(--nv-brand-harmony))
|
||||
clamp(0.74, calc(l + 0.1), 0.88) clamp(0.02, calc(c * 0.9), 0.18) h);
|
||||
calc(
|
||||
clamp(0, (var(--nv-contrast-pivot) - l) * 100, 1) * min(l, var(--nv-contrast-dark)) +
|
||||
(1 - clamp(0, (var(--nv-contrast-pivot) - l) * 100, 1)) * max(l, var(--nv-contrast-light))
|
||||
) clamp(0.02, c, 0.25) h);
|
||||
--nv-dark-warning-hover: oklch(from var(--nv-dark-warning) calc(l + 0.05) c h);
|
||||
--nv-dark-warning-subtle: oklch(from var(--nv-dark-warning) l c h / 0.15);
|
||||
--nv-dark-warning-subtle: oklch(from var(--nv-dark-warning) l c h / 0.2);
|
||||
--nv-dark-warning-border: oklch(from var(--nv-dark-warning) l c h / 0.35);
|
||||
--nv-dark-warning-text: oklch(from var(--nv-dark-warning) clamp(0.76, calc(l + 0.08), 0.9) calc(c * 0.9) h);
|
||||
--nv-dark-warning-text: oklch(from var(--nv-dark-warning) clamp(0.76, calc(l + 0.2), 0.9) calc(c * 0.9) h);
|
||||
--nv-dark-on-warning: oklch(from var(--nv-dark-warning)
|
||||
calc(0.14 + clamp(0, (0.68 - l) * 100, 1) * 0.84)
|
||||
calc(c * 0.2) h);
|
||||
|
||||
/* --- Danger ------------------------------------------------------------- */
|
||||
--nv-dark-danger: oklch(from
|
||||
color-mix(in oklab, var(--nv-palette-danger-400), var(--nv-brand) var(--nv-brand-harmony))
|
||||
clamp(0.72, calc(l + 0.1), 0.88) clamp(0.02, calc(c * 0.9), 0.18) h);
|
||||
color-mix(in oklab, var(--nv-palette-danger-500), var(--nv-brand) var(--nv-brand-harmony))
|
||||
calc(
|
||||
clamp(0, (var(--nv-contrast-pivot) - l) * 100, 1) * min(l, var(--nv-contrast-dark)) +
|
||||
(1 - clamp(0, (var(--nv-contrast-pivot) - l) * 100, 1)) * max(l, var(--nv-contrast-light))
|
||||
) clamp(0.02, c, 0.25) h);
|
||||
--nv-dark-danger-hover: oklch(from var(--nv-dark-danger) calc(l + 0.05) c h);
|
||||
--nv-dark-danger-active: oklch(from var(--nv-dark-danger) calc(l + 0.09) c h);
|
||||
--nv-dark-danger-subtle: oklch(from var(--nv-dark-danger) l c h / 0.15);
|
||||
--nv-dark-danger-subtle: oklch(from var(--nv-dark-danger) l c h / 0.2);
|
||||
--nv-dark-danger-border: oklch(from var(--nv-dark-danger) l c h / 0.35);
|
||||
--nv-dark-danger-text: oklch(from var(--nv-dark-danger) clamp(0.74, calc(l + 0.08), 0.9) calc(c * 0.9) h);
|
||||
--nv-dark-danger-text: oklch(from var(--nv-dark-danger) clamp(0.76, calc(l + 0.2), 0.9) calc(c * 0.9) h);
|
||||
--nv-dark-on-danger: oklch(from var(--nv-dark-danger)
|
||||
calc(0.14 + clamp(0, (0.68 - l) * 100, 1) * 0.84)
|
||||
calc(c * 0.2) h);
|
||||
|
||||
/* --- Depth and material -------------------------------------------------- */
|
||||
--nv-dark-shadow-ambient: rgb(0 0 0 / 0.3);
|
||||
--nv-dark-shadow-penumbra: rgb(0 0 0 / 0.4);
|
||||
--nv-dark-shadow-umbra: rgb(0 0 0 / 0.35);
|
||||
--nv-dark-shadow-inset: rgb(0 0 0 / 0.5);
|
||||
--nv-dark-highlight: rgb(255 255 255 / 0.07);
|
||||
--nv-dark-highlight-strong: rgb(255 255 255 / 0.12);
|
||||
--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-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);
|
||||
|
||||
/* The contact line is what separates graphite from graphite: a raised
|
||||
surface over a dark canvas has no shadow to show, but it always has an
|
||||
edge. */
|
||||
--nv-dark-lowlight: rgb(0 0 0 / 0.5);
|
||||
--nv-dark-outline: rgb(0 0 0 / 0.72);
|
||||
|
||||
--nv-dark-sheen-top: rgb(255 255 255 / 0.09);
|
||||
--nv-dark-sheen-bottom: rgb(0 0 0 / 0.18);
|
||||
--nv-dark-sheen-strong-top: rgb(255 255 255 / 0.3);
|
||||
--nv-dark-sheen-strong-bottom: rgb(0 0 0 / 0.2);
|
||||
--nv-dark-glow: oklch(from var(--nv-dark-accent) l c h / 0.5);
|
||||
|
||||
--nv-dark-knob: #f4f3ef;
|
||||
--nv-dark-knob-shade: rgb(40 32 20 / 0.3);
|
||||
|
||||
/* The higher a surface sits, the more light reaches it. */
|
||||
--nv-dark-elevation-tint-1: rgb(255 255 255 / 0.025);
|
||||
--nv-dark-elevation-tint-2: rgb(255 255 255 / 0.042);
|
||||
--nv-dark-elevation-tint-3: rgb(255 255 255 / 0.06);
|
||||
--nv-dark-elevation-tint-4: rgb(255 255 255 / 0.078);
|
||||
--nv-dark-elevation-tint-5: rgb(255 255 255 / 0.095);
|
||||
--nv-dark-elevation-tint-1: rgb(255 255 255 / 0.03);
|
||||
--nv-dark-elevation-tint-2: rgb(255 255 255 / 0.05);
|
||||
--nv-dark-elevation-tint-3: rgb(255 255 255 / 0.07);
|
||||
--nv-dark-elevation-tint-4: rgb(255 255 255 / 0.09);
|
||||
--nv-dark-elevation-tint-5: rgb(255 255 255 / 0.11);
|
||||
|
||||
--nv-dark-material-frosted: color-mix(in oklab, rgb(23 28 36 / 0.72), var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-material-glass: color-mix(in oklab, rgb(30 36 46 / 0.55), var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-material-border: rgb(255 255 255 / 0.1);
|
||||
--nv-dark-material-frosted: color-mix(in oklab, rgb(34 34 40 / 0.5), var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-material-glass: color-mix(in oklab, rgb(44 44 52 / 0.36), var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-dark-material-border: rgb(255 255 255 / 0.14);
|
||||
|
||||
/* --- Window controls --------------------------------------------------------- */
|
||||
--nv-dark-window-close: var(--nv-palette-window-close);
|
||||
--nv-dark-window-minimize: var(--nv-palette-window-minimize);
|
||||
--nv-dark-window-zoom: var(--nv-palette-window-zoom);
|
||||
--nv-dark-window-inactive: var(--nv-palette-neutral-700);
|
||||
--nv-dark-window-control-ring: rgb(0 0 0 / 0.35);
|
||||
--nv-dark-window-glyph: rgb(0 0 0 / 0.6);
|
||||
|
||||
/* --- Interaction ---------------------------------------------------------- */
|
||||
--nv-dark-focus-ring: oklch(from var(--nv-dark-accent) l c h / 0.5);
|
||||
--nv-dark-focus-ring-contrast: rgb(6 8 12 / 0.9);
|
||||
--nv-dark-selection: oklch(from var(--nv-dark-accent) l c h / 0.3);
|
||||
--nv-dark-focus-ring: oklch(from var(--nv-dark-accent-text) l c h / 0.55);
|
||||
--nv-dark-focus-ring-contrast: rgb(8 8 10 / 0.9);
|
||||
--nv-dark-selection: oklch(from var(--nv-dark-accent) l c h / 0.4);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,12 @@
|
|||
how much of the brand bleeds into the greys. See tokens/color.css for the
|
||||
inputs.
|
||||
|
||||
Light Niveus reads as paper on a cool, slightly recessed canvas: surfaces are
|
||||
near-white, depth comes from soft shadow rather than from contrast, and a
|
||||
crisp top highlight gives raised elements their edge.
|
||||
Light Niveus reads as soft paper and enamel on a warm grey desk: surfaces
|
||||
that hold content stay near-white, so documents never look yellowed; the
|
||||
canvas around them is a few steps warmer and darker, so a window sits *on*
|
||||
something. Raised controls are bevelled — a bright top edge, a soft shade
|
||||
along the bottom, light falling across the face — and shadows are warm,
|
||||
the color of shade on paper rather than of a grey filter.
|
||||
========================================================================== */
|
||||
|
||||
:root {
|
||||
|
|
@ -32,14 +35,14 @@
|
|||
Paper stays paper: the two surfaces that hold content are left untinted so
|
||||
text keeps its contrast. Everything the surfaces sit *in* picks up a trace
|
||||
of the brand, which is what stops an accent from looking stuck on. */
|
||||
--nv-light-background: color-mix(in oklab, #e9ecf3, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-light-background-subtle: color-mix(in oklab, #eff1f6, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-light-surface: var(--nv-palette-neutral-0);
|
||||
--nv-light-surface-elevated: var(--nv-palette-neutral-0);
|
||||
--nv-light-surface-inset: color-mix(in oklab, #e4e8f0, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-light-surface-hover: color-mix(in oklab, #f4f6fa, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-light-surface-active: color-mix(in oklab, #e8ebf2, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-light-scrim: rgb(16 21 30 / 0.32);
|
||||
--nv-light-background: color-mix(in oklab, #e8e5de, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-light-background-subtle: color-mix(in oklab, #eeece6, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-light-surface: #fbfbfa;
|
||||
--nv-light-surface-elevated: #f8f8f6;
|
||||
--nv-light-surface-inset: color-mix(in oklab, #eeede8, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-light-surface-hover: color-mix(in oklab, #f1f0ec, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-light-surface-active: color-mix(in oklab, #e6e5e0, var(--nv-brand) var(--nv-brand-tint));
|
||||
--nv-light-scrim: rgb(28 24 18 / 0.34);
|
||||
|
||||
/* --- Text ---------------------------------------------------------------
|
||||
Primary text stays neutral — it carries the reading contrast. The quieter
|
||||
|
|
@ -52,9 +55,9 @@
|
|||
--nv-light-text-inverted: var(--nv-palette-neutral-0);
|
||||
|
||||
/* --- Borders ------------------------------------------------------------ */
|
||||
--nv-light-border-subtle: rgb(16 21 30 / 0.06);
|
||||
--nv-light-border: rgb(16 21 30 / 0.11);
|
||||
--nv-light-border-strong: rgb(16 21 30 / 0.2);
|
||||
--nv-light-border-subtle: rgb(28 24 18 / 0.07);
|
||||
--nv-light-border: rgb(28 24 18 / 0.13);
|
||||
--nv-light-border-strong: rgb(28 24 18 / 0.22);
|
||||
|
||||
/* --- Accent --------------------------------------------------------------
|
||||
Every filled color is pushed away from the middle of the lightness range
|
||||
|
|
@ -132,13 +135,27 @@
|
|||
/* --- Depth and material -------------------------------------------------
|
||||
Three shadow colors build every elevation level: an ambient contact
|
||||
shadow, a soft penumbra, and a deeper umbra for high elevations. */
|
||||
--nv-light-shadow-ambient: rgb(16 21 30 / 0.05);
|
||||
--nv-light-shadow-penumbra: rgb(16 21 30 / 0.08);
|
||||
--nv-light-shadow-umbra: rgb(16 21 30 / 0.06);
|
||||
--nv-light-shadow-inset: rgb(16 21 30 / 0.12);
|
||||
--nv-light-highlight: rgb(255 255 255 / 0.85);
|
||||
--nv-light-shadow-ambient: rgb(40 32 20 / 0.08);
|
||||
--nv-light-shadow-penumbra: rgb(40 32 20 / 0.1);
|
||||
--nv-light-shadow-umbra: rgb(40 32 20 / 0.16);
|
||||
--nv-light-shadow-inset: rgb(40 32 20 / 0.14);
|
||||
--nv-light-highlight: rgb(255 255 255 / 0.9);
|
||||
--nv-light-highlight-strong: rgb(255 255 255 / 1);
|
||||
|
||||
/* Paper already has a border doing the contact work, so the outline only
|
||||
softens it; the bottom shade is faint for the same reason. */
|
||||
--nv-light-lowlight: rgb(40 32 20 / 0.09);
|
||||
--nv-light-outline: rgb(40 32 20 / 0.05);
|
||||
|
||||
--nv-light-sheen-top: rgb(255 255 255 / 0.9);
|
||||
--nv-light-sheen-bottom: rgb(40 32 20 / 0.07);
|
||||
--nv-light-sheen-strong-top: rgb(255 255 255 / 0.32);
|
||||
--nv-light-sheen-strong-bottom: rgb(20 16 10 / 0.16);
|
||||
--nv-light-glow: oklch(from var(--nv-light-accent) l c h / 0.3);
|
||||
|
||||
--nv-light-knob: var(--nv-palette-neutral-0);
|
||||
--nv-light-knob-shade: rgb(40 32 20 / 0.13);
|
||||
|
||||
/* Light surfaces are already lighter than their canvas; the shadow alone
|
||||
carries elevation, so there is nothing to add. */
|
||||
--nv-light-elevation-tint-1: transparent;
|
||||
|
|
@ -147,9 +164,17 @@
|
|||
--nv-light-elevation-tint-4: transparent;
|
||||
--nv-light-elevation-tint-5: transparent;
|
||||
|
||||
--nv-light-material-frosted: rgb(255 255 255 / 0.72);
|
||||
--nv-light-material-glass: rgb(255 255 255 / 0.5);
|
||||
--nv-light-material-border: rgb(255 255 255 / 0.55);
|
||||
--nv-light-material-frosted: rgb(252 252 250 / 0.56);
|
||||
--nv-light-material-glass: rgb(252 252 250 / 0.4);
|
||||
--nv-light-material-border: rgb(255 255 255 / 0.72);
|
||||
|
||||
/* --- Window controls --------------------------------------------------------- */
|
||||
--nv-light-window-close: var(--nv-palette-window-close);
|
||||
--nv-light-window-minimize: var(--nv-palette-window-minimize);
|
||||
--nv-light-window-zoom: var(--nv-palette-window-zoom);
|
||||
--nv-light-window-inactive: var(--nv-palette-neutral-300);
|
||||
--nv-light-window-control-ring: rgb(0 0 0 / 0.16);
|
||||
--nv-light-window-glyph: rgb(0 0 0 / 0.55);
|
||||
|
||||
/* --- Interaction ----------------------------------------------------------- */
|
||||
--nv-light-focus-ring: oklch(from var(--nv-light-accent) l c h / 0.4);
|
||||
|
|
|
|||
|
|
@ -53,25 +53,37 @@
|
|||
}
|
||||
|
||||
:root {
|
||||
/* --- Neutral ---------------------------------------------------------- */
|
||||
/* --- Neutral ----------------------------------------------------------
|
||||
Warm graphite to warm paper. A grey that leans very slightly towards
|
||||
yellow reads as a material (metal, enamel, paper); a grey that leans blue
|
||||
reads as a screen. The lean is small enough that the brand tint still
|
||||
decides the family. */
|
||||
--nv-palette-neutral-0: #ffffff;
|
||||
--nv-palette-neutral-25: #fbfcfe;
|
||||
--nv-palette-neutral-50: #f7f8fb;
|
||||
--nv-palette-neutral-100: #f1f3f8;
|
||||
--nv-palette-neutral-150: #e8ebf2;
|
||||
--nv-palette-neutral-200: #dfe3ec;
|
||||
--nv-palette-neutral-300: #ccd2de;
|
||||
--nv-palette-neutral-400: #a7b0c0;
|
||||
--nv-palette-neutral-500: #7c8797;
|
||||
--nv-palette-neutral-600: #5b6574;
|
||||
--nv-palette-neutral-700: #434b59;
|
||||
--nv-palette-neutral-800: #2d3340;
|
||||
--nv-palette-neutral-850: #232936;
|
||||
--nv-palette-neutral-900: #1a1f29;
|
||||
--nv-palette-neutral-925: #151a22;
|
||||
--nv-palette-neutral-950: #10141b;
|
||||
--nv-palette-neutral-975: #0b0e14;
|
||||
--nv-palette-neutral-1000: #06080c;
|
||||
--nv-palette-neutral-25: #fcfcfb;
|
||||
--nv-palette-neutral-50: #f8f8f6;
|
||||
--nv-palette-neutral-100: #f2f1ee;
|
||||
--nv-palette-neutral-150: #eae9e5;
|
||||
--nv-palette-neutral-200: #e1e0db;
|
||||
--nv-palette-neutral-300: #cfcec8;
|
||||
--nv-palette-neutral-400: #aeada7;
|
||||
--nv-palette-neutral-500: #87867f;
|
||||
--nv-palette-neutral-600: #63625d;
|
||||
--nv-palette-neutral-700: #4a4945;
|
||||
--nv-palette-neutral-800: #343337;
|
||||
--nv-palette-neutral-850: #2a2a2d;
|
||||
--nv-palette-neutral-900: #1d1d20;
|
||||
--nv-palette-neutral-925: #18181a;
|
||||
--nv-palette-neutral-950: #131315;
|
||||
--nv-palette-neutral-975: #0e0e10;
|
||||
--nv-palette-neutral-1000: #08080a;
|
||||
|
||||
/* --- Window controls ----------------------------------------------------
|
||||
The three lights at the start of a titlebar. They are the one place the
|
||||
system uses fixed colors instead of the brand: their meaning is learned
|
||||
from position and color together, and re-coloring them would unteach it. */
|
||||
--nv-palette-window-close: #ff5f57;
|
||||
--nv-palette-window-minimize: #febc2e;
|
||||
--nv-palette-window-zoom: #28c840;
|
||||
|
||||
/* --- Accent (indigo) --------------------------------------------------- */
|
||||
--nv-palette-accent-50: #eef2ff;
|
||||
|
|
@ -198,6 +210,33 @@
|
|||
--nv-color-highlight: light-dark(var(--nv-light-highlight), var(--nv-dark-highlight));
|
||||
--nv-color-highlight-strong: light-dark(var(--nv-light-highlight-strong), var(--nv-dark-highlight-strong));
|
||||
|
||||
/* The bevel's other half: the shade along a raised surface's bottom edge,
|
||||
and the contact line around anything that sits on another surface. Light
|
||||
along the top and shade along the bottom is what makes a control read as
|
||||
something pressable instead of a painted rectangle. */
|
||||
--nv-color-lowlight: light-dark(var(--nv-light-lowlight), var(--nv-dark-lowlight));
|
||||
--nv-color-outline: light-dark(var(--nv-light-outline), var(--nv-dark-outline));
|
||||
|
||||
/* Sheen: the light falling across a raised face, top to bottom. The plain
|
||||
pair is for neutral controls and surfaces; the strong pair is for filled
|
||||
color (primary buttons, checked marks), where it has to read through a
|
||||
saturated fill. Both are laid over a surface's own color, never instead
|
||||
of it. */
|
||||
--nv-color-sheen-top: light-dark(var(--nv-light-sheen-top), var(--nv-dark-sheen-top));
|
||||
--nv-color-sheen-bottom: light-dark(var(--nv-light-sheen-bottom), var(--nv-dark-sheen-bottom));
|
||||
--nv-color-sheen-strong-top: light-dark(var(--nv-light-sheen-strong-top), var(--nv-dark-sheen-strong-top));
|
||||
--nv-color-sheen-strong-bottom: light-dark(var(--nv-light-sheen-strong-bottom), var(--nv-dark-sheen-strong-bottom));
|
||||
|
||||
/* Glow: light a surface gives off, as opposed to the shade it casts. It is
|
||||
the accent seen through air — strong enough in dark to read as a lit
|
||||
edge, faint in light, where paper does not emit. */
|
||||
--nv-color-glow: light-dark(var(--nv-light-glow), var(--nv-dark-glow));
|
||||
|
||||
/* A knob: the thumb of a switch or a slider. It stays light in both
|
||||
appearances, the way a physical knob does not change color with the room. */
|
||||
--nv-color-knob: light-dark(var(--nv-light-knob), var(--nv-dark-knob));
|
||||
--nv-color-knob-shade: light-dark(var(--nv-light-knob-shade), var(--nv-dark-knob-shade));
|
||||
|
||||
/* Elevation tint: on a dark canvas a shadow has nothing to darken, so a
|
||||
raised surface also has to gain light. Transparent in light, where the
|
||||
shadow does all the work. */
|
||||
|
|
@ -211,6 +250,14 @@
|
|||
--nv-color-material-glass: light-dark(var(--nv-light-material-glass), var(--nv-dark-material-glass));
|
||||
--nv-color-material-border: light-dark(var(--nv-light-material-border), var(--nv-dark-material-border));
|
||||
|
||||
/* --- Window controls ------------------------------------------------------- */
|
||||
--nv-color-window-close: light-dark(var(--nv-light-window-close), var(--nv-dark-window-close));
|
||||
--nv-color-window-minimize: light-dark(var(--nv-light-window-minimize), var(--nv-dark-window-minimize));
|
||||
--nv-color-window-zoom: light-dark(var(--nv-light-window-zoom), var(--nv-dark-window-zoom));
|
||||
--nv-color-window-inactive: light-dark(var(--nv-light-window-inactive), var(--nv-dark-window-inactive));
|
||||
--nv-color-window-control-ring: light-dark(var(--nv-light-window-control-ring), var(--nv-dark-window-control-ring));
|
||||
--nv-color-window-glyph: light-dark(var(--nv-light-window-glyph), var(--nv-dark-window-glyph));
|
||||
|
||||
/* --- Interaction --------------------------------------------------------- */
|
||||
--nv-color-focus-ring: light-dark(var(--nv-light-focus-ring), var(--nv-dark-focus-ring));
|
||||
--nv-color-focus-ring-contrast: light-dark(var(--nv-light-focus-ring-contrast), var(--nv-dark-focus-ring-contrast));
|
||||
|
|
|
|||
|
|
@ -22,34 +22,35 @@
|
|||
|
||||
--nv-elevation-1:
|
||||
0 1px 1px var(--nv-color-shadow-ambient),
|
||||
0 1px 3px -1px var(--nv-color-shadow-penumbra);
|
||||
0 2px 4px -1px var(--nv-color-shadow-penumbra),
|
||||
0 6px 14px -8px var(--nv-color-shadow-umbra);
|
||||
|
||||
--nv-elevation-2:
|
||||
0 1px 1px var(--nv-color-shadow-ambient),
|
||||
0 2px 4px -1px var(--nv-color-shadow-penumbra),
|
||||
0 4px 8px -4px var(--nv-color-shadow-umbra);
|
||||
0 3px 6px -2px var(--nv-color-shadow-penumbra),
|
||||
0 10px 20px -10px var(--nv-color-shadow-umbra);
|
||||
|
||||
--nv-elevation-3:
|
||||
0 1px 1px var(--nv-color-shadow-ambient),
|
||||
0 4px 8px -2px var(--nv-color-shadow-penumbra),
|
||||
0 10px 20px -6px var(--nv-color-shadow-umbra);
|
||||
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);
|
||||
|
||||
--nv-elevation-4:
|
||||
0 1px 2px var(--nv-color-shadow-ambient),
|
||||
0 8px 16px -4px var(--nv-color-shadow-penumbra),
|
||||
0 20px 40px -10px var(--nv-color-shadow-umbra);
|
||||
0 10px 20px -6px var(--nv-color-shadow-penumbra),
|
||||
0 30px 60px -16px var(--nv-color-shadow-umbra);
|
||||
|
||||
--nv-elevation-5:
|
||||
0 1px 2px var(--nv-color-shadow-ambient),
|
||||
0 14px 28px -6px var(--nv-color-shadow-penumbra),
|
||||
0 32px 64px -14px var(--nv-color-shadow-umbra);
|
||||
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);
|
||||
|
||||
/* --- Inset ---------------------------------------------------------------
|
||||
Recessed surfaces: fields, wells, tracks, pressed controls. */
|
||||
--nv-elevation-inset-sm: inset 0 1px 2px var(--nv-color-shadow-inset);
|
||||
--nv-elevation-inset-md:
|
||||
inset 0 1px 1px var(--nv-color-shadow-inset),
|
||||
inset 0 2px 5px -1px var(--nv-color-shadow-inset);
|
||||
inset 0 3px 6px -2px var(--nv-color-shadow-inset);
|
||||
--nv-elevation-inset-lg:
|
||||
inset 0 2px 3px var(--nv-color-shadow-inset),
|
||||
inset 0 6px 12px -3px var(--nv-color-shadow-inset);
|
||||
|
|
@ -64,6 +65,29 @@
|
|||
inset 0 1px 0 var(--nv-color-highlight),
|
||||
inset 0 0 0 1px var(--nv-color-border-subtle);
|
||||
|
||||
/* --- Bevel -----------------------------------------------------------------
|
||||
The highlight's other half. A raised face is lit along its top edge and
|
||||
shaded along its bottom one; together they are the bevel, and the bevel is
|
||||
what makes a Niveus control look like something a finger could press.
|
||||
Raised controls and surfaces take the full bevel; the highlight alone is
|
||||
for things that sit flush. */
|
||||
--nv-lowlight-bottom: inset 0 -1px 0 var(--nv-color-lowlight);
|
||||
--nv-bevel:
|
||||
inset 0 1px 0 var(--nv-color-highlight),
|
||||
inset 0 -1px 0 var(--nv-color-lowlight);
|
||||
|
||||
/* --- Outline ---------------------------------------------------------------
|
||||
The contact line: a hairline just outside a raised surface, where it meets
|
||||
whatever it rests on. In dark it is near-black and does most of the
|
||||
separating; in light it only softens the border beside it. */
|
||||
--nv-outline: 0 0 0 1px var(--nv-color-outline);
|
||||
|
||||
/* --- Ledge -----------------------------------------------------------------
|
||||
A recess catches light on its *lower* lip, the edge facing the viewer.
|
||||
Paired with an inset shadow, it turns a field from a dark rectangle into
|
||||
a well cut into the surface. */
|
||||
--nv-ledge: 0 1px 0 var(--nv-color-highlight);
|
||||
|
||||
/* --- Focus ---------------------------------------------------------------
|
||||
A contrast ring sits under the accent ring so focus stays visible on both
|
||||
light and dark surfaces, and on accent-filled controls. */
|
||||
|
|
@ -71,4 +95,30 @@
|
|||
--nv-focus-ring:
|
||||
0 0 0 1px var(--nv-color-focus-ring-contrast),
|
||||
0 0 0 calc(1px + var(--nv-focus-ring-width)) var(--nv-color-focus-ring);
|
||||
|
||||
/* --- Glow ------------------------------------------------------------------
|
||||
Light a surface emits rather than the shade it casts. Glow is for things
|
||||
that are *on* — a live status, the selected item in a showcase, a lit key —
|
||||
and it always sits beside an elevation, never instead of one: a glowing
|
||||
surface still rests on something.
|
||||
|
||||
The geometry is a scale like elevation. The color is the component's: a
|
||||
tinted surface glows in its own tint, mixed down to --nv-glow-strength. */
|
||||
--nv-glow-strength: 40%;
|
||||
--nv-glow-blur-sm: 12px;
|
||||
--nv-glow-spread-sm: -2px;
|
||||
--nv-glow-blur-md: 28px;
|
||||
--nv-glow-spread-md: -6px;
|
||||
--nv-glow-blur-lg: 64px;
|
||||
--nv-glow-spread-lg: -12px;
|
||||
}
|
||||
|
||||
/* The accent glows are declared on every branded subtree as well as the root:
|
||||
a var() inside a custom property resolves where the property is declared, so
|
||||
a root-only declaration would glow in the root brand everywhere. */
|
||||
:root,
|
||||
[data-nv-brand] {
|
||||
--nv-glow-sm: 0 0 var(--nv-glow-blur-sm) var(--nv-glow-spread-sm) var(--nv-color-glow);
|
||||
--nv-glow-md: 0 0 var(--nv-glow-blur-md) var(--nv-glow-spread-md) var(--nv-color-glow);
|
||||
--nv-glow-lg: 0 0 var(--nv-glow-blur-lg) var(--nv-glow-spread-lg) var(--nv-color-glow);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,29 +8,50 @@
|
|||
solid opaque, stable. The default. Content lives here.
|
||||
elevated solid with depth and a top highlight. Separated from context.
|
||||
inset recessed into its parent. Fields, wells, tracks.
|
||||
frosted translucent, heavily blurred. Chrome that sits over content:
|
||||
navigation bars, toolbars, sticky headers.
|
||||
glass more translucent, less blurred. Contextual layers where the
|
||||
frosted translucent, moderately blurred, bevelled. Chrome that sits
|
||||
over content: bars, docks, launchers, notifications, menus.
|
||||
glass more translucent, lightly blurred. Contextual layers where the
|
||||
content behind should stay legible: overlays, hover cards.
|
||||
|
||||
Translucency has a cost — it tints whatever is behind it and forces the GPU
|
||||
to composite. Use frosted and glass for layers that move over content, not
|
||||
for content itself.
|
||||
Niveus is a solid system with glass used sparingly. Windows, cards, dialogs
|
||||
and everything that holds content are solid; translucency is reserved for
|
||||
the chrome that floats over them. Translucency has a cost — it tints
|
||||
whatever is behind it and forces the GPU to composite — and used everywhere
|
||||
it stops meaning "this floats".
|
||||
|
||||
Sheen is the light falling across a raised face. It is laid over a
|
||||
surface's own color as background-image, so a surface keeps its role color
|
||||
and gains the gradient; anything that does not draw gradients simply shows
|
||||
the role color.
|
||||
========================================================================== */
|
||||
|
||||
:root {
|
||||
/* --- Blur ---------------------------------------------------------------- */
|
||||
--nv-blur-sm: 8px;
|
||||
--nv-blur-md: 16px;
|
||||
--nv-blur-lg: 28px;
|
||||
--nv-blur-xl: 44px;
|
||||
--nv-blur-sm: 12px;
|
||||
--nv-blur-md: 24px;
|
||||
--nv-blur-lg: 36px;
|
||||
--nv-blur-xl: 56px;
|
||||
|
||||
/* Backdrops lose saturation when blurred; a touch of saturate() puts the
|
||||
color back so frosted surfaces do not read as gray films. */
|
||||
color back so frosted surfaces do not read as gray films. Moderate on
|
||||
purpose: the blur says "this floats", it is not the look itself. */
|
||||
--nv-backdrop-saturate: 180%;
|
||||
--nv-backdrop-frosted: blur(var(--nv-blur-lg)) saturate(var(--nv-backdrop-saturate));
|
||||
--nv-backdrop-glass: blur(var(--nv-blur-md)) saturate(var(--nv-backdrop-saturate));
|
||||
--nv-backdrop-scrim: blur(var(--nv-blur-sm)) saturate(120%);
|
||||
--nv-backdrop-frosted: blur(var(--nv-blur-md)) saturate(var(--nv-backdrop-saturate));
|
||||
--nv-backdrop-glass: blur(var(--nv-blur-sm)) saturate(var(--nv-backdrop-saturate));
|
||||
|
||||
/* --- Sheen ------------------------------------------------------------------
|
||||
raised neutral controls: buttons, the selected tab, a titlebar
|
||||
filled saturated fills: primary and danger buttons, checked marks
|
||||
surface large surfaces: windows, dialogs, cards. Brightest along the
|
||||
top edge, easing through the middle and settling into a faint
|
||||
shade at the foot, so a panel reads as a curved, lit body
|
||||
rather than as a flat sheet.
|
||||
knob a round thumb: lit from above, shaded towards its rim */
|
||||
--nv-sheen-raised: linear-gradient(var(--nv-color-sheen-top), var(--nv-color-sheen-bottom));
|
||||
--nv-sheen-filled: linear-gradient(var(--nv-color-sheen-strong-top), var(--nv-color-sheen-strong-bottom));
|
||||
--nv-sheen-surface: linear-gradient(var(--nv-color-sheen-top), transparent 7rem, transparent 55%, var(--nv-color-sheen-bottom));
|
||||
--nv-sheen-knob: radial-gradient(circle at 50% 22%, var(--nv-color-highlight-strong), transparent 45%), radial-gradient(circle at 50% 30%, transparent 40%, var(--nv-color-knob-shade));
|
||||
--nv-backdrop-scrim: blur(var(--nv-blur-sm)) saturate(140%);
|
||||
|
||||
/* --- Material recipes -----------------------------------------------------
|
||||
Each recipe is the full set of properties that defines the material.
|
||||
|
|
@ -41,19 +62,19 @@
|
|||
|
||||
--nv-material-elevated-bg: var(--nv-color-surface-elevated);
|
||||
--nv-material-elevated-border: var(--nv-color-border-subtle);
|
||||
--nv-material-elevated-shadow: var(--nv-highlight-top), var(--nv-elevation-2);
|
||||
--nv-material-elevated-shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-2);
|
||||
|
||||
--nv-material-inset-bg: var(--nv-color-surface-inset);
|
||||
--nv-material-inset-border: var(--nv-color-border-subtle);
|
||||
--nv-material-inset-shadow: var(--nv-elevation-inset-md);
|
||||
--nv-material-inset-shadow: var(--nv-elevation-inset-md), var(--nv-ledge);
|
||||
|
||||
--nv-material-frosted-bg: var(--nv-color-material-frosted);
|
||||
--nv-material-frosted-border: var(--nv-color-material-border);
|
||||
--nv-material-frosted-shadow: var(--nv-highlight-top), var(--nv-elevation-3);
|
||||
--nv-material-frosted-shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-3);
|
||||
|
||||
--nv-material-glass-bg: var(--nv-color-material-glass);
|
||||
--nv-material-glass-border: var(--nv-color-material-border);
|
||||
--nv-material-glass-shadow: var(--nv-highlight-top-strong), var(--nv-elevation-4);
|
||||
--nv-material-glass-shadow: var(--nv-highlight-top-strong), var(--nv-outline), var(--nv-elevation-4);
|
||||
}
|
||||
|
||||
/* Where translucency cannot be honoured — reduced transparency, or a browser
|
||||
|
|
|
|||
|
|
@ -82,6 +82,15 @@
|
|||
--nv-travel-sm: 4px;
|
||||
--nv-travel-md: 8px;
|
||||
--nv-travel-lg: 16px;
|
||||
|
||||
/* --- Sequence ---------------------------------------------------------------
|
||||
The gap between siblings arriving one after another. Short enough that a
|
||||
group reads as one movement, long enough that the eye sees an order. */
|
||||
--nv-stagger: 60ms;
|
||||
|
||||
/* A loading shimmer is a slow, constant sweep — slow enough to read as
|
||||
"waiting", never fast enough to read as an alert. */
|
||||
--nv-duration-shimmer: 1600ms;
|
||||
}
|
||||
|
||||
/* Reduced motion is a system-wide contract, not a per-component opt-in: cut
|
||||
|
|
@ -103,5 +112,6 @@
|
|||
--nv-travel-sm: 0px;
|
||||
--nv-travel-md: 0px;
|
||||
--nv-travel-lg: 0px;
|
||||
--nv-stagger: 0ms;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,18 +2,19 @@
|
|||
Niveus — Radius
|
||||
--------------------------------------------------------------------------
|
||||
Corner softness carries as much of the Niveus character as shadow does.
|
||||
Radii grow with the surface: controls stay modest, containers get rounder,
|
||||
and only pills and avatars go fully round.
|
||||
Radii grow with the surface: controls stay modest and machined, containers
|
||||
get noticeably rounder — a window or a popover is a soft object, a button
|
||||
is a key — and only pills, knobs and avatars go fully round.
|
||||
========================================================================== */
|
||||
|
||||
:root {
|
||||
--nv-radius-none: 0;
|
||||
--nv-radius-xs: 0.25rem; /* 4px — tags, swatches, nested chips */
|
||||
--nv-radius-sm: 0.5rem; /* 8px — small controls, menu items */
|
||||
--nv-radius-md: 0.75rem; /* 12px — buttons, inputs, default control radius */
|
||||
--nv-radius-lg: 1rem; /* 16px — cards, panels */
|
||||
--nv-radius-xl: 1.375rem; /* 22px — dialogs, sheets, large containers */
|
||||
--nv-radius-2xl: 1.75rem; /* 28px — hero surfaces */
|
||||
--nv-radius-md: 0.625rem; /* 10px — buttons, inputs, default control radius */
|
||||
--nv-radius-lg: 0.875rem; /* 14px — cards, panels, menus */
|
||||
--nv-radius-xl: 1.25rem; /* 20px — windows, dialogs, sheets, popovers */
|
||||
--nv-radius-2xl: 1.5rem; /* 24px — docks, hero surfaces */
|
||||
--nv-radius-full: 999rem; /* — pills, avatars, switches */
|
||||
|
||||
/* Nested radius: an inner surface inset by N should be N smaller than its
|
||||
|
|
|
|||
|
|
@ -7,11 +7,13 @@
|
|||
|
||||
:root {
|
||||
/* --- Families ----------------------------------------------------------- */
|
||||
/* Both ship with Niveus (fonts.css); the rest of each stack only covers the
|
||||
moment before they load, or a context that cannot load them. */
|
||||
--nv-font-family-sans:
|
||||
"Inter var", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
"Helvetica Neue", Arial, sans-serif;
|
||||
"Instrument Sans", "Inter var", "Inter", -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
||||
--nv-font-family-mono:
|
||||
"SF Mono", "JetBrains Mono", "IBM Plex Mono", ui-monospace, "Menlo",
|
||||
"JetBrains Mono", "SF Mono", "IBM Plex Mono", ui-monospace, "Menlo",
|
||||
"Consolas", monospace;
|
||||
|
||||
/* --- Size scale ---------------------------------------------------------- */
|
||||
|
|
@ -46,6 +48,8 @@
|
|||
--nv-letter-spacing-normal: 0em;
|
||||
--nv-letter-spacing-wide: 0.02em;
|
||||
--nv-letter-spacing-wider: 0.06em;
|
||||
/* Engraved labels: small mono capitals, spaced like markings on a panel. */
|
||||
--nv-letter-spacing-engraved: 0.1em;
|
||||
|
||||
/* --- Composite roles ------------------------------------------------------
|
||||
Applied as classes in base.css (.nv-display, .nv-heading, …). Kept as
|
||||
|
|
@ -65,6 +69,11 @@
|
|||
var(--nv-line-height-normal) var(--nv-font-family-sans);
|
||||
--nv-font-code: var(--nv-font-weight-regular) var(--nv-font-size-sm) /
|
||||
var(--nv-line-height-normal) var(--nv-font-family-mono);
|
||||
/* The engraved label: section markers, the clock, anything that names a
|
||||
region rather than being read. Paired with uppercase and the engraved
|
||||
tracking (see .nv-overline in base.css). */
|
||||
--nv-font-engraved: var(--nv-font-weight-medium) var(--nv-font-size-2xs) /
|
||||
var(--nv-line-height-snug) var(--nv-font-family-mono);
|
||||
|
||||
/* --- Measure --------------------------------------------------------------- */
|
||||
--nv-measure-narrow: 44ch;
|
||||
|
|
|
|||
944
docs/Website/niveus/tools/niveus_css.py
Normal file
944
docs/Website/niveus/tools/niveus_css.py
Normal file
|
|
@ -0,0 +1,944 @@
|
|||
"""Evaluate the subset of CSS that Niveus's token layer is written in.
|
||||
|
||||
Niveus derives its entire color system from one input with relative color
|
||||
syntax, `color-mix()` and `calc()`. None of that survives a copy-paste into
|
||||
another platform's resources (Android XML, a Qt header, GTK CSS), and
|
||||
re-stating the results by hand would fork the system: the CSS would change and
|
||||
the port would quietly keep the old values.
|
||||
|
||||
So this module implements the language instead, once, here — every port's
|
||||
generator imports it rather than carrying its own copy.
|
||||
|
||||
It is deliberately narrow — it covers exactly what `niveus/tokens/*.css` and
|
||||
`niveus/themes/*.css` use:
|
||||
|
||||
#rrggbb / #rgb / #rrggbbaa rgb(r g b / a) transparent
|
||||
oklch(L C H) oklch(from <color> L C H [/ A])
|
||||
color-mix(in oklab, <a>, <b> <pct>)
|
||||
light-dark(<a>, <b>) var(--x[, fallback])
|
||||
calc(...) clamp(min,v,max) min(...) max(...)
|
||||
|
||||
Inside `oklch(from X ...)` the identifiers `l`, `c`, `h` and `alpha` are bound
|
||||
to X's own OKLCH channels, which is what makes one brand value re-colour
|
||||
everything.
|
||||
|
||||
Color math follows CSS Color 4: sRGB <-> linear sRGB <-> OKLab <-> OKLCH, and
|
||||
out-of-gamut results are gamut-mapped by chroma reduction against deltaEOK,
|
||||
the same approach browsers take. `color-mix()` mixes premultiplied, in OKLab.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Color space conversions (CSS Color 4)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _srgb_to_linear(v: float) -> float:
|
||||
return v / 12.92 if v <= 0.04045 else ((v + 0.055) / 1.055) ** 2.4
|
||||
|
||||
|
||||
def _linear_to_srgb(v: float) -> float:
|
||||
return v * 12.92 if v <= 0.0031308 else 1.055 * (v ** (1 / 2.4)) - 0.055
|
||||
|
||||
|
||||
# linear sRGB -> LMS -> OKLab, per Björn Ottosson.
|
||||
_LSRGB_TO_LMS = (
|
||||
(0.4122214708, 0.5363325363, 0.0514459929),
|
||||
(0.2119034982, 0.6806995451, 0.1073969566),
|
||||
(0.0883024619, 0.2817188376, 0.6299787005),
|
||||
)
|
||||
_LMS_TO_OKLAB = (
|
||||
(0.2104542553, 0.7936177850, -0.0040720468),
|
||||
(1.9779984951, -2.4285922050, 0.4505937099),
|
||||
(0.0259040371, 0.7827717662, -0.8086757660),
|
||||
)
|
||||
_OKLAB_TO_LMS = (
|
||||
(1.0, 0.3963377774, 0.2158037573),
|
||||
(1.0, -0.1055613458, -0.0638541728),
|
||||
(1.0, -0.0894841775, -1.2914855480),
|
||||
)
|
||||
_LMS_TO_LSRGB = (
|
||||
(4.0767416621, -3.3077115913, 0.2309699292),
|
||||
(-1.2684380046, 2.6097574011, -0.3413193965),
|
||||
(-0.0041960863, -0.7034186147, 1.7076147010),
|
||||
)
|
||||
|
||||
|
||||
def _mul(m, v):
|
||||
return tuple(sum(r[i] * v[i] for i in range(3)) for r in m)
|
||||
|
||||
|
||||
def srgb_to_oklab(r: float, g: float, b: float) -> tuple[float, float, float]:
|
||||
lin = (_srgb_to_linear(r), _srgb_to_linear(g), _srgb_to_linear(b))
|
||||
lms = _mul(_LSRGB_TO_LMS, lin)
|
||||
lms_ = tuple(math.copysign(abs(x) ** (1 / 3), x) for x in lms)
|
||||
return _mul(_LMS_TO_OKLAB, lms_)
|
||||
|
||||
|
||||
def oklab_to_srgb(L: float, a: float, b: float) -> tuple[float, float, float]:
|
||||
lms_ = _mul(_OKLAB_TO_LMS, (L, a, b))
|
||||
lms = tuple(x ** 3 for x in lms_)
|
||||
lin = _mul(_LMS_TO_LSRGB, lms)
|
||||
return tuple(_linear_to_srgb(x) for x in lin)
|
||||
|
||||
|
||||
def oklab_to_oklch(L: float, a: float, b: float) -> tuple[float, float, float]:
|
||||
c = math.hypot(a, b)
|
||||
# Below this chroma the hue is meaningless; CSS treats it as `none`, and
|
||||
# carrying a garbage angle through a derivation produces garbage hues.
|
||||
h = 0.0 if c < 1e-6 else math.degrees(math.atan2(b, a)) % 360.0
|
||||
return (L, c, h)
|
||||
|
||||
|
||||
def oklch_to_oklab(L: float, c: float, h: float) -> tuple[float, float, float]:
|
||||
rad = math.radians(h)
|
||||
return (L, c * math.cos(rad), c * math.sin(rad))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CIELAB, for the Android system palette
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Niveus works in OKLab, but Android's tonal palette is defined in terms of
|
||||
# *tone*, which is CIE L* — the same scale Material You's HCT uses for its T
|
||||
# channel. Generating those ramps in OKLab lightness would put every tone at a
|
||||
# slightly wrong luminance, so the palette gets its own, correct, colour space
|
||||
# rather than an approximation.
|
||||
|
||||
# sRGB (D65) -> XYZ
|
||||
_LRGB_TO_XYZ = (
|
||||
(0.4123907993, 0.3575843394, 0.1804807884),
|
||||
(0.2126390059, 0.7151686788, 0.0721923154),
|
||||
(0.0193308187, 0.1191947798, 0.9505321522),
|
||||
)
|
||||
_XYZ_TO_LRGB = (
|
||||
(3.2409699419, -1.5373831776, -0.4986107603),
|
||||
(-0.9692436363, 1.8759675015, 0.0415550574),
|
||||
(0.0556300797, -0.2039769589, 1.0569715142),
|
||||
)
|
||||
# D65 white point
|
||||
_WHITE = (0.9504559271, 1.0, 1.0890577508)
|
||||
|
||||
_EPS = 216 / 24389
|
||||
_KAPPA = 24389 / 27
|
||||
|
||||
|
||||
def _f(t: float) -> float:
|
||||
return t ** (1 / 3) if t > _EPS else (_KAPPA * t + 16) / 116
|
||||
|
||||
|
||||
def _f_inv(t: float) -> float:
|
||||
t3 = t ** 3
|
||||
return t3 if t3 > _EPS else (116 * t - 16) / _KAPPA
|
||||
|
||||
|
||||
def srgb_to_lab(r: float, g: float, b: float) -> tuple[float, float, float]:
|
||||
"""sRGB to CIE L*a*b*. L* is 0-100 and is what Android calls tone."""
|
||||
lin = (_srgb_to_linear(r), _srgb_to_linear(g), _srgb_to_linear(b))
|
||||
x, y, z = _mul(_LRGB_TO_XYZ, lin)
|
||||
fx, fy, fz = _f(x / _WHITE[0]), _f(y / _WHITE[1]), _f(z / _WHITE[2])
|
||||
return (116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz))
|
||||
|
||||
|
||||
def lab_to_srgb(L: float, a: float, b: float) -> tuple[float, float, float]:
|
||||
fy = (L + 16) / 116
|
||||
fx = fy + a / 500
|
||||
fz = fy - b / 200
|
||||
xyz = (_f_inv(fx) * _WHITE[0], _f_inv(fy) * _WHITE[1], _f_inv(fz) * _WHITE[2])
|
||||
return tuple(_linear_to_srgb(v) for v in _mul(_XYZ_TO_LRGB, xyz))
|
||||
|
||||
|
||||
def lch_to_srgb_clamped(L: float, C: float, H: float) -> tuple[float, float, float]:
|
||||
"""A tone at a given hue and chroma, reduced until it fits sRGB.
|
||||
|
||||
Tone is fixed — the whole point of a tonal palette is that tone 40 is the
|
||||
same lightness in every ramp — so chroma is what gives way, never L*.
|
||||
"""
|
||||
rad = math.radians(H)
|
||||
lo, hi = 0.0, max(C, 0.0)
|
||||
best = lab_to_srgb(L, 0.0, 0.0)
|
||||
for _ in range(28):
|
||||
mid = (lo + hi) / 2
|
||||
candidate = lab_to_srgb(L, mid * math.cos(rad), mid * math.sin(rad))
|
||||
if all(-1e-4 <= v <= 1 + 1e-4 for v in candidate):
|
||||
best = candidate
|
||||
lo = mid
|
||||
else:
|
||||
hi = mid
|
||||
return tuple(min(1.0, max(0.0, v)) for v in best)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Values
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Num:
|
||||
value: float
|
||||
unit: str = ""
|
||||
|
||||
def __repr__(self) -> str: # pragma: no cover - debugging aid
|
||||
return f"Num({self.value}{self.unit})"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Color:
|
||||
"""A color held in OKLab plus alpha. `none` means fully transparent."""
|
||||
|
||||
L: float
|
||||
a: float
|
||||
b: float
|
||||
alpha: float = 1.0
|
||||
|
||||
@staticmethod
|
||||
def from_srgb(r: float, g: float, bl: float, alpha: float = 1.0) -> "Color":
|
||||
L, a, b = srgb_to_oklab(r, g, bl)
|
||||
return Color(L, a, b, alpha)
|
||||
|
||||
@property
|
||||
def oklch(self) -> tuple[float, float, float]:
|
||||
return oklab_to_oklch(self.L, self.a, self.b)
|
||||
|
||||
def to_srgb(self) -> tuple[float, float, float]:
|
||||
return oklab_to_srgb(self.L, self.a, self.b)
|
||||
|
||||
# -- gamut mapping ----------------------------------------------------
|
||||
def _in_gamut(self, eps: float = 1e-4) -> bool:
|
||||
return all(-eps <= x <= 1 + eps for x in self.to_srgb())
|
||||
|
||||
def gamut_mapped(self) -> tuple[float, float, float]:
|
||||
"""sRGB triple, chroma-reduced into gamut (CSS Color 4 §13.2)."""
|
||||
if self._in_gamut():
|
||||
return tuple(min(1.0, max(0.0, x)) for x in self.to_srgb())
|
||||
|
||||
L, C, H = self.oklch
|
||||
if L >= 1.0:
|
||||
return (1.0, 1.0, 1.0)
|
||||
if L <= 0.0:
|
||||
return (0.0, 0.0, 0.0)
|
||||
|
||||
lo, hi = 0.0, C
|
||||
clipped = None
|
||||
for _ in range(32):
|
||||
mid = (lo + hi) / 2
|
||||
candidate = Color(*oklch_to_oklab(L, mid, H), self.alpha)
|
||||
raw = candidate.to_srgb()
|
||||
if candidate._in_gamut():
|
||||
lo = mid
|
||||
clipped = tuple(min(1.0, max(0.0, x)) for x in raw)
|
||||
continue
|
||||
clip = tuple(min(1.0, max(0.0, x)) for x in raw)
|
||||
# deltaEOK between the candidate and its clipped form: once the
|
||||
# clip is imperceptible we accept it rather than desaturating more.
|
||||
cl = Color.from_srgb(*clip)
|
||||
if math.dist((candidate.L, candidate.a, candidate.b), (cl.L, cl.a, cl.b)) < 0.02:
|
||||
return clip
|
||||
hi = mid
|
||||
if clipped is not None:
|
||||
return clipped
|
||||
return tuple(min(1.0, max(0.0, x)) for x in self.to_srgb())
|
||||
|
||||
# -- output -----------------------------------------------------------
|
||||
def hex8(self) -> str:
|
||||
"""#AARRGGBB — Android's order."""
|
||||
r, g, b = self.gamut_mapped()
|
||||
a = min(1.0, max(0.0, self.alpha))
|
||||
return "#{:02X}{:02X}{:02X}{:02X}".format(
|
||||
round(a * 255), round(r * 255), round(g * 255), round(b * 255)
|
||||
)
|
||||
|
||||
def hex_rgb(self) -> str:
|
||||
r, g, b = self.gamut_mapped()
|
||||
return "#{:02X}{:02X}{:02X}".format(round(r * 255), round(g * 255), round(b * 255))
|
||||
|
||||
def argb_int(self) -> int:
|
||||
r, g, b = self.gamut_mapped()
|
||||
a = min(1.0, max(0.0, self.alpha))
|
||||
return (
|
||||
(round(a * 255) << 24)
|
||||
| (round(r * 255) << 16)
|
||||
| (round(g * 255) << 8)
|
||||
| round(b * 255)
|
||||
)
|
||||
|
||||
def relative_luminance(self) -> float:
|
||||
r, g, b = self.gamut_mapped()
|
||||
lin = (_srgb_to_linear(r), _srgb_to_linear(g), _srgb_to_linear(b))
|
||||
return 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2]
|
||||
|
||||
|
||||
TRANSPARENT = Color(0.0, 0.0, 0.0, 0.0)
|
||||
|
||||
|
||||
def contrast_ratio(fg: Color, bg: Color) -> float:
|
||||
"""WCAG 2.1 contrast, compositing `fg` over `bg` if it is translucent."""
|
||||
if fg.alpha < 1.0:
|
||||
fr, fg_, fb = fg.gamut_mapped()
|
||||
br, bg_, bb = bg.gamut_mapped()
|
||||
a = fg.alpha
|
||||
fg = Color.from_srgb(
|
||||
fr * a + br * (1 - a), fg_ * a + bg_ * (1 - a), fb * a + bb * (1 - a)
|
||||
)
|
||||
l1, l2 = fg.relative_luminance(), bg.relative_luminance()
|
||||
if l1 < l2:
|
||||
l1, l2 = l2, l1
|
||||
return (l1 + 0.05) / (l2 + 0.05)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tokenizer / parser
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_TOKEN = re.compile(
|
||||
r"""
|
||||
(?P<ws>\s+)
|
||||
| (?P<hex>\#[0-9a-fA-F]{3,8})
|
||||
| (?P<func>[a-zA-Z_][-a-zA-Z0-9_]*(?=\())
|
||||
# A leading `-` starts an identifier only when a digit does not follow,
|
||||
# or `-0.022em` would lex as the identifier `-0`.
|
||||
| (?P<ident>-{1,2}(?![0-9])[-a-zA-Z0-9_]+|[a-zA-Z_][-a-zA-Z0-9_]*)
|
||||
| (?P<num>[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?(?:%|[a-zA-Z]+)?)
|
||||
| (?P<punct>[(),/*+\-])
|
||||
""",
|
||||
re.VERBOSE,
|
||||
)
|
||||
|
||||
|
||||
def _lex(s: str) -> list[tuple[str, str]]:
|
||||
out: list[tuple[str, str]] = []
|
||||
i = 0
|
||||
while i < len(s):
|
||||
m = _TOKEN.match(s, i)
|
||||
if not m:
|
||||
raise ValueError(f"cannot lex at {s[i:i+30]!r} in {s!r}")
|
||||
i = m.end()
|
||||
kind = m.lastgroup
|
||||
if kind == "ws":
|
||||
continue
|
||||
out.append((kind, m.group()))
|
||||
return out
|
||||
|
||||
|
||||
class _Parser:
|
||||
"""Recursive descent over the component-value grammar Niveus uses."""
|
||||
|
||||
def __init__(self, tokens: list[tuple[str, str]], ev: "Evaluator"):
|
||||
self.t = tokens
|
||||
self.i = 0
|
||||
self.ev = ev
|
||||
|
||||
# -- plumbing ---------------------------------------------------------
|
||||
def peek(self) -> tuple[str, str] | None:
|
||||
return self.t[self.i] if self.i < len(self.t) else None
|
||||
|
||||
def next(self) -> tuple[str, str]:
|
||||
tok = self.t[self.i]
|
||||
self.i += 1
|
||||
return tok
|
||||
|
||||
def expect(self, text: str) -> None:
|
||||
tok = self.next()
|
||||
if tok[1] != text:
|
||||
raise ValueError(f"expected {text!r}, got {tok[1]!r}")
|
||||
|
||||
def at(self, text: str) -> bool:
|
||||
tok = self.peek()
|
||||
return tok is not None and tok[1] == text
|
||||
|
||||
def args(self) -> list[list[tuple[str, str]]]:
|
||||
"""Consume `( a, b, c )` and return each argument's raw tokens."""
|
||||
self.expect("(")
|
||||
groups: list[list[tuple[str, str]]] = [[]]
|
||||
depth = 0
|
||||
while True:
|
||||
tok = self.peek()
|
||||
if tok is None:
|
||||
raise ValueError("unterminated (")
|
||||
if tok[1] == "(":
|
||||
depth += 1
|
||||
elif tok[1] == ")":
|
||||
if depth == 0:
|
||||
self.next()
|
||||
break
|
||||
depth -= 1
|
||||
elif tok[1] == "," and depth == 0:
|
||||
self.next()
|
||||
groups.append([])
|
||||
continue
|
||||
groups.append(groups.pop() + [self.next()])
|
||||
return [g for g in groups if g]
|
||||
|
||||
# -- arithmetic -------------------------------------------------------
|
||||
def sum_(self) -> Num:
|
||||
left = self.product()
|
||||
while True:
|
||||
tok = self.peek()
|
||||
if tok is None:
|
||||
break
|
||||
if tok[1] in ("+", "-"):
|
||||
op = self.next()[1]
|
||||
left, right = _same_unit(left, self.product())
|
||||
unit = left.unit or right.unit
|
||||
left = Num(
|
||||
left.value + right.value if op == "+" else left.value - right.value, unit
|
||||
)
|
||||
continue
|
||||
# `calc(l -0.045)` lexes the operand as one signed number, so the
|
||||
# sign has to be read back as the operator it was written as.
|
||||
if tok[0] == "num" and tok[1][0] in "+-":
|
||||
left, right = _same_unit(left, self.product())
|
||||
left = Num(left.value + right.value, left.unit or right.unit)
|
||||
continue
|
||||
break
|
||||
return left
|
||||
|
||||
def product(self) -> Num:
|
||||
left = self.term()
|
||||
while self.peek() and self.peek()[1] in ("*", "/"):
|
||||
op = self.next()[1]
|
||||
right = self.term()
|
||||
unit = left.unit or right.unit
|
||||
if op == "*":
|
||||
left = Num(left.value * right.value, unit)
|
||||
else:
|
||||
left = Num(left.value / right.value, left.unit)
|
||||
return left
|
||||
|
||||
def term(self) -> Num:
|
||||
tok = self.peek()
|
||||
if tok is None:
|
||||
raise ValueError("unexpected end of expression")
|
||||
if tok[1] == "(":
|
||||
self.next()
|
||||
v = self.sum_()
|
||||
self.expect(")")
|
||||
return v
|
||||
if tok[1] == "-":
|
||||
self.next()
|
||||
v = self.term()
|
||||
return Num(-v.value, v.unit)
|
||||
if tok[1] == "+":
|
||||
self.next()
|
||||
return self.term()
|
||||
val = self.value()
|
||||
if isinstance(val, Num):
|
||||
return val
|
||||
raise ValueError(f"expected a number, got {val!r}")
|
||||
|
||||
# -- values -----------------------------------------------------------
|
||||
def value(self):
|
||||
tok = self.next()
|
||||
kind, text = tok
|
||||
|
||||
if kind == "hex":
|
||||
return _parse_hex(text)
|
||||
|
||||
if kind == "num":
|
||||
return _parse_num(text)
|
||||
|
||||
if kind == "func":
|
||||
name = text.lower()
|
||||
if name == "calc":
|
||||
inner = self.args()
|
||||
return _Parser(inner[0], self.ev).sum_()
|
||||
if name in ("clamp", "min", "max"):
|
||||
vals = [_Parser(g, self.ev).sum_() for g in self.args()]
|
||||
if name == "clamp":
|
||||
lo, v, hi = vals
|
||||
unit = v.unit or lo.unit or hi.unit
|
||||
return Num(min(max(v.value, lo.value), hi.value), unit)
|
||||
pick = min if name == "min" else max
|
||||
chosen = pick(vals, key=lambda n: n.value)
|
||||
return chosen
|
||||
if name == "var":
|
||||
groups = self.args()
|
||||
varname = "".join(t[1] for t in groups[0])
|
||||
fallback = groups[1] if len(groups) > 1 else None
|
||||
return self.ev.resolve(varname, fallback)
|
||||
if name == "rgb" or name == "rgba":
|
||||
return _parse_rgb(self.args(), self.ev)
|
||||
if name == "oklch":
|
||||
return _parse_oklch(self.args(), self.ev)
|
||||
if name == "oklab":
|
||||
return _parse_oklab(self.args(), self.ev)
|
||||
if name == "color-mix":
|
||||
return _parse_color_mix(self.args(), self.ev)
|
||||
if name == "light-dark":
|
||||
groups = self.args()
|
||||
pick = groups[0] if self.ev.appearance == "light" else groups[1]
|
||||
return _Parser(pick, self.ev).value()
|
||||
raise ValueError(f"unsupported function {name}()")
|
||||
|
||||
if kind == "ident":
|
||||
low = text.lower()
|
||||
if low == "transparent":
|
||||
return TRANSPARENT
|
||||
if low in ("none", "currentcolor"):
|
||||
return TRANSPARENT
|
||||
if low in _NAMED:
|
||||
return _NAMED[low]
|
||||
if low in self.ev.bindings:
|
||||
return Num(self.ev.bindings[low])
|
||||
raise ValueError(f"unknown identifier {text!r}")
|
||||
|
||||
raise ValueError(f"unexpected token {text!r}")
|
||||
|
||||
|
||||
# The initial value of the root font size. Tokens never mix units, but
|
||||
# component rules do (`calc(var(--_track-block) - 6px)` with a rem on one
|
||||
# side), and adding the numbers without converting is silently wrong.
|
||||
ROOT_FONT_SIZE_PX = 16.0
|
||||
|
||||
|
||||
def _same_unit(a: Num, b: Num) -> tuple[Num, Num]:
|
||||
"""Bring two lengths to a common unit before adding them."""
|
||||
if a.unit == b.unit or not a.unit or not b.unit:
|
||||
return a, b
|
||||
lengths = {"px": 1.0, "rem": ROOT_FONT_SIZE_PX, "em": ROOT_FONT_SIZE_PX}
|
||||
if a.unit in lengths and b.unit in lengths:
|
||||
return (Num(a.value * lengths[a.unit], "px"), Num(b.value * lengths[b.unit], "px"))
|
||||
raise ValueError(f"cannot add {a.unit} to {b.unit}")
|
||||
|
||||
|
||||
_NAMED = {
|
||||
"white": Color.from_srgb(1, 1, 1),
|
||||
"black": Color.from_srgb(0, 0, 0),
|
||||
}
|
||||
|
||||
|
||||
def _parse_hex(text: str) -> Color:
|
||||
h = text[1:]
|
||||
if len(h) in (3, 4):
|
||||
h = "".join(ch * 2 for ch in h)
|
||||
if len(h) == 6:
|
||||
r, g, b = (int(h[i : i + 2], 16) / 255 for i in (0, 2, 4))
|
||||
return Color.from_srgb(r, g, b)
|
||||
if len(h) == 8:
|
||||
r, g, b, a = (int(h[i : i + 2], 16) / 255 for i in (0, 2, 4, 6))
|
||||
return Color.from_srgb(r, g, b, a)
|
||||
raise ValueError(f"bad hex color {text!r}")
|
||||
|
||||
|
||||
def _parse_num(text: str) -> Num:
|
||||
m = re.fullmatch(r"([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)(%|[a-zA-Z]*)", text)
|
||||
if not m:
|
||||
raise ValueError(f"bad number {text!r}")
|
||||
v = float(m.group(1))
|
||||
unit = m.group(2)
|
||||
if unit == "%":
|
||||
return Num(v / 100.0, "%")
|
||||
return Num(v, unit)
|
||||
|
||||
|
||||
def _flat(groups: list[list[tuple[str, str]]]) -> list[tuple[str, str]]:
|
||||
out: list[tuple[str, str]] = []
|
||||
for i, g in enumerate(groups):
|
||||
if i:
|
||||
out.append(("punct", ","))
|
||||
out.extend(g)
|
||||
return out
|
||||
|
||||
|
||||
def _split_slash(tokens: list[tuple[str, str]]) -> tuple[list, list | None]:
|
||||
depth = 0
|
||||
for i, (_, text) in enumerate(tokens):
|
||||
if text == "(":
|
||||
depth += 1
|
||||
elif text == ")":
|
||||
depth -= 1
|
||||
elif text == "/" and depth == 0:
|
||||
return tokens[:i], tokens[i + 1 :]
|
||||
return tokens, None
|
||||
|
||||
|
||||
def _components(tokens: list[tuple[str, str]], ev: "Evaluator", n: int) -> list[Num]:
|
||||
"""Read `n` space-separated numeric components from a token run."""
|
||||
p = _Parser(tokens, ev)
|
||||
out: list[Num] = []
|
||||
for _ in range(n):
|
||||
out.append(p.sum_())
|
||||
return out
|
||||
|
||||
|
||||
def _parse_rgb(groups, ev) -> Color:
|
||||
tokens = _flat(groups)
|
||||
# Both `rgb(r g b / a)` and the legacy `rgb(r, g, b, a)` appear in the wild.
|
||||
body, alpha_tokens = _split_slash(tokens)
|
||||
body = [t for t in body if t[1] != ","]
|
||||
nums = _components(body, ev, 3)
|
||||
vals = []
|
||||
for n in nums:
|
||||
vals.append(n.value if n.unit == "%" else n.value / 255.0)
|
||||
alpha = 1.0
|
||||
if alpha_tokens:
|
||||
alpha = _Parser(alpha_tokens, ev).sum_().value
|
||||
return Color.from_srgb(*vals, alpha)
|
||||
|
||||
|
||||
def _oklch_channels(tokens, ev, base: Color | None):
|
||||
body, alpha_tokens = _split_slash(tokens)
|
||||
bindings = {}
|
||||
if base is not None:
|
||||
L, C, H = base.oklch
|
||||
bindings = {"l": L, "c": C, "h": H, "alpha": base.alpha}
|
||||
with ev.bound(bindings):
|
||||
nums = _components(body, ev, 3)
|
||||
alpha = base.alpha if base is not None else 1.0
|
||||
if alpha_tokens:
|
||||
alpha = _Parser(alpha_tokens, ev).sum_().value
|
||||
return nums, alpha
|
||||
|
||||
|
||||
def _parse_oklch(groups, ev) -> Color:
|
||||
tokens = _flat(groups)
|
||||
base = None
|
||||
if tokens and tokens[0][0] == "ident" and tokens[0][1].lower() == "from":
|
||||
rest = tokens[1:]
|
||||
p = _Parser(rest, ev)
|
||||
base = p.value()
|
||||
if not isinstance(base, Color):
|
||||
raise ValueError("oklch(from …) needs a color")
|
||||
tokens = rest[p.i :]
|
||||
nums, alpha = _oklch_channels(tokens, ev, base)
|
||||
L, C, H = (n.value for n in nums)
|
||||
# A percentage lightness is 0–1; `oklch(50% …)` and `oklch(0.5 …)` agree.
|
||||
return Color(*oklch_to_oklab(L, C, H), alpha)
|
||||
|
||||
|
||||
def _parse_oklab(groups, ev) -> Color:
|
||||
tokens = _flat(groups)
|
||||
base = None
|
||||
if tokens and tokens[0][0] == "ident" and tokens[0][1].lower() == "from":
|
||||
rest = tokens[1:]
|
||||
p = _Parser(rest, ev)
|
||||
base = p.value()
|
||||
tokens = rest[p.i :]
|
||||
body, alpha_tokens = _split_slash(tokens)
|
||||
bindings = {}
|
||||
if base is not None:
|
||||
bindings = {"l": base.L, "a": base.a, "b": base.b, "alpha": base.alpha}
|
||||
with ev.bound(bindings):
|
||||
nums = _components(body, ev, 3)
|
||||
alpha = base.alpha if base is not None else 1.0
|
||||
if alpha_tokens:
|
||||
alpha = _Parser(alpha_tokens, ev).sum_().value
|
||||
return Color(nums[0].value, nums[1].value, nums[2].value, alpha)
|
||||
|
||||
|
||||
def _parse_color_mix(groups, ev) -> Color:
|
||||
# color-mix(in oklab, <a> [pct], <b> [pct])
|
||||
space_tokens = groups[0]
|
||||
space = " ".join(t[1] for t in space_tokens).replace("in ", "").strip()
|
||||
if space not in ("oklab", "oklch", "srgb"):
|
||||
raise ValueError(f"unsupported color-mix space {space!r}")
|
||||
|
||||
parsed: list[tuple[Color, float | None]] = []
|
||||
for g in groups[1:3]:
|
||||
p = _Parser(g, ev)
|
||||
col = p.value()
|
||||
if not isinstance(col, Color):
|
||||
raise ValueError("color-mix needs colors")
|
||||
pct = None
|
||||
if p.peek() is not None:
|
||||
pct = p.sum_().value
|
||||
parsed.append((col, pct))
|
||||
|
||||
(ca, pa), (cb, pb) = parsed
|
||||
if pa is None and pb is None:
|
||||
pa = pb = 0.5
|
||||
elif pa is None:
|
||||
pa = 1.0 - pb
|
||||
elif pb is None:
|
||||
pb = 1.0 - pa
|
||||
total = pa + pb
|
||||
if total == 0:
|
||||
raise ValueError("color-mix percentages sum to zero")
|
||||
pa, pb = pa / total, pb / total
|
||||
|
||||
# CSS mixes premultiplied by alpha, then un-premultiplies.
|
||||
alpha = ca.alpha * pa + cb.alpha * pb
|
||||
if space == "srgb":
|
||||
ra, ga, ba = ca.gamut_mapped()
|
||||
rb, gb, bb = cb.gamut_mapped()
|
||||
comps = [
|
||||
(x * ca.alpha * pa + y * cb.alpha * pb) for x, y in ((ra, rb), (ga, gb), (ba, bb))
|
||||
]
|
||||
if alpha > 0:
|
||||
comps = [x / alpha for x in comps]
|
||||
return Color.from_srgb(*comps, alpha)
|
||||
|
||||
if space == "oklch":
|
||||
La, Ca, Ha = ca.oklch
|
||||
Lb, Cb, Hb = cb.oklch
|
||||
# shorter hue arc
|
||||
if abs(Hb - Ha) > 180:
|
||||
if Hb > Ha:
|
||||
Ha += 360
|
||||
else:
|
||||
Hb += 360
|
||||
L = La * ca.alpha * pa + Lb * cb.alpha * pb
|
||||
C = Ca * ca.alpha * pa + Cb * cb.alpha * pb
|
||||
H = Ha * ca.alpha * pa + Hb * cb.alpha * pb
|
||||
if alpha > 0:
|
||||
L, C, H = L / alpha, C / alpha, H / alpha
|
||||
return Color(*oklch_to_oklab(L, C, H % 360), alpha)
|
||||
|
||||
L = ca.L * ca.alpha * pa + cb.L * cb.alpha * pb
|
||||
a = ca.a * ca.alpha * pa + cb.a * cb.alpha * pb
|
||||
b = ca.b * ca.alpha * pa + cb.b * cb.alpha * pb
|
||||
if alpha > 0:
|
||||
L, a, b = L / alpha, a / alpha, b / alpha
|
||||
return Color(L, a, b, alpha)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Evaluator
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class _Bound:
|
||||
def __init__(self, ev: "Evaluator", extra: dict):
|
||||
self.ev = ev
|
||||
self.extra = extra
|
||||
|
||||
def __enter__(self):
|
||||
self.saved = self.ev.bindings
|
||||
self.ev.bindings = {**self.ev.bindings, **self.extra}
|
||||
|
||||
def __exit__(self, *exc):
|
||||
self.ev.bindings = self.saved
|
||||
return False
|
||||
|
||||
|
||||
class Evaluator:
|
||||
"""Resolves `--nv-*` custom properties for one appearance."""
|
||||
|
||||
def __init__(self, declarations: dict[str, str], appearance: str = "light"):
|
||||
self.declarations = declarations
|
||||
self.appearance = appearance
|
||||
self.bindings: dict[str, float] = {}
|
||||
self._cache: dict[tuple[str, str], object] = {}
|
||||
self._stack: list[str] = []
|
||||
|
||||
def bound(self, extra: dict) -> _Bound:
|
||||
return _Bound(self, extra)
|
||||
|
||||
def override(self, name: str, raw: str) -> None:
|
||||
self.declarations[name] = raw
|
||||
self._cache.clear()
|
||||
|
||||
def resolve(self, name: str, fallback=None):
|
||||
key = (name, self.appearance)
|
||||
if key in self._cache:
|
||||
return self._cache[key]
|
||||
if name not in self.declarations:
|
||||
if fallback is not None:
|
||||
return _Parser(fallback, self).value()
|
||||
raise KeyError(f"undefined custom property {name}")
|
||||
if name in self._stack:
|
||||
raise ValueError(f"cyclic token reference: {' -> '.join(self._stack + [name])}")
|
||||
self._stack.append(name)
|
||||
# Bindings belong to the oklch(from …) that created them, not to a
|
||||
# token resolved from inside it.
|
||||
saved, self.bindings = self.bindings, {}
|
||||
try:
|
||||
value = self.eval(self.declarations[name])
|
||||
finally:
|
||||
self.bindings = saved
|
||||
self._stack.pop()
|
||||
self._cache[key] = value
|
||||
return value
|
||||
|
||||
def eval(self, raw: str):
|
||||
return _Parser(_lex(raw.strip()), self).value()
|
||||
|
||||
def color(self, name: str) -> Color:
|
||||
v = self.resolve(name)
|
||||
if not isinstance(v, Color):
|
||||
raise TypeError(f"{name} is not a color: {v!r}")
|
||||
return v
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reading declarations out of the CSS
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_COMMENT = re.compile(r"/\*.*?\*/", re.S)
|
||||
_DECL = re.compile(r"(--[-a-zA-Z0-9_]+)\s*:\s*([^;}]+)")
|
||||
|
||||
|
||||
def read_declarations(paths, skip_at_rules=True) -> dict[str, str]:
|
||||
"""Collect every `--nv-*: value` declaration, last one winning.
|
||||
|
||||
At-rule bodies (`@media (prefers-reduced-motion)`, `@supports not …`) are
|
||||
skipped: they are alternate appearances of a token, and baking a
|
||||
reduced-motion duration into the default build would be wrong.
|
||||
"""
|
||||
out: dict[str, str] = {}
|
||||
for path in paths:
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
css = _COMMENT.sub("", fh.read())
|
||||
if skip_at_rules:
|
||||
css = _strip_at_rules(css)
|
||||
for name, value in _DECL.findall(css):
|
||||
out[name] = " ".join(value.split())
|
||||
return out
|
||||
|
||||
|
||||
def _strip_at_rules(css: str) -> str:
|
||||
out = []
|
||||
i = 0
|
||||
while i < len(css):
|
||||
at = css.find("@", i)
|
||||
if at == -1:
|
||||
out.append(css[i:])
|
||||
break
|
||||
out.append(css[i:at])
|
||||
# Skip to the matching close brace of the at-rule block.
|
||||
brace = css.find("{", at)
|
||||
if brace == -1:
|
||||
break
|
||||
depth = 0
|
||||
j = brace
|
||||
while j < len(css):
|
||||
if css[j] == "{":
|
||||
depth += 1
|
||||
elif css[j] == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
break
|
||||
j += 1
|
||||
i = j + 1
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def read_at_rule_declarations(paths, condition: str) -> dict[str, str]:
|
||||
"""Declarations from at-rule blocks whose prelude contains `condition`."""
|
||||
out: dict[str, str] = {}
|
||||
for path in paths:
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
css = _COMMENT.sub("", fh.read())
|
||||
for prelude, body in _iter_at_rules(css):
|
||||
if condition in prelude:
|
||||
for name, value in _DECL.findall(body):
|
||||
out[name] = " ".join(value.split())
|
||||
return out
|
||||
|
||||
|
||||
def _iter_at_rules(css: str):
|
||||
i = 0
|
||||
while True:
|
||||
at = css.find("@", i)
|
||||
if at == -1:
|
||||
return
|
||||
brace = css.find("{", at)
|
||||
if brace == -1:
|
||||
return
|
||||
depth = 0
|
||||
j = brace
|
||||
while j < len(css):
|
||||
if css[j] == "{":
|
||||
depth += 1
|
||||
elif css[j] == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
break
|
||||
j += 1
|
||||
yield css[at:brace], css[brace + 1 : j]
|
||||
i = j + 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reading component rules
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# The token layer is custom properties on :root, which read_declarations
|
||||
# covers. A port that draws controls natively (a Qt style, Compose) also needs
|
||||
# the component layer: which role a secondary button's background is, which
|
||||
# shadow it gains on hover, what its disabled border becomes. Those live in
|
||||
# ordinary rules in components/*.css, and restating them in a port would fork
|
||||
# the system just as surely as restating a colour. So they are read here too.
|
||||
|
||||
def _split_selectors(prelude: str) -> list[str]:
|
||||
"""`a, b:not(c, d)` -> [`a`, `b:not(c, d)`], whitespace collapsed."""
|
||||
out, depth, cur = [], 0, []
|
||||
for ch in prelude:
|
||||
if ch in "([":
|
||||
depth += 1
|
||||
elif ch in ")]":
|
||||
depth -= 1
|
||||
if ch == "," and depth == 0:
|
||||
out.append(" ".join("".join(cur).split()))
|
||||
cur = []
|
||||
continue
|
||||
cur.append(ch)
|
||||
tail = " ".join("".join(cur).split())
|
||||
if tail:
|
||||
out.append(tail)
|
||||
return out
|
||||
|
||||
|
||||
def _parse_block_declarations(body: str) -> dict[str, str]:
|
||||
out: dict[str, str] = {}
|
||||
depth, cur = 0, []
|
||||
for ch in body + ";":
|
||||
if ch == "(":
|
||||
depth += 1
|
||||
elif ch == ")":
|
||||
depth -= 1
|
||||
if ch == ";" and depth == 0:
|
||||
decl = "".join(cur).strip()
|
||||
cur = []
|
||||
if ":" in decl:
|
||||
name, value = decl.split(":", 1)
|
||||
out[name.strip()] = " ".join(value.split())
|
||||
continue
|
||||
cur.append(ch)
|
||||
return out
|
||||
|
||||
|
||||
def read_rules(paths, media: tuple[str, ...] = ("hover: hover",)) -> dict[str, dict[str, str]]:
|
||||
"""Every style rule, as selector -> {property: value}, last one winning.
|
||||
|
||||
A grouped selector (`a, b { … }`) is recorded under each of its members.
|
||||
Rules inside `@media` blocks whose condition contains one of `media` are
|
||||
read as if they were top level: `(hover: hover)` only says the device has a
|
||||
pointer, which a desktop always does. Every other at-rule is skipped;
|
||||
those are alternate states (reduced motion, missing features, entry
|
||||
animations) rather than the component's normal appearance.
|
||||
"""
|
||||
rules: dict[str, dict[str, str]] = {}
|
||||
|
||||
def walk(css: str) -> None:
|
||||
i = 0
|
||||
while True:
|
||||
brace = css.find("{", i)
|
||||
if brace == -1:
|
||||
return
|
||||
prelude = css[i:brace].strip()
|
||||
depth, j = 0, brace
|
||||
while j < len(css):
|
||||
if css[j] == "{":
|
||||
depth += 1
|
||||
elif css[j] == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
break
|
||||
j += 1
|
||||
body = css[brace + 1 : j]
|
||||
if prelude.startswith("@"):
|
||||
if prelude.startswith("@media") and any(m in prelude for m in media):
|
||||
walk(body)
|
||||
else:
|
||||
decls = _parse_block_declarations(body)
|
||||
for sel in _split_selectors(prelude):
|
||||
rules.setdefault(sel, {}).update(decls)
|
||||
i = j + 1
|
||||
|
||||
for path in paths:
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
walk(_COMMENT.sub("", fh.read()))
|
||||
return rules
|
||||
Loading…
Reference in a new issue