@threadlabs/looma 0.25.2 → 0.25.4

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.25.2",
3
+ "version": "0.25.4",
4
4
  "type": "module",
5
5
  "description": "Looma components as HTML, Vue, and plain DOM, with design tokens.",
6
6
  "license": "MIT",
@@ -73,6 +73,7 @@
73
73
  display: inline-flex;
74
74
  align-items: center;
75
75
  justify-content: center;
76
+ gap: var(--ui-space-1);
76
77
  box-sizing: border-box;
77
78
  min-inline-size: 0;
78
79
  max-inline-size: 100%;
@@ -97,8 +98,30 @@
97
98
  text-overflow: ellipsis;
98
99
  }
99
100
 
100
- /* Trim leading while retaining the font's ascent and descent. A cap-to-baseline box
101
- would crop glyphs where the label clips horizontal overflow for ellipsis. */
101
+ .label:empty {
102
+ display: none;
103
+ }
104
+
105
+ /* Icon content has its own flex item so label ellipsis never clips its strokes. */
106
+ .icon {
107
+ display: inline-flex;
108
+ align-items: center;
109
+ justify-content: center;
110
+ flex: none;
111
+ }
112
+
113
+ .icon:empty {
114
+ display: none;
115
+ }
116
+
117
+ /* Ordinary icon-plus-text children keep inline ellipsis, with explicit glyph spacing. */
118
+
119
+ .label:has(svg) {
120
+ text-box-trim: none;
121
+ }
122
+
123
+ /* Text-only badges retain their trimmed inline baseline. Icon content needs a flex row;
124
+ trimming its label separately keeps the glyph centered without changing adjacent lines. */
102
125
  @supports (text-box-trim: trim-both) {
103
126
  :scope {
104
127
  display: inline-block;
@@ -108,6 +131,15 @@
108
131
  text-box-trim: trim-both;
109
132
  text-box-edge: text;
110
133
  }
134
+ :scope:has(.icon:not(:empty)),
135
+ :scope:has(.label svg) {
136
+ display: inline-flex;
137
+ text-box-trim: none;
138
+ }
139
+ :scope:has(.icon:not(:empty)) .label {
140
+ text-box-trim: trim-both;
141
+ text-box-edge: text;
142
+ }
111
143
  }
112
144
 
113
145
  :scope[data-ui-badge-state~="size=xs"] {
@@ -246,6 +278,10 @@
246
278
  justify-content: center;
247
279
  }
248
280
 
281
+ :scope[data-ui-badge-state~="shape=square"] .label:empty {
282
+ display: none;
283
+ }
284
+
249
285
  /* A dot uses the badge's tone recipe; only its silhouette and visible label change. */
250
286
  :scope[data-ui-badge-state~="shape=dot"] {
251
287
  position: relative;
@@ -332,8 +368,25 @@
332
368
  /* A constrained badge keeps its surface intact and ellipsizes only its label. The full
333
369
  text stays in the DOM for accessible names and consumers' selected values. */
334
370
 
335
- /* Trim leading while retaining the font's ascent and descent. A cap-to-baseline box
336
- would crop glyphs where the label clips horizontal overflow for ellipsis. */
371
+ /* Icon content has its own flex item so label ellipsis never clips its strokes. */
372
+
373
+ .icon :where([data-slotted], [data-slotted] *):is(svg) {
374
+ display: block;
375
+ inline-size: 1em;
376
+ block-size: 1em;
377
+ flex: none;
378
+ }
379
+
380
+ /* Ordinary icon-plus-text children keep inline ellipsis, with explicit glyph spacing. */
381
+ .label :where([data-slotted], [data-slotted] *):is(svg) {
382
+ inline-size: 1em;
383
+ block-size: 1em;
384
+ vertical-align: middle;
385
+ margin-inline-end: var(--ui-space-1);
386
+ }
387
+
388
+ /* Text-only badges retain their trimmed inline baseline. Icon content needs a flex row;
389
+ trimming its label separately keeps the glyph centered without changing adjacent lines. */
337
390
  @supports (text-box-trim: trim-both) {
338
391
  }
339
392
 
@@ -342,6 +395,10 @@
342
395
 
343
396
  /* An icon mark keeps the existing tone recipe and follows the global spacing and radius. */
344
397
 
398
+ :scope[data-ui-badge-state~="shape=square"] .label :where([data-slotted], [data-slotted] *):is(svg) {
399
+ margin-inline-end: 0;
400
+ }
401
+
345
402
  /* A dot uses the badge's tone recipe; only its silhouette and visible label change. */
346
403
 
347
404
  /* The clipped point needs an inset surface over an outer border-colour shape: a native border
@@ -90,6 +90,7 @@
90
90
  display: inline-flex;
91
91
  align-items: center;
92
92
  justify-content: center;
93
+ column-gap: var(--ui-space-2);
93
94
  vertical-align: middle;
94
95
  color: var(--_tone-text);
95
96
  }
@@ -139,7 +140,6 @@
139
140
  .spinner-wrap {
140
141
  display: inline-flex;
141
142
  flex: none;
142
- margin-inline-end: var(--ui-space-2);
143
143
  vertical-align: middle;
144
144
  }
145
145
 
@@ -187,7 +187,7 @@
187
187
  transform: translateY(-20%) rotate(45deg);
188
188
  }
189
189
 
190
- :scope[aria-expanded="true"] .disclosure-icon {
190
+ :scope[aria-expanded="true"] > .row > .disclosure > .disclosure-icon {
191
191
  transform: rotate(0deg);
192
192
  }
193
193
 
@@ -16,17 +16,30 @@ export function createUiBadge(options = {}) {
16
16
  }
17
17
  element.setAttribute("data-component", "ui-badge");
18
18
  const element0 = document.createElement("span");
19
- element0.setAttribute("class", "label");
19
+ element0.setAttribute("class", "icon");
20
+ element0.setAttribute("aria-hidden", "true");
21
+ if (slots["icon"] ?? [].length > 0) {
22
+ for (const child of slots["icon"] ?? []) {
23
+ const node = typeof child === "string" ? document.createTextNode(child) : child;
24
+ if (node.nodeType === 1) node.setAttribute("data-slotted", "");
25
+ projected.push([node, "icon"]);
26
+ element0.append(node);
27
+ }
28
+ } else {
29
+ }
30
+ element.append(element0);
31
+ const element1 = document.createElement("span");
32
+ element1.setAttribute("class", "label");
20
33
  if (children.length > 0) {
21
34
  for (const child of children) {
22
35
  const node = typeof child === "string" ? document.createTextNode(child) : child;
23
36
  if (node.nodeType === 1) node.setAttribute("data-slotted", "");
24
37
  projected.push([node, ""]);
25
- element0.append(node);
38
+ element1.append(node);
26
39
  }
27
40
  } else {
28
41
  }
29
- element.append(element0);
42
+ element.append(element1);
30
43
  manageGeneratedProps(element, [
31
44
  { name: "shape", attribute: "data-shape", value: componentProps["shape"], default: "pill", type: ["pill","tag","dot","square"], required: false },
32
45
  { name: "size", attribute: "data-size", value: componentProps["size"], default: "md", type: ["xs","md"], required: false },
package/vue/UiBadge.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import type { PropType } from 'vue';
2
- declare var __VLS_1: {};
2
+ declare var __VLS_1: {}, __VLS_3: {};
3
3
  type __VLS_Slots = {} & {
4
- default?: (props: typeof __VLS_1) => any;
4
+ icon?: (props: typeof __VLS_1) => any;
5
+ } & {
6
+ default?: (props: typeof __VLS_3) => any;
5
7
  };
6
8
  declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
7
9
  shape: {
package/vue/UiBadge.vue CHANGED
@@ -177,7 +177,8 @@ const hostState = computed(() =>
177
177
  :data-tone="reflectedProp('tone', 'tone', checkedProps.tone, undefined, false)"
178
178
  :data-variant="reflectedProp('variant', 'variant', checkedProps.variant, undefined, false)"
179
179
  :data-ui-badge-state="hostState || undefined"
180
- ><span class="label"><slot /></span></span>
180
+ ><span class="icon" aria-hidden="true"><slot name="icon" /></span>
181
+ <span class="label"><slot /></span></span>
181
182
  </template>
182
183
 
183
184
  <style scoped>
@@ -261,6 +262,7 @@ const hostState = computed(() =>
261
262
  display: inline-flex;
262
263
  align-items: center;
263
264
  justify-content: center;
265
+ gap: var(--ui-space-1);
264
266
  box-sizing: border-box;
265
267
  min-inline-size: 0;
266
268
  max-inline-size: 100%;
@@ -287,8 +289,43 @@ const hostState = computed(() =>
287
289
  text-overflow: ellipsis;
288
290
  }
289
291
 
290
- /* Trim leading while retaining the font's ascent and descent. A cap-to-baseline box
291
- would crop glyphs where the label clips horizontal overflow for ellipsis. */
292
+ .label:empty {
293
+ display: none;
294
+ }
295
+
296
+ /* Icon content has its own flex item so label ellipsis never clips its strokes. */
297
+ .icon {
298
+ display: inline-flex;
299
+ align-items: center;
300
+ justify-content: center;
301
+ flex: none;
302
+ }
303
+
304
+ .icon:empty {
305
+ display: none;
306
+ }
307
+
308
+ .icon :slotted(svg) {
309
+ display: block;
310
+ inline-size: 1em;
311
+ block-size: 1em;
312
+ flex: none;
313
+ }
314
+
315
+ /* Ordinary icon-plus-text children keep inline ellipsis, with explicit glyph spacing. */
316
+ .label :slotted(svg) {
317
+ inline-size: 1em;
318
+ block-size: 1em;
319
+ vertical-align: middle;
320
+ margin-inline-end: var(--ui-space-1);
321
+ }
322
+
323
+ .label:has(svg) {
324
+ text-box-trim: none;
325
+ }
326
+
327
+ /* Text-only badges retain their trimmed inline baseline. Icon content needs a flex row;
328
+ trimming its label separately keeps the glyph centered without changing adjacent lines. */
292
329
  @supports (text-box-trim: trim-both) {
293
330
  [data-component~="ui-badge"] {
294
331
  display: inline-block;
@@ -298,6 +335,15 @@ const hostState = computed(() =>
298
335
  text-box-trim: trim-both;
299
336
  text-box-edge: text;
300
337
  }
338
+ [data-component~="ui-badge"]:has(.icon:not(:empty)),
339
+ [data-component~="ui-badge"]:has(.label svg) {
340
+ display: inline-flex;
341
+ text-box-trim: none;
342
+ }
343
+ [data-component~="ui-badge"]:has(.icon:not(:empty)) .label {
344
+ text-box-trim: trim-both;
345
+ text-box-edge: text;
346
+ }
301
347
  }
302
348
 
303
349
  [data-component~="ui-badge"][data-ui-badge-state~="size=xs"] {
@@ -436,6 +482,14 @@ const hostState = computed(() =>
436
482
  justify-content: center;
437
483
  }
438
484
 
485
+ [data-component~="ui-badge"][data-ui-badge-state~="shape=square"] .label :slotted(svg) {
486
+ margin-inline-end: 0;
487
+ }
488
+
489
+ [data-component~="ui-badge"][data-ui-badge-state~="shape=square"] .label:empty {
490
+ display: none;
491
+ }
492
+
439
493
  /* A dot uses the badge's tone recipe; only its silhouette and visible label change. */
440
494
  [data-component~="ui-badge"][data-ui-badge-state~="shape=dot"] {
441
495
  position: relative;
package/vue/UiButton.vue CHANGED
@@ -933,6 +933,7 @@ function concat<T extends unknown[]>(...values: T): ConcatResult<T> {
933
933
  display: inline-flex;
934
934
  align-items: center;
935
935
  justify-content: center;
936
+ column-gap: var(--ui-space-2);
936
937
  vertical-align: middle;
937
938
  color: var(--_tone-text);
938
939
  }
@@ -982,7 +983,6 @@ function concat<T extends unknown[]>(...values: T): ConcatResult<T> {
982
983
  .spinner-wrap {
983
984
  display: inline-flex;
984
985
  flex: none;
985
- margin-inline-end: var(--ui-space-2);
986
986
  vertical-align: middle;
987
987
  }
988
988
 
@@ -47,8 +47,8 @@ declare const __VLS_base: import("vue").DefineComponent<import("vue").ExtractPro
47
47
  };
48
48
  }>> & Readonly<{}>, {
49
49
  value: string | null;
50
- disabled: boolean | null;
51
50
  icon: string | null;
51
+ disabled: boolean | null;
52
52
  description: string | null;
53
53
  keywords: string | null;
54
54
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -878,7 +878,7 @@ function concat<T extends unknown[]>(...values: T): ConcatResult<T> {
878
878
  transform: translateY(-20%) rotate(45deg);
879
879
  }
880
880
 
881
- [data-component~="ui-tree-item"][aria-expanded="true"] .disclosure-icon {
881
+ [data-component~="ui-tree-item"][aria-expanded="true"] > .row > .disclosure > .disclosure-icon {
882
882
  transform: rotate(0deg);
883
883
  }
884
884
 
@@ -9,7 +9,11 @@ var _hoisted_1 = [
9
9
  "data-variant",
10
10
  "data-ui-badge-state"
11
11
  ];
12
- var _hoisted_2 = { class: "label" };
12
+ var _hoisted_2 = {
13
+ class: "icon",
14
+ "aria-hidden": "true"
15
+ };
16
+ var _hoisted_3 = { class: "label" };
13
17
  //#endregion
14
18
  //#region .build/vue/UiBadge.vue
15
19
  var UiBadge_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__ */ defineComponent({
@@ -163,12 +167,12 @@ var UiBadge_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE
163
167
  "data-tone": reflectedProp("tone", "tone", checkedProps.value.tone, void 0, false),
164
168
  "data-variant": reflectedProp("variant", "variant", checkedProps.value.variant, void 0, false),
165
169
  "data-ui-badge-state": hostState.value || void 0
166
- }), [createElementVNode("span", _hoisted_2, [renderSlot(_ctx.$slots, "default", {}, void 0, true)])], 16, _hoisted_1)), [[unref(vPropValidity), {
170
+ }), [createElementVNode("span", _hoisted_2, [renderSlot(_ctx.$slots, "icon")]), createElementVNode("span", _hoisted_3, [renderSlot(_ctx.$slots, "default")])], 16, _hoisted_1)), [[unref(vPropValidity), {
167
171
  contract: propValidityContract,
168
172
  values: { ...props }
169
173
  }]]);
170
174
  };
171
175
  }
172
- }), [["__scopeId", "data-v-b1009c5d"]]);
176
+ }), [["__scopeId", "data-v-5065fda2"]]);
173
177
  //#endregion
174
178
  export { UiBadge_default as t };
@@ -755,6 +755,6 @@ var UiButton_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PUR
755
755
  }]]);
756
756
  };
757
757
  }
758
- }), [["__scopeId", "data-v-5c174bca"]]);
758
+ }), [["__scopeId", "data-v-7c2bc2b4"]]);
759
759
  //#endregion
760
760
  export { UiButton_default as t };
@@ -676,6 +676,6 @@ var UiTreeItem_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__P
676
676
  }]]);
677
677
  };
678
678
  }
679
- }), [["__scopeId", "data-v-4981f1b6"]]);
679
+ }), [["__scopeId", "data-v-f2ec0a81"]]);
680
680
  //#endregion
681
681
  export { UiTreeItem_default as t };