@threadlabs/looma 0.25.0 → 0.25.2
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/ui-input-group/ui-input-group.html +55 -5
- package/components/ui-input-group/ui-input-group.js +19 -2
- package/components/ui-textarea/ui-textarea.html +49 -14
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/styles/ui-input-group.css +56 -5
- package/styles/ui-textarea.css +49 -14
- package/vanilla/UiInputGroup.d.ts +2 -0
- package/vanilla/UiInputGroup.js +1 -1
- package/vue/UiInputGroup.d.ts +18 -0
- package/vue/UiInputGroup.vue +103 -7
- package/vue/UiTextarea.vue +92 -15
- package/vue/chunks/UiInputGroup.js +54 -3
- package/vue/chunks/UiTextarea.js +1 -1
- package/vue/components.css +175 -52
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
<template component="ui-input-group" controller="./ui-input-group.js">
|
|
2
2
|
<defs>
|
|
3
|
+
<prop name="multiline" type="boolean" default="false">Groups a Textarea with an action below its text inside the shared frame.</prop>
|
|
4
|
+
<prop name="actionPosition" type="keyword" values="below, top-end" default="below"
|
|
5
|
+
>In a multiline group, below keeps the action under the text. top-end places it inside the textarea at its upper end and reserves text padding for its
|
|
6
|
+
measured width.</prop>
|
|
3
7
|
<prop name="prefixKind" type="keyword" values="text, icon" default="text"
|
|
4
8
|
>Text affixes have their own framing. An icon prefix shares the field surface and uses one compact icon-to-value gap.</prop>
|
|
5
9
|
<prop name="disabled" type="boolean" default="false">Fades the group's framing to match a disabled input inside it. Disable the input itself too.</prop>
|
|
@@ -46,6 +50,17 @@
|
|
|
46
50
|
--_ui-default-input-focus-outline: none;
|
|
47
51
|
--_ui-default-input-radius: 0;
|
|
48
52
|
--_ui-default-input-surface: transparent;
|
|
53
|
+
--_ui-default-textarea-border-width: 0;
|
|
54
|
+
--_ui-default-textarea-border: transparent;
|
|
55
|
+
--_ui-default-textarea-border-hover: transparent;
|
|
56
|
+
--_ui-default-textarea-focus-border: transparent;
|
|
57
|
+
--_ui-default-textarea-invalid-border: transparent;
|
|
58
|
+
--_ui-default-textarea-invalid-focus-shadow: none;
|
|
59
|
+
--_ui-default-textarea-shadow: none;
|
|
60
|
+
--_ui-default-textarea-focus-shadow: none;
|
|
61
|
+
--_ui-default-textarea-focus-outline: none;
|
|
62
|
+
--_ui-default-textarea-radius: 0;
|
|
63
|
+
--_ui-default-textarea-surface: transparent;
|
|
49
64
|
|
|
50
65
|
display: flex;
|
|
51
66
|
align-items: stretch;
|
|
@@ -79,13 +94,13 @@
|
|
|
79
94
|
}
|
|
80
95
|
}
|
|
81
96
|
|
|
82
|
-
:host:hover:not(:has(input:focus, input:disabled, input:read-only, [aria-invalid="true"], :user-invalid)) {
|
|
97
|
+
:host:hover:not(:has(:is(input, textarea):focus, :is(input, textarea):disabled, :is(input, textarea):read-only, [aria-invalid="true"], :user-invalid)) {
|
|
83
98
|
border-color: var(--ui-control-border-hover, var(--ui-text-secondary));
|
|
84
99
|
}
|
|
85
100
|
|
|
86
101
|
/* The ring shows when the field's input would show its own: when it is focus-visible, which a
|
|
87
102
|
text input is for a pointer as well as a keyboard. A focused action button draws its own. */
|
|
88
|
-
:host:has(.field input:focus-visible) {
|
|
103
|
+
:host:has(.field :is(input, textarea):focus-visible) {
|
|
89
104
|
border-color: var(--ui-control-focus, var(--ui-accent-solid));
|
|
90
105
|
box-shadow:
|
|
91
106
|
var(--ui-control-inset),
|
|
@@ -96,7 +111,7 @@
|
|
|
96
111
|
border-color: var(--ui-input-group-invalid-border, var(--ui-field-danger, var(--ui-danger-solid)));
|
|
97
112
|
}
|
|
98
113
|
|
|
99
|
-
:host:has(input:is([aria-invalid="true"], :user-invalid):focus-visible) {
|
|
114
|
+
:host:has(:is(input, textarea):is([aria-invalid="true"], :user-invalid):focus-visible) {
|
|
100
115
|
box-shadow: var(
|
|
101
116
|
--ui-input-group-invalid-focus-shadow,
|
|
102
117
|
var(--ui-control-inset),
|
|
@@ -110,14 +125,14 @@
|
|
|
110
125
|
|
|
111
126
|
/* Forced colors drop box shadows, so the focus ring becomes an outline. */
|
|
112
127
|
@media (forced-colors: active) {
|
|
113
|
-
:host:has(input:focus-visible) {
|
|
128
|
+
:host:has(:is(input, textarea):focus-visible) {
|
|
114
129
|
outline: var(--ui-focus-width) solid Highlight;
|
|
115
130
|
outline-offset: var(--ui-border-width);
|
|
116
131
|
}
|
|
117
132
|
}
|
|
118
133
|
|
|
119
134
|
:host([disabled]),
|
|
120
|
-
:host:has(input:disabled) {
|
|
135
|
+
:host:has(:is(input, textarea):disabled) {
|
|
121
136
|
background: var(--ui-control-surface-disabled, var(--ui-surface-muted));
|
|
122
137
|
box-shadow: none;
|
|
123
138
|
cursor: not-allowed;
|
|
@@ -172,6 +187,41 @@
|
|
|
172
187
|
padding-inline: var(--ui-space-1);
|
|
173
188
|
}
|
|
174
189
|
|
|
190
|
+
/* Multiline text owns the first row; its action remains inside the same frame below it. */
|
|
191
|
+
:host([multiline]) {
|
|
192
|
+
display: grid;
|
|
193
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
:host([multiline]) .field {
|
|
197
|
+
grid-column: 2;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
:host([multiline]) .action {
|
|
201
|
+
grid-column: 1 / -1;
|
|
202
|
+
justify-content: end;
|
|
203
|
+
padding: var(--ui-space-2);
|
|
204
|
+
padding-block-start: 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* The same textarea keeps its full resizeable box. Its text reserves the action's live width,
|
|
208
|
+
so a translated label or pending indicator cannot cover what the person types. */
|
|
209
|
+
:host([multiline][actionPosition="top-end"]) {
|
|
210
|
+
--_ui-default-textarea-padding-inline-end: calc(var(--_ui-input-group-action-inline-size, 7rem) + var(--ui-space-3));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
:host([multiline][actionPosition="top-end"]) .field {
|
|
214
|
+
grid-row: 1;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
:host([multiline][actionPosition="top-end"]) .action {
|
|
218
|
+
grid-column: 2;
|
|
219
|
+
grid-row: 1;
|
|
220
|
+
align-self: start;
|
|
221
|
+
justify-self: end;
|
|
222
|
+
padding: var(--ui-space-2);
|
|
223
|
+
}
|
|
224
|
+
|
|
175
225
|
.affix:empty,
|
|
176
226
|
.action:empty {
|
|
177
227
|
display: none;
|
|
@@ -20,8 +20,23 @@ function connect(host) {
|
|
|
20
20
|
let owned = [];
|
|
21
21
|
let described = null;
|
|
22
22
|
const warned = new Set();
|
|
23
|
+
let action = null;
|
|
24
|
+
const measureAction = () => {
|
|
25
|
+
const width = action?.getBoundingClientRect().width ?? 0;
|
|
26
|
+
element.style.setProperty("--_ui-input-group-action-inline-size", `${width}px`);
|
|
27
|
+
};
|
|
28
|
+
// Observe the actual slot surface rather than estimating a label's width. This also catches
|
|
29
|
+
// pending indicators, font loading, touch target sizing and translated labels.
|
|
30
|
+
const actionObserver = new ResizeObserver(measureAction);
|
|
23
31
|
const wire = () => {
|
|
24
|
-
const
|
|
32
|
+
const nextAction = element.querySelector(":scope > .action");
|
|
33
|
+
if (action !== nextAction) {
|
|
34
|
+
if (action) actionObserver.unobserve(action);
|
|
35
|
+
action = nextAction;
|
|
36
|
+
if (action) actionObserver.observe(action);
|
|
37
|
+
}
|
|
38
|
+
measureAction();
|
|
39
|
+
const input = element.querySelector(".field :is(input, textarea)");
|
|
25
40
|
const affixes = Array.from(element.querySelectorAll(":scope > .affix")).filter((affix) => {
|
|
26
41
|
const text = affix.textContent.trim() !== "" && !affix.querySelector(INTERACTIVE);
|
|
27
42
|
if (text) affix.setAttribute("aria-hidden", "true");
|
|
@@ -50,7 +65,7 @@ function connect(host) {
|
|
|
50
65
|
};
|
|
51
66
|
// Click, not pointerdown: a tap's click is a user activation, so a touch keyboard opens.
|
|
52
67
|
const onClick = (event) => {
|
|
53
|
-
if (pressedAround(event) && !host.props.disabled.value) element.querySelector(".field input")?.focus();
|
|
68
|
+
if (pressedAround(event) && !host.props.disabled.value) element.querySelector(".field :is(input, textarea)")?.focus();
|
|
54
69
|
};
|
|
55
70
|
element.addEventListener("mousedown", onMousedown);
|
|
56
71
|
element.addEventListener("click", onClick);
|
|
@@ -61,6 +76,8 @@ function connect(host) {
|
|
|
61
76
|
wire();
|
|
62
77
|
return () => {
|
|
63
78
|
observer.disconnect();
|
|
79
|
+
actionObserver.disconnect();
|
|
80
|
+
element.style.removeProperty("--_ui-input-group-action-inline-size");
|
|
64
81
|
element.removeEventListener("mousedown", onMousedown);
|
|
65
82
|
element.removeEventListener("click", onClick);
|
|
66
83
|
};
|
|
@@ -26,6 +26,30 @@
|
|
|
26
26
|
></textarea>
|
|
27
27
|
<style>
|
|
28
28
|
/* This component's hooks style the element they are set on, and never a nested one. */
|
|
29
|
+
@property --ui-textarea-border-width {
|
|
30
|
+
syntax: "*";
|
|
31
|
+
inherits: false;
|
|
32
|
+
}
|
|
33
|
+
@property --ui-textarea-border {
|
|
34
|
+
syntax: "*";
|
|
35
|
+
inherits: false;
|
|
36
|
+
}
|
|
37
|
+
@property --ui-textarea-border-hover {
|
|
38
|
+
syntax: "*";
|
|
39
|
+
inherits: false;
|
|
40
|
+
}
|
|
41
|
+
@property --ui-textarea-focus-border {
|
|
42
|
+
syntax: "*";
|
|
43
|
+
inherits: false;
|
|
44
|
+
}
|
|
45
|
+
@property --ui-textarea-focus-outline {
|
|
46
|
+
syntax: "*";
|
|
47
|
+
inherits: false;
|
|
48
|
+
}
|
|
49
|
+
@property --ui-textarea-surface {
|
|
50
|
+
syntax: "*";
|
|
51
|
+
inherits: false;
|
|
52
|
+
}
|
|
29
53
|
@property --ui-textarea-focus-shadow {
|
|
30
54
|
syntax: "*";
|
|
31
55
|
inherits: false;
|
|
@@ -46,6 +70,10 @@
|
|
|
46
70
|
syntax: "*";
|
|
47
71
|
inherits: false;
|
|
48
72
|
}
|
|
73
|
+
@property --ui-textarea-padding-inline-end {
|
|
74
|
+
syntax: "*";
|
|
75
|
+
inherits: false;
|
|
76
|
+
}
|
|
49
77
|
|
|
50
78
|
:host {
|
|
51
79
|
box-sizing: border-box;
|
|
@@ -56,15 +84,17 @@
|
|
|
56
84
|
margin: 0;
|
|
57
85
|
--_padding-block: var(--ui-space-2);
|
|
58
86
|
padding: var(--ui-space-2) var(--ui-space-3);
|
|
87
|
+
padding-inline-end: var(--ui-textarea-padding-inline-end, var(--_ui-default-textarea-padding-inline-end, var(--ui-space-3)));
|
|
59
88
|
appearance: none;
|
|
60
89
|
resize: vertical;
|
|
61
|
-
border: var(--ui-border-width
|
|
62
|
-
|
|
63
|
-
|
|
90
|
+
border: var(--ui-textarea-border-width, var(--_ui-default-textarea-border-width, var(--ui-border-width))) solid
|
|
91
|
+
var(--ui-textarea-border, var(--_ui-default-textarea-border, var(--ui-control-border, var(--ui-border-strong))));
|
|
92
|
+
border-radius: var(--ui-textarea-radius, var(--_ui-default-textarea-radius, var(--ui-field-radius, var(--ui-radius-md))));
|
|
93
|
+
background: var(--ui-textarea-surface, var(--_ui-default-textarea-surface, var(--ui-control-surface, var(--ui-surface-elevated))));
|
|
64
94
|
color: var(--ui-text-primary);
|
|
65
95
|
font: inherit;
|
|
66
96
|
line-height: var(--ui-line-height-md);
|
|
67
|
-
box-shadow: var(--ui-textarea-shadow, var(--ui-control-inset));
|
|
97
|
+
box-shadow: var(--ui-textarea-shadow, var(--_ui-default-textarea-shadow, var(--ui-control-inset)));
|
|
68
98
|
transition:
|
|
69
99
|
border-color var(--ui-motion-fast) var(--ui-motion-ease),
|
|
70
100
|
box-shadow var(--ui-motion-fast) var(--ui-motion-ease),
|
|
@@ -83,29 +113,32 @@
|
|
|
83
113
|
}
|
|
84
114
|
|
|
85
115
|
:host:hover:not(:focus):not(:disabled):not(:read-only):not([aria-invalid="true"]):not(:user-invalid) {
|
|
86
|
-
border-color: var(--ui-control-border-hover, var(--ui-text-secondary));
|
|
116
|
+
border-color: var(--ui-textarea-border-hover, var(--_ui-default-textarea-border-hover, var(--ui-control-border-hover, var(--ui-text-secondary))));
|
|
87
117
|
}
|
|
88
118
|
|
|
89
119
|
:host:focus-visible {
|
|
90
|
-
border-color: var(--ui-control-focus, var(--ui-accent-solid));
|
|
91
|
-
outline: none;
|
|
92
|
-
box-shadow: var(--ui-textarea-focus-shadow, var(--ui-control-inset), var(--ui-control-focus-halo-shadow));
|
|
120
|
+
border-color: var(--ui-textarea-focus-border, var(--_ui-default-textarea-focus-border, var(--ui-control-focus, var(--ui-accent-solid))));
|
|
121
|
+
outline: var(--ui-textarea-focus-outline, var(--_ui-default-textarea-focus-outline, none));
|
|
122
|
+
box-shadow: var(--ui-textarea-focus-shadow, var(--_ui-default-textarea-focus-shadow, var(--ui-control-inset), var(--ui-control-focus-halo-shadow)));
|
|
93
123
|
}
|
|
94
124
|
|
|
95
125
|
/* The app's invalid flag, or the browser's own verdict once the user has left an invalid field. */
|
|
96
126
|
:host[aria-invalid="true"],
|
|
97
127
|
:host:user-invalid {
|
|
98
|
-
border-color: var(--ui-textarea-invalid-border, var(--ui-field-danger, var(--ui-danger-solid)));
|
|
128
|
+
border-color: var(--ui-textarea-invalid-border, var(--_ui-default-textarea-invalid-border, var(--ui-field-danger, var(--ui-danger-solid))));
|
|
99
129
|
}
|
|
100
130
|
|
|
101
131
|
:host:is([aria-invalid="true"], :user-invalid):focus-visible {
|
|
102
132
|
box-shadow: var(
|
|
103
133
|
--ui-textarea-invalid-focus-shadow,
|
|
104
|
-
var(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
134
|
+
var(
|
|
135
|
+
--_ui-default-textarea-invalid-focus-shadow,
|
|
136
|
+
var(--ui-control-inset),
|
|
137
|
+
0 0 0 calc(var(--ui-focus-width) * 1.5) color-mix(
|
|
138
|
+
in srgb,
|
|
139
|
+
var(--ui-textarea-invalid-border, var(--_ui-default-textarea-invalid-border, var(--ui-field-danger, var(--ui-danger-solid)))) 22%,
|
|
140
|
+
transparent
|
|
141
|
+
)
|
|
109
142
|
)
|
|
110
143
|
);
|
|
111
144
|
}
|
|
@@ -120,12 +153,14 @@
|
|
|
120
153
|
:host([size="sm"]) {
|
|
121
154
|
--_padding-block: var(--ui-space-1);
|
|
122
155
|
padding: var(--ui-space-1) var(--ui-space-2);
|
|
156
|
+
padding-inline-end: var(--ui-textarea-padding-inline-end, var(--_ui-default-textarea-padding-inline-end, var(--ui-space-2)));
|
|
123
157
|
font-size: var(--ui-font-size-sm);
|
|
124
158
|
}
|
|
125
159
|
|
|
126
160
|
:host([size="lg"]) {
|
|
127
161
|
--_padding-block: var(--ui-space-3);
|
|
128
162
|
padding: var(--ui-space-3) var(--ui-space-4);
|
|
163
|
+
padding-inline-end: var(--ui-textarea-padding-inline-end, var(--_ui-default-textarea-padding-inline-end, var(--ui-space-4)));
|
|
129
164
|
font-size: var(--ui-font-size-md);
|
|
130
165
|
}
|
|
131
166
|
|