@threadlabs/looma 0.39.0 → 0.39.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -96,7 +96,7 @@
96
96
  from:tabindex="{ true: '0', false: null }[concat(($loading or $pending) and not $disabled)]"
97
97
  >
98
98
  <span $if="$loading or $pending" class="spinner-wrap"><ui-spinner size="sm"></ui-spinner></span>
99
- <slot></slot>
99
+ <span class="label"><slot></slot></span>
100
100
  <span class="badge"><slot name="badge"></slot></span>
101
101
  </a>
102
102
  <button
@@ -109,7 +109,7 @@
109
109
  from:aria-busy="$loading or $pending"
110
110
  >
111
111
  <span $if="$loading or $pending" class="spinner-wrap"><ui-spinner size="sm"></ui-spinner></span>
112
- <slot></slot>
112
+ <span class="label"><slot></slot></span>
113
113
  <span class="badge"><slot name="badge"></slot></span>
114
114
  </button>
115
115
  </template>
@@ -237,6 +237,7 @@
237
237
 
238
238
  :host {
239
239
  --_button-square-size: var(--ui-button-min-block-size, var(--ui-control-size-md));
240
+ box-sizing: border-box !important;
240
241
  appearance: none;
241
242
  min-block-size: var(--ui-button-min-block-size, var(--ui-control-size-md));
242
243
  border-style: solid;
@@ -283,6 +284,21 @@
283
284
  padding-block: var(--ui-space-0-5) !important;
284
285
  }
285
286
 
287
+ /* Trim the text line itself: an anonymous flex item retains font-metric space in some fonts. */
288
+ .label {
289
+ display: block !important;
290
+ align-content: center !important;
291
+ min-inline-size: 0 !important;
292
+ min-block-size: 1lh !important;
293
+ }
294
+
295
+ .label:has(svg) {
296
+ display: inline-flex !important;
297
+ align-items: center !important;
298
+ gap: var(--ui-space-2) !important;
299
+ text-box-trim: none;
300
+ }
301
+
286
302
  .badge {
287
303
  display: inline-flex !important;
288
304
  align-items: center !important;
@@ -294,7 +310,7 @@
294
310
  }
295
311
 
296
312
  @supports (text-box-trim: trim-both) {
297
- :host {
313
+ .label:not(:has(svg)) {
298
314
  text-box-trim: trim-both;
299
315
  text-box-edge: cap alphabetic;
300
316
  }
@@ -622,6 +638,11 @@
622
638
  transition: color var(--ui-motion-fast) var(--ui-motion-ease), text-decoration-color var(--ui-motion-fast) var(--ui-motion-ease);
623
639
  }
624
640
 
641
+ :host[data-appearance="link"] .label {
642
+ display: inline !important;
643
+ text-box-trim: none;
644
+ }
645
+
625
646
  :host[data-appearance="link"]:hover:not(:disabled, [aria-disabled="true"]) {
626
647
  color: var(--ui-button-link-hover-text, var(--ui-text-primary));
627
648
  text-decoration-color: currentColor;
@@ -142,7 +142,8 @@
142
142
 
143
143
  input::after {
144
144
  content: "";
145
- position: absolute;
145
+ position: absolute !important;
146
+ transform: none !important;
146
147
  opacity: 0;
147
148
  transition: opacity var(--ui-motion-fast) var(--ui-motion-ease);
148
149
  }
@@ -155,43 +156,44 @@
155
156
  box-shadow: var(--ui-control-inset);
156
157
  }
157
158
 
158
- /* Centre the tick's box and lift it slightly for the rotated check shape. */
159
+ /* The SVG's painted bounds are centred in its viewBox, including its round stroke.
160
+ Centre that box directly; rotated border boxes do not centre the visible check. */
159
161
  input:checked::after {
160
- inset: 0;
161
- margin: auto !important;
162
- inline-size: calc(var(--_ui-checkbox-size) * 0.28 / 1.125);
163
- block-size: calc(var(--_ui-checkbox-size) * 0.5 / 1.125);
164
- /* Longhands, not `border: solid var(...)`: lowering drops a shorthand that omits a
165
- component, which left the tick with no style and no colour. */
166
- border-style: solid;
167
- border-color: var(--ui-text-on-accent);
168
- border-width: 0 calc(var(--_ui-checkbox-size) * 0.125 / 1.125) calc(var(--_ui-checkbox-size) * 0.125 / 1.125) 0;
162
+ inset: 50% auto auto 50% !important;
163
+ inline-size: calc(var(--_ui-checkbox-size) * 0.7) !important;
164
+ block-size: calc(var(--_ui-checkbox-size) * 0.7) !important;
165
+ background: var(--ui-text-on-accent);
166
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m5 11.5 5 5 9-9'/%3E%3C/svg%3E")
167
+ center / contain no-repeat;
169
168
  opacity: 1;
170
- transform: translateY(calc(var(--_ui-checkbox-size) * -0.09 / 1.125)) rotate(45deg);
169
+ translate: -50% -50% !important;
171
170
  animation: ui-checkbox-check var(--ui-motion-base) var(--ui-motion-ease-out, cubic-bezier(0.16, 1, 0.3, 1)) both;
172
171
  }
173
172
 
174
173
  input:indeterminate::after {
175
- inset: 50% calc(var(--_ui-checkbox-size) * 0.23 / 1.125) auto;
176
- block-size: calc(var(--_ui-checkbox-size) * 0.125 / 1.125);
174
+ mask: none;
175
+ animation: none;
176
+ inset: 50% calc(var(--_ui-checkbox-size) * 0.23 / 1.125) auto !important;
177
+ inline-size: auto !important;
178
+ block-size: calc(var(--_ui-checkbox-size) * 0.125 / 1.125) !important;
177
179
  border-radius: var(--ui-radius-round);
178
180
  background: var(--ui-text-on-accent);
179
181
  opacity: 1;
180
- transform: translateY(-50%);
182
+ translate: 0 -50% !important;
181
183
  }
182
184
 
183
185
  @keyframes ui-checkbox-check {
184
186
  from {
185
187
  opacity: 0;
186
- transform: translateY(calc(var(--_ui-checkbox-size) * -0.09 / 1.125)) rotate(45deg) scale(0.55);
188
+ scale: 0.55;
187
189
  }
188
190
  65% {
189
191
  opacity: 1;
190
- transform: translateY(calc(var(--_ui-checkbox-size) * -0.09 / 1.125)) rotate(45deg) scale(1.12);
192
+ scale: 1.12;
191
193
  }
192
194
  to {
193
195
  opacity: 1;
194
- transform: translateY(calc(var(--_ui-checkbox-size) * -0.09 / 1.125)) rotate(45deg) scale(1);
196
+ scale: 1;
195
197
  }
196
198
  }
197
199
 
@@ -202,6 +204,20 @@
202
204
  }
203
205
  }
204
206
 
207
+ @media (forced-colors: active) {
208
+ input:checked,
209
+ input:indeterminate {
210
+ forced-color-adjust: none;
211
+ background: Highlight;
212
+ border-color: Highlight;
213
+ }
214
+ input:checked::after,
215
+ input:indeterminate::after {
216
+ forced-color-adjust: none;
217
+ background: HighlightText;
218
+ }
219
+ }
220
+
205
221
  input:hover:enabled {
206
222
  border-color: var(--ui-accent-solid);
207
223
  }
@@ -1,5 +1,6 @@
1
1
  <link rel="component" href="../ui-icon/ui-icon.html">
2
2
  <link rel="component" href="../ui-number-input/ui-number-input.html">
3
+ <link rel="component" href="../ui-disclosure/ui-disclosure.html">
3
4
  <template component="ui-light-editor" module="./ui-light-editor.js">
4
5
  <defs>
5
6
  <state name="horizontal" type="string" value="50"></state>
@@ -29,31 +30,31 @@
29
30
  </button>
30
31
  </span>
31
32
  </span>
32
- <span class="coordinates">
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>
55
- </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>
33
+ <span class="hint">Drag or tap to move the light. Arrow keys adjust; Escape cancels.</span>
34
+ <ui-disclosure summary="Exact position"><span class="coordinates">
35
+ <ui-number-input
36
+ data-coordinate="x"
37
+ from:value="$horizontal"
38
+ min="0"
39
+ max="100"
40
+ step="1"
41
+ unit="%"
42
+ label="Light horizontal position"
43
+ from:size="$size"
44
+ from:disabled="$disabled"
45
+ ></ui-number-input>
46
+ <ui-number-input
47
+ data-coordinate="y"
48
+ from:value="$vertical"
49
+ min="0"
50
+ max="100"
51
+ step="1"
52
+ unit="%"
53
+ label="Light vertical position"
54
+ from:size="$size"
55
+ from:disabled="$disabled"
56
+ ></ui-number-input>
57
+ </span></ui-disclosure>
57
58
  </span>
58
59
  <style>
59
60
  :host {
@@ -1,4 +1,4 @@
1
- <template component="ui-skeleton">
1
+ <template component="ui-skeleton" module="./ui-skeleton.js">
2
2
  <defs>
3
3
  <prop name="shape" type="keyword" values="text, circle, rectangle" default="text"
4
4
  >The content's silhouette. text reserves one inherited line box with a shorter bar inside it; circle matches a medium Avatar; rectangle reserves a media
@@ -8,8 +8,12 @@
8
8
  Avatar's outer size for circles.</prop>
9
9
  <prop name="height" type="string" default=""
10
10
  >Block size as a CSS length. Empty reserves one line for text, a square for circles, or 8rem for rectangles. Set only width to keep a circle square.</prop>
11
- <prop name="animated" type="boolean" default="false"
12
- >Gently pulses the placeholder. Off by default, and always still when the reader requests reduced motion.</prop>
11
+ <prop
12
+ name="animated"
13
+ type="boolean"
14
+ default="true"
15
+ default-true-reason="A travelling highlight identifies pending content by default; reduced-motion preferences always keep it static."
16
+ >Moves a subtle highlight from left to right. On by default; pass false to keep it static. Reduced-motion preferences always disable the shimmer.</prop>
13
17
  </defs>
14
18
  <span aria-hidden="true" style:--ui-skeleton-width="$width" style:--ui-skeleton-height="$height"></span>
15
19
  <style>
@@ -22,8 +26,20 @@
22
26
  syntax: "*";
23
27
  inherits: false;
24
28
  }
29
+ @property --_ui-skeleton-viewport-x {
30
+ syntax: "<length>";
31
+ inherits: false;
32
+ initial-value: 0px;
33
+ }
34
+ @property --_ui-skeleton-viewport-scale {
35
+ syntax: "<number>";
36
+ inherits: false;
37
+ initial-value: 1;
38
+ }
25
39
 
26
40
  :host {
41
+ --_ui-skeleton-paint-x: var(--_ui-skeleton-viewport-x);
42
+ --_ui-skeleton-paint-scale: var(--_ui-skeleton-viewport-scale);
27
43
  display: block !important;
28
44
  position: relative;
29
45
  box-sizing: border-box !important;
@@ -65,23 +81,36 @@
65
81
  }
66
82
 
67
83
  :host([animated])::before {
68
- animation: ui-skeleton-pulse 1.6s ease-in-out infinite;
84
+ background-image: linear-gradient(
85
+ 90deg,
86
+ transparent,
87
+ light-dark(rgb(255 255 255 / 0.6), rgb(255 255 255 / 0.14)) 50%,
88
+ transparent
89
+ );
90
+ background-size: calc(100vw / var(--_ui-skeleton-paint-scale)) 100%;
91
+ background-position: calc(0vw - var(--_ui-skeleton-paint-x)) 0;
92
+ background-repeat: repeat-x;
93
+ animation: ui-skeleton-shimmer 3s linear infinite;
69
94
  }
70
- @keyframes ui-skeleton-pulse {
71
- 50% {
72
- opacity: 0.45;
95
+ @keyframes ui-skeleton-shimmer {
96
+ to {
97
+ background-position: calc(100vw / var(--_ui-skeleton-paint-scale) - var(--_ui-skeleton-paint-x)) 0;
73
98
  }
74
99
  }
75
100
  @media (prefers-reduced-motion: reduce) {
76
101
  :host([animated])::before {
77
102
  animation: none;
103
+ background-image: none;
78
104
  }
79
105
  }
80
106
  @media (forced-colors: active) {
81
107
  :host::before {
82
108
  background: CanvasText;
83
109
  opacity: 0.2;
110
+ }
111
+ :host([animated])::before {
84
112
  animation: none;
113
+ background-image: none;
85
114
  }
86
115
  }
87
116
  :host[hidden] {
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Keep one viewport gradient aligned through mounting, scrolling and transformed surfaces.
3
+ * @lifecycle CSS owns motion preferences; geometry tracking stops with the animation or disconnect.
4
+ */
5
+ export function connect(host) {
6
+ const element = host.element;
7
+ const view = element.ownerDocument.defaultView;
8
+ let motion;
9
+ let frame = 0;
10
+ let previousPosition = "";
11
+ let previousScale = "";
12
+ const trackPosition = () => {
13
+ frame = 0;
14
+ if (motion?.playState !== "running") return;
15
+ const bounds = element.getBoundingClientRect();
16
+ const width = parseFloat(view.getComputedStyle(element).width);
17
+ const scale = bounds.width > 0 && width > 0 ? bounds.width / width : 1;
18
+ const position = `${bounds.left / scale}px`;
19
+ const scaleValue = String(scale);
20
+ if (position !== previousPosition) {
21
+ element.style.setProperty("--_ui-skeleton-viewport-x", position);
22
+ previousPosition = position;
23
+ }
24
+ if (scaleValue !== previousScale) {
25
+ element.style.setProperty("--_ui-skeleton-viewport-scale", scaleValue);
26
+ previousScale = scaleValue;
27
+ }
28
+ frame = view.requestAnimationFrame(trackPosition);
29
+ };
30
+ const synchronize = () => {
31
+ const names = view.getComputedStyle(element, "::before").animationName.split(",").map(name => name.trim());
32
+ for (const animation of element.getAnimations({ subtree: true })) {
33
+ if (names.includes(animation.animationName)) {
34
+ motion = animation;
35
+ motion.startTime = 0;
36
+ }
37
+ }
38
+ if (!frame) trackPosition();
39
+ };
40
+ element.addEventListener("animationstart", synchronize);
41
+ synchronize();
42
+ return () => {
43
+ element.removeEventListener("animationstart", synchronize);
44
+ view.cancelAnimationFrame(frame);
45
+ };
46
+ }