@threadlabs/looma 0.37.6 → 0.39.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 (52) hide show
  1. package/EXPRESSIVE-MARKDOWN.md +2 -0
  2. package/README.md +1 -1
  3. package/components/ui-button/ui-button.html +20 -5
  4. package/components/ui-combobox/ui-combobox.html +24 -3
  5. package/components/ui-combobox/ui-combobox.js +18 -3
  6. package/components/ui-icon-button/ui-icon-button.html +17 -8
  7. package/components/ui-menu-item/ui-menu-item.html +6 -1
  8. package/components/ui-select/ui-select.html +12 -0
  9. package/dist/index.js +1 -1
  10. package/docs/README.md +25 -12
  11. package/docs/catalog.json +78 -31
  12. package/docs/cli.mjs +1 -1
  13. package/docs/commands.mjs +2 -2
  14. package/docs/customizer-render.mjs +36 -20
  15. package/docs/customizer.js +76 -25
  16. package/docs/documentation.css +2 -0
  17. package/docs/index.d.ts +3 -3
  18. package/docs/local-server.mjs +1 -1
  19. package/docs/palette.mjs +3 -3
  20. package/docs/site.mjs +7 -7
  21. package/docs/theme-session.mjs +1 -0
  22. package/docs/theme.mjs +11 -10
  23. package/editor/expressmark.js +12 -4
  24. package/package.json +6 -6
  25. package/styles/ui-button.css +19 -9
  26. package/styles/ui-combobox.css +40 -2
  27. package/styles/ui-icon-button.css +12 -2
  28. package/styles/ui-menu-item.css +6 -1
  29. package/styles/ui-select.css +9 -0
  30. package/theme-system.css +7 -2
  31. package/vanilla/UiButton.js +1 -1
  32. package/vanilla/UiCombobox.d.ts +1 -0
  33. package/vanilla/UiCombobox.js +38 -37
  34. package/vanilla/UiIconButton.d.ts +1 -1
  35. package/vanilla/UiIconButton.js +1 -1
  36. package/vanilla/UiSelect.d.ts +1 -0
  37. package/vanilla/UiSelect.js +7 -7
  38. package/vue/UiButton.vue +30 -12
  39. package/vue/UiCombobox.d.ts +6 -0
  40. package/vue/UiCombobox.vue +70 -6
  41. package/vue/UiIconButton.d.ts +2 -2
  42. package/vue/UiIconButton.vue +23 -18
  43. package/vue/UiMenuItem.vue +6 -1
  44. package/vue/UiSelect.d.ts +6 -0
  45. package/vue/UiSelect.vue +30 -1
  46. package/vue/chunks/UiButton.js +4 -4
  47. package/vue/chunks/UiCombobox.js +72 -46
  48. package/vue/chunks/UiIconButton.js +6 -7
  49. package/vue/chunks/UiMenuItem.js +1 -1
  50. package/vue/chunks/UiSelect.js +27 -2
  51. package/vue/components.css +126 -25
  52. package/vue/editor/index.js +2 -3
package/docs/theme.mjs CHANGED
@@ -3,7 +3,7 @@ import { mixColour as mix, paletteSeeds, companionSurface, toLch, fromLch } from
3
3
 
4
4
  const bounds = {
5
5
  'text-size': [14, 24], 'control-roominess': [2, 4], 'corner-roundness': [0, 32],
6
- 'line-weight': [.5, 3], spacing: [.75, 1.5], 'scale-contrast': [1, 1.5],
6
+ 'line-weight': [.5, 3], rhythm: [.75, 1.5], spacing: [.75, 1.5], 'scale-contrast': [1, 1.5],
7
7
  'space-curve': [.5, 2], 'text-weight': [400, 700], 'outer-depth': [0, 2], 'inner-depth': [0, 2],
8
8
  'response-time': [0, 600],
9
9
  };
@@ -16,12 +16,13 @@ export const themeFields = [
16
16
  ['Colour', 'text', 'Text', '--ui-text', 'colour', '#1a1a1a', 'Readable content; supporting text follows this and Surface.'],
17
17
  ['Colour', 'warmth', 'Surface warmth', null, 'fraction', '0.5', '0% cool, 50% original, 100% warm paper and ink; your brand hue stays independent.'],
18
18
  ['Colour', 'color-intensity', 'Color intensity', null, 'fraction', '0.5', '0% quiet, 50% original, 100% vivid, coordinated across surfaces, actions and status colors.'],
19
- ['Colour', 'palette-relationship', 'Palette relationship', null, 'choice', 'preserve', 'Keep seed hues, or relate quiet surfaces to the brand through neighboring or complementary hues.'],
20
- ['Type', 'body-font', 'Body font', '--ui-font-sans', 'font', 'system-ui, sans-serif', 'The shared interface typeface.'],
21
- ['Type', 'display-font', 'Display font', '--ui-font-display', 'font', 'system-ui, sans-serif', 'The heading typeface.'],
19
+ ['Colour', 'palette-relationship', 'Palette relationship', null, 'choice', 'preserve', 'Keep base hues, or relate quiet surfaces to the brand through neighboring or complementary hues.'],
20
+ ['Type', 'body-font', 'Font', '--ui-font-sans', 'font', 'system-ui, sans-serif', 'The shared interface typeface.'],
21
+ ['Type', 'display-font', 'Heading font', '--ui-font-display', 'font', 'system-ui, sans-serif', 'The heading typeface.'],
22
22
  ['Type', 'text-size', 'Base text size', '--ui-font-size', 'positive-length', '16px', 'Sets the document root in px. Type, spacing and layout use rem relationships and scale from this base once.'],
23
23
  ['Type', 'text-weight', 'Emphasis weight', '--ui-font-medium', 'weight', '500', 'Control-label emphasis; body and headings keep their roles.'],
24
24
  ['Size & spacing', 'control-roominess', 'Control roominess', '--ui-control-roominess', 'positive', '2.5', 'Shared control height as a multiple of base text size; buttons, fields and swatches stay aligned.'],
25
+ ['Size & spacing', 'rhythm', 'Rhythm', '--ui-rhythm', 'positive', '1', 'More or less breathing room between lines and groups. Spacing fine-tunes gaps independently.'],
25
26
  ['Size & spacing', 'spacing', 'Spacing', '--ui-spacing', 'positive', '1', 'More or less breathing room throughout the interface.'],
26
27
  ['Size & spacing', 'scale-contrast', 'Size ratio', '--ui-scale-ratio', 'ratio', '1.2', 'The multiplier between small, medium and large controls. At 1 the sizes are equal; higher values separate them.'],
27
28
  ['Size & spacing', 'space-curve', 'Spacing curve', '--ui-space-curve', 'nonnegative', '1', 'Spacing growth: 1 is linear, 2 is quadratic. Independent of control sizes.'],
@@ -31,7 +32,7 @@ export const themeFields = [
31
32
  ['Material', 'inner-depth', 'Inner depth', '--ui-inner-depth', 'nonnegative', '0.3', 'How strongly fields and pressed controls sit into their surface.'],
32
33
  ['Material', 'glassiness', 'Glassiness', '--ui-glassiness', 'fraction', '0', 'Clear through richly frosted: the upper range adds stronger blur, saturation and glass edges. Content stays readable.'],
33
34
  ['Material', 'surface-separation', 'Surface separation', '--ui-surface-separation', 'fraction', '0', 'One material offset, scaled by popup purpose and actual size: quiet tooltips through substantial dialogs.'],
34
- ['Material', 'translucency', 'Overlay translucency', '--ui-overlay-translucency', 'fraction', '0', 'How much background comes through floating surfaces.'],
35
+ ['Material', 'translucency', 'Overlay translucency', '--ui-overlay-translucency', 'fraction', '0', 'How much background comes through floating material around opaque content.'],
35
36
  ['Material', 'edge-light', 'Edge light', '--ui-edge-light', 'fraction', '0', 'Light caught by surface edges, independent of depth.'],
36
37
  ['Material', 'finish', 'Finish', '--ui-finish', 'fraction', '0', 'Matte through polished: how strongly the material catches a reflection.'],
37
38
  ['Material', 'glow', 'Glow', '--ui-glow', 'fraction', '0', 'Reflected light beyond a surface, from a quiet halo to a vivid bloom at the upper end.'],
@@ -47,11 +48,11 @@ export const defaultRecipe = () => ({ schemaVersion: 1, values: Object.fromEntri
47
48
  const starting = values => ({ schemaVersion: 1, values: { ...defaultRecipe().values, ...values } });
48
49
  export const startingPoints = {
49
50
  clear: defaultRecipe(),
50
- bookish: starting({ action: '#2e1228', canvas: '#f7f7f5', surface: '#fffefd', text: '#0d0c0c', 'body-font': 'Arial, sans-serif', 'display-font': 'Georgia, serif', 'text-weight': '400', 'control-roominess': '2.625', 'scale-contrast': '1.18', 'outer-depth': '1', 'inner-depth': '1', glassiness: '0.8', 'surface-separation': '0.65', translucency: '0.18', 'edge-light': '1', finish: '0.85', glow: '0.3', 'light-spread': '0.3', 'light-strength': '0.9', 'response-time': '150ms', 'response-curve': 'ease' }),
51
- wiki: starting({ action: '#4f46e5', canvas: '#ffffff', surface: '#ffffff', text: '#1a1a1a', 'control-roominess': '2.125', spacing: '0.85', 'scale-contrast': '1.15', 'corner-roundness': '6px', 'outer-depth': '0.35', 'inner-depth': '0', 'response-time': '110ms' }),
52
- editorial: starting({ action: '#182c24', canvas: '#eee9dc', surface: '#fffdf5', text: '#182c24', 'corner-roundness': '0px', 'outer-depth': '0', 'inner-depth': '0', 'body-font': 'Georgia, serif', 'display-font': 'Georgia, serif', 'text-weight': '400', 'line-weight': '2px', 'response-time': '90ms', 'response-curve': 'linear' }),
53
- night: starting({ action: '#a7b8ff', canvas: '#11141c', surface: '#202634', text: '#edf1fb', 'corner-roundness': '12px', 'outer-depth': '1.4', 'inner-depth': '0.45', translucency: '0.2', 'edge-light': '0.65', 'response-time': '210ms', 'response-curve': 'cubic-bezier(0.2,0.8,0.2,1)' }),
54
- garden: starting({ action: '#256342', canvas: '#eef5e7', surface: '#fcfff6', text: '#173121', 'corner-roundness': '22px', 'outer-depth': '0.35', 'inner-depth': '0', 'edge-light': '0.2', 'control-roominess': '2.875', 'scale-contrast': '1.24', spacing: '1.05', 'response-time': '180ms', 'response-curve': 'cubic-bezier(0.16,1,0.3,1)' }),
51
+ bookish: starting({ rhythm: '1.05', action: '#2e1228', canvas: '#f7f7f5', surface: '#fffefd', text: '#0d0c0c', 'body-font': 'Arial, sans-serif', 'display-font': 'Georgia, serif', 'text-weight': '400', 'control-roominess': '2.625', 'scale-contrast': '1.18', 'outer-depth': '1', 'inner-depth': '1', glassiness: '0.8', 'surface-separation': '0.65', translucency: '0.18', 'edge-light': '1', finish: '0.85', glow: '0.3', 'light-spread': '0.3', 'light-strength': '0.9', 'response-time': '150ms', 'response-curve': 'ease' }),
52
+ wiki: starting({ rhythm: '0.8', 'text-weight': '400', 'space-curve': '0.8', action: '#4f46e5', canvas: '#ffffff', surface: '#ffffff', text: '#1a1a1a', 'control-roominess': '2.125', spacing: '0.85', 'scale-contrast': '1.15', 'corner-roundness': '6px', 'outer-depth': '0.35', 'inner-depth': '0', 'response-time': '110ms' }),
53
+ editorial: starting({ rhythm: '1.3', 'text-size': '17px', spacing: '1.12', 'space-curve': '1.4', 'scale-contrast': '1.3', action: '#182c24', canvas: '#eee9dc', surface: '#fffdf5', text: '#182c24', 'corner-roundness': '0px', 'outer-depth': '0', 'inner-depth': '0', 'body-font': 'Georgia, serif', 'display-font': 'Georgia, serif', 'text-weight': '400', 'line-weight': '2px', 'response-time': '90ms', 'response-curve': 'linear' }),
54
+ night: starting({ rhythm: '0.9', 'display-font': 'ui-monospace, monospace', 'text-weight': '600', 'control-roominess': '2.375', spacing: '0.9', 'scale-contrast': '1.25', 'space-curve': '1.2', finish: '0.6', glow: '0.45', glassiness: '0.45', 'surface-separation': '0.25', 'light-x': '0.25', 'light-spread': '0.65', action: '#a7b8ff', canvas: '#11141c', surface: '#202634', text: '#edf1fb', 'corner-roundness': '12px', 'outer-depth': '1.4', 'inner-depth': '0.45', translucency: '0.2', 'edge-light': '0.65', 'response-time': '210ms', 'response-curve': 'cubic-bezier(0.2,0.8,0.2,1)' }),
55
+ garden: starting({ rhythm: '1.2', 'body-font': 'ui-rounded, system-ui, sans-serif', 'display-font': 'Georgia, serif', 'text-weight': '400', 'space-curve': '1.15', finish: '0.15', 'light-spread': '1', action: '#256342', canvas: '#f4edda', surface: '#fff8e7', text: '#173121', 'corner-roundness': '22px', 'outer-depth': '0.35', 'inner-depth': '0', 'edge-light': '0.2', 'control-roominess': '2.875', 'scale-contrast': '1.24', spacing: '1.05', 'response-time': '180ms', 'response-curve': 'cubic-bezier(0.16,1,0.3,1)' }),
55
56
  };
56
57
  export const startingPointLabels = { clear: 'Clear', bookish: 'Bookish', wiki: 'Wiki', editorial: 'Editorial', night: 'Night', garden: 'Garden' };
57
58
  const paletteKeys = themeFields.filter(field => field.family === 'Colour').map(field => field.id);
@@ -109,7 +109,7 @@ function safeBackground(value, path) {
109
109
  value: "color",
110
110
  attributes: { style }
111
111
  }]
112
- })), (node) => typeof node.attributes?.style === "string")?.attributes?.style;
112
+ }), { syntax: "compatible" }), (node) => typeof node.attributes?.style === "string")?.attributes?.style;
113
113
  if (typeof accepted !== "string" || accepted.replace(/\s/g, "").replace(/;$/, "") !== style.replace(/\s/g, "")) fail(path, "unsafe or unsupported table background");
114
114
  }
115
115
  function safeNavigation(value, image, path) {
@@ -129,7 +129,7 @@ function safeNavigation(value, image, path) {
129
129
  value: "link"
130
130
  }]
131
131
  }]
132
- })), (child) => child.type === (image ? "image" : "link"))?.url !== value) fail(path, "unsafe or unsupported navigation URL");
132
+ }), { syntax: "compatible" }), (child) => child.type === (image ? "image" : "link"))?.url !== value) fail(path, "unsafe or unsupported navigation URL");
133
133
  }
134
134
  var MARKS = {
135
135
  bold: "strong",
@@ -605,6 +605,7 @@ function editorDocumentToExpressmark(document, options = {}) {
605
605
  return {
606
606
  type: "document",
607
607
  version: 1,
608
+ syntax: options.syntax ?? "compatible",
608
609
  body: (document.content ?? []).map((node, index) => {
609
610
  const child = toNode(node, `document.body[${index}]`, context);
610
611
  return child.type === "image" ? {
@@ -1007,7 +1008,11 @@ function expressmarkToEditorDocument(document) {
1007
1008
  * @failure Unsupported editor data raises EditorMarkdownConversionError before any input is changed.
1008
1009
  */
1009
1010
  function editorDocumentToMarkdown(document, options = {}) {
1010
- return stringify(editorDocumentToExpressmark(document, options), options);
1011
+ const conversion = {
1012
+ syntax: "compatible",
1013
+ ...options
1014
+ };
1015
+ return stringify(editorDocumentToExpressmark(document, conversion), conversion);
1011
1016
  }
1012
1017
  /**
1013
1018
  * Serializes the current editor, including its current display cache and concrete TOC entries.
@@ -1024,7 +1029,10 @@ function editorToMarkdown(editor, options = {}) {
1024
1029
  * @failure Unsupported content or root metadata stops conversion instead of being silently flattened.
1025
1030
  */
1026
1031
  function markdownToEditorDocument(markdown, options = {}) {
1027
- return expressmarkToEditorDocument(parse(markdown, options));
1032
+ return expressmarkToEditorDocument(parse(markdown, {
1033
+ syntax: "compatible",
1034
+ ...options
1035
+ }));
1028
1036
  }
1029
1037
  //#endregion
1030
1038
  export { EditorMarkdownConversionError, editorDocumentToExpressmark, editorDocumentToMarkdown, editorToMarkdown, expressmarkToEditorDocument, markdownToEditorDocument };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadlabs/looma",
3
- "version": "0.37.6",
3
+ "version": "0.39.0",
4
4
  "type": "module",
5
5
  "description": "Looma components as HTML, Vue, and plain DOM, with design tokens.",
6
6
  "license": "MIT",
@@ -119,7 +119,7 @@
119
119
  "lowlight": "^3.3.0",
120
120
  "markdown-it": "14.3.1",
121
121
  "lucide": "~0.554.0",
122
- "expressmark": "0.1.6"
122
+ "expressmark": ">=0.2.0 <1"
123
123
  },
124
124
  "peerDependencies": {
125
125
  "@nextwebwg/html-next": "1.0.0-alpha.69",
@@ -127,9 +127,9 @@
127
127
  "@tiptap/pm": ">=2 <3",
128
128
  "@tiptap/vue-3": ">=2 <3",
129
129
  "vue": "^3.5.43",
130
- "markupress": "^0.8.7 || ^0.9.0",
131
- "@nextwebwg/htmlkit": "1.0.0-alpha.69",
132
- "vite": "^8.3.4"
130
+ "markupress": ">=0.10.0 <1",
131
+ "@threadlabs/markupkit": ">=0.2.0 <1",
132
+ "vite": ">=7.2.7 <9"
133
133
  },
134
134
  "peerDependenciesMeta": {
135
135
  "@tiptap/core": {
@@ -147,7 +147,7 @@
147
147
  "markupress": {
148
148
  "optional": true
149
149
  },
150
- "@nextwebwg/htmlkit": {
150
+ "@threadlabs/markupkit": {
151
151
  "optional": true
152
152
  },
153
153
  "vite": {
@@ -268,7 +268,6 @@
268
268
  background-color: var(--ui-button-hover-surface, oklch(from var(--_tone) l c h / 12%));
269
269
  }
270
270
 
271
- /* A neutral card encloses content quietly; ordinary outlines keep their action-tone edge. */
272
271
  :scope:where([data-component~="ui-button"])[data-appearance="card"][data-tone="neutral"] {
273
272
  border-color: var(--ui-button-border, var(--ui-border));
274
273
  }
@@ -277,6 +276,12 @@
277
276
  border-color: var(--ui-button-hover-border, var(--ui-border-strong));
278
277
  }
279
278
 
279
+ :scope:where([data-component~="ui-button"])[data-appearance="card"][aria-pressed="true"] {
280
+ background-color: var(--ui-selection-surface);
281
+ border-color: var(--ui-selection-border);
282
+ color: var(--ui-selection-text);
283
+ }
284
+
280
285
  /* Pressed reads as pressed in: the highlight goes, and the shadow moves inside. */
281
286
  :scope:where([data-component~="ui-button"]):active:not(:disabled, [aria-disabled="true"]) {
282
287
  scale: var(--ui-press-scale, 1);
@@ -324,6 +329,18 @@
324
329
  background-color: var(--ui-button-surface, var(--ui-neutral-surface));
325
330
  color: var(--ui-button-text, var(--ui-text));
326
331
  }
332
+ :scope:where([data-component~="ui-button"])[data-tone="neutral"][data-appearance="outline"] {
333
+ border-color: var(--ui-button-border, var(--ui-control-border));
334
+ background-color: var(--ui-button-surface, transparent);
335
+ }
336
+ :scope:where([data-component~="ui-button"])[data-tone="neutral"][data-appearance="outline"]:hover:not(:disabled, [aria-disabled="true"]) {
337
+ border-color: var(--ui-button-hover-border, var(--ui-control-border-hover, var(--ui-control-border)));
338
+ background-color: var(--ui-button-hover-surface, var(--ui-neutral-hover));
339
+ }
340
+ :scope:where([data-component~="ui-button"])[data-tone="neutral"][data-appearance="outline"]:active:not(:disabled, [aria-disabled="true"]) {
341
+ border-color: var(--ui-button-hover-border, var(--ui-control-border-hover, var(--ui-control-border)));
342
+ background-color: var(--ui-button-active-surface, var(--ui-neutral-active));
343
+ }
327
344
  :scope:where([data-component~="ui-button"])[data-tone="neutral"][data-appearance="solid"]:hover:not(:disabled, [aria-disabled="true"]) {
328
345
  background-color: var(--ui-button-hover-surface, var(--ui-neutral-hover));
329
346
  }
@@ -411,10 +428,8 @@
411
428
  border-color: transparent;
412
429
  }
413
430
 
414
- /* Pressed, or holding its menu open, a ghost button sinks slightly: a light surface with a faint
415
- edge and a soft inner shadow. Lighter than hover, so an open menu's trigger never reads as heavy. */
416
431
  :scope:where([data-component~="ui-button"])[data-appearance="ghost"]:is(:active, [aria-expanded="true"]):not(:disabled, [aria-disabled="true"]) {
417
- background-color: var(--ui-button-ghost-active-surface, color-mix(in oklch, var(--ui-surface), var(--_tone) 3%));
432
+ background-color: var(--ui-button-ghost-active-surface, oklch(from var(--_tone) l c h / 18%));
418
433
  border-color: transparent;
419
434
  box-shadow: inset 0 0 0 1px oklch(from var(--_tone) l c h / 10%), inset 0 1px 2px oklch(from var(--_tone) l c h / 8%);
420
435
  }
@@ -548,8 +563,6 @@
548
563
  /* A link is never :enabled, so interactive states test "not unavailable" instead: for a button that
549
564
  is exactly :enabled, at the same specificity, and it also matches a link that can be followed. */
550
565
 
551
- /* A neutral card encloses content quietly; ordinary outlines keep their action-tone edge. */
552
-
553
566
  /* Pressed reads as pressed in: the highlight goes, and the shadow moves inside. */
554
567
 
555
568
  /* Tone is the colour, variant is the volume. Every pairing works: a danger outline is a
@@ -559,9 +572,6 @@
559
572
 
560
573
  /* Tints derive from the text colour, not a surface token, so hover and press show on any background. */
561
574
 
562
- /* Pressed, or holding its menu open, a ghost button sinks slightly: a light surface with a faint
563
- edge and a soft inner shadow. Lighter than hover, so an open menu's trigger never reads as heavy. */
564
-
565
575
  /* A soft halo that transitions in with the other box-shadow changes, layered on the raised shadow. */
566
576
 
567
577
  /* A loading action stays visually active; only an explicitly disabled action takes the
@@ -136,6 +136,21 @@
136
136
  flex: 1 1 auto;
137
137
  min-inline-size: 0;
138
138
  }
139
+ .start {
140
+ display: flex !important;
141
+ align-items: center !important;
142
+ flex: none;
143
+ padding-inline-start: var(--ui-space-3) !important;
144
+ }
145
+ .start:empty {
146
+ display: none !important;
147
+ }
148
+ :scope:where([data-component~="ui-combobox"])[data-ui-combobox-state~="size=sm"] .start {
149
+ padding-inline-start: var(--ui-space-2) !important;
150
+ }
151
+ :scope:where([data-component~="ui-combobox"])[data-ui-combobox-state~="size=lg"] .start {
152
+ padding-inline-start: var(--ui-space-4) !important;
153
+ }
139
154
  /* A ghost icon button: the affordance is the mark, not a box around it. */
140
155
  .help {
141
156
  flex: none;
@@ -174,6 +189,10 @@
174
189
  block-size: var(--ui-icon-size-sm);
175
190
  }
176
191
 
192
+ :scope:where([data-component~="ui-combobox"])[data-ui-combobox-state~="muted"] input:not(:disabled) {
193
+ color: var(--ui-text-secondary);
194
+ }
195
+
177
196
  input:where(:not([data-component], [data-slotted]))::selection {
178
197
  background: color-mix(in srgb, var(--ui-accent-solid) 20%, transparent);
179
198
  color: inherit;
@@ -258,7 +277,7 @@
258
277
  padding: var(--ui-space-1) !important;
259
278
  border: var(--ui-border-width) solid var(--_ui-combobox-popup-border, var(--ui-overlay-border, var(--ui-border-strong)));
260
279
  border-radius: var(--_ui-combobox-popup-radius, var(--ui-overlay-radius, var(--ui-radius-md)));
261
- background: var(--_ui-combobox-popup-surface, var(--ui-overlay-surface, var(--ui-surface-elevated)));
280
+ background: var(--_ui-combobox-popup-surface, var(--ui-material-content, var(--ui-surface-elevated)));
262
281
  color: var(--ui-text-primary);
263
282
  box-shadow: var(--_ui-combobox-popup-shadow, var(--ui-overlay-shadow, var(--ui-elevation-overlay, var(--ui-shadow-lg))));
264
283
  }
@@ -537,6 +556,21 @@
537
556
  flex: 1 1 auto;
538
557
  min-inline-size: 0;
539
558
  }
559
+ .start:where(:scope, :scope *) {
560
+ display: flex !important;
561
+ align-items: center !important;
562
+ flex: none;
563
+ padding-inline-start: var(--ui-space-3) !important;
564
+ }
565
+ .start:empty:where(:scope, :scope *) {
566
+ display: none !important;
567
+ }
568
+ :where([data-component~="ui-combobox"])[data-ui-combobox-state~="size=sm"] .start:where(:scope, :not([data-component], [data-slotted]:not([data-slotted~="ui-combobox"]))) {
569
+ padding-inline-start: var(--ui-space-2) !important;
570
+ }
571
+ :where([data-component~="ui-combobox"])[data-ui-combobox-state~="size=lg"] .start:where(:scope, :not([data-component], [data-slotted]:not([data-slotted~="ui-combobox"]))) {
572
+ padding-inline-start: var(--ui-space-4) !important;
573
+ }
540
574
  /* A ghost icon button: the affordance is the mark, not a box around it. */
541
575
  .help:where(:scope, :scope *) {
542
576
  flex: none;
@@ -574,6 +608,10 @@
574
608
  block-size: var(--ui-icon-size-sm);
575
609
  }
576
610
 
611
+ :where([data-component~="ui-combobox"])[data-ui-combobox-state~="muted"] input:not(:disabled):where(:scope, :not([data-component], [data-slotted]:not([data-slotted~="ui-combobox"]))) {
612
+ color: var(--ui-text-secondary);
613
+ }
614
+
577
615
  input:where(:not([data-component], [data-slotted]:not([data-slotted~="ui-combobox"]))):where(:scope, :scope *)::selection {
578
616
  background: color-mix(in srgb, var(--ui-accent-solid) 20%, transparent);
579
617
  color: inherit;
@@ -657,7 +695,7 @@
657
695
  padding: var(--ui-space-1) !important;
658
696
  border: var(--ui-border-width) solid var(--_ui-combobox-popup-border, var(--ui-overlay-border, var(--ui-border-strong)));
659
697
  border-radius: var(--_ui-combobox-popup-radius, var(--ui-overlay-radius, var(--ui-radius-md)));
660
- background: var(--_ui-combobox-popup-surface, var(--ui-overlay-surface, var(--ui-surface-elevated)));
698
+ background: var(--_ui-combobox-popup-surface, var(--ui-material-content, var(--ui-surface-elevated)));
661
699
  color: var(--ui-text-primary);
662
700
  box-shadow: var(--_ui-combobox-popup-shadow, var(--ui-overlay-shadow, var(--ui-elevation-overlay, var(--ui-shadow-lg))));
663
701
  }
@@ -172,10 +172,14 @@
172
172
  color: var(--_icon-button-text);
173
173
  }
174
174
 
175
- /* Omitted tone retains the existing accent solid; explicit neutral is additive. */
175
+ /* Neutral actions share the same quiet surface and border roles as fields. */
176
176
  :scope:where([data-component~="ui-icon-button"])[data-tone="neutral"] {
177
177
  --_tone-highlight: var(--ui-neutral-highlight, none);
178
178
  --_tone-glow: var(--ui-neutral-glow, 0 0 transparent);
179
+ --_icon-button-outline-surface: transparent;
180
+ --_icon-button-border: var(--ui-control-border);
181
+ --_icon-button-hover-border: var(--ui-control-border-hover, var(--ui-control-border));
182
+ --_icon-button-text: var(--ui-text);
179
183
  --_icon-button-solid-surface: var(--ui-neutral-surface);
180
184
  --_icon-button-solid-hover-surface: var(--ui-neutral-hover);
181
185
  --_icon-button-solid-text: var(--ui-text);
@@ -363,6 +367,12 @@
363
367
  :scope:where([data-component~="ui-icon-button"])[data-appearance="outline"]:hover:enabled:not([aria-disabled="true"]) {
364
368
  border-color: var(--ui-icon-button-hover-border, var(--_icon-button-hover-border, var(--ui-control-border-hover, var(--ui-control-border))));
365
369
  }
370
+ :scope:where([data-component~="ui-icon-button"])[data-tone="neutral"][data-appearance="outline"]:hover:enabled:not([aria-disabled="true"]) {
371
+ background-color: var(--ui-icon-button-hover-surface, var(--ui-neutral-hover));
372
+ }
373
+ :scope:where([data-component~="ui-icon-button"])[data-tone="neutral"][data-appearance="outline"]:active:enabled:not([aria-disabled="true"]) {
374
+ background-color: var(--ui-icon-button-active-surface, var(--ui-neutral-active));
375
+ }
366
376
 
367
377
  :scope:where([data-component~="ui-icon-button"])[data-appearance="solid"]:hover:enabled:not([aria-disabled="true"]) {
368
378
  background-color: var(--ui-icon-button-solid-hover-surface, var(--_icon-button-solid-hover-surface, var(--ui-accent-hover)));
@@ -487,7 +497,7 @@
487
497
  @scope ([data-component~="ui-icon-button"]) to ([data-component]) {
488
498
  /* This component's hooks style the element they are set on, and never a nested one. */
489
499
 
490
- /* Omitted tone retains the existing accent solid; explicit neutral is additive. */
500
+ /* Neutral actions share the same quiet surface and border roles as fields. */
491
501
 
492
502
  /* At rest an anticipatory button shows only its guide dot. The icon grows into place when
493
503
  proximity, hover, or focus reveals it; the button's hit area and guide remain stable. */
@@ -131,7 +131,12 @@
131
131
  }
132
132
 
133
133
  :scope:where([data-component~="ui-menu-item"]):is(:hover, :focus-visible):not([aria-disabled="true"]) {
134
- background: var(--ui-menu-item-hover-surface, var(--ui-surface-subtle));
134
+ background: var(--ui-menu-item-hover-surface, var(--ui-accent-subtle));
135
+ }
136
+
137
+ :scope:where([data-component~="ui-menu-item"])[aria-checked="true"]:not([aria-disabled="true"]) {
138
+ background: var(--ui-selection-surface);
139
+ color: var(--ui-selection-text);
135
140
  }
136
141
 
137
142
  :scope:where([data-component~="ui-menu-item"]):focus-visible {
@@ -88,6 +88,10 @@
88
88
  background-size: 1.125rem 1.125rem, 100% 100%;
89
89
  }
90
90
 
91
+ :scope:where([data-component~="ui-select"])[data-ui-select-state~="muted"] {
92
+ color: var(--ui-text-secondary);
93
+ }
94
+
91
95
  :scope:where([data-component~="ui-select"]):hover:not(:focus):not(:disabled):not([aria-invalid="true"]):not(:is(:user-invalid, [data-user-invalid])) {
92
96
  border-color: var(--ui-select-border-hover, var(--ui-control-border-hover, var(--ui-text-secondary)));
93
97
  }
@@ -160,6 +164,11 @@
160
164
 
161
165
  /* The select draws its own chevron: the native caret cannot be inset or sized consistently.
162
166
  * The root stays a native <s-lect>, so the icon is a background image rather than an element. */
167
+ :scope:where([data-component~="ui-select"])[data-ui-select-state~="muted"] :where([data-slotted], [data-slotted] *):is(option),
168
+ :scope:where([data-component~="ui-select"])[data-ui-select-state~="muted"] :where([data-slotted], [data-slotted] *):is(optgroup) {
169
+ color: var(--ui-text-primary);
170
+ background-color: var(--ui-control-surface);
171
+ }
163
172
 
164
173
  /* The app's invalid flag, or the browser's own verdict once the user has left an invalid field. */
165
174
 
package/theme-system.css CHANGED
@@ -14,6 +14,7 @@
14
14
  @layer theme {
15
15
  :root {
16
16
  --ui-spacing: 1;
17
+ --ui-rhythm: 1;
17
18
  --ui-control-roominess: 2.5;
18
19
  --ui-scale-ratio: 1.2;
19
20
  --ui-space-curve: 1;
@@ -36,7 +37,11 @@
36
37
  contrast cannot unexpectedly turn linear spacing into quadratic spacing. */
37
38
  --ui-control-size: calc(1rem * var(--ui-control-roominess));
38
39
  --_ui-theme-ratio: max(1, var(--ui-scale-ratio));
39
- --_ui-theme-unit: calc(1rem * 0.25 * max(0, var(--ui-spacing)));
40
+ --_ui-theme-unit: calc(1rem * 0.25 * max(0, var(--ui-spacing)) * clamp(0.75, var(--ui-rhythm), 1.5));
41
+ --ui-line-height-tight: clamp(1.1, calc(1.2 + (var(--ui-rhythm) - 1) * 0.2), 1.3);
42
+ --ui-line-height: clamp(1.35, calc(1.4 + (var(--ui-rhythm) - 1) * 0.4), 1.6);
43
+ --ui-line-height-md: var(--ui-line-height);
44
+ --ui-line-height-relaxed: calc(var(--ui-line-height) * 1.15);
40
45
  --_ui-theme-curve: max(0, var(--ui-space-curve));
41
46
  --ui-control-size-sm: calc(var(--ui-control-size) / var(--_ui-theme-ratio));
42
47
  --ui-control-size-md: var(--ui-control-size);
@@ -89,7 +94,7 @@
89
94
  --ui-elevation-dialog: var(--ui-shadow-lg);
90
95
  --ui-control-inset: inset calc(var(--_ui-light-inline) * var(--_ui-theme-inner)) calc(var(--_ui-light-block) * var(--_ui-theme-inner)) calc(3px * var(--_ui-theme-inner)) rgb(0 0 0 / 0.15), inset calc(-1 * var(--_ui-light-inline)) calc(-1 * var(--_ui-light-block)) 0 rgb(255 255 255 / calc(var(--_ui-theme-edge) * var(--ui-light-strength) * 0.18)), var(--ui-material-glow);
91
96
  --ui-pressed: inset calc(2 * var(--_ui-light-inline) * var(--_ui-theme-inner)) calc(2 * var(--_ui-light-block) * var(--_ui-theme-inner)) calc(4px * var(--_ui-theme-inner)) rgb(0 0 0 / 0.2);
92
- --ui-overlay-surface: color-mix(in srgb, var(--ui-surface-raised) calc((1 - clamp(0, var(--ui-overlay-translucency), 1)) * 100%), transparent);
97
+ --ui-overlay-surface: var(--ui-surface-raised);
93
98
  /* Decorative outer material is allowed to transmit the scene. Content keeps
94
99
  its own opaque plane; changing the scene cannot undermine text contrast. */
95
100
  --ui-material-content: var(--ui-surface-raised);
@@ -27,7 +27,7 @@ let P0, P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P
27
27
  const S = { n: ["appearance","colorTone"], t: ["?","?"], f: import.meta.url, g: "ui-button", k: 0, r: readonlyView, a: (host, I) => C(host, I), b: generatedInputs };
28
28
  const A = [["a",{},[],"",[],["data-ui-button-state","data-ui-button-prop-align","data-ui-button-prop-as","data-ui-button-prop-disabled","data-ui-button-prop-href","data-ui-button-prop-loading","data-ui-button-prop-pending","data-ui-button-prop-preset","data-ui-button-prop-rel","data-ui-button-prop-shape","data-ui-button-prop-size","data-ui-button-prop-square","data-ui-button-prop-stretch","data-ui-button-prop-target","data-ui-button-prop-tone","data-ui-button-prop-type","data-ui-button-prop-variant","data-appearance","data-tone","href","target","rel","aria-disabled","aria-busy","tabindex"]],["button",{},[],"",[],["data-ui-button-state","data-ui-button-prop-align","data-ui-button-prop-as","data-ui-button-prop-disabled","data-ui-button-prop-href","data-ui-button-prop-loading","data-ui-button-prop-pending","data-ui-button-prop-preset","data-ui-button-prop-rel","data-ui-button-prop-shape","data-ui-button-prop-size","data-ui-button-prop-square","data-ui-button-prop-stretch","data-ui-button-prop-target","data-ui-button-prop-tone","data-ui-button-prop-type","data-ui-button-prop-variant","data-appearance","data-tone","type","disabled","aria-disabled","aria-busy"]],["a",{},[],"",[],["data-ui-button-state","data-ui-button-prop-align","data-ui-button-prop-as","data-ui-button-prop-disabled","data-ui-button-prop-href","data-ui-button-prop-loading","data-ui-button-prop-pending","data-ui-button-prop-preset","data-ui-button-prop-rel","data-ui-button-prop-shape","data-ui-button-prop-size","data-ui-button-prop-square","data-ui-button-prop-stretch","data-ui-button-prop-target","data-ui-button-prop-tone","data-ui-button-prop-type","data-ui-button-prop-variant","data-appearance","data-tone","href","target","rel","aria-disabled","aria-busy","tabindex"]],["button",{},[],"",[],["data-ui-button-state","data-ui-button-prop-align","data-ui-button-prop-as","data-ui-button-prop-disabled","data-ui-button-prop-href","data-ui-button-prop-loading","data-ui-button-prop-pending","data-ui-button-prop-preset","data-ui-button-prop-rel","data-ui-button-prop-shape","data-ui-button-prop-size","data-ui-button-prop-square","data-ui-button-prop-stretch","data-ui-button-prop-target","data-ui-button-prop-tone","data-ui-button-prop-type","data-ui-button-prop-variant","data-appearance","data-tone","type","disabled","aria-disabled","aria-busy"]]];
29
29
  const Q = [[],[],[],[]];
30
- const D = { g: "ui-button", props: { "align": { "required":false,"values":["center","start"],"default":"center", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat)] }, "as": { "required":false,"values":["button","a"],"default":"button", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat)] }, "disabled": { "required":false,"default":false, c: checkBoolean, t: "boolean", j: 0, m: "b" }, "href": { "required":false, c: checkString, t: "string", j: 0, m: "s" }, "loading": { "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" }, "preset": { "required":false,"values":["none","primary","standard","quiet","destructive"],"default":"none", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat)] }, "rel": { "required":false, c: checkString, t: "string", j: 0, m: "s" }, "shape": { "required":false,"values":["rounded","pill"],"default":"rounded", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat)] }, "size": { "required":false,"values":["xs","sm","md","lg"],"default":"md", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat)] }, "square": { "required":false,"default":false, c: checkBoolean, t: "boolean", j: 0, m: "b" }, "stretch": { "required":false,"default":false, c: checkBoolean, t: "boolean", j: 0, m: "b" }, "target": { "required":false, c: checkString, t: "string", j: 0, m: "s" }, "tone": { "required":false,"values":["accent","neutral","danger","success","warning","info"],"default":"accent", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat)] }, "type": { "required":false,"values":["button","submit","reset"],"default":"button", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat)] }, "variant": { "required":false,"values":["outline","solid","danger","ghost","link","card","field"],"default":"outline", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat)] } } };
30
+ const D = { g: "ui-button", props: { "align": { "required":false,"values":["center","start"],"default":"center", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat)] }, "as": { "required":false,"values":["button","a"],"default":"button", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat)] }, "disabled": { "required":false,"default":false, c: checkBoolean, t: "boolean", j: 0, m: "b" }, "href": { "required":false, c: checkString, t: "string", j: 0, m: "s" }, "loading": { "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" }, "preset": { "required":false,"values":["none","primary","standard","quiet","destructive"],"default":"none", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat)] }, "rel": { "required":false, c: checkString, t: "string", j: 0, m: "s" }, "shape": { "required":false,"values":["rounded","pill"],"default":"rounded", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat)] }, "size": { "required":false,"values":["xs","sm","md","lg"],"default":"md", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat)] }, "square": { "required":false,"default":false, c: checkBoolean, t: "boolean", j: 0, m: "b" }, "stretch": { "required":false,"default":false, c: checkBoolean, t: "boolean", j: 0, m: "b" }, "target": { "required":false, c: checkString, t: "string", j: 0, m: "s" }, "tone": { "required":false,"values":["accent","neutral","danger","success","warning","info"],"default":"neutral", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat)] }, "type": { "required":false,"values":["button","submit","reset"],"default":"button", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat)] }, "variant": { "required":false,"values":["outline","solid","danger","ghost","link","card","field"],"default":"outline", c: checkFormat("keyword", keywordFormat), t: "keyword", j: 0, m: ["p",formatOf(keywordFormat)] } } };
31
31
  const C = (host) => activateModule(controller, host);
32
32
 
33
33
  export function createUiButton(options = {}, html) {
@@ -36,6 +36,7 @@ export interface UiComboboxProps {
36
36
  labelVisibility?: "visible" | "sr-only" | null;
37
37
  loading?: boolean | null;
38
38
  multiple?: boolean | null;
39
+ muted?: boolean | null;
39
40
  name?: string | null;
40
41
  openOnFocus?: boolean | null;
41
42
  placeholder?: string | null;