@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,112 @@
1
+ # Tree / outliner
2
+
3
+ Nested rows at arbitrary depth with disclosure, selection and a reorder handle.
4
+ Layer stacks, file browsers, settings trees, org charts, comment threads, nested
5
+ navigation. `.juno-list` is flat and `.juno-accordion` is single-level; this
6
+ nests.
7
+
8
+ ## Web
9
+
10
+ ```html
11
+ <ul class="juno-tree" role="tree" aria-label="Scene">
12
+ <li
13
+ class="juno-tree__item"
14
+ role="treeitem"
15
+ aria-level="1"
16
+ aria-expanded="true"
17
+ aria-selected="false"
18
+ >
19
+ <div class="juno-tree__row">
20
+ <button class="juno-tree__caret" tabindex="-1" aria-hidden="true"></button>
21
+ <svg class="juno-icon juno-tree__icon" aria-hidden="true"><use href="…#juno-i-stack" /></svg>
22
+ <span class="juno-tree__label">Basemap</span>
23
+ <span class="juno-tree__count">3</span>
24
+ <span class="juno-tree__trail"><span class="juno-badge juno--nominal">ON</span></span>
25
+ <button class="juno-tree__handle" aria-label="Reorder Basemap"></button>
26
+ </div>
27
+ <ul class="juno-tree__group" role="group">
28
+ <li class="juno-tree__item" role="treeitem" aria-level="2" aria-selected="true">…</li>
29
+ </ul>
30
+ </li>
31
+ </ul>
32
+ ```
33
+
34
+ | Class / prop | Effect |
35
+ | -------------------- | ------------------------------------------------------------------------ |
36
+ | `.juno-tree` | The `role="tree"` root. Sets `--juno-tree-indent`. |
37
+ | `.juno-tree__item` | One `role="treeitem"`. Carries `aria-expanded` / `-level` / `-selected`. |
38
+ | `.juno-tree__group` | A nested `role="group"`. One indent step; depth is structural. |
39
+ | `.juno-tree__row` | The focusable line. Holds the tap floor. |
40
+ | `.juno-tree__caret` | Disclosure triangle; rotates on `aria-expanded="true"`. |
41
+ | `.juno-tree__count` | Count badge slot on a group row. |
42
+ | `.juno-tree__trail` | Trailing-control slot, as on `.juno-list__row`. |
43
+ | `.juno-tree__handle` | Reorder affordance and its hit area. |
44
+ | `--juno-tree-indent` | One indent step (default `space.16`). |
45
+
46
+ Expansion and collapse are `aria-expanded` on the item — the app owns it, as with
47
+ `aria-pressed` elsewhere in junoui. No JS is needed for the visuals.
48
+
49
+ ## The ARIA contract
50
+
51
+ This is the half apps get wrong, so it is stated rather than implied.
52
+
53
+ - **`role="tree"`** on the root, with an accessible name (`aria-label` or
54
+ `aria-labelledby`). **`role="group"`** on every nested list, **`role="treeitem"`**
55
+ on every item.
56
+ - **`aria-level`** on every item, 1-based. Required: the nesting is visual, and
57
+ a screen reader does not infer depth from indentation.
58
+ - **`aria-expanded`** on branches only. **Its absence is what makes an item a
59
+ leaf** — do not put `aria-expanded="false"` on a childless row, or it is
60
+ announced as a collapsed branch that never opens.
61
+ - **`aria-selected`** for what the next action applies to. Distinct from
62
+ `aria-current` (which node you are _on_) and from `:hover`. A layer stack has
63
+ all three at once, which is why they paint differently.
64
+ - **Roving tabindex**: exactly one row has `tabindex="0"`, the rest `-1`. One Tab
65
+ stop for the whole tree, not one per node.
66
+ - **Multi-select** is `aria-multiselectable="true"` on the root plus
67
+ `aria-selected` on each item.
68
+
69
+ ### Keyboard
70
+
71
+ | Key | Action |
72
+ | ------------ | --------------------------------------------------------------- |
73
+ | ↓ / ↑ | Next / previous **visible** item, across levels |
74
+ | → | Closed branch: expand. Open branch: first child. Leaf: nothing. |
75
+ | ← | Open branch: collapse. Otherwise: parent. |
76
+ | Home / End | First / last visible item |
77
+ | Enter, Space | Select |
78
+ | `*` | Expand every sibling at this level |
79
+
80
+ A tree without this is a list of buttons wearing tree roles. Because it is
81
+ behaviour, no stylesheet can ship it — so junoui ships a stateless enhancer:
82
+
83
+ ```js
84
+ import { enhanceTree } from 'junoui/tree';
85
+ const stop = enhanceTree(document.querySelector('.juno-tree'));
86
+ ```
87
+
88
+ It stores nothing: expansion and selection live on the DOM and belong to you. It
89
+ moves focus and dispatches `juno-tree-toggle` / `juno-tree-select` (bubbling,
90
+ cancelable, `detail.item`) — it does **not** expand, collapse, select or reorder,
91
+ because in a real outliner expanding a node may need to load it.
92
+
93
+ ## Touch
94
+
95
+ The row holds `--juno-size-tap-min`, which becomes the 44px comfortable target on
96
+ a coarse pointer. The caret and the handle **paint** small so a dense tree stays
97
+ dense, and grow only their **hit area** with a transparent overlay — a 44px
98
+ painted caret would swallow the row it sits in.
99
+
100
+ **The reorder handle is explicit, and that is not a style choice.** Long-press-drag
101
+ is the obvious gesture and the wrong one: a tree sitting on or beside a pan/zoom
102
+ surface has to let the pan win, and a gesture that means "reorder" here and "pan"
103
+ one pixel to the left is a coin flip. The handle carries `touch-action: none` on
104
+ itself alone, so dragging it never scrolls while the rest of the row still pans.
105
+
106
+ ## What junoui does not do
107
+
108
+ Reorder **logic** — the drop calculation, the model mutation, autoscroll. junoui
109
+ ships the affordance, its hit area, and the drop-target styling
110
+ (`data-juno-drop="before|after|into"`, plus `data-juno-dragging`). A drop lands
111
+ _between_ rows by default: the edge line says "between", a filled highlight says
112
+ "into this one", and those are different operations in a tree.
@@ -121,9 +121,21 @@ file that deliberately names wrong classes, and 3 consumer-owned names.
121
121
  Generated, the typo class of defect cannot exist and the divergence becomes
122
122
  a visible decision.
123
123
 
124
- **Open question for review:** the two lists should probably become one set with
125
- a per-property opt-out, but "which primitives want `manipulation`" and "which
126
- want the highlight killed" may genuinely differ. Needs a decision, not a merge.
124
+ **Shipped**, and the question is answered: **one set**, decided on the rules'
125
+ own rationales rather than merged for tidiness. The tap-highlight rule exists so
126
+ a UA square "never flashes past a rounded control on tap", and every name the
127
+ shorter list omitted is a rounded tappable — `.juno-chip` and
128
+ `.juno-pillbar__overflow` are 999px pills, `.juno-seg__opt` and
129
+ `.juno-toggle-btn` carry `radius-3`. The omission had no stated reason and the
130
+ rationale covers them, so it was an omission, not a decision.
131
+
132
+ `src/css/touch-surfaces.mjs` declares the set; `bundle-css.mjs` emits both
133
+ rules. `touch-action` stays out of the coarse block (a hybrid device reports a
134
+ fine primary pointer while still taking touch input); the highlight stays in it.
135
+
136
+ The guard checks the declared set against the **component sources**, not the
137
+ manifest — the manifest is built from the bundle, and the bundle now contains
138
+ these lists, so a misspelled member would vouch for itself there.
127
139
 
128
140
  ## C. Pointer-first responsiveness
129
141
 
@@ -225,17 +237,17 @@ the guard that would have failed did not exist on that branch (20260826-039).
225
237
 
226
238
  ## Sequencing
227
239
 
228
- | Slice | Contents | Depends on |
229
- | ----- | -------------------------------------------- | ---------- |
230
- | ~~1~~ | ~~A (manifest + helper)~~ — **shipped** | — |
231
- | 2 | B2 (generate the `:where()` lists) | 1 |
232
- | 3 | C (one pointer-first mechanism) | — |
233
- | 4 | D (buckets + floating chrome owns its inset) | 3 |
234
- | 5 | E (pillbar budget) | — |
235
- | 6 | F (doctor) | 1, 3 |
236
- | 7 | G (checklist, reduced to what F checks) | 6 |
237
-
238
- Slice 1, B1 and E-for-dock are done. 024–027 are absorbed and closed.
240
+ | Slice | Contents | Depends on |
241
+ | ----- | ---------------------------------------------------- | ---------- |
242
+ | ~~1~~ | ~~A (manifest + helper)~~ — **shipped** | — |
243
+ | ~~2~~ | ~~B2 (generate the `:where()` lists)~~ — **shipped** | 1 |
244
+ | 3 | C (one pointer-first mechanism) | — |
245
+ | 4 | D (buckets + floating chrome owns its inset) | 3 |
246
+ | 5 | E (pillbar budget) | — |
247
+ | 6 | F (doctor) | 1, 3 |
248
+ | 7 | G (checklist, reduced to what F checks) | 6 |
249
+
250
+ Slices 1 and 2, B1 and E-for-dock are done. 024–027 are absorbed and closed.
239
251
 
240
252
  **Review asks:** the three open questions above (A's public subset, B's two
241
253
  lists, C's condition), and whether slice 6 is worth its cost before slice 1
@@ -34,6 +34,21 @@ const svg = subsetSprite(sprite, ['gear', 'x', 'squares-four']);
34
34
  decoration.
35
35
  - Node-only tooling on purpose: subsetting at runtime would defeat the point.
36
36
 
37
+ ## What is in the set
38
+
39
+ Phosphor Icons (bold), MIT — one family, one licence, vendored under
40
+ `src/icons/` with the licence beside them. Media, files, system and status,
41
+ plus a spatial group for map, GIS, CAD and 3D consumers: `crosshair`,
42
+ `crosshair-simple`, `ruler`, `polygon`, `path`, `map-pin`, `map-trifold`,
43
+ `stack`, `globe`, `mountains`, `cube`, `compass`, `scissors`, `selection`.
44
+
45
+ **Adding an icon means adding a Phosphor bold glyph.** Not "an icon that looks
46
+ similar" — the set's value is that it is one family, and a glyph from elsewhere
47
+ arrives on a different canvas, at a different optical weight, often carrying its
48
+ own `fill`. `test/icons-family.test.mjs` enforces the contract: one `0 0 256 256`
49
+ canvas, colour inherited via `currentColor`, no `<style>`, no classes, no
50
+ external references, and the sprite and `src/icons/` agreeing in both directions.
51
+
37
52
  ## Vite
38
53
 
39
54
  ```js
@@ -76,7 +91,7 @@ installSprite(sprite);
76
91
  | Export | Carries | Use |
77
92
  | ---------------------- | ------------------------- | --------------------------------------------- |
78
93
  | `junoui/icons/install` | ~1 kB, no icons | You subset. Pass your own sprite. |
79
- | `junoui/icons/inline` | the full 66-symbol sprite | You don't subset. Import for the side effect. |
94
+ | `junoui/icons/inline` | the full 80-symbol sprite | You don't subset. Import for the side effect. |
80
95
 
81
96
  - **Both are id-guarded on the same `juno-icon-sprite` holder**, so importing
82
97
  the full module alongside a subset does not produce two hidden holders
package/docs/native.md CHANGED
@@ -1,4 +1,4 @@
1
- # Native mobile (Android / iOS)
1
+ # Native (Android / iOS / Rust)
2
2
 
3
3
  Native platforms can't parse `oklch()`, so colors are pre-converted to sRGB hex
4
4
  during the build. Values match the web rendering.
@@ -43,6 +43,43 @@ let body: CGFloat = JunoTokens.fontSize14
43
43
  - Colors are `UIColor` constants named `<palette><Mode><Role>` (camelCase).
44
44
  - Dimensions are `CGFloat` constants.
45
45
 
46
+ ## Rust
47
+
48
+ Any native Rust stack — egui, iced, Slint, Bevy, Dioxus desktop, Tauri's Rust
49
+ side. Vendor `dist/rust/juno_tokens.rs` into your crate, or add junoui as a
50
+ build dependency and copy it in `build.rs`.
51
+
52
+ ```rust
53
+ include!(concat!(env!("OUT_DIR"), "/juno_tokens.rs")); // or `mod juno_tokens;`
54
+
55
+ let bg = STANDARD_DARK.s0.to_f32_array(); // a whole theme, picked at runtime
56
+ let accent = STANDARD_DARK_ACTIVE; // or one role, flat
57
+ let gap = SPACE_16; // f32 px
58
+ let fade = MOTION_DURATION_BASE_MS; // f32 milliseconds
59
+ ```
60
+
61
+ | Emitted as | From |
62
+ | -------------------------------------------------- | ---------------------------------------------------- |
63
+ | `Rgba` (`hex()`, `to_f32_array()`, `with_alpha()`) | every color token |
64
+ | `Palette` + one const per palette/mode | the same colors, grouped |
65
+ | `f32` | `px` lengths |
66
+ | `f32`, suffixed `_MS` | `ms` durations |
67
+ | `i32` | whole numbers (z-index, font weight) |
68
+ | `f32` | ratios (opacity, line height) |
69
+ | `&str` | CSS-authored values, verbatim (shadows, font stacks) |
70
+
71
+ **`to_f32_array()` is sRGB-encoded, not linear.** If your pipeline wants linear
72
+ (wgpu with a non-sRGB surface format), convert at your boundary — junoui cannot
73
+ know which surface you created.
74
+
75
+ **Shadows and font stacks are shipped unparsed.** A Rust renderer cannot consume
76
+ `0 4px 14px rgb(0 0 0 / 0.35)` directly; they are here so the values live in one
77
+ place, not as ready-made native input.
78
+
79
+ **Do not transcribe these values into your own crate.** That copy is exactly
80
+ what this target exists to stop — it goes stale on the first patch release and
81
+ no lint catches it.
82
+
46
83
  ## Keeping in sync
47
84
 
48
85
  Re-run `npm run build` in junoui and re-copy the files (or script the copy in your
@@ -113,6 +113,21 @@ Color encodes status, never decoration. Each role has exactly one meaning.
113
113
  | `shadow.2` | `0 4px 14px rgb(0 0 0 / 0.35)` | `var(--juno-shadow-2)` | `JunoTokens.shadow2` |
114
114
  | `shadow.3` | `0 12px 32px rgb(0 0 0 / 0.50)` | `var(--juno-shadow-3)` | `JunoTokens.shadow3` |
115
115
 
116
+ ### ink
117
+
118
+ | Token | Value | CSS variable | Flutter / iOS |
119
+ |---|---|---|---|
120
+ | `ink.canvas.ink` | `#FFFFFF` | `var(--juno-ink-canvas-ink)` | `JunoTokens.inkCanvasInk` |
121
+ | `ink.canvas.halo` | `#000000` | `var(--juno-ink-canvas-halo)` | `JunoTokens.inkCanvasHalo` |
122
+ | `ink.canvas.halo-width` | `2px` | `var(--juno-ink-canvas-halo-width)` | `JunoTokens.inkCanvasHalo-width` |
123
+ | `ink.canvas.halo-width-lg` | `3px` | `var(--juno-ink-canvas-halo-width-lg)` | `JunoTokens.inkCanvasHalo-width-lg` |
124
+ | `ink.canvas.scrim` | `0.28` | `var(--juno-ink-canvas-scrim)` | `JunoTokens.inkCanvasScrim` |
125
+ | `ink.vivid.nominal` | `oklch(76% 0.28 148)` | `var(--juno-ink-vivid-nominal)` | `JunoTokens.inkVividNominal` |
126
+ | `ink.vivid.active` | `oklch(76% 0.24 205)` | `var(--juno-ink-vivid-active)` | `JunoTokens.inkVividActive` |
127
+ | `ink.vivid.target` | `oklch(70% 0.30 328)` | `var(--juno-ink-vivid-target)` | `JunoTokens.inkVividTarget` |
128
+ | `ink.vivid.caution` | `oklch(82% 0.20 82)` | `var(--juno-ink-vivid-caution)` | `JunoTokens.inkVividCaution` |
129
+ | `ink.vivid.warning` | `oklch(68% 0.28 25)` | `var(--juno-ink-vivid-warning)` | `JunoTokens.inkVividWarning` |
130
+
116
131
  ### Motion
117
132
 
118
133
  | Token | Value | CSS variable | Flutter / iOS |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junoput01/junoui",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "junoui — a token-driven design system. Color carries semantic meaning, never decoration: every hue has one assigned role (NOMINAL / ACTIVE / TARGET / CAUTION / WARNING). Ships multi-platform tokens (CSS, SCSS, JS/TS, JSON, Android, iOS, Flutter) plus a framework-agnostic CSS component layer.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -48,8 +48,11 @@
48
48
  "./android/dimens": "./dist/android/dimens.xml",
49
49
  "./ios": "./dist/ios/JunoTokens.swift",
50
50
  "./flutter": "./dist/flutter/juno_tokens.dart",
51
+ "./rust": "./dist/rust/juno_tokens.rs",
51
52
  "./icons": "./dist/icons/juno-icons.svg",
52
53
  "./subset": "./tools/subset-sprite.mjs",
54
+ "./tree": "./tools/tree.mjs",
55
+ "./gizmo": "./tools/gizmo.mjs",
53
56
  "./testing": "./tools/testing.mjs",
54
57
  "./classes.json": "./dist/classes.json",
55
58
  "./icons/inline": "./dist/icons/inline.js",
package/src/css/base.css CHANGED
@@ -189,47 +189,18 @@ code, kbd, samp, pre { font-family: var(--juno-font-family-mono); }
189
189
  `.juno-input` font-size later in the bundle. It did, silently, until
190
190
  20260815-006's coarse-pointer project measured it. */
191
191
 
192
- /* Kill the UA tap-highlight square on the interactive surfaces so it never
193
- flashes past a rounded control on tap. Consumers were adding this by hand
194
- per component; make it a first-class touch default. See 20260802-020. */
195
- :where(
196
- .juno-btn,
197
- .juno-dock__item,
198
- .juno-pillbar__item,
199
- .juno-tabs__tab,
200
- .juno-list__row,
201
- .juno-menu__item
202
- ) {
203
- -webkit-tap-highlight-color: transparent;
204
- }
192
+ /* The tap-highlight default that used to live here is GENERATED now, from
193
+ src/css/touch-surfaces.mjs, together with the touch-action default that
194
+ shares its member list. Two hand-maintained lists had drifted from the
195
+ classes and from each other; see that file. */
205
196
  }
206
197
 
207
- /* Tappable primitives opt out of double-tap-to-zoom. A browser that still
208
- recognises that gesture has to WAIT after the first tap to see whether a
209
- second one is coming, which reads as a late, mushy tap on exactly the
210
- surfaces a phone UI is built from. `manipulation` keeps panning and
211
- pinch-zoom (so the page stays zoomable never `none` here, that would be an
212
- a11y regression) and drops only the double-tap.
213
- NOT inside the pointer:coarse block above: a hybrid device (touch laptop,
214
- iPad with a trackpad) reports a fine primary pointer while still taking
215
- touch input, and the property is inert on a mouse anyway.
216
- Community convention — no primary Apple/WebKit source names it; see
217
- docs/ios-conformance.md. Named components only, so a consumer's own elements
218
- are untouched. See 20260803-038. */
219
- :where(
220
- .juno-btn,
221
- .juno-dock__item,
222
- .juno-pillbar__item,
223
- .juno-pillbar__overflow,
224
- .juno-tabs__tab,
225
- .juno-list__row,
226
- .juno-menu__item,
227
- .juno-seg__opt,
228
- .juno-chip,
229
- .juno-toggle-btn
230
- ) {
231
- touch-action: manipulation;
232
- }
198
+ /* The touch defaults that used to be two hand-maintained `:where()` lists here
199
+ are GENERATED from src/css/touch-surfaces.mjs and emitted straight after this
200
+ file. Both lists had drifted from the classes (`.juno-seg__option`,
201
+ `.juno-list__item`: neither exists, so `:where()` matched nothing and the
202
+ rule parsed anyway) and from each other. One declared set now feeds both.
203
+ See 20260826-024 and docs/conformance-kit.md. */
233
204
 
234
205
  /* Gesture-owned surfaces — for an element whose pointer events are fully
235
206
  driven by app JS (drag-pan, pinch-zoom, swipe classification: a state
@@ -0,0 +1,97 @@
1
+ /* ════════════════════════════════════════════════════════════════════
2
+ * Component — Canvas ink (marks drawn over arbitrary imagery)
3
+ * For text and vector ink sitting on a photo, a map, a video frame or a
4
+ * camera feed: anywhere the background is CONTENT rather than one of
5
+ * junoui's surfaces.
6
+ *
7
+ * WHY IT IS NOT JUST A COLOUR. junoui's whole contrast story assumes a
8
+ * controlled surface, s0 through s3 — every role colour's ratio is
9
+ * computed against a known background. Over imagery there is no known
10
+ * background. In one orthophoto a black shadow and a snowfield are
11
+ * adjacent pixels, so no single ink colour is legible and no contrast
12
+ * ratio can be asserted about one. The answer is a PAIR that spans the
13
+ * luminance range: over a light backing the halo carries the contrast,
14
+ * over a dark one the ink does. Neither half works alone, which is why
15
+ * they are applied together by one class.
16
+ *
17
+ * NOT THEMED, on purpose. A satellite image does not get lighter because
18
+ * the user chose light mode. Theming this pair would make it track the
19
+ * app's surface, which is precisely the background it is NOT over.
20
+ * Usage:
21
+ * <figcaption class="juno-canvas-ink">Sector 7 · 1.2 km</figcaption>
22
+ * <svg class="juno-canvas-ink"><path class="juno-canvas-ink__stroke" …/></svg>
23
+ * <div class="juno-canvas-scrim">…chrome floating over the content…</div>
24
+ * ════════════════════════════════════════════════════════════════════ */
25
+
26
+ /* Text. Four offset shadows rather than one blur: a blurred shadow fades at
27
+ the glyph's corners, which is exactly where a thin stroke needs the most
28
+ help, and it costs the same. `paint-order` is for SVG text, where a real
29
+ stroke is available and better. */
30
+ .juno-canvas-ink {
31
+ color: var(--juno-ink-canvas-ink);
32
+ paint-order: stroke fill;
33
+ stroke: var(--juno-ink-canvas-halo);
34
+ stroke-width: var(--juno-ink-canvas-halo-width);
35
+ text-shadow:
36
+ var(--juno-ink-canvas-halo-width) 0 0 var(--juno-ink-canvas-halo),
37
+ calc(-1 * var(--juno-ink-canvas-halo-width)) 0 0 var(--juno-ink-canvas-halo),
38
+ 0 var(--juno-ink-canvas-halo-width) 0 var(--juno-ink-canvas-halo),
39
+ 0 calc(-1 * var(--juno-ink-canvas-halo-width)) 0 var(--juno-ink-canvas-halo);
40
+ }
41
+
42
+ /* Display sizes: a 2px halo reads as a hairline against a heavier stroke. */
43
+ .juno-canvas-ink--lg {
44
+ stroke-width: var(--juno-ink-canvas-halo-width-lg);
45
+ text-shadow:
46
+ var(--juno-ink-canvas-halo-width-lg) 0 0 var(--juno-ink-canvas-halo),
47
+ calc(-1 * var(--juno-ink-canvas-halo-width-lg)) 0 0 var(--juno-ink-canvas-halo),
48
+ 0 var(--juno-ink-canvas-halo-width-lg) 0 var(--juno-ink-canvas-halo),
49
+ 0 calc(-1 * var(--juno-ink-canvas-halo-width-lg)) 0 var(--juno-ink-canvas-halo);
50
+ }
51
+
52
+ /* Vector ink — measurement lines, selection outlines. Two passes: the halo
53
+ is the same path drawn wider underneath. In SVG give the halo element
54
+ this class and the mark element __stroke; in canvas, stroke twice. */
55
+ .juno-canvas-ink__halo {
56
+ fill: none;
57
+ stroke: var(--juno-ink-canvas-halo);
58
+ stroke-width: calc(var(--juno-ink-canvas-halo-width) * 2);
59
+ stroke-linecap: round;
60
+ stroke-linejoin: round;
61
+ }
62
+
63
+ .juno-canvas-ink__stroke {
64
+ fill: none;
65
+ stroke: var(--juno-ink-canvas-ink);
66
+ stroke-linecap: round;
67
+ stroke-linejoin: round;
68
+ }
69
+
70
+ /* Status over canvas. The themed role hues are chosen against s0–s3 and wash
71
+ out over a saturated backdrop, so these are the same hues at raised chroma.
72
+ Louder than the panel roles by design — they will look wrong on a panel. */
73
+ .juno-canvas-ink--nominal { color: var(--juno-ink-vivid-nominal); }
74
+ .juno-canvas-ink--active { color: var(--juno-ink-vivid-active); }
75
+ .juno-canvas-ink--target { color: var(--juno-ink-vivid-target); }
76
+ .juno-canvas-ink--caution { color: var(--juno-ink-vivid-caution); }
77
+ .juno-canvas-ink--warning { color: var(--juno-ink-vivid-warning); }
78
+
79
+ /* Chrome floating over live content. Deliberately NOT --juno-opacity-scrim
80
+ (0.62): that value suppresses a modal's background, and here the background
81
+ is the thing the chrome is annotating — greying it out defeats the purpose. */
82
+ .juno-canvas-scrim {
83
+ background: color-mix(in srgb, var(--juno-s0) calc(var(--juno-ink-canvas-scrim) * 100%), transparent);
84
+ -webkit-backdrop-filter: blur(8px);
85
+ backdrop-filter: blur(8px);
86
+ }
87
+
88
+ /* A translucent bar over moving content is the case where reduced
89
+ transparency matters most — the motion behind the text is the problem, not
90
+ the blur. Fall back to the opaque surface. */
91
+ @media (prefers-reduced-transparency: reduce) {
92
+ .juno-canvas-scrim {
93
+ background: var(--juno-s0);
94
+ -webkit-backdrop-filter: none;
95
+ backdrop-filter: none;
96
+ }
97
+ }