@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/dist/android/dimens.xml +2 -0
  3. package/dist/classes.json +175 -1
  4. package/dist/css/juno-tokens.css +10 -0
  5. package/dist/css/juno.css +861 -39
  6. package/dist/flutter/juno_tokens.dart +10 -0
  7. package/dist/icons/inline.js +1 -1
  8. package/dist/icons/juno-icons.svg +14 -0
  9. package/dist/ios/JunoTokens.swift +10 -0
  10. package/dist/js/tokens.js +16 -0
  11. package/dist/json/tokens.json +51 -0
  12. package/dist/rust/juno_tokens.rs +383 -0
  13. package/dist/scss/_juno-tokens.scss +10 -0
  14. package/docs/components/canvas-ink.md +71 -0
  15. package/docs/components/gizmo.md +114 -0
  16. package/docs/components/swatch.md +95 -0
  17. package/docs/components/tree.md +112 -0
  18. package/docs/conformance-kit.md +26 -14
  19. package/docs/icon-subsetting.md +16 -1
  20. package/docs/native.md +38 -1
  21. package/docs/tokens-reference.md +15 -0
  22. package/package.json +4 -1
  23. package/src/css/base.css +10 -39
  24. package/src/css/components/canvas-ink.css +97 -0
  25. package/src/css/components/gizmo.css +238 -0
  26. package/src/css/components/swatch.css +187 -0
  27. package/src/css/components/tree.css +259 -0
  28. package/src/css/touch-surfaces.mjs +95 -0
  29. package/src/icons/compass.svg +1 -0
  30. package/src/icons/crosshair-simple.svg +1 -0
  31. package/src/icons/crosshair.svg +1 -0
  32. package/src/icons/cube.svg +1 -0
  33. package/src/icons/globe.svg +1 -0
  34. package/src/icons/map-pin.svg +1 -0
  35. package/src/icons/map-trifold.svg +1 -0
  36. package/src/icons/mountains.svg +1 -0
  37. package/src/icons/path.svg +1 -0
  38. package/src/icons/polygon.svg +1 -0
  39. package/src/icons/ruler.svg +1 -0
  40. package/src/icons/scissors.svg +1 -0
  41. package/src/icons/selection.svg +1 -0
  42. package/src/icons/stack.svg +1 -0
  43. package/tools/gizmo.mjs +144 -0
  44. 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.