@junoput01/junoui 0.7.0 → 0.9.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/CHANGELOG.md +139 -0
- package/dist/android/dimens.xml +2 -0
- package/dist/classes.json +175 -1
- package/dist/css/juno-tokens.css +10 -0
- package/dist/css/juno.css +861 -39
- package/dist/flutter/juno_tokens.dart +10 -0
- package/dist/icons/inline.js +1 -1
- package/dist/icons/juno-icons.svg +14 -0
- package/dist/ios/JunoTokens.swift +10 -0
- package/dist/js/tokens.js +16 -0
- package/dist/json/tokens.json +51 -0
- package/dist/rust/juno_tokens.rs +383 -0
- package/dist/scss/_juno-tokens.scss +10 -0
- package/docs/components/canvas-ink.md +71 -0
- package/docs/components/gizmo.md +114 -0
- package/docs/components/swatch.md +95 -0
- package/docs/components/tree.md +112 -0
- package/docs/conformance-kit.md +26 -14
- package/docs/icon-subsetting.md +16 -1
- package/docs/native.md +38 -1
- package/docs/tokens-reference.md +15 -0
- package/package.json +4 -1
- package/src/css/base.css +10 -39
- package/src/css/components/canvas-ink.css +97 -0
- package/src/css/components/gizmo.css +238 -0
- package/src/css/components/swatch.css +187 -0
- package/src/css/components/tree.css +259 -0
- package/src/css/touch-surfaces.mjs +95 -0
- package/src/icons/compass.svg +1 -0
- package/src/icons/crosshair-simple.svg +1 -0
- package/src/icons/crosshair.svg +1 -0
- package/src/icons/cube.svg +1 -0
- package/src/icons/globe.svg +1 -0
- package/src/icons/map-pin.svg +1 -0
- package/src/icons/map-trifold.svg +1 -0
- package/src/icons/mountains.svg +1 -0
- package/src/icons/path.svg +1 -0
- package/src/icons/polygon.svg +1 -0
- package/src/icons/ruler.svg +1 -0
- package/src/icons/scissors.svg +1 -0
- package/src/icons/selection.svg +1 -0
- package/src/icons/stack.svg +1 -0
- package/tools/gizmo.mjs +144 -0
- package/tools/tree.mjs +178 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
// junoui design tokens — Rust. Generated; do not edit.
|
|
2
|
+
//
|
|
3
|
+
// Regenerate with `npm run build` in the junoui repo. Every value here comes
|
|
4
|
+
// from the same DTCG source as the CSS, Swift, Dart and Android outputs, so a
|
|
5
|
+
// token change reaches all of them at once. Do not transcribe these values
|
|
6
|
+
// into your own crate: that copy is what goes stale.
|
|
7
|
+
#![allow(dead_code)]
|
|
8
|
+
|
|
9
|
+
/// A non-premultiplied sRGB color with 8 bits per channel.
|
|
10
|
+
///
|
|
11
|
+
/// Deliberately plain, and deliberately not a dependency on any UI crate:
|
|
12
|
+
/// junoui does not know whether you are on egui, iced, Slint, Bevy or wgpu
|
|
13
|
+
/// directly. Convert at your boundary — `to_f32_array()` is what most GPU
|
|
14
|
+
/// paths want, and it is sRGB-encoded, NOT linear. If your pipeline expects
|
|
15
|
+
/// linear (wgpu with a non-sRGB surface format), convert there; junoui cannot
|
|
16
|
+
/// know which surface you created.
|
|
17
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
18
|
+
pub struct Rgba {
|
|
19
|
+
pub r: u8,
|
|
20
|
+
pub g: u8,
|
|
21
|
+
pub b: u8,
|
|
22
|
+
pub a: u8,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
impl Rgba {
|
|
26
|
+
/// From a 0xRRGGBB literal, fully opaque.
|
|
27
|
+
pub const fn hex(v: u32) -> Self {
|
|
28
|
+
Self {
|
|
29
|
+
r: ((v >> 16) & 0xFF) as u8,
|
|
30
|
+
g: ((v >> 8) & 0xFF) as u8,
|
|
31
|
+
b: (v & 0xFF) as u8,
|
|
32
|
+
a: 0xFF,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// sRGB-encoded components in 0.0..=1.0, in RGBA order.
|
|
37
|
+
pub const fn to_f32_array(self) -> [f32; 4] {
|
|
38
|
+
[
|
|
39
|
+
self.r as f32 / 255.0,
|
|
40
|
+
self.g as f32 / 255.0,
|
|
41
|
+
self.b as f32 / 255.0,
|
|
42
|
+
self.a as f32 / 255.0,
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// The same color at a different alpha — for the opacity tokens below.
|
|
47
|
+
pub const fn with_alpha(self, a: u8) -> Self {
|
|
48
|
+
Self { a, ..self }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// One theme's semantic roles. Fields are generated from junoui's role list,
|
|
53
|
+
/// so every palette/mode constant below carries all of them.
|
|
54
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
55
|
+
pub struct Palette {
|
|
56
|
+
pub nominal: Rgba,
|
|
57
|
+
pub active: Rgba,
|
|
58
|
+
pub target: Rgba,
|
|
59
|
+
pub caution: Rgba,
|
|
60
|
+
pub warning: Rgba,
|
|
61
|
+
pub data: Rgba,
|
|
62
|
+
pub data_dim: Rgba,
|
|
63
|
+
pub label: Rgba,
|
|
64
|
+
pub muted: Rgba,
|
|
65
|
+
pub border: Rgba,
|
|
66
|
+
pub border_strong: Rgba,
|
|
67
|
+
pub s0: Rgba,
|
|
68
|
+
pub s1: Rgba,
|
|
69
|
+
pub s2: Rgba,
|
|
70
|
+
pub s3: Rgba,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── themes ──────────────────────────────────────────────────────────────
|
|
74
|
+
pub const COLORBLIND_DARK: Palette = Palette {
|
|
75
|
+
nominal: Rgba::hex(0x78A9FF),
|
|
76
|
+
active: Rgba::hex(0xBE95FF),
|
|
77
|
+
target: Rgba::hex(0xFF7EB6),
|
|
78
|
+
caution: Rgba::hex(0xF1C21B),
|
|
79
|
+
warning: Rgba::hex(0xFF832B),
|
|
80
|
+
data: Rgba::hex(0xEAE7E2),
|
|
81
|
+
data_dim: Rgba::hex(0x45423C),
|
|
82
|
+
label: Rgba::hex(0x8C8981),
|
|
83
|
+
muted: Rgba::hex(0x625F59),
|
|
84
|
+
border: Rgba::hex(0x312D27),
|
|
85
|
+
border_strong: Rgba::hex(0x4B4740),
|
|
86
|
+
s0: Rgba::hex(0x060402),
|
|
87
|
+
s1: Rgba::hex(0x0D0B06),
|
|
88
|
+
s2: Rgba::hex(0x16130E),
|
|
89
|
+
s3: Rgba::hex(0x221F19),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
pub const COLORBLIND_LIGHT: Palette = Palette {
|
|
93
|
+
nominal: Rgba::hex(0x0043CE),
|
|
94
|
+
active: Rgba::hex(0x6929C4),
|
|
95
|
+
target: Rgba::hex(0x9F1853),
|
|
96
|
+
caution: Rgba::hex(0x7D4F00),
|
|
97
|
+
warning: Rgba::hex(0xBA4300),
|
|
98
|
+
data: Rgba::hex(0x0B0906),
|
|
99
|
+
data_dim: Rgba::hex(0xA19E99),
|
|
100
|
+
label: Rgba::hex(0x504D47),
|
|
101
|
+
muted: Rgba::hex(0x8B8984),
|
|
102
|
+
border: Rgba::hex(0xC7C4BD),
|
|
103
|
+
border_strong: Rgba::hex(0xAEAAA4),
|
|
104
|
+
s0: Rgba::hex(0xF8F5EF),
|
|
105
|
+
s1: Rgba::hex(0xEBE7E0),
|
|
106
|
+
s2: Rgba::hex(0xDEDAD3),
|
|
107
|
+
s3: Rgba::hex(0xD1CDC7),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
pub const SOFT_DARK: Palette = Palette {
|
|
111
|
+
nominal: Rgba::hex(0x38A065),
|
|
112
|
+
active: Rgba::hex(0x00A8BE),
|
|
113
|
+
target: Rgba::hex(0x9C5CAF),
|
|
114
|
+
caution: Rgba::hex(0xD09945),
|
|
115
|
+
warning: Rgba::hex(0xD15D4D),
|
|
116
|
+
data: Rgba::hex(0xE0DED8),
|
|
117
|
+
data_dim: Rgba::hex(0x47453F),
|
|
118
|
+
label: Rgba::hex(0x8C8982),
|
|
119
|
+
muted: Rgba::hex(0x615F59),
|
|
120
|
+
border: Rgba::hex(0x32302B),
|
|
121
|
+
border_strong: Rgba::hex(0x4D4A44),
|
|
122
|
+
s0: Rgba::hex(0x070602),
|
|
123
|
+
s1: Rgba::hex(0x0F0D08),
|
|
124
|
+
s2: Rgba::hex(0x181610),
|
|
125
|
+
s3: Rgba::hex(0x24211B),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
pub const SOFT_LIGHT: Palette = Palette {
|
|
129
|
+
nominal: Rgba::hex(0x00582C),
|
|
130
|
+
active: Rgba::hex(0x006174),
|
|
131
|
+
target: Rgba::hex(0x673477),
|
|
132
|
+
caution: Rgba::hex(0x8A5700),
|
|
133
|
+
warning: Rgba::hex(0x86281D),
|
|
134
|
+
data: Rgba::hex(0x0A0906),
|
|
135
|
+
data_dim: Rgba::hex(0x9D9B96),
|
|
136
|
+
label: Rgba::hex(0x55524C),
|
|
137
|
+
muted: Rgba::hex(0x8A8782),
|
|
138
|
+
border: Rgba::hex(0xCDCAC3),
|
|
139
|
+
border_strong: Rgba::hex(0xB4B1AA),
|
|
140
|
+
s0: Rgba::hex(0xF4F2EA),
|
|
141
|
+
s1: Rgba::hex(0xE7E4DD),
|
|
142
|
+
s2: Rgba::hex(0xDAD7D0),
|
|
143
|
+
s3: Rgba::hex(0xCDCAC3),
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
pub const STANDARD_DARK: Palette = Palette {
|
|
147
|
+
nominal: Rgba::hex(0x00CB4C),
|
|
148
|
+
active: Rgba::hex(0x00CDDF),
|
|
149
|
+
target: Rgba::hex(0xD63DE6),
|
|
150
|
+
caution: Rgba::hex(0xF4A000),
|
|
151
|
+
warning: Rgba::hex(0xF13A32),
|
|
152
|
+
data: Rgba::hex(0xEAE7E2),
|
|
153
|
+
data_dim: Rgba::hex(0x45423C),
|
|
154
|
+
label: Rgba::hex(0x8C8981),
|
|
155
|
+
muted: Rgba::hex(0x625F59),
|
|
156
|
+
border: Rgba::hex(0x312D27),
|
|
157
|
+
border_strong: Rgba::hex(0x4B4740),
|
|
158
|
+
s0: Rgba::hex(0x060402),
|
|
159
|
+
s1: Rgba::hex(0x0D0B06),
|
|
160
|
+
s2: Rgba::hex(0x16130E),
|
|
161
|
+
s3: Rgba::hex(0x221F19),
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
pub const STANDARD_LIGHT: Palette = Palette {
|
|
165
|
+
nominal: Rgba::hex(0x006400),
|
|
166
|
+
active: Rgba::hex(0x006B98),
|
|
167
|
+
target: Rgba::hex(0x8B0097),
|
|
168
|
+
caution: Rgba::hex(0x9B4C00),
|
|
169
|
+
warning: Rgba::hex(0xA10000),
|
|
170
|
+
data: Rgba::hex(0x0B0906),
|
|
171
|
+
data_dim: Rgba::hex(0xA19E99),
|
|
172
|
+
label: Rgba::hex(0x504D47),
|
|
173
|
+
muted: Rgba::hex(0x8B8984),
|
|
174
|
+
border: Rgba::hex(0xC7C4BD),
|
|
175
|
+
border_strong: Rgba::hex(0xAEAAA4),
|
|
176
|
+
s0: Rgba::hex(0xF8F5EF),
|
|
177
|
+
s1: Rgba::hex(0xEBE7E0),
|
|
178
|
+
s2: Rgba::hex(0xDEDAD3),
|
|
179
|
+
s3: Rgba::hex(0xD1CDC7),
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// ── colors, flat ────────────────────────────────────────────────────────
|
|
183
|
+
pub const COLORBLIND_DARK_NOMINAL: Rgba = Rgba::hex(0x78A9FF);
|
|
184
|
+
pub const COLORBLIND_DARK_ACTIVE: Rgba = Rgba::hex(0xBE95FF);
|
|
185
|
+
pub const COLORBLIND_DARK_TARGET: Rgba = Rgba::hex(0xFF7EB6);
|
|
186
|
+
pub const COLORBLIND_DARK_CAUTION: Rgba = Rgba::hex(0xF1C21B);
|
|
187
|
+
pub const COLORBLIND_DARK_WARNING: Rgba = Rgba::hex(0xFF832B);
|
|
188
|
+
pub const COLORBLIND_DARK_DATA: Rgba = Rgba::hex(0xEAE7E2);
|
|
189
|
+
pub const COLORBLIND_DARK_LABEL: Rgba = Rgba::hex(0x8C8981);
|
|
190
|
+
pub const COLORBLIND_DARK_MUTED: Rgba = Rgba::hex(0x625F59);
|
|
191
|
+
pub const COLORBLIND_DARK_BORDER: Rgba = Rgba::hex(0x312D27);
|
|
192
|
+
pub const COLORBLIND_DARK_S0: Rgba = Rgba::hex(0x060402);
|
|
193
|
+
pub const COLORBLIND_DARK_S1: Rgba = Rgba::hex(0x0D0B06);
|
|
194
|
+
pub const COLORBLIND_DARK_S2: Rgba = Rgba::hex(0x16130E);
|
|
195
|
+
pub const COLORBLIND_DARK_S3: Rgba = Rgba::hex(0x221F19);
|
|
196
|
+
pub const COLORBLIND_DARK_DATA_DIM: Rgba = Rgba::hex(0x45423C);
|
|
197
|
+
pub const COLORBLIND_DARK_BORDER_STRONG: Rgba = Rgba::hex(0x4B4740);
|
|
198
|
+
pub const COLORBLIND_LIGHT_NOMINAL: Rgba = Rgba::hex(0x0043CE);
|
|
199
|
+
pub const COLORBLIND_LIGHT_ACTIVE: Rgba = Rgba::hex(0x6929C4);
|
|
200
|
+
pub const COLORBLIND_LIGHT_TARGET: Rgba = Rgba::hex(0x9F1853);
|
|
201
|
+
pub const COLORBLIND_LIGHT_CAUTION: Rgba = Rgba::hex(0x7D4F00);
|
|
202
|
+
pub const COLORBLIND_LIGHT_WARNING: Rgba = Rgba::hex(0xBA4300);
|
|
203
|
+
pub const COLORBLIND_LIGHT_DATA: Rgba = Rgba::hex(0x0B0906);
|
|
204
|
+
pub const COLORBLIND_LIGHT_LABEL: Rgba = Rgba::hex(0x504D47);
|
|
205
|
+
pub const COLORBLIND_LIGHT_MUTED: Rgba = Rgba::hex(0x8B8984);
|
|
206
|
+
pub const COLORBLIND_LIGHT_BORDER: Rgba = Rgba::hex(0xC7C4BD);
|
|
207
|
+
pub const COLORBLIND_LIGHT_S0: Rgba = Rgba::hex(0xF8F5EF);
|
|
208
|
+
pub const COLORBLIND_LIGHT_S1: Rgba = Rgba::hex(0xEBE7E0);
|
|
209
|
+
pub const COLORBLIND_LIGHT_S2: Rgba = Rgba::hex(0xDEDAD3);
|
|
210
|
+
pub const COLORBLIND_LIGHT_S3: Rgba = Rgba::hex(0xD1CDC7);
|
|
211
|
+
pub const COLORBLIND_LIGHT_DATA_DIM: Rgba = Rgba::hex(0xA19E99);
|
|
212
|
+
pub const COLORBLIND_LIGHT_BORDER_STRONG: Rgba = Rgba::hex(0xAEAAA4);
|
|
213
|
+
pub const SOFT_DARK_NOMINAL: Rgba = Rgba::hex(0x38A065);
|
|
214
|
+
pub const SOFT_DARK_ACTIVE: Rgba = Rgba::hex(0x00A8BE);
|
|
215
|
+
pub const SOFT_DARK_TARGET: Rgba = Rgba::hex(0x9C5CAF);
|
|
216
|
+
pub const SOFT_DARK_CAUTION: Rgba = Rgba::hex(0xD09945);
|
|
217
|
+
pub const SOFT_DARK_WARNING: Rgba = Rgba::hex(0xD15D4D);
|
|
218
|
+
pub const SOFT_DARK_DATA: Rgba = Rgba::hex(0xE0DED8);
|
|
219
|
+
pub const SOFT_DARK_LABEL: Rgba = Rgba::hex(0x8C8982);
|
|
220
|
+
pub const SOFT_DARK_MUTED: Rgba = Rgba::hex(0x615F59);
|
|
221
|
+
pub const SOFT_DARK_BORDER: Rgba = Rgba::hex(0x32302B);
|
|
222
|
+
pub const SOFT_DARK_S0: Rgba = Rgba::hex(0x070602);
|
|
223
|
+
pub const SOFT_DARK_S1: Rgba = Rgba::hex(0x0F0D08);
|
|
224
|
+
pub const SOFT_DARK_S2: Rgba = Rgba::hex(0x181610);
|
|
225
|
+
pub const SOFT_DARK_S3: Rgba = Rgba::hex(0x24211B);
|
|
226
|
+
pub const SOFT_DARK_DATA_DIM: Rgba = Rgba::hex(0x47453F);
|
|
227
|
+
pub const SOFT_DARK_BORDER_STRONG: Rgba = Rgba::hex(0x4D4A44);
|
|
228
|
+
pub const SOFT_LIGHT_NOMINAL: Rgba = Rgba::hex(0x00582C);
|
|
229
|
+
pub const SOFT_LIGHT_ACTIVE: Rgba = Rgba::hex(0x006174);
|
|
230
|
+
pub const SOFT_LIGHT_TARGET: Rgba = Rgba::hex(0x673477);
|
|
231
|
+
pub const SOFT_LIGHT_CAUTION: Rgba = Rgba::hex(0x8A5700);
|
|
232
|
+
pub const SOFT_LIGHT_WARNING: Rgba = Rgba::hex(0x86281D);
|
|
233
|
+
pub const SOFT_LIGHT_DATA: Rgba = Rgba::hex(0x0A0906);
|
|
234
|
+
pub const SOFT_LIGHT_LABEL: Rgba = Rgba::hex(0x55524C);
|
|
235
|
+
pub const SOFT_LIGHT_MUTED: Rgba = Rgba::hex(0x8A8782);
|
|
236
|
+
pub const SOFT_LIGHT_BORDER: Rgba = Rgba::hex(0xCDCAC3);
|
|
237
|
+
pub const SOFT_LIGHT_S0: Rgba = Rgba::hex(0xF4F2EA);
|
|
238
|
+
pub const SOFT_LIGHT_S1: Rgba = Rgba::hex(0xE7E4DD);
|
|
239
|
+
pub const SOFT_LIGHT_S2: Rgba = Rgba::hex(0xDAD7D0);
|
|
240
|
+
pub const SOFT_LIGHT_S3: Rgba = Rgba::hex(0xCDCAC3);
|
|
241
|
+
pub const SOFT_LIGHT_DATA_DIM: Rgba = Rgba::hex(0x9D9B96);
|
|
242
|
+
pub const SOFT_LIGHT_BORDER_STRONG: Rgba = Rgba::hex(0xB4B1AA);
|
|
243
|
+
pub const STANDARD_DARK_NOMINAL: Rgba = Rgba::hex(0x00CB4C);
|
|
244
|
+
pub const STANDARD_DARK_ACTIVE: Rgba = Rgba::hex(0x00CDDF);
|
|
245
|
+
pub const STANDARD_DARK_TARGET: Rgba = Rgba::hex(0xD63DE6);
|
|
246
|
+
pub const STANDARD_DARK_CAUTION: Rgba = Rgba::hex(0xF4A000);
|
|
247
|
+
pub const STANDARD_DARK_WARNING: Rgba = Rgba::hex(0xF13A32);
|
|
248
|
+
pub const STANDARD_DARK_DATA: Rgba = Rgba::hex(0xEAE7E2);
|
|
249
|
+
pub const STANDARD_DARK_LABEL: Rgba = Rgba::hex(0x8C8981);
|
|
250
|
+
pub const STANDARD_DARK_MUTED: Rgba = Rgba::hex(0x625F59);
|
|
251
|
+
pub const STANDARD_DARK_BORDER: Rgba = Rgba::hex(0x312D27);
|
|
252
|
+
pub const STANDARD_DARK_S0: Rgba = Rgba::hex(0x060402);
|
|
253
|
+
pub const STANDARD_DARK_S1: Rgba = Rgba::hex(0x0D0B06);
|
|
254
|
+
pub const STANDARD_DARK_S2: Rgba = Rgba::hex(0x16130E);
|
|
255
|
+
pub const STANDARD_DARK_S3: Rgba = Rgba::hex(0x221F19);
|
|
256
|
+
pub const STANDARD_DARK_DATA_DIM: Rgba = Rgba::hex(0x45423C);
|
|
257
|
+
pub const STANDARD_DARK_BORDER_STRONG: Rgba = Rgba::hex(0x4B4740);
|
|
258
|
+
pub const STANDARD_LIGHT_NOMINAL: Rgba = Rgba::hex(0x006400);
|
|
259
|
+
pub const STANDARD_LIGHT_ACTIVE: Rgba = Rgba::hex(0x006B98);
|
|
260
|
+
pub const STANDARD_LIGHT_TARGET: Rgba = Rgba::hex(0x8B0097);
|
|
261
|
+
pub const STANDARD_LIGHT_CAUTION: Rgba = Rgba::hex(0x9B4C00);
|
|
262
|
+
pub const STANDARD_LIGHT_WARNING: Rgba = Rgba::hex(0xA10000);
|
|
263
|
+
pub const STANDARD_LIGHT_DATA: Rgba = Rgba::hex(0x0B0906);
|
|
264
|
+
pub const STANDARD_LIGHT_LABEL: Rgba = Rgba::hex(0x504D47);
|
|
265
|
+
pub const STANDARD_LIGHT_MUTED: Rgba = Rgba::hex(0x8B8984);
|
|
266
|
+
pub const STANDARD_LIGHT_BORDER: Rgba = Rgba::hex(0xC7C4BD);
|
|
267
|
+
pub const STANDARD_LIGHT_S0: Rgba = Rgba::hex(0xF8F5EF);
|
|
268
|
+
pub const STANDARD_LIGHT_S1: Rgba = Rgba::hex(0xEBE7E0);
|
|
269
|
+
pub const STANDARD_LIGHT_S2: Rgba = Rgba::hex(0xDEDAD3);
|
|
270
|
+
pub const STANDARD_LIGHT_S3: Rgba = Rgba::hex(0xD1CDC7);
|
|
271
|
+
pub const STANDARD_LIGHT_DATA_DIM: Rgba = Rgba::hex(0xA19E99);
|
|
272
|
+
pub const STANDARD_LIGHT_BORDER_STRONG: Rgba = Rgba::hex(0xAEAAA4);
|
|
273
|
+
|
|
274
|
+
// ── canvas ink — unthemed, for marks over arbitrary imagery ─────────────
|
|
275
|
+
pub const INK_CANVAS_INK: Rgba = Rgba::hex(0xFFFFFF);
|
|
276
|
+
pub const INK_CANVAS_HALO: Rgba = Rgba::hex(0x000000);
|
|
277
|
+
pub const INK_VIVID_NOMINAL: Rgba = Rgba::hex(0x00DC27);
|
|
278
|
+
pub const INK_VIVID_ACTIVE: Rgba = Rgba::hex(0x00D6F8);
|
|
279
|
+
pub const INK_VIVID_TARGET: Rgba = Rgba::hex(0xF82DF9);
|
|
280
|
+
pub const INK_VIVID_CAUTION: Rgba = Rgba::hex(0xFFB300);
|
|
281
|
+
pub const INK_VIVID_WARNING: Rgba = Rgba::hex(0xFF002E);
|
|
282
|
+
|
|
283
|
+
// ── lengths (px) ────────────────────────────────────────────────────────
|
|
284
|
+
pub const BORDER_WIDTH_1: f32 = 1.0;
|
|
285
|
+
pub const BORDER_WIDTH_2: f32 = 2.0;
|
|
286
|
+
pub const BORDER_WIDTH_3: f32 = 3.0;
|
|
287
|
+
pub const BP_SM: f32 = 640.0;
|
|
288
|
+
pub const BP_MD: f32 = 768.0;
|
|
289
|
+
pub const BP_LG: f32 = 1024.0;
|
|
290
|
+
pub const BP_XL: f32 = 1280.0;
|
|
291
|
+
pub const BP_2XL: f32 = 1536.0;
|
|
292
|
+
pub const INK_CANVAS_HALO_WIDTH: f32 = 2.0;
|
|
293
|
+
pub const INK_CANVAS_HALO_WIDTH_LG: f32 = 3.0;
|
|
294
|
+
pub const RADIUS_2: f32 = 2.0;
|
|
295
|
+
pub const RADIUS_3: f32 = 3.0;
|
|
296
|
+
pub const RADIUS_4: f32 = 4.0;
|
|
297
|
+
pub const RADIUS_5: f32 = 5.0;
|
|
298
|
+
pub const RADIUS_8: f32 = 8.0;
|
|
299
|
+
pub const SIZE_TAP_MIN: f32 = 24.0;
|
|
300
|
+
pub const SIZE_TAP_COMFORTABLE: f32 = 44.0;
|
|
301
|
+
pub const SIZE_DOT_SM: f32 = 8.0;
|
|
302
|
+
pub const SIZE_DOT_MD: f32 = 10.0;
|
|
303
|
+
pub const SPACE_2: f32 = 2.0;
|
|
304
|
+
pub const SPACE_4: f32 = 4.0;
|
|
305
|
+
pub const SPACE_8: f32 = 8.0;
|
|
306
|
+
pub const SPACE_10: f32 = 10.0;
|
|
307
|
+
pub const SPACE_12: f32 = 12.0;
|
|
308
|
+
pub const SPACE_16: f32 = 16.0;
|
|
309
|
+
pub const SPACE_20: f32 = 20.0;
|
|
310
|
+
pub const SPACE_24: f32 = 24.0;
|
|
311
|
+
pub const SPACE_28: f32 = 28.0;
|
|
312
|
+
pub const SPACE_32: f32 = 32.0;
|
|
313
|
+
pub const SPACE_40: f32 = 40.0;
|
|
314
|
+
pub const SPACE_56: f32 = 56.0;
|
|
315
|
+
pub const SPACE_72: f32 = 72.0;
|
|
316
|
+
pub const SPACE_96: f32 = 96.0;
|
|
317
|
+
pub const FONT_SIZE_10: f32 = 10.0;
|
|
318
|
+
pub const FONT_SIZE_11: f32 = 11.0;
|
|
319
|
+
pub const FONT_SIZE_12: f32 = 12.0;
|
|
320
|
+
pub const FONT_SIZE_13: f32 = 13.0;
|
|
321
|
+
pub const FONT_SIZE_14: f32 = 14.0;
|
|
322
|
+
pub const FONT_SIZE_16: f32 = 16.0;
|
|
323
|
+
pub const FONT_SIZE_18: f32 = 18.0;
|
|
324
|
+
pub const FONT_SIZE_20: f32 = 20.0;
|
|
325
|
+
pub const FONT_SIZE_28: f32 = 28.0;
|
|
326
|
+
pub const FONT_SIZE_32: f32 = 32.0;
|
|
327
|
+
pub const FONT_SIZE_36: f32 = 36.0;
|
|
328
|
+
pub const FONT_SIZE_38: f32 = 38.0;
|
|
329
|
+
pub const FONT_SIZE_48: f32 = 48.0;
|
|
330
|
+
pub const FONT_SIZE_52: f32 = 52.0;
|
|
331
|
+
|
|
332
|
+
// ── durations (ms) ──────────────────────────────────────────────────────
|
|
333
|
+
pub const MOTION_DURATION_INSTANT_MS: f32 = 80.0;
|
|
334
|
+
pub const MOTION_DURATION_QUICK_MS: f32 = 140.0;
|
|
335
|
+
pub const MOTION_DURATION_BASE_MS: f32 = 200.0;
|
|
336
|
+
pub const MOTION_DURATION_DELIBERATE_MS: f32 = 300.0;
|
|
337
|
+
|
|
338
|
+
// ── whole numbers (z-index, font weight) ────────────────────────────────
|
|
339
|
+
pub const FONT_WEIGHT_LIGHT: i32 = 300;
|
|
340
|
+
pub const FONT_WEIGHT_REGULAR: i32 = 400;
|
|
341
|
+
pub const FONT_WEIGHT_MEDIUM: i32 = 500;
|
|
342
|
+
pub const FONT_WEIGHT_SEMIBOLD: i32 = 600;
|
|
343
|
+
pub const FONT_WEIGHT_BOLD: i32 = 700;
|
|
344
|
+
pub const FONT_LINEHEIGHT_NONE: i32 = 1;
|
|
345
|
+
pub const Z_SURFACE: i32 = 0;
|
|
346
|
+
pub const Z_RAISED: i32 = 100;
|
|
347
|
+
pub const Z_ANCHORED: i32 = 2000;
|
|
348
|
+
pub const Z_OVERLAY: i32 = 4000;
|
|
349
|
+
pub const Z_ALERT: i32 = 5000;
|
|
350
|
+
|
|
351
|
+
// ── ratios (opacity, line height) ───────────────────────────────────────
|
|
352
|
+
pub const INK_CANVAS_SCRIM: f32 = 0.28;
|
|
353
|
+
pub const OPACITY_DISABLED: f32 = 0.45;
|
|
354
|
+
pub const OPACITY_MUTED: f32 = 0.65;
|
|
355
|
+
pub const OPACITY_SCRIM: f32 = 0.62;
|
|
356
|
+
pub const FONT_LINEHEIGHT_TIGHT: f32 = 1.1;
|
|
357
|
+
pub const FONT_LINEHEIGHT_SNUG: f32 = 1.2;
|
|
358
|
+
pub const FONT_LINEHEIGHT_NORMAL: f32 = 1.4;
|
|
359
|
+
pub const FONT_LINEHEIGHT_RELAXED: f32 = 1.5;
|
|
360
|
+
pub const FONT_LINEHEIGHT_LOOSE: f32 = 1.7;
|
|
361
|
+
|
|
362
|
+
// ── CSS-authored values, verbatim ───────────────────────────────────────
|
|
363
|
+
//
|
|
364
|
+
// Shadows and font stacks are authored as CSS and are shipped unparsed. A
|
|
365
|
+
// Rust renderer cannot consume `0 4px 14px rgb(0 0 0 / 0.35)` directly — it is
|
|
366
|
+
// here so the values exist in one place and a webview or a parser can reach
|
|
367
|
+
// them, not as ready-made native input.
|
|
368
|
+
pub const SHADOW_1: &str = "0 1px 2px rgb(0 0 0 / 0.30)";
|
|
369
|
+
pub const SHADOW_2: &str = "0 4px 14px rgb(0 0 0 / 0.35)";
|
|
370
|
+
pub const SHADOW_3: &str = "0 12px 32px rgb(0 0 0 / 0.50)";
|
|
371
|
+
pub const MOTION_EASE_DECEL: &str = "cubic-bezier(0.2, 0.8, 0.2, 1)";
|
|
372
|
+
pub const MOTION_EASE_ACCEL: &str = "cubic-bezier(0.4, 0, 1, 1)";
|
|
373
|
+
pub const MOTION_EASE_STANDARD: &str = "cubic-bezier(0.2, 0.7, 0.3, 1)";
|
|
374
|
+
pub const MOTION_EASE_SPRING: &str = "cubic-bezier(0.2, 0.9, 0.3, 1.2)";
|
|
375
|
+
pub const FONT_FAMILY_SANS: &str = "'B612', sans-serif";
|
|
376
|
+
pub const FONT_FAMILY_MONO: &str = "'B612 Mono', monospace";
|
|
377
|
+
pub const FONT_TRACKING_TIGHT: &str = "0.02em";
|
|
378
|
+
pub const FONT_TRACKING_NORMAL: &str = "0.06em";
|
|
379
|
+
pub const FONT_TRACKING_LABEL: &str = "0.08em";
|
|
380
|
+
pub const FONT_TRACKING_WIDE: &str = "0.12em";
|
|
381
|
+
pub const FONT_TRACKING_CAPS: &str = "0.15em";
|
|
382
|
+
pub const FONT_TRACKING_WIDER: &str = "0.2em";
|
|
383
|
+
pub const FONT_TRACKING_WIDEST: &str = "0.3em";
|
|
@@ -101,6 +101,16 @@ $juno-bp-2xl: 1536px !default;
|
|
|
101
101
|
$juno-shadow-1: 0 1px 2px rgb(0 0 0 / 0.30) !default;
|
|
102
102
|
$juno-shadow-2: 0 4px 14px rgb(0 0 0 / 0.35) !default;
|
|
103
103
|
$juno-shadow-3: 0 12px 32px rgb(0 0 0 / 0.50) !default;
|
|
104
|
+
$juno-ink-canvas-ink: #FFFFFF !default;
|
|
105
|
+
$juno-ink-canvas-halo: #000000 !default;
|
|
106
|
+
$juno-ink-canvas-halo-width: 2px !default;
|
|
107
|
+
$juno-ink-canvas-halo-width-lg: 3px !default;
|
|
108
|
+
$juno-ink-canvas-scrim: 0.28 !default;
|
|
109
|
+
$juno-ink-vivid-nominal: oklch(76% 0.28 148) !default;
|
|
110
|
+
$juno-ink-vivid-active: oklch(76% 0.24 205) !default;
|
|
111
|
+
$juno-ink-vivid-target: oklch(70% 0.30 328) !default;
|
|
112
|
+
$juno-ink-vivid-caution: oklch(82% 0.20 82) !default;
|
|
113
|
+
$juno-ink-vivid-warning: oklch(68% 0.28 25) !default;
|
|
104
114
|
$juno-motion-duration-instant: 80ms !default;
|
|
105
115
|
$juno-motion-duration-quick: 140ms !default;
|
|
106
116
|
$juno-motion-duration-base: 200ms !default;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Canvas ink
|
|
2
|
+
|
|
3
|
+
Marks drawn over **arbitrary imagery** — a photo, a map, a video frame, a camera
|
|
4
|
+
feed. Anywhere the background is content rather than one of junoui's surfaces.
|
|
5
|
+
|
|
6
|
+
## Why this is not just a colour
|
|
7
|
+
|
|
8
|
+
junoui's contrast story assumes a controlled surface, `s0` through `s3`: every
|
|
9
|
+
role colour's ratio is computed against a known background. Over imagery there
|
|
10
|
+
is no known background. In one orthophoto a black shadow and a snowfield are
|
|
11
|
+
adjacent pixels, so **no single ink colour is legible and no contrast ratio can
|
|
12
|
+
be asserted about one**.
|
|
13
|
+
|
|
14
|
+
The answer is a pair that spans the luminance range. Over a light backing the
|
|
15
|
+
halo carries the contrast; over a dark one the ink does. Neither half works
|
|
16
|
+
alone, which is why one class applies both.
|
|
17
|
+
|
|
18
|
+
```html
|
|
19
|
+
<figcaption class="juno-canvas-ink">Sector 7 · 1.2 km</figcaption>
|
|
20
|
+
<figcaption class="juno-canvas-ink juno-canvas-ink--warning">Signal lost</figcaption>
|
|
21
|
+
|
|
22
|
+
<svg>
|
|
23
|
+
<path class="juno-canvas-ink__halo" d="…" />
|
|
24
|
+
<path class="juno-canvas-ink__stroke" d="…" />
|
|
25
|
+
</svg>
|
|
26
|
+
|
|
27
|
+
<div class="juno-canvas-scrim">…chrome floating over live content…</div>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
| Class / token | Effect |
|
|
31
|
+
| ------------------------------------- | ------------------------------------------------ |
|
|
32
|
+
| `.juno-canvas-ink` | Text: the ink colour plus a four-offset halo. |
|
|
33
|
+
| `.juno-canvas-ink--lg` | The wider halo, for `font.size.20` and up. |
|
|
34
|
+
| `.juno-canvas-ink--<role>` | A status hue at raised chroma, keeping the halo. |
|
|
35
|
+
| `.juno-canvas-ink__halo` / `__stroke` | Vector marks: two passes of the same path. |
|
|
36
|
+
| `.juno-canvas-scrim` | Backdrop for chrome floating over live content. |
|
|
37
|
+
| `ink.canvas.ink` / `ink.canvas.halo` | The pair. |
|
|
38
|
+
| `ink.canvas.halo-width` / `-lg` | Outline widths. |
|
|
39
|
+
| `ink.vivid.*` | Role hues at raised chroma. |
|
|
40
|
+
| `ink.canvas.scrim` | `0.28`, distinct from `opacity.scrim`. |
|
|
41
|
+
|
|
42
|
+
## Three things that are decided, not preferences
|
|
43
|
+
|
|
44
|
+
**The halo is pure black because the arithmetic says so.** The worst backing is
|
|
45
|
+
a mid grey, where both halves are weakest at once — not the extremes, which are
|
|
46
|
+
the easy cases. The sweep floor there is **4.61:1** against a 4.5:1 requirement.
|
|
47
|
+
A tasteful near-black with a blue cast (`#0A0C10`) measures 4.43:1 and fails on
|
|
48
|
+
three of 256 grey backings. The pair has roughly 0.1 of headroom, so neither
|
|
49
|
+
half can be nudged for looks. `test/canvas-ink.test.mjs` sweeps every grey.
|
|
50
|
+
|
|
51
|
+
**It is not themed.** A satellite image does not get lighter because the user
|
|
52
|
+
chose light mode. Theming the pair would make it track the app's surface, which
|
|
53
|
+
is precisely the background it is _not_ over. A test fails if `ink.canvas.ink`
|
|
54
|
+
or `ink.canvas.halo` ever appears under a mode or palette selector.
|
|
55
|
+
|
|
56
|
+
**The scrim is not `opacity.scrim`.** `0.62` is tuned to suppress a modal's
|
|
57
|
+
background. Here the background is the thing the chrome is annotating — greying
|
|
58
|
+
it out defeats the purpose. `0.28` is the value for content you still want read.
|
|
59
|
+
|
|
60
|
+
## Four offsets, not one blur
|
|
61
|
+
|
|
62
|
+
A blurred shadow fades at the glyph's corners, which is exactly where a thin
|
|
63
|
+
stroke needs the most help. Four hard offsets cost the same and hold the corners.
|
|
64
|
+
In SVG, `paint-order: stroke fill` gives a real stroke and is better; the class
|
|
65
|
+
sets both.
|
|
66
|
+
|
|
67
|
+
## Native
|
|
68
|
+
|
|
69
|
+
Every target carries these tokens — `INK_CANVAS_INK` / `INK_CANVAS_HALO` in
|
|
70
|
+
Rust, `inkCanvasInk` in Swift and Dart. The halo mechanism is yours to apply:
|
|
71
|
+
stroke the path twice, or draw text with an outline pass first.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Viewport orientation gizmo
|
|
2
|
+
|
|
3
|
+
The orientation widget any 3D or map viewport ships: a compass ring with
|
|
4
|
+
clickable snap targets, a secondary arc for a second angle (pitch, tilt,
|
|
5
|
+
elevation) inside a clamped range, and a centre target that resets the view.
|
|
6
|
+
|
|
7
|
+
CAD and BIM viewers, product configurators, virtual tours, model previews, floor
|
|
8
|
+
plans, any map with a tilt.
|
|
9
|
+
|
|
10
|
+
## A ring, not a cube
|
|
11
|
+
|
|
12
|
+
An Autodesk-style ViewCube is the wrong shape for anything with a **privileged
|
|
13
|
+
up-vector** — a map, a terrain, a site plan — because there is no meaningful
|
|
14
|
+
front, right or bottom face to click. A ring degrades to that case and
|
|
15
|
+
generalises to free orbit; a cube does not go the other way.
|
|
16
|
+
|
|
17
|
+
## Web
|
|
18
|
+
|
|
19
|
+
```html
|
|
20
|
+
<div
|
|
21
|
+
class="juno-gizmo"
|
|
22
|
+
role="group"
|
|
23
|
+
aria-label="View orientation"
|
|
24
|
+
style="--juno-gizmo-heading:45deg; --juno-gizmo-pitch:35deg;"
|
|
25
|
+
>
|
|
26
|
+
<p class="juno-gizmo__readout" aria-live="polite">
|
|
27
|
+
Facing north-east, 45 degrees. Tilted 35 degrees.
|
|
28
|
+
</p>
|
|
29
|
+
<div class="juno-gizmo__ring">
|
|
30
|
+
<span class="juno-gizmo__needle" aria-hidden="true"></span>
|
|
31
|
+
<button class="juno-gizmo__mark" style="--juno-gizmo-at:0deg" aria-label="Face north">N</button>
|
|
32
|
+
<!-- …seven more, every 45° -->
|
|
33
|
+
<button class="juno-gizmo__center" aria-label="Reset view to north, level">⌖</button>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="juno-gizmo__arc"><span class="juno-gizmo__arc-hand" aria-hidden="true"></span></div>
|
|
36
|
+
</div>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
| Class / prop | Effect |
|
|
40
|
+
| --------------------------------- | -------------------------------------------- |
|
|
41
|
+
| `.juno-gizmo` | Root. Holds the angles and the derived size. |
|
|
42
|
+
| `.juno-gizmo__readout` | The spoken state. `aria-live="polite"`. |
|
|
43
|
+
| `.juno-gizmo__ring` | The compass ring. |
|
|
44
|
+
| `.juno-gizmo__needle` | Points at `--juno-gizmo-heading`. |
|
|
45
|
+
| `.juno-gizmo__mark` | A snap target. A real `<button>`. |
|
|
46
|
+
| `.juno-gizmo__center` | Reset target. |
|
|
47
|
+
| `.juno-gizmo__arc` / `__arc-hand` | The second angle, clamped. |
|
|
48
|
+
| `--juno-gizmo-heading` / `-pitch` | **The app writes these.** |
|
|
49
|
+
| `--juno-gizmo-at` | One mark's own bearing. |
|
|
50
|
+
| `--juno-gizmo-pitch-min` / `-max` | The clamp (default `0deg`–`85deg`). |
|
|
51
|
+
| `--juno-gizmo-size` | Ring diameter. Derived — see below. |
|
|
52
|
+
|
|
53
|
+
**The app owns the camera.** junoui rotates the needle and the hand; it never
|
|
54
|
+
stores or changes an angle.
|
|
55
|
+
|
|
56
|
+
## The accessibility contract
|
|
57
|
+
|
|
58
|
+
This is why the component is upstream rather than in each app.
|
|
59
|
+
|
|
60
|
+
- **`role="group"`** with an accessible name on the root.
|
|
61
|
+
- **Snap targets are real `<button>`s.** Not a canvas hit test, not a `div` with
|
|
62
|
+
a click handler. A button is focusable, activates on Enter _and_ Space, is
|
|
63
|
+
announced as a control, and works in a screen reader's forms mode.
|
|
64
|
+
- **Every mark carries an accessible name**, because `"N"` is a letter, not a
|
|
65
|
+
name. `aria-label="Face north"`.
|
|
66
|
+
- **One focus stop.** Tab reaches the gizmo once; arrow keys move between marks
|
|
67
|
+
and **wrap**, Enter activates. Eight tab stops for eight compass points is what
|
|
68
|
+
apps ship and what makes the widget unusable by keyboard.
|
|
69
|
+
- **The bearing is announced in words.** A rotating needle announces nothing, and
|
|
70
|
+
`"37deg"` is a number the listener has to convert. The readout is a live region
|
|
71
|
+
saying _"Facing north-east, 37 degrees. Tilted 45 degrees."_
|
|
72
|
+
- **`aria-current="true"`** on the mark the camera is nearest — not a class, since
|
|
73
|
+
the app must say it for the screen reader anyway.
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
import { enhanceGizmo, orientationLabel, bearingLabel } from 'junoui/gizmo';
|
|
77
|
+
|
|
78
|
+
enhanceGizmo(el); // one focus stop, wrapping arrows
|
|
79
|
+
readout.textContent = orientationLabel(yaw, pitch);
|
|
80
|
+
bearingLabel(37); // → "north-east"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Stateless, like `junoui/tree`: it moves focus and lets the marks' own click
|
|
84
|
+
handlers fire. It never writes an angle.
|
|
85
|
+
|
|
86
|
+
## The diameter is derived, not chosen
|
|
87
|
+
|
|
88
|
+
`N` marks sit evenly around the rim, and each is inset from it by half a target,
|
|
89
|
+
so their centres lie on a circle of radius `d/2 − tap/2`. The straight-line
|
|
90
|
+
distance between two adjacent centres — the **chord**, not the arc — must be at
|
|
91
|
+
least one tap target:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
d ≥ tap · (1 / sin(π / N) + 1)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
which is what `--juno-gizmo-size` computes: `158.98px` at `N = 8` and a 44px
|
|
98
|
+
target.
|
|
99
|
+
|
|
100
|
+
**This derivation was wrong twice, and both errors were invisible in the
|
|
101
|
+
source.** Sizing off the _arc_ between centres (`N · tap / π`) gives 112.05px,
|
|
102
|
+
whose chord is 42.9px — every neighbouring pair overlapping by 1.1px. Correcting
|
|
103
|
+
to the chord but forgetting the inset gives 114.98px and measures 27.16px between
|
|
104
|
+
centres. Both were caught by measuring a laid-out ring, which is why
|
|
105
|
+
`test/visual/gizmo.spec.mjs` asserts the distance between every pair of marks
|
|
106
|
+
rather than trusting the formula. The tap floor **moves** — 24px on a
|
|
107
|
+
fine pointer, 44px on a coarse one — so a ring with a hard-coded diameter has
|
|
108
|
+
eight overlapping targets on a phone. Setting `--juno-gizmo-size` larger is fine;
|
|
109
|
+
`max()` stops it going below what the marks need.
|
|
110
|
+
|
|
111
|
+
## Motion
|
|
112
|
+
|
|
113
|
+
Snap transitions run on `--juno-motion-scale`, so `prefers-reduced-motion`
|
|
114
|
+
collapses them through the base layer without a component-local media query.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Colour swatch & palette
|
|
2
|
+
|
|
3
|
+
Showing a user-chosen colour, and letting someone pick one. Diagrams, calendars,
|
|
4
|
+
tag and label systems, chart series colours, annotation tools, theming UIs,
|
|
5
|
+
kanban boards.
|
|
6
|
+
|
|
7
|
+
## The hard part is not the square
|
|
8
|
+
|
|
9
|
+
A swatch shows an **arbitrary** colour, so every piece of chrome on it — its
|
|
10
|
+
border, its focus ring, its checked indicator — has to stay visible against a
|
|
11
|
+
colour junoui has never seen. A single hairline fails at one end of the range: a
|
|
12
|
+
dark border vanishes on near-black, a light one vanishes on near-white, and the
|
|
13
|
+
swatch that loses its border is the one that has merged with the panel behind it.
|
|
14
|
+
|
|
15
|
+
So the border is a **pair** of hairlines, one dark (inset) and one light
|
|
16
|
+
(outset). Whatever the swatch, one of them contrasts; the other is the one you
|
|
17
|
+
do not notice. `test/swatch.test.mjs` sweeps the swatch colour and asserts one
|
|
18
|
+
ring always clears 3:1, rather than asserting a border value.
|
|
19
|
+
|
|
20
|
+
**Focus rings sit outside the swatch, with an offset**, so their contrast is
|
|
21
|
+
against the panel — a known surface — rather than against a hue junoui cannot
|
|
22
|
+
predict. A ring drawn _on_ the swatch has the same unsolvable problem, and a
|
|
23
|
+
thicker ring does not fix a hue collision.
|
|
24
|
+
|
|
25
|
+
## Web
|
|
26
|
+
|
|
27
|
+
```html
|
|
28
|
+
<span
|
|
29
|
+
class="juno-swatch"
|
|
30
|
+
style="--juno-swatch-color:#C41E3A"
|
|
31
|
+
role="img"
|
|
32
|
+
aria-label="Crimson"
|
|
33
|
+
></span>
|
|
34
|
+
|
|
35
|
+
<button
|
|
36
|
+
class="juno-swatch juno-swatch--button"
|
|
37
|
+
style="--juno-swatch-color:#1F6FEB"
|
|
38
|
+
aria-label="Annotation colour: Azure"
|
|
39
|
+
popovertarget="palette"
|
|
40
|
+
></button>
|
|
41
|
+
|
|
42
|
+
<div class="juno-popover" popover id="palette">
|
|
43
|
+
<div class="juno-palette" role="listbox" aria-label="Annotation colour">
|
|
44
|
+
<button
|
|
45
|
+
class="juno-palette__option"
|
|
46
|
+
role="option"
|
|
47
|
+
aria-selected="true"
|
|
48
|
+
style="--juno-swatch-color:#1F6FEB"
|
|
49
|
+
aria-label="Azure"
|
|
50
|
+
>
|
|
51
|
+
<svg class="juno-icon juno-palette__check" aria-hidden="true">
|
|
52
|
+
<use href="…#juno-i-check" />
|
|
53
|
+
</svg>
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
| Class / prop | Effect |
|
|
60
|
+
| ----------------------------- | ------------------------------------------------------- |
|
|
61
|
+
| `.juno-swatch` | The square. Two-tone ring, sized off the control scale. |
|
|
62
|
+
| `.juno-swatch--circle` | Round rather than square. |
|
|
63
|
+
| `.juno-swatch--sm` | `space.16` — inline with body text. |
|
|
64
|
+
| `.juno-swatch--lg` | `size.tap.comfortable` — a primary trigger. |
|
|
65
|
+
| `.juno-swatch--button` | Swatch used as a trigger. |
|
|
66
|
+
| `.juno-swatch--none` | "No colour" — a slash, not a grey. |
|
|
67
|
+
| `.juno-palette__option--none` | The same, as a choice in the grid. |
|
|
68
|
+
| `.juno-palette` | The grid inside a `.juno-popover`. |
|
|
69
|
+
| `.juno-palette__option` | One choice. Selected shows a glyph **and** a ring. |
|
|
70
|
+
| `--juno-swatch-color` | **The app writes this.** |
|
|
71
|
+
| `--juno-swatch-size` | Defaults to `size.tap.min`. |
|
|
72
|
+
| `--juno-palette-columns` | Grid width (default `6`). |
|
|
73
|
+
|
|
74
|
+
The app owns the colour list and which one is chosen.
|
|
75
|
+
|
|
76
|
+
## Colour is never the only signal
|
|
77
|
+
|
|
78
|
+
junoui's standing rule, and a bare swatch is exactly what violates it.
|
|
79
|
+
|
|
80
|
+
- **Every swatch carries an accessible name.** `aria-label="Crimson"`, not a bare
|
|
81
|
+
square. A decorative swatch beside its own visible label can be `aria-hidden`,
|
|
82
|
+
but a swatch that _is_ the information needs the name.
|
|
83
|
+
- **The checked state is a glyph**, not a hue. "The chosen one looks slightly
|
|
84
|
+
different" is invisible to anyone who cannot separate the two hues — and to
|
|
85
|
+
anyone reading a screenshot. The selected option also grows a ring in the
|
|
86
|
+
active role, so there are two non-colour cues.
|
|
87
|
+
- **The check itself sits on an arbitrary colour**, so it gets a light glyph with
|
|
88
|
+
a dark halo — the canvas-ink pair at glyph scale.
|
|
89
|
+
- **"No colour" is a slash, not a grey.** A consumer without this paints unset as
|
|
90
|
+
a mid grey and the user cannot tell _grey_ from _none_, which is a different
|
|
91
|
+
thing to know.
|
|
92
|
+
|
|
93
|
+
`role="listbox"` + `role="option"` + `aria-selected` is the contract for a
|
|
94
|
+
single-choice palette. The popover, its trigger and the open/close behaviour are
|
|
95
|
+
`.juno-popover`'s — this is the grid that goes inside it.
|