@quo-systems/ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +26 -0
- package/css/base.css +155 -0
- package/css/components.css +314 -0
- package/css/theme-default.css +152 -0
- package/css/tokens.css +182 -0
- package/dist/css.d.ts +5 -0
- package/dist/css.js +6 -0
- package/dist/elements/theme.d.ts +9 -0
- package/dist/elements/theme.js +75 -0
- package/dist/tokens/livery-cli.d.ts +2 -0
- package/dist/tokens/livery-cli.js +29 -0
- package/dist/tokens/livery.d.ts +11 -0
- package/dist/tokens/livery.js +39 -0
- package/dist/tokens/manifest.d.ts +10 -0
- package/dist/tokens/manifest.js +105 -0
- package/elements/theme.ts +79 -0
- package/package.json +58 -0
- package/tokens/livery-cli.ts +33 -0
- package/tokens/livery.ts +58 -0
- package/tokens/manifest.ts +109 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The kit's own face: one neutral palette, light and dark, so a page renders
|
|
3
|
+
* with no design at all. This is what the dock's screen wears, and what a
|
|
4
|
+
* mock wears before its design exists.
|
|
5
|
+
*
|
|
6
|
+
* Layered, so a design's unlayered tokens.css replaces it slot for slot with
|
|
7
|
+
* no specificity trick. Three declarations and each is needed: the light
|
|
8
|
+
* palette at the bare root, the dark one under the system preference for a
|
|
9
|
+
* page that has chosen nothing, and the dark one again under an explicit
|
|
10
|
+
* data-theme so a chosen theme wins in both directions.
|
|
11
|
+
*
|
|
12
|
+
* Every pairing here clears 4.5:1 for body text and 3:1 for a focus ring
|
|
13
|
+
* against both of its neighbours. --nv-color-fg-subtle is the one exception
|
|
14
|
+
* by design: it is for rules, disabled glyphs and decoration, never for text
|
|
15
|
+
* a reader must read.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
@layer nv.tokens {
|
|
19
|
+
:root {
|
|
20
|
+
color-scheme: light;
|
|
21
|
+
|
|
22
|
+
--nv-color-bg: #ffffff;
|
|
23
|
+
--nv-color-bg-raised: #f7f7f8;
|
|
24
|
+
--nv-color-bg-raised-hover: #f0f0f2;
|
|
25
|
+
--nv-color-bg-raised-strong: #e8e8ec;
|
|
26
|
+
--nv-color-bg-sunken: #f2f2f4;
|
|
27
|
+
--nv-color-bg-overlay: rgb(20 20 24 / 45%);
|
|
28
|
+
|
|
29
|
+
--nv-color-fg: #16161a;
|
|
30
|
+
--nv-color-fg-body: #3a3a42;
|
|
31
|
+
--nv-color-fg-muted: #5f5f6a;
|
|
32
|
+
--nv-color-fg-subtle: #8a8a94;
|
|
33
|
+
--nv-color-fg-on-accent: #ffffff;
|
|
34
|
+
|
|
35
|
+
--nv-color-border: #e2e2e6;
|
|
36
|
+
--nv-color-border-strong: #c9c9d0;
|
|
37
|
+
--nv-color-border-focus: #2559c8;
|
|
38
|
+
|
|
39
|
+
--nv-color-accent: #2559c8;
|
|
40
|
+
--nv-color-accent-hover: #1d47a3;
|
|
41
|
+
--nv-color-accent-active: #163a86;
|
|
42
|
+
--nv-color-accent-subtle: rgb(37 89 200 / 10%);
|
|
43
|
+
|
|
44
|
+
--nv-color-success: #0f7052;
|
|
45
|
+
--nv-color-success-subtle: rgb(15 112 82 / 12%);
|
|
46
|
+
--nv-color-warning: #8a5200;
|
|
47
|
+
--nv-color-warning-subtle: rgb(138 82 0 / 12%);
|
|
48
|
+
--nv-color-danger: #b32020;
|
|
49
|
+
--nv-color-danger-subtle: rgb(179 32 32 / 12%);
|
|
50
|
+
--nv-color-info: #4a4a55;
|
|
51
|
+
--nv-color-info-subtle: rgb(74 74 85 / 10%);
|
|
52
|
+
|
|
53
|
+
--nv-color-scroll-thumb: #c9c9d0;
|
|
54
|
+
--nv-color-scroll-thumb-hover: #a5a5b0;
|
|
55
|
+
|
|
56
|
+
--nv-shadow-sm: 0 1px 2px rgb(16 16 20 / 8%);
|
|
57
|
+
--nv-shadow-md: 0 4px 10px rgb(16 16 20 / 10%);
|
|
58
|
+
--nv-shadow-lg: 0 10px 24px rgb(16 16 20 / 12%);
|
|
59
|
+
--nv-shadow-xl: 0 24px 48px rgb(16 16 20 / 16%);
|
|
60
|
+
--nv-shadow-focus: 0 0 0 3px rgb(37 89 200 / 28%);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@media (prefers-color-scheme: dark) {
|
|
64
|
+
:root:not([data-theme='light']) {
|
|
65
|
+
color-scheme: dark;
|
|
66
|
+
|
|
67
|
+
--nv-color-bg: #131317;
|
|
68
|
+
--nv-color-bg-raised: #1b1b21;
|
|
69
|
+
--nv-color-bg-raised-hover: #212129;
|
|
70
|
+
--nv-color-bg-raised-strong: #292932;
|
|
71
|
+
--nv-color-bg-sunken: #0e0e11;
|
|
72
|
+
--nv-color-bg-overlay: rgb(8 8 10 / 65%);
|
|
73
|
+
|
|
74
|
+
--nv-color-fg: #f2f2f5;
|
|
75
|
+
--nv-color-fg-body: #c9c9d2;
|
|
76
|
+
--nv-color-fg-muted: #9a9aa6;
|
|
77
|
+
--nv-color-fg-subtle: #6c6c78;
|
|
78
|
+
--nv-color-fg-on-accent: #0e0e11;
|
|
79
|
+
|
|
80
|
+
--nv-color-border: #2c2c35;
|
|
81
|
+
--nv-color-border-strong: #3d3d48;
|
|
82
|
+
--nv-color-border-focus: #8ab0ff;
|
|
83
|
+
|
|
84
|
+
--nv-color-accent: #8ab0ff;
|
|
85
|
+
--nv-color-accent-hover: #a6c3ff;
|
|
86
|
+
--nv-color-accent-active: #6d97ea;
|
|
87
|
+
--nv-color-accent-subtle: rgb(138 176 255 / 14%);
|
|
88
|
+
|
|
89
|
+
--nv-color-success: #55c496;
|
|
90
|
+
--nv-color-success-subtle: rgb(85 196 150 / 14%);
|
|
91
|
+
--nv-color-warning: #e0a33c;
|
|
92
|
+
--nv-color-warning-subtle: rgb(224 163 60 / 14%);
|
|
93
|
+
--nv-color-danger: #ff7b7b;
|
|
94
|
+
--nv-color-danger-subtle: rgb(255 123 123 / 14%);
|
|
95
|
+
--nv-color-info: #a8a8b4;
|
|
96
|
+
--nv-color-info-subtle: rgb(168 168 180 / 12%);
|
|
97
|
+
|
|
98
|
+
--nv-color-scroll-thumb: #3d3d48;
|
|
99
|
+
--nv-color-scroll-thumb-hover: #5a5a68;
|
|
100
|
+
|
|
101
|
+
--nv-shadow-sm: 0 1px 2px rgb(0 0 0 / 50%);
|
|
102
|
+
--nv-shadow-md: 0 4px 10px rgb(0 0 0 / 55%);
|
|
103
|
+
--nv-shadow-lg: 0 10px 24px rgb(0 0 0 / 60%);
|
|
104
|
+
--nv-shadow-xl: 0 24px 48px rgb(0 0 0 / 70%);
|
|
105
|
+
--nv-shadow-focus: 0 0 0 3px rgb(138 176 255 / 32%);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
:root[data-theme='dark'] {
|
|
110
|
+
color-scheme: dark;
|
|
111
|
+
|
|
112
|
+
--nv-color-bg: #131317;
|
|
113
|
+
--nv-color-bg-raised: #1b1b21;
|
|
114
|
+
--nv-color-bg-raised-hover: #212129;
|
|
115
|
+
--nv-color-bg-raised-strong: #292932;
|
|
116
|
+
--nv-color-bg-sunken: #0e0e11;
|
|
117
|
+
--nv-color-bg-overlay: rgb(8 8 10 / 65%);
|
|
118
|
+
|
|
119
|
+
--nv-color-fg: #f2f2f5;
|
|
120
|
+
--nv-color-fg-body: #c9c9d2;
|
|
121
|
+
--nv-color-fg-muted: #9a9aa6;
|
|
122
|
+
--nv-color-fg-subtle: #6c6c78;
|
|
123
|
+
--nv-color-fg-on-accent: #0e0e11;
|
|
124
|
+
|
|
125
|
+
--nv-color-border: #2c2c35;
|
|
126
|
+
--nv-color-border-strong: #3d3d48;
|
|
127
|
+
--nv-color-border-focus: #8ab0ff;
|
|
128
|
+
|
|
129
|
+
--nv-color-accent: #8ab0ff;
|
|
130
|
+
--nv-color-accent-hover: #a6c3ff;
|
|
131
|
+
--nv-color-accent-active: #6d97ea;
|
|
132
|
+
--nv-color-accent-subtle: rgb(138 176 255 / 14%);
|
|
133
|
+
|
|
134
|
+
--nv-color-success: #55c496;
|
|
135
|
+
--nv-color-success-subtle: rgb(85 196 150 / 14%);
|
|
136
|
+
--nv-color-warning: #e0a33c;
|
|
137
|
+
--nv-color-warning-subtle: rgb(224 163 60 / 14%);
|
|
138
|
+
--nv-color-danger: #ff7b7b;
|
|
139
|
+
--nv-color-danger-subtle: rgb(255 123 123 / 14%);
|
|
140
|
+
--nv-color-info: #a8a8b4;
|
|
141
|
+
--nv-color-info-subtle: rgb(168 168 180 / 12%);
|
|
142
|
+
|
|
143
|
+
--nv-color-scroll-thumb: #3d3d48;
|
|
144
|
+
--nv-color-scroll-thumb-hover: #5a5a68;
|
|
145
|
+
|
|
146
|
+
--nv-shadow-sm: 0 1px 2px rgb(0 0 0 / 50%);
|
|
147
|
+
--nv-shadow-md: 0 4px 10px rgb(0 0 0 / 55%);
|
|
148
|
+
--nv-shadow-lg: 0 10px 24px rgb(0 0 0 / 60%);
|
|
149
|
+
--nv-shadow-xl: 0 24px 48px rgb(0 0 0 / 70%);
|
|
150
|
+
--nv-shadow-focus: 0 0 0 3px rgb(138 176 255 / 32%);
|
|
151
|
+
}
|
|
152
|
+
}
|
package/css/tokens.css
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The token contract: every structural scale, declared once at the root.
|
|
3
|
+
*
|
|
4
|
+
* Two halves, and the split is the law. The kit owns structure, the space,
|
|
5
|
+
* radius, type, motion and z scales below, and a design may never redefine
|
|
6
|
+
* one; `tokens/manifest.ts` is that list, and conformance reads it. A design
|
|
7
|
+
* owns colour and shadow, the manifest's theme slots, and declares them
|
|
8
|
+
* unlayered so they win over anything layered here without a specificity
|
|
9
|
+
* trick. Structure is immutable; only the face is a design's.
|
|
10
|
+
*
|
|
11
|
+
* This file declares no colour. `theme-default.css` is the kit's own face, so
|
|
12
|
+
* a page renders with no design at all, and a design replaces it.
|
|
13
|
+
*
|
|
14
|
+
* Responsive CSS keys on :root[data-tier]. A width media query is illegal
|
|
15
|
+
* everywhere; the only width source is BREAKPOINTS in the manifest. The one
|
|
16
|
+
* prefers-reduced-motion block in the kit is here: --nv-motion scales every
|
|
17
|
+
* decorative duration to zero under reduce.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
@layer nv.tokens, nv.reset, nv.base, nv.components;
|
|
21
|
+
|
|
22
|
+
@layer nv.tokens {
|
|
23
|
+
:root {
|
|
24
|
+
/* Spacing, raw ramp */
|
|
25
|
+
--nv-space-0: 0;
|
|
26
|
+
--nv-space-05: 0.125rem;
|
|
27
|
+
--nv-space-1: 0.25rem;
|
|
28
|
+
--nv-space-2: 0.5rem;
|
|
29
|
+
--nv-space-3: 0.75rem;
|
|
30
|
+
--nv-space-4: 1rem;
|
|
31
|
+
--nv-space-5: 1.5rem;
|
|
32
|
+
--nv-space-6: 2rem;
|
|
33
|
+
--nv-space-7: 3rem;
|
|
34
|
+
--nv-space-8: 4rem;
|
|
35
|
+
|
|
36
|
+
/* Spacing, semantic. Density and tier remap these in one place. */
|
|
37
|
+
--nv-page-gutter: 1rem;
|
|
38
|
+
--nv-section-gap: 2rem;
|
|
39
|
+
--nv-stack-gap: 1rem;
|
|
40
|
+
--nv-card-pad: 1rem;
|
|
41
|
+
--nv-form-gap: 0.75rem;
|
|
42
|
+
|
|
43
|
+
/* Radii */
|
|
44
|
+
--nv-radius-sm: 4px;
|
|
45
|
+
--nv-radius-md: 8px;
|
|
46
|
+
--nv-radius-lg: 12px;
|
|
47
|
+
--nv-radius-xl: 16px;
|
|
48
|
+
--nv-radius-full: 9999px;
|
|
49
|
+
|
|
50
|
+
/* Font slots. A design points these at its own faces and ships the
|
|
51
|
+
files; the kit itself carries no webfont and falls back to the
|
|
52
|
+
system stacks below. */
|
|
53
|
+
--nv-font-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
|
|
54
|
+
--nv-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
55
|
+
--nv-font-display: var(--nv-font-sans);
|
|
56
|
+
|
|
57
|
+
/* Type ramp, rem, with fluid display steps */
|
|
58
|
+
--nv-text-2xs: 0.6875rem;
|
|
59
|
+
--nv-text-xs: 0.75rem;
|
|
60
|
+
--nv-text-sm: 0.875rem;
|
|
61
|
+
--nv-text-base: 1rem;
|
|
62
|
+
--nv-text-lg: 1.125rem;
|
|
63
|
+
--nv-text-xl: 1.25rem;
|
|
64
|
+
--nv-text-2xl: 1.5rem;
|
|
65
|
+
--nv-text-3xl: clamp(1.75rem, 1.35rem + 1.6vw, 2rem);
|
|
66
|
+
--nv-text-4xl: clamp(2rem, 1.55rem + 2vw, 2.625rem);
|
|
67
|
+
|
|
68
|
+
/* Controls only, and never density-remapped: under 16px effective, iOS
|
|
69
|
+
zooms the page when the field takes focus. */
|
|
70
|
+
--nv-text-input: 1rem;
|
|
71
|
+
|
|
72
|
+
--nv-leading-tight: 1.2;
|
|
73
|
+
--nv-leading-normal: 1.5;
|
|
74
|
+
|
|
75
|
+
--nv-weight-normal: 400;
|
|
76
|
+
--nv-weight-medium: 500;
|
|
77
|
+
--nv-weight-semi: 600;
|
|
78
|
+
--nv-weight-bold: 700;
|
|
79
|
+
|
|
80
|
+
/* Controls, touch floor, icons. A control is sized
|
|
81
|
+
max(var(--nv-control-*), var(--nv-tap-target)), never one alone. */
|
|
82
|
+
--nv-control-sm: 2rem;
|
|
83
|
+
--nv-control-md: 2.5rem;
|
|
84
|
+
--nv-control-lg: 2.75rem;
|
|
85
|
+
--nv-tap-target: 0px;
|
|
86
|
+
--nv-icon-sm: 1rem;
|
|
87
|
+
--nv-icon-md: 1.25rem;
|
|
88
|
+
--nv-icon-lg: 1.5rem;
|
|
89
|
+
|
|
90
|
+
/* Shell geometry */
|
|
91
|
+
--nv-header-h: 3.5rem;
|
|
92
|
+
--nv-tabbar-h: 3.5rem;
|
|
93
|
+
--nv-sidebar-w: 17.5rem;
|
|
94
|
+
--nv-rail-w: 4.5rem;
|
|
95
|
+
--nv-aside-w: 20rem;
|
|
96
|
+
|
|
97
|
+
/* The width table. No other file mints a width. */
|
|
98
|
+
--nv-page-w-xs: 27.5rem;
|
|
99
|
+
--nv-page-w-sm: 30rem;
|
|
100
|
+
--nv-page-w-md: 36rem;
|
|
101
|
+
--nv-page-w-lg: 75rem;
|
|
102
|
+
|
|
103
|
+
/* Motion. Every decorative duration scales through --nv-motion; a
|
|
104
|
+
spinner stays on --nv-duration-spin, because essential motion keeps
|
|
105
|
+
its meaning under reduce. No logic keys on a decorative
|
|
106
|
+
transitionend: at --nv-motion 0 a transition fires no event. */
|
|
107
|
+
--nv-motion: 1;
|
|
108
|
+
--nv-duration-fast: calc(150ms * var(--nv-motion));
|
|
109
|
+
--nv-duration-normal: calc(250ms * var(--nv-motion));
|
|
110
|
+
--nv-duration-slow: calc(400ms * var(--nv-motion));
|
|
111
|
+
--nv-duration-ambient: calc(2s * var(--nv-motion));
|
|
112
|
+
--nv-duration-spin: 750ms;
|
|
113
|
+
|
|
114
|
+
--nv-ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
|
115
|
+
--nv-ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
|
116
|
+
|
|
117
|
+
/* The z ladder. A literal z-index of 2 or more is banned everywhere. */
|
|
118
|
+
--nv-z-sticky: 10;
|
|
119
|
+
--nv-z-chrome: 50;
|
|
120
|
+
--nv-z-overlay: 100;
|
|
121
|
+
--nv-z-dialog: 200;
|
|
122
|
+
--nv-z-popover: 300;
|
|
123
|
+
--nv-z-toast: 400;
|
|
124
|
+
--nv-z-tooltip: 500;
|
|
125
|
+
|
|
126
|
+
/* Insets. A native wrap may override these inline, which beats every
|
|
127
|
+
layer. --nv-inset-bottom is the bottom pad for every docked surface,
|
|
128
|
+
a max() and never a sum: on iOS the keyboard covers the home
|
|
129
|
+
indicator, so adding the two pads twice. */
|
|
130
|
+
--nv-safe-top: env(safe-area-inset-top, 0px);
|
|
131
|
+
--nv-safe-right: env(safe-area-inset-right, 0px);
|
|
132
|
+
--nv-safe-bottom: env(safe-area-inset-bottom, 0px);
|
|
133
|
+
--nv-safe-left: env(safe-area-inset-left, 0px);
|
|
134
|
+
--nv-kb-inset: 0px;
|
|
135
|
+
--nv-inset-bottom: max(var(--nv-safe-bottom), var(--nv-kb-inset));
|
|
136
|
+
|
|
137
|
+
/* Optional slots, defaulted from the required ones so a design that
|
|
138
|
+
says nothing about them still paints. */
|
|
139
|
+
--nv-color-link: var(--nv-color-accent);
|
|
140
|
+
--nv-color-link-hover: var(--nv-color-accent-hover);
|
|
141
|
+
--nv-color-border-subtle: var(--nv-color-border);
|
|
142
|
+
--nv-color-danger-hover: var(--nv-color-danger);
|
|
143
|
+
--nv-color-fg-on-danger: var(--nv-color-fg-on-accent);
|
|
144
|
+
--nv-brand-healthy: var(--nv-color-success);
|
|
145
|
+
--nv-brand-reconnecting: var(--nv-color-warning);
|
|
146
|
+
--nv-brand-offline: var(--nv-color-danger);
|
|
147
|
+
--nv-brand-logo-size: 2rem;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
:root[data-tier='tablet'],
|
|
151
|
+
:root[data-tier='desktop'] {
|
|
152
|
+
--nv-page-gutter: 1.5rem;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@media (pointer: coarse) {
|
|
156
|
+
:root {
|
|
157
|
+
--nv-tap-target: 44px;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@media (prefers-reduced-motion: reduce) {
|
|
162
|
+
:root {
|
|
163
|
+
--nv-motion: 0;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* Density: one mode, unpinned, so any subtree hosts it and a dense region
|
|
168
|
+
such as a table scopes it to itself. --nv-text-input and --nv-tap-target
|
|
169
|
+
stay out: a field never falls under 16px, and the coarse floor still
|
|
170
|
+
wins through max(). */
|
|
171
|
+
[data-density='compact'] {
|
|
172
|
+
--nv-text-sm: 0.8125rem;
|
|
173
|
+
--nv-text-base: 0.9375rem;
|
|
174
|
+
--nv-text-lg: 1.0625rem;
|
|
175
|
+
--nv-control-sm: 1.75rem;
|
|
176
|
+
--nv-control-md: 2.25rem;
|
|
177
|
+
--nv-card-pad: 0.75rem;
|
|
178
|
+
--nv-stack-gap: 0.75rem;
|
|
179
|
+
--nv-form-gap: 0.5rem;
|
|
180
|
+
--nv-section-gap: 1.5rem;
|
|
181
|
+
}
|
|
182
|
+
}
|
package/dist/css.d.ts
ADDED
package/dist/css.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Generated from the files beside them. Never edit: run npm run build.
|
|
2
|
+
export const TOKENS = "@layer nv.tokens, nv.reset, nv.base, nv.components;\n@layer nv.tokens {\n :root {\n \n --nv-space-0: 0;\n --nv-space-05: 0.125rem;\n --nv-space-1: 0.25rem;\n --nv-space-2: 0.5rem;\n --nv-space-3: 0.75rem;\n --nv-space-4: 1rem;\n --nv-space-5: 1.5rem;\n --nv-space-6: 2rem;\n --nv-space-7: 3rem;\n --nv-space-8: 4rem;\n \n --nv-page-gutter: 1rem;\n --nv-section-gap: 2rem;\n --nv-stack-gap: 1rem;\n --nv-card-pad: 1rem;\n --nv-form-gap: 0.75rem;\n \n --nv-radius-sm: 4px;\n --nv-radius-md: 8px;\n --nv-radius-lg: 12px;\n --nv-radius-xl: 16px;\n --nv-radius-full: 9999px;\n \n --nv-font-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;\n --nv-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n --nv-font-display: var(--nv-font-sans);\n \n --nv-text-2xs: 0.6875rem;\n --nv-text-xs: 0.75rem;\n --nv-text-sm: 0.875rem;\n --nv-text-base: 1rem;\n --nv-text-lg: 1.125rem;\n --nv-text-xl: 1.25rem;\n --nv-text-2xl: 1.5rem;\n --nv-text-3xl: clamp(1.75rem, 1.35rem + 1.6vw, 2rem);\n --nv-text-4xl: clamp(2rem, 1.55rem + 2vw, 2.625rem);\n \n --nv-text-input: 1rem;\n --nv-leading-tight: 1.2;\n --nv-leading-normal: 1.5;\n --nv-weight-normal: 400;\n --nv-weight-medium: 500;\n --nv-weight-semi: 600;\n --nv-weight-bold: 700;\n \n --nv-control-sm: 2rem;\n --nv-control-md: 2.5rem;\n --nv-control-lg: 2.75rem;\n --nv-tap-target: 0px;\n --nv-icon-sm: 1rem;\n --nv-icon-md: 1.25rem;\n --nv-icon-lg: 1.5rem;\n \n --nv-header-h: 3.5rem;\n --nv-tabbar-h: 3.5rem;\n --nv-sidebar-w: 17.5rem;\n --nv-rail-w: 4.5rem;\n --nv-aside-w: 20rem;\n \n --nv-page-w-xs: 27.5rem;\n --nv-page-w-sm: 30rem;\n --nv-page-w-md: 36rem;\n --nv-page-w-lg: 75rem;\n \n --nv-motion: 1;\n --nv-duration-fast: calc(150ms * var(--nv-motion));\n --nv-duration-normal: calc(250ms * var(--nv-motion));\n --nv-duration-slow: calc(400ms * var(--nv-motion));\n --nv-duration-ambient: calc(2s * var(--nv-motion));\n --nv-duration-spin: 750ms;\n --nv-ease-out: cubic-bezier(0.16, 1, 0.3, 1);\n --nv-ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);\n \n --nv-z-sticky: 10;\n --nv-z-chrome: 50;\n --nv-z-overlay: 100;\n --nv-z-dialog: 200;\n --nv-z-popover: 300;\n --nv-z-toast: 400;\n --nv-z-tooltip: 500;\n \n --nv-safe-top: env(safe-area-inset-top, 0px);\n --nv-safe-right: env(safe-area-inset-right, 0px);\n --nv-safe-bottom: env(safe-area-inset-bottom, 0px);\n --nv-safe-left: env(safe-area-inset-left, 0px);\n --nv-kb-inset: 0px;\n --nv-inset-bottom: max(var(--nv-safe-bottom), var(--nv-kb-inset));\n \n --nv-color-link: var(--nv-color-accent);\n --nv-color-link-hover: var(--nv-color-accent-hover);\n --nv-color-border-subtle: var(--nv-color-border);\n --nv-color-danger-hover: var(--nv-color-danger);\n --nv-color-fg-on-danger: var(--nv-color-fg-on-accent);\n --nv-brand-healthy: var(--nv-color-success);\n --nv-brand-reconnecting: var(--nv-color-warning);\n --nv-brand-offline: var(--nv-color-danger);\n --nv-brand-logo-size: 2rem;\n }\n :root[data-tier='tablet'],\n :root[data-tier='desktop'] {\n --nv-page-gutter: 1.5rem;\n }\n @media (pointer: coarse) {\n :root {\n --nv-tap-target: 44px;\n }\n }\n @media (prefers-reduced-motion: reduce) {\n :root {\n --nv-motion: 0;\n }\n }\n \n [data-density='compact'] {\n --nv-text-sm: 0.8125rem;\n --nv-text-base: 0.9375rem;\n --nv-text-lg: 1.0625rem;\n --nv-control-sm: 1.75rem;\n --nv-control-md: 2.25rem;\n --nv-card-pad: 0.75rem;\n --nv-stack-gap: 0.75rem;\n --nv-form-gap: 0.5rem;\n --nv-section-gap: 1.5rem;\n }\n}";
|
|
3
|
+
export const THEME_DEFAULT = "@layer nv.tokens {\n :root {\n color-scheme: light;\n --nv-color-bg: #ffffff;\n --nv-color-bg-raised: #f7f7f8;\n --nv-color-bg-raised-hover: #f0f0f2;\n --nv-color-bg-raised-strong: #e8e8ec;\n --nv-color-bg-sunken: #f2f2f4;\n --nv-color-bg-overlay: rgb(20 20 24 / 45%);\n --nv-color-fg: #16161a;\n --nv-color-fg-body: #3a3a42;\n --nv-color-fg-muted: #5f5f6a;\n --nv-color-fg-subtle: #8a8a94;\n --nv-color-fg-on-accent: #ffffff;\n --nv-color-border: #e2e2e6;\n --nv-color-border-strong: #c9c9d0;\n --nv-color-border-focus: #2559c8;\n --nv-color-accent: #2559c8;\n --nv-color-accent-hover: #1d47a3;\n --nv-color-accent-active: #163a86;\n --nv-color-accent-subtle: rgb(37 89 200 / 10%);\n --nv-color-success: #0f7052;\n --nv-color-success-subtle: rgb(15 112 82 / 12%);\n --nv-color-warning: #8a5200;\n --nv-color-warning-subtle: rgb(138 82 0 / 12%);\n --nv-color-danger: #b32020;\n --nv-color-danger-subtle: rgb(179 32 32 / 12%);\n --nv-color-info: #4a4a55;\n --nv-color-info-subtle: rgb(74 74 85 / 10%);\n --nv-color-scroll-thumb: #c9c9d0;\n --nv-color-scroll-thumb-hover: #a5a5b0;\n --nv-shadow-sm: 0 1px 2px rgb(16 16 20 / 8%);\n --nv-shadow-md: 0 4px 10px rgb(16 16 20 / 10%);\n --nv-shadow-lg: 0 10px 24px rgb(16 16 20 / 12%);\n --nv-shadow-xl: 0 24px 48px rgb(16 16 20 / 16%);\n --nv-shadow-focus: 0 0 0 3px rgb(37 89 200 / 28%);\n }\n @media (prefers-color-scheme: dark) {\n :root:not([data-theme='light']) {\n color-scheme: dark;\n --nv-color-bg: #131317;\n --nv-color-bg-raised: #1b1b21;\n --nv-color-bg-raised-hover: #212129;\n --nv-color-bg-raised-strong: #292932;\n --nv-color-bg-sunken: #0e0e11;\n --nv-color-bg-overlay: rgb(8 8 10 / 65%);\n --nv-color-fg: #f2f2f5;\n --nv-color-fg-body: #c9c9d2;\n --nv-color-fg-muted: #9a9aa6;\n --nv-color-fg-subtle: #6c6c78;\n --nv-color-fg-on-accent: #0e0e11;\n --nv-color-border: #2c2c35;\n --nv-color-border-strong: #3d3d48;\n --nv-color-border-focus: #8ab0ff;\n --nv-color-accent: #8ab0ff;\n --nv-color-accent-hover: #a6c3ff;\n --nv-color-accent-active: #6d97ea;\n --nv-color-accent-subtle: rgb(138 176 255 / 14%);\n --nv-color-success: #55c496;\n --nv-color-success-subtle: rgb(85 196 150 / 14%);\n --nv-color-warning: #e0a33c;\n --nv-color-warning-subtle: rgb(224 163 60 / 14%);\n --nv-color-danger: #ff7b7b;\n --nv-color-danger-subtle: rgb(255 123 123 / 14%);\n --nv-color-info: #a8a8b4;\n --nv-color-info-subtle: rgb(168 168 180 / 12%);\n --nv-color-scroll-thumb: #3d3d48;\n --nv-color-scroll-thumb-hover: #5a5a68;\n --nv-shadow-sm: 0 1px 2px rgb(0 0 0 / 50%);\n --nv-shadow-md: 0 4px 10px rgb(0 0 0 / 55%);\n --nv-shadow-lg: 0 10px 24px rgb(0 0 0 / 60%);\n --nv-shadow-xl: 0 24px 48px rgb(0 0 0 / 70%);\n --nv-shadow-focus: 0 0 0 3px rgb(138 176 255 / 32%);\n }\n }\n :root[data-theme='dark'] {\n color-scheme: dark;\n --nv-color-bg: #131317;\n --nv-color-bg-raised: #1b1b21;\n --nv-color-bg-raised-hover: #212129;\n --nv-color-bg-raised-strong: #292932;\n --nv-color-bg-sunken: #0e0e11;\n --nv-color-bg-overlay: rgb(8 8 10 / 65%);\n --nv-color-fg: #f2f2f5;\n --nv-color-fg-body: #c9c9d2;\n --nv-color-fg-muted: #9a9aa6;\n --nv-color-fg-subtle: #6c6c78;\n --nv-color-fg-on-accent: #0e0e11;\n --nv-color-border: #2c2c35;\n --nv-color-border-strong: #3d3d48;\n --nv-color-border-focus: #8ab0ff;\n --nv-color-accent: #8ab0ff;\n --nv-color-accent-hover: #a6c3ff;\n --nv-color-accent-active: #6d97ea;\n --nv-color-accent-subtle: rgb(138 176 255 / 14%);\n --nv-color-success: #55c496;\n --nv-color-success-subtle: rgb(85 196 150 / 14%);\n --nv-color-warning: #e0a33c;\n --nv-color-warning-subtle: rgb(224 163 60 / 14%);\n --nv-color-danger: #ff7b7b;\n --nv-color-danger-subtle: rgb(255 123 123 / 14%);\n --nv-color-info: #a8a8b4;\n --nv-color-info-subtle: rgb(168 168 180 / 12%);\n --nv-color-scroll-thumb: #3d3d48;\n --nv-color-scroll-thumb-hover: #5a5a68;\n --nv-shadow-sm: 0 1px 2px rgb(0 0 0 / 50%);\n --nv-shadow-md: 0 4px 10px rgb(0 0 0 / 55%);\n --nv-shadow-lg: 0 10px 24px rgb(0 0 0 / 60%);\n --nv-shadow-xl: 0 24px 48px rgb(0 0 0 / 70%);\n --nv-shadow-focus: 0 0 0 3px rgb(138 176 255 / 32%);\n }\n}";
|
|
4
|
+
export const BASE = "@layer nv.reset {\n *,\n *::before,\n *::after {\n box-sizing: border-box;\n }\n :where(body, h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd) {\n margin: 0;\n }\n :where(ul, ol)[role='list'] {\n padding: 0;\n margin: 0;\n list-style: none;\n }\n :where(img, picture, video, canvas, svg) {\n display: block;\n max-width: 100%;\n }\n :where(input, button, textarea, select) {\n font: inherit;\n color: inherit;\n }\n :where(p, h1, h2, h3, h4, h5, h6) {\n overflow-wrap: break-word;\n }\n}\n@layer nv.base {\n :root {\n scrollbar-width: thin;\n scrollbar-color: var(--nv-color-scroll-thumb) transparent;\n }\n *::-webkit-scrollbar {\n width: 5px;\n height: 5px;\n background: transparent;\n }\n *::-webkit-scrollbar-track {\n background: transparent;\n }\n *::-webkit-scrollbar-thumb {\n background: var(--nv-color-scroll-thumb);\n border-radius: 3px;\n }\n *::-webkit-scrollbar-thumb:hover {\n background: var(--nv-color-scroll-thumb-hover);\n }\n *::-webkit-scrollbar-corner {\n background: transparent;\n }\n \n :where(body) {\n background: var(--nv-color-bg);\n color: var(--nv-color-fg-body);\n font-family: var(--nv-font-sans);\n font-size: var(--nv-text-base);\n line-height: var(--nv-leading-normal);\n -webkit-text-size-adjust: 100%;\n }\n :where(h1, h2, h3, h4, h5, h6) {\n color: var(--nv-color-fg);\n font-family: var(--nv-font-display);\n font-weight: var(--nv-weight-semi);\n line-height: var(--nv-leading-tight);\n text-wrap: balance;\n }\n :where(h1) {\n font-size: var(--nv-text-4xl);\n letter-spacing: -0.02em;\n }\n :where(h2) {\n font-size: var(--nv-text-3xl);\n letter-spacing: -0.015em;\n }\n :where(h3) {\n font-size: var(--nv-text-2xl);\n }\n :where(h4) {\n font-size: var(--nv-text-xl);\n }\n :where(h5) {\n font-size: var(--nv-text-lg);\n }\n :where(h6) {\n font-size: var(--nv-text-base);\n }\n :where(a) {\n color: var(--nv-color-link);\n text-underline-offset: 0.15em;\n }\n :where(a:hover) {\n color: var(--nv-color-link-hover);\n }\n :where(code, kbd, samp, pre) {\n font-family: var(--nv-font-mono);\n font-size: 0.9em;\n }\n :where(hr) {\n height: 1px;\n border: 0;\n background: var(--nv-color-border);\n }\n :focus-visible {\n outline: 2px solid var(--nv-color-border-focus);\n outline-offset: 2px;\n }\n .nv-sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n border: 0;\n }\n}";
|
|
5
|
+
export const COMPONENTS = "@layer nv.components {\n \n .nv-stack {\n display: flex;\n flex-direction: column;\n gap: var(--nv-stack-gap);\n }\n .nv-row {\n display: flex;\n flex-wrap: wrap;\n gap: var(--nv-space-2) var(--nv-space-4);\n align-items: baseline;\n }\n .nv-row[data-align='center'] {\n align-items: center;\n }\n .nv-page {\n width: 100%;\n max-width: var(--nv-page-w-md);\n margin-inline: auto;\n padding-inline: var(--nv-page-gutter);\n }\n .nv-page[data-width='sm'] {\n max-width: var(--nv-page-w-sm);\n }\n .nv-page[data-width='lg'] {\n max-width: var(--nv-page-w-lg);\n }\n .nv-section {\n padding-block: var(--nv-section-gap);\n }\n \n .nv-t-title {\n color: var(--nv-color-fg);\n font-family: var(--nv-font-display);\n font-size: var(--nv-text-3xl);\n font-weight: var(--nv-weight-semi);\n line-height: var(--nv-leading-tight);\n letter-spacing: -0.02em;\n }\n .nv-t-lead {\n color: var(--nv-color-fg);\n font-size: var(--nv-text-xl);\n line-height: var(--nv-leading-normal);\n }\n .nv-t-body {\n color: var(--nv-color-fg-body);\n font-size: var(--nv-text-base);\n }\n .nv-t-quiet {\n color: var(--nv-color-fg-muted);\n font-size: var(--nv-text-sm);\n }\n .nv-t-label {\n color: var(--nv-color-fg-muted);\n font-size: var(--nv-text-xs);\n font-weight: var(--nv-weight-medium);\n text-transform: uppercase;\n letter-spacing: 0.06em;\n }\n \n .nv-button {\n display: inline-flex;\n gap: var(--nv-space-2);\n align-items: center;\n justify-content: center;\n min-height: max(var(--nv-control-md), var(--nv-tap-target));\n padding-inline: var(--nv-space-4);\n font-size: var(--nv-text-sm);\n font-weight: var(--nv-weight-medium);\n color: var(--nv-color-fg-on-accent);\n text-decoration: none;\n cursor: pointer;\n background: var(--nv-color-accent);\n border: 1px solid transparent;\n border-radius: var(--nv-radius-md);\n transition: background var(--nv-duration-fast) var(--nv-ease-out);\n }\n .nv-button:hover {\n background: var(--nv-color-accent-hover);\n }\n .nv-button:active {\n background: var(--nv-color-accent-active);\n }\n .nv-button[disabled],\n .nv-button[aria-disabled='true'] {\n cursor: not-allowed;\n opacity: 0.55;\n }\n .nv-button[data-variant='quiet'] {\n color: var(--nv-color-fg);\n background: transparent;\n border-color: var(--nv-color-border-strong);\n }\n .nv-button[data-variant='quiet']:hover {\n background: var(--nv-color-bg-raised-hover);\n }\n .nv-button[data-variant='bare'] {\n min-height: max(var(--nv-control-sm), var(--nv-tap-target));\n padding-inline: var(--nv-space-2);\n color: var(--nv-color-fg-muted);\n background: transparent;\n }\n .nv-button[data-variant='bare']:hover {\n color: var(--nv-color-fg);\n background: var(--nv-color-bg-raised-hover);\n }\n .nv-button[data-variant='danger'] {\n color: var(--nv-color-fg-on-danger);\n background: var(--nv-color-danger);\n }\n .nv-button[data-variant='danger']:hover {\n background: var(--nv-color-danger-hover);\n }\n \n :where(input:not([type='checkbox'], [type='radio']), select, textarea) {\n width: 100%;\n min-height: max(var(--nv-control-md), var(--nv-tap-target));\n padding: var(--nv-space-2) var(--nv-space-3);\n font-size: var(--nv-text-input);\n color: var(--nv-color-fg);\n background: var(--nv-color-bg-raised);\n border: 1px solid var(--nv-color-border-strong);\n border-radius: var(--nv-radius-md);\n }\n :where(textarea) {\n min-height: calc(var(--nv-control-md) * 2);\n resize: vertical;\n }\n :where(input, select, textarea):disabled {\n cursor: not-allowed;\n opacity: 0.55;\n }\n :where(input, select, textarea)[aria-invalid='true'] {\n border-color: var(--nv-color-danger);\n }\n :where(input[type='checkbox'], input[type='radio']) {\n width: var(--nv-icon-md);\n height: var(--nv-icon-md);\n accent-color: var(--nv-color-accent);\n }\n .nv-field {\n display: flex;\n flex-direction: column;\n gap: var(--nv-space-1);\n }\n .nv-field > label {\n color: var(--nv-color-fg);\n font-size: var(--nv-text-sm);\n font-weight: var(--nv-weight-medium);\n }\n .nv-field > .nv-hint {\n color: var(--nv-color-fg-muted);\n font-size: var(--nv-text-xs);\n }\n .nv-field > .nv-error {\n color: var(--nv-color-danger);\n font-size: var(--nv-text-xs);\n }\n .nv-field[data-inline] {\n flex-direction: row;\n gap: var(--nv-space-2);\n align-items: center;\n }\n :where(fieldset) {\n display: flex;\n flex-direction: column;\n gap: var(--nv-form-gap);\n padding: var(--nv-card-pad);\n margin: 0;\n border: 1px solid var(--nv-color-border);\n border-radius: var(--nv-radius-md);\n }\n :where(legend) {\n padding-inline: var(--nv-space-2);\n color: var(--nv-color-fg);\n font-size: var(--nv-text-sm);\n font-weight: var(--nv-weight-medium);\n }\n \n .nv-card {\n display: flex;\n flex-direction: column;\n gap: var(--nv-space-2);\n padding: var(--nv-card-pad);\n background: var(--nv-color-bg-raised);\n border: 1px solid var(--nv-color-border);\n border-radius: var(--nv-radius-lg);\n }\n .nv-cards {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));\n gap: var(--nv-space-4);\n }\n .nv-notice {\n display: flex;\n flex-direction: column;\n gap: var(--nv-space-1);\n padding: var(--nv-space-3) var(--nv-space-4);\n color: var(--nv-color-fg);\n background: var(--nv-color-info-subtle);\n border-inline-start: 3px solid var(--nv-color-info);\n border-radius: var(--nv-radius-sm);\n }\n .nv-notice[data-tone='success'] {\n background: var(--nv-color-success-subtle);\n border-inline-start-color: var(--nv-color-success);\n }\n .nv-notice[data-tone='warning'] {\n background: var(--nv-color-warning-subtle);\n border-inline-start-color: var(--nv-color-warning);\n }\n .nv-notice[data-tone='danger'] {\n background: var(--nv-color-danger-subtle);\n border-inline-start-color: var(--nv-color-danger);\n }\n \n .nv-table-scroll {\n overflow-x: auto;\n }\n :where(table) {\n width: 100%;\n border-collapse: collapse;\n }\n :where(th, td) {\n padding: var(--nv-space-2) var(--nv-space-3);\n text-align: start;\n border-bottom: 1px solid var(--nv-color-border);\n }\n :where(th) {\n color: var(--nv-color-fg);\n font-size: var(--nv-text-sm);\n font-weight: var(--nv-weight-semi);\n }\n \n nv-theme {\n display: inline-flex;\n gap: var(--nv-space-05);\n padding: var(--nv-space-05);\n background: var(--nv-color-bg-raised);\n border: 1px solid var(--nv-color-border);\n border-radius: var(--nv-radius-full);\n }\n nv-theme button[aria-pressed='true'] {\n color: var(--nv-color-fg);\n background: var(--nv-color-bg-raised-strong);\n }\n}";
|
|
6
|
+
export const CSS = [TOKENS, THEME_DEFAULT, BASE, COMPONENTS].join('\n');
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const THEME_KEY = "nv.theme";
|
|
2
|
+
export declare const prepaint: string;
|
|
3
|
+
export declare function applyTheme(value: string, root: HTMLElement): void;
|
|
4
|
+
export declare class NvTheme extends HTMLElement {
|
|
5
|
+
connectedCallback(): void;
|
|
6
|
+
disconnectedCallback(): void;
|
|
7
|
+
handleEvent(event: Event): void;
|
|
8
|
+
private mark;
|
|
9
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// <nv-theme>: the theme choice, and nothing else.
|
|
3
|
+
//
|
|
4
|
+
// The buttons are the page's, server-rendered, one per theme the surface
|
|
5
|
+
// offers plus `auto` where it offers both. This element only wires them up:
|
|
6
|
+
// it reads the stored choice, writes data-theme on the document, and marks
|
|
7
|
+
// which button is pressed. With no script the buttons are still there and
|
|
8
|
+
// still labelled, and the page still paints, because `auto` is the state a
|
|
9
|
+
// document in no theme is already in.
|
|
10
|
+
//
|
|
11
|
+
// The store is one key, so the app, the screen and a website all read the
|
|
12
|
+
// same choice on the same origin.
|
|
13
|
+
export const THEME_KEY = 'nv.theme';
|
|
14
|
+
// Inline this in <head>, before any stylesheet, or the first paint is the
|
|
15
|
+
// wrong theme and the second one is a flash. It is the only script the kit
|
|
16
|
+
// asks a page to inline, and it is this short on purpose.
|
|
17
|
+
export const prepaint = `try{var t=localStorage.getItem(${JSON.stringify(THEME_KEY)});` +
|
|
18
|
+
`if(t&&t!=='auto')document.documentElement.dataset.theme=t}catch(e){}`;
|
|
19
|
+
// Storage is gone in a private window, cleared by a browser that blocks site
|
|
20
|
+
// data, and throws outright in some embeddings. A theme control is a
|
|
21
|
+
// convenience, so every read and write fails quietly and the surface stays
|
|
22
|
+
// on `auto`.
|
|
23
|
+
function stored() {
|
|
24
|
+
try {
|
|
25
|
+
return localStorage.getItem(THEME_KEY);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function store(value) {
|
|
32
|
+
try {
|
|
33
|
+
if (value === 'auto')
|
|
34
|
+
localStorage.removeItem(THEME_KEY);
|
|
35
|
+
else
|
|
36
|
+
localStorage.setItem(THEME_KEY, value);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
/* a choice that cannot be remembered still applies to this page */
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function applyTheme(value, root) {
|
|
43
|
+
if (value === 'auto')
|
|
44
|
+
delete root.dataset.theme;
|
|
45
|
+
else
|
|
46
|
+
root.dataset.theme = value;
|
|
47
|
+
}
|
|
48
|
+
export class NvTheme extends HTMLElement {
|
|
49
|
+
connectedCallback() {
|
|
50
|
+
this.addEventListener('click', this);
|
|
51
|
+
this.mark(stored() ?? 'auto');
|
|
52
|
+
}
|
|
53
|
+
disconnectedCallback() {
|
|
54
|
+
this.removeEventListener('click', this);
|
|
55
|
+
}
|
|
56
|
+
handleEvent(event) {
|
|
57
|
+
const button = event.target?.closest('[data-theme]');
|
|
58
|
+
if (!button || !this.contains(button))
|
|
59
|
+
return;
|
|
60
|
+
const chosen = button.dataset.theme ?? 'auto';
|
|
61
|
+
store(chosen);
|
|
62
|
+
this.mark(chosen);
|
|
63
|
+
}
|
|
64
|
+
// The document and the buttons say the same thing, always, and the element
|
|
65
|
+
// is the only writer of either.
|
|
66
|
+
mark(chosen) {
|
|
67
|
+
applyTheme(chosen, this.ownerDocument.documentElement);
|
|
68
|
+
for (const button of this.querySelectorAll('[data-theme]')) {
|
|
69
|
+
button.setAttribute('aria-pressed', String(button.dataset.theme === chosen));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (typeof customElements !== 'undefined' && !customElements.get('nv-theme')) {
|
|
74
|
+
customElements.define('nv-theme', NvTheme);
|
|
75
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// `nv-livery [dir]` compiles the design in a folder: it reads `livery.json`
|
|
4
|
+
// and writes `tokens.css` beside it. A design holds no build script of its
|
|
5
|
+
// own, so there is one compiler in the tree and every design calls it.
|
|
6
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { resolve } from 'node:path';
|
|
8
|
+
import { renderTokensCss } from './livery.js';
|
|
9
|
+
const dir = resolve(process.argv[2] ?? '.');
|
|
10
|
+
const from = resolve(dir, 'livery.json');
|
|
11
|
+
const to = resolve(dir, 'tokens.css');
|
|
12
|
+
let livery;
|
|
13
|
+
try {
|
|
14
|
+
livery = JSON.parse(readFileSync(from, 'utf8'));
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
process.stderr.write(`nv-livery: ${err.message}\n`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
let css;
|
|
21
|
+
try {
|
|
22
|
+
css = renderTokensCss(livery);
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
process.stderr.write(`nv-livery: ${err.message}\n`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
writeFileSync(to, css);
|
|
29
|
+
process.stdout.write(`nv-livery: ${Object.keys(livery.themes).length} themes from the ${livery.name} design\n`);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type Theme = {
|
|
2
|
+
colorScheme: 'light' | 'dark';
|
|
3
|
+
slots: Record<string, string>;
|
|
4
|
+
};
|
|
5
|
+
export type Livery = {
|
|
6
|
+
name: string;
|
|
7
|
+
themes: Record<string, Theme>;
|
|
8
|
+
};
|
|
9
|
+
export declare function missingSlots(slots: Record<string, string>): string[];
|
|
10
|
+
export declare function forbiddenSlots(slots: Record<string, string>): string[];
|
|
11
|
+
export declare function renderTokensCss(livery: Livery): string;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The livery compiler. A design is one document, `livery.json`, and this
|
|
3
|
+
// turns it into the CSS that document means. The compiler is the kit's
|
|
4
|
+
// because it is the same for every design; the palette is the design's
|
|
5
|
+
// because it is nobody else's.
|
|
6
|
+
import { THEME_SLOTS_REQUIRED, designMaySet } from './manifest.js';
|
|
7
|
+
export function missingSlots(slots) {
|
|
8
|
+
return THEME_SLOTS_REQUIRED.filter((slot) => slots[slot] === undefined);
|
|
9
|
+
}
|
|
10
|
+
// A design that reaches for structure is refused by name, so the message says
|
|
11
|
+
// which property and not merely that something was wrong.
|
|
12
|
+
export function forbiddenSlots(slots) {
|
|
13
|
+
return Object.keys(slots).filter((slot) => !designMaySet(slot));
|
|
14
|
+
}
|
|
15
|
+
// Throws on the first theme that leaves a required slot undefined or sets one
|
|
16
|
+
// that belongs to the kit. A design either paints a whole face or it does not
|
|
17
|
+
// compile.
|
|
18
|
+
export function renderTokensCss(livery) {
|
|
19
|
+
const blocks = [];
|
|
20
|
+
for (const [theme, { colorScheme, slots }] of Object.entries(livery.themes)) {
|
|
21
|
+
const missing = missingSlots(slots);
|
|
22
|
+
if (missing.length > 0) {
|
|
23
|
+
throw new Error(`${theme}: livery.json declares no value for:\n ${missing.join('\n ')}`);
|
|
24
|
+
}
|
|
25
|
+
const forbidden = forbiddenSlots(slots);
|
|
26
|
+
if (forbidden.length > 0) {
|
|
27
|
+
throw new Error(`${theme}: structure belongs to the kit, and livery.json sets:\n ${forbidden.join('\n ')}`);
|
|
28
|
+
}
|
|
29
|
+
const declared = Object.entries(slots)
|
|
30
|
+
.map(([slot, value]) => ` ${slot}: ${value};`)
|
|
31
|
+
.join('\n');
|
|
32
|
+
blocks.push(`:root[data-theme='${theme}'] {\n color-scheme: ${colorScheme};\n\n${declared}\n}`);
|
|
33
|
+
}
|
|
34
|
+
return (`/*\n * Generated from livery.json. Never edit: run nv-livery.\n *\n` +
|
|
35
|
+
` * The ${livery.name} palette, colour and shadow only, worn by whichever\n` +
|
|
36
|
+
` * estate wears the ${livery.name} design. Declared unlayered, so it wins\n` +
|
|
37
|
+
` * over the kit's layered defaults without a specificity trick.\n */\n\n` +
|
|
38
|
+
`${blocks.join('\n\n')}\n`);
|
|
39
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const BREAKPOINTS: Readonly<{
|
|
2
|
+
tablet: 768;
|
|
3
|
+
desktop: 1280;
|
|
4
|
+
}>;
|
|
5
|
+
export declare const STRUCTURAL_PREFIXES: readonly string[];
|
|
6
|
+
export declare const ROLE_COLOR_PREFIX = "--nv-color-role-";
|
|
7
|
+
export declare const THEME_SLOTS_REQUIRED: readonly string[];
|
|
8
|
+
export declare const THEME_SLOTS_OPTIONAL: readonly string[];
|
|
9
|
+
export declare const FONT_SLOTS: readonly string[];
|
|
10
|
+
export declare function designMaySet(property: string): boolean;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The contract, as data. Every rule about what a design may set and what it
|
|
3
|
+
// may not is one of these lists, so a validator and a reviewer read the same
|
|
4
|
+
// thing. `css/tokens.css` declares the structural side; a design declares the
|
|
5
|
+
// slots.
|
|
6
|
+
// The only width constants there are. Everything else derives from these or
|
|
7
|
+
// from data-tier, and a raw width media query in a component is illegal.
|
|
8
|
+
export const BREAKPOINTS = Object.freeze({ tablet: 768, desktop: 1280 });
|
|
9
|
+
// Root-only, immutable. A design that defines a property under any of these
|
|
10
|
+
// prefixes fails conformance.
|
|
11
|
+
export const STRUCTURAL_PREFIXES = Object.freeze([
|
|
12
|
+
'--nv-space-',
|
|
13
|
+
'--nv-radius-',
|
|
14
|
+
'--nv-text-',
|
|
15
|
+
'--nv-leading-',
|
|
16
|
+
'--nv-weight-',
|
|
17
|
+
'--nv-duration-',
|
|
18
|
+
'--nv-ease-',
|
|
19
|
+
'--nv-motion',
|
|
20
|
+
'--nv-z-',
|
|
21
|
+
'--nv-control-',
|
|
22
|
+
'--nv-icon-',
|
|
23
|
+
'--nv-tap-target',
|
|
24
|
+
'--nv-safe-',
|
|
25
|
+
'--nv-kb-inset',
|
|
26
|
+
'--nv-inset-bottom',
|
|
27
|
+
'--nv-page-',
|
|
28
|
+
'--nv-stack-gap',
|
|
29
|
+
'--nv-card-pad',
|
|
30
|
+
'--nv-form-gap',
|
|
31
|
+
'--nv-section-gap',
|
|
32
|
+
'--nv-header-h',
|
|
33
|
+
'--nv-tabbar-h',
|
|
34
|
+
'--nv-sidebar-w',
|
|
35
|
+
'--nv-rail-w',
|
|
36
|
+
'--nv-aside-w',
|
|
37
|
+
]);
|
|
38
|
+
// A design may mint a colour under this prefix for a role of its own. Every
|
|
39
|
+
// read of one carries a muted fallback, so an unminted role paints quiet
|
|
40
|
+
// rather than nothing.
|
|
41
|
+
export const ROLE_COLOR_PREFIX = '--nv-color-role-';
|
|
42
|
+
// The slots a design owns, and every one of them is required in every theme
|
|
43
|
+
// it declares. A palette with a hole in it is a palette that paints a
|
|
44
|
+
// surprise on the one screen nobody opened.
|
|
45
|
+
export const THEME_SLOTS_REQUIRED = Object.freeze([
|
|
46
|
+
'--nv-color-bg',
|
|
47
|
+
'--nv-color-bg-raised',
|
|
48
|
+
'--nv-color-bg-raised-hover',
|
|
49
|
+
'--nv-color-bg-raised-strong',
|
|
50
|
+
'--nv-color-bg-sunken',
|
|
51
|
+
'--nv-color-bg-overlay',
|
|
52
|
+
'--nv-color-fg',
|
|
53
|
+
'--nv-color-fg-body',
|
|
54
|
+
'--nv-color-fg-muted',
|
|
55
|
+
'--nv-color-fg-subtle',
|
|
56
|
+
'--nv-color-fg-on-accent',
|
|
57
|
+
'--nv-color-border',
|
|
58
|
+
'--nv-color-border-strong',
|
|
59
|
+
'--nv-color-border-focus',
|
|
60
|
+
'--nv-color-accent',
|
|
61
|
+
'--nv-color-accent-hover',
|
|
62
|
+
'--nv-color-accent-active',
|
|
63
|
+
'--nv-color-accent-subtle',
|
|
64
|
+
'--nv-color-success',
|
|
65
|
+
'--nv-color-success-subtle',
|
|
66
|
+
'--nv-color-warning',
|
|
67
|
+
'--nv-color-warning-subtle',
|
|
68
|
+
'--nv-color-danger',
|
|
69
|
+
'--nv-color-danger-subtle',
|
|
70
|
+
'--nv-color-info',
|
|
71
|
+
'--nv-color-info-subtle',
|
|
72
|
+
'--nv-color-scroll-thumb',
|
|
73
|
+
'--nv-color-scroll-thumb-hover',
|
|
74
|
+
'--nv-shadow-sm',
|
|
75
|
+
'--nv-shadow-md',
|
|
76
|
+
'--nv-shadow-lg',
|
|
77
|
+
'--nv-shadow-xl',
|
|
78
|
+
'--nv-shadow-focus',
|
|
79
|
+
]);
|
|
80
|
+
// Overridable, and defaulted at the root from the required ones, so a design
|
|
81
|
+
// that says nothing about them still paints.
|
|
82
|
+
export const THEME_SLOTS_OPTIONAL = Object.freeze([
|
|
83
|
+
'--nv-color-link',
|
|
84
|
+
'--nv-color-link-hover',
|
|
85
|
+
'--nv-color-border-subtle',
|
|
86
|
+
'--nv-color-danger-hover',
|
|
87
|
+
'--nv-color-fg-on-danger',
|
|
88
|
+
'--nv-brand-healthy',
|
|
89
|
+
'--nv-brand-reconnecting',
|
|
90
|
+
'--nv-brand-offline',
|
|
91
|
+
'--nv-brand-logo-size',
|
|
92
|
+
]);
|
|
93
|
+
// A design may point these at its own faces. It ships the files itself.
|
|
94
|
+
export const FONT_SLOTS = Object.freeze(['--nv-font-sans', '--nv-font-mono', '--nv-font-display']);
|
|
95
|
+
// True when a design may set this property. Every other `--nv-` name is
|
|
96
|
+
// structure and belongs to the kit.
|
|
97
|
+
export function designMaySet(property) {
|
|
98
|
+
if (STRUCTURAL_PREFIXES.some((prefix) => property.startsWith(prefix)))
|
|
99
|
+
return false;
|
|
100
|
+
if (property.startsWith(ROLE_COLOR_PREFIX))
|
|
101
|
+
return true;
|
|
102
|
+
if (FONT_SLOTS.includes(property))
|
|
103
|
+
return true;
|
|
104
|
+
return THEME_SLOTS_REQUIRED.includes(property) || THEME_SLOTS_OPTIONAL.includes(property);
|
|
105
|
+
}
|