@junoput01/junoui 0.6.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.
- package/CHANGELOG.md +209 -0
- package/README.md +17 -16
- package/dist/android/dimens.xml +2 -0
- package/dist/classes.json +1774 -0
- package/dist/css/juno-tokens.css +10 -0
- package/dist/css/juno.css +1057 -56
- package/dist/flutter/juno_tokens.dart +10 -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/accessibility.md +6 -0
- package/docs/browser-support.md +3 -0
- package/docs/components/button.md +11 -2
- package/docs/components/canvas-ink.md +71 -0
- package/docs/components/dock.md +34 -0
- package/docs/components/fold-slot.md +26 -1
- 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 +255 -0
- package/docs/getting-started.md +14 -0
- package/docs/integration.md +52 -6
- package/docs/ios-conformance.md +200 -3
- package/docs/ios-pwa.md +273 -0
- package/docs/native.md +38 -1
- package/docs/tokens-reference.md +15 -0
- package/package.json +8 -2
- package/src/css/base.css +35 -43
- package/src/css/components/button.css +42 -2
- package/src/css/components/canvas-ink.css +97 -0
- package/src/css/components/dock.css +65 -6
- package/src/css/components/fold-slot.css +49 -3
- package/src/css/components/gizmo.css +238 -0
- package/src/css/components/segmented.css +15 -2
- 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/tools/gizmo.mjs +144 -0
- package/tools/testing.mjs +177 -0
- package/tools/tree.mjs +178 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
2
|
+
* Component — Viewport orientation gizmo (compass ring + pitch arc)
|
|
3
|
+
* The orientation widget any 3D or map viewport ships: a ring showing a
|
|
4
|
+
* heading with clickable snap targets, a secondary arc for a second
|
|
5
|
+
* angle (pitch / tilt / elevation) inside a clamped range, and a centre
|
|
6
|
+
* target that resets the view.
|
|
7
|
+
*
|
|
8
|
+
* A RING, NOT A CUBE, and that is a design decision rather than a style
|
|
9
|
+
* one. An Autodesk-style ViewCube is the wrong shape for anything with a
|
|
10
|
+
* privileged up-vector — a map, a terrain, a site plan — because there is
|
|
11
|
+
* no meaningful front, right or bottom face to click. A ring degrades to
|
|
12
|
+
* the up-vector case and generalises to the free-orbit one; a cube does
|
|
13
|
+
* not go the other way.
|
|
14
|
+
*
|
|
15
|
+
* THE APP OWNS THE CAMERA. It writes two angles in as custom properties
|
|
16
|
+
* and junoui rotates the marks; nothing here stores or changes state.
|
|
17
|
+
* element.style.setProperty('--juno-gizmo-heading', `${yaw}deg`);
|
|
18
|
+
* element.style.setProperty('--juno-gizmo-pitch', `${pitch}deg`);
|
|
19
|
+
*
|
|
20
|
+
* Usage — see docs/components/gizmo.md for the full a11y contract, which
|
|
21
|
+
* is the half apps get wrong and the reason this is upstream:
|
|
22
|
+
* <div class="juno-gizmo" role="group" aria-label="View orientation">
|
|
23
|
+
* <p class="juno-gizmo__readout" aria-live="polite">Facing north, 0 degrees.</p>
|
|
24
|
+
* <div class="juno-gizmo__ring">
|
|
25
|
+
* <span class="juno-gizmo__needle" aria-hidden="true"></span>
|
|
26
|
+
* <button class="juno-gizmo__mark" style="--juno-gizmo-at:0deg"
|
|
27
|
+
* aria-label="Face north" aria-current="true">N</button>
|
|
28
|
+
* …seven more…
|
|
29
|
+
* <button class="juno-gizmo__center" aria-label="Reset view">⌖</button>
|
|
30
|
+
* </div>
|
|
31
|
+
* <div class="juno-gizmo__arc">
|
|
32
|
+
* <span class="juno-gizmo__arc-hand" aria-hidden="true"></span>
|
|
33
|
+
* </div>
|
|
34
|
+
* </div>
|
|
35
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
36
|
+
|
|
37
|
+
.juno-gizmo {
|
|
38
|
+
--juno-role: var(--juno-active);
|
|
39
|
+
|
|
40
|
+
/* The angles the app writes. Defaults are a level view facing north, so a
|
|
41
|
+
gizmo that is mounted before the camera reports is not a blank circle. */
|
|
42
|
+
--juno-gizmo-heading: 0deg;
|
|
43
|
+
--juno-gizmo-pitch: 0deg;
|
|
44
|
+
|
|
45
|
+
/* MINIMUM DIAMETER, DERIVED — and derived from the CHORD, which is the
|
|
46
|
+
correction that matters. N marks sit evenly around the rim, so the
|
|
47
|
+
straight-line distance between two adjacent centres is d * sin(pi / N).
|
|
48
|
+
For their targets not to overlap THAT must be at least one tap target:
|
|
49
|
+
|
|
50
|
+
d >= tap / sin(pi / N)
|
|
51
|
+
|
|
52
|
+
...and the marks sit INSIDE the rim, so their centres are on a circle of
|
|
53
|
+
radius d/2 - tap/2, not d/2. Substituting that and solving:
|
|
54
|
+
|
|
55
|
+
d >= tap * (1 / sin(pi / N) + 1)
|
|
56
|
+
|
|
57
|
+
THIS DERIVATION WAS WRONG TWICE, and the browser guard caught both. First
|
|
58
|
+
it sized off the ARC between centres (N * tap / pi) — an arc is longer
|
|
59
|
+
than its chord, so at N=8 and a 44px target that gave 112.05px whose chord
|
|
60
|
+
is 42.9px, a 1.1px overlap on every neighbouring pair. Corrected to the
|
|
61
|
+
chord, it still measured 27.16px between centres, because the inset had
|
|
62
|
+
been left out of the radius. Neither error is visible in the source; both
|
|
63
|
+
are one measurement away.
|
|
64
|
+
|
|
65
|
+
The tap floor MOVES, 24px to 44px on a coarse pointer, which is precisely
|
|
66
|
+
when the ring has to grow; a hard-coded diameter ships eight overlapping
|
|
67
|
+
targets to a phone. The 96px is an aesthetic floor, not a derived one —
|
|
68
|
+
a 63px ring is legible-but-cramped on desktop — and max() keeps whichever
|
|
69
|
+
is larger. */
|
|
70
|
+
--juno-gizmo-marks: 8;
|
|
71
|
+
--juno-gizmo-size: max(
|
|
72
|
+
96px,
|
|
73
|
+
calc(var(--juno-size-tap-min) * (1 / sin(180deg / var(--juno-gizmo-marks)) + 1))
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
display: inline-flex;
|
|
77
|
+
flex-direction: column;
|
|
78
|
+
align-items: center;
|
|
79
|
+
gap: var(--juno-space-8);
|
|
80
|
+
font-family: var(--juno-font-family-sans);
|
|
81
|
+
color: var(--juno-data);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* The spoken state. Visible by default — a bearing is useful to everyone —
|
|
85
|
+
but it is the live region either way, because a rotating needle announces
|
|
86
|
+
nothing. Consumers that want it invisible use .juno-sr-only rather than
|
|
87
|
+
display:none, which would take it out of the accessibility tree too. */
|
|
88
|
+
.juno-gizmo__readout {
|
|
89
|
+
margin: 0;
|
|
90
|
+
font-size: var(--juno-font-size-11);
|
|
91
|
+
font-variant-numeric: tabular-nums;
|
|
92
|
+
letter-spacing: var(--juno-font-tracking-label);
|
|
93
|
+
color: var(--juno-label);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.juno-gizmo__ring {
|
|
97
|
+
position: relative;
|
|
98
|
+
inline-size: var(--juno-gizmo-size);
|
|
99
|
+
block-size: var(--juno-gizmo-size);
|
|
100
|
+
border-radius: 50%;
|
|
101
|
+
border: var(--juno-border-width-1) solid var(--juno-border);
|
|
102
|
+
background: color-mix(in srgb, var(--juno-s1) 88%, transparent);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* The needle points where the camera looks. Rotated by the app's angle, and
|
|
106
|
+
the transition runs on the motion scale so prefers-reduced-motion collapses
|
|
107
|
+
it without this file needing its own media query (base.css owns that). */
|
|
108
|
+
.juno-gizmo__needle {
|
|
109
|
+
position: absolute;
|
|
110
|
+
inset: 0;
|
|
111
|
+
display: grid;
|
|
112
|
+
place-items: start center;
|
|
113
|
+
pointer-events: none;
|
|
114
|
+
rotate: var(--juno-gizmo-heading);
|
|
115
|
+
transition: rotate calc(var(--juno-motion-duration-base) * var(--juno-motion-scale))
|
|
116
|
+
var(--juno-motion-ease-standard);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.juno-gizmo__needle::before {
|
|
120
|
+
content: '';
|
|
121
|
+
inline-size: var(--juno-border-width-2);
|
|
122
|
+
block-size: 38%;
|
|
123
|
+
margin-block-start: var(--juno-space-4);
|
|
124
|
+
background: linear-gradient(var(--juno-role), transparent);
|
|
125
|
+
border-radius: var(--juno-border-width-2);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* ── snap targets ──────────────────────────────────────────────────────
|
|
129
|
+
REAL BUTTONS. Not a canvas hit test, not a div with a click handler: a
|
|
130
|
+
button is focusable, activates on Enter and Space, is announced as a
|
|
131
|
+
control, and works with a screen reader's forms mode. That is most of
|
|
132
|
+
this component's value and the reason it is upstream.
|
|
133
|
+
|
|
134
|
+
Positioned by rotating the mark out to the rim and counter-rotating the
|
|
135
|
+
glyph, so the letter stays upright at every angle. --juno-gizmo-at is the
|
|
136
|
+
mark's own bearing, set per element. */
|
|
137
|
+
.juno-gizmo__mark {
|
|
138
|
+
position: absolute;
|
|
139
|
+
inset-block-start: 50%;
|
|
140
|
+
inset-inline-start: 50%;
|
|
141
|
+
inline-size: var(--juno-size-tap-min);
|
|
142
|
+
block-size: var(--juno-size-tap-min);
|
|
143
|
+
margin: calc(var(--juno-size-tap-min) / -2);
|
|
144
|
+
display: grid;
|
|
145
|
+
place-items: center;
|
|
146
|
+
padding: 0;
|
|
147
|
+
border: none;
|
|
148
|
+
border-radius: 50%;
|
|
149
|
+
background: transparent;
|
|
150
|
+
color: var(--juno-label);
|
|
151
|
+
font: inherit;
|
|
152
|
+
font-size: var(--juno-font-size-11);
|
|
153
|
+
font-weight: var(--juno-font-weight-bold);
|
|
154
|
+
cursor: pointer;
|
|
155
|
+
transform: rotate(var(--juno-gizmo-at, 0deg))
|
|
156
|
+
translate(0, calc(var(--juno-gizmo-size) / -2 + var(--juno-size-tap-min) / 2))
|
|
157
|
+
rotate(calc(-1 * var(--juno-gizmo-at, 0deg)));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.juno-gizmo__mark:hover {
|
|
161
|
+
color: var(--juno-data);
|
|
162
|
+
background: var(--juno-s2);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* The mark the camera is currently nearest. aria-current, not a class: the
|
|
166
|
+
app already has to say it for the screen reader, and a class would be a
|
|
167
|
+
second copy of the same fact. */
|
|
168
|
+
.juno-gizmo__mark[aria-current='true'] {
|
|
169
|
+
color: var(--juno-role);
|
|
170
|
+
background: var(--juno-s3);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.juno-gizmo__mark:focus-visible,
|
|
174
|
+
.juno-gizmo__center:focus-visible {
|
|
175
|
+
outline: var(--juno-border-width-2) solid var(--juno-active);
|
|
176
|
+
outline-offset: var(--juno-space-2);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* Centre target — reset to the default view. */
|
|
180
|
+
.juno-gizmo__center {
|
|
181
|
+
position: absolute;
|
|
182
|
+
inset-block-start: 50%;
|
|
183
|
+
inset-inline-start: 50%;
|
|
184
|
+
inline-size: var(--juno-size-tap-min);
|
|
185
|
+
block-size: var(--juno-size-tap-min);
|
|
186
|
+
margin: calc(var(--juno-size-tap-min) / -2);
|
|
187
|
+
display: grid;
|
|
188
|
+
place-items: center;
|
|
189
|
+
padding: 0;
|
|
190
|
+
border: var(--juno-border-width-1) solid var(--juno-border);
|
|
191
|
+
border-radius: 50%;
|
|
192
|
+
background: var(--juno-s1);
|
|
193
|
+
color: var(--juno-label);
|
|
194
|
+
font: inherit;
|
|
195
|
+
cursor: pointer;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.juno-gizmo__center:hover {
|
|
199
|
+
color: var(--juno-role);
|
|
200
|
+
border-color: var(--juno-role);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* ── pitch arc ─────────────────────────────────────────────────────────
|
|
204
|
+
The second angle, inside a CLAMPED range — a camera that can tilt 0..85°
|
|
205
|
+
should not show a hand that can point anywhere, or the widget lies about
|
|
206
|
+
what the app will accept. The clamp lives here so every consumer gets it
|
|
207
|
+
without re-deriving it from its own camera limits. */
|
|
208
|
+
.juno-gizmo__arc {
|
|
209
|
+
--juno-gizmo-pitch-min: 0deg;
|
|
210
|
+
--juno-gizmo-pitch-max: 85deg;
|
|
211
|
+
|
|
212
|
+
position: relative;
|
|
213
|
+
inline-size: var(--juno-gizmo-size);
|
|
214
|
+
block-size: calc(var(--juno-gizmo-size) / 2);
|
|
215
|
+
overflow: hidden;
|
|
216
|
+
border-block-end: var(--juno-border-width-1) solid var(--juno-border);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.juno-gizmo__arc-hand {
|
|
220
|
+
position: absolute;
|
|
221
|
+
inset-block-end: 0;
|
|
222
|
+
inset-inline-start: 50%;
|
|
223
|
+
inline-size: var(--juno-border-width-2);
|
|
224
|
+
block-size: 80%;
|
|
225
|
+
background: var(--juno-role);
|
|
226
|
+
transform-origin: bottom center;
|
|
227
|
+
rotate: clamp(var(--juno-gizmo-pitch-min), var(--juno-gizmo-pitch), var(--juno-gizmo-pitch-max));
|
|
228
|
+
transition: rotate calc(var(--juno-motion-duration-base) * var(--juno-motion-scale))
|
|
229
|
+
var(--juno-motion-ease-standard);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* ── touch ─────────────────────────────────────────────────────────────
|
|
233
|
+
Nothing to grow here, and that is the point: the marks are sized in
|
|
234
|
+
--juno-size-tap-min, which base.css promotes to the comfortable target on a
|
|
235
|
+
coarse pointer, and --juno-gizmo-size is derived from it — so the whole ring
|
|
236
|
+
grows and the marks stay non-overlapping by construction. A gizmo that keeps
|
|
237
|
+
its desktop diameter on a phone has eight overlapping targets, which is the
|
|
238
|
+
defect this derivation exists to make impossible. */
|
|
@@ -38,11 +38,20 @@
|
|
|
38
38
|
opacity: 0;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
/* the pill — a span after the radio, or the aria-pressed button itself
|
|
41
|
+
/* the pill — a span after the radio, or the aria-pressed button itself.
|
|
42
|
+
The tap floor is on the PAINTED box, not on the label that wraps it: the
|
|
43
|
+
label is a bare inline-flex and takes its height from this. Segmented is a
|
|
44
|
+
phone-first control and is routinely the only control on a whole settings
|
|
45
|
+
section, so it holds --juno-size-tap-min like .juno-btn does — 24px (WCAG
|
|
46
|
+
2.2 AA, 2.5.8) on a fine pointer, promoted to the 44px comfortable target
|
|
47
|
+
under (pointer: coarse) by base.css. Without it the pill computed ~25px
|
|
48
|
+
from its padding alone, which is the AA floor by accident and nothing on
|
|
49
|
+
touch. See 20260826-025. */
|
|
42
50
|
.juno-seg__opt input + span,
|
|
43
51
|
button.juno-seg__opt {
|
|
44
52
|
display: inline-flex;
|
|
45
53
|
align-items: center;
|
|
54
|
+
min-block-size: var(--juno-size-tap-min);
|
|
46
55
|
gap: var(--juno-gap-control);
|
|
47
56
|
font-family: var(--juno-font-family-sans);
|
|
48
57
|
font-size: var(--juno-font-size-11);
|
|
@@ -90,7 +99,11 @@ button.juno-seg__opt:disabled {
|
|
|
90
99
|
cursor: not-allowed;
|
|
91
100
|
}
|
|
92
101
|
|
|
93
|
-
/* Small —
|
|
102
|
+
/* Small — dense toolbars: less type, tighter padding. UNLIKE .juno-btn--sm it
|
|
103
|
+
does NOT drop below the tap floor, which the base rule above holds: --sm is a
|
|
104
|
+
density modifier here, not an opt-out of the touch target. A segmented row is
|
|
105
|
+
often the only control on a settings section, so a sub-tap variant of it has
|
|
106
|
+
no safe use on a phone. */
|
|
94
107
|
.juno-seg--sm .juno-seg__opt input + span,
|
|
95
108
|
.juno-seg--sm > button.juno-seg__opt {
|
|
96
109
|
font-size: var(--juno-font-size-10);
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
2
|
+
* Component — Colour swatch + palette
|
|
3
|
+
* Showing a user-chosen colour, and letting someone pick one. Diagrams,
|
|
4
|
+
* calendars, tag and label systems, chart series colours, annotation
|
|
5
|
+
* tools, theming UIs, kanban boards.
|
|
6
|
+
*
|
|
7
|
+
* THE HARD PART IS NOT THE SQUARE. A swatch shows an ARBITRARY colour, so
|
|
8
|
+
* every piece of chrome on it — its border, its focus ring, its checked
|
|
9
|
+
* indicator — has to stay visible against a colour junoui has never seen.
|
|
10
|
+
* A single hairline fails at one end of the range: a dark border vanishes
|
|
11
|
+
* on near-black, a light one vanishes on near-white, and the swatch that
|
|
12
|
+
* loses its border is the one that has merged with the panel behind it.
|
|
13
|
+
*
|
|
14
|
+
* The answer is a PAIR of hairlines drawn as an inset and an outset ring.
|
|
15
|
+
* They do NOT do the same job, which is the part worth reading twice: the
|
|
16
|
+
* inset one composites over the SWATCH and edges it against its own fill,
|
|
17
|
+
* the outset one composites over the PANEL and separates it from the
|
|
18
|
+
* surface. With the swatch's own contrast against that panel, the boundary
|
|
19
|
+
* has three ways to be visible and needs only one of them.
|
|
20
|
+
*
|
|
21
|
+
* Guarded by sweeping the swatch colour against both panels rather than by
|
|
22
|
+
* asserting a border value (test/swatch.test.mjs).
|
|
23
|
+
*
|
|
24
|
+
* COLOUR IS NEVER THE ONLY SIGNAL. junoui's own standing rule, and a bare
|
|
25
|
+
* swatch is exactly what violates it: every swatch carries an accessible
|
|
26
|
+
* NAME, and the checked state carries a GLYPH, not just a hue.
|
|
27
|
+
*
|
|
28
|
+
* The app owns the colour list and which one is chosen.
|
|
29
|
+
* Usage:
|
|
30
|
+
* <button class="juno-swatch juno-swatch--button" style="--juno-swatch-color:#C41E3A"
|
|
31
|
+
* aria-label="Crimson"></button>
|
|
32
|
+
*
|
|
33
|
+
* <div class="juno-popover" popover id="palette">
|
|
34
|
+
* <div class="juno-palette" role="listbox" aria-label="Annotation colour">
|
|
35
|
+
* <button class="juno-palette__option" role="option" aria-selected="true"
|
|
36
|
+
* style="--juno-swatch-color:#C41E3A" aria-label="Crimson">
|
|
37
|
+
* <svg class="juno-icon juno-palette__check" aria-hidden="true">
|
|
38
|
+
* <use href="…#juno-i-check" /></svg>
|
|
39
|
+
* </button>
|
|
40
|
+
* </div>
|
|
41
|
+
* </div>
|
|
42
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
43
|
+
|
|
44
|
+
.juno-swatch {
|
|
45
|
+
/* The colour the app is showing. Named rather than an inline `background`
|
|
46
|
+
so the rings below can sit on top of it without the caller reassembling
|
|
47
|
+
the whole box-shadow stack. */
|
|
48
|
+
--juno-swatch-color: var(--juno-muted);
|
|
49
|
+
|
|
50
|
+
/* Sized off the control scale, so a swatch in a form row matches the
|
|
51
|
+
controls beside it and grows with them on a coarse pointer. */
|
|
52
|
+
--juno-swatch-size: var(--juno-size-tap-min);
|
|
53
|
+
|
|
54
|
+
display: inline-block;
|
|
55
|
+
inline-size: var(--juno-swatch-size);
|
|
56
|
+
block-size: var(--juno-swatch-size);
|
|
57
|
+
flex-shrink: 0;
|
|
58
|
+
border: none;
|
|
59
|
+
border-radius: var(--juno-radius-3);
|
|
60
|
+
background: var(--juno-swatch-color);
|
|
61
|
+
|
|
62
|
+
/* THE TWO-TONE RING, and the alphas are measured rather than chosen.
|
|
63
|
+
The inset ring composites over the SWATCH and gives it an edge against
|
|
64
|
+
its own fill; the outset ring composites over the PANEL — an outset
|
|
65
|
+
box-shadow is outside the element — and separates the swatch from the
|
|
66
|
+
surface behind it. Between them, plus the swatch's own contrast with the
|
|
67
|
+
panel, the boundary is discernible whatever colour the app supplies.
|
|
68
|
+
|
|
69
|
+
At 0.45 / 0.35 that property FAILS: on a light panel a mid-grey swatch
|
|
70
|
+
(around #919191) leaves the best of the three at 2.57:1, under the 3:1
|
|
71
|
+
non-text floor. 0.65 on both takes the worst case to 3.39:1. The sweep in
|
|
72
|
+
test/swatch.test.mjs is what measured it — the first version of that test
|
|
73
|
+
asked the wrong question (both rings against the swatch) and reported 145
|
|
74
|
+
failures; the right question is whether the BOUNDARY is visible, and it
|
|
75
|
+
has three ways to be.
|
|
76
|
+
|
|
77
|
+
Deliberately not `border`, which would eat into the colour area and change
|
|
78
|
+
the swatch's size with its style. */
|
|
79
|
+
box-shadow:
|
|
80
|
+
inset 0 0 0 var(--juno-border-width-1) rgb(0 0 0 / 0.65),
|
|
81
|
+
0 0 0 var(--juno-border-width-1) rgb(255 255 255 / 0.65);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.juno-swatch--circle {
|
|
85
|
+
border-radius: 50%;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.juno-swatch--sm {
|
|
89
|
+
--juno-swatch-size: var(--juno-space-16);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.juno-swatch--lg {
|
|
93
|
+
--juno-swatch-size: var(--juno-size-tap-comfortable);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* A swatch used as a trigger — the thing you click to open the palette. */
|
|
97
|
+
.juno-swatch--button {
|
|
98
|
+
padding: 0;
|
|
99
|
+
cursor: pointer;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* FOCUS RINGS ON AN ARBITRARY HUE. The ring is drawn OUTSIDE the swatch,
|
|
103
|
+
with an offset, so its contrast is against the panel — a known surface —
|
|
104
|
+
rather than against a colour junoui cannot predict. A ring drawn on the
|
|
105
|
+
swatch itself has the same unsolvable problem as the border, and "use a
|
|
106
|
+
thicker ring" does not fix a hue collision. */
|
|
107
|
+
.juno-swatch--button:focus-visible,
|
|
108
|
+
.juno-palette__option:focus-visible {
|
|
109
|
+
outline: var(--juno-border-width-2) solid var(--juno-active);
|
|
110
|
+
outline-offset: var(--juno-space-4);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* ── palette ───────────────────────────────────────────────────────────
|
|
114
|
+
A grid of swatches inside a .juno-popover. junoui ships the grid and the
|
|
115
|
+
states; the app owns the list and which one is chosen. */
|
|
116
|
+
.juno-palette {
|
|
117
|
+
display: grid;
|
|
118
|
+
grid-template-columns: repeat(var(--juno-palette-columns, 6), auto);
|
|
119
|
+
gap: var(--juno-space-8);
|
|
120
|
+
padding: var(--juno-space-4);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.juno-palette__option {
|
|
124
|
+
--juno-swatch-color: var(--juno-muted);
|
|
125
|
+
--juno-swatch-size: var(--juno-size-tap-min);
|
|
126
|
+
|
|
127
|
+
position: relative;
|
|
128
|
+
inline-size: var(--juno-swatch-size);
|
|
129
|
+
block-size: var(--juno-swatch-size);
|
|
130
|
+
display: grid;
|
|
131
|
+
place-items: center;
|
|
132
|
+
padding: 0;
|
|
133
|
+
border: none;
|
|
134
|
+
border-radius: var(--juno-radius-3);
|
|
135
|
+
background: var(--juno-swatch-color);
|
|
136
|
+
cursor: pointer;
|
|
137
|
+
box-shadow:
|
|
138
|
+
inset 0 0 0 var(--juno-border-width-1) rgb(0 0 0 / 0.65),
|
|
139
|
+
0 0 0 var(--juno-border-width-1) rgb(255 255 255 / 0.65);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* THE CHECKED STATE IS A GLYPH, NOT A HUE. Colour is never the only signal,
|
|
143
|
+
and "the chosen one is the one that looks slightly different" is exactly
|
|
144
|
+
the failure that rule exists to prevent — it is invisible to anyone who
|
|
145
|
+
cannot separate the two hues, and to anyone looking at a screenshot.
|
|
146
|
+
|
|
147
|
+
The check itself sits on the arbitrary colour, so it gets the same
|
|
148
|
+
treatment as the swatch's own ring: a light glyph with a dark halo, which
|
|
149
|
+
is the canvas-ink pair at glyph scale. */
|
|
150
|
+
.juno-palette__check {
|
|
151
|
+
display: none;
|
|
152
|
+
color: #fff;
|
|
153
|
+
filter: drop-shadow(0 0 var(--juno-border-width-1) rgb(0 0 0 / 0.9))
|
|
154
|
+
drop-shadow(0 0 var(--juno-border-width-2) rgb(0 0 0 / 0.7));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.juno-palette__option[aria-selected='true'] .juno-palette__check,
|
|
158
|
+
.juno-palette__option[aria-checked='true'] .juno-palette__check {
|
|
159
|
+
display: block;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* ...and a second, non-glyph cue for the same state, because a check inside a
|
|
163
|
+
28px square is small: the selected option also grows a ring in the active
|
|
164
|
+
role, drawn OUTSIDE the swatch where its contrast is against the panel. */
|
|
165
|
+
.juno-palette__option[aria-selected='true'],
|
|
166
|
+
.juno-palette__option[aria-checked='true'] {
|
|
167
|
+
box-shadow:
|
|
168
|
+
inset 0 0 0 var(--juno-border-width-1) rgb(0 0 0 / 0.65),
|
|
169
|
+
0 0 0 var(--juno-border-width-1) rgb(255 255 255 / 0.65),
|
|
170
|
+
0 0 0 calc(var(--juno-border-width-1) + var(--juno-border-width-2)) var(--juno-active);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* A swatch showing "no colour" — transparent, unset, inherit. Without this a
|
|
174
|
+
consumer paints it in a mid grey and the user cannot tell "grey" from
|
|
175
|
+
"none", which is a different thing to know. */
|
|
176
|
+
.juno-swatch--none,
|
|
177
|
+
.juno-palette__option--none {
|
|
178
|
+
background:
|
|
179
|
+
linear-gradient(
|
|
180
|
+
to bottom right,
|
|
181
|
+
transparent calc(50% - var(--juno-border-width-1)),
|
|
182
|
+
var(--juno-warning) calc(50% - var(--juno-border-width-1)),
|
|
183
|
+
var(--juno-warning) calc(50% + var(--juno-border-width-1)),
|
|
184
|
+
transparent calc(50% + var(--juno-border-width-1))
|
|
185
|
+
)
|
|
186
|
+
var(--juno-s1);
|
|
187
|
+
}
|