@junoput01/junoui 0.7.0 → 0.8.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.
@@ -95,6 +95,14 @@ class JunoTokens {
95
95
  static const Color standardLightDataDim = Color(0xFFA19E99);
96
96
  static const Color standardLightBorderStrong = Color(0xFFAEAAA4);
97
97
 
98
+ static const Color inkCanvasInk = Color(0xFFFFFFFF);
99
+ static const Color inkCanvasHalo = Color(0xFF000000);
100
+ static const Color inkVividNominal = Color(0xFF00DC27);
101
+ static const Color inkVividActive = Color(0xFF00D6F8);
102
+ static const Color inkVividTarget = Color(0xFFF82DF9);
103
+ static const Color inkVividCaution = Color(0xFFFFB300);
104
+ static const Color inkVividWarning = Color(0xFFFF002E);
105
+
98
106
  static const double borderWidth1 = 1;
99
107
  static const double borderWidth2 = 2;
100
108
  static const double borderWidth3 = 3;
@@ -103,6 +111,8 @@ class JunoTokens {
103
111
  static const double bpLg = 1024;
104
112
  static const double bpXl = 1280;
105
113
  static const double bp2xl = 1536;
114
+ static const double inkCanvasHaloWidth = 2;
115
+ static const double inkCanvasHaloWidthLg = 3;
106
116
  static const double radius2 = 2;
107
117
  static const double radius3 = 3;
108
118
  static const double radius4 = 4;
@@ -101,6 +101,14 @@ public enum JunoTokens {
101
101
  public static let standardLightDataDim = JunoColor.hex(0xA19E99)
102
102
  public static let standardLightBorderStrong = JunoColor.hex(0xAEAAA4)
103
103
 
104
+ public static let inkCanvasInk = JunoColor.hex(0xFFFFFF)
105
+ public static let inkCanvasHalo = JunoColor.hex(0x000000)
106
+ public static let inkVividNominal = JunoColor.hex(0x00DC27)
107
+ public static let inkVividActive = JunoColor.hex(0x00D6F8)
108
+ public static let inkVividTarget = JunoColor.hex(0xF82DF9)
109
+ public static let inkVividCaution = JunoColor.hex(0xFFB300)
110
+ public static let inkVividWarning = JunoColor.hex(0xFF002E)
111
+
104
112
  public static let borderWidth1: CGFloat = 1
105
113
  public static let borderWidth2: CGFloat = 2
106
114
  public static let borderWidth3: CGFloat = 3
@@ -109,6 +117,8 @@ public enum JunoTokens {
109
117
  public static let bpLg: CGFloat = 1024
110
118
  public static let bpXl: CGFloat = 1280
111
119
  public static let bp2xl: CGFloat = 1536
120
+ public static let inkCanvasHaloWidth: CGFloat = 2
121
+ public static let inkCanvasHaloWidthLg: CGFloat = 3
112
122
  public static let radius2: CGFloat = 2
113
123
  public static let radius3: CGFloat = 3
114
124
  public static let radius4: CGFloat = 4
package/dist/js/tokens.js CHANGED
@@ -130,6 +130,22 @@ export const CORE = {
130
130
  "2": "0 4px 14px rgb(0 0 0 / 0.35)",
131
131
  "3": "0 12px 32px rgb(0 0 0 / 0.50)"
132
132
  },
133
+ "ink": {
134
+ "canvas": {
135
+ "ink": "#FFFFFF",
136
+ "halo": "#000000",
137
+ "halo-width": "2px",
138
+ "halo-width-lg": "3px",
139
+ "scrim": 0.28
140
+ },
141
+ "vivid": {
142
+ "nominal": "oklch(76% 0.28 148)",
143
+ "active": "oklch(76% 0.24 205)",
144
+ "target": "oklch(70% 0.30 328)",
145
+ "caution": "oklch(82% 0.20 82)",
146
+ "warning": "oklch(68% 0.28 25)"
147
+ }
148
+ },
133
149
  "motion": {
134
150
  "duration": {
135
151
  "instant": "80ms",
@@ -532,6 +532,57 @@
532
532
  "$description": "e3 — modal, drawer (overlay layer)."
533
533
  }
534
534
  },
535
+ "ink": {
536
+ "canvas": {
537
+ "ink": {
538
+ "$value": "#FFFFFF",
539
+ "$type": "color",
540
+ "$description": "The mark itself: glyphs, measurement lines, selection outlines. Near-white rather than a role hue because it must survive a saturated backdrop, and paired with `halo` it is the high-luminance half of the pair."
541
+ },
542
+ "halo": {
543
+ "$value": "#000000",
544
+ "$type": "color",
545
+ "$description": "The outline drawn behind and around `ink` (text-shadow, paint-order stroke, or a widened stroke pass). Pure black, and that is FORCED rather than chosen: with a white ink the worst backing is a mid grey where both halves are at their weakest, and the sweep floor there is 4.61:1 at #000000 against a 4.5:1 requirement. A tasteful near-black with a blue cast (#0A0C10) drops it to 4.43:1 and fails. The pair has ~0.1 of headroom, which is why both halves sit at the extremes of the luminance range and neither can be nudged for looks."
546
+ },
547
+ "halo-width": {
548
+ "$value": "2px",
549
+ "$type": "dimension",
550
+ "$description": "Outline width at body sizes. Two device pixels is the smallest that survives resampling on a moving map; below that the halo thins to nothing at the glyph's corners."
551
+ },
552
+ "halo-width-lg": {
553
+ "$value": "3px",
554
+ "$type": "dimension",
555
+ "$description": "Outline width for display sizes (font.size.20 and up), where a 2px halo reads as a hairline against the stroke weight."
556
+ },
557
+ "scrim": {
558
+ "$value": 0.28,
559
+ "$type": "number",
560
+ "$description": "Backdrop opacity for chrome floating OVER live content — a translucent bar on a map or a video. Deliberately distinct from opacity.scrim (0.62), which is tuned to suppress a full-screen modal's background and is far too heavy here: it would grey out the very content the chrome is annotating."
561
+ }
562
+ },
563
+ "vivid": {
564
+ "nominal": {
565
+ "$value": "oklch(76% 0.28 148)",
566
+ "$type": "color"
567
+ },
568
+ "active": {
569
+ "$value": "oklch(76% 0.24 205)",
570
+ "$type": "color"
571
+ },
572
+ "target": {
573
+ "$value": "oklch(70% 0.30 328)",
574
+ "$type": "color"
575
+ },
576
+ "caution": {
577
+ "$value": "oklch(82% 0.20 82)",
578
+ "$type": "color"
579
+ },
580
+ "warning": {
581
+ "$value": "oklch(68% 0.28 25)",
582
+ "$type": "color"
583
+ }
584
+ }
585
+ },
535
586
  "motion": {
536
587
  "duration": {
537
588
  "instant": {
@@ -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.