@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadlabs/looma",
3
- "version": "0.25.0",
3
+ "version": "0.25.2",
4
4
  "type": "module",
5
5
  "description": "Looma components as HTML, Vue, and plain DOM, with design tokens.",
6
6
  "license": "MIT",
@@ -35,6 +35,17 @@
35
35
  --_ui-default-input-focus-outline: none;
36
36
  --_ui-default-input-radius: 0;
37
37
  --_ui-default-input-surface: transparent;
38
+ --_ui-default-textarea-border-width: 0;
39
+ --_ui-default-textarea-border: transparent;
40
+ --_ui-default-textarea-border-hover: transparent;
41
+ --_ui-default-textarea-focus-border: transparent;
42
+ --_ui-default-textarea-invalid-border: transparent;
43
+ --_ui-default-textarea-invalid-focus-shadow: none;
44
+ --_ui-default-textarea-shadow: none;
45
+ --_ui-default-textarea-focus-shadow: none;
46
+ --_ui-default-textarea-focus-outline: none;
47
+ --_ui-default-textarea-radius: 0;
48
+ --_ui-default-textarea-surface: transparent;
38
49
 
39
50
  display: flex;
40
51
  align-items: stretch;
@@ -68,13 +79,13 @@
68
79
  }
69
80
  }
70
81
 
71
- :scope:hover:not(:has(input:focus, input:disabled, input:read-only, [aria-invalid="true"], :is(:user-invalid, [data-user-invalid]))) {
82
+ :scope:hover:not(:has(:is(input, textarea):focus, :is(input, textarea):disabled, :is(input, textarea):read-only, [aria-invalid="true"], :is(:user-invalid, [data-user-invalid]))) {
72
83
  border-color: var(--ui-control-border-hover, var(--ui-text-secondary));
73
84
  }
74
85
 
75
86
  /* The ring shows when the field's input would show its own: when it is focus-visible, which a
76
87
  text input is for a pointer as well as a keyboard. A focused action button draws its own. */
77
- :scope:has(.field input:focus-visible) {
88
+ :scope:has(.field :is(input, textarea):focus-visible) {
78
89
  border-color: var(--ui-control-focus, var(--ui-accent-solid));
79
90
  box-shadow:
80
91
  var(--ui-control-inset),
@@ -85,7 +96,7 @@
85
96
  border-color: var(--ui-input-group-invalid-border, var(--ui-field-danger, var(--ui-danger-solid)));
86
97
  }
87
98
 
88
- :scope:has(input:is([aria-invalid="true"], :is(:user-invalid, [data-user-invalid])):focus-visible) {
99
+ :scope:has(:is(input, textarea):is([aria-invalid="true"], :is(:user-invalid, [data-user-invalid])):focus-visible) {
89
100
  box-shadow: var(
90
101
  --ui-input-group-invalid-focus-shadow,
91
102
  var(--ui-control-inset),
@@ -99,14 +110,14 @@
99
110
 
100
111
  /* Forced colors drop box shadows, so the focus ring becomes an outline. */
101
112
  @media (forced-colors: active) {
102
- :scope:has(input:focus-visible) {
113
+ :scope:has(:is(input, textarea):focus-visible) {
103
114
  outline: var(--ui-focus-width) solid Highlight;
104
115
  outline-offset: var(--ui-border-width);
105
116
  }
106
117
  }
107
118
 
108
119
  :scope[data-ui-input-group-state~="disabled"],
109
- :scope:has(input:disabled) {
120
+ :scope:has(:is(input, textarea):disabled) {
110
121
  background: var(--ui-control-surface-disabled, var(--ui-surface-muted));
111
122
  box-shadow: none;
112
123
  cursor: not-allowed;
@@ -161,6 +172,41 @@
161
172
  padding-inline: var(--ui-space-1);
162
173
  }
163
174
 
175
+ /* Multiline text owns the first row; its action remains inside the same frame below it. */
176
+ :scope[data-ui-input-group-state~="multiline"] {
177
+ display: grid;
178
+ grid-template-columns: auto minmax(0, 1fr) auto;
179
+ }
180
+
181
+ :scope[data-ui-input-group-state~="multiline"] .field {
182
+ grid-column: 2;
183
+ }
184
+
185
+ :scope[data-ui-input-group-state~="multiline"] .action {
186
+ grid-column: 1 / -1;
187
+ justify-content: end;
188
+ padding: var(--ui-space-2);
189
+ padding-block-start: 0;
190
+ }
191
+
192
+ /* The same textarea keeps its full resizeable box. Its text reserves the action's live width,
193
+ so a translated label or pending indicator cannot cover what the person types. */
194
+ :scope[data-ui-input-group-state~="multiline"][data-ui-input-group-state~="actionPosition=top-end"] {
195
+ --_ui-default-textarea-padding-inline-end: calc(var(--_ui-input-group-action-inline-size, 7rem) + var(--ui-space-3));
196
+ }
197
+
198
+ :scope[data-ui-input-group-state~="multiline"][data-ui-input-group-state~="actionPosition=top-end"] .field {
199
+ grid-row: 1;
200
+ }
201
+
202
+ :scope[data-ui-input-group-state~="multiline"][data-ui-input-group-state~="actionPosition=top-end"] .action {
203
+ grid-column: 2;
204
+ grid-row: 1;
205
+ align-self: start;
206
+ justify-self: end;
207
+ padding: var(--ui-space-2);
208
+ }
209
+
164
210
  .affix:empty,
165
211
  .action:empty {
166
212
  display: none;
@@ -198,5 +244,10 @@
198
244
  /* Centred rather than padded above and below: the group is exactly a lone Input's height, so a
199
245
  small button sits inside it with the difference split evenly, and the field keeps its height. */
200
246
 
247
+ /* Multiline text owns the first row; its action remains inside the same frame below it. */
248
+
249
+ /* The same textarea keeps its full resizeable box. Its text reserves the action's live width,
250
+ so a translated label or pending indicator cannot cover what the person types. */
251
+
201
252
  /* A component that sets its display still honours the hidden attribute on its root. */
202
253
  }
@@ -1,4 +1,28 @@
1
1
  /* Generated by HTML Next 1.0.0-alpha.29. Do not edit. */
2
+ @property --ui-textarea-border-width {
3
+ syntax: "*";
4
+ inherits: false;
5
+ }
6
+ @property --ui-textarea-border {
7
+ syntax: "*";
8
+ inherits: false;
9
+ }
10
+ @property --ui-textarea-border-hover {
11
+ syntax: "*";
12
+ inherits: false;
13
+ }
14
+ @property --ui-textarea-focus-border {
15
+ syntax: "*";
16
+ inherits: false;
17
+ }
18
+ @property --ui-textarea-focus-outline {
19
+ syntax: "*";
20
+ inherits: false;
21
+ }
22
+ @property --ui-textarea-surface {
23
+ syntax: "*";
24
+ inherits: false;
25
+ }
2
26
  @property --ui-textarea-focus-shadow {
3
27
  syntax: "*";
4
28
  inherits: false;
@@ -19,6 +43,10 @@
19
43
  syntax: "*";
20
44
  inherits: false;
21
45
  }
46
+ @property --ui-textarea-padding-inline-end {
47
+ syntax: "*";
48
+ inherits: false;
49
+ }
22
50
  @scope ([data-component~="ui-textarea"]) to ([data-component], [data-slotted]) {
23
51
  /* This component's hooks style the element they are set on, and never a nested one. */
24
52
 
@@ -31,15 +59,17 @@
31
59
  margin: 0;
32
60
  --_padding-block: var(--ui-space-2);
33
61
  padding: var(--ui-space-2) var(--ui-space-3);
62
+ padding-inline-end: var(--ui-textarea-padding-inline-end, var(--_ui-default-textarea-padding-inline-end, var(--ui-space-3)));
34
63
  appearance: none;
35
64
  resize: vertical;
36
- border: var(--ui-border-width) solid var(--ui-control-border, var(--ui-border-strong));
37
- border-radius: var(--ui-textarea-radius, var(--ui-field-radius, var(--ui-radius-md)));
38
- background: var(--ui-control-surface, var(--ui-surface-elevated));
65
+ border: var(--ui-textarea-border-width, var(--_ui-default-textarea-border-width, var(--ui-border-width))) solid
66
+ var(--ui-textarea-border, var(--_ui-default-textarea-border, var(--ui-control-border, var(--ui-border-strong))));
67
+ border-radius: var(--ui-textarea-radius, var(--_ui-default-textarea-radius, var(--ui-field-radius, var(--ui-radius-md))));
68
+ background: var(--ui-textarea-surface, var(--_ui-default-textarea-surface, var(--ui-control-surface, var(--ui-surface-elevated))));
39
69
  color: var(--ui-text-primary);
40
70
  font: inherit;
41
71
  line-height: var(--ui-line-height-md);
42
- box-shadow: var(--ui-textarea-shadow, var(--ui-control-inset));
72
+ box-shadow: var(--ui-textarea-shadow, var(--_ui-default-textarea-shadow, var(--ui-control-inset)));
43
73
  transition:
44
74
  border-color var(--ui-motion-fast) var(--ui-motion-ease),
45
75
  box-shadow var(--ui-motion-fast) var(--ui-motion-ease),
@@ -58,29 +88,32 @@
58
88
  }
59
89
 
60
90
  :scope:hover:not(:focus):not(:disabled):not(:read-only):not([aria-invalid="true"]):not(:is(:user-invalid, [data-user-invalid])) {
61
- border-color: var(--ui-control-border-hover, var(--ui-text-secondary));
91
+ border-color: var(--ui-textarea-border-hover, var(--_ui-default-textarea-border-hover, var(--ui-control-border-hover, var(--ui-text-secondary))));
62
92
  }
63
93
 
64
94
  :scope:focus-visible {
65
- border-color: var(--ui-control-focus, var(--ui-accent-solid));
66
- outline: none;
67
- box-shadow: var(--ui-textarea-focus-shadow, var(--ui-control-inset), var(--ui-control-focus-halo-shadow));
95
+ border-color: var(--ui-textarea-focus-border, var(--_ui-default-textarea-focus-border, var(--ui-control-focus, var(--ui-accent-solid))));
96
+ outline: var(--ui-textarea-focus-outline, var(--_ui-default-textarea-focus-outline, none));
97
+ box-shadow: var(--ui-textarea-focus-shadow, var(--_ui-default-textarea-focus-shadow, var(--ui-control-inset), var(--ui-control-focus-halo-shadow)));
68
98
  }
69
99
 
70
100
  /* The app's invalid flag, or the browser's own verdict once the user has left an invalid field. */
71
101
  :scope[aria-invalid="true"],
72
102
  :scope:is(:user-invalid, [data-user-invalid]) {
73
- border-color: var(--ui-textarea-invalid-border, var(--ui-field-danger, var(--ui-danger-solid)));
103
+ border-color: var(--ui-textarea-invalid-border, var(--_ui-default-textarea-invalid-border, var(--ui-field-danger, var(--ui-danger-solid))));
74
104
  }
75
105
 
76
106
  :scope:is([aria-invalid="true"], :is(:user-invalid, [data-user-invalid])):focus-visible {
77
107
  box-shadow: var(
78
108
  --ui-textarea-invalid-focus-shadow,
79
- var(--ui-control-inset),
80
- 0 0 0 calc(var(--ui-focus-width) * 1.5) color-mix(
81
- in srgb,
82
- var(--ui-textarea-invalid-border, var(--ui-field-danger, var(--ui-danger-solid))) 22%,
83
- transparent
109
+ var(
110
+ --_ui-default-textarea-invalid-focus-shadow,
111
+ var(--ui-control-inset),
112
+ 0 0 0 calc(var(--ui-focus-width) * 1.5) color-mix(
113
+ in srgb,
114
+ var(--ui-textarea-invalid-border, var(--_ui-default-textarea-invalid-border, var(--ui-field-danger, var(--ui-danger-solid)))) 22%,
115
+ transparent
116
+ )
84
117
  )
85
118
  );
86
119
  }
@@ -95,12 +128,14 @@
95
128
  :scope[data-ui-textarea-state~="size=sm"] {
96
129
  --_padding-block: var(--ui-space-1);
97
130
  padding: var(--ui-space-1) var(--ui-space-2);
131
+ padding-inline-end: var(--ui-textarea-padding-inline-end, var(--_ui-default-textarea-padding-inline-end, var(--ui-space-2)));
98
132
  font-size: var(--ui-font-size-sm);
99
133
  }
100
134
 
101
135
  :scope[data-ui-textarea-state~="size=lg"] {
102
136
  --_padding-block: var(--ui-space-3);
103
137
  padding: var(--ui-space-3) var(--ui-space-4);
138
+ padding-inline-end: var(--ui-textarea-padding-inline-end, var(--_ui-default-textarea-padding-inline-end, var(--ui-space-4)));
104
139
  font-size: var(--ui-font-size-md);
105
140
  }
106
141
 
@@ -6,7 +6,9 @@ export interface UiInputGroupElement extends HTMLDivElement {
6
6
  removeEventListener<K extends keyof UiInputGroupEventMap>(type: K, listener: (this: UiInputGroupElement, event: UiInputGroupEventMap[K]) => unknown, options?: boolean | EventListenerOptions): void;
7
7
  }
8
8
  export interface UiInputGroupProps {
9
+ actionPosition?: "below" | "top-end" | null;
9
10
  disabled?: boolean | null;
11
+ multiline?: boolean | null;
10
12
  prefixKind?: "text" | "icon" | null;
11
13
  attributes?: Readonly<Record<string, string | number | boolean | null | undefined>>;
12
14
  children?: readonly (string | Node)[];
@@ -3,7 +3,7 @@ import { manageComponentLifecycle } from "@nextwebwg/html-next/runtime";
3
3
  import * as controller from "../components/ui-input-group/ui-input-group.js";
4
4
  import "../styles/ui-input-group.css";
5
5
 
6
- const definition = {...{"contract":{"tag":"ui-input-group","props":{"disabled":{"type":"boolean","required":false,"target":{"attribute":"disabled"},"default":false},"prefixKind":{"type":{"kind":"terminal","name":"keyword"},"values":["text","icon"],"required":false,"target":{"attribute":"prefixkind"},"default":"text"}}},"template":{"kind":"element","name":"div","attributes":[],"children":[{"kind":"element","name":"span","attributes":[{"kind":"literal","name":"class","value":"affix"}],"children":[{"kind":"slot","fallback":[],"name":"prefix"}]},{"kind":"element","name":"span","attributes":[{"kind":"literal","name":"class","value":"field"}],"children":[{"kind":"slot"}]},{"kind":"element","name":"span","attributes":[{"kind":"literal","name":"class","value":"affix"}],"children":[{"kind":"slot","fallback":[],"name":"suffix"}]},{"kind":"element","name":"span","attributes":[{"kind":"literal","name":"class","value":"action"}],"children":[{"kind":"slot","fallback":[],"name":"action"}]}]},"declarations":[],"root":{"kind":"native","element":"div","choices":["div"]}},source:{file:import.meta.url},css:""};
6
+ const definition = {...{"contract":{"tag":"ui-input-group","props":{"actionPosition":{"type":{"kind":"terminal","name":"keyword"},"values":["below","top-end"],"required":false,"target":{"attribute":"actionposition"},"default":"below"},"disabled":{"type":"boolean","required":false,"target":{"attribute":"disabled"},"default":false},"multiline":{"type":"boolean","required":false,"target":{"attribute":"multiline"},"default":false},"prefixKind":{"type":{"kind":"terminal","name":"keyword"},"values":["text","icon"],"required":false,"target":{"attribute":"prefixkind"},"default":"text"}}},"template":{"kind":"element","name":"div","attributes":[],"children":[{"kind":"element","name":"span","attributes":[{"kind":"literal","name":"class","value":"affix"}],"children":[{"kind":"slot","fallback":[],"name":"prefix"}]},{"kind":"element","name":"span","attributes":[{"kind":"literal","name":"class","value":"field"}],"children":[{"kind":"slot"}]},{"kind":"element","name":"span","attributes":[{"kind":"literal","name":"class","value":"affix"}],"children":[{"kind":"slot","fallback":[],"name":"suffix"}]},{"kind":"element","name":"span","attributes":[{"kind":"literal","name":"class","value":"action"}],"children":[{"kind":"slot","fallback":[],"name":"action"}]}]},"declarations":[],"root":{"kind":"native","element":"div","choices":["div"]}},source:{file:import.meta.url},css:""};
7
7
 
8
8
  export function createUiInputGroup(options = {}) {
9
9
  const { attributes = {}, children = [], slots = {}, ...componentProps } = options;
@@ -10,25 +10,43 @@ type __VLS_Slots = {} & {
10
10
  action?: (props: typeof __VLS_7) => any;
11
11
  };
12
12
  declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
13
+ actionPosition: {
14
+ type: PropType<"below" | "top-end" | null>;
15
+ default: string;
16
+ };
13
17
  disabled: {
14
18
  type: PropType<boolean | null>;
15
19
  default: boolean;
16
20
  };
21
+ multiline: {
22
+ type: PropType<boolean | null>;
23
+ default: boolean;
24
+ };
17
25
  prefixKind: {
18
26
  type: PropType<"text" | "icon" | null>;
19
27
  default: string;
20
28
  };
21
29
  }>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
30
+ actionPosition: {
31
+ type: PropType<"below" | "top-end" | null>;
32
+ default: string;
33
+ };
22
34
  disabled: {
23
35
  type: PropType<boolean | null>;
24
36
  default: boolean;
25
37
  };
38
+ multiline: {
39
+ type: PropType<boolean | null>;
40
+ default: boolean;
41
+ };
26
42
  prefixKind: {
27
43
  type: PropType<"text" | "icon" | null>;
28
44
  default: string;
29
45
  };
30
46
  }>> & Readonly<{}>, {
31
47
  disabled: boolean | null;
48
+ multiline: boolean | null;
49
+ actionPosition: "top-end" | "below" | null;
32
50
  prefixKind: "text" | "icon" | null;
33
51
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
34
52
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -9,6 +9,15 @@ defineOptions({ inheritAttrs: false })
9
9
 
10
10
  const propValidityContract = {
11
11
  props: {
12
+ actionPosition: {
13
+ type: { kind: 'terminal', name: 'keyword' },
14
+ required: false,
15
+ target: { attribute: 'actionposition' },
16
+ description:
17
+ '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\n measured width.',
18
+ values: ['below', 'top-end'],
19
+ default: 'below',
20
+ },
12
21
  disabled: {
13
22
  type: { kind: 'terminal', name: 'boolean' },
14
23
  required: false,
@@ -17,6 +26,13 @@ const propValidityContract = {
17
26
  'Fades the group\'s framing to match a disabled input inside it. Disable the input itself too.',
18
27
  default: false,
19
28
  },
29
+ multiline: {
30
+ type: { kind: 'terminal', name: 'boolean' },
31
+ required: false,
32
+ target: { attribute: 'multiline' },
33
+ description: 'Groups a Textarea with an action below its text inside the shared frame.',
34
+ default: false,
35
+ },
20
36
  prefixKind: {
21
37
  type: { kind: 'terminal', name: 'keyword' },
22
38
  required: false,
@@ -29,11 +45,28 @@ const propValidityContract = {
29
45
  },
30
46
  } as const
31
47
  const props = defineProps({
48
+ actionPosition: {
49
+ type: null as unknown as PropType<'below' | 'top-end' | null>,
50
+ default: 'below',
51
+ },
32
52
  disabled: { type: null as unknown as PropType<boolean | null>, default: false },
53
+ multiline: { type: null as unknown as PropType<boolean | null>, default: false },
33
54
  prefixKind: { type: null as unknown as PropType<'text' | 'icon' | null>, default: 'text' },
34
55
  })
35
- const acceptedProps: Record<string, unknown> = { disabled: false, prefixKind: 'text' }
56
+ const acceptedProps: Record<string, unknown> = {
57
+ actionPosition: 'below',
58
+ disabled: false,
59
+ multiline: false,
60
+ prefixKind: 'text',
61
+ }
36
62
  const checkedProps = computed(() => ({
63
+ actionPosition: checkedProp<'below' | 'top-end'>(
64
+ props['actionPosition'],
65
+ { kind: 'terminal', name: 'keyword' },
66
+ false,
67
+ 'actionPosition',
68
+ acceptedProps,
69
+ ) as 'below' | 'top-end',
37
70
  disabled: checkedProp<boolean>(
38
71
  props['disabled'],
39
72
  { kind: 'terminal', name: 'boolean' },
@@ -41,6 +74,13 @@ const checkedProps = computed(() => ({
41
74
  'disabled',
42
75
  acceptedProps,
43
76
  ) as boolean,
77
+ multiline: checkedProp<boolean>(
78
+ props['multiline'],
79
+ { kind: 'terminal', name: 'boolean' },
80
+ false,
81
+ 'multiline',
82
+ acceptedProps,
83
+ ) as boolean,
44
84
  prefixKind: checkedProp<'text' | 'icon'>(
45
85
  props['prefixKind'],
46
86
  { kind: 'terminal', name: 'keyword' },
@@ -112,6 +152,10 @@ const hostState = computed(() =>
112
152
  checkedProps.value.prefixKind && 'prefixKind',
113
153
  checkedProps.value.prefixKind != null
114
154
  && `prefixKind=${encodeURIComponent(checkedProps.value.prefixKind)}`,
155
+ checkedProps.value.multiline && 'multiline',
156
+ checkedProps.value.actionPosition && 'actionPosition',
157
+ checkedProps.value.actionPosition != null
158
+ && `actionPosition=${encodeURIComponent(checkedProps.value.actionPosition)}`,
115
159
  ].filter(Boolean).join(' ')
116
160
  )
117
161
 
@@ -132,7 +176,7 @@ useComponentHost(() => import('../components/ui-input-group/ui-input-group.js'),
132
176
  ? (incoming[name] ?? incoming[kebab] ?? null)
133
177
  : null
134
178
  },
135
- propNames: ['disabled', 'prefixKind'],
179
+ propNames: ['actionPosition', 'disabled', 'multiline', 'prefixKind'],
136
180
  propValidity: (name: string) =>
137
181
  propValidityState({ contract: propValidityContract, values: { ...props } }, name),
138
182
  })
@@ -143,7 +187,9 @@ useComponentHost(() => import('../components/ui-input-group/ui-input-group.js'),
143
187
  data-component="ui-input-group"
144
188
  v-bind="nativeAttrs($attrs)"
145
189
  v-prop-validity="{ contract: propValidityContract, values: { ...props } }"
190
+ :data-action-position="reflectedProp('actionPosition', 'action-position', checkedProps.actionPosition, undefined, false)"
146
191
  :data-disabled="reflectedProp('disabled', 'disabled', checkedProps.disabled, undefined, false)"
192
+ :data-multiline="reflectedProp('multiline', 'multiline', checkedProps.multiline, undefined, false)"
147
193
  :data-prefix-kind="reflectedProp('prefixKind', 'prefix-kind', checkedProps.prefixKind, undefined, false)"
148
194
  :data-ui-input-group-state="hostState || undefined"
149
195
  ref="root"
@@ -191,6 +237,17 @@ useComponentHost(() => import('../components/ui-input-group/ui-input-group.js'),
191
237
  --_ui-default-input-focus-outline: none;
192
238
  --_ui-default-input-radius: 0;
193
239
  --_ui-default-input-surface: transparent;
240
+ --_ui-default-textarea-border-width: 0;
241
+ --_ui-default-textarea-border: transparent;
242
+ --_ui-default-textarea-border-hover: transparent;
243
+ --_ui-default-textarea-focus-border: transparent;
244
+ --_ui-default-textarea-invalid-border: transparent;
245
+ --_ui-default-textarea-invalid-focus-shadow: none;
246
+ --_ui-default-textarea-shadow: none;
247
+ --_ui-default-textarea-focus-shadow: none;
248
+ --_ui-default-textarea-focus-outline: none;
249
+ --_ui-default-textarea-radius: 0;
250
+ --_ui-default-textarea-surface: transparent;
194
251
 
195
252
  display: flex;
196
253
  align-items: stretch;
@@ -226,13 +283,13 @@ useComponentHost(() => import('../components/ui-input-group/ui-input-group.js'),
226
283
  }
227
284
  }
228
285
 
229
- [data-component~="ui-input-group"]:hover:not(:has(input:focus, input:disabled, input:read-only, [aria-invalid="true"], :is(:user-invalid, [data-user-invalid]))) {
286
+ [data-component~="ui-input-group"]:hover:not(:has(:is(input, textarea):focus, :is(input, textarea):disabled, :is(input, textarea):read-only, [aria-invalid="true"], :is(:user-invalid, [data-user-invalid]))) {
230
287
  border-color: var(--ui-control-border-hover, var(--ui-text-secondary));
231
288
  }
232
289
 
233
290
  /* The ring shows when the field's input would show its own: when it is focus-visible, which a
234
291
  text input is for a pointer as well as a keyboard. A focused action button draws its own. */
235
- [data-component~="ui-input-group"]:has(.field input:focus-visible) {
292
+ [data-component~="ui-input-group"]:has(.field :is(input, textarea):focus-visible) {
236
293
  border-color: var(--ui-control-focus, var(--ui-accent-solid));
237
294
  box-shadow:
238
295
  var(--ui-control-inset),
@@ -246,7 +303,7 @@ useComponentHost(() => import('../components/ui-input-group/ui-input-group.js'),
246
303
  );
247
304
  }
248
305
 
249
- [data-component~="ui-input-group"]:has(input:is([aria-invalid="true"], :is(:user-invalid, [data-user-invalid])):focus-visible) {
306
+ [data-component~="ui-input-group"]:has(:is(input, textarea):is([aria-invalid="true"], :is(:user-invalid, [data-user-invalid])):focus-visible) {
250
307
  box-shadow: var(
251
308
  --ui-input-group-invalid-focus-shadow,
252
309
  var(--ui-control-inset),
@@ -260,14 +317,14 @@ useComponentHost(() => import('../components/ui-input-group/ui-input-group.js'),
260
317
 
261
318
  /* Forced colors drop box shadows, so the focus ring becomes an outline. */
262
319
  @media (forced-colors: active) {
263
- [data-component~="ui-input-group"]:has(input:focus-visible) {
320
+ [data-component~="ui-input-group"]:has(:is(input, textarea):focus-visible) {
264
321
  outline: var(--ui-focus-width) solid Highlight;
265
322
  outline-offset: var(--ui-border-width);
266
323
  }
267
324
  }
268
325
 
269
326
  [data-component~="ui-input-group"][data-ui-input-group-state~="disabled"],
270
- [data-component~="ui-input-group"]:has(input:disabled) {
327
+ [data-component~="ui-input-group"]:has(:is(input, textarea):disabled) {
271
328
  background: var(--ui-control-surface-disabled, var(--ui-surface-muted));
272
329
  box-shadow: none;
273
330
  cursor: not-allowed;
@@ -323,6 +380,45 @@ useComponentHost(() => import('../components/ui-input-group/ui-input-group.js'),
323
380
  padding-inline: var(--ui-space-1);
324
381
  }
325
382
 
383
+ /* Multiline text owns the first row; its action remains inside the same frame below it. */
384
+ [data-component~="ui-input-group"][data-ui-input-group-state~="multiline"] {
385
+ display: grid;
386
+ grid-template-columns: auto minmax(0, 1fr) auto;
387
+ }
388
+
389
+ [data-component~="ui-input-group"][data-ui-input-group-state~="multiline"] .field {
390
+ grid-column: 2;
391
+ }
392
+
393
+ [data-component~="ui-input-group"][data-ui-input-group-state~="multiline"] .action {
394
+ grid-column: 1 / -1;
395
+ justify-content: end;
396
+ padding: var(--ui-space-2);
397
+ padding-block-start: 0;
398
+ }
399
+
400
+ /* The same textarea keeps its full resizeable box. Its text reserves the action's live width,
401
+ so a translated label or pending indicator cannot cover what the person types. */
402
+ [data-component~="ui-input-group"][data-ui-input-group-state~="multiline"][data-ui-input-group-state~="actionPosition=top-end"] {
403
+ --_ui-default-textarea-padding-inline-end: calc(
404
+ var(--_ui-input-group-action-inline-size, 7rem) + var(--ui-space-3)
405
+ );
406
+ }
407
+
408
+ [data-component~="ui-input-group"][data-ui-input-group-state~="multiline"][data-ui-input-group-state~="actionPosition=top-end"]
409
+ .field {
410
+ grid-row: 1;
411
+ }
412
+
413
+ [data-component~="ui-input-group"][data-ui-input-group-state~="multiline"][data-ui-input-group-state~="actionPosition=top-end"]
414
+ .action {
415
+ grid-column: 2;
416
+ grid-row: 1;
417
+ align-self: start;
418
+ justify-self: end;
419
+ padding: var(--ui-space-2);
420
+ }
421
+
326
422
  .affix:empty,
327
423
  .action:empty {
328
424
  display: none;