@threadlabs/looma 0.26.4 → 0.26.6

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.
@@ -13,9 +13,10 @@
13
13
  aria-hidden when the surrounding control already names that state. square is a fixed rounded square for an icon or single glyph beside a heading; hide a
14
14
  decorative icon when adjacent words name it. circle frames a visible glyph, icon or short bounded count in a fixed circle, centered in both
15
15
  directions.</prop>
16
- <prop name="size" type="keyword" values="xs, md, lg" default="md"
17
- >Scale. xs is compact; md is the usual text badge; lg adds room to fixed marks while retaining the usual text size. A square is 24px at xs, 32px at md and
18
- 40px at lg. A circle is 16px at xs, 24px at md and 32px at lg; use lg for short counts such as 30 or 99+.</prop>
16
+ <prop name="size" type="keyword" values="xs, sm, md, lg" default="md"
17
+ >Scale. xs is compact; sm keeps the usual badge geometry with compact text, fitting short counts such as 99+ inside a 24px circle. md is the usual text
18
+ badge; lg adds room to fixed marks while retaining the usual text size. A square is 24px at xs, 32px at sm and md, and 40px at lg. A circle is 16px at xs,
19
+ 24px at sm and md, and 32px at lg.</prop>
19
20
  </defs>
20
21
  <span>
21
22
  <span class="icon" aria-hidden="true"><slot name="icon"></slot></span>
@@ -35,6 +36,10 @@
35
36
  syntax: "*";
36
37
  inherits: false;
37
38
  }
39
+ @property --ui-badge-circle-opacity {
40
+ syntax: "*";
41
+ inherits: false;
42
+ }
38
43
  @property --ui-badge-dot-size {
39
44
  syntax: "*";
40
45
  inherits: false;
@@ -191,6 +196,11 @@
191
196
  line-height: var(--ui-badge-line-height, 1);
192
197
  }
193
198
 
199
+ :host([size="sm"]) {
200
+ font-size: var(--ui-badge-font-size, var(--ui-font-size-2xs));
201
+ line-height: var(--ui-badge-line-height, 1);
202
+ }
203
+
194
204
  :host([tone="accent"]) {
195
205
  --_badge-surface: var(--ui-accent-strong-surface, var(--ui-accent-solid));
196
206
  --_badge-border: var(--ui-accent-strong-surface, var(--ui-accent-solid));
@@ -272,8 +282,35 @@
272
282
  background-clip: padding-box;
273
283
  }
274
284
 
285
+ /* A circular count has a tone-tinted surface, a separate lighter rim and readable tone ink.
286
+ Dark controls retain a shaded surface and theme ink with a quieter rim. */
275
287
  :host([variant="inverse"][shape="circle"]) {
276
- box-shadow: var(--ui-shadow-sm);
288
+ --_badge-color-surface: light-dark(
289
+ color-mix(in srgb, color-mix(in oklab, var(--_ui-badge-color) 0%, var(--ui-surface-default)) var(--ui-badge-circle-opacity, 66%), transparent),
290
+ color-mix(in srgb, color-mix(in oklab, var(--_ui-badge-color) 0%, var(--ui-surface-default)) var(--ui-badge-circle-opacity, 56%), transparent)
291
+ );
292
+ --_badge-color-border: light-dark(
293
+ color-mix(in srgb, color-mix(in oklab, var(--_ui-badge-color) 0%, var(--ui-surface-default)) 88%, transparent),
294
+ color-mix(in srgb, color-mix(in oklab, var(--_ui-badge-color) 0%, var(--ui-surface-default)) 44%, transparent)
295
+ );
296
+ --_badge-color-text: light-dark(
297
+ color-mix(in srgb, var(--_badge-color-ink) 65%, var(--ui-text-primary)),
298
+ color-mix(in oklab, var(--_ui-badge-color) 0%, var(--ui-text-primary))
299
+ );
300
+ --_badge-count-surface: light-dark(
301
+ color-mix(in srgb, var(--_badge-text) var(--ui-badge-circle-opacity, 66%), transparent),
302
+ color-mix(in srgb, var(--_badge-text) var(--ui-badge-circle-opacity, 56%), transparent)
303
+ );
304
+ --_badge-outline-surface: var(--ui-badge-surface, var(--_badge-color-surface, var(--_badge-count-surface)));
305
+ border-color: var(
306
+ --ui-badge-border,
307
+ var(--_badge-color-border, light-dark(color-mix(in srgb, var(--_badge-text) 88%, transparent), color-mix(in srgb, var(--_badge-text) 44%, transparent)))
308
+ );
309
+ color: var(
310
+ --ui-badge-text,
311
+ var(--_badge-color-text, light-dark(color-mix(in srgb, var(--_badge-surface) 65%, var(--ui-text-primary)), var(--ui-text-primary)))
312
+ );
313
+ box-shadow: 0 1px 1px light-dark(rgb(0 0 0 / 0.08), rgb(0 0 0 / 0.12));
277
314
  }
278
315
 
279
316
  /* Neutral reverses a subtle surface/ink pair rather than a strong tone. Keep its original
@@ -78,6 +78,7 @@
78
78
  >
79
79
  <span $if="$loading or $pending" class="spinner-wrap"><ui-spinner size="sm"></ui-spinner></span>
80
80
  <slot></slot>
81
+ <span class="badge"><slot name="badge"></slot></span>
81
82
  </a>
82
83
  <button
83
84
  $else
@@ -88,6 +89,7 @@
88
89
  >
89
90
  <span $if="$loading or $pending" class="spinner-wrap"><ui-spinner size="sm"></ui-spinner></span>
90
91
  <slot></slot>
92
+ <span class="badge"><slot name="badge"></slot></span>
91
93
  </button>
92
94
  </template>
93
95
  <style>
@@ -166,6 +168,16 @@
166
168
  }
167
169
 
168
170
  :host {
171
+ /* Optional inherited action-group values keep text and icon actions under the same light.
172
+ One fine top/start highlight follows the bottom/end lowlight at a joined action seam.
173
+ A single ambient lift avoids stacking a face gradient or another inset bevel. */
174
+ --_ui-action-start: 1px;
175
+ --_ui-action-edge: var(
176
+ --ui-action-edge,
177
+ inset var(--_ui-action-start) 1px 0 light-dark(rgb(255 255 255 / 0.08), rgb(255 255 255 / 0.06)),
178
+ inset calc(-1 * var(--_ui-action-start)) -1px 0 light-dark(rgb(0 0 0 / 0.08), rgb(0 0 0 / 0.08))
179
+ );
180
+ --_ui-action-shadow: var(--_ui-action-edge), 0 1px 2px light-dark(rgb(0 0 0 / 0.10), rgb(0 0 0 / 0.20));
169
181
  --_hue: var(--ui-accent);
170
182
  --_hue-hover: var(--ui-accent-hover);
171
183
  --_hue-active: var(--ui-accent-active);
@@ -186,6 +198,10 @@
186
198
  color: var(--_tone-text);
187
199
  }
188
200
 
201
+ :host:dir(rtl) {
202
+ --_ui-action-start: -1px;
203
+ }
204
+
189
205
  :host([align="start"]) {
190
206
  justify-content: flex-start;
191
207
  text-align: start;
@@ -212,8 +228,8 @@
212
228
  white-space: var(--ui-button-white-space, inherit);
213
229
  line-height: var(--ui-line-height-tight);
214
230
  padding: var(--ui-space-2) var(--ui-space-3);
215
- background-image: var(--ui-raised-highlight);
216
- box-shadow: var(--ui-button-shadow, var(--ui-raised));
231
+ background-image: none;
232
+ box-shadow: var(--ui-button-shadow, var(--ui-action-shadow, var(--_ui-action-shadow)));
217
233
  cursor: pointer;
218
234
  transition:
219
235
  background-color var(--ui-motion-fast) var(--ui-motion-ease),
@@ -234,6 +250,22 @@
234
250
  vertical-align: middle;
235
251
  }
236
252
 
253
+ /* A count is an adornment, not another line of label text. Keep the standard control height
254
+ with enough inset for a 24px mark; large content can still grow the button naturally. */
255
+ :host:not([variant="link"]):has(.badge:not(:empty)) {
256
+ padding-block: var(--ui-space-0-5);
257
+ }
258
+
259
+ .badge {
260
+ display: inline-flex;
261
+ align-items: center;
262
+ flex: none;
263
+ }
264
+
265
+ .badge:empty {
266
+ display: none;
267
+ }
268
+
237
269
  @supports (text-box-trim: trim-both) {
238
270
  :host {
239
271
  text-box-trim: trim-both;
@@ -307,6 +339,15 @@
307
339
  background-color: var(--ui-button-hover-surface, oklch(from var(--_tone) l c h / 12%));
308
340
  }
309
341
 
342
+ /* A neutral card encloses content quietly; ordinary outlines keep their action-tone edge. */
343
+ :host([variant="card"][tone="neutral"]) {
344
+ border-color: var(--ui-button-border, var(--ui-border));
345
+ }
346
+
347
+ :host([variant="card"][tone="neutral"]):hover:not(:disabled, [aria-disabled="true"]) {
348
+ border-color: var(--ui-button-hover-border, var(--ui-border-strong));
349
+ }
350
+
310
351
  /* Pressed reads as pressed in: the highlight goes, and the shadow moves inside. */
311
352
  :host:active:not(:disabled, [aria-disabled="true"]) {
312
353
  background-color: var(--ui-button-active-surface, oklch(from var(--_tone) l c h / 18%));
@@ -318,7 +359,7 @@
318
359
  border-color: var(--_tone);
319
360
  background-color: var(--_tone);
320
361
  color: var(--_on-tone);
321
- box-shadow: var(--ui-button-solid-shadow, var(--ui-raised));
362
+ box-shadow: var(--ui-button-solid-shadow, var(--ui-action-shadow, var(--_ui-action-shadow)));
322
363
  }
323
364
 
324
365
  :is(:host([variant="solid"]), :host([variant="danger"])):hover:not(:disabled, [aria-disabled="true"]) {
@@ -400,16 +441,20 @@
400
441
  :host:focus-visible {
401
442
  outline: none;
402
443
  box-shadow:
403
- var(--ui-button-shadow, var(--ui-raised)),
444
+ var(--ui-button-shadow, var(--ui-action-shadow, var(--_ui-action-shadow))),
404
445
  var(--ui-focus-halo);
405
446
  }
406
447
 
407
448
  :host([variant="solid"]):focus-visible {
408
449
  box-shadow:
409
- var(--ui-button-solid-shadow, var(--ui-raised)),
450
+ var(--ui-button-solid-shadow, var(--ui-action-shadow, var(--_ui-action-shadow))),
410
451
  var(--ui-focus-halo);
411
452
  }
412
453
 
454
+ :is(:host([variant="ghost"]), :host([variant="link"]), :host([variant="card"])):focus-visible {
455
+ box-shadow: var(--ui-focus-halo);
456
+ }
457
+
413
458
  /* A loading action stays visually active; only an explicitly disabled action takes the
414
459
  neutral disabled palette. Its flat surface and muted ink make the state unambiguous. */
415
460
  :host:is(:disabled, [aria-disabled="true"]:not([aria-busy="true"])) {
@@ -116,6 +116,16 @@
116
116
  }
117
117
 
118
118
  :host {
119
+ /* Optional inherited action-group values keep text and icon actions under the same light.
120
+ One fine top/start highlight follows the bottom/end lowlight at a joined action seam.
121
+ A single ambient lift avoids stacking a face gradient or another inset bevel. */
122
+ --_ui-action-start: 1px;
123
+ --_ui-action-edge: var(
124
+ --ui-action-edge,
125
+ inset var(--_ui-action-start) 1px 0 light-dark(rgb(255 255 255 / 0.08), rgb(255 255 255 / 0.06)),
126
+ inset calc(-1 * var(--_ui-action-start)) -1px 0 light-dark(rgb(0 0 0 / 0.08), rgb(0 0 0 / 0.08))
127
+ );
128
+ --_ui-action-shadow: var(--_ui-action-edge), 0 1px 2px light-dark(rgb(0 0 0 / 0.10), rgb(0 0 0 / 0.20));
119
129
  /* Hooks do not inherit, so the parts below read them from the root. */
120
130
  --_ui-icon-button-icon-size: var(--ui-icon-button-icon-size);
121
131
  --_ui-icon-button-icon-stroke-width: var(--ui-icon-button-icon-stroke-width);
@@ -124,6 +134,10 @@
124
134
  color: var(--ui-text-secondary);
125
135
  }
126
136
 
137
+ :host:dir(rtl) {
138
+ --_ui-action-start: -1px;
139
+ }
140
+
127
141
  :host([tone="accent"]) {
128
142
  --_icon-button-border: var(--ui-accent-solid);
129
143
  --_icon-button-hover-border: var(--ui-accent-hover);
@@ -297,19 +311,16 @@
297
311
  :host([variant="outline"]) {
298
312
  border-color: var(--ui-icon-button-border, var(--_icon-button-border, var(--ui-control-border, var(--ui-border-strong))));
299
313
  background-color: var(--ui-icon-button-outline-surface, var(--_icon-button-outline-surface, var(--ui-surface-default)));
300
- background-image: var(--ui-raised-highlight);
314
+ background-image: none;
301
315
  color: var(--ui-icon-button-outline-text, var(--_icon-button-text, var(--ui-text-primary)));
302
- box-shadow: var(--ui-raised);
316
+ box-shadow: var(--ui-action-shadow, var(--_ui-action-shadow));
303
317
  }
304
318
 
305
319
  :host([variant="solid"]) {
306
320
  background-color: var(--ui-icon-button-solid-surface, var(--_icon-button-solid-surface, var(--ui-accent-solid)));
307
- background-image: var(--ui-raised-highlight);
321
+ background-image: none;
308
322
  color: var(--ui-icon-button-solid-text, var(--_icon-button-solid-text, var(--ui-text-on-accent)));
309
- box-shadow: var(
310
- --ui-icon-button-solid-shadow,
311
- 0 1px 2px color-mix(in srgb, var(--ui-accent-solid) 18%, transparent)
312
- );
323
+ box-shadow: var(--ui-icon-button-solid-shadow, var(--ui-action-shadow, var(--_ui-action-shadow)));
313
324
  }
314
325
 
315
326
  :host:hover:enabled:not([aria-disabled="true"]) {
@@ -373,6 +384,23 @@
373
384
  box-shadow: var(--ui-focus-halo);
374
385
  }
375
386
 
387
+ :host([variant="outline"]):focus-visible {
388
+ box-shadow:
389
+ var(--ui-action-shadow, var(--_ui-action-shadow)),
390
+ var(--ui-focus-halo);
391
+ }
392
+
393
+ :host([variant="solid"]):focus-visible {
394
+ box-shadow:
395
+ var(--ui-icon-button-solid-shadow, var(--ui-action-shadow, var(--_ui-action-shadow))),
396
+ var(--ui-focus-halo);
397
+ }
398
+
399
+ :host:is(:host([variant="outline"]), :host([variant="solid"])):active:enabled:not([aria-disabled="true"]) {
400
+ background-image: none;
401
+ box-shadow: var(--ui-pressed);
402
+ }
403
+
376
404
  :host([variant="outline"])[aria-pressed="true"]:enabled:focus-visible {
377
405
  box-shadow:
378
406
  var(--ui-pressed),
@@ -1,5 +1,8 @@
1
1
  <template component="ui-input-group" controller="./ui-input-group.js">
2
2
  <defs>
3
+ <prop name="variant" type="keyword" values="outline, subtle" default="outline"
4
+ >outline uses the standard field edge and surface. subtle uses a quiet decorative edge and muted surface for a multiline composer. Both retain the same
5
+ focus, invalid and disabled states, and the field inside shares the group's one frame.</prop>
3
6
  <prop name="multiline" type="boolean" default="false">Groups a Textarea with an action below its text inside the shared frame.</prop>
4
7
  <prop name="actionPosition" type="keyword" values="below, top-end" default="below"
5
8
  >In a multiline group, below keeps the action under the text. top-end places it inside the textarea at its upper end and reserves text padding for its
@@ -79,6 +82,12 @@
79
82
  transition: border-color var(--ui-motion-fast) var(--ui-motion-ease), box-shadow var(--ui-motion-fast) var(--ui-motion-ease);
80
83
  }
81
84
 
85
+ :host([variant="subtle"]) {
86
+ border-color: var(--ui-border);
87
+ background: var(--ui-surface-muted);
88
+ box-shadow: none;
89
+ }
90
+
82
91
  /* The frame takes its input's size, so a small group lines up with small buttons and fields. */
83
92
  :host:has(.field [data-ui-input-state~="size=sm"]) {
84
93
  min-block-size: var(--ui-control-size-sm);