@threadlabs/looma 0.26.0 → 0.26.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadlabs/looma",
3
- "version": "0.26.0",
3
+ "version": "0.26.2",
4
4
  "type": "module",
5
5
  "description": "Looma components as HTML, Vue, and plain DOM, with design tokens.",
6
6
  "license": "MIT",
@@ -7,6 +7,10 @@
7
7
  syntax: "*";
8
8
  inherits: false;
9
9
  }
10
+ @property --ui-badge-circle-size {
11
+ syntax: "*";
12
+ inherits: false;
13
+ }
10
14
  @property --ui-badge-dot-size {
11
15
  syntax: "*";
12
16
  inherits: false;
@@ -282,6 +286,36 @@
282
286
  display: none;
283
287
  }
284
288
 
289
+ /* A visible glyph needs fixed geometry, unlike a content-sized pill or a hidden-label dot. */
290
+ :scope[data-ui-badge-state~="shape=circle"] {
291
+ --_badge-circle-size: var(--ui-badge-circle-size, calc(var(--ui-space-3) * 2));
292
+ display: inline-flex;
293
+ flex: none;
294
+ inline-size: var(--_badge-circle-size);
295
+ block-size: var(--_badge-circle-size);
296
+ min-inline-size: var(--_badge-circle-size);
297
+ min-block-size: var(--_badge-circle-size);
298
+ padding: 0;
299
+ border-radius: var(--ui-radius-round);
300
+ text-box-trim: none;
301
+ }
302
+
303
+ :scope[data-ui-badge-state~="shape=circle"][data-ui-badge-state~="size=xs"] {
304
+ --_badge-circle-size: var(--ui-badge-circle-size, var(--ui-space-4));
305
+ }
306
+
307
+ :scope[data-ui-badge-state~="shape=circle"] .label {
308
+ display: inline-flex;
309
+ align-items: center;
310
+ justify-content: center;
311
+ line-height: 1;
312
+ text-box-trim: none;
313
+ }
314
+
315
+ :scope[data-ui-badge-state~="shape=circle"] .label:empty {
316
+ display: none;
317
+ }
318
+
285
319
  /* A dot uses the badge's tone recipe; only its silhouette and visible label change. */
286
320
  :scope[data-ui-badge-state~="shape=dot"] {
287
321
  position: relative;
@@ -399,6 +433,12 @@
399
433
  margin-inline-end: 0;
400
434
  }
401
435
 
436
+ /* A visible glyph needs fixed geometry, unlike a content-sized pill or a hidden-label dot. */
437
+
438
+ :scope[data-ui-badge-state~="shape=circle"] .label :where([data-slotted], [data-slotted] *):is(svg) {
439
+ margin-inline-end: 0;
440
+ }
441
+
402
442
  /* A dot uses the badge's tone recipe; only its silhouette and visible label change. */
403
443
 
404
444
  /* The clipped point needs an inset surface over an outer border-colour shape: a native border
@@ -23,6 +23,14 @@
23
23
  :scope[data-ui-container-state~="measure=narrow"] {
24
24
  --_container-measure: 45ch;
25
25
  }
26
+ :scope[data-ui-container-state~="fill"] {
27
+ box-sizing: border-box;
28
+ display: flex;
29
+ flex-direction: column;
30
+ flex: 1;
31
+ min-inline-size: 0;
32
+ min-block-size: 0;
33
+ }
26
34
  :scope[data-ui-container-state~="measure=wide"] {
27
35
  --_container-measure: 80ch;
28
36
  }
@@ -104,7 +104,7 @@
104
104
  display: none;
105
105
  }
106
106
 
107
- /* Header and actions stay pinned; only the body scrolls when content is long. */
107
+ /* Own the layout wrapper; nested component roots are outside these scoped rules. */
108
108
  header {
109
109
  display: flex;
110
110
  align-items: center;
@@ -183,9 +183,11 @@
183
183
  }
184
184
 
185
185
  .body {
186
+ display: flex;
187
+ flex-direction: column;
186
188
  flex: 1 1 auto;
187
189
  min-block-size: 0;
188
- padding: var(--ui-space-5);
190
+ padding: var(--ui-space-3) var(--ui-space-5) var(--ui-space-5);
189
191
  }
190
192
 
191
193
  footer {
@@ -194,7 +196,7 @@
194
196
  flex-wrap: wrap;
195
197
  justify-content: flex-end;
196
198
  gap: var(--ui-space-2);
197
- padding: var(--ui-space-3) var(--ui-space-5);
199
+ padding: 0 var(--ui-space-5) var(--ui-space-5);
198
200
  }
199
201
 
200
202
  footer:empty {
@@ -223,7 +225,7 @@
223
225
  @scope ([data-component~="ui-dialog"]) to ([data-component]) {
224
226
  /* This component's hooks style the element they are set on, and never a nested one. */
225
227
 
226
- /* Header and actions stay pinned; only the body scrolls when content is long. */
228
+ /* Own the layout wrapper; nested component roots are outside these scoped rules. */
227
229
 
228
230
  /* :host-qualified so page heading rules that reach in cannot outrank the dialog chrome. */
229
231
 
@@ -87,6 +87,21 @@
87
87
  color: var(--ui-text-secondary);
88
88
  }
89
89
 
90
+ :scope[data-ui-icon-button-state~="tone=accent"] {
91
+ --_icon-button-border: var(--ui-accent-solid);
92
+ --_icon-button-hover-border: var(--ui-accent-hover);
93
+ --_icon-button-text: var(--ui-accent-subtle-text, var(--ui-accent-solid));
94
+ --_icon-button-outline-surface: color-mix(in srgb, var(--ui-accent-solid) 5%, var(--ui-surface-default));
95
+ color: var(--_icon-button-text);
96
+ }
97
+
98
+ /* Omitted tone retains the existing accent solid; explicit neutral is additive. */
99
+ :scope[data-ui-icon-button-state~="tone=neutral"] {
100
+ --_icon-button-solid-surface: var(--ui-text-primary);
101
+ --_icon-button-solid-hover-surface: var(--ui-text-primary);
102
+ --_icon-button-solid-text: var(--ui-surface-default);
103
+ }
104
+
90
105
  :scope[data-ui-icon-button-state~="anticipatory"] {
91
106
  position: relative;
92
107
  }
@@ -119,13 +134,13 @@
119
134
  }
120
135
 
121
136
  @media (hover: hover) and (pointer: fine) {
122
- :scope[data-ui-icon-button-state~="anticipatory"]:not(:scope[data-ui-icon-button-state~="variant=solid"], :scope[data-ui-icon-button-state~="loading"], :scope[data-ui-icon-button-state~="pending"], :disabled, [data-ui-proximity="near"], :hover, :focus-visible) {
137
+ :scope[data-ui-icon-button-state~="anticipatory"]:not(:scope[data-ui-icon-button-state~="variant=solid"], :scope[data-ui-icon-button-state~="loading"], :scope[data-ui-icon-button-state~="pending"], :disabled, [aria-pressed="true"], [data-ui-proximity="near"], :hover, :focus-visible) {
123
138
  border-color: transparent;
124
139
  background-color: transparent;
125
140
  box-shadow: none;
126
141
  }
127
142
 
128
- :scope[data-ui-icon-button-state~="anticipatory"]:not(:scope[data-ui-icon-button-state~="variant=solid"], :scope[data-ui-icon-button-state~="loading"], :scope[data-ui-icon-button-state~="pending"], :disabled, [data-ui-proximity="near"], :hover, :focus-visible)
143
+ :scope[data-ui-icon-button-state~="anticipatory"]:not(:scope[data-ui-icon-button-state~="variant=solid"], :scope[data-ui-icon-button-state~="loading"], :scope[data-ui-icon-button-state~="pending"], :disabled, [aria-pressed="true"], [data-ui-proximity="near"], :hover, :focus-visible)
129
144
  .content {
130
145
  opacity: 0;
131
146
  transform: scale(0.65);
@@ -133,7 +148,7 @@
133
148
  }
134
149
  }
135
150
 
136
- :scope[data-ui-icon-button-state~="anticipatory"]:is(:scope[data-ui-icon-button-state~="variant=solid"], [data-ui-proximity="near"], :hover, :focus-visible)::before {
151
+ :scope[data-ui-icon-button-state~="anticipatory"]:is(:scope[data-ui-icon-button-state~="variant=solid"], [aria-pressed="true"], [data-ui-proximity="near"], :hover, :focus-visible)::before {
137
152
  opacity: 0;
138
153
  }
139
154
 
@@ -243,17 +258,17 @@
243
258
  }
244
259
 
245
260
  :scope[data-ui-icon-button-state~="variant=outline"] {
246
- border-color: var(--ui-icon-button-border, var(--ui-control-border, var(--ui-border-strong)));
247
- background-color: var(--ui-icon-button-outline-surface, var(--ui-surface-default));
261
+ border-color: var(--ui-icon-button-border, var(--_icon-button-border, var(--ui-control-border, var(--ui-border-strong))));
262
+ background-color: var(--ui-icon-button-outline-surface, var(--_icon-button-outline-surface, var(--ui-surface-default)));
248
263
  background-image: var(--ui-raised-highlight);
249
- color: var(--ui-icon-button-outline-text, var(--ui-text-primary));
264
+ color: var(--ui-icon-button-outline-text, var(--_icon-button-text, var(--ui-text-primary)));
250
265
  box-shadow: var(--ui-raised);
251
266
  }
252
267
 
253
268
  :scope[data-ui-icon-button-state~="variant=solid"] {
254
- background-color: var(--ui-icon-button-solid-surface, var(--ui-accent-solid));
269
+ background-color: var(--ui-icon-button-solid-surface, var(--_icon-button-solid-surface, var(--ui-accent-solid)));
255
270
  background-image: var(--ui-raised-highlight);
256
- color: var(--ui-icon-button-solid-text, var(--ui-text-on-accent));
271
+ color: var(--ui-icon-button-solid-text, var(--_icon-button-solid-text, var(--ui-text-on-accent)));
257
272
  box-shadow: var(
258
273
  --ui-icon-button-solid-shadow,
259
274
  0 1px 2px color-mix(in srgb, var(--ui-accent-solid) 18%, transparent)
@@ -263,20 +278,32 @@
263
278
  :scope:hover:enabled:not([aria-disabled="true"]) {
264
279
  /* Derived from the text colour so the ghost hover shows on any background. */
265
280
  background-color: var(--ui-icon-button-hover-surface, color-mix(in srgb, currentColor 8%, transparent));
266
- color: var(--ui-icon-button-hover-text, var(--ui-text-primary));
281
+ color: var(--ui-icon-button-hover-text, var(--_icon-button-text, var(--ui-text-primary)));
267
282
  }
268
283
 
269
284
  :scope[data-ui-icon-button-state~="variant=outline"]:hover:enabled:not([aria-disabled="true"]) {
270
- border-color: var(--ui-icon-button-hover-border, var(--ui-control-border-hover, var(--ui-control-border)));
285
+ border-color: var(--ui-icon-button-hover-border, var(--_icon-button-hover-border, var(--ui-control-border-hover, var(--ui-control-border))));
271
286
  }
272
287
 
273
288
  :scope[data-ui-icon-button-state~="variant=solid"]:hover:enabled:not([aria-disabled="true"]) {
274
- background-color: var(--ui-icon-button-solid-hover-surface, var(--ui-accent-hover));
289
+ background-color: var(--ui-icon-button-solid-hover-surface, var(--_icon-button-solid-hover-surface, var(--ui-accent-hover)));
290
+ }
291
+
292
+ :scope[data-ui-icon-button-state~="variant=solid"][data-ui-icon-button-state~="tone=accent"]:hover:enabled:not([aria-disabled="true"]),
293
+ :scope[data-ui-icon-button-state~="variant=solid"][data-ui-icon-button-state~="tone=neutral"]:hover:enabled:not([aria-disabled="true"]) {
294
+ color: var(--ui-icon-button-solid-text, var(--_icon-button-solid-text, var(--ui-text-on-accent)));
275
295
  }
276
296
 
277
297
  :scope[data-ui-icon-button-state~="variant=ghost"][aria-expanded="true"]:enabled:not([aria-disabled="true"]) {
278
298
  background-color: var(--ui-icon-button-active-surface, color-mix(in srgb, currentColor 16%, transparent));
279
- color: var(--ui-icon-button-hover-text, var(--ui-text-primary));
299
+ color: var(--ui-icon-button-hover-text, var(--_icon-button-text, var(--ui-text-primary)));
300
+ box-shadow: var(--ui-pressed);
301
+ }
302
+
303
+ /* Native toggle state persists after pointer release, including during loading. */
304
+ :scope[data-ui-icon-button-state~="variant=outline"][aria-pressed="true"]:enabled {
305
+ background-color: var(--ui-icon-button-active-surface, color-mix(in srgb, currentColor 12%, var(--ui-surface-default)));
306
+ background-image: none;
280
307
  box-shadow: var(--ui-pressed);
281
308
  }
282
309
 
@@ -309,6 +336,22 @@
309
336
  box-shadow: var(--ui-focus-halo);
310
337
  }
311
338
 
339
+ :scope[data-ui-icon-button-state~="variant=outline"][aria-pressed="true"]:enabled:focus-visible {
340
+ box-shadow:
341
+ var(--ui-pressed),
342
+ var(--ui-focus-halo);
343
+ }
344
+
345
+ @media (forced-colors: active) {
346
+ :scope[data-ui-icon-button-state~="variant=outline"][aria-pressed="true"]:enabled {
347
+ outline: var(--ui-focus-width) solid Highlight;
348
+ outline-offset: calc(-2 * var(--ui-focus-width));
349
+ }
350
+ :scope[data-ui-icon-button-state~="variant=outline"][aria-pressed="true"]:enabled:focus-visible {
351
+ outline-offset: var(--ui-focus-width);
352
+ }
353
+ }
354
+
312
355
  /* Disabled is a flat neutral control, independent of the active variant's tone. */
313
356
  :scope:disabled {
314
357
  border-color: var(--ui-disabled-border);
@@ -349,6 +392,8 @@
349
392
  @scope ([data-component~="ui-icon-button"]) to ([data-component]) {
350
393
  /* This component's hooks style the element they are set on, and never a nested one. */
351
394
 
395
+ /* Omitted tone retains the existing accent solid; explicit neutral is additive. */
396
+
352
397
  /* At rest an anticipatory button shows only its guide dot. The icon grows into place when
353
398
  proximity, hover, or focus reveals it; the button's hit area and guide remain stable. */
354
399
 
@@ -368,9 +413,14 @@
368
413
  @media (pointer: coarse) {
369
414
  }
370
415
 
416
+ /* Native toggle state persists after pointer release, including during loading. */
417
+
371
418
  @media (prefers-reduced-motion: reduce) {
372
419
  }
373
420
 
421
+ @media (forced-colors: active) {
422
+ }
423
+
374
424
  /* Disabled is a flat neutral control, independent of the active variant's tone. */
375
425
 
376
426
  :where([data-slotted], [data-slotted] *):is(svg) {
@@ -31,6 +31,8 @@
31
31
  display: flow-root;
32
32
  }
33
33
 
34
+ /* Trim projected edges without changing spacing inside an item. */
35
+
34
36
  :scope[data-ui-scroll-area-state~="orientation=horizontal"] {
35
37
  overflow-x: auto;
36
38
  overflow-y: hidden;
@@ -61,5 +63,14 @@
61
63
  into whatever is behind the area without needing to know its colour. Before upgrade, its
62
64
  zero-length defaults leave the authored scroll area fully visible. */
63
65
 
66
+ /* Trim projected edges without changing spacing inside an item. */
67
+ :scope[data-ui-scroll-area-state~="trim"] .content > :where([data-slotted], [data-slotted] *):is(:first-child) {
68
+ margin-block-start: 0;
69
+ }
70
+
71
+ :scope[data-ui-scroll-area-state~="trim"] .content > :where([data-slotted], [data-slotted] *):is(:last-child) {
72
+ margin-block-end: 0;
73
+ }
74
+
64
75
  /* A component that sets its display still honours the hidden attribute on its root. */
65
76
  }
@@ -6,7 +6,7 @@ export interface UiBadgeElement extends HTMLSpanElement {
6
6
  removeEventListener<K extends keyof UiBadgeEventMap>(type: K, listener: (this: UiBadgeElement, event: UiBadgeEventMap[K]) => unknown, options?: boolean | EventListenerOptions): void;
7
7
  }
8
8
  export interface UiBadgeProps {
9
- shape?: "pill" | "tag" | "dot" | "square" | null;
9
+ shape?: "pill" | "tag" | "dot" | "square" | "circle" | null;
10
10
  size?: "xs" | "md" | null;
11
11
  tone?: "neutral" | "accent" | "info" | "success" | "warning" | "danger" | null;
12
12
  variant?: "solid" | "subtle" | "outline" | null;
@@ -7,7 +7,7 @@ const T1 = ["",[]];
7
7
  const T2 = ["",[]];
8
8
  let P0, P1, P2;
9
9
  const S = { n: [], t: [], f: import.meta.url, g: "ui-badge" };
10
- const D = { props: { "shape": { "required":false,"values":["pill","tag","dot","square"],"default":"pill", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat),false] }, "size": { "required":false,"values":["xs","md"],"default":"md", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat),false] }, "tone": { "required":false,"values":["neutral","accent","info","success","warning","danger"],"default":"neutral", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat),false] }, "variant": { "required":false,"values":["solid","subtle","outline"],"default":"subtle", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat),false] } } };
10
+ const D = { props: { "shape": { "required":false,"values":["pill","tag","dot","square","circle"],"default":"pill", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat),false] }, "size": { "required":false,"values":["xs","md"],"default":"md", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat),false] }, "tone": { "required":false,"values":["neutral","accent","info","success","warning","danger"],"default":"neutral", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat),false] }, "variant": { "required":false,"values":["solid","subtle","outline"],"default":"subtle", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat),false] } } };
11
11
 
12
12
  export function createUiBadge(options = {}, html) {
13
13
  const { attributes = {}, children = [], slots = {}, ...componentProps } = options;
@@ -6,6 +6,7 @@ export interface UiContainerElement extends HTMLDivElement {
6
6
  removeEventListener<K extends keyof UiContainerEventMap>(type: K, listener: (this: UiContainerElement, event: UiContainerEventMap[K]) => unknown, options?: boolean | EventListenerOptions): void;
7
7
  }
8
8
  export interface UiContainerProps {
9
+ fill?: boolean | null;
9
10
  gutters?: "s" | "m" | "l" | null;
10
11
  measure?: "narrow" | "wide" | null;
11
12
  attributes?: Readonly<Record<string, string | number | boolean | null | undefined>>;
@@ -1,12 +1,12 @@
1
1
  // Generated by HTML Next 1.0.0-alpha.35 for Vanilla DOM. Do not edit.
2
- import { attachGeneratedController, buildTemplate, writeAttribute, dispose, formatOf, keywordFormat, hostState, acceptProps, manageProps, project, fillSlot, checkFormat } from "@nextwebwg/html-next/generated-runtime";
2
+ import { attachGeneratedController, buildTemplate, writeAttribute, dispose, formatOf, keywordFormat, hostState, acceptProps, manageProps, project, fillSlot, checkBoolean, checkFormat } from "@nextwebwg/html-next/generated-runtime";
3
3
  import "../styles/ui-container.css";
4
4
 
5
5
  const T0 = ["",[],4];
6
6
  const T1 = ["",[]];
7
7
  let P0, P1;
8
8
  const S = { n: [], t: [], f: import.meta.url, g: "ui-container" };
9
- const D = { props: { "gutters": { "required":false,"values":["s","m","l"], c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat),false] }, "measure": { "required":false,"values":["narrow","wide"], c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat),false] } } };
9
+ const D = { props: { "fill": { "required":false,"default":false, c: checkBoolean, t: "boolean", j: 0, m: "b" }, "gutters": { "required":false,"values":["s","m","l"], c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat),false] }, "measure": { "required":false,"values":["narrow","wide"], c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat),false] } } };
10
10
 
11
11
  export function createUiContainer(options = {}, html) {
12
12
  const { attributes = {}, children = [], slots = {}, ...componentProps } = options;
@@ -17,7 +17,7 @@ export function createUiContainer(options = {}, html) {
17
17
  }
18
18
  element.setAttribute("data-component", "ui-container");
19
19
  element.append((P0 ??= buildTemplate(T0)).cloneNode(true));
20
- const v = [null, null];
20
+ const v = [null, null, null];
21
21
  const B = acceptProps(element, D, S.n, v, componentProps, [], html);
22
22
  const m1 = (d) => {
23
23
  const n = (P1 ??= buildTemplate(T1)).cloneNode(true);
@@ -28,8 +28,8 @@ export function createUiContainer(options = {}, html) {
28
28
  const p1 = (r, c, d) => {
29
29
  };
30
30
  const p0 = (r, c, d) => {
31
- if (c & 3) {
32
- const x0 = hostState(["measure","gutters"], [v[1], v[0]]);
31
+ if (c & 7) {
32
+ const x0 = hostState(["measure","fill","gutters"], [v[2], v[0], v[1]]);
33
33
  const s0 = x0;
34
34
  if (s0 !== r.v0) writeAttribute(r.n, "data-ui-container-state", (r.v0 = s0) === "" ? null : s0);
35
35
  }
@@ -3,7 +3,7 @@ import { attachGeneratedController, buildTemplate, clearRegion, toAttribute, toT
3
3
  import * as controller from "../components/ui-dialog/ui-dialog.js";
4
4
  import "../styles/ui-dialog.css";
5
5
 
6
- const T0 = ["",[],["header",[],1,["button",["class","close","type","button","aria-label","Close"]]],["ui-scroll-area",["class","body"],4],["footer",[],4]];
6
+ const T0 = ["",[],["header",[],1,["button",["class","close","type","button","aria-label","Close"]]],["div",["class","body"],["ui-scroll-area",[],4]],["footer",[],4]];
7
7
  const T1 = ["h2",["class","title"],0];
8
8
  const T2 = ["",[]];
9
9
  const T3 = ["",[]];
@@ -59,10 +59,15 @@ export function createUiDialog(options = {}, html) {
59
59
  const s0 = toAttribute(x0, "aria-label");
60
60
  if (s0 !== r.v0) writeAttribute(r.n, "aria-label", r.v0 = s0);
61
61
  }
62
+ if (c === -1) {
63
+ const x1 = true;
64
+ const s1 = toAttribute(x1, "trim");
65
+ if (s1 !== r.v1) writeAttribute(r.a3, "trim", r.v1 = s1);
66
+ }
62
67
  if (c & 516) {
63
- const x1 = hostState(["size","alert"], [v[9], v[2]]);
64
- const s1 = x1;
65
- if (s1 !== r.v1) writeAttribute(r.n, "data-ui-dialog-state", (r.v1 = s1) === "" ? null : s1);
68
+ const x2 = hostState(["size","alert"], [v[9], v[2]]);
69
+ const s2 = x2;
70
+ if (s2 !== r.v2) writeAttribute(r.n, "data-ui-dialog-state", (r.v2 = s2) === "" ? null : s2);
66
71
  }
67
72
  if (c & 32) {
68
73
  const t0 = truthy(v[5]);
@@ -74,10 +79,10 @@ export function createUiDialog(options = {}, html) {
74
79
  }
75
80
  } else if (r.b0 !== undefined) p1(r.b0, c, d);
76
81
  } else if (r.b0 !== undefined) p1(r.b0, c, d);
77
- if (r.f1 === undefined) { r.f1 = 1; const at = fillSlot(r.a3, r.e1, "", J, false, undefined, d);
82
+ if (r.f1 === undefined) { r.f1 = 1; const at = fillSlot(r.a4, r.e1, "", J, false, undefined, d);
78
83
  if (Array.isArray(at)) (r.z ??= []).push(() => { at[0].l.delete(at[1]); dispose(at[1]); }); else if (at !== undefined) { r.b1 = m2(d); at.before(r.b1.n); } }
79
84
  else if (r.b1 !== undefined) p2(r.b1, c, d);
80
- if (r.f2 === undefined) { r.f2 = 1; const at = fillSlot(r.a4, r.e2, "actions", J, false, undefined, d);
85
+ if (r.f2 === undefined) { r.f2 = 1; const at = fillSlot(r.a5, r.e2, "actions", J, false, undefined, d);
81
86
  if (Array.isArray(at)) (r.z ??= []).push(() => { at[0].l.delete(at[1]); dispose(at[1]); }); else if (at !== undefined) { r.b2 = m3(d); at.before(r.b2.n); } }
82
87
  else if (r.b2 !== undefined) p3(r.b2, c, d);
83
88
  };
@@ -86,8 +91,9 @@ export function createUiDialog(options = {}, html) {
86
91
  const t2 = t1.nextSibling.nextSibling;
87
92
  const t3 = t0.nextSibling;
88
93
  const t4 = t3.firstChild;
89
- const t5 = t3.nextSibling.firstChild;
90
- const R = { n: element, v0: undefined, v1: undefined, a1: t1, e0: t1.nextSibling, b0: undefined, a3: t4, e1: t4.nextSibling, b1: undefined, f1: undefined, a4: t5, e2: t5.nextSibling, b2: undefined, f2: undefined };
94
+ const t5 = t4.firstChild;
95
+ const t6 = t3.nextSibling.firstChild;
96
+ const R = { n: element, a3: t4, v0: undefined, v1: null, v2: undefined, a1: t1, e0: t1.nextSibling, b0: undefined, a4: t5, e1: t5.nextSibling, b1: undefined, f1: undefined, a5: t6, e2: t6.nextSibling, b2: undefined, f2: undefined };
91
97
  const I = { B };
92
98
  const J = project(I, children, slots);
93
99
  I.R = new Set();
@@ -14,6 +14,7 @@ export interface UiIconButtonProps {
14
14
  pending?: boolean | null;
15
15
  round?: boolean | null;
16
16
  size?: "sm" | "md" | "lg" | null;
17
+ tone?: "accent" | "neutral" | null;
17
18
  variant?: "ghost" | "outline" | "solid" | null;
18
19
  attributes?: Readonly<Record<string, string | number | boolean | null | undefined>>;
19
20
  children?: readonly (string | Node)[];
@@ -8,7 +8,7 @@ const T1 = ["ui-spinner",["size","sm"]];
8
8
  const T2 = ["",[]];
9
9
  let P0, P1, P2;
10
10
  const S = { n: [], t: [], f: import.meta.url, g: "ui-icon-button" };
11
- const D = { props: { "anticipatory": { "required":false,"default":false, c: checkBoolean, t: "boolean", j: 0, m: "b" }, "disabled": { "required":false,"default":false, c: checkBoolean, t: "boolean", j: 0, m: "b" }, "label": { "required":false, c: checkString, t: "string", j: 0, m: "s" }, "loading": { "required":false,"default":false, c: checkBoolean, t: "boolean", j: 0, m: "b" }, "matchButton": { "required":false,"default":false, c: checkBoolean, t: "boolean", j: 0, m: "b" }, "pending": { "required":false,"default":false, c: checkBoolean, t: "boolean", j: 0, m: "b" }, "round": { "required":false,"default":false, c: checkBoolean, t: "boolean", j: 0, m: "b" }, "size": { "required":false,"values":["sm","md","lg"],"default":"md", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat),false] }, "variant": { "required":false,"values":["ghost","outline","solid"],"default":"ghost", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat),false] } } };
11
+ const D = { props: { "anticipatory": { "required":false,"default":false, c: checkBoolean, t: "boolean", j: 0, m: "b" }, "disabled": { "required":false,"default":false, c: checkBoolean, t: "boolean", j: 0, m: "b" }, "label": { "required":false, c: checkString, t: "string", j: 0, m: "s" }, "loading": { "required":false,"default":false, c: checkBoolean, t: "boolean", j: 0, m: "b" }, "matchButton": { "required":false,"default":false, c: checkBoolean, t: "boolean", j: 0, m: "b" }, "pending": { "required":false,"default":false, c: checkBoolean, t: "boolean", j: 0, m: "b" }, "round": { "required":false,"default":false, c: checkBoolean, t: "boolean", j: 0, m: "b" }, "size": { "required":false,"values":["sm","md","lg"],"default":"md", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat),false] }, "tone": { "required":false,"values":["accent","neutral"], c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat),false] }, "variant": { "required":false,"values":["ghost","outline","solid"],"default":"ghost", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat),false] } } };
12
12
  const C = (host) => controller.default(host);
13
13
 
14
14
  export function createUiIconButton(options = {}, html) {
@@ -21,7 +21,7 @@ export function createUiIconButton(options = {}, html) {
21
21
  if (!element.hasAttribute("type")) element.setAttribute("type", "button");
22
22
  element.setAttribute("data-component", "ui-icon-button");
23
23
  element.append((P0 ??= buildTemplate(T0)).cloneNode(true));
24
- const v = [null, null, null, null, null, null, null, null, null];
24
+ const v = [null, null, null, null, null, null, null, null, null, null];
25
25
  const B = acceptProps(element, D, S.n, v, componentProps, ["matchButton"], html);
26
26
  const m1 = (d) => {
27
27
  const n = (P1 ??= buildTemplate(T1)).cloneNode(true);
@@ -67,8 +67,8 @@ export function createUiIconButton(options = {}, html) {
67
67
  const s4 = toAttribute(x4, "aria-busy");
68
68
  if (s4 !== r.v4) writeAttribute(r.n, "aria-busy", r.v4 = s4);
69
69
  }
70
- if (c & 505) {
71
- const x5 = hostState(["anticipatory","variant","loading","pending","size","matchButton","round"], [v[0], v[8], v[3], v[5], v[7], v[4], v[6]]);
70
+ if (c & 1017) {
71
+ const x5 = hostState(["tone","anticipatory","variant","loading","pending","size","matchButton","round"], [v[8], v[0], v[9], v[3], v[5], v[7], v[4], v[6]]);
72
72
  const s5 = x5;
73
73
  if (s5 !== r.v5) writeAttribute(r.n, "data-ui-icon-button-state", (r.v5 = s5) === "" ? null : s5);
74
74
  }
@@ -9,6 +9,7 @@ export interface UiScrollAreaProps {
9
9
  fill?: boolean | null;
10
10
  orientation?: "vertical" | "horizontal" | null;
11
11
  size?: "content" | "sm" | "md" | "lg" | null;
12
+ trim?: boolean | null;
12
13
  attributes?: Readonly<Record<string, string | number | boolean | null | undefined>>;
13
14
  children?: readonly (string | Node)[];
14
15
  slots?: Readonly<Record<string, readonly (string | Node)[]>>;
@@ -7,7 +7,7 @@ const T0 = ["",[],["div",["class","content"],4]];
7
7
  const T1 = ["",[]];
8
8
  let P0, P1;
9
9
  const S = { n: [], t: [], f: import.meta.url, g: "ui-scroll-area" };
10
- const D = { props: { "fill": { "required":false,"default":false, c: checkBoolean, t: "boolean", j: 0, m: "b" }, "orientation": { "required":false,"values":["vertical","horizontal"],"default":"vertical", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat),false] }, "size": { "required":false,"values":["content","sm","md","lg"],"default":"content", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat),false] } } };
10
+ const D = { props: { "fill": { "required":false,"default":false, c: checkBoolean, t: "boolean", j: 0, m: "b" }, "orientation": { "required":false,"values":["vertical","horizontal"],"default":"vertical", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat),false] }, "size": { "required":false,"values":["content","sm","md","lg"],"default":"content", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat),false] }, "trim": { "required":false,"default":false, c: checkBoolean, t: "boolean", j: 0, m: "b" } } };
11
11
  const C = (host) => controller.default(host);
12
12
 
13
13
  export function createUiScrollArea(options = {}, html) {
@@ -19,7 +19,7 @@ export function createUiScrollArea(options = {}, html) {
19
19
  }
20
20
  element.setAttribute("data-component", "ui-scroll-area");
21
21
  element.append((P0 ??= buildTemplate(T0)).cloneNode(true));
22
- const v = [null, null, null];
22
+ const v = [null, null, null, null];
23
23
  const B = acceptProps(element, D, S.n, v, componentProps, [], html);
24
24
  const m1 = (d) => {
25
25
  const n = (P1 ??= buildTemplate(T1)).cloneNode(true);
@@ -30,8 +30,8 @@ export function createUiScrollArea(options = {}, html) {
30
30
  const p1 = (r, c, d) => {
31
31
  };
32
32
  const p0 = (r, c, d) => {
33
- if (c & 7) {
34
- const x0 = hostState(["fill","orientation","size"], [v[0], v[1], v[2]]);
33
+ if (c & 15) {
34
+ const x0 = hostState(["fill","orientation","size","trim"], [v[0], v[1], v[2], v[3]]);
35
35
  const s0 = x0;
36
36
  if (s0 !== r.v0) writeAttribute(r.n, "data-ui-scroll-area-state", (r.v0 = s0) === "" ? null : s0);
37
37
  }
package/vue/UiBadge.d.ts CHANGED
@@ -7,7 +7,7 @@ type __VLS_Slots = {} & {
7
7
  };
8
8
  declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
9
9
  shape: {
10
- type: PropType<"pill" | "tag" | "dot" | "square" | null>;
10
+ type: PropType<"pill" | "tag" | "dot" | "square" | "circle" | null>;
11
11
  default: string;
12
12
  };
13
13
  size: {
@@ -24,7 +24,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
24
24
  };
25
25
  }>, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
26
26
  shape: {
27
- type: PropType<"pill" | "tag" | "dot" | "square" | null>;
27
+ type: PropType<"pill" | "tag" | "dot" | "square" | "circle" | null>;
28
28
  default: string;
29
29
  };
30
30
  size: {
@@ -40,7 +40,7 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
40
40
  default: string;
41
41
  };
42
42
  }>> & Readonly<{}>, {
43
- shape: "dot" | "tag" | "square" | "pill" | null;
43
+ shape: "circle" | "dot" | "tag" | "square" | "pill" | null;
44
44
  size: "xs" | "md" | null;
45
45
  tone: "info" | "neutral" | "accent" | "success" | "warning" | "danger" | null;
46
46
  variant: "solid" | "subtle" | "outline" | null;
package/vue/UiBadge.vue CHANGED
@@ -13,8 +13,8 @@ const propValidityContract = {
13
13
  required: false,
14
14
  target: { attribute: 'shape' },
15
15
  description:
16
- 'Silhouette. pill is rounded at both ends; tag is a pointed label. dot is a compact circular signal with visually hidden text: include a state label, or set\n 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\n decorative icon when adjacent words name it.',
17
- values: ['pill', 'tag', 'dot', 'square'],
16
+ 'Silhouette. pill is rounded at both ends; tag is a pointed label. dot is a compact circular signal with visually hidden text: include a state label, or set\n 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\n decorative icon when adjacent words name it. circle frames a visible single glyph or icon in a fixed circle, centered in both directions.',
17
+ values: ['pill', 'tag', 'dot', 'square', 'circle'],
18
18
  default: 'pill',
19
19
  },
20
20
  size: {
@@ -22,7 +22,7 @@ const propValidityContract = {
22
22
  required: false,
23
23
  target: { attribute: 'size' },
24
24
  description:
25
- 'Scale. xs is a compact count beside small controls; md is the usual text badge. A square is 24px at xs and 32px at md with default spacing.',
25
+ 'Scale. xs is a compact count beside small controls; md is the usual text badge. A square is 24px at xs and 32px at md with default spacing. A circle is\n 16px at xs and 24px at md.',
26
26
  values: ['xs', 'md'],
27
27
  default: 'md',
28
28
  },
@@ -48,7 +48,7 @@ const propValidityContract = {
48
48
  } as const
49
49
  const props = defineProps({
50
50
  shape: {
51
- type: null as unknown as PropType<'pill' | 'tag' | 'dot' | 'square' | null>,
51
+ type: null as unknown as PropType<'pill' | 'tag' | 'dot' | 'square' | 'circle' | null>,
52
52
  default: 'pill',
53
53
  },
54
54
  size: { type: null as unknown as PropType<'xs' | 'md' | null>, default: 'md' },
@@ -70,13 +70,13 @@ const acceptedProps: Record<string, unknown> = {
70
70
  variant: 'subtle',
71
71
  }
72
72
  const checkedProps = computed(() => ({
73
- shape: checkedProp<'pill' | 'tag' | 'dot' | 'square'>(
73
+ shape: checkedProp<'pill' | 'tag' | 'dot' | 'square' | 'circle'>(
74
74
  props['shape'],
75
75
  { kind: 'terminal', name: 'keyword' },
76
76
  false,
77
77
  'shape',
78
78
  acceptedProps,
79
- ) as 'pill' | 'tag' | 'dot' | 'square',
79
+ ) as 'pill' | 'tag' | 'dot' | 'square' | 'circle',
80
80
  size: checkedProp<'xs' | 'md'>(
81
81
  props['size'],
82
82
  { kind: 'terminal', name: 'keyword' },
@@ -191,6 +191,10 @@ const hostState = computed(() =>
191
191
  syntax: "*";
192
192
  inherits: false;
193
193
  }
194
+ @property --ui-badge-circle-size {
195
+ syntax: "*";
196
+ inherits: false;
197
+ }
194
198
  @property --ui-badge-dot-size {
195
199
  syntax: "*";
196
200
  inherits: false;
@@ -490,6 +494,40 @@ const hostState = computed(() =>
490
494
  display: none;
491
495
  }
492
496
 
497
+ /* A visible glyph needs fixed geometry, unlike a content-sized pill or a hidden-label dot. */
498
+ [data-component~="ui-badge"][data-ui-badge-state~="shape=circle"] {
499
+ --_badge-circle-size: var(--ui-badge-circle-size, calc(var(--ui-space-3) * 2));
500
+ display: inline-flex;
501
+ flex: none;
502
+ inline-size: var(--_badge-circle-size);
503
+ block-size: var(--_badge-circle-size);
504
+ min-inline-size: var(--_badge-circle-size);
505
+ min-block-size: var(--_badge-circle-size);
506
+ padding: 0;
507
+ border-radius: var(--ui-radius-round);
508
+ text-box-trim: none;
509
+ }
510
+
511
+ [data-component~="ui-badge"][data-ui-badge-state~="shape=circle"][data-ui-badge-state~="size=xs"] {
512
+ --_badge-circle-size: var(--ui-badge-circle-size, var(--ui-space-4));
513
+ }
514
+
515
+ [data-component~="ui-badge"][data-ui-badge-state~="shape=circle"] .label {
516
+ display: inline-flex;
517
+ align-items: center;
518
+ justify-content: center;
519
+ line-height: 1;
520
+ text-box-trim: none;
521
+ }
522
+
523
+ [data-component~="ui-badge"][data-ui-badge-state~="shape=circle"] .label :slotted(svg) {
524
+ margin-inline-end: 0;
525
+ }
526
+
527
+ [data-component~="ui-badge"][data-ui-badge-state~="shape=circle"] .label:empty {
528
+ display: none;
529
+ }
530
+
493
531
  /* A dot uses the badge's tone recipe; only its silhouette and visible label change. */
494
532
  [data-component~="ui-badge"][data-ui-badge-state~="shape=dot"] {
495
533
  position: relative;