@threadlabs/looma 0.35.0 → 0.36.0

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 (66) hide show
  1. package/components/ui-button/ui-button.html +11 -4
  2. package/components/ui-color-picker/ui-color-picker.html +1 -1
  3. package/components/ui-combobox/ui-combobox.html +1 -1
  4. package/components/ui-curve-editor/ui-curve-editor.html +1 -0
  5. package/components/ui-curve-editor/ui-curve-editor.js +15 -6
  6. package/components/ui-input/ui-input.html +4 -1
  7. package/components/ui-light-editor/ui-light-editor.html +143 -19
  8. package/components/ui-light-editor/ui-light-editor.js +26 -9
  9. package/components/ui-reel/ui-reel.html +2 -1
  10. package/components/ui-select/ui-select.html +10 -6
  11. package/components/ui-tabs/ui-tabs.html +2 -3
  12. package/components/ui-tabs/ui-tabs.js +0 -9
  13. package/components/ui-tooltip/ui-tooltip.html +2 -1
  14. package/dist/index.js +1 -1
  15. package/docs/README.md +36 -8
  16. package/docs/card.html +7 -6
  17. package/docs/card.js +1 -0
  18. package/docs/catalog-view.mjs +2 -1
  19. package/docs/catalog.json +46 -12
  20. package/docs/customizer-render.mjs +38 -18
  21. package/docs/customizer.js +128 -21
  22. package/docs/fonts.mjs +45 -0
  23. package/docs/index.d.ts +2 -2
  24. package/docs/palette.mjs +67 -0
  25. package/docs/site.mjs +5 -4
  26. package/docs/theme-session.mjs +28 -9
  27. package/docs/theme.mjs +69 -27
  28. package/docs/value-editors.mjs +6 -4
  29. package/package.json +2 -2
  30. package/styles/ui-button.css +11 -4
  31. package/styles/ui-color-picker.css +1 -1
  32. package/styles/ui-combobox.css +2 -2
  33. package/styles/ui-input.css +4 -1
  34. package/styles/ui-light-editor.css +111 -16
  35. package/styles/ui-select.css +10 -6
  36. package/styles/ui-tabs.css +2 -3
  37. package/styles/ui-tooltip.css +2 -1
  38. package/theme-system.css +14 -2
  39. package/tokens.css +11 -1
  40. package/vanilla/UiCurveEditor.d.ts +1 -0
  41. package/vanilla/UiCurveEditor.js +1 -1
  42. package/vanilla/UiLightEditor.d.ts +1 -0
  43. package/vanilla/UiLightEditor.js +2 -2
  44. package/vue/UiButton.vue +4 -2
  45. package/vue/UiColorPicker.vue +1 -1
  46. package/vue/UiCombobox.vue +2 -2
  47. package/vue/UiCurveEditor.d.ts +6 -0
  48. package/vue/UiCurveEditor.vue +2 -0
  49. package/vue/UiInput.vue +4 -1
  50. package/vue/UiLightEditor.d.ts +8 -0
  51. package/vue/UiLightEditor.vue +22 -3
  52. package/vue/UiReel.vue +1 -1
  53. package/vue/UiSelect.vue +1 -1
  54. package/vue/UiTabs.vue +2 -3
  55. package/vue/UiTooltip.vue +2 -1
  56. package/vue/chunks/UiButton.js +1 -1
  57. package/vue/chunks/UiColorPicker.js +1 -1
  58. package/vue/chunks/UiCombobox.js +1 -1
  59. package/vue/chunks/UiCurveEditor.js +5 -3
  60. package/vue/chunks/UiInput.js +1 -1
  61. package/vue/chunks/UiLightEditor.js +16 -6
  62. package/vue/chunks/UiReel.js +2 -2
  63. package/vue/chunks/UiSelect.js +1 -1
  64. package/vue/chunks/UiTabs.js +1 -1
  65. package/vue/chunks/UiTooltip.js +1 -1
  66. package/vue/components.css +47 -19
@@ -249,7 +249,9 @@
249
249
  line-height: var(--ui-line-height-tight);
250
250
  padding: var(--ui-control-inset-block-md, var(--ui-space-2)) var(--ui-action-inset-md, var(--ui-space-3)) !important;
251
251
  background-image: var(--ui-surface-highlight, none);
252
- box-shadow: var(--ui-button-shadow, var(--ui-action-shadow, var(--_ui-action-shadow))), var(--_tone-glow);
252
+ box-shadow:
253
+ var(--ui-button-shadow, var(--ui-action-shadow, var(--_ui-action-shadow))),
254
+ var(--_tone-glow);
253
255
  cursor: pointer;
254
256
  transition:
255
257
  scale var(--ui-motion-press, var(--ui-motion-fast)) var(--ui-motion-ease),
@@ -382,7 +384,9 @@
382
384
  background-color: var(--_tone);
383
385
  color: var(--_on-tone);
384
386
  background-image: var(--_tone-highlight);
385
- box-shadow: var(--ui-button-solid-shadow, var(--ui-action-shadow, var(--_ui-action-shadow))), var(--_tone-glow);
387
+ box-shadow:
388
+ var(--ui-button-solid-shadow, var(--ui-action-shadow, var(--_ui-action-shadow))),
389
+ var(--_tone-glow);
386
390
  }
387
391
 
388
392
  :is(:host[data-appearance="solid"], :host[data-appearance="danger"]):hover:not(:disabled, [aria-disabled="true"]) {
@@ -552,8 +556,11 @@
552
556
 
553
557
  /* The size states outrank :host, so sm is named too, or a small button would stay small. */
554
558
  @media (pointer: coarse) {
555
- :host, :host([size="sm"]) {
556
- min-block-size: var(--ui-control-min-block-size);
559
+ :host {
560
+ min-block-size: max(var(--ui-control-size-md), var(--ui-control-min-block-size));
561
+ }
562
+ :host([size="sm"]) {
563
+ min-block-size: max(var(--ui-control-size-sm), var(--ui-control-min-block-size));
557
564
  }
558
565
  }
559
566
 
@@ -127,7 +127,7 @@
127
127
  --_size: max(var(--ui-control-size-md), var(--ui-control-min-block-size));
128
128
  }
129
129
  :host([size="sm"]) {
130
- --_size: var(--ui-control-min-block-size);
130
+ --_size: max(var(--ui-control-size-sm), var(--ui-control-min-block-size));
131
131
  --_font: var(--ui-font-size-md);
132
132
  }
133
133
  }
@@ -882,7 +882,7 @@
882
882
  /* Under touch a small field is still a touch-sized target, as a small button is, and keeps
883
883
  body-size text: below 16px, iOS zooms the page into the focused field. */
884
884
  :host([size="sm"]) {
885
- --_row: var(--ui-control-min-block-size);
885
+ --_row: max(var(--ui-control-size-sm), var(--ui-control-min-block-size));
886
886
  }
887
887
  :host([size="sm"]) input {
888
888
  font-size: inherit;
@@ -13,6 +13,7 @@
13
13
  <prop name="disabled" type="boolean" default="false">Prevents typing, stepping and pointer or keyboard changes while keeping the value visible.</prop>
14
14
  <event name="input" type="object"><prop name="value" type="string" required></prop></event>
15
15
  <event name="change" type="object"><prop name="value" type="string" required></prop></event>
16
+ <event name="cancel" type="object"><prop name="value" type="string" required></prop></event>
16
17
  </defs>
17
18
  <span>
18
19
  <span class="plot-frame" from:aria-label="$label" role="group"><span class="plot" $ref="plot">
@@ -24,21 +24,30 @@ export function connect(host) {
24
24
  if (!drag || event.pointerId !== drag.id || host.props.disabled.value) return;
25
25
  const rect = plot.getBoundingClientRect();
26
26
  if (!rect.width || !rect.height) return;
27
- points[drag.index] = (event.clientX - rect.left) / rect.width;
28
- points[drag.index + 1] = 2 - (event.clientY - rect.top) / rect.height * 3;
27
+ points[drag.index] = (event.clientX - rect.left - drag.offsetX) / rect.width;
28
+ points[drag.index + 1] = 2 - (event.clientY - rect.top - drag.offsetY) / rect.height * 3;
29
29
  paint(); emit("input");
30
30
  };
31
- const finish = event => { if (drag?.id !== event.pointerId) return; drag = undefined; if (!host.props.disabled.value) emit("change"); };
31
+ const finish = event => {
32
+ if (drag?.id !== event.pointerId) return;
33
+ const start = drag; drag = undefined;
34
+ if (start.handle.hasPointerCapture(start.id)) start.handle.releasePointerCapture(start.id);
35
+ if (event.type !== 'pointerup' || host.props.disabled.value) { points = start.points; paint(); emit('input'); emit('cancel'); }
36
+ else emit('change');
37
+ };
32
38
  const cleanups = [];
33
39
  for (const [index, handle] of [[0, first], [2, second]]) {
34
- const down = event => { if (host.props.disabled.value || event.button !== 0) return; event.preventDefault(); handle.focus(); drag = { id: event.pointerId, index }; handle.setPointerCapture(event.pointerId); };
40
+ const down = event => { if (host.props.disabled.value || event.button !== 0) return; event.preventDefault(); handle.focus(); const rect = plot.getBoundingClientRect();
41
+ drag = { id: event.pointerId, index, handle, points: [...points], offsetX: event.clientX - rect.left - points[index] * rect.width, offsetY: event.clientY - rect.top - (2 - points[index + 1]) / 3 * rect.height };
42
+ handle.setPointerCapture(event.pointerId); };
35
43
  const key = event => {
44
+ if (event.key === 'Escape' && drag) { event.preventDefault(); finish({ pointerId: drag.id, type: 'cancel' }); return; }
36
45
  if (host.props.disabled.value || !["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key)) return;
37
46
  event.preventDefault(); const step = event.shiftKey ? .1 : .01;
38
47
  points[index + (event.key === "ArrowUp" || event.key === "ArrowDown" ? 1 : 0)] += (event.key === "ArrowLeft" || event.key === "ArrowDown" ? -step : step);
39
48
  paint(); emit("input"); emit("change");
40
49
  };
41
- for (const [type, callback] of [["pointerdown", down], ["pointermove", move], ["pointerup", finish], ["pointercancel", finish], ["keydown", key]]) {
50
+ for (const [type, callback] of [["pointerdown", down], ["pointermove", move], ["pointerup", finish], ["pointercancel", finish], ["lostpointercapture", finish], ["keydown", key]]) {
42
51
  handle.addEventListener(type, callback); cleanups.push(() => handle.removeEventListener(type, callback));
43
52
  }
44
53
  }
@@ -47,5 +56,5 @@ export function connect(host) {
47
56
  for (const type of ["input", "change"]) { field.addEventListener(type, edit); cleanups.push(() => field.removeEventListener(type, edit)); }
48
57
  });
49
58
  const stop = host.effect(() => { const value = host.props.value.value; if (value === external) return; external = value; try { points = curvePoints(value); } catch { /* Keep the last valid curve. */ } paint(); });
50
- return () => { stop(); cleanups.forEach(cleanup => cleanup()); };
59
+ return () => { stop(); cleanups.forEach(cleanup => cleanup()); if (drag?.handle.hasPointerCapture(drag.id)) drag.handle.releasePointerCapture(drag.id); };
51
60
  }
@@ -150,7 +150,10 @@
150
150
  }
151
151
 
152
152
  :host([size="sm"]) {
153
- min-block-size: var(--ui-input-min-block-size, var(--_ui-default-input-min-block-size, var(--ui-control-min-block-size)));
153
+ min-block-size: var(
154
+ --ui-input-min-block-size,
155
+ var(--_ui-default-input-min-block-size, max(var(--ui-control-size-sm), var(--ui-control-min-block-size)))
156
+ );
154
157
  }
155
158
  }
156
159
 
@@ -1,3 +1,4 @@
1
+ <link rel="component" href="../ui-icon/ui-icon.html">
1
2
  <link rel="component" href="../ui-number-input/ui-number-input.html">
2
3
  <template component="ui-light-editor" module="./ui-light-editor.js">
3
4
  <defs>
@@ -10,36 +11,159 @@
10
11
  <prop name="disabled" type="boolean" default="false">Prevents pointer, keyboard and exact edits.</prop>
11
12
  <event name="input" type="object"><prop name="x" type="number" required></prop><prop name="y" type="number" required></prop></event>
12
13
  <event name="change" type="object"><prop name="x" type="number" required></prop><prop name="y" type="number" required></prop></event>
14
+ <event name="cancel" type="object"><prop name="x" type="number" required></prop><prop name="y" type="number" required></prop></event>
13
15
  </defs>
14
16
  <span>
15
17
  <span class="stage" from:aria-label="$label" role="group">
16
18
  <span class="plane" $ref="plane">
17
19
  <svg viewBox="0 0 100 60" preserveAspectRatio="none" aria-hidden="true"><path d="M0 30 H100 M50 0 V60"></path></svg>
18
20
  <span class="specimen"><slot></slot></span>
19
- <button type="button" class="source" $ref="source" from:disabled="$disabled" from:aria-label="concat($label, '. Arrow keys move the source; Shift moves faster.')"></button>
21
+ <button
22
+ type="button"
23
+ class="source"
24
+ $ref="source"
25
+ from:disabled="$disabled"
26
+ from:aria-label="concat($label, '. Arrow keys move the source; Shift moves faster.')"
27
+ >
28
+ <span class="source-glyph"><ui-icon name="lightbulb"></ui-icon></span>
29
+ </button>
20
30
  </span>
21
31
  </span>
22
32
  <span class="coordinates">
23
- <ui-number-input data-coordinate="x" from:value="$horizontal" min="0" max="100" step="1" unit="%" label="Light horizontal position" from:size="$size" from:disabled="$disabled"></ui-number-input>
24
- <ui-number-input data-coordinate="y" from:value="$vertical" min="0" max="100" step="1" unit="%" label="Light vertical position" from:size="$size" from:disabled="$disabled"></ui-number-input>
33
+ <ui-number-input
34
+ data-coordinate="x"
35
+ from:value="$horizontal"
36
+ min="0"
37
+ max="100"
38
+ step="1"
39
+ unit="%"
40
+ label="Light horizontal position"
41
+ from:size="$size"
42
+ from:disabled="$disabled"
43
+ ></ui-number-input>
44
+ <ui-number-input
45
+ data-coordinate="y"
46
+ from:value="$vertical"
47
+ min="0"
48
+ max="100"
49
+ step="1"
50
+ unit="%"
51
+ label="Light vertical position"
52
+ from:size="$size"
53
+ from:disabled="$disabled"
54
+ ></ui-number-input>
25
55
  </span>
26
- <span class="hint">Drag the light, use arrow keys, or enter its position. Top centre is 50%, 0%.</span>
56
+ <span class="hint">Drag the light, tap the background, use arrow keys, or enter its position. Escape cancels a drag. Top centre is 50%, 0%.</span>
27
57
  </span>
28
58
  <style>
29
- :host { display: grid !important; gap: var(--ui-space-3) !important; min-inline-size: 0; padding: 0 !important; margin: 0 !important; }
30
- .stage { box-sizing: border-box !important; display: block !important; block-size: 13rem; padding: calc(var(--ui-control-min-block-size) / 2) !important; border: var(--ui-border-width) solid var(--ui-control-border); border-radius: var(--ui-field-radius, var(--ui-radius-md)); background: var(--ui-neutral-surface, var(--ui-surface)); }
31
- .plane { position: relative; display: block !important; inline-size: 100%; block-size: 100%; }
32
- svg { inline-size: 100%; block-size: 100%; overflow: visible; }
33
- path { fill: none; stroke: var(--ui-border); stroke-width: 1; vector-effect: non-scaling-stroke; }
34
- .specimen { position: absolute; inset: 0; display: grid !important; place-items: center !important; }
35
- .source { position: absolute; left: 50%; top: 0; translate: -50% -50%; display: grid !important; place-items: center !important; inline-size: max(2rem, var(--ui-control-min-block-size)); block-size: max(2rem, var(--ui-control-min-block-size)); padding: 0 !important; margin: 0 !important; border: 0; border-radius: 50%; background: transparent; cursor: grab; touch-action: none; }
36
- .source::after { content: ""; inline-size: 1rem; block-size: 1rem; border: var(--ui-border-width) solid var(--ui-accent); border-radius: 50%; background: var(--ui-surface-raised); box-shadow: 0 0 0 var(--ui-space-2) color-mix(in srgb, var(--ui-accent) 12%, transparent), var(--ui-shadow-sm); }
37
- .source:active { cursor: grabbing; }
38
- .source:focus-visible { outline: var(--ui-focus-width) solid var(--ui-focus-ring); outline-offset: var(--ui-space-1); }
39
- .source:disabled { cursor: not-allowed; }
40
- .source:disabled::after { background: var(--ui-disabled-surface); border-color: var(--ui-disabled-border); box-shadow: none; }
41
- .coordinates { display: grid !important; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--ui-space-2) !important; }
42
- .hint { font-size: var(--ui-font-size-sm); color: var(--ui-text-secondary); }
43
- @media (forced-colors: active) { .source::after { background: Canvas; border-color: CanvasText; } .source:focus-visible { outline-color: Highlight; } }
59
+ :host {
60
+ display: grid !important;
61
+ gap: var(--ui-space-3) !important;
62
+ min-inline-size: 0;
63
+ padding: 0 !important;
64
+ margin: 0 !important;
65
+ }
66
+ .stage {
67
+ box-sizing: border-box !important;
68
+ display: block !important;
69
+ block-size: 13rem;
70
+ padding: calc(var(--ui-control-min-block-size) / 2) !important;
71
+ border: var(--ui-border-width) solid var(--ui-control-border);
72
+ border-radius: var(--ui-field-radius, var(--ui-radius-md));
73
+ background: var(--ui-neutral-surface, var(--ui-surface));
74
+ }
75
+ .plane {
76
+ position: relative;
77
+ display: block !important;
78
+ inline-size: 100%;
79
+ block-size: 100%;
80
+ }
81
+ svg {
82
+ inline-size: 100%;
83
+ block-size: 100%;
84
+ overflow: visible;
85
+ }
86
+ path {
87
+ fill: none;
88
+ stroke: var(--ui-border);
89
+ stroke-width: 1;
90
+ vector-effect: non-scaling-stroke;
91
+ }
92
+ .specimen {
93
+ position: absolute;
94
+ inset: 0;
95
+ display: grid !important;
96
+ place-items: center !important;
97
+ }
98
+ .source {
99
+ position: absolute;
100
+ left: 50%;
101
+ top: 0;
102
+ translate: -50% -50%;
103
+ display: grid !important;
104
+ place-items: center !important;
105
+ inline-size: max(2rem, var(--ui-control-min-block-size));
106
+ block-size: max(2rem, var(--ui-control-min-block-size));
107
+ padding: 0 !important;
108
+ margin: 0 !important;
109
+ border: 0;
110
+ border-radius: 50%;
111
+ background: transparent;
112
+ cursor: grab;
113
+ touch-action: none;
114
+ }
115
+ .source-glyph {
116
+ position: relative;
117
+ z-index: 1;
118
+ display: inline-flex !important;
119
+ color: var(--ui-accent);
120
+ font-size: var(--ui-font-size-lg);
121
+ }
122
+ .source::after {
123
+ content: "";
124
+ position: absolute;
125
+ inline-size: 2rem;
126
+ block-size: 2rem;
127
+ border: var(--ui-border-width) solid var(--ui-accent);
128
+ border-radius: 50%;
129
+ background: var(--ui-surface-raised);
130
+ box-shadow: 0 0 0 var(--ui-space-2) color-mix(in srgb, var(--ui-accent) 12%, transparent), var(--ui-shadow-sm);
131
+ }
132
+ .source:active {
133
+ cursor: grabbing;
134
+ }
135
+ .source:focus-visible {
136
+ outline: var(--ui-focus-width) solid var(--ui-focus-ring);
137
+ outline-offset: var(--ui-space-1);
138
+ }
139
+ .source:disabled {
140
+ cursor: not-allowed;
141
+ }
142
+ .source:disabled::after {
143
+ background: var(--ui-disabled-surface);
144
+ border-color: var(--ui-disabled-border);
145
+ box-shadow: none;
146
+ }
147
+ .coordinates {
148
+ display: grid !important;
149
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
150
+ gap: var(--ui-space-2) !important;
151
+ }
152
+ .hint {
153
+ font-size: var(--ui-font-size-sm);
154
+ color: var(--ui-text-secondary);
155
+ }
156
+ @media (forced-colors: active) {
157
+ .source-glyph {
158
+ color: CanvasText;
159
+ }
160
+ .source::after {
161
+ background: Canvas;
162
+ border-color: CanvasText;
163
+ }
164
+ .source:focus-visible {
165
+ outline-color: Highlight;
166
+ }
167
+ }
44
168
  </style>
45
169
  </template>
@@ -2,7 +2,7 @@ const coordinate = value => Number.isFinite(Number(value)) ? Math.round(Math.max
2
2
 
3
3
  export function connect(host) {
4
4
  const { plane, source } = host.refs;
5
- let x = coordinate(host.props.x.value), y = coordinate(host.props.y.value), drag;
5
+ let x = coordinate(host.props.x.value), y = coordinate(host.props.y.value), drag, suppressClick = false;
6
6
  const listeners = [];
7
7
  const listen = (element, type, fn) => { element.addEventListener(type, fn); listeners.push(() => element.removeEventListener(type, fn)); };
8
8
  const paint = () => {
@@ -14,21 +14,38 @@ export function connect(host) {
14
14
  const emit = type => host.dispatch(type, { x, y });
15
15
  listen(source, 'pointerdown', event => {
16
16
  if (host.props.disabled.value || event.button !== 0) return;
17
- event.preventDefault(); source.focus(); drag = event.pointerId; source.setPointerCapture(drag);
17
+ event.preventDefault(); source.focus();
18
+ const rect = plane.getBoundingClientRect();
19
+ drag = { id: event.pointerId, x, y, offsetX: event.clientX - rect.left - x * rect.width, offsetY: event.clientY - rect.top - y * rect.height };
20
+ source.setPointerCapture(drag.id);
18
21
  });
19
22
  listen(source, 'pointermove', event => {
20
- if (drag !== event.pointerId || host.props.disabled.value) return;
23
+ if (drag?.id !== event.pointerId || host.props.disabled.value) return;
21
24
  const rect = plane.getBoundingClientRect(); if (!rect.width || !rect.height) return;
22
- x = (event.clientX - rect.left) / rect.width; y = (event.clientY - rect.top) / rect.height;
25
+ x = (event.clientX - rect.left - drag.offsetX) / rect.width; y = (event.clientY - rect.top - drag.offsetY) / rect.height;
23
26
  paint(); emit('input');
24
27
  });
25
28
  const end = event => {
26
- if (drag !== event.pointerId) return;
27
- drag = undefined;
28
- if (source.hasPointerCapture(event.pointerId)) source.releasePointerCapture(event.pointerId);
29
- if (!host.props.disabled.value) emit('change');
29
+ if (drag?.id !== event.pointerId) return;
30
+ const start = drag; drag = undefined; suppressClick = true;
31
+ if (source.hasPointerCapture(start.id)) source.releasePointerCapture(start.id);
32
+ if (event.type !== 'pointerup' || host.props.disabled.value) {
33
+ x = start.x; y = start.y; paint(); emit('input'); emit('cancel');
34
+ } else if (!host.props.disabled.value) emit('change');
30
35
  };
31
36
  listen(source, 'pointerup', end); listen(source, 'pointercancel', end); listen(source, 'lostpointercapture', end);
37
+ listen(source, 'keydown', event => {
38
+ if (event.key === 'Escape' && drag) { event.preventDefault(); end({ pointerId: drag.id, type: 'cancel' }); }
39
+ });
40
+ // A background tap is deliberate placement; scrolling and specimen controls keep native gestures.
41
+ listen(plane, 'pointerdown', () => { suppressClick = false; });
42
+ listen(plane, 'click', event => {
43
+ if (suppressClick) { suppressClick = false; return; }
44
+ if (host.props.disabled.value || event.target.closest('button,input,select,textarea,a,[role="button"]')) return;
45
+ const rect = plane.getBoundingClientRect(); if (!rect.width || !rect.height) return;
46
+ x = (event.clientX - rect.left) / rect.width; y = (event.clientY - rect.top) / rect.height;
47
+ paint(); emit('input'); emit('change');
48
+ });
32
49
  listen(source, 'keydown', event => {
33
50
  if (host.props.disabled.value || !['ArrowLeft','ArrowRight','ArrowUp','ArrowDown','Home','End'].includes(event.key)) return;
34
51
  event.preventDefault(); const step = event.shiftKey ? .1 : .01;
@@ -49,5 +66,5 @@ export function connect(host) {
49
66
  });
50
67
  }
51
68
  const stop = host.effect(() => { x = coordinate(host.props.x.value); y = coordinate(host.props.y.value); paint(); });
52
- return () => { stop(); listeners.forEach(cleanup => cleanup()); if (drag !== undefined && source.hasPointerCapture(drag)) source.releasePointerCapture(drag); };
69
+ return () => { stop(); listeners.forEach(cleanup => cleanup()); if (drag && source.hasPointerCapture(drag.id)) source.releasePointerCapture(drag.id); };
53
70
  }
@@ -8,7 +8,8 @@
8
8
  scrolls sideways once they overflow.</prop>
9
9
  <prop name="snap" type="keyword" values="start, center"
10
10
  >Snaps scrolling to items: start comes to rest with an item's start edge at the reel's start, and center with an item centred in the reel. Unset, the reel
11
- scrolls freely.</prop>
11
+ scrolls freely. Wrap a component item in a plain element when snapping: the wrapper receives snap alignment while the component keeps ownership of its
12
+ root styles.</prop>
12
13
  </defs>
13
14
  <div role="region" tabindex="0"><slot></slot></div>
14
15
  <style>
@@ -84,10 +84,12 @@
84
84
  --_ui-select-highlight: var(--ui-surface-highlight, linear-gradient(transparent, transparent));
85
85
  appearance: none;
86
86
  padding-inline-end: calc(var(--ui-space-3) + 1.5rem) !important;
87
- background-image: var(
88
- --ui-select-chevron,
89
- url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235c5c58' stroke-width='2.25' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E")
90
- ), var(--_ui-select-highlight);
87
+ background-image:
88
+ var(
89
+ --ui-select-chevron,
90
+ url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235c5c58' stroke-width='2.25' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E")
91
+ ),
92
+ var(--_ui-select-highlight);
91
93
  background-repeat: no-repeat;
92
94
  background-position: right var(--ui-space-3) center, center;
93
95
  background-size: 1.125rem 1.125rem, 100% 100%;
@@ -134,7 +136,9 @@
134
136
  padding: var(--ui-control-inset-block-sm, var(--ui-space-1)) var(--ui-space-2) !important;
135
137
  padding-inline-end: calc(var(--ui-space-2) + var(--ui-icon-size-sm) + var(--ui-space-1)) !important;
136
138
  background-position: right var(--ui-space-2) center, center;
137
- background-size: var(--ui-icon-size-sm) var(--ui-icon-size-sm), 100% 100%;
139
+ background-size:
140
+ var(--ui-icon-size-sm) var(--ui-icon-size-sm),
141
+ 100% 100%;
138
142
  font-size: var(--ui-font-size-sm);
139
143
  }
140
144
 
@@ -154,7 +158,7 @@
154
158
  }
155
159
 
156
160
  :host([size="sm"]) {
157
- min-block-size: var(--ui-control-min-block-size);
161
+ min-block-size: max(var(--ui-control-size-sm), var(--ui-control-min-block-size));
158
162
  font-size: inherit;
159
163
  }
160
164
  }
@@ -98,10 +98,9 @@
98
98
  max-inline-size: 100%;
99
99
  /* Tabs that do not fit scroll sideways rather than wrapping or squeezing. */
100
100
  overflow-x: auto;
101
- /* overflow-x: auto would make the block axis scrollable too; a sideways swipe must not scroll the
102
- strip or the page vertically. */
101
+ /* Contain sideways overscroll without trapping vertical page scrolling. */
103
102
  overflow-y: hidden;
104
- overscroll-behavior: contain;
103
+ overscroll-behavior-inline: contain;
105
104
  scrollbar-width: thin;
106
105
  border-block-end: var(--ui-border-width) solid var(--ui-border-divider);
107
106
  }
@@ -122,17 +122,9 @@ export function connect(host) {
122
122
  if (list.contains(event.relatedTarget)) return;
123
123
  for (const button of tabButtons()) button.tabIndex = button.value === host.state.internalValue ? 0 : -1;
124
124
  };
125
- // A vertical mouse wheel over an overflowing strip scrolls it sideways instead of the page.
126
- // Trackpad gestures already carry a horizontal delta and are left to the browser.
127
- const onWheel = (event) => {
128
- if (list.scrollWidth <= list.clientWidth || event.deltaX !== 0 || event.ctrlKey) return;
129
- event.preventDefault();
130
- list.scrollLeft += event.deltaY;
131
- };
132
125
  list.addEventListener("click", onClick);
133
126
  list.addEventListener("keydown", onKeydown);
134
127
  list.addEventListener("focusout", onFocusout);
135
- list.addEventListener("wheel", onWheel, { passive: false });
136
128
  const observer = new MutationObserver(readPanels);
137
129
  observer.observe(container, { childList: true });
138
130
  const tabObserver = new MutationObserver(syncButtons);
@@ -153,6 +145,5 @@ export function connect(host) {
153
145
  list.removeEventListener("click", onClick);
154
146
  list.removeEventListener("keydown", onKeydown);
155
147
  list.removeEventListener("focusout", onFocusout);
156
- list.removeEventListener("wheel", onWheel);
157
148
  };
158
149
  }
@@ -238,7 +238,8 @@
238
238
  border-inline-end-width: var(--ui-border-width);
239
239
  border-block-end-width: var(--ui-border-width);
240
240
  border-end-end-radius: var(--ui-border-width);
241
- transform: rotate(45deg) scaleX(1.25) scaleY(0.85);
241
+ /* Stretch in screen axes after rotation so both arrow shoulders stay level. */
242
+ transform: scale(1.25, 0.85) rotate(45deg);
242
243
  }
243
244
 
244
245
  :host[data-ui-actual-placement="bottom"] .surface::after {