@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
@@ -316,8 +316,9 @@
316
316
  border-color: transparent;
317
317
  }
318
318
 
319
+ /* The size states outrank :host, so sm is named too, or a small button would stay small. */
319
320
  @media (pointer: coarse) {
320
- :host {
321
+ :host, :host-state([size="sm"]) {
321
322
  min-block-size: var(--ui-control-min-block-size);
322
323
  }
323
324
  }
@@ -14,6 +14,10 @@
14
14
  <prop name="value" type="string" default="on"
15
15
  >The value a form submits under name while the checkbox is checked, also reported in the change event's detail so one handler can tell checkboxes apart. on
16
16
  by default, as on a native checkbox.</prop>
17
+ <prop name="size" type="sm | md | lg" default="md"
18
+ >Aligns the checkbox with fields and buttons of the same size. It keeps its box size; the label takes that size's field type size and its first line centres
19
+ in that control height (sm 2rem, lg 3rem by default), so in a row of inputs, selects, and buttons of one size it shares their height and their text
20
+ baseline. Under a coarse pointer an sm row grows to the 44px touch minimum, as they do. md is the standard layout.</prop>
17
21
  <state name="internalChecked" :value="false"></state>
18
22
  <event name="change" type="object({ checked: boolean, value: string, trigger: keyboard | pointer | programmatic })"></event>
19
23
  </defs>
@@ -154,6 +158,37 @@
154
158
  cursor: not-allowed;
155
159
  }
156
160
 
161
+ /* A sized choice keeps its box and aligns instead: the label takes that size's field type size,
162
+ and the first line centres in that control height, so the label shares a row's baseline.
163
+ Multi-line labels and descriptions still grow downward from that first line. */
164
+ :host-state([size="sm"]) {
165
+ --_font: var(--ui-font-size-sm);
166
+ --_row: var(--ui-control-size-sm);
167
+ }
168
+
169
+ :host-state([size="lg"]) {
170
+ --_font: var(--ui-font-size-md);
171
+ --_row: var(--ui-control-size-lg);
172
+ }
173
+
174
+ :host-state([size="sm"]),
175
+ :host-state([size="lg"]) {
176
+ --_line: calc(var(--_font) * var(--ui-line-height-md));
177
+ font-size: var(--_font);
178
+ padding-block: calc((var(--_row) - var(--_line)) / 2);
179
+ }
180
+
181
+ :host-state([size="sm"]) input,
182
+ :host-state([size="lg"]) input {
183
+ margin-top: calc((var(--_line) - var(--_ui-checkbox-size, 1.125rem)) / 2);
184
+ }
185
+
186
+ @media (pointer: coarse) {
187
+ :host-state([size="sm"]) {
188
+ --_row: var(--ui-control-min-block-size);
189
+ }
190
+ }
191
+
157
192
  /* A component that sets its display still honours the hidden attribute on its root. */
158
193
  :host[hidden] {
159
194
  display: none;
@@ -44,8 +44,10 @@
44
44
  <prop name="required" type="boolean" default="false"
45
45
  >Requires a value: adds an asterisk to the label, marks the input required for native form validation, and makes validation report "A value is required."
46
46
  when the field is left empty. In multiple mode a chosen item satisfies it, whatever is left in the input.</prop>
47
- <prop name="size" type="sm | md" default="md"
48
- >Field height. md is the standard control height; sm is the small control height with tighter padding, for dense forms.</prop>
47
+ <prop name="size" type="sm | md | lg" default="md"
48
+ >Field height, padding, and type size, matching Button's sizes so a field and a button of one size share a row. sm is the small control height (2rem by
49
+ default) with smaller text, for compact rows such as a table row or a filter bar; md is the standard control height; lg is taller with roomier padding.
50
+ Under a coarse pointer, sm grows to the 44px touch minimum and keeps body-size text. The label keeps its size.</prop>
49
51
  <prop name="labelVisibility" type="visible | sr-only" default="visible"
50
52
  >visible shows the label above the field; sr-only hides it visually but keeps it as the input's accessible name. Use sr-only only when the surrounding
51
53
  layout already makes the field's purpose clear.</prop>
@@ -395,9 +397,18 @@
395
397
  margin: 0;
396
398
  outline: none;
397
399
  }
400
+ :host-state([size="sm"]) {
401
+ --_row: var(--ui-control-size-sm);
402
+ }
398
403
  :host-state([size="sm"]) input {
399
- min-block-size: calc(var(--ui-control-size-sm) - 2px);
404
+ min-block-size: calc(var(--_row) - 2px);
400
405
  padding: var(--ui-space-1) var(--ui-space-2);
406
+ font-size: var(--ui-font-size-sm);
407
+ }
408
+ :host-state([size="lg"]) input {
409
+ min-block-size: calc(var(--ui-control-size-lg) - 2px);
410
+ padding: var(--ui-space-3) var(--ui-space-4);
411
+ font-size: var(--ui-font-size-md);
401
412
  }
402
413
  button {
403
414
  appearance: none;
@@ -498,6 +509,14 @@
498
509
  min-inline-size: 2.75rem;
499
510
  min-block-size: 2.75rem;
500
511
  }
512
+ /* Under touch a small field is still a touch-sized target, as a small button is, and keeps
513
+ body-size text: below 16px, iOS zooms the page into the focused field. */
514
+ :host-state([size="sm"]) {
515
+ --_row: var(--ui-control-min-block-size);
516
+ }
517
+ :host-state([size="sm"]) input {
518
+ font-size: inherit;
519
+ }
501
520
  }
502
521
  @media (forced-colors: active) {
503
522
  .field, .popup {
@@ -555,6 +574,22 @@
555
574
  min-block-size: 1.75rem;
556
575
  padding: var(--ui-space-0-5) var(--ui-space-1);
557
576
  }
577
+ /* A small multiple field starts at the small control height; chosen items still wrap it taller. */
578
+ :host-state([multiple]):host-state([size="sm"]) .field {
579
+ min-block-size: 0;
580
+ padding: var(--ui-space-0-5);
581
+ }
582
+ :host-state([multiple]):host-state([size="sm"]) input {
583
+ min-block-size: calc(var(--_row) - 2px - 2 * var(--ui-space-0-5));
584
+ padding: 0 var(--ui-space-1);
585
+ }
586
+ /* A large multiple field starts at the large control height, as sm starts at the small one. */
587
+ :host-state([multiple]):host-state([size="lg"]) .field {
588
+ min-block-size: 0;
589
+ }
590
+ :host-state([multiple]):host-state([size="lg"]) input {
591
+ min-block-size: calc(var(--ui-control-size-lg) - 2px - 2 * var(--ui-space-1));
592
+ }
558
593
  .item {
559
594
  appearance: none;
560
595
  display: inline-flex;
@@ -1,32 +1,77 @@
1
1
  <template component="ui-description-list">
2
2
  <defs>
3
- <prop name="layout" type="rows | tiles" default="rows"
4
- >rows sets each term beside its value, one per line, for a short summary; tiles sets each pair on its own bordered tile in a grid, term above value, for
5
- figures and facts to scan.</prop>
3
+ <prop name="layout" type="rows | stacked | grid | tiles" default="rows"
4
+ >rows sets each term beside its value, one per line, for a short summary; stacked sets every term above its value, one pair per line, for a narrow space;
5
+ grid sets the pairs, term above value, in as many columns as fit, for facts to scan across; tiles sets each pair on its own bordered tile in a grid, for
6
+ figures to compare.</prop>
7
+ <prop name="columns" type="'2' | '3' | '4'"
8
+ >The most columns grid and tiles set on a wide list; a narrow list still drops to fewer. Left unset, they fit as many as the width allows.</prop>
9
+ <prop name="density" type="comfortable | compact" default="comfortable"
10
+ >How much room each pair takes. comfortable spaces the pairs apart to read; compact sets them close together, for facts inside other content such as a card
11
+ or a table's detail row.</prop>
6
12
  </defs>
7
13
  <dl><slot></slot></dl>
8
14
  <style>
9
15
  /* Named values: a term and what it is, for each item (ui-description-item). */
10
16
  /* Rows line every term up in one column and every value in the next, through subgrid. */
11
17
  :host {
18
+ --_gap: var(--ui-space-2);
19
+ --_gap-compact: var(--ui-space-1);
20
+ --_column-gap: var(--ui-space-6);
21
+ --_min: 10rem;
22
+ --_track: var(--_min);
23
+ --_tile-padding: var(--ui-space-3);
12
24
  display: grid;
13
25
  grid-template-columns: minmax(6rem, max-content) minmax(0, 1fr);
14
- gap: var(--ui-space-2) var(--ui-space-6);
26
+ gap: var(--_gap) var(--_column-gap);
15
27
  min-inline-size: 0;
16
28
  margin: 0;
17
29
  }
18
30
 
19
- /* Tiles pass their shape to the items through private properties, which cross into each item. */
31
+ /* Stacked, grid, and tiles set each term above its value, passing the shape to the items through
32
+ private properties, which cross into each item. */
33
+ :host-state([layout="stacked"]),
34
+ :host-state([layout="grid"]),
20
35
  :host-state([layout="tiles"]) {
21
36
  --_ui-description-display: flex;
22
37
  --_ui-description-column: auto;
38
+ --_ui-description-align: stretch;
39
+ --_gap: var(--ui-space-4);
40
+ --_gap-compact: var(--ui-space-2);
41
+ grid-template-columns: minmax(0, 1fr);
42
+ }
43
+
44
+ /* A column is at least --_min wide; with columns set, at least its share of the width, so no more
45
+ than that many fit. */
46
+ :host-state([layout="grid"]),
47
+ :host-state([layout="tiles"]) {
48
+ grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--_track)), 1fr));
49
+ }
50
+
51
+ :host-state([layout="tiles"]) {
23
52
  --_ui-description-align: flex-start;
24
53
  --_ui-description-value-size: var(--ui-font-size-lg);
25
- --_ui-description-padding: var(--ui-space-3);
54
+ --_ui-description-padding: var(--_tile-padding);
26
55
  --_ui-description-border-width: 1px;
27
- display: grid;
28
- grid-template-columns: repeat(auto-fill, minmax(min(100%, 10rem), 1fr));
29
- gap: var(--ui-space-3);
56
+ --_gap: var(--ui-space-3);
57
+ --_gap-compact: var(--ui-space-2);
58
+ --_column-gap: var(--_gap);
59
+ }
60
+
61
+ :host-state([columns="2"]) {
62
+ --_track: max(var(--_min), (100% - var(--_column-gap)) / 2);
63
+ }
64
+ :host-state([columns="3"]) {
65
+ --_track: max(var(--_min), (100% - 2 * var(--_column-gap)) / 3);
66
+ }
67
+ :host-state([columns="4"]) {
68
+ --_track: max(var(--_min), (100% - 3 * var(--_column-gap)) / 4);
69
+ }
70
+
71
+ /* Compact takes each layout's closer spacing. */
72
+ :host-state([density="compact"]) {
73
+ --_gap: var(--_gap-compact);
74
+ --_tile-padding: var(--ui-space-2);
30
75
  }
31
76
 
32
77
  /* A component that sets its display still honours the hidden attribute on its root. */
@@ -7,6 +7,11 @@
7
7
  <prop name="invalid" type="boolean" default="false">Exposes invalid state to assistive technology and styling.</prop>
8
8
  <prop name="readonly" type="boolean" default="false">Allows selection without allowing edits.</prop>
9
9
  <prop name="required" type="boolean" default="false">Marks the control as required for form validation.</prop>
10
+ <prop name="size" type="sm | md | lg" default="md"
11
+ >Height, padding, and type size, matching Button's sizes so a field and a button of one size share a row. sm is the small control height (2rem by default)
12
+ with smaller text, for compact rows such as a table row or a filter bar; md is the standard control height; lg is taller with roomier padding. Under a
13
+ coarse pointer, sm grows to the 44px touch minimum and keeps body-size text. Change it after render through this option; the element's size property is
14
+ the native one.</prop>
10
15
  </defs>
11
16
  <input :value="value" :disabled="disabled" :readonly="readonly" :required="required" :aria-invalid="invalid">
12
17
  <style>
@@ -84,6 +89,28 @@
84
89
  background-color var(--ui-motion-fast) var(--ui-motion-ease);
85
90
  }
86
91
 
92
+ /* Each size reads the hook and then an Input Group's default first, so a group, which draws the frame, can zero it. */
93
+ :host-state([size="sm"]) {
94
+ min-block-size: var(--ui-input-min-block-size, var(--_ui-default-input-min-block-size, var(--ui-control-size-sm)));
95
+ padding: var(--ui-space-1) var(--ui-space-2);
96
+ font-size: var(--ui-font-size-sm);
97
+ }
98
+
99
+ :host-state([size="lg"]) {
100
+ min-block-size: var(--ui-input-min-block-size, var(--_ui-default-input-min-block-size, var(--ui-control-size-lg)));
101
+ padding: var(--ui-space-3) var(--ui-space-4);
102
+ font-size: var(--ui-font-size-md);
103
+ }
104
+
105
+ /* Under touch a small field is still a touch-sized target, as a small button is, and keeps
106
+ body-size text: below 16px, iOS zooms the page into the focused field. */
107
+ @media (pointer: coarse) {
108
+ :host-state([size="sm"]) {
109
+ min-block-size: var(--ui-input-min-block-size, var(--_ui-default-input-min-block-size, var(--ui-control-min-block-size)));
110
+ font-size: inherit;
111
+ }
112
+ }
113
+
87
114
  /* Tabbing into a field selects its text; tint that selection softly instead of the page highlight. */
88
115
  :host::selection {
89
116
  background: color-mix(in srgb, var(--ui-accent-solid) 20%, transparent);
@@ -50,6 +50,21 @@
50
50
  transition: border-color var(--ui-motion-fast) var(--ui-motion-ease), box-shadow var(--ui-motion-fast) var(--ui-motion-ease);
51
51
  }
52
52
 
53
+ /* The frame takes its input's size, so a small group lines up with small buttons and fields. */
54
+ :host:has(.field [data-ui-input-state~="size=sm"]) {
55
+ min-block-size: var(--ui-control-size-sm);
56
+ }
57
+
58
+ :host:has(.field [data-ui-input-state~="size=lg"]) {
59
+ min-block-size: var(--ui-control-size-lg);
60
+ }
61
+
62
+ @media (pointer: coarse) {
63
+ :host:has(.field [data-ui-input-state~="size=sm"]) {
64
+ min-block-size: var(--ui-control-min-block-size);
65
+ }
66
+ }
67
+
53
68
  :host:hover:not(:has(input:focus, input:disabled)) {
54
69
  border-color: var(--ui-control-border-hover, var(--ui-text-secondary));
55
70
  }
@@ -12,6 +12,10 @@
12
12
  <prop name="value" type="string" default="on"
13
13
  >The value this radio stands for: submitted with a form, reported in its change event, and matched against a ui-radio-group's value to decide which radio is
14
14
  checked.</prop>
15
+ <prop name="size" type="sm | md | lg" default="md"
16
+ >Aligns the radio with fields and buttons of the same size. It keeps its box size; the label takes that size's field type size and its first line centres in
17
+ that control height (sm 2rem, lg 3rem by default), so in a row of inputs, selects, and buttons of one size it shares their height and their text baseline.
18
+ Under a coarse pointer an sm row grows to the 44px touch minimum, as they do. md is the standard layout.</prop>
15
19
  <state name="internalChecked" :value="false"></state>
16
20
  <event name="change" type="object({ checked: boolean, value: string, trigger: keyboard | pointer | programmatic })"></event>
17
21
  </defs>
@@ -90,6 +94,37 @@
90
94
  cursor: not-allowed;
91
95
  }
92
96
 
97
+ /* A sized choice keeps its box and aligns instead: the label takes that size's field type size,
98
+ and the first line centres in that control height, so the label shares a row's baseline.
99
+ Multi-line labels and descriptions still grow downward from that first line. */
100
+ :host-state([size="sm"]) {
101
+ --_font: var(--ui-font-size-sm);
102
+ --_row: var(--ui-control-size-sm);
103
+ }
104
+
105
+ :host-state([size="lg"]) {
106
+ --_font: var(--ui-font-size-md);
107
+ --_row: var(--ui-control-size-lg);
108
+ }
109
+
110
+ :host-state([size="sm"]),
111
+ :host-state([size="lg"]) {
112
+ --_line: calc(var(--_font) * var(--ui-line-height-md));
113
+ font-size: var(--_font);
114
+ padding-block: calc((var(--_row) - var(--_line)) / 2);
115
+ }
116
+
117
+ :host-state([size="sm"]) input,
118
+ :host-state([size="lg"]) input {
119
+ margin-top: calc((var(--_line) - var(--_ui-radio-size, 1.125rem)) / 2);
120
+ }
121
+
122
+ @media (pointer: coarse) {
123
+ :host-state([size="sm"]) {
124
+ --_row: var(--ui-control-min-block-size);
125
+ }
126
+ }
127
+
93
128
  /* A component that sets its display still honours the hidden attribute on its root. */
94
129
  :host[hidden] {
95
130
  display: none;
@@ -6,6 +6,11 @@
6
6
  <prop name="disabled" type="boolean" default="false">Prevents selection and form submission.</prop>
7
7
  <prop name="invalid" type="boolean" default="false">Exposes invalid state to assistive technology and styling.</prop>
8
8
  <prop name="required" type="boolean" default="false">Requires a non-empty option for form validation.</prop>
9
+ <prop name="size" type="sm | md | lg" default="md"
10
+ >Height, padding, chevron, and type size, matching Button's sizes so a field and a button of one size share a row. sm is the small control height (2rem by
11
+ default) with smaller text and a smaller chevron, for compact rows such as a table row or a filter bar; md is the standard control height; lg is taller
12
+ with roomier padding. Under a coarse pointer, sm grows to the 44px touch minimum and keeps body-size text. It is not the native size attribute: a select
13
+ is always a single-choice dropdown. Change it after render through this option; the element's size property is the native one.</prop>
9
14
  </defs>
10
15
  <select :disabled="disabled" :required="required" :aria-invalid="invalid"><slot></slot></select>
11
16
  <style>
@@ -88,9 +93,30 @@
88
93
  cursor: not-allowed;
89
94
  }
90
95
 
91
- :host[size]:not([size="1"]) {
92
- min-block-size: 7rem;
93
- padding: var(--ui-space-2);
96
+ :host-state([size="sm"]) {
97
+ min-block-size: var(--ui-control-size-sm);
98
+ padding: var(--ui-space-1) var(--ui-space-2);
99
+ padding-inline-end: calc(var(--ui-space-2) + var(--ui-icon-size-sm) + var(--ui-space-1));
100
+ background-position: right var(--ui-space-2) center;
101
+ background-size: var(--ui-icon-size-sm) var(--ui-icon-size-sm);
102
+ font-size: var(--ui-font-size-sm);
103
+ }
104
+
105
+ :host-state([size="lg"]) {
106
+ min-block-size: var(--ui-control-size-lg);
107
+ padding: var(--ui-space-3) var(--ui-space-4);
108
+ padding-inline-end: calc(var(--ui-space-4) + 1.5rem);
109
+ background-position: right var(--ui-space-4) center;
110
+ font-size: var(--ui-font-size-md);
111
+ }
112
+
113
+ /* Under touch a small field is still a touch-sized target, as a small button is, and keeps
114
+ body-size text: below 16px, iOS zooms the page into the focused field. */
115
+ @media (pointer: coarse) {
116
+ :host-state([size="sm"]) {
117
+ min-block-size: var(--ui-control-min-block-size);
118
+ font-size: inherit;
119
+ }
94
120
  }
95
121
 
96
122
  /* A component that sets its display still honours the hidden attribute on its root. */
@@ -9,6 +9,10 @@
9
9
  <prop name="value" type="string" default="on"
10
10
  >The value a form submits under name while the switch is on, also reported in the change event's detail so one handler can tell switches apart. on by
11
11
  default, as on a native checkbox.</prop>
12
+ <prop name="size" type="sm | md | lg" default="md"
13
+ >Aligns the switch with fields and buttons of the same size. It keeps its track size; the label takes that size's field type size and its first line centres
14
+ in that control height (sm 2rem, lg 3rem by default), so in a row of inputs, selects, and buttons of one size it shares their height and their text
15
+ baseline. Under a coarse pointer an sm row grows to the 44px touch minimum, as they do. md is the standard layout.</prop>
12
16
  <state name="internalChecked" :value="false"></state>
13
17
  <event name="change" type="object({ checked: boolean, value: string, trigger: keyboard | pointer | programmatic })"></event>
14
18
  </defs>
@@ -131,6 +135,38 @@
131
135
  }
132
136
  }
133
137
 
138
+ /* A sized switch keeps its track and aligns instead: the label takes that size's field type size,
139
+ and the first line centres in that control height, so the label shares a row's baseline.
140
+ Multi-line labels and descriptions still grow downward from that first line. */
141
+ :host-state([size="sm"]) {
142
+ --_font: var(--ui-font-size-sm);
143
+ --_row: var(--ui-control-size-sm);
144
+ }
145
+
146
+ :host-state([size="lg"]) {
147
+ --_font: var(--ui-font-size-md);
148
+ --_row: var(--ui-control-size-lg);
149
+ }
150
+
151
+ :host-state([size="sm"]),
152
+ :host-state([size="lg"]) {
153
+ --_line: calc(var(--_font) * var(--ui-line-height-md));
154
+ font-size: var(--_font);
155
+ padding-block: calc((var(--_row) - var(--ui-switch-track-height, 1.5rem)) / 2);
156
+ }
157
+
158
+ /* The label centres on the track. */
159
+ :host-state([size="sm"]) .text,
160
+ :host-state([size="lg"]) .text {
161
+ margin-top: calc((var(--_ui-switch-track-height, 1.5rem) - var(--_line)) / 2);
162
+ }
163
+
164
+ @media (pointer: coarse) {
165
+ :host-state([size="sm"]) {
166
+ --_row: var(--ui-control-min-block-size);
167
+ }
168
+ }
169
+
134
170
  /* A component that sets its display still honours the hidden attribute on its root. */
135
171
  :host[hidden] {
136
172
  display: none;
@@ -0,0 +1,131 @@
1
+ <template component="ui-table" controller="./ui-table.js">
2
+ <defs>
3
+ <prop name="density" type="comfortable | compact" default="comfortable"
4
+ >How tall each row is. Every row is at least one control tall, so a row holding a control lines up with the rows around it. comfortable fits a control of
5
+ the standard height (2.5rem), for tables read or edited at a glance; compact fits a small one (2rem), such as a Checkbox or a Button with size="sm", for
6
+ long tables to scan.</prop>
7
+ <prop name="stickyHeader" type="boolean" default="false"
8
+ >Keeps the header row in view while the rows scroll beneath it. The table scrolls inside its own box, so this needs a bounded height, such as a table that
9
+ fills a fixed-height panel.</prop>
10
+ </defs>
11
+ <div><slot></slot></div>
12
+ <style>
13
+ /* Styles an authored <table> (caption, thead, tbody, th scope) without replacing it: the table is
14
+ meaningful before JavaScript, and a screen reader reads it as a table. The root scrolls a table
15
+ wider than it, or taller than a bounded height; while it does, the controller makes the root a named, focusable region so a
16
+ keyboard can scroll it. A cell's data-ui-align (start, center, or end; start by default) sets its
17
+ content along the row, such as end for a column of figures. */
18
+ :host {
19
+ --_pad-block: var(--ui-space-1);
20
+ --_pad-inline: var(--ui-space-3);
21
+ --_row-size: var(--ui-control-size);
22
+ display: block;
23
+ box-sizing: border-box;
24
+ max-inline-size: 100%;
25
+ min-inline-size: 0;
26
+ min-block-size: 0;
27
+ overflow: auto;
28
+ overscroll-behavior-inline: contain;
29
+ /* The sideways scroll fade every Looma scroller uses (see ui-scroll-area). Only the inline edges
30
+ fade, so a sticky header never fades. */
31
+ --_ui-scroll-fade-direction: to right;
32
+ --_ui-scroll-fade-size: var(--ui-scroll-fade-size, 24px);
33
+ mask-image: linear-gradient(
34
+ var(--_ui-scroll-fade-direction),
35
+ transparent,
36
+ #000 var(--_ui-scroll-fade-start),
37
+ #000 calc(100% - var(--_ui-scroll-fade-end)),
38
+ transparent
39
+ );
40
+ animation: ui-scroll-fade-start linear, ui-scroll-fade-start-hold linear, ui-scroll-fade-end-hold linear, ui-scroll-fade-end linear;
41
+ animation-timeline: scroll(self inline);
42
+ animation-range:
43
+ 0 var(--_ui-scroll-fade-size),
44
+ var(--_ui-scroll-fade-size) calc(100% + 1px),
45
+ -1px calc(100% - var(--_ui-scroll-fade-size)),
46
+ calc(100% - var(--_ui-scroll-fade-size)) 100%;
47
+ }
48
+
49
+ :host:dir(rtl) {
50
+ --_ui-scroll-fade-direction: to left;
51
+ }
52
+
53
+ :host:focus-visible {
54
+ outline: 2px solid var(--ui-focus-ring);
55
+ outline-offset: -2px;
56
+ }
57
+
58
+ :host-state([density="compact"]) {
59
+ --_pad-block: var(--ui-space-0-5);
60
+ --_pad-inline: var(--ui-space-2);
61
+ --_row-size: var(--ui-control-size-sm);
62
+ }
63
+
64
+ /* Separate borders stay on their cells when a sticky header scrolls; collapsed ones stay behind. */
65
+ :slotted(table) {
66
+ inline-size: 100%;
67
+ border-collapse: separate;
68
+ border-spacing: 0;
69
+ color: var(--ui-text-primary);
70
+ font-size: var(--ui-font-size-sm);
71
+ line-height: var(--ui-line-height);
72
+ font-variant-numeric: tabular-nums;
73
+ }
74
+
75
+ :slotted(caption) {
76
+ padding-block-end: var(--ui-space-2);
77
+ color: var(--ui-text-secondary);
78
+ font-weight: var(--ui-font-medium);
79
+ text-align: start;
80
+ }
81
+
82
+ /* Every row is at least one control tall, so a row holding a checkbox, select, or input keeps the
83
+ rhythm of the rows around it. A cell's height is its minimum. */
84
+ :slotted(th),
85
+ :slotted(td) {
86
+ block-size: var(--_row-size);
87
+ padding: var(--_pad-block) var(--_pad-inline);
88
+ border-block-end: 1px solid var(--ui-border-subtle);
89
+ text-align: start;
90
+ vertical-align: middle;
91
+ }
92
+
93
+ :slotted(thead th) {
94
+ block-size: auto;
95
+ padding-block: var(--ui-space-2);
96
+ border-block-end-color: var(--ui-border-default);
97
+ background: var(--ui-surface);
98
+ color: var(--ui-text-muted);
99
+ font-weight: var(--ui-font-medium);
100
+ white-space: nowrap;
101
+ vertical-align: bottom;
102
+ }
103
+
104
+ :slotted(tbody th) {
105
+ font-weight: var(--ui-font-medium);
106
+ }
107
+
108
+ :slotted(tbody tr:last-child > *) {
109
+ border-block-end-width: 0;
110
+ }
111
+
112
+ :slotted([data-ui-align="center"]) {
113
+ text-align: center;
114
+ }
115
+
116
+ :slotted([data-ui-align="end"]) {
117
+ text-align: end;
118
+ }
119
+
120
+ :host-state([stickyHeader]) :slotted(thead th) {
121
+ position: sticky;
122
+ inset-block-start: 0;
123
+ z-index: 1;
124
+ }
125
+
126
+ /* A component that sets its display still honours the hidden attribute on its root. */
127
+ :host[hidden] {
128
+ display: none;
129
+ }
130
+ </style>
131
+ </template>
@@ -0,0 +1,55 @@
1
+ // While the table is wider than the root, or taller than a root of bounded height, the root scrolls
2
+ // it: it becomes a region a keyboard can focus and scroll with the arrow keys, named by the table's caption unless the author named it. When
3
+ // the table fits, the root is plain again, so it is not an empty stop in the tab order.
4
+ let captions = 0;
5
+
6
+ export default function controller(host) {
7
+ const element = host.element;
8
+ const authored = element.hasAttribute("tabindex") || element.hasAttribute("role");
9
+ if (authored) return undefined;
10
+
11
+ // What this controller set, so a table moved or re-inserted starts plain and is judged again.
12
+ let region = false;
13
+ let labelled = false;
14
+ const plain = () => {
15
+ if (region) {
16
+ element.removeAttribute("role");
17
+ element.removeAttribute("tabindex");
18
+ }
19
+ if (labelled) element.removeAttribute("aria-labelledby");
20
+ region = labelled = false;
21
+ };
22
+ const update = () => {
23
+ const scrolls = element.scrollWidth > element.clientWidth || element.scrollHeight > element.clientHeight;
24
+ if (!scrolls) return plain();
25
+ element.setAttribute("role", "region");
26
+ element.setAttribute("tabindex", "0");
27
+ region = true;
28
+ const caption = element.querySelector(":scope > table > caption");
29
+ if (caption && !element.hasAttribute("aria-label") && !element.hasAttribute("aria-labelledby")) {
30
+ caption.id ||= `ui-table-caption-${++captions}`;
31
+ element.setAttribute("aria-labelledby", caption.id);
32
+ labelled = true;
33
+ }
34
+ };
35
+
36
+ const resize = new ResizeObserver(update);
37
+ const observe = () => {
38
+ resize.disconnect();
39
+ resize.observe(element);
40
+ for (const child of element.children) resize.observe(child);
41
+ };
42
+ // A table rendered or replaced later is observed too.
43
+ const children = new MutationObserver(() => {
44
+ observe();
45
+ update();
46
+ });
47
+ children.observe(element, { childList: true });
48
+ observe();
49
+ update();
50
+ return () => {
51
+ resize.disconnect();
52
+ children.disconnect();
53
+ plain();
54
+ };
55
+ }
@@ -8,6 +8,9 @@
8
8
  <prop name="readonly" type="boolean" default="false">Allows selection without allowing edits.</prop>
9
9
  <prop name="required" type="boolean" default="false">Marks the control as required for form validation.</prop>
10
10
  <prop name="rows" type="integer" default="4">Initial number of visible text rows.</prop>
11
+ <prop name="size" type="sm | md | lg" default="md"
12
+ >Padding and type size, matching Input's sizes so a textarea sits with fields of the same size; rows sets its height. sm has tighter padding and smaller
13
+ text, for compact forms; lg has roomier padding. Under a coarse pointer, sm keeps body-size text.</prop>
11
14
  </defs>
12
15
  <textarea :value="value" :disabled="disabled" :readonly="readonly" :required="required" :aria-invalid="invalid" :rows="rows"></textarea>
13
16
  <style>
@@ -88,6 +91,23 @@
88
91
  cursor: not-allowed;
89
92
  }
90
93
 
94
+ :host-state([size="sm"]) {
95
+ padding: var(--ui-space-1) var(--ui-space-2);
96
+ font-size: var(--ui-font-size-sm);
97
+ }
98
+
99
+ :host-state([size="lg"]) {
100
+ padding: var(--ui-space-3) var(--ui-space-4);
101
+ font-size: var(--ui-font-size-md);
102
+ }
103
+
104
+ /* Below 16px, iOS zooms the page into the focused field, so touch keeps body-size text. */
105
+ @media (pointer: coarse) {
106
+ :host-state([size="sm"]) {
107
+ font-size: inherit;
108
+ }
109
+ }
110
+
91
111
  /* A component that sets its display still honours the hidden attribute on its root. */
92
112
  :host[hidden] {
93
113
  display: none;