@threadlabs/looma 0.13.6 → 0.14.1

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 (80) hide show
  1. package/components/ui-button/ui-button.html +2 -1
  2. package/components/ui-checkbox/ui-checkbox.html +35 -0
  3. package/components/ui-combobox/ui-combobox.html +38 -3
  4. package/components/ui-description-list/ui-description-list.html +54 -9
  5. package/components/ui-input/ui-input.html +27 -0
  6. package/components/ui-input-group/ui-input-group.html +15 -0
  7. package/components/ui-radio/ui-radio.html +35 -0
  8. package/components/ui-select/ui-select.html +29 -3
  9. package/components/ui-switch/ui-switch.html +36 -0
  10. package/components/ui-table/ui-table.html +131 -0
  11. package/components/ui-table/ui-table.js +55 -0
  12. package/components/ui-textarea/ui-textarea.html +20 -0
  13. package/dist/index.js +8 -7
  14. package/package.json +1 -1
  15. package/styles/ui-button.css +3 -1
  16. package/styles/ui-checkbox.css +38 -0
  17. package/styles/ui-combobox.css +38 -1
  18. package/styles/ui-description-list.css +52 -7
  19. package/styles/ui-input-group.css +20 -0
  20. package/styles/ui-input.css +29 -0
  21. package/styles/ui-radio.css +38 -0
  22. package/styles/ui-select.css +29 -3
  23. package/styles/ui-switch.css +41 -0
  24. package/styles/ui-table.css +134 -0
  25. package/styles/ui-textarea.css +21 -0
  26. package/vanilla/UiCheckbox.d.ts +1 -0
  27. package/vanilla/UiCheckbox.js +1 -1
  28. package/vanilla/UiCombobox.d.ts +1 -1
  29. package/vanilla/UiCombobox.js +1 -1
  30. package/vanilla/UiDescriptionList.d.ts +3 -1
  31. package/vanilla/UiDescriptionList.js +6 -2
  32. package/vanilla/UiInput.d.ts +1 -0
  33. package/vanilla/UiInput.js +1 -1
  34. package/vanilla/UiRadio.d.ts +1 -0
  35. package/vanilla/UiRadio.js +1 -1
  36. package/vanilla/UiSelect.d.ts +1 -0
  37. package/vanilla/UiSelect.js +1 -1
  38. package/vanilla/UiSwitch.d.ts +1 -0
  39. package/vanilla/UiSwitch.js +1 -1
  40. package/vanilla/UiTable.d.ts +15 -0
  41. package/vanilla/UiTable.js +30 -0
  42. package/vanilla/UiTextarea.d.ts +1 -0
  43. package/vanilla/UiTextarea.js +1 -1
  44. package/vanilla/index.js +1 -0
  45. package/vue/UiButton.vue +2 -1
  46. package/vue/UiCheckbox.d.ts +2 -0
  47. package/vue/UiCheckbox.vue +34 -0
  48. package/vue/UiCombobox.d.ts +2 -2
  49. package/vue/UiCombobox.vue +39 -2
  50. package/vue/UiDescriptionList.d.ts +5 -2
  51. package/vue/UiDescriptionList.vue +51 -7
  52. package/vue/UiInput.d.ts +2 -0
  53. package/vue/UiInput.vue +41 -0
  54. package/vue/UiInputGroup.vue +15 -0
  55. package/vue/UiList.d.ts +1 -1
  56. package/vue/UiRadio.d.ts +2 -0
  57. package/vue/UiRadio.vue +34 -0
  58. package/vue/UiSelect.d.ts +2 -0
  59. package/vue/UiSelect.vue +34 -3
  60. package/vue/UiSwitch.d.ts +2 -0
  61. package/vue/UiSwitch.vue +35 -0
  62. package/vue/UiTable.d.ts +20 -0
  63. package/vue/UiTable.js +2 -0
  64. package/vue/UiTable.vue +172 -0
  65. package/vue/UiTextarea.d.ts +2 -0
  66. package/vue/UiTextarea.vue +27 -0
  67. package/vue/chunks/UiButton.js +1 -1
  68. package/vue/chunks/UiCheckbox.js +3 -2
  69. package/vue/chunks/UiCombobox.js +1 -1
  70. package/vue/chunks/UiDescriptionList.js +11 -3
  71. package/vue/chunks/UiInput.js +7 -2
  72. package/vue/chunks/UiInputGroup.js +1 -1
  73. package/vue/chunks/UiRadio.js +3 -2
  74. package/vue/chunks/UiSelect.js +7 -2
  75. package/vue/chunks/UiSwitch.js +3 -2
  76. package/vue/chunks/UiTable.js +40 -0
  77. package/vue/chunks/UiTextarea.js +7 -2
  78. package/vue/components.css +522 -174
  79. package/vue/index.d.ts +1 -0
  80. package/vue/index.js +2 -1
package/vanilla/index.js CHANGED
@@ -55,6 +55,7 @@ export * from "./UiStack.js";
55
55
  export * from "./UiStatusMessage.js";
56
56
  export * from "./UiSwitch.js";
57
57
  export * from "./UiSwitcher.js";
58
+ export * from "./UiTable.js";
58
59
  export * from "./UiTabs.js";
59
60
  export * from "./UiText.js";
60
61
  export * from "./UiTextarea.js";
package/vue/UiButton.vue CHANGED
@@ -357,8 +357,9 @@ function attribute(value: unknown): any {
357
357
  border-color: transparent;
358
358
  }
359
359
 
360
+ /* The size states outrank :host, so sm is named too, or a small button would stay small. */
360
361
  @media (pointer: coarse) {
361
- [data-component~="ui-button"] {
362
+ [data-component~="ui-button"], [data-component~="ui-button"][data-ui-button-state~="size=sm"] {
362
363
  min-block-size: var(--ui-control-min-block-size);
363
364
  }
364
365
  }
@@ -4,6 +4,7 @@ type __VLS_Props = {
4
4
  indeterminate?: boolean;
5
5
  name?: string;
6
6
  required?: boolean;
7
+ size?: 'sm' | 'md' | 'lg';
7
8
  value?: string;
8
9
  };
9
10
  declare var __VLS_1: {}, __VLS_3: {};
@@ -30,6 +31,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
30
31
  "onUpdate:value"?: ((value: string) => any) | undefined;
31
32
  }>, {
32
33
  name: string;
34
+ size: "sm" | "md" | "lg";
33
35
  value: string;
34
36
  disabled: boolean;
35
37
  checked: boolean;
@@ -13,6 +13,7 @@ const props = withDefaults(
13
13
  indeterminate?: boolean
14
14
  name?: string
15
15
  required?: boolean
16
+ size?: 'sm' | 'md' | 'lg'
16
17
  value?: string
17
18
  }>(),
18
19
  {
@@ -21,6 +22,7 @@ const props = withDefaults(
21
22
  indeterminate: false,
22
23
  name: '',
23
24
  required: false,
25
+ size: 'md',
24
26
  value: 'on',
25
27
  },
26
28
  )
@@ -45,6 +47,7 @@ const internalChecked = ref(false)
45
47
  const hostState = computed(() =>
46
48
  [
47
49
  props.disabled && 'disabled',
50
+ props.size && `size size=${props.size}`,
48
51
  ].filter(Boolean).join(' ')
49
52
  )
50
53
 
@@ -215,6 +218,37 @@ input:focus-visible {
215
218
  cursor: not-allowed;
216
219
  }
217
220
 
221
+ /* A sized choice keeps its box and aligns instead: the label takes that size's field type size,
222
+ and the first line centres in that control height, so the label shares a row's baseline.
223
+ Multi-line labels and descriptions still grow downward from that first line. */
224
+ [data-component~="ui-checkbox"][data-ui-checkbox-state~="size=sm"] {
225
+ --_font: var(--ui-font-size-sm);
226
+ --_row: var(--ui-control-size-sm);
227
+ }
228
+
229
+ [data-component~="ui-checkbox"][data-ui-checkbox-state~="size=lg"] {
230
+ --_font: var(--ui-font-size-md);
231
+ --_row: var(--ui-control-size-lg);
232
+ }
233
+
234
+ [data-component~="ui-checkbox"][data-ui-checkbox-state~="size=sm"],
235
+ [data-component~="ui-checkbox"][data-ui-checkbox-state~="size=lg"] {
236
+ --_line: calc(var(--_font) * var(--ui-line-height-md));
237
+ font-size: var(--_font);
238
+ padding-block: calc((var(--_row) - var(--_line)) / 2);
239
+ }
240
+
241
+ [data-component~="ui-checkbox"][data-ui-checkbox-state~="size=sm"] input,
242
+ [data-component~="ui-checkbox"][data-ui-checkbox-state~="size=lg"] input {
243
+ margin-top: calc((var(--_line) - var(--_ui-checkbox-size, 1.125rem)) / 2);
244
+ }
245
+
246
+ @media (pointer: coarse) {
247
+ [data-component~="ui-checkbox"][data-ui-checkbox-state~="size=sm"] {
248
+ --_row: var(--ui-control-min-block-size);
249
+ }
250
+ }
251
+
218
252
  /* A component that sets its display still honours the hidden attribute on its root. */
219
253
  [data-component~="ui-checkbox"][hidden] {
220
254
  display: none;
@@ -21,7 +21,7 @@ type __VLS_Props = {
21
21
  readonly?: boolean;
22
22
  required?: boolean;
23
23
  selectedValues?: readonly (string)[];
24
- size?: 'sm' | 'md';
24
+ size?: 'sm' | 'md' | 'lg';
25
25
  tokenSeparators?: readonly (string)[];
26
26
  value?: string | null;
27
27
  };
@@ -247,7 +247,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
247
247
  }>, {
248
248
  label: string;
249
249
  name: string;
250
- size: "sm" | "md";
250
+ size: "sm" | "md" | "lg";
251
251
  disabled: boolean;
252
252
  required: boolean;
253
253
  allowCreate: boolean;
@@ -33,7 +33,7 @@ const props = withDefaults(
33
33
  readonly?: boolean
34
34
  required?: boolean
35
35
  selectedValues?: readonly (string)[]
36
- size?: 'sm' | 'md'
36
+ size?: 'sm' | 'md' | 'lg'
37
37
  tokenSeparators?: readonly (string)[]
38
38
  value?: string | null
39
39
  }>(),
@@ -834,9 +834,18 @@ input {
834
834
  margin: 0;
835
835
  outline: none;
836
836
  }
837
+ [data-component~="ui-combobox"][data-ui-combobox-state~="size=sm"] {
838
+ --_row: var(--ui-control-size-sm);
839
+ }
837
840
  [data-component~="ui-combobox"][data-ui-combobox-state~="size=sm"] input {
838
- min-block-size: calc(var(--ui-control-size-sm) - 2px);
841
+ min-block-size: calc(var(--_row) - 2px);
839
842
  padding: var(--ui-space-1) var(--ui-space-2);
843
+ font-size: var(--ui-font-size-sm);
844
+ }
845
+ [data-component~="ui-combobox"][data-ui-combobox-state~="size=lg"] input {
846
+ min-block-size: calc(var(--ui-control-size-lg) - 2px);
847
+ padding: var(--ui-space-3) var(--ui-space-4);
848
+ font-size: var(--ui-font-size-md);
840
849
  }
841
850
  button {
842
851
  appearance: none;
@@ -937,6 +946,14 @@ input:disabled, button:disabled {
937
946
  min-inline-size: 2.75rem;
938
947
  min-block-size: 2.75rem;
939
948
  }
949
+ /* Under touch a small field is still a touch-sized target, as a small button is, and keeps
950
+ body-size text: below 16px, iOS zooms the page into the focused field. */
951
+ [data-component~="ui-combobox"][data-ui-combobox-state~="size=sm"] {
952
+ --_row: var(--ui-control-min-block-size);
953
+ }
954
+ [data-component~="ui-combobox"][data-ui-combobox-state~="size=sm"] input {
955
+ font-size: inherit;
956
+ }
940
957
  }
941
958
  @media (forced-colors: active) {
942
959
  .field, .popup {
@@ -995,6 +1012,26 @@ input:disabled, button:disabled {
995
1012
  min-block-size: 1.75rem;
996
1013
  padding: var(--ui-space-0-5) var(--ui-space-1);
997
1014
  }
1015
+ /* A small multiple field starts at the small control height; chosen items still wrap it taller. */
1016
+ [data-component~="ui-combobox"][data-ui-combobox-state~="multiple"][data-component~="ui-combobox"][data-ui-combobox-state~="size=sm"]
1017
+ .field {
1018
+ min-block-size: 0;
1019
+ padding: var(--ui-space-0-5);
1020
+ }
1021
+ [data-component~="ui-combobox"][data-ui-combobox-state~="multiple"][data-component~="ui-combobox"][data-ui-combobox-state~="size=sm"]
1022
+ input {
1023
+ min-block-size: calc(var(--_row) - 2px - 2 * var(--ui-space-0-5));
1024
+ padding: 0 var(--ui-space-1);
1025
+ }
1026
+ /* A large multiple field starts at the large control height, as sm starts at the small one. */
1027
+ [data-component~="ui-combobox"][data-ui-combobox-state~="multiple"][data-component~="ui-combobox"][data-ui-combobox-state~="size=lg"]
1028
+ .field {
1029
+ min-block-size: 0;
1030
+ }
1031
+ [data-component~="ui-combobox"][data-ui-combobox-state~="multiple"][data-component~="ui-combobox"][data-ui-combobox-state~="size=lg"]
1032
+ input {
1033
+ min-block-size: calc(var(--ui-control-size-lg) - 2px - 2 * var(--ui-space-1));
1034
+ }
998
1035
  .item {
999
1036
  appearance: none;
1000
1037
  display: inline-flex;
@@ -1,12 +1,15 @@
1
1
  type __VLS_Props = {
2
- layout?: 'rows' | 'tiles';
2
+ columns?: '2' | '3' | '4';
3
+ density?: 'comfortable' | 'compact';
4
+ layout?: 'rows' | 'stacked' | 'grid' | 'tiles';
3
5
  };
4
6
  declare var __VLS_1: {};
5
7
  type __VLS_Slots = {} & {
6
8
  default?: (props: typeof __VLS_1) => any;
7
9
  };
8
10
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
9
- layout: "rows" | "tiles";
11
+ density: "comfortable" | "compact";
12
+ layout: "rows" | "stacked" | "grid" | "tiles";
10
13
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
11
14
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
12
15
  declare const _default: typeof __VLS_export;
@@ -6,9 +6,12 @@ defineOptions({ inheritAttrs: false })
6
6
 
7
7
  const props = withDefaults(
8
8
  defineProps<{
9
- layout?: 'rows' | 'tiles'
9
+ columns?: '2' | '3' | '4'
10
+ density?: 'comfortable' | 'compact'
11
+ layout?: 'rows' | 'stacked' | 'grid' | 'tiles'
10
12
  }>(),
11
13
  {
14
+ density: 'comfortable',
12
15
  layout: 'rows',
13
16
  },
14
17
  )
@@ -17,6 +20,8 @@ const props = withDefaults(
17
20
  const hostState = computed(() =>
18
21
  [
19
22
  props.layout && `layout layout=${props.layout}`,
23
+ props.columns && `columns columns=${props.columns}`,
24
+ props.density && `density density=${props.density}`,
20
25
  ].filter(Boolean).join(' ')
21
26
  )
22
27
  </script>
@@ -35,24 +40,63 @@ const hostState = computed(() =>
35
40
  /* Named values: a term and what it is, for each item (ui-description-item). */
36
41
  /* Rows line every term up in one column and every value in the next, through subgrid. */
37
42
  [data-component~="ui-description-list"] {
43
+ --_gap: var(--ui-space-2);
44
+ --_gap-compact: var(--ui-space-1);
45
+ --_column-gap: var(--ui-space-6);
46
+ --_min: 10rem;
47
+ --_track: var(--_min);
48
+ --_tile-padding: var(--ui-space-3);
38
49
  display: grid;
39
50
  grid-template-columns: minmax(6rem, max-content) minmax(0, 1fr);
40
- gap: var(--ui-space-2) var(--ui-space-6);
51
+ gap: var(--_gap) var(--_column-gap);
41
52
  min-inline-size: 0;
42
53
  margin: 0;
43
54
  }
44
55
 
45
- /* Tiles pass their shape to the items through private properties, which cross into each item. */
56
+ /* Stacked, grid, and tiles set each term above its value, passing the shape to the items through
57
+ private properties, which cross into each item. */
58
+ [data-component~="ui-description-list"][data-ui-description-list-state~="layout=stacked"],
59
+ [data-component~="ui-description-list"][data-ui-description-list-state~="layout=grid"],
46
60
  [data-component~="ui-description-list"][data-ui-description-list-state~="layout=tiles"] {
47
61
  --_ui-description-display: flex;
48
62
  --_ui-description-column: auto;
63
+ --_ui-description-align: stretch;
64
+ --_gap: var(--ui-space-4);
65
+ --_gap-compact: var(--ui-space-2);
66
+ grid-template-columns: minmax(0, 1fr);
67
+ }
68
+
69
+ /* A column is at least --_min wide; with columns set, at least its share of the width, so no more
70
+ than that many fit. */
71
+ [data-component~="ui-description-list"][data-ui-description-list-state~="layout=grid"],
72
+ [data-component~="ui-description-list"][data-ui-description-list-state~="layout=tiles"] {
73
+ grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--_track)), 1fr));
74
+ }
75
+
76
+ [data-component~="ui-description-list"][data-ui-description-list-state~="layout=tiles"] {
49
77
  --_ui-description-align: flex-start;
50
78
  --_ui-description-value-size: var(--ui-font-size-lg);
51
- --_ui-description-padding: var(--ui-space-3);
79
+ --_ui-description-padding: var(--_tile-padding);
52
80
  --_ui-description-border-width: 1px;
53
- display: grid;
54
- grid-template-columns: repeat(auto-fill, minmax(min(100%, 10rem), 1fr));
55
- gap: var(--ui-space-3);
81
+ --_gap: var(--ui-space-3);
82
+ --_gap-compact: var(--ui-space-2);
83
+ --_column-gap: var(--_gap);
84
+ }
85
+
86
+ [data-component~="ui-description-list"][data-ui-description-list-state~="columns=2"] {
87
+ --_track: max(var(--_min), (100% - var(--_column-gap)) / 2);
88
+ }
89
+ [data-component~="ui-description-list"][data-ui-description-list-state~="columns=3"] {
90
+ --_track: max(var(--_min), (100% - 2 * var(--_column-gap)) / 3);
91
+ }
92
+ [data-component~="ui-description-list"][data-ui-description-list-state~="columns=4"] {
93
+ --_track: max(var(--_min), (100% - 3 * var(--_column-gap)) / 4);
94
+ }
95
+
96
+ /* Compact takes each layout's closer spacing. */
97
+ [data-component~="ui-description-list"][data-ui-description-list-state~="density=compact"] {
98
+ --_gap: var(--_gap-compact);
99
+ --_tile-padding: var(--ui-space-2);
56
100
  }
57
101
 
58
102
  /* A component that sets its display still honours the hidden attribute on its root. */
package/vue/UiInput.d.ts CHANGED
@@ -3,6 +3,7 @@ type __VLS_Props = {
3
3
  invalid?: boolean;
4
4
  readonly?: boolean;
5
5
  required?: boolean;
6
+ size?: 'sm' | 'md' | 'lg';
6
7
  value?: string;
7
8
  modelValue?: string | null;
8
9
  };
@@ -12,6 +13,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
12
13
  "onUpdate:modelValue"?: ((value: string) => any) | undefined;
13
14
  }>, {
14
15
  invalid: boolean;
16
+ size: "sm" | "md" | "lg";
15
17
  disabled: boolean;
16
18
  required: boolean;
17
19
  readonly: boolean;
package/vue/UiInput.vue CHANGED
@@ -12,6 +12,7 @@ const props = withDefaults(
12
12
  invalid?: boolean
13
13
  readonly?: boolean
14
14
  required?: boolean
15
+ size?: 'sm' | 'md' | 'lg'
15
16
  value?: string
16
17
  modelValue?: string | null
17
18
  }>(),
@@ -20,6 +21,7 @@ const props = withDefaults(
20
21
  invalid: false,
21
22
  readonly: false,
22
23
  required: false,
24
+ size: 'md',
23
25
  },
24
26
  )
25
27
  const emit = defineEmits<{
@@ -32,6 +34,13 @@ const model = computed({
32
34
 
33
35
  const root = ref<HTMLElement | null>(null)
34
36
 
37
+ /** The values the styles' :host-state() rules test. */
38
+ const hostState = computed(() =>
39
+ [
40
+ props.size && `size size=${props.size}`,
41
+ ].filter(Boolean).join(' ')
42
+ )
43
+
35
44
  const dispatch = createDispatch(root)
36
45
 
37
46
  useComponentHost(controllerModule.default, {
@@ -50,6 +59,7 @@ useComponentHost(controllerModule.default, {
50
59
  :required="required"
51
60
  :aria-invalid="invalid"
52
61
  v-model="model"
62
+ :data-ui-input-state="hostState || undefined"
53
63
  ref="root"
54
64
  >
55
65
  </template>
@@ -138,6 +148,37 @@ useComponentHost(controllerModule.default, {
138
148
  background-color var(--ui-motion-fast) var(--ui-motion-ease);
139
149
  }
140
150
 
151
+ /* Each size reads the hook and then an Input Group's default first, so a group, which draws the frame, can zero it. */
152
+ [data-component~="ui-input"][data-ui-input-state~="size=sm"] {
153
+ min-block-size: var(
154
+ --ui-input-min-block-size,
155
+ var(--_ui-default-input-min-block-size, var(--ui-control-size-sm))
156
+ );
157
+ padding: var(--ui-space-1) var(--ui-space-2);
158
+ font-size: var(--ui-font-size-sm);
159
+ }
160
+
161
+ [data-component~="ui-input"][data-ui-input-state~="size=lg"] {
162
+ min-block-size: var(
163
+ --ui-input-min-block-size,
164
+ var(--_ui-default-input-min-block-size, var(--ui-control-size-lg))
165
+ );
166
+ padding: var(--ui-space-3) var(--ui-space-4);
167
+ font-size: var(--ui-font-size-md);
168
+ }
169
+
170
+ /* Under touch a small field is still a touch-sized target, as a small button is, and keeps
171
+ body-size text: below 16px, iOS zooms the page into the focused field. */
172
+ @media (pointer: coarse) {
173
+ [data-component~="ui-input"][data-ui-input-state~="size=sm"] {
174
+ min-block-size: var(
175
+ --ui-input-min-block-size,
176
+ var(--_ui-default-input-min-block-size, var(--ui-control-min-block-size))
177
+ );
178
+ font-size: inherit;
179
+ }
180
+ }
181
+
141
182
  /* Tabbing into a field selects its text; tint that selection softly instead of the page highlight. */
142
183
  [data-component~="ui-input"]::selection {
143
184
  background: color-mix(in srgb, var(--ui-accent-solid) 20%, transparent);
@@ -91,6 +91,21 @@ useComponentHost(controllerModule.default, {
91
91
  box-shadow var(--ui-motion-fast) var(--ui-motion-ease);
92
92
  }
93
93
 
94
+ /* The frame takes its input's size, so a small group lines up with small buttons and fields. */
95
+ [data-component~="ui-input-group"]:has(.field [data-ui-input-state~="size=sm"]) {
96
+ min-block-size: var(--ui-control-size-sm);
97
+ }
98
+
99
+ [data-component~="ui-input-group"]:has(.field [data-ui-input-state~="size=lg"]) {
100
+ min-block-size: var(--ui-control-size-lg);
101
+ }
102
+
103
+ @media (pointer: coarse) {
104
+ [data-component~="ui-input-group"]:has(.field [data-ui-input-state~="size=sm"]) {
105
+ min-block-size: var(--ui-control-min-block-size);
106
+ }
107
+ }
108
+
94
109
  [data-component~="ui-input-group"]:hover:not(:has(input:focus, input:disabled)) {
95
110
  border-color: var(--ui-control-border-hover, var(--ui-text-secondary));
96
111
  }
package/vue/UiList.d.ts CHANGED
@@ -7,8 +7,8 @@ type __VLS_Slots = {} & {
7
7
  default?: (props: typeof __VLS_1) => any;
8
8
  };
9
9
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
10
- layout: "rows" | "grid";
11
10
  density: "comfortable" | "compact";
11
+ layout: "rows" | "grid";
12
12
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
13
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
14
14
  declare const _default: typeof __VLS_export;
package/vue/UiRadio.d.ts CHANGED
@@ -3,6 +3,7 @@ type __VLS_Props = {
3
3
  disabled?: boolean;
4
4
  name?: string;
5
5
  required?: boolean;
6
+ size?: 'sm' | 'md' | 'lg';
6
7
  value?: string;
7
8
  };
8
9
  declare var __VLS_1: {}, __VLS_3: {};
@@ -29,6 +30,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
29
30
  "onUpdate:value"?: ((value: string) => any) | undefined;
30
31
  }>, {
31
32
  name: string;
33
+ size: "sm" | "md" | "lg";
32
34
  value: string;
33
35
  disabled: boolean;
34
36
  checked: boolean;
package/vue/UiRadio.vue CHANGED
@@ -12,6 +12,7 @@ const props = withDefaults(
12
12
  disabled?: boolean
13
13
  name?: string
14
14
  required?: boolean
15
+ size?: 'sm' | 'md' | 'lg'
15
16
  value?: string
16
17
  }>(),
17
18
  {
@@ -19,6 +20,7 @@ const props = withDefaults(
19
20
  disabled: false,
20
21
  name: '',
21
22
  required: false,
23
+ size: 'md',
22
24
  value: 'on',
23
25
  },
24
26
  )
@@ -43,6 +45,7 @@ const internalChecked = ref(false)
43
45
  const hostState = computed(() =>
44
46
  [
45
47
  props.disabled && 'disabled',
48
+ props.size && `size size=${props.size}`,
46
49
  ].filter(Boolean).join(' ')
47
50
  )
48
51
 
@@ -159,6 +162,37 @@ input:focus-visible {
159
162
  cursor: not-allowed;
160
163
  }
161
164
 
165
+ /* A sized choice keeps its box and aligns instead: the label takes that size's field type size,
166
+ and the first line centres in that control height, so the label shares a row's baseline.
167
+ Multi-line labels and descriptions still grow downward from that first line. */
168
+ [data-component~="ui-radio"][data-ui-radio-state~="size=sm"] {
169
+ --_font: var(--ui-font-size-sm);
170
+ --_row: var(--ui-control-size-sm);
171
+ }
172
+
173
+ [data-component~="ui-radio"][data-ui-radio-state~="size=lg"] {
174
+ --_font: var(--ui-font-size-md);
175
+ --_row: var(--ui-control-size-lg);
176
+ }
177
+
178
+ [data-component~="ui-radio"][data-ui-radio-state~="size=sm"],
179
+ [data-component~="ui-radio"][data-ui-radio-state~="size=lg"] {
180
+ --_line: calc(var(--_font) * var(--ui-line-height-md));
181
+ font-size: var(--_font);
182
+ padding-block: calc((var(--_row) - var(--_line)) / 2);
183
+ }
184
+
185
+ [data-component~="ui-radio"][data-ui-radio-state~="size=sm"] input,
186
+ [data-component~="ui-radio"][data-ui-radio-state~="size=lg"] input {
187
+ margin-top: calc((var(--_line) - var(--_ui-radio-size, 1.125rem)) / 2);
188
+ }
189
+
190
+ @media (pointer: coarse) {
191
+ [data-component~="ui-radio"][data-ui-radio-state~="size=sm"] {
192
+ --_row: var(--ui-control-min-block-size);
193
+ }
194
+ }
195
+
162
196
  /* A component that sets its display still honours the hidden attribute on its root. */
163
197
  [data-component~="ui-radio"][hidden] {
164
198
  display: none;
package/vue/UiSelect.d.ts CHANGED
@@ -2,6 +2,7 @@ type __VLS_Props = {
2
2
  disabled?: boolean;
3
3
  invalid?: boolean;
4
4
  required?: boolean;
5
+ size?: 'sm' | 'md' | 'lg';
5
6
  value?: string;
6
7
  modelValue?: string | null;
7
8
  };
@@ -15,6 +16,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
15
16
  "onUpdate:modelValue"?: ((value: string) => any) | undefined;
16
17
  }>, {
17
18
  invalid: boolean;
19
+ size: "sm" | "md" | "lg";
18
20
  disabled: boolean;
19
21
  required: boolean;
20
22
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
package/vue/UiSelect.vue CHANGED
@@ -11,6 +11,7 @@ const props = withDefaults(
11
11
  disabled?: boolean
12
12
  invalid?: boolean
13
13
  required?: boolean
14
+ size?: 'sm' | 'md' | 'lg'
14
15
  value?: string
15
16
  modelValue?: string | null
16
17
  }>(),
@@ -18,6 +19,7 @@ const props = withDefaults(
18
19
  disabled: false,
19
20
  invalid: false,
20
21
  required: false,
22
+ size: 'md',
21
23
  },
22
24
  )
23
25
  const emit = defineEmits<{
@@ -30,6 +32,13 @@ const model = computed({
30
32
 
31
33
  const root = ref<HTMLElement | null>(null)
32
34
 
35
+ /** The values the styles' :host-state() rules test. */
36
+ const hostState = computed(() =>
37
+ [
38
+ props.size && `size size=${props.size}`,
39
+ ].filter(Boolean).join(' ')
40
+ )
41
+
33
42
  const dispatch = createDispatch(root)
34
43
 
35
44
  useComponentHost(controllerModule.default, {
@@ -47,6 +56,7 @@ useComponentHost(controllerModule.default, {
47
56
  :required="required"
48
57
  :aria-invalid="invalid"
49
58
  v-model="model"
59
+ :data-ui-select-state="hostState || undefined"
50
60
  ref="root"
51
61
  >
52
62
  <slot />
@@ -137,9 +147,30 @@ useComponentHost(controllerModule.default, {
137
147
  cursor: not-allowed;
138
148
  }
139
149
 
140
- [data-component~="ui-select"][size]:not([size="1"]) {
141
- min-block-size: 7rem;
142
- padding: var(--ui-space-2);
150
+ [data-component~="ui-select"][data-ui-select-state~="size=sm"] {
151
+ min-block-size: var(--ui-control-size-sm);
152
+ padding: var(--ui-space-1) var(--ui-space-2);
153
+ padding-inline-end: calc(var(--ui-space-2) + var(--ui-icon-size-sm) + var(--ui-space-1));
154
+ background-position: right var(--ui-space-2) center;
155
+ background-size: var(--ui-icon-size-sm) var(--ui-icon-size-sm);
156
+ font-size: var(--ui-font-size-sm);
157
+ }
158
+
159
+ [data-component~="ui-select"][data-ui-select-state~="size=lg"] {
160
+ min-block-size: var(--ui-control-size-lg);
161
+ padding: var(--ui-space-3) var(--ui-space-4);
162
+ padding-inline-end: calc(var(--ui-space-4) + 1.5rem);
163
+ background-position: right var(--ui-space-4) center;
164
+ font-size: var(--ui-font-size-md);
165
+ }
166
+
167
+ /* Under touch a small field is still a touch-sized target, as a small button is, and keeps
168
+ body-size text: below 16px, iOS zooms the page into the focused field. */
169
+ @media (pointer: coarse) {
170
+ [data-component~="ui-select"][data-ui-select-state~="size=sm"] {
171
+ min-block-size: var(--ui-control-min-block-size);
172
+ font-size: inherit;
173
+ }
143
174
  }
144
175
 
145
176
  /* A component that sets its display still honours the hidden attribute on its root. */
package/vue/UiSwitch.d.ts CHANGED
@@ -3,6 +3,7 @@ type __VLS_Props = {
3
3
  disabled?: boolean;
4
4
  name?: string;
5
5
  required?: boolean;
6
+ size?: 'sm' | 'md' | 'lg';
6
7
  value?: string;
7
8
  };
8
9
  declare var __VLS_1: {}, __VLS_3: {};
@@ -29,6 +30,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
29
30
  "onUpdate:value"?: ((value: string) => any) | undefined;
30
31
  }>, {
31
32
  name: string;
33
+ size: "sm" | "md" | "lg";
32
34
  value: string;
33
35
  disabled: boolean;
34
36
  checked: boolean;
package/vue/UiSwitch.vue CHANGED
@@ -12,6 +12,7 @@ const props = withDefaults(
12
12
  disabled?: boolean
13
13
  name?: string
14
14
  required?: boolean
15
+ size?: 'sm' | 'md' | 'lg'
15
16
  value?: string
16
17
  }>(),
17
18
  {
@@ -19,6 +20,7 @@ const props = withDefaults(
19
20
  disabled: false,
20
21
  name: '',
21
22
  required: false,
23
+ size: 'md',
22
24
  value: 'on',
23
25
  },
24
26
  )
@@ -43,6 +45,7 @@ const internalChecked = ref(false)
43
45
  const hostState = computed(() =>
44
46
  [
45
47
  props.disabled && 'disabled',
48
+ props.size && `size size=${props.size}`,
46
49
  ].filter(Boolean).join(' ')
47
50
  )
48
51
 
@@ -206,6 +209,38 @@ input:focus-visible {
206
209
  }
207
210
  }
208
211
 
212
+ /* A sized switch keeps its track and aligns instead: the label takes that size's field type size,
213
+ and the first line centres in that control height, so the label shares a row's baseline.
214
+ Multi-line labels and descriptions still grow downward from that first line. */
215
+ [data-component~="ui-switch"][data-ui-switch-state~="size=sm"] {
216
+ --_font: var(--ui-font-size-sm);
217
+ --_row: var(--ui-control-size-sm);
218
+ }
219
+
220
+ [data-component~="ui-switch"][data-ui-switch-state~="size=lg"] {
221
+ --_font: var(--ui-font-size-md);
222
+ --_row: var(--ui-control-size-lg);
223
+ }
224
+
225
+ [data-component~="ui-switch"][data-ui-switch-state~="size=sm"],
226
+ [data-component~="ui-switch"][data-ui-switch-state~="size=lg"] {
227
+ --_line: calc(var(--_font) * var(--ui-line-height-md));
228
+ font-size: var(--_font);
229
+ padding-block: calc((var(--_row) - var(--ui-switch-track-height, 1.5rem)) / 2);
230
+ }
231
+
232
+ /* The label centres on the track. */
233
+ [data-component~="ui-switch"][data-ui-switch-state~="size=sm"] .text,
234
+ [data-component~="ui-switch"][data-ui-switch-state~="size=lg"] .text {
235
+ margin-top: calc((var(--_ui-switch-track-height, 1.5rem) - var(--_line)) / 2);
236
+ }
237
+
238
+ @media (pointer: coarse) {
239
+ [data-component~="ui-switch"][data-ui-switch-state~="size=sm"] {
240
+ --_row: var(--ui-control-min-block-size);
241
+ }
242
+ }
243
+
209
244
  /* A component that sets its display still honours the hidden attribute on its root. */
210
245
  [data-component~="ui-switch"][hidden] {
211
246
  display: none;