@threadlabs/looma 0.11.6 → 0.12.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/components/shared/describe.js +9 -0
- package/components/ui-button/ui-button.html +6 -19
- package/components/ui-checkbox/ui-checkbox.html +22 -1
- package/components/ui-checkbox/ui-checkbox.js +2 -0
- package/components/ui-icon-button/ui-icon-button.html +4 -14
- package/components/ui-radio/ui-radio.html +22 -1
- package/components/ui-radio/ui-radio.js +2 -0
- package/components/ui-sidebar/ui-sidebar.js +3 -1
- package/components/ui-switch/ui-switch.html +22 -1
- package/components/ui-switch/ui-switch.js +2 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/styles/ui-button.css +11 -25
- package/styles/ui-checkbox.css +21 -1
- package/styles/ui-icon-button.css +4 -13
- package/styles/ui-radio.css +21 -1
- package/styles/ui-switch.css +21 -1
- package/theme-dark.css +2 -0
- package/theme-high-contrast.css +3 -0
- package/tokens.css +3 -0
- package/vanilla/UiCheckbox.js +18 -3
- package/vanilla/UiRadio.js +18 -3
- package/vanilla/UiSwitch.js +18 -3
- package/vue/UiButton.vue +6 -19
- package/vue/UiCheckbox.d.ts +3 -1
- package/vue/UiCheckbox.vue +24 -2
- package/vue/UiIconButton.vue +3 -13
- package/vue/UiRadio.d.ts +3 -1
- package/vue/UiRadio.vue +24 -2
- package/vue/UiSwitch.d.ts +3 -1
- package/vue/UiSwitch.vue +24 -2
- package/vue/chunks/UiButton.js +1 -1
- package/vue/chunks/UiCheckbox.js +14 -4
- package/vue/chunks/UiIconButton.js +1 -1
- package/vue/chunks/UiRadio.js +14 -4
- package/vue/chunks/UiSwitch.js +14 -4
- package/vue/components.css +150 -123
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
let descriptions = 0;
|
|
2
|
+
|
|
3
|
+
// A choice's description (Radio, Checkbox, Switch) sits inside its label, so a press on it still
|
|
4
|
+
// chooses. It is hidden from the label and named as the input's description instead, so a screen
|
|
5
|
+
// reader says "Private, radio button, Only the people you add can open it" rather than one long name.
|
|
6
|
+
export function describeInput(input, description) {
|
|
7
|
+
description.id ||= `ui-description-${++descriptions}`;
|
|
8
|
+
input.setAttribute("aria-describedby", description.id);
|
|
9
|
+
}
|
|
@@ -220,31 +220,19 @@
|
|
|
220
220
|
var(--ui-focus-halo);
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
/* Disabled
|
|
224
|
-
disabled outline is still an outline and a disabled ghost still has no edge.
|
|
225
|
-
|
|
226
|
-
highlight and shadow, because nothing that cannot be pressed should
|
|
223
|
+
/* Disabled keeps the shape and washes out the rest: every variant keeps its own structure, so a
|
|
224
|
+
disabled outline is still an outline and a disabled ghost still has no edge. One filter fades
|
|
225
|
+
fill, border, and text together (--ui-disabled-filter), and the theme decides which way is
|
|
226
|
+
toward the page. The highlight and shadow go, because nothing that cannot be pressed should
|
|
227
|
+
look raised. */
|
|
227
228
|
:host:is(:disabled, [aria-disabled="true"]) {
|
|
228
|
-
|
|
229
|
-
page, which desaturates it at the same time. Toward the page rather than toward white,
|
|
230
|
-
so a dark theme dims where a light one lightens — the same rule, both schemes. */
|
|
231
|
-
--_tone: color-mix(in oklab, var(--_hue) 58%, var(--ui-surface));
|
|
232
|
-
--_tone-hover: var(--_tone);
|
|
233
|
-
--_tone-active: var(--_tone);
|
|
234
|
-
--_tone-text: color-mix(in oklab, var(--_hue-text) 72%, var(--ui-surface));
|
|
235
|
-
--_on-tone: var(--_tone-text);
|
|
236
|
-
|
|
229
|
+
filter: var(--ui-button-disabled-filter, var(--ui-disabled-filter));
|
|
237
230
|
opacity: var(--ui-button-disabled-opacity, 1);
|
|
238
231
|
cursor: not-allowed;
|
|
239
232
|
background-image: none;
|
|
240
233
|
box-shadow: none;
|
|
241
234
|
}
|
|
242
235
|
|
|
243
|
-
/* A filled button keeps its fill, faded further so its text stays legible on it. */
|
|
244
|
-
:is(:host-state([variant="solid"]), :host-state([variant="danger"])):is(:disabled, [aria-disabled="true"]) {
|
|
245
|
-
--_tone: color-mix(in oklab, var(--_hue) 34%, var(--ui-surface));
|
|
246
|
-
}
|
|
247
|
-
|
|
248
236
|
/* Ghost has no hover to fall back on, so disabled is the one state it has to state outright:
|
|
249
237
|
it takes a surface rather than reading as plain text. The surface is a wash of the faded tone,
|
|
250
238
|
the way hover washes the live one, so it stays light in every tone. An opaque mix toward the
|
|
@@ -311,7 +299,6 @@
|
|
|
311
299
|
:host-state([variant="link"]):is(:disabled, [aria-disabled="true"]) {
|
|
312
300
|
background: none;
|
|
313
301
|
border-color: transparent;
|
|
314
|
-
color: var(--ui-button-disabled-text, var(--ui-disabled-text));
|
|
315
302
|
}
|
|
316
303
|
|
|
317
304
|
:host-state([variant="link"]):focus-visible {
|
|
@@ -28,7 +28,10 @@
|
|
|
28
28
|
:name="name"
|
|
29
29
|
:value="value"
|
|
30
30
|
>
|
|
31
|
-
<span class="
|
|
31
|
+
<span class="text">
|
|
32
|
+
<span class="label"><slot></slot></span>
|
|
33
|
+
<span class="description" $ref="description" aria-hidden="true"><slot name="description"></slot></span>
|
|
34
|
+
</span>
|
|
32
35
|
</label>
|
|
33
36
|
<style>
|
|
34
37
|
:host {
|
|
@@ -40,11 +43,29 @@
|
|
|
40
43
|
cursor: pointer;
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
/* The label, and under it an optional line saying what the choice means. */
|
|
47
|
+
.text {
|
|
48
|
+
display: flex;
|
|
49
|
+
flex-direction: column;
|
|
50
|
+
gap: var(--ui-space-1);
|
|
51
|
+
min-inline-size: 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
43
54
|
.label {
|
|
44
55
|
min-inline-size: 0;
|
|
45
56
|
line-height: var(--ui-line-height-md);
|
|
46
57
|
}
|
|
47
58
|
|
|
59
|
+
.description {
|
|
60
|
+
color: var(--ui-text-secondary);
|
|
61
|
+
font-size: var(--ui-font-size-sm);
|
|
62
|
+
line-height: var(--ui-line-height);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.description:empty {
|
|
66
|
+
display: none;
|
|
67
|
+
}
|
|
68
|
+
|
|
48
69
|
input {
|
|
49
70
|
appearance: none;
|
|
50
71
|
-webkit-appearance: none;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterFormReset } from "../shared/form-reset.js";
|
|
2
|
+
import { describeInput } from "../shared/describe.js";
|
|
2
3
|
import { trackTrigger } from "../shared/trigger.js";
|
|
3
4
|
|
|
4
5
|
// `checked` sets the control initially and whenever it changes; the user's changes update the state.
|
|
@@ -6,6 +7,7 @@ import { trackTrigger } from "../shared/trigger.js";
|
|
|
6
7
|
// the native default alone cannot hold it there.
|
|
7
8
|
export default function controller(host) {
|
|
8
9
|
const input = host.refs.input;
|
|
10
|
+
describeInput(input, host.refs.description);
|
|
9
11
|
const [trigger, stopTracking] = trackTrigger(host);
|
|
10
12
|
let external = host.state.checked;
|
|
11
13
|
host.state.internalChecked = Boolean(external);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template component="ui-icon-button" controller="./ui-icon-button.js">
|
|
2
2
|
<defs>
|
|
3
3
|
<prop name="disabled" type="boolean" default="false"
|
|
4
|
-
>Disables the button, so it cannot be pressed or focused, and
|
|
4
|
+
>Disables the button, so it cannot be pressed or focused, and washes it out: the same shape, with less colour and contrast (--ui-disabled-filter).</prop>
|
|
5
5
|
<prop name="label" type="string">Accessible name for the icon.</prop>
|
|
6
6
|
<prop name="size" type="sm | md | lg" default="md"
|
|
7
7
|
>Button and icon size together: sm is 1.75rem with a small icon, md is 2rem, and lg is 2.5rem with a large icon. On touch, every size gets an invisible hit
|
|
@@ -177,22 +177,12 @@
|
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
/* Disabled colours fall back to each variant's own; ghost takes only the icon colour. */
|
|
180
|
+
/* Disabled washes the button out the way Button does: the same shape, one filter. */
|
|
180
181
|
:host:disabled {
|
|
182
|
+
filter: var(--ui-icon-button-disabled-filter, var(--ui-disabled-filter));
|
|
181
183
|
opacity: var(--ui-icon-button-disabled-opacity, 1);
|
|
182
|
-
color: var(--ui-icon-button-disabled-text, var(--ui-disabled-text));
|
|
183
184
|
cursor: not-allowed;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
:host-state([variant="outline"]):disabled {
|
|
187
|
-
border-color: var(--ui-icon-button-disabled-border, var(--ui-border));
|
|
188
|
-
background-color: var(--ui-icon-button-disabled-surface, var(--ui-disabled-surface));
|
|
189
|
-
color: var(--ui-icon-button-disabled-text, var(--ui-disabled-text));
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
:host-state([variant="solid"]):disabled {
|
|
193
|
-
border-color: var(--ui-icon-button-disabled-border, var(--ui-border));
|
|
194
|
-
background-color: var(--ui-icon-button-disabled-surface, var(--ui-disabled-surface));
|
|
195
|
-
color: var(--ui-icon-button-disabled-text, var(--ui-disabled-text));
|
|
185
|
+
box-shadow: none;
|
|
196
186
|
}
|
|
197
187
|
|
|
198
188
|
:host {
|
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
</defs>
|
|
18
18
|
<label>
|
|
19
19
|
<input type="radio" $ref="input" .checked="internalChecked" :disabled="disabled" :required="required" :value="value" :name="name">
|
|
20
|
-
<span class="
|
|
20
|
+
<span class="text">
|
|
21
|
+
<span class="label"><slot></slot></span>
|
|
22
|
+
<span class="description" $ref="description" aria-hidden="true"><slot name="description"></slot></span>
|
|
23
|
+
</span>
|
|
21
24
|
</label>
|
|
22
25
|
<style>
|
|
23
26
|
:host {
|
|
@@ -29,11 +32,29 @@
|
|
|
29
32
|
cursor: pointer;
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
/* The label, and under it an optional line saying what the choice means. */
|
|
36
|
+
.text {
|
|
37
|
+
display: flex;
|
|
38
|
+
flex-direction: column;
|
|
39
|
+
gap: var(--ui-space-1);
|
|
40
|
+
min-inline-size: 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
32
43
|
.label {
|
|
33
44
|
min-inline-size: 0;
|
|
34
45
|
line-height: var(--ui-line-height-md);
|
|
35
46
|
}
|
|
36
47
|
|
|
48
|
+
.description {
|
|
49
|
+
color: var(--ui-text-secondary);
|
|
50
|
+
font-size: var(--ui-font-size-sm);
|
|
51
|
+
line-height: var(--ui-line-height);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.description:empty {
|
|
55
|
+
display: none;
|
|
56
|
+
}
|
|
57
|
+
|
|
37
58
|
input {
|
|
38
59
|
flex: 0 0 auto;
|
|
39
60
|
inline-size: var(--ui-radio-size, 1.125rem);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterFormReset } from "../shared/form-reset.js";
|
|
2
|
+
import { describeInput } from "../shared/describe.js";
|
|
2
3
|
import { trackTrigger } from "../shared/trigger.js";
|
|
3
4
|
|
|
4
5
|
// `checked` sets the control initially and whenever it changes; the user's changes update the state.
|
|
@@ -6,6 +7,7 @@ import { trackTrigger } from "../shared/trigger.js";
|
|
|
6
7
|
// the native default alone cannot hold it there.
|
|
7
8
|
export default function controller(host) {
|
|
8
9
|
const input = host.refs.input;
|
|
10
|
+
describeInput(input, host.refs.description);
|
|
9
11
|
const [trigger, stopTracking] = trackTrigger(host);
|
|
10
12
|
let external = host.state.checked;
|
|
11
13
|
host.state.internalChecked = Boolean(external);
|
|
@@ -130,8 +130,10 @@ export default function controller(host) {
|
|
|
130
130
|
if (!invoker || !element.id || invoker.getAttribute("commandfor") !== element.id) return;
|
|
131
131
|
toggle(triggerOf(invoker));
|
|
132
132
|
};
|
|
133
|
+
// The sidebar's own toggle event shares the popover's event name and bubbles; hearing it here would
|
|
134
|
+
// report it again, forever. Only the drawer's own popover ToggleEvent, which has a newState, counts.
|
|
133
135
|
const onPopoverToggle = (event) => {
|
|
134
|
-
if (!host.state.drawer) return;
|
|
136
|
+
if (!host.state.drawer || event.target !== element || typeof event.newState !== "string") return;
|
|
135
137
|
host.dispatch("toggle", { open: event.newState === "open", mode: "drawer", trigger: "programmatic" });
|
|
136
138
|
};
|
|
137
139
|
|
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
</defs>
|
|
15
15
|
<label>
|
|
16
16
|
<input type="checkbox" role="switch" $ref="input" .checked="internalChecked" :disabled="disabled" :required="required" :name="name" :value="value">
|
|
17
|
-
<span class="
|
|
17
|
+
<span class="text">
|
|
18
|
+
<span class="label"><slot></slot></span>
|
|
19
|
+
<span class="description" $ref="description" aria-hidden="true"><slot name="description"></slot></span>
|
|
20
|
+
</span>
|
|
18
21
|
</label>
|
|
19
22
|
<style>
|
|
20
23
|
:host {
|
|
@@ -26,11 +29,29 @@
|
|
|
26
29
|
cursor: pointer;
|
|
27
30
|
}
|
|
28
31
|
|
|
32
|
+
/* The label, and under it an optional line saying what the choice means. */
|
|
33
|
+
.text {
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-direction: column;
|
|
36
|
+
gap: var(--ui-space-1);
|
|
37
|
+
min-inline-size: 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
29
40
|
.label {
|
|
30
41
|
min-inline-size: 0;
|
|
31
42
|
line-height: var(--ui-line-height-md);
|
|
32
43
|
}
|
|
33
44
|
|
|
45
|
+
.description {
|
|
46
|
+
color: var(--ui-text-secondary);
|
|
47
|
+
font-size: var(--ui-font-size-sm);
|
|
48
|
+
line-height: var(--ui-line-height);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.description:empty {
|
|
52
|
+
display: none;
|
|
53
|
+
}
|
|
54
|
+
|
|
34
55
|
input {
|
|
35
56
|
appearance: none;
|
|
36
57
|
-webkit-appearance: none;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { afterFormReset } from "../shared/form-reset.js";
|
|
2
|
+
import { describeInput } from "../shared/describe.js";
|
|
2
3
|
import { trackTrigger } from "../shared/trigger.js";
|
|
3
4
|
|
|
4
5
|
// `checked` sets the control initially and whenever it changes; the user's changes update the state.
|
|
@@ -6,6 +7,7 @@ import { trackTrigger } from "../shared/trigger.js";
|
|
|
6
7
|
// the native default alone cannot hold it there.
|
|
7
8
|
export default function controller(host) {
|
|
8
9
|
const input = host.refs.input;
|
|
10
|
+
describeInput(input, host.refs.description);
|
|
9
11
|
const [trigger, stopTracking] = trackTrigger(host);
|
|
10
12
|
let external = host.state.checked;
|
|
11
13
|
host.state.internalChecked = Boolean(external);
|