@realmorg/ui 2.0.0-alpha.2 → 2.0.0-alpha.4

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 (65) hide show
  1. package/README.md +6 -6
  2. package/components/accordion.html +40 -1
  3. package/components/alert-dialog.html +40 -0
  4. package/components/alert.html +14 -0
  5. package/components/aspect-ratio.html +6 -0
  6. package/components/attachment.html +14 -0
  7. package/components/avatar.html +22 -0
  8. package/components/badge.html +6 -0
  9. package/components/breadcrumb.html +30 -0
  10. package/components/bubble.html +10 -0
  11. package/components/button-group.html +14 -0
  12. package/components/button.html +41 -22
  13. package/components/calendar.html +6 -0
  14. package/components/card.html +30 -0
  15. package/components/carousel.html +22 -0
  16. package/components/chart.html +6 -0
  17. package/components/checkbox.html +6 -0
  18. package/components/code-block.html +6 -0
  19. package/components/collapsible.html +14 -0
  20. package/components/combobox.html +26 -0
  21. package/components/command.html +12 -0
  22. package/components/context-menu.html +14 -0
  23. package/components/dialog.html +49 -3
  24. package/components/direction.html +6 -0
  25. package/components/drawer.html +46 -3
  26. package/components/dropdown-menu.html +75 -1
  27. package/components/empty.html +26 -0
  28. package/components/field.html +26 -0
  29. package/components/hover-card.html +41 -1
  30. package/components/input-group.html +16 -0
  31. package/components/input-otp.html +12 -0
  32. package/components/input.html +12 -0
  33. package/components/item.html +38 -0
  34. package/components/kbd.html +10 -0
  35. package/components/label.html +6 -0
  36. package/components/marker.html +6 -0
  37. package/components/menubar.html +36 -1
  38. package/components/message-scroller.html +6 -0
  39. package/components/message.html +18 -0
  40. package/components/navigation-menu.html +29 -0
  41. package/components/pagination.html +6 -0
  42. package/components/popover.html +53 -1
  43. package/components/progress.html +6 -0
  44. package/components/radio-group.html +6 -0
  45. package/components/resizable.html +14 -0
  46. package/components/scroll-area.html +6 -0
  47. package/components/select.html +12 -0
  48. package/components/separator.html +6 -0
  49. package/components/sheet.html +30 -0
  50. package/components/sidebar.html +52 -2
  51. package/components/skeleton.html +6 -0
  52. package/components/slider.html +28 -1
  53. package/components/spinner.html +6 -0
  54. package/components/switch.html +6 -0
  55. package/components/table.html +6 -0
  56. package/components/tabs.html +18 -0
  57. package/components/textarea.html +12 -0
  58. package/components/toaster.html +6 -0
  59. package/components/toggle-group.html +6 -0
  60. package/components/toggle.html +6 -0
  61. package/components/tooltip.html +43 -1
  62. package/dist/theme.css +13 -0
  63. package/dist/ui.html +1171 -37
  64. package/package.json +4 -3
  65. package/theme.css +13 -0
package/dist/ui.html CHANGED
@@ -14,6 +14,12 @@
14
14
  </script>
15
15
 
16
16
  <style>
17
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
18
+ touch-action does not reach shadow content in iOS Safari. */
19
+ :host,
20
+ * {
21
+ touch-action: manipulation;
22
+ }
17
23
  :host {
18
24
  display: block;
19
25
  }
@@ -41,6 +47,10 @@
41
47
  </script>
42
48
 
43
49
  <style>
50
+ :host,
51
+ * {
52
+ touch-action: manipulation;
53
+ }
44
54
  :host {
45
55
  display: block;
46
56
  border-bottom: 1px solid var(--border);
@@ -79,11 +89,32 @@
79
89
  width: calc(var(--spacing) * 4);
80
90
  height: calc(var(--spacing) * 4);
81
91
  color: var(--muted-foreground);
82
- transition: rotate 0.2s;
92
+ transition: rotate var(--default-transition-duration) var(--default-transition-timing-function);
83
93
  }
84
94
  details[open] svg {
85
95
  rotate: 180deg;
86
96
  }
97
+ details::details-content {
98
+ display: grid;
99
+ grid-template-rows: 0fr;
100
+ transition:
101
+ grid-template-rows var(--default-transition-duration) var(--default-transition-timing-function),
102
+ content-visibility var(--default-transition-duration) allow-discrete;
103
+ }
104
+ details[open]::details-content {
105
+ grid-template-rows: 1fr;
106
+ }
107
+ slot:not([name]) {
108
+ display: block;
109
+ min-height: 0;
110
+ overflow: hidden;
111
+ }
112
+ @media (prefers-reduced-motion: reduce) {
113
+ svg,
114
+ details::details-content {
115
+ transition: none;
116
+ }
117
+ }
87
118
  </style>
88
119
 
89
120
  <details _open="@open" ontoggle="toggled(this)">
@@ -97,6 +128,10 @@
97
128
 
98
129
  <custom-element name="ui-accordion-trigger" slot="trigger">
99
130
  <style>
131
+ :host,
132
+ * {
133
+ touch-action: manipulation;
134
+ }
100
135
  :host {
101
136
  display: flex;
102
137
  flex: 1;
@@ -110,6 +145,10 @@
110
145
 
111
146
  <custom-element name="ui-accordion-content">
112
147
  <style>
148
+ :host,
149
+ * {
150
+ touch-action: manipulation;
151
+ }
113
152
  :host {
114
153
  display: block;
115
154
  padding-bottom: calc(var(--spacing) * 4);
@@ -181,6 +220,12 @@
181
220
  </script>
182
221
 
183
222
  <style>
223
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
224
+ touch-action does not reach shadow content in iOS Safari. */
225
+ :host,
226
+ * {
227
+ touch-action: manipulation;
228
+ }
184
229
  :host {
185
230
  display: contents;
186
231
  }
@@ -188,6 +233,10 @@
188
233
  box-sizing: border-box;
189
234
  width: 100%;
190
235
  max-width: min(calc(var(--spacing) * 128), calc(100% - calc(var(--spacing) * 8)));
236
+ /* See ui-dialog: an explicit height keeps it from stretching, svh from resizing on scroll. */
237
+ height: fit-content;
238
+ max-height: calc(100svh - calc(var(--spacing) * 8));
239
+ overflow: auto;
191
240
  padding: calc(var(--spacing) * 6);
192
241
  border: 1px solid var(--border);
193
242
  border-radius: var(--radius-lg);
@@ -197,6 +246,8 @@
197
246
  }
198
247
  dialog[open] {
199
248
  display: grid;
249
+ /* Safari stretches a grid overlay to its max-height unless the rows are packed at the start. */
250
+ align-content: start;
200
251
  gap: calc(var(--spacing) * 4);
201
252
  }
202
253
  dialog::backdrop {
@@ -221,6 +272,10 @@
221
272
 
222
273
  <custom-element name="ui-alert-dialog-trigger" slot="trigger">
223
274
  <style>
275
+ :host,
276
+ * {
277
+ touch-action: manipulation;
278
+ }
224
279
  :host,
225
280
  span {
226
281
  display: contents;
@@ -232,6 +287,10 @@
232
287
 
233
288
  <custom-element name="ui-alert-dialog-header" slot="header">
234
289
  <style>
290
+ :host,
291
+ * {
292
+ touch-action: manipulation;
293
+ }
235
294
  :host {
236
295
  display: grid;
237
296
  gap: calc(var(--spacing) * 2);
@@ -243,6 +302,10 @@
243
302
 
244
303
  <custom-element name="ui-alert-dialog-title">
245
304
  <style>
305
+ :host,
306
+ * {
307
+ touch-action: manipulation;
308
+ }
246
309
  :host {
247
310
  display: block;
248
311
  }
@@ -258,6 +321,10 @@
258
321
 
259
322
  <custom-element name="ui-alert-dialog-description">
260
323
  <style>
324
+ :host,
325
+ * {
326
+ touch-action: manipulation;
327
+ }
261
328
  :host {
262
329
  display: block;
263
330
  }
@@ -273,6 +340,10 @@
273
340
 
274
341
  <custom-element name="ui-alert-dialog-footer" slot="footer">
275
342
  <style>
343
+ :host,
344
+ * {
345
+ touch-action: manipulation;
346
+ }
276
347
  :host {
277
348
  display: flex;
278
349
  flex-wrap: wrap;
@@ -293,6 +364,10 @@
293
364
  </script>
294
365
 
295
366
  <style>
367
+ :host,
368
+ * {
369
+ touch-action: manipulation;
370
+ }
296
371
  :host {
297
372
  display: inline-flex;
298
373
  }
@@ -307,6 +382,10 @@
307
382
  </script>
308
383
 
309
384
  <style>
385
+ :host,
386
+ * {
387
+ touch-action: manipulation;
388
+ }
310
389
  :host {
311
390
  display: inline-flex;
312
391
  }
@@ -323,6 +402,12 @@
323
402
  </script>
324
403
 
325
404
  <style>
405
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
406
+ touch-action does not reach shadow content in iOS Safari. */
407
+ :host,
408
+ * {
409
+ touch-action: manipulation;
410
+ }
326
411
  :host {
327
412
  position: relative;
328
413
  display: grid;
@@ -361,6 +446,10 @@
361
446
 
362
447
  <custom-element name="ui-alert-title">
363
448
  <style>
449
+ :host,
450
+ * {
451
+ touch-action: manipulation;
452
+ }
364
453
  :host {
365
454
  display: block;
366
455
  min-height: calc(var(--spacing) * 4);
@@ -374,6 +463,10 @@
374
463
 
375
464
  <custom-element name="ui-alert-description">
376
465
  <style>
466
+ :host,
467
+ * {
468
+ touch-action: manipulation;
469
+ }
377
470
  :host {
378
471
  display: grid;
379
472
  justify-items: start;
@@ -391,6 +484,12 @@
391
484
  </script>
392
485
 
393
486
  <style>
487
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
488
+ touch-action does not reach shadow content in iOS Safari. */
489
+ :host,
490
+ * {
491
+ touch-action: manipulation;
492
+ }
394
493
  :host {
395
494
  display: block;
396
495
  position: relative;
@@ -415,6 +514,12 @@
415
514
  </script>
416
515
 
417
516
  <style>
517
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
518
+ touch-action does not reach shadow content in iOS Safari. */
519
+ :host,
520
+ * {
521
+ touch-action: manipulation;
522
+ }
418
523
  :host {
419
524
  display: inline-flex;
420
525
  align-items: center;
@@ -459,6 +564,10 @@
459
564
 
460
565
  <custom-element name="ui-attachment-media" slot="media">
461
566
  <style>
567
+ :host,
568
+ * {
569
+ touch-action: manipulation;
570
+ }
462
571
  :host {
463
572
  display: grid;
464
573
  place-content: center;
@@ -485,6 +594,10 @@
485
594
 
486
595
  <custom-element name="ui-attachment-actions" slot="actions">
487
596
  <style>
597
+ :host,
598
+ * {
599
+ touch-action: manipulation;
600
+ }
488
601
  :host {
489
602
  display: flex;
490
603
  gap: calc(var(--spacing) * 1);
@@ -514,6 +627,12 @@
514
627
  </script>
515
628
 
516
629
  <style>
630
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
631
+ touch-action does not reach shadow content in iOS Safari. */
632
+ :host,
633
+ * {
634
+ touch-action: manipulation;
635
+ }
517
636
  :host {
518
637
  position: relative;
519
638
  display: inline-flex;
@@ -551,6 +670,10 @@
551
670
  </script>
552
671
 
553
672
  <style>
673
+ :host,
674
+ * {
675
+ touch-action: manipulation;
676
+ }
554
677
  :host {
555
678
  display: contents;
556
679
  }
@@ -568,6 +691,10 @@
568
691
 
569
692
  <custom-element name="ui-avatar-fallback">
570
693
  <style>
694
+ :host,
695
+ * {
696
+ touch-action: manipulation;
697
+ }
571
698
  :host {
572
699
  display: contents;
573
700
  }
@@ -588,6 +715,10 @@
588
715
 
589
716
  <custom-element name="ui-avatar-group" role="group">
590
717
  <style>
718
+ :host,
719
+ * {
720
+ touch-action: manipulation;
721
+ }
591
722
  :host {
592
723
  display: flex;
593
724
  align-items: center;
@@ -605,6 +736,10 @@
605
736
 
606
737
  <custom-element name="ui-avatar-group-count">
607
738
  <style>
739
+ :host,
740
+ * {
741
+ touch-action: manipulation;
742
+ }
608
743
  :host {
609
744
  display: inline-flex;
610
745
  flex-shrink: 0;
@@ -631,6 +766,12 @@
631
766
  </script>
632
767
 
633
768
  <style>
769
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
770
+ touch-action does not reach shadow content in iOS Safari. */
771
+ :host,
772
+ * {
773
+ touch-action: manipulation;
774
+ }
634
775
  :host {
635
776
  display: inline-flex;
636
777
  align-items: center;
@@ -685,6 +826,12 @@
685
826
  </script>
686
827
 
687
828
  <style>
829
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
830
+ touch-action does not reach shadow content in iOS Safari. */
831
+ :host,
832
+ * {
833
+ touch-action: manipulation;
834
+ }
688
835
  :host {
689
836
  display: block;
690
837
  }
@@ -695,6 +842,10 @@
695
842
 
696
843
  <custom-element name="ui-breadcrumb-list">
697
844
  <style>
845
+ :host,
846
+ * {
847
+ touch-action: manipulation;
848
+ }
698
849
  :host {
699
850
  display: block;
700
851
  }
@@ -717,6 +868,10 @@
717
868
 
718
869
  <custom-element name="ui-breadcrumb-item" role="listitem">
719
870
  <style>
871
+ :host,
872
+ * {
873
+ touch-action: manipulation;
874
+ }
720
875
  :host {
721
876
  display: inline-flex;
722
877
  align-items: center;
@@ -733,6 +888,10 @@
733
888
  </script>
734
889
 
735
890
  <style>
891
+ :host,
892
+ * {
893
+ touch-action: manipulation;
894
+ }
736
895
  a {
737
896
  color: inherit;
738
897
  text-decoration: none;
@@ -753,6 +912,10 @@
753
912
 
754
913
  <custom-element name="ui-breadcrumb-page">
755
914
  <style>
915
+ :host,
916
+ * {
917
+ touch-action: manipulation;
918
+ }
756
919
  span {
757
920
  font-weight: var(--font-weight-normal);
758
921
  color: var(--foreground);
@@ -768,6 +931,10 @@
768
931
  </script>
769
932
 
770
933
  <style>
934
+ :host,
935
+ * {
936
+ touch-action: manipulation;
937
+ }
771
938
  :host {
772
939
  display: inline-flex;
773
940
  align-items: center;
@@ -784,6 +951,10 @@
784
951
 
785
952
  <custom-element name="ui-breadcrumb-ellipsis">
786
953
  <style>
954
+ :host,
955
+ * {
956
+ touch-action: manipulation;
957
+ }
787
958
  :host {
788
959
  display: inline-flex;
789
960
  align-items: center;
@@ -818,6 +989,12 @@
818
989
  </script>
819
990
 
820
991
  <style>
992
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
993
+ touch-action does not reach shadow content in iOS Safari. */
994
+ :host,
995
+ * {
996
+ touch-action: manipulation;
997
+ }
821
998
  :host {
822
999
  display: flex;
823
1000
  flex-direction: column;
@@ -867,6 +1044,10 @@
867
1044
 
868
1045
  <custom-element name="ui-bubble-reactions" slot="reactions">
869
1046
  <style>
1047
+ :host,
1048
+ * {
1049
+ touch-action: manipulation;
1050
+ }
870
1051
  :host {
871
1052
  display: flex;
872
1053
  flex-wrap: wrap;
@@ -883,6 +1064,12 @@
883
1064
  </script>
884
1065
 
885
1066
  <style>
1067
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
1068
+ touch-action does not reach shadow content in iOS Safari. */
1069
+ :host,
1070
+ * {
1071
+ touch-action: manipulation;
1072
+ }
886
1073
  :host {
887
1074
  display: inline-flex;
888
1075
  align-items: stretch;
@@ -912,6 +1099,10 @@
912
1099
 
913
1100
  <custom-element name="ui-button-group-text">
914
1101
  <style>
1102
+ :host,
1103
+ * {
1104
+ touch-action: manipulation;
1105
+ }
915
1106
  :host {
916
1107
  display: flex;
917
1108
  align-items: center;
@@ -948,6 +1139,10 @@
948
1139
  </script>
949
1140
 
950
1141
  <style>
1142
+ :host,
1143
+ * {
1144
+ touch-action: manipulation;
1145
+ }
951
1146
  :host {
952
1147
  position: relative;
953
1148
  z-index: 1;
@@ -975,6 +1170,9 @@
975
1170
  popovertargetaction: prop.enum(["toggle", "show", "hide"]),
976
1171
  commandfor: prop.string(),
977
1172
  command: prop.string(),
1173
+ href: prop.string(),
1174
+ target: prop.string(),
1175
+ rel: prop.string(),
978
1176
  });
979
1177
 
980
1178
  const NATIVE_COMMANDS = "commandForElement" in HTMLButtonElement.prototype;
@@ -1026,6 +1224,12 @@
1026
1224
  </script>
1027
1225
 
1028
1226
  <style>
1227
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
1228
+ touch-action does not reach shadow content in iOS Safari. */
1229
+ :host,
1230
+ * {
1231
+ touch-action: manipulation;
1232
+ }
1029
1233
  :host {
1030
1234
  display: inline-flex;
1031
1235
  vertical-align: middle;
@@ -1033,7 +1237,11 @@
1033
1237
  :host([hidden]) {
1034
1238
  display: none;
1035
1239
  }
1036
- button {
1240
+ is-visible,
1241
+ is-hidden {
1242
+ display: contents;
1243
+ }
1244
+ .control {
1037
1245
  all: unset;
1038
1246
  box-sizing: border-box;
1039
1247
  display: inline-flex;
@@ -1061,14 +1269,15 @@
1061
1269
  }
1062
1270
  /* Mixing toward the text colour darkens a light-mode fill and lightens a dark-mode one, so
1063
1271
  hover never lowers the text contrast. */
1064
- button:hover {
1272
+ .control:hover {
1065
1273
  background: color-mix(in oklab, var(--primary) 88%, var(--foreground));
1066
1274
  }
1067
- button:focus-visible {
1275
+ .control:focus-visible {
1068
1276
  border-color: var(--ring);
1069
1277
  box-shadow: var(--focus-ring);
1070
1278
  }
1071
- button:disabled {
1279
+ .control:disabled,
1280
+ :host([disabled]) a.control {
1072
1281
  pointer-events: none;
1073
1282
  opacity: 0.5;
1074
1283
  }
@@ -1078,77 +1287,82 @@
1078
1287
  flex-shrink: 0;
1079
1288
  pointer-events: none;
1080
1289
  }
1081
- :host(:state(attr-variant-is-destructive)) button {
1290
+ :host(:state(attr-variant-is-destructive)) .control {
1082
1291
  background: var(--destructive);
1083
1292
  color: var(--destructive-foreground);
1084
1293
  }
1085
- :host(:state(attr-variant-is-destructive)) button:hover {
1294
+ :host(:state(attr-variant-is-destructive)) .control:hover {
1086
1295
  background: color-mix(in oklab, var(--destructive) 88%, var(--foreground));
1087
1296
  }
1088
- :host(:state(attr-variant-is-outline)) button {
1297
+ :host(:state(attr-variant-is-outline)) .control {
1089
1298
  border-color: var(--border);
1090
1299
  background: var(--background);
1091
1300
  color: var(--foreground);
1092
1301
  box-shadow: var(--shadow-xs);
1093
1302
  }
1094
- :host(:state(attr-variant-is-outline)) button:hover,
1095
- :host(:state(attr-variant-is-ghost)) button:hover {
1303
+ :host(:state(attr-variant-is-outline)) .control:hover,
1304
+ :host(:state(attr-variant-is-ghost)) .control:hover {
1096
1305
  background: var(--accent);
1097
1306
  color: var(--accent-foreground);
1098
1307
  }
1099
- :host(:state(attr-variant-is-secondary)) button {
1308
+ :host(:state(attr-variant-is-secondary)) .control {
1100
1309
  background: var(--secondary);
1101
1310
  color: var(--secondary-foreground);
1102
1311
  }
1103
- :host(:state(attr-variant-is-secondary)) button:hover {
1312
+ :host(:state(attr-variant-is-secondary)) .control:hover {
1104
1313
  background: color-mix(in oklab, var(--secondary) 80%, transparent);
1105
1314
  }
1106
- :host(:state(attr-variant-is-ghost)) button {
1315
+ :host(:state(attr-variant-is-ghost)) .control {
1107
1316
  background: transparent;
1108
1317
  color: var(--foreground);
1109
1318
  }
1110
- :host(:state(attr-variant-is-link)) button {
1319
+ :host(:state(attr-variant-is-link)) .control {
1111
1320
  background: transparent;
1112
1321
  color: var(--primary);
1113
1322
  text-underline-offset: 4px;
1114
1323
  }
1115
- :host(:state(attr-variant-is-link)) button:hover {
1324
+ :host(:state(attr-variant-is-link)) .control:hover {
1116
1325
  text-decoration: underline;
1117
1326
  }
1118
- :host(:state(attr-size-is-xs)) button {
1327
+ :host(:state(attr-size-is-xs)) .control {
1119
1328
  height: calc(var(--spacing) * 6);
1120
1329
  gap: calc(var(--spacing) * 1);
1121
1330
  padding: 0 calc(var(--spacing) * 2);
1122
1331
  font-size: var(--text-xs);
1123
1332
  }
1124
- :host(:state(attr-size-is-sm)) button {
1333
+ :host(:state(attr-size-is-sm)) .control {
1125
1334
  height: calc(var(--spacing) * 8);
1126
1335
  gap: calc(var(--spacing) * 1.5);
1127
1336
  padding: 0 calc(var(--spacing) * 3);
1128
1337
  }
1129
- :host(:state(attr-size-is-lg)) button {
1338
+ :host(:state(attr-size-is-lg)) .control {
1130
1339
  height: calc(var(--spacing) * 10);
1131
1340
  padding: 0 calc(var(--spacing) * 6);
1132
1341
  }
1133
- :host(:is(:state(attr-size-is-icon), :state(attr-size-is-icon-xs), :state(attr-size-is-icon-sm), :state(attr-size-is-icon-lg))) button {
1342
+ :host(:is(:state(attr-size-is-icon), :state(attr-size-is-icon-xs), :state(attr-size-is-icon-sm), :state(attr-size-is-icon-lg))) .control {
1134
1343
  padding: 0;
1135
1344
  width: calc(var(--spacing) * 9);
1136
1345
  }
1137
- :host(:state(attr-size-is-icon-xs)) button {
1346
+ :host(:state(attr-size-is-icon-xs)) .control {
1138
1347
  width: calc(var(--spacing) * 6);
1139
1348
  height: calc(var(--spacing) * 6);
1140
1349
  }
1141
- :host(:state(attr-size-is-icon-sm)) button {
1350
+ :host(:state(attr-size-is-icon-sm)) .control {
1142
1351
  width: calc(var(--spacing) * 8);
1143
1352
  height: calc(var(--spacing) * 8);
1144
1353
  }
1145
- :host(:state(attr-size-is-icon-lg)) button {
1354
+ :host(:state(attr-size-is-icon-lg)) .control {
1146
1355
  width: calc(var(--spacing) * 10);
1147
1356
  height: calc(var(--spacing) * 10);
1148
1357
  }
1149
1358
  </style>
1150
1359
 
1151
- <button type="button" ref="button" part="button" _aria-label="@label" _aria-description="@description" _disabled="@disabled" onclick="activate()"><slot></slot></button>
1360
+ <is-visible when="@href">
1361
+ <a class="control" ref="link" part="button" _href="@href" _target="@target" _rel="@rel" _aria-label="@label" _aria-description="@description"><slot></slot></a>
1362
+ </is-visible>
1363
+ <is-hidden when="@href">
1364
+ <button class="control" type="button" ref="button" part="button" _aria-label="@label" _aria-description="@description" _disabled="@disabled" onclick="activate()"><slot></slot></button>
1365
+ </is-hidden>
1152
1366
  </custom-element>
1153
1367
 
1154
1368
  <custom-element name="ui-calendar" form-associated>
@@ -1231,6 +1445,12 @@
1231
1445
  </script>
1232
1446
 
1233
1447
  <style>
1448
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
1449
+ touch-action does not reach shadow content in iOS Safari. */
1450
+ :host,
1451
+ * {
1452
+ touch-action: manipulation;
1453
+ }
1234
1454
  :host {
1235
1455
  display: inline-block;
1236
1456
  padding: calc(var(--spacing) * 3);
@@ -1322,6 +1542,12 @@
1322
1542
 
1323
1543
  <custom-element name="ui-card">
1324
1544
  <style>
1545
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
1546
+ touch-action does not reach shadow content in iOS Safari. */
1547
+ :host,
1548
+ * {
1549
+ touch-action: manipulation;
1550
+ }
1325
1551
  :host {
1326
1552
  display: flex;
1327
1553
  flex-direction: column;
@@ -1340,6 +1566,10 @@
1340
1566
 
1341
1567
  <custom-element name="ui-card-header">
1342
1568
  <style>
1569
+ :host,
1570
+ * {
1571
+ touch-action: manipulation;
1572
+ }
1343
1573
  :host {
1344
1574
  display: grid;
1345
1575
  grid-auto-rows: min-content;
@@ -1361,6 +1591,10 @@
1361
1591
  </script>
1362
1592
 
1363
1593
  <style>
1594
+ :host,
1595
+ * {
1596
+ touch-action: manipulation;
1597
+ }
1364
1598
  :host {
1365
1599
  display: block;
1366
1600
  }
@@ -1377,6 +1611,10 @@
1377
1611
 
1378
1612
  <custom-element name="ui-card-description">
1379
1613
  <style>
1614
+ :host,
1615
+ * {
1616
+ touch-action: manipulation;
1617
+ }
1380
1618
  :host {
1381
1619
  display: block;
1382
1620
  font-size: var(--text-sm);
@@ -1389,6 +1627,10 @@
1389
1627
 
1390
1628
  <custom-element name="ui-card-action">
1391
1629
  <style>
1630
+ :host,
1631
+ * {
1632
+ touch-action: manipulation;
1633
+ }
1392
1634
  :host {
1393
1635
  display: block;
1394
1636
  grid-column: 2;
@@ -1402,6 +1644,10 @@
1402
1644
 
1403
1645
  <custom-element name="ui-card-content">
1404
1646
  <style>
1647
+ :host,
1648
+ * {
1649
+ touch-action: manipulation;
1650
+ }
1405
1651
  :host {
1406
1652
  display: block;
1407
1653
  padding: 0 calc(var(--spacing) * 6);
@@ -1413,6 +1659,10 @@
1413
1659
 
1414
1660
  <custom-element name="ui-card-footer">
1415
1661
  <style>
1662
+ :host,
1663
+ * {
1664
+ touch-action: manipulation;
1665
+ }
1416
1666
  :host {
1417
1667
  display: flex;
1418
1668
  align-items: center;
@@ -1499,6 +1749,12 @@
1499
1749
  </script>
1500
1750
 
1501
1751
  <style>
1752
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
1753
+ touch-action does not reach shadow content in iOS Safari. */
1754
+ :host,
1755
+ * {
1756
+ touch-action: manipulation;
1757
+ }
1502
1758
  :host {
1503
1759
  --carousel-direction: row;
1504
1760
  --carousel-axis: x;
@@ -1554,6 +1810,10 @@
1554
1810
 
1555
1811
  <custom-element name="ui-carousel-content">
1556
1812
  <style>
1813
+ :host,
1814
+ * {
1815
+ touch-action: manipulation;
1816
+ }
1557
1817
  :host {
1558
1818
  display: flex;
1559
1819
  min-width: 0;
@@ -1586,6 +1846,10 @@
1586
1846
  </script>
1587
1847
 
1588
1848
  <style>
1849
+ :host,
1850
+ * {
1851
+ touch-action: manipulation;
1852
+ }
1589
1853
  :host {
1590
1854
  display: block;
1591
1855
  box-sizing: border-box;
@@ -1601,6 +1865,10 @@
1601
1865
 
1602
1866
  <custom-element name="ui-carousel-previous">
1603
1867
  <style>
1868
+ :host,
1869
+ * {
1870
+ touch-action: manipulation;
1871
+ }
1604
1872
  :host {
1605
1873
  display: inline-flex;
1606
1874
  }
@@ -1611,6 +1879,10 @@
1611
1879
 
1612
1880
  <custom-element name="ui-carousel-next">
1613
1881
  <style>
1882
+ :host,
1883
+ * {
1884
+ touch-action: manipulation;
1885
+ }
1614
1886
  :host {
1615
1887
  display: inline-flex;
1616
1888
  }
@@ -1705,6 +1977,12 @@
1705
1977
  </script>
1706
1978
 
1707
1979
  <style>
1980
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
1981
+ touch-action does not reach shadow content in iOS Safari. */
1982
+ :host,
1983
+ * {
1984
+ touch-action: manipulation;
1985
+ }
1708
1986
  :host {
1709
1987
  display: block;
1710
1988
  width: 100%;
@@ -1799,6 +2077,12 @@
1799
2077
  </script>
1800
2078
 
1801
2079
  <style>
2080
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
2081
+ touch-action does not reach shadow content in iOS Safari. */
2082
+ :host,
2083
+ * {
2084
+ touch-action: manipulation;
2085
+ }
1802
2086
  :host(:state(attr-checked)) label {
1803
2087
  color: var(--foreground);
1804
2088
  }
@@ -1890,6 +2174,12 @@
1890
2174
  </script>
1891
2175
 
1892
2176
  <style>
2177
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
2178
+ touch-action does not reach shadow content in iOS Safari. */
2179
+ :host,
2180
+ * {
2181
+ touch-action: manipulation;
2182
+ }
1893
2183
  :host {
1894
2184
  position: relative;
1895
2185
  display: block;
@@ -1949,6 +2239,12 @@
1949
2239
  </script>
1950
2240
 
1951
2241
  <style>
2242
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
2243
+ touch-action does not reach shadow content in iOS Safari. */
2244
+ :host,
2245
+ * {
2246
+ touch-action: manipulation;
2247
+ }
1952
2248
  :host {
1953
2249
  display: block;
1954
2250
  }
@@ -1978,6 +2274,10 @@
1978
2274
 
1979
2275
  <custom-element name="ui-collapsible-trigger" slot="trigger">
1980
2276
  <style>
2277
+ :host,
2278
+ * {
2279
+ touch-action: manipulation;
2280
+ }
1981
2281
  :host {
1982
2282
  display: flex;
1983
2283
  align-items: center;
@@ -1998,6 +2298,10 @@
1998
2298
 
1999
2299
  <custom-element name="ui-collapsible-content">
2000
2300
  <style>
2301
+ :host,
2302
+ * {
2303
+ touch-action: manipulation;
2304
+ }
2001
2305
  :host {
2002
2306
  display: flex;
2003
2307
  flex-direction: column;
@@ -2067,6 +2371,12 @@
2067
2371
  combo.query = "";
2068
2372
  combo.active = Math.max(0, combo.visible.findIndex((item) => item.chosen));
2069
2373
  refs.list.showPopover();
2374
+ // Once open, the list keeps the side it opened on. Re-deciding while the page scrolls makes
2375
+ // it jump, because iOS Safari's toolbar changes the viewport height mid-scroll.
2376
+ // Firefox holds a box that does not fit against the screen edge, so touching it counts too.
2377
+ internals.states.delete("flipped");
2378
+ const box = refs.list.getBoundingClientRect();
2379
+ if (box.bottom >= innerHeight || box.top <= 0) internals.states.add("flipped");
2070
2380
  }
2071
2381
 
2072
2382
  function close() {
@@ -2101,6 +2411,12 @@
2101
2411
  </script>
2102
2412
 
2103
2413
  <style>
2414
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
2415
+ touch-action does not reach shadow content in iOS Safari. */
2416
+ :host,
2417
+ * {
2418
+ touch-action: manipulation;
2419
+ }
2104
2420
  :host {
2105
2421
  display: block;
2106
2422
  min-width: calc(var(--spacing) * 48);
@@ -2134,6 +2450,12 @@
2134
2450
  font-size: var(--text-sm);
2135
2451
  outline: none;
2136
2452
  }
2453
+ /* iOS Safari zooms in on a focused field whose text is under 16px. */
2454
+ @media (pointer: coarse) {
2455
+ input {
2456
+ font-size: var(--text-base);
2457
+ }
2458
+ }
2137
2459
  .chevron {
2138
2460
  width: calc(var(--spacing) * 4);
2139
2461
  height: calc(var(--spacing) * 4);
@@ -2155,9 +2477,17 @@
2155
2477
  box-shadow: var(--shadow-md);
2156
2478
  inset: auto;
2157
2479
  position-anchor: --combobox;
2480
+ height: fit-content;
2158
2481
  position-area: bottom span-right;
2159
2482
  position-try-fallbacks: flip-block;
2160
2483
  }
2484
+ [role="listbox"]:popover-open {
2485
+ position-try-fallbacks: none;
2486
+ }
2487
+ :host(:state(flipped)) [role="listbox"] {
2488
+ position-area: top span-right;
2489
+ margin: 0 0 calc(var(--spacing) * 1);
2490
+ }
2161
2491
  [role="option"] {
2162
2492
  display: flex;
2163
2493
  align-items: center;
@@ -2309,6 +2639,12 @@
2309
2639
  </script>
2310
2640
 
2311
2641
  <style>
2642
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
2643
+ touch-action does not reach shadow content in iOS Safari. */
2644
+ :host,
2645
+ * {
2646
+ touch-action: manipulation;
2647
+ }
2312
2648
  :host {
2313
2649
  display: flex;
2314
2650
  flex-direction: column;
@@ -2340,6 +2676,12 @@
2340
2676
  font-size: var(--text-sm);
2341
2677
  outline: none;
2342
2678
  }
2679
+ /* iOS Safari zooms in on a focused field whose text is under 16px. */
2680
+ @media (pointer: coarse) {
2681
+ input {
2682
+ font-size: var(--text-base);
2683
+ }
2684
+ }
2343
2685
  input::placeholder {
2344
2686
  color: var(--muted-foreground);
2345
2687
  }
@@ -2437,6 +2779,12 @@
2437
2779
  </script>
2438
2780
 
2439
2781
  <style>
2782
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
2783
+ touch-action does not reach shadow content in iOS Safari. */
2784
+ :host,
2785
+ * {
2786
+ touch-action: manipulation;
2787
+ }
2440
2788
  :host {
2441
2789
  display: contents;
2442
2790
  }
@@ -2447,6 +2795,10 @@
2447
2795
 
2448
2796
  <custom-element name="ui-context-menu-trigger">
2449
2797
  <style>
2798
+ :host,
2799
+ * {
2800
+ touch-action: manipulation;
2801
+ }
2450
2802
  :host {
2451
2803
  display: block;
2452
2804
  }
@@ -2475,6 +2827,10 @@
2475
2827
  </script>
2476
2828
 
2477
2829
  <style>
2830
+ :host,
2831
+ * {
2832
+ touch-action: manipulation;
2833
+ }
2478
2834
  :host {
2479
2835
  position: fixed;
2480
2836
  box-sizing: border-box;
@@ -2569,12 +2925,21 @@
2569
2925
  observe(() => {
2570
2926
  const dialog = refs.dialog;
2571
2927
  if (!dialog || props.open === dialog.open) return;
2572
- if (props.open) dialog.showModal();
2573
- else dialog.close();
2928
+ if (!props.open) return dialog.close();
2929
+ dialog.showModal();
2930
+ // Left to the browser, focus lands on the first button (often close) with a focus ring after a
2931
+ // tap, and Firefox and WebKit ignore autofocus on slotted content.
2932
+ (host.querySelector("[autofocus]") ?? dialog).focus();
2574
2933
  });
2575
2934
  </script>
2576
2935
 
2577
2936
  <style>
2937
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
2938
+ touch-action does not reach shadow content in iOS Safari. */
2939
+ :host,
2940
+ * {
2941
+ touch-action: manipulation;
2942
+ }
2578
2943
  :host {
2579
2944
  display: contents;
2580
2945
  }
@@ -2582,7 +2947,10 @@
2582
2947
  box-sizing: border-box;
2583
2948
  width: 100%;
2584
2949
  max-width: min(calc(var(--spacing) * 128), calc(100% - calc(var(--spacing) * 8)));
2585
- max-height: calc(100% - calc(var(--spacing) * 8));
2950
+ /* Without an explicit height, a dialog pinned to both edges of the screen can stretch to
2951
+ full height. svh stays the same while a phone's address bar shows and hides. */
2952
+ height: fit-content;
2953
+ max-height: calc(100svh - calc(var(--spacing) * 8));
2586
2954
  padding: calc(var(--spacing) * 6);
2587
2955
  border: 1px solid var(--border);
2588
2956
  border-radius: var(--radius-lg);
@@ -2591,8 +2959,13 @@
2591
2959
  box-shadow: var(--shadow-lg);
2592
2960
  overflow: auto;
2593
2961
  }
2962
+ dialog:focus {
2963
+ outline: none;
2964
+ }
2594
2965
  dialog[open] {
2595
2966
  display: grid;
2967
+ /* Safari stretches a grid overlay to its max-height unless the rows are packed at the start. */
2968
+ align-content: start;
2596
2969
  gap: calc(var(--spacing) * 4);
2597
2970
  animation: zoom 0.2s ease-out;
2598
2971
  }
@@ -2611,7 +2984,11 @@
2611
2984
  display: flex;
2612
2985
  flex-direction: column;
2613
2986
  }
2987
+ [part="body"] {
2988
+ display: contents;
2989
+ }
2614
2990
  :host(:is(:state(attr-side-is-left), :state(attr-side-is-right))) [part="body"] {
2991
+ display: block;
2615
2992
  flex: 1;
2616
2993
  }
2617
2994
  :host(:state(attr-side-is-right)) dialog {
@@ -2680,6 +3057,7 @@
2680
3057
  <dialog
2681
3058
  ref="dialog"
2682
3059
  part="dialog"
3060
+ tabindex="-1"
2683
3061
  _aria-label="#dialog.label"
2684
3062
  _aria-description="#dialog.description"
2685
3063
  onclose="closed()"
@@ -2696,6 +3074,10 @@
2696
3074
 
2697
3075
  <custom-element name="ui-dialog-trigger" slot="trigger">
2698
3076
  <style>
3077
+ :host,
3078
+ * {
3079
+ touch-action: manipulation;
3080
+ }
2699
3081
  :host,
2700
3082
  span {
2701
3083
  display: contents;
@@ -2711,6 +3093,10 @@
2711
3093
  </script>
2712
3094
 
2713
3095
  <style>
3096
+ :host,
3097
+ * {
3098
+ touch-action: manipulation;
3099
+ }
2714
3100
  :host,
2715
3101
  span {
2716
3102
  display: contents;
@@ -2722,6 +3108,10 @@
2722
3108
 
2723
3109
  <custom-element name="ui-dialog-header" slot="header">
2724
3110
  <style>
3111
+ :host,
3112
+ * {
3113
+ touch-action: manipulation;
3114
+ }
2725
3115
  :host {
2726
3116
  display: grid;
2727
3117
  gap: calc(var(--spacing) * 2);
@@ -2733,6 +3123,10 @@
2733
3123
 
2734
3124
  <custom-element name="ui-dialog-title">
2735
3125
  <style>
3126
+ :host,
3127
+ * {
3128
+ touch-action: manipulation;
3129
+ }
2736
3130
  :host {
2737
3131
  display: block;
2738
3132
  }
@@ -2749,6 +3143,10 @@
2749
3143
 
2750
3144
  <custom-element name="ui-dialog-description">
2751
3145
  <style>
3146
+ :host,
3147
+ * {
3148
+ touch-action: manipulation;
3149
+ }
2752
3150
  :host {
2753
3151
  display: block;
2754
3152
  }
@@ -2764,6 +3162,10 @@
2764
3162
 
2765
3163
  <custom-element name="ui-dialog-footer" slot="footer">
2766
3164
  <style>
3165
+ :host,
3166
+ * {
3167
+ touch-action: manipulation;
3168
+ }
2767
3169
  :host {
2768
3170
  display: flex;
2769
3171
  flex-wrap: wrap;
@@ -2785,6 +3187,12 @@
2785
3187
  </script>
2786
3188
 
2787
3189
  <style>
3190
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
3191
+ touch-action does not reach shadow content in iOS Safari. */
3192
+ :host,
3193
+ * {
3194
+ touch-action: manipulation;
3195
+ }
2788
3196
  :host {
2789
3197
  display: contents;
2790
3198
  }
@@ -2859,20 +3267,33 @@
2859
3267
  observe(() => {
2860
3268
  const dialog = refs.dialog;
2861
3269
  if (!dialog || props.open === dialog.open) return;
2862
- if (props.open) dialog.showModal();
2863
- else dialog.close();
3270
+ if (!props.open) return dialog.close();
3271
+ dialog.showModal();
3272
+ // Left to the browser, focus lands on the first button (often close) with a focus ring after a
3273
+ // tap, and Firefox and WebKit ignore autofocus on slotted content.
3274
+ (host.querySelector("[autofocus]") ?? dialog).focus();
2864
3275
  });
2865
3276
  </script>
2866
3277
 
2867
3278
  <style>
3279
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
3280
+ touch-action does not reach shadow content in iOS Safari. */
3281
+ :host,
3282
+ * {
3283
+ touch-action: manipulation;
3284
+ }
2868
3285
  :host {
2869
3286
  display: contents;
2870
3287
  }
3288
+ dialog:focus {
3289
+ outline: none;
3290
+ }
2871
3291
  dialog {
2872
3292
  box-sizing: border-box;
2873
3293
  width: 100%;
2874
3294
  max-width: none;
2875
- max-height: 80dvh;
3295
+ height: fit-content;
3296
+ max-height: 80svh;
2876
3297
  margin: auto 0 0;
2877
3298
  padding: calc(var(--spacing) * 4) calc(var(--spacing) * 6) calc(var(--spacing) * 6);
2878
3299
  border: 1px solid var(--border);
@@ -2898,9 +3319,14 @@
2898
3319
  }
2899
3320
  dialog[open] {
2900
3321
  display: grid;
3322
+ /* Safari stretches a grid overlay to its max-height unless the rows are packed at the start. */
3323
+ align-content: start;
2901
3324
  gap: calc(var(--spacing) * 4);
2902
3325
  animation: slide 0.25s ease-out;
2903
3326
  }
3327
+ [part="body"] {
3328
+ display: contents;
3329
+ }
2904
3330
  dialog::backdrop {
2905
3331
  background: rgb(0 0 0 / 0.5);
2906
3332
  }
@@ -2933,6 +3359,7 @@
2933
3359
  <dialog
2934
3360
  ref="dialog"
2935
3361
  part="dialog"
3362
+ tabindex="-1"
2936
3363
  _aria-label="#drawer.label"
2937
3364
  _aria-description="#drawer.description"
2938
3365
  onclose="closed()"
@@ -2946,6 +3373,10 @@
2946
3373
 
2947
3374
  <custom-element name="ui-drawer-trigger" slot="trigger">
2948
3375
  <style>
3376
+ :host,
3377
+ * {
3378
+ touch-action: manipulation;
3379
+ }
2949
3380
  :host,
2950
3381
  span {
2951
3382
  display: contents;
@@ -2957,6 +3388,10 @@
2957
3388
 
2958
3389
  <custom-element name="ui-drawer-close">
2959
3390
  <style>
3391
+ :host,
3392
+ * {
3393
+ touch-action: manipulation;
3394
+ }
2960
3395
  :host,
2961
3396
  span {
2962
3397
  display: contents;
@@ -2968,6 +3403,10 @@
2968
3403
 
2969
3404
  <custom-element name="ui-drawer-header" slot="header">
2970
3405
  <style>
3406
+ :host,
3407
+ * {
3408
+ touch-action: manipulation;
3409
+ }
2971
3410
  :host {
2972
3411
  display: grid;
2973
3412
  gap: calc(var(--spacing) * 1.5);
@@ -2979,6 +3418,10 @@
2979
3418
 
2980
3419
  <custom-element name="ui-drawer-title">
2981
3420
  <style>
3421
+ :host,
3422
+ * {
3423
+ touch-action: manipulation;
3424
+ }
2982
3425
  :host {
2983
3426
  display: block;
2984
3427
  }
@@ -2994,6 +3437,10 @@
2994
3437
 
2995
3438
  <custom-element name="ui-drawer-description">
2996
3439
  <style>
3440
+ :host,
3441
+ * {
3442
+ touch-action: manipulation;
3443
+ }
2997
3444
  :host {
2998
3445
  display: block;
2999
3446
  }
@@ -3009,6 +3456,10 @@
3009
3456
 
3010
3457
  <custom-element name="ui-drawer-footer" slot="footer">
3011
3458
  <style>
3459
+ :host,
3460
+ * {
3461
+ touch-action: manipulation;
3462
+ }
3012
3463
  :host {
3013
3464
  display: grid;
3014
3465
  gap: calc(var(--spacing) * 2);
@@ -3039,6 +3490,12 @@
3039
3490
  </script>
3040
3491
 
3041
3492
  <style>
3493
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
3494
+ touch-action does not reach shadow content in iOS Safari. */
3495
+ :host,
3496
+ * {
3497
+ touch-action: manipulation;
3498
+ }
3042
3499
  :host {
3043
3500
  display: contents;
3044
3501
  }
@@ -3049,6 +3506,10 @@
3049
3506
 
3050
3507
  <custom-element name="ui-dropdown-menu-trigger">
3051
3508
  <style>
3509
+ :host,
3510
+ * {
3511
+ touch-action: manipulation;
3512
+ }
3052
3513
  :host {
3053
3514
  display: inline-block;
3054
3515
  vertical-align: middle;
@@ -3066,8 +3527,15 @@
3066
3527
 
3067
3528
  const ITEMS = ":is(ui-menu-item, ui-menu-checkbox-item, ui-menu-radio-item):not([disabled])";
3068
3529
 
3530
+ // Once open, the menu keeps the side it opened on. Re-deciding while the page scrolls makes it
3531
+ // jump, because iOS Safari's toolbar changes the viewport height mid-scroll.
3532
+ // Firefox holds a box that does not fit against the screen edge, so touching it counts too.
3069
3533
  function opened(event) {
3070
- if (event.newState === "open") host.querySelector(ITEMS)?.focus();
3534
+ if (event.newState !== "open") return;
3535
+ internals.states.delete("flipped");
3536
+ const box = host.getBoundingClientRect();
3537
+ if (box.bottom >= innerHeight || box.top <= 0) internals.states.add("flipped");
3538
+ host.querySelector(ITEMS)?.focus({ preventScroll: true });
3071
3539
  }
3072
3540
 
3073
3541
  function leave(event) {
@@ -3082,6 +3550,10 @@
3082
3550
  </script>
3083
3551
 
3084
3552
  <style>
3553
+ :host,
3554
+ * {
3555
+ touch-action: manipulation;
3556
+ }
3085
3557
  :host {
3086
3558
  box-sizing: border-box;
3087
3559
  min-width: calc(var(--spacing) * 32);
@@ -3093,6 +3565,9 @@
3093
3565
  color: var(--popover-foreground);
3094
3566
  box-shadow: var(--shadow-md);
3095
3567
  inset: auto;
3568
+ height: fit-content;
3569
+ max-height: calc(100svh - calc(var(--spacing) * 8));
3570
+ overflow-y: auto;
3096
3571
  position-area: bottom span-right;
3097
3572
  position-try-fallbacks: flip-block, flip-inline;
3098
3573
  }
@@ -3107,6 +3582,24 @@
3107
3582
  position-area: top span-left;
3108
3583
  margin: 0 0 calc(var(--spacing) * 1);
3109
3584
  }
3585
+ :host(:popover-open) {
3586
+ position-try-fallbacks: flip-inline;
3587
+ }
3588
+ :host(:state(flipped)) {
3589
+ position-area: top span-right;
3590
+ margin: 0 0 calc(var(--spacing) * 1);
3591
+ }
3592
+ :host(:state(flipped):state(attr-placement-is-bottom-end)) {
3593
+ position-area: top span-left;
3594
+ }
3595
+ :host(:state(flipped):state(attr-placement-is-top-start)) {
3596
+ position-area: bottom span-right;
3597
+ margin: calc(var(--spacing) * 1) 0 0;
3598
+ }
3599
+ :host(:state(flipped):state(attr-placement-is-top-end)) {
3600
+ position-area: bottom span-left;
3601
+ margin: calc(var(--spacing) * 1) 0 0;
3602
+ }
3110
3603
  </style>
3111
3604
 
3112
3605
  <div part="menu" focusgroup="menu block wrap"><slot></slot></div>
@@ -3143,6 +3636,10 @@
3143
3636
  </script>
3144
3637
 
3145
3638
  <style>
3639
+ :host,
3640
+ * {
3641
+ touch-action: manipulation;
3642
+ }
3146
3643
  :host {
3147
3644
  display: flex;
3148
3645
  align-items: center;
@@ -3210,6 +3707,10 @@
3210
3707
  </script>
3211
3708
 
3212
3709
  <style>
3710
+ :host,
3711
+ * {
3712
+ touch-action: manipulation;
3713
+ }
3213
3714
  :host {
3214
3715
  display: flex;
3215
3716
  align-items: center;
@@ -3265,6 +3766,10 @@
3265
3766
  </script>
3266
3767
 
3267
3768
  <style>
3769
+ :host,
3770
+ * {
3771
+ touch-action: manipulation;
3772
+ }
3268
3773
  :host {
3269
3774
  display: block;
3270
3775
  }
@@ -3305,6 +3810,10 @@
3305
3810
  </script>
3306
3811
 
3307
3812
  <style>
3813
+ :host,
3814
+ * {
3815
+ touch-action: manipulation;
3816
+ }
3308
3817
  :host {
3309
3818
  display: flex;
3310
3819
  align-items: center;
@@ -3346,6 +3855,10 @@
3346
3855
 
3347
3856
  <custom-element name="ui-menu-label">
3348
3857
  <style>
3858
+ :host,
3859
+ * {
3860
+ touch-action: manipulation;
3861
+ }
3349
3862
  :host {
3350
3863
  display: block;
3351
3864
  padding: calc(var(--spacing) * 1.5) calc(var(--spacing) * 2);
@@ -3359,6 +3872,10 @@
3359
3872
 
3360
3873
  <custom-element name="ui-menu-group" role="group">
3361
3874
  <style>
3875
+ :host,
3876
+ * {
3877
+ touch-action: manipulation;
3878
+ }
3362
3879
  :host {
3363
3880
  display: block;
3364
3881
  }
@@ -3369,6 +3886,10 @@
3369
3886
 
3370
3887
  <custom-element name="ui-menu-separator" role="separator">
3371
3888
  <style>
3889
+ :host,
3890
+ * {
3891
+ touch-action: manipulation;
3892
+ }
3372
3893
  :host {
3373
3894
  display: block;
3374
3895
  height: 1px;
@@ -3380,6 +3901,10 @@
3380
3901
 
3381
3902
  <custom-element name="ui-menu-shortcut">
3382
3903
  <style>
3904
+ :host,
3905
+ * {
3906
+ touch-action: manipulation;
3907
+ }
3383
3908
  :host {
3384
3909
  margin-inline-start: auto;
3385
3910
  font-size: var(--text-xs);
@@ -3393,6 +3918,12 @@
3393
3918
 
3394
3919
  <custom-element name="ui-empty">
3395
3920
  <style>
3921
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
3922
+ touch-action does not reach shadow content in iOS Safari. */
3923
+ :host,
3924
+ * {
3925
+ touch-action: manipulation;
3926
+ }
3396
3927
  :host {
3397
3928
  display: flex;
3398
3929
  flex-direction: column;
@@ -3413,6 +3944,10 @@
3413
3944
 
3414
3945
  <custom-element name="ui-empty-header">
3415
3946
  <style>
3947
+ :host,
3948
+ * {
3949
+ touch-action: manipulation;
3950
+ }
3416
3951
  :host {
3417
3952
  display: flex;
3418
3953
  flex-direction: column;
@@ -3427,6 +3962,10 @@
3427
3962
 
3428
3963
  <custom-element name="ui-empty-media">
3429
3964
  <style>
3965
+ :host,
3966
+ * {
3967
+ touch-action: manipulation;
3968
+ }
3430
3969
  :host {
3431
3970
  display: flex;
3432
3971
  flex-shrink: 0;
@@ -3456,6 +3995,10 @@
3456
3995
  </script>
3457
3996
 
3458
3997
  <style>
3998
+ :host,
3999
+ * {
4000
+ touch-action: manipulation;
4001
+ }
3459
4002
  :host {
3460
4003
  display: block;
3461
4004
  }
@@ -3472,6 +4015,10 @@
3472
4015
 
3473
4016
  <custom-element name="ui-empty-description">
3474
4017
  <style>
4018
+ :host,
4019
+ * {
4020
+ touch-action: manipulation;
4021
+ }
3475
4022
  :host {
3476
4023
  display: block;
3477
4024
  font-size: var(--text-sm);
@@ -3484,6 +4031,10 @@
3484
4031
 
3485
4032
  <custom-element name="ui-empty-content">
3486
4033
  <style>
4034
+ :host,
4035
+ * {
4036
+ touch-action: manipulation;
4037
+ }
3487
4038
  :host {
3488
4039
  display: flex;
3489
4040
  flex-direction: column;
@@ -3508,6 +4059,12 @@
3508
4059
  </script>
3509
4060
 
3510
4061
  <style>
4062
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
4063
+ touch-action does not reach shadow content in iOS Safari. */
4064
+ :host,
4065
+ * {
4066
+ touch-action: manipulation;
4067
+ }
3511
4068
  :host {
3512
4069
  display: flex;
3513
4070
  flex-direction: column;
@@ -3541,6 +4098,10 @@
3541
4098
 
3542
4099
  <custom-element name="ui-field-content">
3543
4100
  <style>
4101
+ :host,
4102
+ * {
4103
+ touch-action: manipulation;
4104
+ }
3544
4105
  :host {
3545
4106
  display: flex;
3546
4107
  flex: 1;
@@ -3555,6 +4116,10 @@
3555
4116
 
3556
4117
  <custom-element name="ui-field-description">
3557
4118
  <style>
4119
+ :host,
4120
+ * {
4121
+ touch-action: manipulation;
4122
+ }
3558
4123
  :host {
3559
4124
  display: block;
3560
4125
  font-size: var(--text-sm);
@@ -3568,6 +4133,10 @@
3568
4133
 
3569
4134
  <custom-element name="ui-field-group">
3570
4135
  <style>
4136
+ :host,
4137
+ * {
4138
+ touch-action: manipulation;
4139
+ }
3571
4140
  :host {
3572
4141
  display: flex;
3573
4142
  flex-direction: column;
@@ -3587,6 +4156,10 @@
3587
4156
  </script>
3588
4157
 
3589
4158
  <style>
4159
+ :host,
4160
+ * {
4161
+ touch-action: manipulation;
4162
+ }
3590
4163
  :host {
3591
4164
  display: flex;
3592
4165
  flex-direction: column;
@@ -3603,6 +4176,10 @@
3603
4176
 
3604
4177
  <custom-element name="ui-field-legend" slot="legend">
3605
4178
  <style>
4179
+ :host,
4180
+ * {
4181
+ touch-action: manipulation;
4182
+ }
3606
4183
  :host {
3607
4184
  display: block;
3608
4185
  font-size: var(--text-base);
@@ -3649,6 +4226,12 @@
3649
4226
  </script>
3650
4227
 
3651
4228
  <style>
4229
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
4230
+ touch-action does not reach shadow content in iOS Safari. */
4231
+ :host,
4232
+ * {
4233
+ touch-action: manipulation;
4234
+ }
3652
4235
  :host {
3653
4236
  display: contents;
3654
4237
  }
@@ -3659,6 +4242,10 @@
3659
4242
 
3660
4243
  <custom-element name="ui-hover-card-trigger">
3661
4244
  <style>
4245
+ :host,
4246
+ * {
4247
+ touch-action: manipulation;
4248
+ }
3662
4249
  :host {
3663
4250
  display: inline-block;
3664
4251
  }
@@ -3669,14 +4256,32 @@
3669
4256
 
3670
4257
  <custom-element name="ui-hover-card-content">
3671
4258
  <script>
3672
- defineProps({
4259
+ const props = defineProps({
3673
4260
  placement: prop.enum(["bottom", "top", "left", "right"]).default("bottom"),
3674
4261
  });
3675
4262
 
4263
+ const BLOCK_SIDES = new Set(["bottom", "top"]);
4264
+
4265
+ // Once open, it keeps the side it opened on. Re-deciding while the page scrolls makes it jump,
4266
+ // because iOS Safari's toolbar changes the viewport height mid-scroll.
4267
+ // Firefox holds a box that does not fit against the screen edge, so touching it counts too.
4268
+ function opened(event) {
4269
+ internals.states.delete("flipped");
4270
+ if (event.newState !== "open" || !BLOCK_SIDES.has(props.placement)) return;
4271
+ const box = host.getBoundingClientRect();
4272
+ if (box.bottom >= innerHeight || box.top <= 0) internals.states.add("flipped");
4273
+ }
4274
+
3676
4275
  attrs.popover ??= "manual";
4276
+
4277
+ listen("toggle", opened);
3677
4278
  </script>
3678
4279
 
3679
4280
  <style>
4281
+ :host,
4282
+ * {
4283
+ touch-action: manipulation;
4284
+ }
3680
4285
  :host {
3681
4286
  box-sizing: border-box;
3682
4287
  width: calc(var(--spacing) * 64);
@@ -3688,15 +4293,27 @@
3688
4293
  color: var(--popover-foreground);
3689
4294
  box-shadow: var(--shadow-md);
3690
4295
  inset: auto;
4296
+ height: fit-content;
4297
+ max-height: calc(100svh - calc(var(--spacing) * 8));
4298
+ overflow-y: auto;
3691
4299
  position-area: bottom;
3692
4300
  position-try-fallbacks: flip-block, flip-inline;
3693
4301
  }
4302
+ :host(:popover-open) {
4303
+ position-try-fallbacks: flip-inline;
4304
+ }
3694
4305
  :host(:state(attr-width)) {
3695
4306
  width: calc(var(--spacing) * var(--attr-width));
3696
4307
  }
3697
4308
  :host(:state(attr-placement-is-top)) {
3698
4309
  position-area: top;
3699
4310
  }
4311
+ :host(:state(flipped)) {
4312
+ position-area: top;
4313
+ }
4314
+ :host(:state(flipped):state(attr-placement-is-top)) {
4315
+ position-area: bottom;
4316
+ }
3700
4317
  :host(:state(attr-placement-is-left)) {
3701
4318
  position-area: left;
3702
4319
  }
@@ -3724,6 +4341,12 @@
3724
4341
  </script>
3725
4342
 
3726
4343
  <style>
4344
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
4345
+ touch-action does not reach shadow content in iOS Safari. */
4346
+ :host,
4347
+ * {
4348
+ touch-action: manipulation;
4349
+ }
3727
4350
  :host {
3728
4351
  display: flex;
3729
4352
  align-items: center;
@@ -3751,6 +4374,12 @@
3751
4374
  font-size: var(--text-sm);
3752
4375
  outline: none;
3753
4376
  }
4377
+ /* iOS Safari zooms in on a focused field whose text is under 16px. */
4378
+ @media (pointer: coarse) {
4379
+ input {
4380
+ font-size: var(--text-base);
4381
+ }
4382
+ }
3754
4383
  input::placeholder {
3755
4384
  color: var(--muted-foreground);
3756
4385
  }
@@ -3762,6 +4391,10 @@
3762
4391
 
3763
4392
  <custom-element name="ui-input-group-addon">
3764
4393
  <style>
4394
+ :host,
4395
+ * {
4396
+ touch-action: manipulation;
4397
+ }
3765
4398
  :host {
3766
4399
  display: flex;
3767
4400
  order: -1;
@@ -3866,6 +4499,12 @@
3866
4499
  </script>
3867
4500
 
3868
4501
  <style>
4502
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
4503
+ touch-action does not reach shadow content in iOS Safari. */
4504
+ :host,
4505
+ * {
4506
+ touch-action: manipulation;
4507
+ }
3869
4508
  :host {
3870
4509
  display: inline-flex;
3871
4510
  }
@@ -3886,6 +4525,12 @@
3886
4525
  text-align: center;
3887
4526
  outline: none;
3888
4527
  }
4528
+ /* iOS Safari zooms in on a focused field whose text is under 16px. */
4529
+ @media (pointer: coarse) {
4530
+ input {
4531
+ font-size: var(--text-base);
4532
+ }
4533
+ }
3889
4534
  input:first-child,
3890
4535
  input[data-gap] {
3891
4536
  margin-left: 0;
@@ -3952,6 +4597,12 @@
3952
4597
  </script>
3953
4598
 
3954
4599
  <style>
4600
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
4601
+ touch-action does not reach shadow content in iOS Safari. */
4602
+ :host,
4603
+ * {
4604
+ touch-action: manipulation;
4605
+ }
3955
4606
  :host {
3956
4607
  display: block;
3957
4608
  min-width: 0;
@@ -3971,6 +4622,12 @@
3971
4622
  outline: none;
3972
4623
  transition: color var(--default-transition-duration), box-shadow var(--default-transition-duration);
3973
4624
  }
4625
+ /* iOS Safari zooms in on a focused field whose text is under 16px. */
4626
+ @media (pointer: coarse) {
4627
+ input {
4628
+ font-size: var(--text-base);
4629
+ }
4630
+ }
3974
4631
  input::placeholder {
3975
4632
  color: var(--muted-foreground);
3976
4633
  }
@@ -4023,6 +4680,12 @@
4023
4680
  </script>
4024
4681
 
4025
4682
  <style>
4683
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
4684
+ touch-action does not reach shadow content in iOS Safari. */
4685
+ :host,
4686
+ * {
4687
+ touch-action: manipulation;
4688
+ }
4026
4689
  :host {
4027
4690
  display: flex;
4028
4691
  flex-wrap: wrap;
@@ -4051,6 +4714,10 @@
4051
4714
 
4052
4715
  <custom-element name="ui-item-group" role="list">
4053
4716
  <style>
4717
+ :host,
4718
+ * {
4719
+ touch-action: manipulation;
4720
+ }
4054
4721
  :host {
4055
4722
  display: flex;
4056
4723
  flex-direction: column;
@@ -4062,6 +4729,10 @@
4062
4729
 
4063
4730
  <custom-element name="ui-item-media">
4064
4731
  <style>
4732
+ :host,
4733
+ * {
4734
+ touch-action: manipulation;
4735
+ }
4065
4736
  :host {
4066
4737
  display: flex;
4067
4738
  flex-shrink: 0;
@@ -4099,6 +4770,10 @@
4099
4770
 
4100
4771
  <custom-element name="ui-item-content">
4101
4772
  <style>
4773
+ :host,
4774
+ * {
4775
+ touch-action: manipulation;
4776
+ }
4102
4777
  :host {
4103
4778
  display: flex;
4104
4779
  flex: 1;
@@ -4113,6 +4788,10 @@
4113
4788
 
4114
4789
  <custom-element name="ui-item-title">
4115
4790
  <style>
4791
+ :host,
4792
+ * {
4793
+ touch-action: manipulation;
4794
+ }
4116
4795
  :host {
4117
4796
  display: flex;
4118
4797
  align-items: center;
@@ -4127,6 +4806,10 @@
4127
4806
 
4128
4807
  <custom-element name="ui-item-description">
4129
4808
  <style>
4809
+ :host,
4810
+ * {
4811
+ touch-action: manipulation;
4812
+ }
4130
4813
  :host {
4131
4814
  display: -webkit-box;
4132
4815
  overflow: hidden;
@@ -4143,6 +4826,10 @@
4143
4826
 
4144
4827
  <custom-element name="ui-item-actions">
4145
4828
  <style>
4829
+ :host,
4830
+ * {
4831
+ touch-action: manipulation;
4832
+ }
4146
4833
  :host {
4147
4834
  display: flex;
4148
4835
  align-items: center;
@@ -4155,6 +4842,10 @@
4155
4842
 
4156
4843
  <custom-element name="ui-item-header">
4157
4844
  <style>
4845
+ :host,
4846
+ * {
4847
+ touch-action: manipulation;
4848
+ }
4158
4849
  :host {
4159
4850
  display: flex;
4160
4851
  flex-basis: 100%;
@@ -4169,6 +4860,10 @@
4169
4860
 
4170
4861
  <custom-element name="ui-item-footer">
4171
4862
  <style>
4863
+ :host,
4864
+ * {
4865
+ touch-action: manipulation;
4866
+ }
4172
4867
  :host {
4173
4868
  display: flex;
4174
4869
  flex-basis: 100%;
@@ -4183,6 +4878,12 @@
4183
4878
 
4184
4879
  <custom-element name="ui-kbd">
4185
4880
  <style>
4881
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
4882
+ touch-action does not reach shadow content in iOS Safari. */
4883
+ :host,
4884
+ * {
4885
+ touch-action: manipulation;
4886
+ }
4186
4887
  :host {
4187
4888
  display: inline-flex;
4188
4889
  align-items: center;
@@ -4207,6 +4908,10 @@
4207
4908
 
4208
4909
  <custom-element name="ui-kbd-group">
4209
4910
  <style>
4911
+ :host,
4912
+ * {
4913
+ touch-action: manipulation;
4914
+ }
4210
4915
  :host {
4211
4916
  display: inline-flex;
4212
4917
  align-items: center;
@@ -4241,6 +4946,12 @@
4241
4946
  </script>
4242
4947
 
4243
4948
  <style>
4949
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
4950
+ touch-action does not reach shadow content in iOS Safari. */
4951
+ :host,
4952
+ * {
4953
+ touch-action: manipulation;
4954
+ }
4244
4955
  :host {
4245
4956
  display: inline-flex;
4246
4957
  align-items: center;
@@ -4264,6 +4975,12 @@
4264
4975
  </script>
4265
4976
 
4266
4977
  <style>
4978
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
4979
+ touch-action does not reach shadow content in iOS Safari. */
4980
+ :host,
4981
+ * {
4982
+ touch-action: manipulation;
4983
+ }
4267
4984
  :host {
4268
4985
  display: flex;
4269
4986
  align-items: center;
@@ -4296,6 +5013,12 @@
4296
5013
 
4297
5014
  <custom-element name="ui-menubar" role="menubar">
4298
5015
  <style>
5016
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
5017
+ touch-action does not reach shadow content in iOS Safari. */
5018
+ :host,
5019
+ * {
5020
+ touch-action: manipulation;
5021
+ }
4299
5022
  :host {
4300
5023
  display: flex;
4301
5024
  align-items: center;
@@ -4338,6 +5061,10 @@
4338
5061
  </script>
4339
5062
 
4340
5063
  <style>
5064
+ :host,
5065
+ * {
5066
+ touch-action: manipulation;
5067
+ }
4341
5068
  :host {
4342
5069
  display: contents;
4343
5070
  }
@@ -4348,6 +5075,10 @@
4348
5075
 
4349
5076
  <custom-element name="ui-menubar-trigger">
4350
5077
  <style>
5078
+ :host,
5079
+ * {
5080
+ touch-action: manipulation;
5081
+ }
4351
5082
  :host {
4352
5083
  display: inline-flex;
4353
5084
  }
@@ -4376,8 +5107,15 @@
4376
5107
  <script>
4377
5108
  const ITEMS = ":is(ui-menu-item, ui-menu-checkbox-item, ui-menu-radio-item):not([disabled])";
4378
5109
 
5110
+ // Once open, it keeps the side it opened on. Re-deciding while the page scrolls makes it jump,
5111
+ // because iOS Safari's toolbar changes the viewport height mid-scroll.
5112
+ // Firefox holds a box that does not fit against the screen edge, so touching it counts too.
4379
5113
  function opened(event) {
4380
- if (event.newState === "open") host.querySelector(ITEMS)?.focus();
5114
+ if (event.newState !== "open") return;
5115
+ internals.states.delete("flipped");
5116
+ const box = host.getBoundingClientRect();
5117
+ if (box.bottom >= innerHeight || box.top <= 0) internals.states.add("flipped");
5118
+ host.querySelector(ITEMS)?.focus({ preventScroll: true });
4381
5119
  }
4382
5120
 
4383
5121
  function leave(event) {
@@ -4392,6 +5130,10 @@
4392
5130
  </script>
4393
5131
 
4394
5132
  <style>
5133
+ :host,
5134
+ * {
5135
+ touch-action: manipulation;
5136
+ }
4395
5137
  :host {
4396
5138
  box-sizing: border-box;
4397
5139
  min-width: calc(var(--spacing) * 48);
@@ -4403,9 +5145,19 @@
4403
5145
  color: var(--popover-foreground);
4404
5146
  box-shadow: var(--shadow-md);
4405
5147
  inset: auto;
5148
+ height: fit-content;
5149
+ max-height: calc(100svh - calc(var(--spacing) * 8));
5150
+ overflow-y: auto;
4406
5151
  position-area: bottom span-right;
4407
5152
  position-try-fallbacks: flip-block, flip-inline;
4408
5153
  }
5154
+ :host(:popover-open) {
5155
+ position-try-fallbacks: flip-inline;
5156
+ }
5157
+ :host(:state(flipped)) {
5158
+ position-area: top span-right;
5159
+ margin: 0 0 calc(var(--spacing) * 2) calc(var(--spacing) * -1);
5160
+ }
4409
5161
  </style>
4410
5162
 
4411
5163
  <div part="menu" focusgroup="menu block wrap"><slot></slot></div>
@@ -4440,6 +5192,12 @@
4440
5192
  </script>
4441
5193
 
4442
5194
  <style>
5195
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
5196
+ touch-action does not reach shadow content in iOS Safari. */
5197
+ :host,
5198
+ * {
5199
+ touch-action: manipulation;
5200
+ }
4443
5201
  :host {
4444
5202
  position: relative;
4445
5203
  display: block;
@@ -4478,6 +5236,12 @@
4478
5236
  </script>
4479
5237
 
4480
5238
  <style>
5239
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
5240
+ touch-action does not reach shadow content in iOS Safari. */
5241
+ :host,
5242
+ * {
5243
+ touch-action: manipulation;
5244
+ }
4481
5245
  :host {
4482
5246
  display: flex;
4483
5247
  gap: calc(var(--spacing) * 2);
@@ -4510,6 +5274,10 @@
4510
5274
 
4511
5275
  <custom-element name="ui-message-avatar" slot="avatar">
4512
5276
  <style>
5277
+ :host,
5278
+ * {
5279
+ touch-action: manipulation;
5280
+ }
4513
5281
  :host {
4514
5282
  display: flex;
4515
5283
  align-self: flex-end;
@@ -4522,6 +5290,10 @@
4522
5290
 
4523
5291
  <custom-element name="ui-message-header" slot="header">
4524
5292
  <style>
5293
+ :host,
5294
+ * {
5295
+ touch-action: manipulation;
5296
+ }
4525
5297
  :host {
4526
5298
  display: flex;
4527
5299
  align-items: center;
@@ -4537,6 +5309,10 @@
4537
5309
 
4538
5310
  <custom-element name="ui-message-footer" slot="footer">
4539
5311
  <style>
5312
+ :host,
5313
+ * {
5314
+ touch-action: manipulation;
5315
+ }
4540
5316
  :host {
4541
5317
  display: flex;
4542
5318
  align-items: center;
@@ -4556,6 +5332,12 @@
4556
5332
  </script>
4557
5333
 
4558
5334
  <style>
5335
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
5336
+ touch-action does not reach shadow content in iOS Safari. */
5337
+ :host,
5338
+ * {
5339
+ touch-action: manipulation;
5340
+ }
4559
5341
  :host {
4560
5342
  display: block;
4561
5343
  }
@@ -4566,6 +5348,10 @@
4566
5348
 
4567
5349
  <custom-element name="ui-navigation-menu-list">
4568
5350
  <style>
5351
+ :host,
5352
+ * {
5353
+ touch-action: manipulation;
5354
+ }
4569
5355
  :host {
4570
5356
  display: block;
4571
5357
  }
@@ -4609,6 +5395,10 @@
4609
5395
  </script>
4610
5396
 
4611
5397
  <style>
5398
+ :host,
5399
+ * {
5400
+ touch-action: manipulation;
5401
+ }
4612
5402
  :host {
4613
5403
  position: relative;
4614
5404
  display: block;
@@ -4629,6 +5419,9 @@
4629
5419
  box-shadow: var(--shadow-md);
4630
5420
  inset: auto;
4631
5421
  position-anchor: --navigation-trigger;
5422
+ height: fit-content;
5423
+ max-height: calc(100svh - calc(var(--spacing) * 8));
5424
+ overflow-y: auto;
4632
5425
  position-area: bottom span-right;
4633
5426
  }
4634
5427
  </style>
@@ -4639,6 +5432,10 @@
4639
5432
 
4640
5433
  <custom-element name="ui-navigation-menu-trigger" delegates-focus>
4641
5434
  <style>
5435
+ :host,
5436
+ * {
5437
+ touch-action: manipulation;
5438
+ }
4642
5439
  :host {
4643
5440
  display: inline-flex;
4644
5441
  }
@@ -4681,6 +5478,10 @@
4681
5478
 
4682
5479
  <custom-element name="ui-navigation-menu-content" slot="content">
4683
5480
  <style>
5481
+ :host,
5482
+ * {
5483
+ touch-action: manipulation;
5484
+ }
4684
5485
  :host {
4685
5486
  display: grid;
4686
5487
  gap: calc(var(--spacing) * 1);
@@ -4713,6 +5514,10 @@
4713
5514
  </script>
4714
5515
 
4715
5516
  <style>
5517
+ :host,
5518
+ * {
5519
+ touch-action: manipulation;
5520
+ }
4716
5521
  :host {
4717
5522
  display: var(--ui-navigation-link-display, inline-flex);
4718
5523
  }
@@ -4813,6 +5618,12 @@
4813
5618
  </script>
4814
5619
 
4815
5620
  <style>
5621
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
5622
+ touch-action does not reach shadow content in iOS Safari. */
5623
+ :host,
5624
+ * {
5625
+ touch-action: manipulation;
5626
+ }
4816
5627
  :host {
4817
5628
  display: flex;
4818
5629
  justify-content: center;
@@ -4931,6 +5742,12 @@
4931
5742
  </script>
4932
5743
 
4933
5744
  <style>
5745
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
5746
+ touch-action does not reach shadow content in iOS Safari. */
5747
+ :host,
5748
+ * {
5749
+ touch-action: manipulation;
5750
+ }
4934
5751
  :host {
4935
5752
  display: contents;
4936
5753
  }
@@ -4941,6 +5758,10 @@
4941
5758
 
4942
5759
  <custom-element name="ui-popover-trigger">
4943
5760
  <style>
5761
+ :host,
5762
+ * {
5763
+ touch-action: manipulation;
5764
+ }
4944
5765
  :host {
4945
5766
  display: inline-block;
4946
5767
  vertical-align: middle;
@@ -4952,14 +5773,32 @@
4952
5773
 
4953
5774
  <custom-element name="ui-popover-content">
4954
5775
  <script>
4955
- defineProps({
5776
+ const props = defineProps({
4956
5777
  placement: prop.enum(["bottom", "top", "left", "right"]).default("bottom"),
4957
5778
  });
4958
5779
 
5780
+ const BLOCK_SIDES = new Set(["bottom", "top"]);
5781
+
5782
+ // Once open, it keeps the side it opened on. Re-deciding while the page scrolls makes it jump,
5783
+ // because iOS Safari's toolbar changes the viewport height mid-scroll.
5784
+ // Firefox holds a box that does not fit against the screen edge, so touching it counts too.
5785
+ function opened(event) {
5786
+ internals.states.delete("flipped");
5787
+ if (event.newState !== "open" || !BLOCK_SIDES.has(props.placement)) return;
5788
+ const box = host.getBoundingClientRect();
5789
+ if (box.bottom >= innerHeight || box.top <= 0) internals.states.add("flipped");
5790
+ }
5791
+
4959
5792
  attrs.popover ??= "auto";
5793
+
5794
+ listen("toggle", opened);
4960
5795
  </script>
4961
5796
 
4962
5797
  <style>
5798
+ :host,
5799
+ * {
5800
+ touch-action: manipulation;
5801
+ }
4963
5802
  :host {
4964
5803
  box-sizing: border-box;
4965
5804
  width: calc(var(--spacing) * 72);
@@ -4971,11 +5810,17 @@
4971
5810
  color: var(--popover-foreground);
4972
5811
  box-shadow: var(--shadow-md);
4973
5812
  inset: auto;
5813
+ height: fit-content;
5814
+ max-height: calc(100svh - calc(var(--spacing) * 8));
5815
+ overflow-y: auto;
4974
5816
  position-area: bottom;
4975
5817
  position-try-fallbacks: flip-block, flip-inline;
4976
5818
  }
4977
5819
  :host(:popover-open) {
5820
+ position-try-fallbacks: flip-inline;
4978
5821
  display: grid;
5822
+ /* Safari stretches a grid overlay to its max-height unless the rows are packed at the start. */
5823
+ align-content: start;
4979
5824
  gap: calc(var(--spacing) * 4);
4980
5825
  }
4981
5826
  :host(:state(attr-width)) {
@@ -4984,6 +5829,12 @@
4984
5829
  :host(:state(attr-placement-is-top)) {
4985
5830
  position-area: top;
4986
5831
  }
5832
+ :host(:state(flipped)) {
5833
+ position-area: top;
5834
+ }
5835
+ :host(:state(flipped):state(attr-placement-is-top)) {
5836
+ position-area: bottom;
5837
+ }
4987
5838
  :host(:state(attr-placement-is-left)) {
4988
5839
  position-area: left;
4989
5840
  }
@@ -5004,6 +5855,10 @@
5004
5855
 
5005
5856
  <custom-element name="ui-popover-header">
5006
5857
  <style>
5858
+ :host,
5859
+ * {
5860
+ touch-action: manipulation;
5861
+ }
5007
5862
  :host {
5008
5863
  display: grid;
5009
5864
  gap: calc(var(--spacing) * 1);
@@ -5015,6 +5870,10 @@
5015
5870
 
5016
5871
  <custom-element name="ui-popover-title">
5017
5872
  <style>
5873
+ :host,
5874
+ * {
5875
+ touch-action: manipulation;
5876
+ }
5018
5877
  :host {
5019
5878
  display: block;
5020
5879
  }
@@ -5031,6 +5890,10 @@
5031
5890
 
5032
5891
  <custom-element name="ui-popover-description">
5033
5892
  <style>
5893
+ :host,
5894
+ * {
5895
+ touch-action: manipulation;
5896
+ }
5034
5897
  :host {
5035
5898
  display: block;
5036
5899
  }
@@ -5054,6 +5917,12 @@
5054
5917
  </script>
5055
5918
 
5056
5919
  <style>
5920
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
5921
+ touch-action does not reach shadow content in iOS Safari. */
5922
+ :host,
5923
+ * {
5924
+ touch-action: manipulation;
5925
+ }
5057
5926
  :host {
5058
5927
  display: block;
5059
5928
  width: 100%;
@@ -5123,6 +5992,12 @@
5123
5992
  </script>
5124
5993
 
5125
5994
  <style>
5995
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
5996
+ touch-action does not reach shadow content in iOS Safari. */
5997
+ :host,
5998
+ * {
5999
+ touch-action: manipulation;
6000
+ }
5126
6001
  :host {
5127
6002
  display: grid;
5128
6003
  gap: calc(var(--spacing) * 3);
@@ -5208,6 +6083,12 @@
5208
6083
  </script>
5209
6084
 
5210
6085
  <style>
6086
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
6087
+ touch-action does not reach shadow content in iOS Safari. */
6088
+ :host,
6089
+ * {
6090
+ touch-action: manipulation;
6091
+ }
5211
6092
  :host {
5212
6093
  display: flex;
5213
6094
  width: 100%;
@@ -5236,6 +6117,10 @@
5236
6117
  </script>
5237
6118
 
5238
6119
  <style>
6120
+ :host,
6121
+ * {
6122
+ touch-action: manipulation;
6123
+ }
5239
6124
  :host {
5240
6125
  display: block;
5241
6126
  flex: var(--attr-size) 1 0;
@@ -5311,6 +6196,10 @@
5311
6196
  </script>
5312
6197
 
5313
6198
  <style>
6199
+ :host,
6200
+ * {
6201
+ touch-action: manipulation;
6202
+ }
5314
6203
  :host {
5315
6204
  position: relative;
5316
6205
  display: flex;
@@ -5365,6 +6254,12 @@
5365
6254
  </script>
5366
6255
 
5367
6256
  <style>
6257
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
6258
+ touch-action does not reach shadow content in iOS Safari. */
6259
+ :host,
6260
+ * {
6261
+ touch-action: manipulation;
6262
+ }
5368
6263
  :host {
5369
6264
  display: block;
5370
6265
  position: relative;
@@ -5441,6 +6336,12 @@
5441
6336
  </script>
5442
6337
 
5443
6338
  <style>
6339
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
6340
+ touch-action does not reach shadow content in iOS Safari. */
6341
+ :host,
6342
+ * {
6343
+ touch-action: manipulation;
6344
+ }
5444
6345
  :host {
5445
6346
  display: block;
5446
6347
  min-width: calc(var(--spacing) * 32);
@@ -5466,6 +6367,12 @@
5466
6367
  outline: none;
5467
6368
  cursor: pointer;
5468
6369
  }
6370
+ /* iOS Safari zooms in on a focused field whose text is under 16px. */
6371
+ @media (pointer: coarse) {
6372
+ select {
6373
+ font-size: var(--text-base);
6374
+ }
6375
+ }
5469
6376
  :host(:state(attr-size-is-sm)) select {
5470
6377
  height: calc(var(--spacing) * 8);
5471
6378
  }
@@ -5510,6 +6417,12 @@
5510
6417
  </script>
5511
6418
 
5512
6419
  <style>
6420
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
6421
+ touch-action does not reach shadow content in iOS Safari. */
6422
+ :host,
6423
+ * {
6424
+ touch-action: manipulation;
6425
+ }
5513
6426
  :host {
5514
6427
  display: block;
5515
6428
  flex-shrink: 0;
@@ -5577,6 +6490,12 @@
5577
6490
  </script>
5578
6491
 
5579
6492
  <style>
6493
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
6494
+ touch-action does not reach shadow content in iOS Safari. */
6495
+ :host,
6496
+ * {
6497
+ touch-action: manipulation;
6498
+ }
5580
6499
  :host {
5581
6500
  display: contents;
5582
6501
  }
@@ -5592,6 +6511,10 @@
5592
6511
 
5593
6512
  <custom-element name="ui-sheet-trigger" slot="trigger">
5594
6513
  <style>
6514
+ :host,
6515
+ * {
6516
+ touch-action: manipulation;
6517
+ }
5595
6518
  :host,
5596
6519
  span {
5597
6520
  display: contents;
@@ -5603,6 +6526,10 @@
5603
6526
 
5604
6527
  <custom-element name="ui-sheet-close">
5605
6528
  <style>
6529
+ :host,
6530
+ * {
6531
+ touch-action: manipulation;
6532
+ }
5606
6533
  :host,
5607
6534
  span {
5608
6535
  display: contents;
@@ -5614,6 +6541,10 @@
5614
6541
 
5615
6542
  <custom-element name="ui-sheet-header" slot="header">
5616
6543
  <style>
6544
+ :host,
6545
+ * {
6546
+ touch-action: manipulation;
6547
+ }
5617
6548
  :host {
5618
6549
  display: grid;
5619
6550
  gap: calc(var(--spacing) * 2);
@@ -5625,6 +6556,10 @@
5625
6556
 
5626
6557
  <custom-element name="ui-sheet-title">
5627
6558
  <style>
6559
+ :host,
6560
+ * {
6561
+ touch-action: manipulation;
6562
+ }
5628
6563
  :host {
5629
6564
  display: block;
5630
6565
  }
@@ -5641,6 +6576,10 @@
5641
6576
 
5642
6577
  <custom-element name="ui-sheet-description">
5643
6578
  <style>
6579
+ :host,
6580
+ * {
6581
+ touch-action: manipulation;
6582
+ }
5644
6583
  :host {
5645
6584
  display: block;
5646
6585
  }
@@ -5656,6 +6595,10 @@
5656
6595
 
5657
6596
  <custom-element name="ui-sheet-footer" slot="footer">
5658
6597
  <style>
6598
+ :host,
6599
+ * {
6600
+ touch-action: manipulation;
6601
+ }
5659
6602
  :host {
5660
6603
  display: flex;
5661
6604
  flex-wrap: wrap;
@@ -5706,11 +6649,17 @@
5706
6649
  </script>
5707
6650
 
5708
6651
  <style>
6652
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
6653
+ touch-action does not reach shadow content in iOS Safari. */
6654
+ :host,
6655
+ * {
6656
+ touch-action: manipulation;
6657
+ }
5709
6658
  :host {
5710
6659
  --ui-sidebar-width: calc(var(--spacing) * 64);
5711
6660
  --ui-sidebar-label: block;
5712
6661
  display: flex;
5713
- min-height: 100dvh;
6662
+ min-height: 100svh;
5714
6663
  background: var(--sidebar);
5715
6664
  }
5716
6665
  :host(:state(attr-collapsed)) {
@@ -5721,7 +6670,7 @@
5721
6670
  position: sticky;
5722
6671
  top: 0;
5723
6672
  width: var(--ui-sidebar-width);
5724
- height: 100dvh;
6673
+ height: 100svh;
5725
6674
  flex-shrink: 0;
5726
6675
  transition: width 0.2s ease;
5727
6676
  }
@@ -5760,6 +6709,10 @@
5760
6709
 
5761
6710
  <custom-element name="ui-sidebar" slot="sidebar">
5762
6711
  <style>
6712
+ :host,
6713
+ * {
6714
+ touch-action: manipulation;
6715
+ }
5763
6716
  :host {
5764
6717
  display: flex;
5765
6718
  flex-direction: column;
@@ -5778,6 +6731,10 @@
5778
6731
 
5779
6732
  <custom-element name="ui-sidebar-header" slot="header">
5780
6733
  <style>
6734
+ :host,
6735
+ * {
6736
+ touch-action: manipulation;
6737
+ }
5781
6738
  :host {
5782
6739
  display: flex;
5783
6740
  flex-direction: column;
@@ -5791,6 +6748,10 @@
5791
6748
 
5792
6749
  <custom-element name="ui-sidebar-footer" slot="footer">
5793
6750
  <style>
6751
+ :host,
6752
+ * {
6753
+ touch-action: manipulation;
6754
+ }
5794
6755
  :host {
5795
6756
  display: flex;
5796
6757
  flex-direction: column;
@@ -5804,6 +6765,10 @@
5804
6765
 
5805
6766
  <custom-element name="ui-sidebar-content">
5806
6767
  <style>
6768
+ :host,
6769
+ * {
6770
+ touch-action: manipulation;
6771
+ }
5807
6772
  :host {
5808
6773
  display: block;
5809
6774
  flex: 1;
@@ -5830,6 +6795,10 @@
5830
6795
  </script>
5831
6796
 
5832
6797
  <style>
6798
+ :host,
6799
+ * {
6800
+ touch-action: manipulation;
6801
+ }
5833
6802
  :host {
5834
6803
  display: flex;
5835
6804
  flex-direction: column;
@@ -5842,6 +6811,10 @@
5842
6811
 
5843
6812
  <custom-element name="ui-sidebar-group-label">
5844
6813
  <style>
6814
+ :host,
6815
+ * {
6816
+ touch-action: manipulation;
6817
+ }
5845
6818
  :host {
5846
6819
  display: var(--ui-sidebar-label, block);
5847
6820
  height: calc(var(--spacing) * 8);
@@ -5859,6 +6832,10 @@
5859
6832
 
5860
6833
  <custom-element name="ui-sidebar-menu">
5861
6834
  <style>
6835
+ :host,
6836
+ * {
6837
+ touch-action: manipulation;
6838
+ }
5862
6839
  :host {
5863
6840
  display: block;
5864
6841
  }
@@ -5877,6 +6854,10 @@
5877
6854
 
5878
6855
  <custom-element name="ui-sidebar-menu-item" role="listitem">
5879
6856
  <style>
6857
+ :host,
6858
+ * {
6859
+ touch-action: manipulation;
6860
+ }
5880
6861
  :host {
5881
6862
  position: relative;
5882
6863
  display: block;
@@ -5917,6 +6898,10 @@
5917
6898
  </script>
5918
6899
 
5919
6900
  <style>
6901
+ :host,
6902
+ * {
6903
+ touch-action: manipulation;
6904
+ }
5920
6905
  :host {
5921
6906
  display: block;
5922
6907
  }
@@ -5958,6 +6943,10 @@
5958
6943
 
5959
6944
  <custom-element name="ui-sidebar-menu-badge">
5960
6945
  <style>
6946
+ :host,
6947
+ * {
6948
+ touch-action: manipulation;
6949
+ }
5961
6950
  :host {
5962
6951
  display: var(--ui-sidebar-label, block);
5963
6952
  position: absolute;
@@ -5989,6 +6978,10 @@
5989
6978
 
5990
6979
  <custom-element name="ui-sidebar-inset">
5991
6980
  <style>
6981
+ :host,
6982
+ * {
6983
+ touch-action: manipulation;
6984
+ }
5992
6985
  :host {
5993
6986
  display: flex;
5994
6987
  flex: 1;
@@ -6011,6 +7004,12 @@
6011
7004
 
6012
7005
  <custom-element name="ui-skeleton">
6013
7006
  <style>
7007
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
7008
+ touch-action does not reach shadow content in iOS Safari. */
7009
+ :host,
7010
+ * {
7011
+ touch-action: manipulation;
7012
+ }
6014
7013
  :host {
6015
7014
  display: block;
6016
7015
  border-radius: var(--radius-md);
@@ -6056,9 +7055,21 @@
6056
7055
  return `${((props.value - props.min) / (props.max - props.min || 1)) * 100}%`;
6057
7056
  },
6058
7057
  });
7058
+
7059
+ // Writing the value Safari just set, during a finger drag, makes the thumb jump.
7060
+ observe(() => {
7061
+ const value = String(props.value);
7062
+ if (refs.control && refs.control.value !== value) refs.control.value = value;
7063
+ });
6059
7064
  </script>
6060
7065
 
6061
7066
  <style>
7067
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
7068
+ touch-action does not reach shadow content in iOS Safari. */
7069
+ :host,
7070
+ * {
7071
+ touch-action: manipulation;
7072
+ }
6062
7073
  :host {
6063
7074
  display: block;
6064
7075
  min-width: calc(var(--spacing) * 32);
@@ -6072,6 +7083,8 @@
6072
7083
  background: transparent;
6073
7084
  cursor: pointer;
6074
7085
  outline: none;
7086
+ /* The slider owns the whole gesture, so the browser never turns a drag into a page pan. */
7087
+ touch-action: none;
6075
7088
  }
6076
7089
  input::-webkit-slider-runnable-track {
6077
7090
  height: calc(var(--spacing) * 1.5);
@@ -6103,6 +7116,20 @@
6103
7116
  input:focus-visible::-webkit-slider-thumb {
6104
7117
  box-shadow: 0 0 0 4px color-mix(in oklab, var(--ring) 50%, transparent);
6105
7118
  }
7119
+ @media (pointer: coarse) {
7120
+ input {
7121
+ height: calc(var(--spacing) * 6);
7122
+ }
7123
+ input::-webkit-slider-thumb {
7124
+ width: calc(var(--spacing) * 6);
7125
+ height: calc(var(--spacing) * 6);
7126
+ margin-top: calc(var(--spacing) * -2.25);
7127
+ }
7128
+ input::-moz-range-thumb {
7129
+ width: calc(var(--spacing) * 6);
7130
+ height: calc(var(--spacing) * 6);
7131
+ }
7132
+ }
6106
7133
  input:disabled {
6107
7134
  cursor: not-allowed;
6108
7135
  opacity: 0.5;
@@ -6116,7 +7143,6 @@
6116
7143
  _min="@min"
6117
7144
  _max="@max"
6118
7145
  _step="@step"
6119
- _value="@value"
6120
7146
  _disabled="@disabled"
6121
7147
  _aria-label="@label"
6122
7148
  >
@@ -6136,6 +7162,12 @@
6136
7162
  </script>
6137
7163
 
6138
7164
  <style>
7165
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
7166
+ touch-action does not reach shadow content in iOS Safari. */
7167
+ :host,
7168
+ * {
7169
+ touch-action: manipulation;
7170
+ }
6139
7171
  :host {
6140
7172
  display: inline-flex;
6141
7173
  width: calc(var(--attr-size) * 1px);
@@ -6182,6 +7214,12 @@
6182
7214
  </script>
6183
7215
 
6184
7216
  <style>
7217
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
7218
+ touch-action does not reach shadow content in iOS Safari. */
7219
+ :host,
7220
+ * {
7221
+ touch-action: manipulation;
7222
+ }
6185
7223
  :host(:state(attr-checked)) label {
6186
7224
  color: var(--foreground);
6187
7225
  }
@@ -6282,6 +7320,12 @@
6282
7320
  </script>
6283
7321
 
6284
7322
  <style>
7323
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
7324
+ touch-action does not reach shadow content in iOS Safari. */
7325
+ :host,
7326
+ * {
7327
+ touch-action: manipulation;
7328
+ }
6285
7329
  :host {
6286
7330
  display: block;
6287
7331
  width: 100%;
@@ -6376,6 +7420,12 @@
6376
7420
  </script>
6377
7421
 
6378
7422
  <style>
7423
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
7424
+ touch-action does not reach shadow content in iOS Safari. */
7425
+ :host,
7426
+ * {
7427
+ touch-action: manipulation;
7428
+ }
6379
7429
  :host {
6380
7430
  display: flex;
6381
7431
  flex-direction: column;
@@ -6393,6 +7443,10 @@
6393
7443
 
6394
7444
  <custom-element name="ui-tabs-list">
6395
7445
  <style>
7446
+ :host,
7447
+ * {
7448
+ touch-action: manipulation;
7449
+ }
6396
7450
  :host {
6397
7451
  display: block;
6398
7452
  width: fit-content;
@@ -6429,6 +7483,10 @@
6429
7483
  </script>
6430
7484
 
6431
7485
  <style>
7486
+ :host,
7487
+ * {
7488
+ touch-action: manipulation;
7489
+ }
6432
7490
  :host {
6433
7491
  display: flex;
6434
7492
  flex: 1;
@@ -6488,6 +7546,10 @@
6488
7546
 
6489
7547
  <custom-element name="ui-tabs-content" role="tabpanel">
6490
7548
  <style>
7549
+ :host,
7550
+ * {
7551
+ touch-action: manipulation;
7552
+ }
6491
7553
  :host {
6492
7554
  display: block;
6493
7555
  flex: 1;
@@ -6518,6 +7580,12 @@
6518
7580
  </script>
6519
7581
 
6520
7582
  <style>
7583
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
7584
+ touch-action does not reach shadow content in iOS Safari. */
7585
+ :host,
7586
+ * {
7587
+ touch-action: manipulation;
7588
+ }
6521
7589
  :host {
6522
7590
  display: block;
6523
7591
  }
@@ -6538,6 +7606,12 @@
6538
7606
  field-sizing: content;
6539
7607
  resize: vertical;
6540
7608
  }
7609
+ /* iOS Safari zooms in on a focused field whose text is under 16px. */
7610
+ @media (pointer: coarse) {
7611
+ textarea {
7612
+ font-size: var(--text-base);
7613
+ }
7614
+ }
6541
7615
  textarea::placeholder {
6542
7616
  color: var(--muted-foreground);
6543
7617
  }
@@ -6615,6 +7689,12 @@
6615
7689
  </script>
6616
7690
 
6617
7691
  <style>
7692
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
7693
+ touch-action does not reach shadow content in iOS Safari. */
7694
+ :host,
7695
+ * {
7696
+ touch-action: manipulation;
7697
+ }
6618
7698
  ol {
6619
7699
  position: fixed;
6620
7700
  inset: auto calc(var(--spacing) * 4) calc(var(--spacing) * 4) auto;
@@ -6736,6 +7816,12 @@
6736
7816
  </script>
6737
7817
 
6738
7818
  <style>
7819
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
7820
+ touch-action does not reach shadow content in iOS Safari. */
7821
+ :host,
7822
+ * {
7823
+ touch-action: manipulation;
7824
+ }
6739
7825
  :host {
6740
7826
  display: inline-flex;
6741
7827
  align-items: center;
@@ -6781,6 +7867,12 @@
6781
7867
  </script>
6782
7868
 
6783
7869
  <style>
7870
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
7871
+ touch-action does not reach shadow content in iOS Safari. */
7872
+ :host,
7873
+ * {
7874
+ touch-action: manipulation;
7875
+ }
6784
7876
  :host {
6785
7877
  display: inline-flex;
6786
7878
  }
@@ -6885,6 +7977,12 @@
6885
7977
  </script>
6886
7978
 
6887
7979
  <style>
7980
+ /* A double tap must not zoom the page. Set inside the shadow root, because a page-level
7981
+ touch-action does not reach shadow content in iOS Safari. */
7982
+ :host,
7983
+ * {
7984
+ touch-action: manipulation;
7985
+ }
6888
7986
  :host {
6889
7987
  display: contents;
6890
7988
  }
@@ -6895,6 +7993,10 @@
6895
7993
 
6896
7994
  <custom-element name="ui-tooltip-trigger">
6897
7995
  <style>
7996
+ :host,
7997
+ * {
7998
+ touch-action: manipulation;
7999
+ }
6898
8000
  :host {
6899
8001
  display: inline-block;
6900
8002
  vertical-align: middle;
@@ -6906,14 +8008,32 @@
6906
8008
 
6907
8009
  <custom-element name="ui-tooltip-content" role="tooltip">
6908
8010
  <script>
6909
- defineProps({
8011
+ const props = defineProps({
6910
8012
  placement: prop.enum(["top", "bottom", "left", "right"]).default("top"),
6911
8013
  });
6912
8014
 
8015
+ const BLOCK_SIDES = new Set(["top", "bottom"]);
8016
+
8017
+ // Once open, it keeps the side it opened on. Re-deciding while the page scrolls makes it jump,
8018
+ // because iOS Safari's toolbar changes the viewport height mid-scroll.
8019
+ // Firefox holds a box that does not fit against the screen edge, so touching it counts too.
8020
+ function opened(event) {
8021
+ internals.states.delete("flipped");
8022
+ if (event.newState !== "open" || !BLOCK_SIDES.has(props.placement)) return;
8023
+ const box = host.getBoundingClientRect();
8024
+ if (box.bottom >= innerHeight || box.top <= 0) internals.states.add("flipped");
8025
+ }
8026
+
6913
8027
  attrs.popover ??= "manual";
8028
+
8029
+ listen("toggle", opened);
6914
8030
  </script>
6915
8031
 
6916
8032
  <style>
8033
+ :host,
8034
+ * {
8035
+ touch-action: manipulation;
8036
+ }
6917
8037
  :host {
6918
8038
  margin: 0 0 calc(var(--spacing) * 1);
6919
8039
  padding: calc(var(--spacing) * 1.5) calc(var(--spacing) * 3);
@@ -6923,6 +8043,9 @@
6923
8043
  color: var(--background);
6924
8044
  font-size: var(--text-xs);
6925
8045
  inset: auto;
8046
+ height: fit-content;
8047
+ max-height: calc(100svh - calc(var(--spacing) * 8));
8048
+ overflow-y: auto;
6926
8049
  position-area: top;
6927
8050
  position-try-fallbacks: flip-block;
6928
8051
  }
@@ -6930,6 +8053,17 @@
6930
8053
  position-area: bottom;
6931
8054
  margin: calc(var(--spacing) * 1) 0 0;
6932
8055
  }
8056
+ :host(:popover-open) {
8057
+ position-try-fallbacks: none;
8058
+ }
8059
+ :host(:state(flipped)) {
8060
+ position-area: bottom;
8061
+ margin: calc(var(--spacing) * 1) 0 0;
8062
+ }
8063
+ :host(:state(flipped):state(attr-placement-is-bottom)) {
8064
+ position-area: top;
8065
+ margin: 0 0 calc(var(--spacing) * 1);
8066
+ }
6933
8067
  :host(:state(attr-placement-is-left)) {
6934
8068
  position-area: left;
6935
8069
  margin: 0 calc(var(--spacing) * 1) 0 0;