@threadlabs/looma 0.7.3 → 0.8.0

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.
Files changed (50) hide show
  1. package/components/ui-avatar/ui-avatar.html +1 -1
  2. package/components/ui-avatar-group/ui-avatar-group.html +1 -1
  3. package/components/ui-badge/ui-badge.html +20 -0
  4. package/components/ui-button/ui-button.html +38 -15
  5. package/components/ui-combobox/ui-combobox.html +1 -1
  6. package/components/ui-editor-slash-menu/ui-editor-slash-menu.html +1 -1
  7. package/components/ui-editor-table-context-menu/ui-editor-table-context-menu.html +1 -1
  8. package/components/ui-editor-table-toolbar/ui-editor-table-toolbar.html +1 -1
  9. package/components/ui-form-field/ui-form-field.html +3 -3
  10. package/components/ui-tree-item/ui-tree-item.html +1 -1
  11. package/dist/index.js +1 -1
  12. package/package.json +1 -1
  13. package/styles/ui-avatar-group.css +1 -1
  14. package/styles/ui-avatar.css +1 -1
  15. package/styles/ui-badge.css +22 -0
  16. package/styles/ui-button.css +20 -14
  17. package/styles/ui-combobox.css +1 -1
  18. package/styles/ui-editor-slash-menu.css +1 -1
  19. package/styles/ui-editor-table-context-menu.css +1 -1
  20. package/styles/ui-editor-table-toolbar.css +1 -1
  21. package/styles/ui-form-field.css +3 -3
  22. package/styles/ui-tree-item.css +1 -1
  23. package/tokens.css +2 -3
  24. package/vanilla/UiBadge.d.ts +1 -0
  25. package/vanilla/UiBadge.js +4 -2
  26. package/vanilla/UiButton.d.ts +5 -0
  27. package/vanilla/UiButton.js +8 -6
  28. package/vue/UiAvatar.vue +1 -1
  29. package/vue/UiAvatarGroup.vue +1 -1
  30. package/vue/UiBadge.d.ts +2 -0
  31. package/vue/UiBadge.vue +29 -0
  32. package/vue/UiButton.d.ts +9 -2
  33. package/vue/UiButton.vue +48 -18
  34. package/vue/UiCombobox.vue +1 -1
  35. package/vue/UiEditorSlashMenu.vue +1 -1
  36. package/vue/UiEditorTableContextMenu.vue +1 -1
  37. package/vue/UiEditorTableToolbar.vue +1 -1
  38. package/vue/UiFormField.vue +3 -3
  39. package/vue/UiTreeItem.vue +1 -1
  40. package/vue/chunks/UiAvatar.js +1 -1
  41. package/vue/chunks/UiAvatarGroup.js +1 -1
  42. package/vue/chunks/UiBadge.js +7 -2
  43. package/vue/chunks/UiButton.js +52 -10
  44. package/vue/chunks/UiCombobox.js +1 -1
  45. package/vue/chunks/UiEditorSlashMenu.js +1 -1
  46. package/vue/chunks/UiEditorTableContextMenu.js +1 -1
  47. package/vue/chunks/UiEditorTableToolbar.js +1 -1
  48. package/vue/chunks/UiFormField.js +1 -1
  49. package/vue/chunks/UiTreeItem.js +1 -1
  50. package/vue/components.css +270 -241
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadlabs/looma",
3
- "version": "0.7.3",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "description": "Looma components as HTML, Vue, and plain DOM, with design tokens.",
6
6
  "license": "MIT",
@@ -30,7 +30,7 @@
30
30
  var(--ui-avatar-group-overlap-shadow, var(--_overlap-shadow)),
31
31
  var(--ui-avatar-group-edge-ring, var(--_edge-ring));
32
32
  font: inherit;
33
- font-size: 0.75rem;
33
+ font-size: var(--ui-font-size-xs);
34
34
  font-weight: 600;
35
35
  flex-shrink: 0;
36
36
  }
@@ -11,7 +11,7 @@
11
11
  background: var(--ui-avatar-surface, var(--ui-accent-soft));
12
12
  color: var(--ui-avatar-text, var(--ui-accent-solid));
13
13
  font: inherit;
14
- font-size: 0.875rem;
14
+ font-size: var(--ui-font-size-sm);
15
15
  font-weight: 600;
16
16
  text-transform: uppercase;
17
17
  }
@@ -95,6 +95,24 @@
95
95
  --_badge-text: var(--ui-info-subtle-text, var(--ui-info-solid));
96
96
  }
97
97
 
98
+ /* A tag is flat at the start and points at the end. The point is cut from the box, so it has no
99
+ outline to follow: the edge is the fill. The end padding grows by the point's depth so the
100
+ label never runs into it. */
101
+ :scope[data-ui-badge-state~="shape=tag"] {
102
+ --_badge-point: var(--ui-badge-point, var(--ui-space-2));
103
+
104
+ border-color: transparent;
105
+ border-radius: 0;
106
+ padding-inline-end: calc(var(--ui-badge-padding-inline, var(--ui-space-2)) + var(--_badge-point));
107
+ clip-path: polygon(0 0, calc(100% - var(--_badge-point)) 0, 100% 50%, calc(100% - var(--_badge-point)) 100%, 0 100%);
108
+ }
109
+
110
+ :scope[data-ui-badge-state~="shape=tag"]:dir(rtl) {
111
+ padding-inline-end: var(--ui-badge-padding-inline, var(--ui-space-2));
112
+ padding-inline-start: calc(var(--ui-badge-padding-inline, var(--ui-space-2)) + var(--_badge-point));
113
+ clip-path: polygon(var(--_badge-point) 0, 100% 0, 100% 100%, var(--_badge-point) 100%, 0 50%);
114
+ }
115
+
98
116
  /* A component that sets its display still honours the hidden attribute on its root. */
99
117
  :scope[hidden] {
100
118
  display: none;
@@ -107,5 +125,9 @@
107
125
  @supports (text-box-trim: trim-both) {
108
126
  }
109
127
 
128
+ /* A tag is flat at the start and points at the end. The point is cut from the box, so it has no
129
+ outline to follow: the edge is the fill. The end padding grows by the point's depth so the
130
+ label never runs into it. */
131
+
110
132
  /* A component that sets its display still honours the hidden attribute on its root. */
111
133
  }
@@ -41,6 +41,7 @@
41
41
  color: var(--ui-button-text, var(--_tone-text));
42
42
  font: inherit;
43
43
  font-weight: var(--ui-font-medium);
44
+ text-decoration: none;
44
45
  line-height: var(--ui-line-height-tight);
45
46
  padding: var(--ui-space-2) var(--ui-space-3);
46
47
  background-image: var(--ui-raised-highlight);
@@ -64,22 +65,24 @@
64
65
  :scope[data-ui-button-state~="size=sm"] {
65
66
  min-block-size: var(--ui-control-size-sm);
66
67
  padding: var(--ui-space-1) var(--ui-space-2);
67
- font-size: 0.875rem;
68
+ font-size: var(--ui-font-size-sm);
68
69
  }
69
70
 
70
71
  :scope[data-ui-button-state~="size=lg"] {
71
72
  min-block-size: var(--ui-control-size-lg);
72
73
  padding: var(--ui-space-3) var(--ui-space-4);
73
- font-size: 1rem;
74
+ font-size: var(--ui-font-size-md);
74
75
  }
75
76
 
76
- :scope:hover:enabled {
77
+ /* A link is never :enabled, so interactive states test "not unavailable" instead: for a button that
78
+ is exactly :enabled, at the same specificity, and it also matches a link that can be followed. */
79
+ :scope:hover:not(:disabled, [aria-disabled="true"]) {
77
80
  border-color: var(--ui-button-hover-border, var(--_tone-hover));
78
81
  background-color: var(--ui-button-hover-surface, oklch(from var(--_tone) l c h / 12%));
79
82
  }
80
83
 
81
84
  /* Pressed reads as pressed in: the highlight goes, and the shadow moves inside. */
82
- :scope:active:enabled {
85
+ :scope:active:not(:disabled, [aria-disabled="true"]) {
83
86
  background-color: var(--ui-button-active-surface, oklch(from var(--_tone) l c h / 18%));
84
87
  background-image: none;
85
88
  box-shadow: var(--ui-pressed);
@@ -92,12 +95,12 @@
92
95
  box-shadow: var(--ui-button-solid-shadow, var(--ui-raised));
93
96
  }
94
97
 
95
- :is(:scope[data-ui-button-state~="variant=solid"], :scope[data-ui-button-state~="variant=danger"]):hover:enabled {
98
+ :is(:scope[data-ui-button-state~="variant=solid"], :scope[data-ui-button-state~="variant=danger"]):hover:not(:disabled, [aria-disabled="true"]) {
96
99
  background-color: var(--_tone-hover);
97
100
  border-color: var(--_tone-hover);
98
101
  }
99
102
 
100
- :is(:scope[data-ui-button-state~="variant=solid"], :scope[data-ui-button-state~="variant=danger"]):active:enabled {
103
+ :is(:scope[data-ui-button-state~="variant=solid"], :scope[data-ui-button-state~="variant=danger"]):active:not(:disabled, [aria-disabled="true"]) {
101
104
  background-color: var(--_tone-active);
102
105
  border-color: var(--_tone-active);
103
106
  background-image: none;
@@ -154,12 +157,12 @@
154
157
  }
155
158
 
156
159
  /* Tints derive from the text colour, not a surface token, so hover and press show on any background. */
157
- :scope[data-ui-button-state~="variant=ghost"]:hover:enabled {
160
+ :scope[data-ui-button-state~="variant=ghost"]:hover:not(:disabled, [aria-disabled="true"]) {
158
161
  background-color: var(--ui-button-ghost-hover-surface, oklch(from var(--_tone) l c h / 10%));
159
162
  border-color: transparent;
160
163
  }
161
164
 
162
- :scope[data-ui-button-state~="variant=ghost"]:active:enabled {
165
+ :scope[data-ui-button-state~="variant=ghost"]:active:not(:disabled, [aria-disabled="true"]) {
163
166
  background-color: var(--ui-button-ghost-active-surface, oklch(from var(--_tone) l c h / 16%));
164
167
  border-color: transparent;
165
168
  box-shadow: var(--ui-pressed);
@@ -183,7 +186,7 @@
183
186
  disabled outline is still an outline and a disabled ghost still has no edge. Intent goes — an
184
187
  unavailable action says "unavailable", not "unavailable, and destructive" — and so do the
185
188
  highlight and shadow, because nothing that cannot be pressed should look raised. */
186
- :scope:disabled {
189
+ :scope:is(:disabled, [aria-disabled="true"]) {
187
190
  /* Derived from the tone itself, not from a separate disabled palette: faded toward the
188
191
  page, which desaturates it at the same time. Toward the page rather than toward white,
189
192
  so a dark theme dims where a light one lightens — the same rule, both schemes. */
@@ -200,7 +203,7 @@
200
203
  }
201
204
 
202
205
  /* A filled button keeps its fill, faded further so its text stays legible on it. */
203
- :is(:scope[data-ui-button-state~="variant=solid"], :scope[data-ui-button-state~="variant=danger"]):disabled {
206
+ :is(:scope[data-ui-button-state~="variant=solid"], :scope[data-ui-button-state~="variant=danger"]):is(:disabled, [aria-disabled="true"]) {
204
207
  --_tone: color-mix(in oklab, var(--_hue) 34%, var(--ui-surface));
205
208
  }
206
209
 
@@ -208,7 +211,7 @@
208
211
  it takes a surface rather than reading as plain text. The surface is a wash of the faded tone,
209
212
  the way hover washes the live one, so it stays light in every tone. An opaque mix toward the
210
213
  hue came out a mid-grey slab for neutral, whose hue is the ink: louder than the enabled button. */
211
- :scope[data-ui-button-state~="variant=ghost"]:disabled {
214
+ :scope[data-ui-button-state~="variant=ghost"]:is(:disabled, [aria-disabled="true"]) {
212
215
  background-color: oklch(from var(--_tone) l c h / 14%);
213
216
  border-color: transparent;
214
217
  }
@@ -238,19 +241,19 @@
238
241
  transition: color var(--ui-motion-fast) var(--ui-motion-ease), text-decoration-color var(--ui-motion-fast) var(--ui-motion-ease);
239
242
  }
240
243
 
241
- :scope[data-ui-button-state~="variant=link"]:hover:enabled {
244
+ :scope[data-ui-button-state~="variant=link"]:hover:not(:disabled, [aria-disabled="true"]) {
242
245
  color: var(--ui-button-link-hover-text, var(--ui-text-primary));
243
246
  text-decoration-color: currentColor;
244
247
  background: none;
245
248
  transform: none;
246
249
  }
247
250
 
248
- :scope[data-ui-button-state~="variant=link"]:active:enabled {
251
+ :scope[data-ui-button-state~="variant=link"]:active:not(:disabled, [aria-disabled="true"]) {
249
252
  background: none;
250
253
  transform: none;
251
254
  }
252
255
 
253
- :scope[data-ui-button-state~="variant=link"]:disabled {
256
+ :scope[data-ui-button-state~="variant=link"]:is(:disabled, [aria-disabled="true"]) {
254
257
  background: none;
255
258
  border-color: transparent;
256
259
  color: var(--ui-button-disabled-text, var(--ui-disabled-text));
@@ -272,6 +275,9 @@
272
275
  @supports (text-box-trim: trim-both) {
273
276
  }
274
277
 
278
+ /* A link is never :enabled, so interactive states test "not unavailable" instead: for a button that
279
+ is exactly :enabled, at the same specificity, and it also matches a link that can be followed. */
280
+
275
281
  /* Pressed reads as pressed in: the highlight goes, and the shadow moves inside. */
276
282
 
277
283
  /* Tone is the colour, variant is the volume. Every pairing works: a danger outline is a
@@ -9,7 +9,7 @@
9
9
  }
10
10
  label {
11
11
  color: var(--ui-combobox-label-text, var(--ui-text-primary));
12
- font-size: var(--ui-combobox-label-font-size, 0.875rem);
12
+ font-size: var(--ui-combobox-label-font-size, var(--ui-font-size-sm));
13
13
  font-weight: 500;
14
14
  }
15
15
  /* The field and anything that annotates it sit on one row; the help button is beside the box,
@@ -152,7 +152,7 @@
152
152
  align-items: center;
153
153
  justify-content: center;
154
154
  font-family: var(--ui-font-family-mono, ui-monospace, monospace);
155
- font-size: 10px;
155
+ font-size: var(--ui-font-size-2xs);
156
156
  background: var(--ui-surface-hover);
157
157
  border: 1px solid var(--ui-border);
158
158
  border-radius: 3px;
@@ -101,7 +101,7 @@
101
101
 
102
102
  .heading {
103
103
  padding: var(--ui-space-1) var(--ui-space-2);
104
- font-size: 0.6875rem;
104
+ font-size: var(--ui-font-size-2xs);
105
105
  font-weight: 700;
106
106
  letter-spacing: 0.05em;
107
107
  text-transform: uppercase;
@@ -141,7 +141,7 @@
141
141
 
142
142
  .menu-heading {
143
143
  padding: var(--ui-space-1) var(--ui-space-2);
144
- font-size: 0.6875rem;
144
+ font-size: var(--ui-font-size-2xs);
145
145
  font-weight: 700;
146
146
  letter-spacing: 0.05em;
147
147
  text-transform: uppercase;
@@ -18,12 +18,12 @@
18
18
 
19
19
  .label {
20
20
  color: var(--ui-text-secondary);
21
- font-size: 0.95rem;
21
+ font-size: var(--ui-font-size-sm);
22
22
  }
23
23
 
24
24
  .help {
25
25
  color: var(--ui-text-muted);
26
- font-size: 0.875rem;
26
+ font-size: var(--ui-font-size-sm);
27
27
  line-height: var(--ui-line-height-md);
28
28
  }
29
29
 
@@ -43,7 +43,7 @@
43
43
  .error,
44
44
  :where([data-slotted], [data-slotted] *):is([role="alert"]) {
45
45
  color: var(--ui-accent-active);
46
- font-size: 0.875rem;
46
+ font-size: var(--ui-font-size-sm);
47
47
  }
48
48
 
49
49
  /* A component that sets its display still honours the hidden attribute on its root. */
@@ -17,7 +17,7 @@
17
17
  align-items: center;
18
18
  min-block-size: var(--ui-tree-item-min-block-size, var(--_tree-row-min-height));
19
19
  min-inline-size: 0;
20
- font-size: var(--ui-tree-item-font-size, var(--ui-font-size-ui));
20
+ font-size: var(--ui-tree-item-font-size, var(--ui-font-size-sm));
21
21
  border-radius: var(--ui-radius-md);
22
22
  color: var(--ui-text-secondary);
23
23
  transition:
package/tokens.css CHANGED
@@ -36,7 +36,6 @@
36
36
  "Segoe UI Emoji";
37
37
  --ui-font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
38
38
  --ui-font-size: 1rem;
39
- --ui-font-size-sm: 0.875rem;
40
39
  --ui-font-medium: 500;
41
40
  --ui-line-height: 1.4;
42
41
 
@@ -78,8 +77,8 @@
78
77
  /* Type scale, from the base size. */
79
78
  --ui-font-size-2xs: calc(var(--ui-font-size) * 0.6875);
80
79
  --ui-font-size-xs: calc(var(--ui-font-size) * 0.75);
81
- /* Dense interactive text: legible without body-copy spacing. */
82
- --ui-font-size-ui: calc(var(--ui-font-size) * 0.9375);
80
+ /* Dense text: controls, labels, and metadata. Two steps below body copy, never one pixel. */
81
+ --ui-font-size-sm: calc(var(--ui-font-size) * 0.875);
83
82
  --ui-font-size-md: var(--ui-font-size);
84
83
  --ui-font-size-lg: calc(var(--ui-font-size) * 1.125);
85
84
  --ui-font-size-xl: calc(var(--ui-font-size) * 1.25);
@@ -6,6 +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" | null;
9
10
  tone?: "neutral" | "accent" | "info" | "success" | "warning" | "danger" | null;
10
11
  variant?: "solid" | "subtle" | null;
11
12
  attributes?: Readonly<Record<string, string | number | boolean | null | undefined>>;
@@ -5,8 +5,9 @@ import "../styles/ui-badge.css";
5
5
  export function createUiBadge(options = {}) {
6
6
  const { attributes = {}, children = [], slots = {}, as, ...componentProps } = options;
7
7
  const projected = [];
8
- const prop0 = componentProps["tone"] === undefined ? "neutral" : componentProps["tone"];
9
- const prop1 = componentProps["variant"] === undefined ? "subtle" : componentProps["variant"];
8
+ const prop0 = componentProps["shape"] === undefined ? "pill" : componentProps["shape"];
9
+ const prop1 = componentProps["tone"] === undefined ? "neutral" : componentProps["tone"];
10
+ const prop2 = componentProps["variant"] === undefined ? "subtle" : componentProps["variant"];
10
11
  const element = document.createElement("span");
11
12
  for (const [name, value] of Object.entries(attributes)) {
12
13
  if (value === null || value === undefined || value === false) continue;
@@ -22,6 +23,7 @@ export function createUiBadge(options = {}) {
22
23
  } else {
23
24
  }
24
25
  manageGeneratedProps(element, [
26
+ { name: "shape", attribute: "data-shape", value: componentProps["shape"], default: "pill", type: ["pill","tag"], required: false },
25
27
  { name: "tone", attribute: "data-tone", value: componentProps["tone"], default: "neutral", type: ["neutral","accent","info","success","warning","danger"], required: false },
26
28
  { name: "variant", attribute: "data-variant", value: componentProps["variant"], default: "subtle", type: ["solid","subtle"], required: false },
27
29
  ]);
@@ -8,12 +8,17 @@ export interface UiButtonElement extends HTMLButtonElement {
8
8
  export interface UiButtonProps {
9
9
  align?: "center" | "start" | null;
10
10
  disabled?: boolean | null;
11
+ href?: string | null;
12
+ rel?: string | null;
11
13
  size?: "sm" | "md" | "lg" | null;
12
14
  stretch?: boolean | null;
15
+ target?: string | null;
13
16
  tone?: "accent" | "neutral" | "danger" | "success" | "warning" | "info" | null;
17
+ type?: "button" | "submit" | "reset" | null;
14
18
  variant?: "outline" | "solid" | "danger" | "ghost" | "link" | null;
15
19
  attributes?: Readonly<Record<string, string | number | boolean | null | undefined>>;
16
20
  children?: readonly (string | Node)[];
17
21
  slots?: Readonly<Record<string, readonly (string | Node)[]>>;
22
+ as?: "button" | "a";
18
23
  }
19
24
  export declare function createUiButton(props?: UiButtonProps): UiButtonElement;
@@ -2,20 +2,22 @@
2
2
  import { manageComponentLifecycle } from "@nextwebwg/html-next/runtime";
3
3
  import "../styles/ui-button.css";
4
4
 
5
- const definition = {...{"contract":{"tag":"ui-button","props":{"align":{"type":{"enum":["center","start"]},"required":false,"target":{"attribute":"align"},"default":"center"},"disabled":{"type":"boolean","required":false,"target":{"attribute":"disabled"},"default":false},"size":{"type":{"enum":["sm","md","lg"]},"required":false,"target":{"attribute":"size"},"default":"md"},"stretch":{"type":"boolean","required":false,"target":{"attribute":"stretch"},"default":false},"tone":{"type":{"enum":["accent","neutral","danger","success","warning","info"]},"required":false,"target":{"attribute":"tone"},"default":"accent"},"variant":{"type":{"enum":["outline","solid","danger","ghost","link"]},"required":false,"target":{"attribute":"variant"},"default":"outline"}}},"template":{"kind":"element","name":"button","attributes":[{"kind":"literal","name":"type","value":"button"},{"kind":"attribute","name":"disabled","expression":"disabled","expressionPlan":{"source":"disabled","ast":{"kind":"id","name":"disabled"},"dependencies":["disabled"]}}],"children":[{"kind":"slot"}]},"declarations":[],"root":{"kind":"native","element":"button","choices":["button"]}},source:{file:import.meta.url},css:""};
5
+ const definition = {...{"contract":{"tag":"ui-button","props":{"align":{"type":{"enum":["center","start"]},"required":false,"target":{"attribute":"align"},"default":"center"},"disabled":{"type":"boolean","required":false,"target":{"attribute":"disabled"},"default":false},"href":{"type":"string","required":false,"target":{"attribute":"href"}},"rel":{"type":"string","required":false,"target":{"attribute":"rel"}},"size":{"type":{"enum":["sm","md","lg"]},"required":false,"target":{"attribute":"size"},"default":"md"},"stretch":{"type":"boolean","required":false,"target":{"attribute":"stretch"},"default":false},"target":{"type":"string","required":false,"target":{"attribute":"target"}},"tone":{"type":{"enum":["accent","neutral","danger","success","warning","info"]},"required":false,"target":{"attribute":"tone"},"default":"accent"},"type":{"type":{"enum":["button","submit","reset"]},"required":false,"target":{"attribute":"type"},"default":"button"},"variant":{"type":{"enum":["outline","solid","danger","ghost","link"]},"required":false,"target":{"attribute":"variant"},"default":"outline"}}},"template":{"kind":"element","name":"button","attributes":[{"kind":"attribute","name":"type","expression":"{ true: type, false: null }[format('%s', not href)]","expressionPlan":{"source":"{ true: type, false: null }[format('%s', not href)]","ast":{"kind":"index","object":{"kind":"object","pairs":[{"key":"true","value":{"kind":"id","name":"type"}},{"key":"false","value":{"kind":"literal","value":null}}]},"index":{"kind":"call","fn":"format","args":[{"kind":"literal","value":"%s"},{"kind":"unary","op":"not","operand":{"kind":"id","name":"href"}}]}},"dependencies":["href","type"]}},{"kind":"attribute","name":"disabled","expression":"{ true: true, false: null }[format('%s', disabled and not href)]","expressionPlan":{"source":"{ true: true, false: null }[format('%s', disabled and not href)]","ast":{"kind":"index","object":{"kind":"object","pairs":[{"key":"true","value":{"kind":"literal","value":true}},{"key":"false","value":{"kind":"literal","value":null}}]},"index":{"kind":"call","fn":"format","args":[{"kind":"literal","value":"%s"},{"kind":"binary","op":"and","left":{"kind":"id","name":"disabled"},"right":{"kind":"unary","op":"not","operand":{"kind":"id","name":"href"}}}]}},"dependencies":["disabled","href"]}},{"kind":"attribute","name":"href","expression":"{ true: null, false: href }[format('%s', disabled)]","expressionPlan":{"source":"{ true: null, false: href }[format('%s', disabled)]","ast":{"kind":"index","object":{"kind":"object","pairs":[{"key":"true","value":{"kind":"literal","value":null}},{"key":"false","value":{"kind":"id","name":"href"}}]},"index":{"kind":"call","fn":"format","args":[{"kind":"literal","value":"%s"},{"kind":"id","name":"disabled"}]}},"dependencies":["disabled","href"]}},{"kind":"attribute","name":"target","expression":"target","expressionPlan":{"source":"target","ast":{"kind":"id","name":"target"},"dependencies":["target"]}},{"kind":"attribute","name":"rel","expression":"rel","expressionPlan":{"source":"rel","ast":{"kind":"id","name":"rel"},"dependencies":["rel"]}},{"kind":"attribute","name":"aria-disabled","expression":"{ true: 'true', false: null }[format('%s', disabled and href)]","expressionPlan":{"source":"{ true: 'true', false: null }[format('%s', disabled and href)]","ast":{"kind":"index","object":{"kind":"object","pairs":[{"key":"true","value":{"kind":"literal","value":"true"}},{"key":"false","value":{"kind":"literal","value":null}}]},"index":{"kind":"call","fn":"format","args":[{"kind":"literal","value":"%s"},{"kind":"binary","op":"and","left":{"kind":"id","name":"disabled"},"right":{"kind":"id","name":"href"}}]}},"dependencies":["disabled","href"]}}],"children":[{"kind":"slot"}]},"declarations":[],"root":{"kind":"native","element":"button","choices":["button","a"]}},source:{file:import.meta.url},css:""};
6
6
 
7
7
  export function createUiButton(options = {}) {
8
8
  const { attributes = {}, children = [], slots = {}, as, ...componentProps } = options;
9
9
  const projected = [];
10
- const element = document.createElement("button");
10
+ const element = document.createElement(as ?? "button");
11
11
  for (const [name, value] of Object.entries(attributes)) {
12
12
  if (value === null || value === undefined || value === false) continue;
13
13
  element.setAttribute(name, value === true ? "" : String(value));
14
14
  }
15
- if (!element.hasAttribute("type")) element.setAttribute("type", "button");
16
- const value0 = componentProps["disabled"] === undefined ? false : componentProps["disabled"];
17
- if (value0 === true) element.setAttribute("disabled", "");
18
- else element.removeAttribute("disabled");
15
+ const value0 = componentProps["target"] === undefined ? undefined : componentProps["target"];
16
+ if (value0 === null || value0 === undefined) element.removeAttribute("target");
17
+ else element.setAttribute("target", String(value0));
18
+ const value1 = componentProps["rel"] === undefined ? undefined : componentProps["rel"];
19
+ if (value1 === null || value1 === undefined) element.removeAttribute("rel");
20
+ else element.setAttribute("rel", String(value1));
19
21
  element.setAttribute("data-component", "ui-button");
20
22
  if (children.length > 0) {
21
23
  for (const child of children) {
package/vue/UiAvatar.vue CHANGED
@@ -62,7 +62,7 @@ useComponentHost(controllerModule.default, {
62
62
  background: var(--ui-avatar-surface, var(--ui-accent-soft));
63
63
  color: var(--ui-avatar-text, var(--ui-accent-solid));
64
64
  font: inherit;
65
- font-size: 0.875rem;
65
+ font-size: var(--ui-font-size-sm);
66
66
  font-weight: 600;
67
67
  text-transform: uppercase;
68
68
  }
@@ -85,7 +85,7 @@ useComponentHost(controllerModule.default, {
85
85
  var(--ui-avatar-group-overlap-shadow, var(--_overlap-shadow)),
86
86
  var(--ui-avatar-group-edge-ring, var(--_edge-ring));
87
87
  font: inherit;
88
- font-size: 0.75rem;
88
+ font-size: var(--ui-font-size-xs);
89
89
  font-weight: 600;
90
90
  flex-shrink: 0;
91
91
  }
package/vue/UiBadge.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  type __VLS_Props = {
2
+ shape?: 'pill' | 'tag';
2
3
  tone?: 'neutral' | 'accent' | 'info' | 'success' | 'warning' | 'danger';
3
4
  variant?: 'solid' | 'subtle';
4
5
  };
@@ -7,6 +8,7 @@ type __VLS_Slots = {} & {
7
8
  default?: (props: typeof __VLS_1) => any;
8
9
  };
9
10
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
11
+ shape: "pill" | "tag";
10
12
  tone: "neutral" | "accent" | "info" | "success" | "warning" | "danger";
11
13
  variant: "solid" | "subtle";
12
14
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
package/vue/UiBadge.vue CHANGED
@@ -6,10 +6,12 @@ defineOptions({ inheritAttrs: false })
6
6
 
7
7
  const props = withDefaults(
8
8
  defineProps<{
9
+ shape?: 'pill' | 'tag'
9
10
  tone?: 'neutral' | 'accent' | 'info' | 'success' | 'warning' | 'danger'
10
11
  variant?: 'solid' | 'subtle'
11
12
  }>(),
12
13
  {
14
+ shape: 'pill',
13
15
  tone: 'neutral',
14
16
  variant: 'subtle',
15
17
  },
@@ -20,6 +22,7 @@ const hostState = computed(() =>
20
22
  [
21
23
  props.tone && `tone tone=${props.tone}`,
22
24
  props.variant && `variant variant=${props.variant}`,
25
+ props.shape && `shape shape=${props.shape}`,
23
26
  ].filter(Boolean).join(' ')
24
27
  )
25
28
  </script>
@@ -129,6 +132,32 @@ const hostState = computed(() =>
129
132
  --_badge-text: var(--ui-info-subtle-text, var(--ui-info-solid));
130
133
  }
131
134
 
135
+ /* A tag is flat at the start and points at the end. The point is cut from the box, so it has no
136
+ outline to follow: the edge is the fill. The end padding grows by the point's depth so the
137
+ label never runs into it. */
138
+ [data-component~="ui-badge"][data-ui-badge-state~="shape=tag"] {
139
+ --_badge-point: var(--ui-badge-point, var(--ui-space-2));
140
+
141
+ border-color: transparent;
142
+ border-radius: 0;
143
+ padding-inline-end: calc(var(--ui-badge-padding-inline, var(--ui-space-2)) + var(--_badge-point));
144
+ clip-path: polygon(
145
+ 0 0,
146
+ calc(100% - var(--_badge-point)) 0,
147
+ 100% 50%,
148
+ calc(100% - var(--_badge-point)) 100%,
149
+ 0 100%
150
+ );
151
+ }
152
+
153
+ [data-component~="ui-badge"][data-ui-badge-state~="shape=tag"]:dir(rtl) {
154
+ padding-inline-end: var(--ui-badge-padding-inline, var(--ui-space-2));
155
+ padding-inline-start: calc(
156
+ var(--ui-badge-padding-inline, var(--ui-space-2)) + var(--_badge-point)
157
+ );
158
+ clip-path: polygon(var(--_badge-point) 0, 100% 0, 100% 100%, var(--_badge-point) 100%, 0 50%);
159
+ }
160
+
132
161
  /* A component that sets its display still honours the hidden attribute on its root. */
133
162
  [data-component~="ui-badge"][hidden] {
134
163
  display: none;
package/vue/UiButton.d.ts CHANGED
@@ -1,18 +1,25 @@
1
1
  type __VLS_Props = {
2
+ as?: 'button' | 'a';
2
3
  align?: 'center' | 'start';
3
4
  disabled?: boolean;
5
+ href?: string;
6
+ rel?: string;
4
7
  size?: 'sm' | 'md' | 'lg';
5
8
  stretch?: boolean;
9
+ target?: string;
6
10
  tone?: 'accent' | 'neutral' | 'danger' | 'success' | 'warning' | 'info';
11
+ type?: 'button' | 'submit' | 'reset';
7
12
  variant?: 'outline' | 'solid' | 'danger' | 'ghost' | 'link';
8
13
  };
9
- declare var __VLS_1: {};
14
+ declare var __VLS_8: {};
10
15
  type __VLS_Slots = {} & {
11
- default?: (props: typeof __VLS_1) => any;
16
+ default?: (props: typeof __VLS_8) => any;
12
17
  };
13
18
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
19
+ type: "button" | "submit" | "reset";
14
20
  tone: "accent" | "neutral" | "danger" | "success" | "warning" | "info";
15
21
  variant: "outline" | "solid" | "danger" | "ghost" | "link";
22
+ as: "button" | "a";
16
23
  align: "center" | "start";
17
24
  disabled: boolean;
18
25
  size: "sm" | "md" | "lg";