@threadlabs/looma 0.23.22 → 0.23.24

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.
@@ -205,6 +205,8 @@ export function createAnchoredSurface(surface, options = {}) {
205
205
  let point = null;
206
206
  let open = false;
207
207
  let frame = null;
208
+ let anchorFrame = null;
209
+ let lastAnchorRect = null;
208
210
  let abort = null;
209
211
  let sizeObserver = null;
210
212
  surface.setAttribute("popover", "manual");
@@ -218,12 +220,31 @@ export function createAnchoredSurface(surface, options = {}) {
218
220
  return;
219
221
  }
220
222
  if (!anchor) return;
221
- fallbackPosition(surface, anchor.getBoundingClientRect(), placement, gap(), viewportGap);
223
+ lastAnchorRect = anchor.getBoundingClientRect();
224
+ fallbackPosition(surface, lastAnchorRect, placement, gap(), viewportGap);
222
225
  };
223
226
  const schedule = () => {
224
227
  if (open && frame === null) frame = owner.requestAnimationFrame(position);
225
228
  };
229
+ // Resize and scroll events do not report an ancestor's transition or layout shift.
230
+ // While visible, sample the anchor and only re-position when its bounds change.
231
+ const trackAnchor = () => {
232
+ anchorFrame = null;
233
+ if (!open || !anchor || point) return;
234
+ const rect = anchor.getBoundingClientRect();
235
+ if (!lastAnchorRect || rect.left !== lastAnchorRect.left || rect.top !== lastAnchorRect.top
236
+ || rect.width !== lastAnchorRect.width || rect.height !== lastAnchorRect.height) schedule();
237
+ anchorFrame = owner.requestAnimationFrame(trackAnchor);
238
+ };
239
+ const stopTracking = () => {
240
+ if (anchorFrame !== null) owner.cancelAnimationFrame(anchorFrame);
241
+ anchorFrame = null;
242
+ lastAnchorRect = null;
243
+ };
226
244
  const syncListeners = () => {
245
+ if (open && anchor && !point) {
246
+ if (anchorFrame === null) anchorFrame = owner.requestAnimationFrame(trackAnchor);
247
+ } else stopTracking();
227
248
  const needed = open && (point || anchor);
228
249
  if (!needed) {
229
250
  abort?.abort();
@@ -251,7 +272,7 @@ export function createAnchoredSurface(surface, options = {}) {
251
272
  showAtPoint(next) { point = next; open = true; syncListeners(); show(surface); position(); schedule(); observeSize(); },
252
273
  hide() { open = false; point = null; syncListeners(); stopSize(); if (frame !== null) owner.cancelAnimationFrame(frame); frame = null; hide(surface); },
253
274
  refresh: schedule,
254
- destroy() { open = false; abort?.abort(); abort = null; stopSize(); if (frame !== null) owner.cancelAnimationFrame(frame); frame = null; hide(surface); anchor = null; },
275
+ destroy() { open = false; stopTracking(); abort?.abort(); abort = null; stopSize(); if (frame !== null) owner.cancelAnimationFrame(frame); frame = null; hide(surface); anchor = null; },
255
276
  };
256
277
  }
257
278
 
@@ -1,9 +1,11 @@
1
1
  <link rel="component" href="../ui-spinner/ui-spinner.html">
2
+ <link rel="component" href="../ui-icon/ui-icon.html">
2
3
  <template component="ui-button" controller="./ui-button.js">
3
4
  <defs>
4
- <prop name="variant" type="keyword" values="outline, solid, danger, ghost, link" default="outline"
5
+ <prop name="variant" type="keyword" values="outline, solid, danger, ghost, link, card" default="outline"
5
6
  >Emphasis: how loudly the button asks. solid fills with its tone, outline draws its tone at the edge over a wash of it, ghost carries only text until it is
6
- hovered, and link is an inline text action that reads as a link. danger is solid in the danger tone, kept for the call sites that name it.</prop>
7
+ hovered, and link is an inline text action that reads as a link. danger is solid in the danger tone, kept for the call sites that name it. card uses the
8
+ same tone on a full-width action with an icon slot, wrapping content, and a centered action slot.</prop>
7
9
  <prop name="size" type="keyword" values="xs, sm, md, lg" default="md"
8
10
  >Height, padding, and type size. xs is 1.5rem with the smallest text, for a control inside a line of small text; sm is the small control height (2rem by
9
11
  default) with tighter padding and smaller text, md is the standard control height (2.5rem), and lg is taller (3rem) with roomier padding. Under touch, sm
@@ -37,6 +39,34 @@
37
39
  table keyed by a boolean's text picks each value, and null leaves the attribute off. -->
38
40
  </defs>
39
41
  <template $match>
42
+ <a
43
+ $when="variant = 'card' and as = 'a'"
44
+ from:href="{ true: null, false: href }[concat(disabled or loading or pending)]"
45
+ from:target="target"
46
+ from:rel="rel"
47
+ from:aria-disabled="{ true: 'true', false: null }[concat(disabled or loading or pending)]"
48
+ from:aria-busy="loading or pending"
49
+ from:tabindex="{ true: '0', false: null }[concat((loading or pending) and not disabled)]"
50
+ >
51
+ <span class="card-icon"><ui-spinner $if="loading or pending" size="sm"></ui-spinner><template $if="not (loading or pending)"><slot
52
+ name="icon"
53
+ ></slot></template></span>
54
+ <span class="card-content"><slot></slot></span>
55
+ <span class="card-action"><slot name="action"><ui-icon name="chevron-right" aria-hidden="true"></ui-icon></slot></span>
56
+ </a>
57
+ <button
58
+ $when="variant = 'card'"
59
+ from:type="type"
60
+ from:disabled="disabled"
61
+ from:aria-disabled="{ true: 'true', false: null }[concat(loading or pending)]"
62
+ from:aria-busy="loading or pending"
63
+ >
64
+ <span class="card-icon"><ui-spinner $if="loading or pending" size="sm"></ui-spinner><template $if="not (loading or pending)"><slot
65
+ name="icon"
66
+ ></slot></template></span>
67
+ <span class="card-content"><slot></slot></span>
68
+ <span class="card-action"><slot name="action"><ui-icon name="chevron-right" aria-hidden="true"></ui-icon></slot></span>
69
+ </button>
40
70
  <a
41
71
  $when="as = 'a'"
42
72
  from:href="{ true: null, false: href }[concat(disabled or loading or pending)]"
@@ -233,6 +263,42 @@
233
263
  font-size: var(--ui-font-size-md);
234
264
  }
235
265
 
266
+ /* Card actions inherit Button's palette and behavior; only their shared layout changes.
267
+ The icon may contain any decorative composition. Empty icons remove their column. */
268
+ :host-state([variant="card"]) {
269
+ display: grid;
270
+ grid-template-columns: auto minmax(0, 1fr) auto;
271
+ align-items: start;
272
+ gap: var(--ui-space-3);
273
+ inline-size: 100%;
274
+ min-block-size: max(var(--ui-control-min-block-size), var(--ui-control-size-md));
275
+ text-align: start;
276
+ white-space: normal;
277
+ box-shadow: none;
278
+ background-image: none;
279
+ }
280
+ :host-state([variant="card"][size="lg"]) {
281
+ min-block-size: max(var(--ui-control-min-block-size), var(--ui-control-size-lg));
282
+ }
283
+ :host-state([variant="card"]):has(.card-icon:empty) {
284
+ grid-template-columns: minmax(0, 1fr) auto;
285
+ }
286
+ .card-icon {
287
+ display: flex;
288
+ align-items: flex-start;
289
+ }
290
+ .card-icon:empty {
291
+ display: none;
292
+ }
293
+ .card-content {
294
+ min-inline-size: 0;
295
+ }
296
+ .card-action {
297
+ display: flex;
298
+ align-items: center;
299
+ align-self: center;
300
+ }
301
+
236
302
  /* A link is never :enabled, so interactive states test "not unavailable" instead: for a button that
237
303
  is exactly :enabled, at the same specificity, and it also matches a link that can be followed. */
238
304
  :host:hover:not(:disabled, [aria-disabled="true"]) {
@@ -1,5 +1,7 @@
1
1
  <template component="ui-separator">
2
2
  <defs>
3
+ <prop name="emphasis" type="keyword" values="normal, strong" default="normal"
4
+ >Normal uses the decorative divider color; strong uses the stronger enclosing-edge color for clearer separation.</prop>
3
5
  <prop name="orientation" type="keyword" values="horizontal, vertical" default="horizontal"
4
6
  >Whether the separator divides stacked content (horizontal) or inline content (vertical).</prop>
5
7
  <prop name="selectable" type="boolean" default="false">Gives a document divider a pointer-sized hit area. The editor owns selection and deletion.</prop>
@@ -19,13 +21,17 @@
19
21
  align-self: stretch;
20
22
  margin: 0;
21
23
  border: 0;
22
- border-block-start: var(--ui-border-width) solid var(--ui-separator-color, var(--ui-border-divider));
24
+ border-block-start: var(--ui-border-width) solid var(--ui-separator-color, var(--_separator-color, var(--ui-border-divider)));
25
+ }
26
+
27
+ :host-state([emphasis="strong"]) {
28
+ --_separator-color: var(--ui-border-strong);
23
29
  }
24
30
 
25
31
  :host[aria-orientation="vertical"] {
26
32
  block-size: auto;
27
33
  border-block-start: 0;
28
- border-inline-start: var(--ui-border-width) solid var(--ui-separator-color, var(--ui-border-divider));
34
+ border-inline-start: var(--ui-border-width) solid var(--ui-separator-color, var(--_separator-color, var(--ui-border-divider)));
29
35
  }
30
36
 
31
37
  :host-state([selectable]) {