@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.
@@ -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
@@ -0,0 +1,5 @@
1
+ export declare const TOKENS: string;
2
+ export declare const THEME_DEFAULT: string;
3
+ export declare const BASE: string;
4
+ export declare const COMPONENTS: string;
5
+ export declare const CSS: string;
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,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -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
+ }