@threadlabs/looma 0.9.1 → 0.10.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 (64) hide show
  1. package/components/shared/form-reset.js +21 -0
  2. package/components/ui-avatar/ui-avatar.html +12 -4
  3. package/components/ui-avatar-group/ui-avatar-group.html +5 -2
  4. package/components/ui-badge/ui-badge.html +6 -2
  5. package/components/ui-button/ui-button.html +6 -2
  6. package/components/ui-callout/ui-callout.html +3 -1
  7. package/components/ui-checkbox/ui-checkbox.html +24 -6
  8. package/components/ui-checkbox/ui-checkbox.js +9 -0
  9. package/components/ui-cluster/ui-cluster.html +6 -2
  10. package/components/ui-combobox/ui-combobox.html +54 -20
  11. package/components/ui-combobox/ui-combobox.js +31 -12
  12. package/components/ui-container/ui-container.html +5 -2
  13. package/components/ui-disclosure/ui-disclosure.html +6 -3
  14. package/components/ui-editable/ui-editable.html +3 -1
  15. package/components/ui-editable/ui-editable.js +7 -6
  16. package/components/ui-editor-mention-menu/ui-editor-mention-menu.html +3 -1
  17. package/components/ui-editor-slash-menu/ui-editor-slash-menu.html +3 -1
  18. package/components/ui-floating-action-button/ui-floating-action-button.html +5 -3
  19. package/components/ui-form-field/ui-form-field.html +9 -3
  20. package/components/ui-grid/ui-grid.html +6 -2
  21. package/components/ui-icon/ui-icon.html +3 -1
  22. package/components/ui-icon-button/ui-icon-button.html +11 -4
  23. package/components/ui-input/ui-input.html +3 -1
  24. package/components/ui-input/ui-input.js +5 -2
  25. package/components/ui-menu-item/ui-menu-item.html +5 -2
  26. package/components/ui-radio/ui-radio.html +12 -5
  27. package/components/ui-radio/ui-radio.js +11 -0
  28. package/components/ui-radio-group/ui-radio-group.html +17 -7
  29. package/components/ui-radio-group/ui-radio-group.js +16 -3
  30. package/components/ui-reel/ui-reel.html +9 -3
  31. package/components/ui-search-result-row/ui-search-result-row.html +5 -3
  32. package/components/ui-select/ui-select.html +3 -1
  33. package/components/ui-select/ui-select.js +5 -2
  34. package/components/ui-stack/ui-stack.html +9 -3
  35. package/components/ui-switch/ui-switch.html +10 -5
  36. package/components/ui-switch/ui-switch.js +9 -0
  37. package/components/ui-switcher/ui-switcher.html +9 -3
  38. package/components/ui-tabs/ui-tabs.html +8 -3
  39. package/components/ui-textarea/ui-textarea.html +3 -1
  40. package/components/ui-textarea/ui-textarea.js +5 -2
  41. package/components/ui-tree/ui-tree.html +7 -3
  42. package/components/ui-tree-item/ui-tree-item.html +32 -11
  43. package/dist/index.js +1 -1
  44. package/package.json +1 -1
  45. package/vanilla/UiCheckbox.d.ts +1 -0
  46. package/vanilla/UiCheckbox.js +7 -4
  47. package/vanilla/UiCombobox.js +113 -99
  48. package/vanilla/UiRadioGroup.js +7 -1
  49. package/vanilla/UiSearchResultRow.js +4 -1
  50. package/vanilla/UiSwitch.d.ts +1 -0
  51. package/vanilla/UiSwitch.js +7 -4
  52. package/vue/UiCheckbox.d.ts +2 -0
  53. package/vue/UiCheckbox.vue +3 -0
  54. package/vue/UiCombobox.vue +27 -4
  55. package/vue/UiRadioGroup.vue +2 -0
  56. package/vue/UiSearchResultRow.vue +1 -0
  57. package/vue/UiSwitch.d.ts +2 -0
  58. package/vue/UiSwitch.vue +3 -0
  59. package/vue/chunks/UiCheckbox.js +4 -1
  60. package/vue/chunks/UiCombobox.js +67 -39
  61. package/vue/chunks/UiRadioGroup.js +8 -2
  62. package/vue/chunks/UiSearchResultRow.js +7 -2
  63. package/vue/chunks/UiSwitch.js +4 -1
  64. package/vue/components.css +89 -89
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Calls back once a form reset has restored `control`'s form to its defaults. The browser restores
3
+ * each native control after the reset event has been dispatched, and a reset button's event runs
4
+ * its listeners before that, so the callback waits a task.
5
+ *
6
+ * @ownership A controller that keeps its own copy of a control's state reads it back here; the
7
+ * native defaults (defaultValue, defaultChecked, defaultSelected) are what the reset restores.
8
+ * @lifecycle Returns the function that stops listening.
9
+ */
10
+ export function afterFormReset(control, callback) {
11
+ const document = control.ownerDocument;
12
+ let timer;
13
+ const onReset = (event) => {
14
+ if (event.target === control.form) timer = setTimeout(callback);
15
+ };
16
+ document.addEventListener("reset", onReset);
17
+ return () => {
18
+ clearTimeout(timer);
19
+ document.removeEventListener("reset", onReset);
20
+ };
21
+ }
@@ -1,9 +1,17 @@
1
1
  <template component="ui-avatar" controller="./ui-avatar.js">
2
2
  <defs>
3
- <prop name="src" type="string">Image URL.</prop>
4
- <prop name="alt" type="string" default="">alt token.</prop>
5
- <prop name="name" type="string" default="">name token.</prop>
6
- <prop name="fallback" type="string" default="">fallback token.</prop>
3
+ <prop name="src" type="string"
4
+ >The image to show, cropped to fill the circle. Until it loads, or if it fails, the avatar shows initials instead. An img element authored inside the avatar
5
+ takes precedence over src.</prop>
6
+ <prop name="alt" type="string" default=""
7
+ >The avatar's accessible name, announced for it as an image. When empty, name is used, then "Avatar". Initials come from name first, so alt supplies them
8
+ only when name is empty. An authored img carries its own alt instead.</prop>
9
+ <prop name="name" type="string" default=""
10
+ >The person's name. The initials are drawn from it: the first letters of the first two words, or the first two letters of a single word. It is also the
11
+ accessible name when alt is empty.</prop>
12
+ <prop name="fallback" type="string" default=""
13
+ >Text shown instead of the initials derived from name or alt, whenever there is no image. It is uppercased; keep it to a couple of characters, because the
14
+ circle clips anything wider.</prop>
7
15
  <state name="hasImage" :value="false"></state>
8
16
  <state name="hasAuthoredImage" :value="false"></state>
9
17
  <state name="initials" :value="'?'"></state>
@@ -1,7 +1,10 @@
1
1
  <template component="ui-avatar-group" controller="./ui-avatar-group.js">
2
2
  <defs>
3
- <prop name="max" type="number" default="5">max token.</prop>
4
- <prop name="label" type="string" default="People">label token.</prop>
3
+ <prop name="max" type="number" default="5"
4
+ >How many avatars to show. The rest are hidden and counted in a +N badge after the last visible one. Fractions round down, and 0 hides every avatar behind
5
+ the badge.</prop>
6
+ <prop name="label" type="string" default="People"
7
+ >The group's accessible name, announced for the set of avatars as a whole. Name who they are rather than repeating the default.</prop>
5
8
  <state name="overflowCount" :value="0"></state>
6
9
  </defs>
7
10
  <div role="group" :aria-label="label">
@@ -1,7 +1,11 @@
1
1
  <template component="ui-badge">
2
2
  <defs>
3
- <prop name="variant" type="solid | subtle" default="subtle">variant token.</prop>
4
- <prop name="tone" type="neutral | accent | info | success | warning | danger" default="neutral">tone token.</prop>
3
+ <prop name="variant" type="solid | subtle" default="subtle"
4
+ >Emphasis. subtle sets the text in its tone over a soft wash of it; solid fills with the tone and uses contrasting text, for a status that has to stand out.
5
+ The neutral tone looks the same in both.</prop>
6
+ <prop name="tone" type="neutral | accent | info | success | warning | danger" default="neutral"
7
+ >Which colour the badge speaks in. neutral is a quiet grey for plain labels; accent draws the eye in the accent colour; info, success, warning, and danger
8
+ mark a status, so match the tone to what the badge says.</prop>
5
9
  <prop name="shape" type="pill | tag" default="pill"
6
10
  >Silhouette. pill is rounded at both ends; tag is a label: flat at the start and coming to a point at the end, for a tag or a chosen chip.</prop>
7
11
  </defs>
@@ -3,11 +3,15 @@
3
3
  <prop name="variant" type="outline | solid | danger | ghost | link" default="outline"
4
4
  >Emphasis: how loudly the button asks. solid fills with its tone, outline draws its tone at the edge over a wash of it, ghost carries only text until it is
5
5
  hovered, and link is an inline text action that reads as a link. danger is solid in the danger tone, kept for the call sites that name it.</prop>
6
- <prop name="size" type="sm | md | lg" default="md">Control size.</prop>
6
+ <prop name="size" type="sm | md | lg" default="md"
7
+ >Height, padding, and type size. sm is the small control height (2rem by default) with tighter padding and smaller text, md is the standard control height
8
+ (2.5rem), and lg is taller (3rem) with roomier padding.</prop>
7
9
  <prop name="tone" type="accent | neutral | danger | success | warning | info" default="accent"
8
10
  >Which colour the button speaks in. It applies to every variant, so an outline, a ghost, and a solid in the same tone are the same action at three volumes:
9
11
  tone="danger" variant="outline" is a destructive secondary action. neutral is the quiet one, for a button that should not claim a colour.</prop>
10
- <prop name="disabled" type="boolean" default="false">disabled token.</prop>
12
+ <prop name="disabled" type="boolean" default="false"
13
+ >Makes the button unavailable: it cannot be pressed, its tone fades, and it loses its raised look. A disabled link (as="a" with href) drops its href and
14
+ states aria-disabled="true" instead, because a link has no native disabled state.</prop>
11
15
  <prop name="align" type="center | start" default="center"
12
16
  >Content alignment. start lays the content out like a list option: from the start edge, with start-aligned text.</prop>
13
17
  <prop name="stretch" type="boolean" default="false">Fills the inline size of its container.</prop>
@@ -1,6 +1,8 @@
1
1
  <template component="ui-callout">
2
2
  <defs>
3
- <prop name="tone" type="info | note | warning | success | danger" default="info">Semantic message tone.</prop>
3
+ <prop name="tone" type="info | note | warning | success | danger" default="info"
4
+ >Which kind of message the callout carries, shown by its icon and colour: info (the default) for neutral information, note for an aside in muted grey,
5
+ warning for a caution, success for a confirmation, and danger for an error or a destructive consequence.</prop>
4
6
  </defs>
5
7
  <div role="note">
6
8
  <span class="icon" aria-hidden="true">
@@ -1,15 +1,33 @@
1
1
  <template component="ui-checkbox" controller="./ui-checkbox.js">
2
2
  <defs>
3
- <prop name="checked" type="boolean" default="false">Whether the checkbox is checked initially or when set externally.</prop>
4
- <prop name="disabled" type="boolean" default="false">disabled token.</prop>
5
- <prop name="indeterminate" type="boolean" default="false">indeterminate token.</prop>
6
- <prop name="required" type="boolean" default="false">required token.</prop>
7
- <prop name="value" type="string" default="on">value token.</prop>
3
+ <prop name="checked" type="boolean" default="false"
4
+ >Whether the checkbox is checked initially or when set externally. A form reset returns it to this.</prop>
5
+ <prop name="disabled" type="boolean" default="false">Disables the native checkbox, so it cannot be toggled or focused.</prop>
6
+ <prop name="indeterminate" type="boolean" default="false"
7
+ >Shows the mixed state, for a checkbox that summarises a group whose items are partly checked. It sets the native indeterminate property, which is
8
+ independent of checked, so update it as the group changes.</prop>
9
+ <prop name="required" type="boolean" default="false"
10
+ >Marks the native checkbox required, so native form validation treats it as invalid until it is checked.</prop>
11
+ <prop name="name" type="string" default=""
12
+ >The native name the checkbox submits under: a form that contains it sends name=value while it is checked, and nothing while it is unchecked, as a native
13
+ checkbox does. Left empty, it takes no part in form submission.</prop>
14
+ <prop name="value" type="string" default="on"
15
+ >The value a form submits under name while the checkbox is checked, also reported in the change event's detail so one handler can tell checkboxes apart. on
16
+ by default, as on a native checkbox.</prop>
8
17
  <state name="internalChecked" :value="false"></state>
9
18
  <event name="change" type="object({ checked: boolean, value: string, trigger: keyboard | pointer | programmatic })"></event>
10
19
  </defs>
11
20
  <label>
12
- <input type="checkbox" $ref="input" .checked="internalChecked" .indeterminate="indeterminate" :disabled="disabled" :required="required" :value="value">
21
+ <input
22
+ type="checkbox"
23
+ $ref="input"
24
+ .checked="internalChecked"
25
+ .indeterminate="indeterminate"
26
+ :disabled="disabled"
27
+ :required="required"
28
+ :name="name"
29
+ :value="value"
30
+ >
13
31
  <span class="label"><slot></slot></span>
14
32
  </label>
15
33
  <style>
@@ -1,12 +1,16 @@
1
+ import { afterFormReset } from "../shared/form-reset.js";
1
2
  import { trackTrigger } from "../shared/trigger.js";
2
3
 
3
4
  // `checked` sets the control initially and whenever it changes; the user's changes update the state.
5
+ // A form reset returns the control to `checked`. Vue writes the checked attribute on every render, so
6
+ // the native default alone cannot hold it there.
4
7
  export default function controller(host) {
5
8
  const input = host.refs.input;
6
9
  const [trigger, stopTracking] = trackTrigger(host);
7
10
  let external = host.state.checked;
8
11
  host.state.internalChecked = Boolean(external);
9
12
  const stop = host.effect(() => {
13
+ input.defaultChecked = Boolean(host.state.checked);
10
14
  if (host.state.checked === external) return;
11
15
  external = host.state.checked;
12
16
  host.state.internalChecked = Boolean(external);
@@ -16,8 +20,13 @@ export default function controller(host) {
16
20
  host.dispatch("change", { checked: input.checked, value: String(host.state.value ?? "on"), trigger: trigger() });
17
21
  };
18
22
  input.addEventListener("change", onChange);
23
+ const stopReset = afterFormReset(input, () => {
24
+ host.state.internalChecked = Boolean(external);
25
+ input.checked = Boolean(external);
26
+ });
19
27
  return () => {
20
28
  stop();
29
+ stopReset();
21
30
  stopTracking();
22
31
  input.removeEventListener("change", onChange);
23
32
  };
@@ -1,7 +1,11 @@
1
1
  <template component="ui-cluster">
2
2
  <defs>
3
- <prop name="gap" type="xs | s | m | l | xl">gap token.</prop>
4
- <prop name="align" type="start | center | end | stretch">align token.</prop>
3
+ <prop name="gap" type="xs | s | m | l | xl"
4
+ >Space between items, on the spacing scale: xs is 0.5rem, s 0.75rem (also the default), m 1rem, l 1.5rem, and xl 2rem. The --ui-cluster-gap custom property,
5
+ when set, overrides it.</prop>
6
+ <prop name="align" type="start | center | end | stretch"
7
+ >How items line up within each row: center (the default) aligns their middles, start their tops, end their bottoms, and stretch makes every item as tall as
8
+ its row.</prop>
5
9
  </defs>
6
10
  <div><slot></slot></div>
7
11
  <style>
@@ -2,25 +2,54 @@
2
2
  <link rel="component" href="../ui-tooltip/ui-tooltip.html">
3
3
  <template component="ui-combobox" controller="./ui-combobox.js">
4
4
  <defs>
5
- <prop name="label" type="string" default="">label token.</prop>
6
- <prop name="placeholder" type="string" default="">placeholder token.</prop>
7
- <prop name="name" type="string" default="">name token.</prop>
8
- <prop name="value" type="string | null">The selected value in single mode.</prop>
5
+ <prop name="label" type="string" default=""
6
+ >The field's label, tied to its input and shown above it; required adds an asterisk. It also names the suggestion list. Pair it with
7
+ labelVisibility="sr-only" when the context already labels the field visibly.</prop>
8
+ <prop name="placeholder" type="string" default=""
9
+ >Hint text shown in the empty input, as on a native input. It disappears as the user types, so it never replaces label.</prop>
10
+ <prop name="name" type="string" default=""
11
+ >The form field name the combobox submits its value under, never the label shown in the field. Single mode sends one entry: the selected option's value, the
12
+ typed text when allowFreeText is set and no option is selected, or an empty string. Multiple mode sends one entry per chosen item's value, and nothing
13
+ when none is chosen. A disabled combobox sends nothing.</prop>
14
+ <prop name="value" type="string | null"
15
+ >The selected value in single mode, which a named combobox submits with its form. The field shows the matching option's label, and a form reset returns to
16
+ this value.</prop>
9
17
  <prop name="items" type="list(object({ id: string, value: string, label: string, group?: string, disabled?: boolean }))"
10
- >The selected items in multiple mode, as JSON.</prop>
11
- <prop name="multiple" type="boolean" default="false">multiple token.</prop>
12
- <prop name="tokenSeparators" type="list(string)">tokenSeparators token.</prop>
13
- <prop name="query" type="string">query token.</prop>
18
+ >The selected items in multiple mode, as JSON; a named combobox submits each one's value. A form reset returns to these items, or to none when unset.</prop>
19
+ <prop name="multiple" type="boolean" default="false"
20
+ >Lets the user choose several options. Each choice becomes a removable badge before the input, the list stays open between picks, and choosing a checked
21
+ option removes it. value-change then reports the whole list of items; use items, not value, in this mode.</prop>
22
+ <prop name="tokenSeparators" type="list(string)"
23
+ >Keys that commit the typed text in multiple mode, as KeyboardEvent key values such as "," or ";". The text is added as the option whose label matches it,
24
+ ignoring case, or with allowCreate is reported as create-item; otherwise it stays in the input.</prop>
25
+ <prop name="query" type="string"
26
+ >The input's text, when the consumer controls it. Once set, the consumer owns the text: typing reports query-change, and the input returns to query unless
27
+ query is updated to match. Leave it unset to let the combobox manage its own text, showing the chosen option's label.</prop>
14
28
  <prop name="allowFreeText" type="boolean" default="false">Allows values that do not match an authored option.</prop>
15
29
  <prop name="allowCreate" type="boolean" default="false">Offers the current query as a new option.</prop>
16
- <prop name="disabled" type="boolean" default="false">disabled token.</prop>
17
- <prop name="readonly" type="boolean" default="false">Whether the current value may be selected but not edited.</prop>
18
- <prop name="required" type="boolean" default="false">required token.</prop>
19
- <prop name="size" type="sm | md" default="md">size token.</prop>
20
- <prop name="labelVisibility" type="visible | sr-only" default="visible">labelVisibility token.</prop>
21
- <prop name="disclosure" type="boolean" default="false">disclosure token.</prop>
22
- <prop name="clearable" type="boolean" default="false">clearable token.</prop>
23
- <prop name="help" type="string" default="">help token.</prop>
30
+ <prop name="disabled" type="boolean" default="false"
31
+ >Disables the input and every button in the field: the chosen-item badges, the clear and disclosure buttons, and the help button. The list closes and cannot
32
+ be opened, the value cannot be cleared, and a named combobox submits nothing with its form.</prop>
33
+ <prop name="readonly" type="boolean" default="false"
34
+ >Shows the current value without letting it change: the text can be selected but not edited, the list does not open, and the clear, disclosure, and badge
35
+ remove buttons are disabled. A named combobox still submits its value.</prop>
36
+ <prop name="required" type="boolean" default="false"
37
+ >Requires a value: adds an asterisk to the label, marks the input required for native form validation, and makes validation report "A value is required."
38
+ when the field is left empty. In multiple mode a chosen item satisfies it, whatever is left in the input.</prop>
39
+ <prop name="size" type="sm | md" default="md"
40
+ >Field height. md is the standard control height; sm is the small control height with tighter padding, for dense forms.</prop>
41
+ <prop name="labelVisibility" type="visible | sr-only" default="visible"
42
+ >visible shows the label above the field; sr-only hides it visually but keeps it as the input's accessible name. Use sr-only only when the surrounding
43
+ layout already makes the field's purpose clear.</prop>
44
+ <prop name="disclosure" type="boolean" default="false"
45
+ >Adds a chevron button that opens and closes the full, unfiltered list of options, for users who would rather browse than type. ArrowDown opens the same
46
+ list with or without it.</prop>
47
+ <prop name="clearable" type="boolean" default="false"
48
+ >Adds a clear button to the field that empties the input and clears the selection, reported as value-change with kind "clear". In multiple mode it clears
49
+ only the typed text; chosen items stay.</prop>
50
+ <prop name="help" type="string" default=""
51
+ >Help text for the field. When set, a help button appears beside the field and opens a tooltip with this text; while it is open, the input is described by
52
+ it.</prop>
24
53
  <state name="internalItems" type="list(object({ id: string, value: string, label: string, group?: string, disabled?: boolean }))" :value="[]"></state>
25
54
  <state name="raw" :value="''"></state>
26
55
  <state name="display" :value="''"></state>
@@ -41,6 +70,7 @@
41
70
  :value="{ status: 'pristine', touched: false, dirty: false, issues: [] }"
42
71
  ></state>
43
72
  <state name="uid" :value="''"></state>
73
+ <state name="submitted" :value="''"></state>
44
74
  <state
45
75
  name="groups"
46
76
  type="list(object({ name: string, role: group | presentation, label: string | null, rows: list(object({ id: string, value: string, label: string, group?: string, disabled: boolean, selected: boolean, index: integer })) }))"
@@ -113,12 +143,15 @@
113
143
  :aria-busy="validationStatus = 'pending'"
114
144
  .value="display"
115
145
  :placeholder="placeholder"
116
- :name="name"
117
146
  :disabled="disabled"
118
147
  :readonly="readonly"
119
- :required="required"
148
+ :required="required and not (multiple and internalItems[0])"
120
149
  >
121
- <button $if="clearable" type="button" data-combobox-action="clear" aria-label="Clear">
150
+ <input $if="name and not multiple" type="hidden" :name="name" :value="submitted" :disabled="disabled">
151
+ <template $if="name and multiple">
152
+ <input $each="item of internalItems" $key="item.id" type="hidden" :name="name" :value="item.value" :disabled="disabled">
153
+ </template>
154
+ <button $if="clearable" type="button" data-combobox-action="clear" aria-label="Clear" :disabled="disabled or readonly">
122
155
  <svg class="affordance-icon" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
123
156
  <path d="M7 7l10 10M17 7 7 17"></path>
124
157
  </svg>
@@ -130,6 +163,7 @@
130
163
  tabindex="-1"
131
164
  aria-label="Show suggestions"
132
165
  :aria-controls="format('%s-listbox', uid)"
166
+ :disabled="disabled or readonly"
133
167
  >
134
168
  <svg
135
169
  class="affordance-icon"
@@ -196,7 +230,7 @@
196
230
  </div>
197
231
  <slot name="footer"></slot>
198
232
  </div>
199
- <div class="message" :id="format('%s-validation', uid)" :hidden="not validation.issues.length">
233
+ <div class="message" :id="format('%s-validation', uid)" :hidden="not validation.issues[0]">
200
234
  <div $each="issue of validation.issues" $value="issue.message"></div>
201
235
  </div>
202
236
  <div class="sr-only" role="status" aria-live="polite" aria-atomic="true" $value="statusText"></div>
@@ -1,3 +1,4 @@
1
+ import { afterFormReset } from "../shared/form-reset.js";
1
2
  import { closeOverlay, createAnchoredSurface, openOverlay } from "../shared/overlay.js";
2
3
 
3
4
  const instances = new WeakMap();
@@ -288,7 +289,8 @@ export default function controller(host) {
288
289
  // Native constraints and the free-text policy; application validation stays in the form.
289
290
  const current = config();
290
291
  const result = { output: host.state.raw, issues: [] };
291
- if (host.state.required && !String(host.state.raw).trim()) result.issues = [...result.issues, { message: "A value is required." }];
292
+ const empty = host.state.multiple ? !items().length : !String(host.state.raw).trim();
293
+ if (host.state.required && empty) result.issues = [...result.issues, { message: "A value is required." }];
292
294
  else if (host.state.raw && host.state.selected === null && !current.allowFreeText && !current.allowCreate) result.issues = [...result.issues, { message: "Choose a suggestion." }];
293
295
  if (result.issues.some((issue) => issue.severity !== "warning")) result.output = undefined;
294
296
  if (!run.signal.aborted && alive) setValidation(result);
@@ -335,6 +337,8 @@ export default function controller(host) {
335
337
  host.state.groups = groups;
336
338
  host.state.creatable = canCreate();
337
339
  host.state.createIndex = rows.length;
340
+ // What a named combobox submits in single mode: the value, never the label the field shows.
341
+ host.state.submitted = String(host.state.selected ?? (host.state.allowFreeText ? host.state.raw ?? "" : ""));
338
342
  host.state.message = host.state.loading ? "Loading suggestions…"
339
343
  : host.state.lookupError || (!rows.length && !host.state.creatable ? "No suggestions." : "");
340
344
  const validation = host.state.validation ?? { status: "pristine", issues: [] };
@@ -388,6 +392,7 @@ export default function controller(host) {
388
392
  const option = event.target.closest?.('[role="option"][data-index]');
389
393
  if (option) { choose(Number(option.dataset.index), "pointer"); return; }
390
394
  const action = event.target.closest?.("[data-combobox-action]")?.dataset.comboboxAction;
395
+ if (action && (host.state.disabled || host.state.readonly)) return;
391
396
  if (action === "clear") { lastSelection = null; commit(null, "", null, "clear", "pointer"); close(); input.focus(); }
392
397
  else if (action === "disclosure") { host.state.expanded ? close() : open("disclosure"); input.focus(); }
393
398
  else if (field.contains(event.target) && !event.target.closest?.("button")) input.focus();
@@ -406,18 +411,22 @@ export default function controller(host) {
406
411
  const onTooltipOpen = (event) => { if (event.target.closest?.('[data-component~="ui-tooltip"]')) host.state.helpOpen = true; };
407
412
  const onTooltipClose = (event) => { if (event.target.closest?.('[data-component~="ui-tooltip"]')) host.state.helpOpen = false; };
408
413
 
409
- if (host.state.multiple) {
410
- host.state.selected = null;
411
- host.state.raw = host.state.query ?? "";
412
- } else {
413
- host.state.selected = host.state.value ?? null;
414
- host.state.raw = host.state.query ?? "";
415
- if (host.state.query === undefined && !host.state.raw && host.state.selected !== null) {
416
- host.state.raw = config().options?.find((row) => row.value === host.state.selected)?.label ?? host.state.selected;
414
+ // The selection the props describe: where the combobox starts, and where a form reset returns it.
415
+ const applyDefaults = () => {
416
+ if (host.state.multiple) {
417
+ host.state.selected = null;
418
+ host.state.raw = host.state.query ?? "";
419
+ } else {
420
+ host.state.selected = host.state.value ?? null;
421
+ host.state.raw = host.state.query ?? "";
422
+ if (host.state.query === undefined && !host.state.raw && host.state.selected !== null) {
423
+ host.state.raw = config().options?.find((row) => row.value === host.state.selected)?.label ?? host.state.selected;
424
+ }
425
+ if (host.state.query === undefined && host.state.selected !== null && host.state.raw === host.state.selected) awaitingLabel = host.state.selected;
417
426
  }
418
- if (host.state.query === undefined && host.state.selected !== null && host.state.raw === host.state.selected) awaitingLabel = host.state.selected;
419
- }
420
- host.state.display = host.state.raw;
427
+ host.state.display = host.state.raw;
428
+ };
429
+ applyDefaults();
421
430
  initialRaw = host.state.raw;
422
431
  host.state.validation = { status: "pristine", touched: false, dirty: false, issues: [] };
423
432
  surface = createAnchoredSurface(popup, { anchor: field, placement: "bottom-start" });
@@ -437,6 +446,15 @@ export default function controller(host) {
437
446
  host.state.display = label;
438
447
  awaitingLabel = null;
439
448
  };
449
+ // Like a native control, a reset reports no change; the browser has already emptied the input.
450
+ const stopReset = afterFormReset(input, () => {
451
+ close();
452
+ lastSelection = null;
453
+ host.state.internalItems = Array.isArray(host.state.items) ? host.state.items : [];
454
+ applyDefaults();
455
+ input.value = host.state.display;
456
+ resetValidation();
457
+ });
440
458
  const observer = new MutationObserver(relabel);
441
459
  observer.observe(authored, { childList: true, subtree: true, characterData: true, attributes: true });
442
460
  let lastItems = host.state.items;
@@ -456,6 +474,7 @@ export default function controller(host) {
456
474
  return () => {
457
475
  alive = false;
458
476
  stop();
477
+ stopReset();
459
478
  observer.disconnect();
460
479
  for (const [name, listener] of Object.entries(listeners)) element.removeEventListener(name, listener);
461
480
  close();
@@ -1,7 +1,10 @@
1
1
  <template component="ui-container">
2
2
  <defs>
3
- <prop name="measure" type="narrow | wide">measure token.</prop>
4
- <prop name="gutters" type="s | m | l">gutters token.</prop>
3
+ <prop name="measure" type="narrow | wide"
4
+ >The container's maximum width, in characters of the current font: narrow is 45ch, wide is 80ch, and unset is 65ch, a comfortable reading line. The
5
+ container centres itself when its parent is wider. The --ui-container-measure custom property, when set, overrides it.</prop>
6
+ <prop name="gutters" type="s | m | l"
7
+ >Inline padding on both sides, which keeps content off the edges of a narrow screen: s is 0.75rem, m is 1rem (also the default), and l is 1.5rem.</prop>
5
8
  </defs>
6
9
  <div><slot></slot></div>
7
10
  <style>
@@ -1,9 +1,12 @@
1
1
  <template component="ui-disclosure" controller="./ui-disclosure.js">
2
2
  <defs>
3
3
  <prop name="density" type="comfortable | compact" default="comfortable">Compact trades padding and type size for rows that fit more on screen.</prop>
4
- <prop name="summary" type="string" default="Details">summary token.</prop>
5
- <prop name="open" type="boolean" default="false">open token.</prop>
6
- <prop name="disabled" type="boolean" default="false">disabled token.</prop>
4
+ <prop name="summary" type="string" default="Details"
5
+ >The text of the trigger row that opens and closes the disclosure. It is the trigger button's accessible name, so name what the content is.</prop>
6
+ <prop name="open" type="boolean" default="false"
7
+ >Whether the disclosure is open initially or when set externally. The trigger toggles it from there, reporting each change as an open or close event.</prop>
8
+ <prop name="disabled" type="boolean" default="false"
9
+ >Disables the trigger and mutes its text, so the disclosure keeps its current state and the user cannot toggle it.</prop>
7
10
  <state name="internalOpen" :value="false"></state>
8
11
  <state name="contentId" :value="''"></state>
9
12
  <event
@@ -1,6 +1,8 @@
1
1
  <template component="ui-editable" controller="./ui-editable.js">
2
2
  <defs>
3
- <prop name="value" type="string" default="">The displayed and initial editable value.</prop>
3
+ <prop name="value" type="string" default=""
4
+ >The displayed value, and the text the editor starts from. It is not a form field: nothing is submitted with a form, so a form that needs the value copies
5
+ it from the change event into a field of its own.</prop>
4
6
  <prop name="label" type="string" default="Edit value">Accessible name for the editor.</prop>
5
7
  <prop name="edit" type="boolean" default="false">Whether edit mode is active initially or when set externally.</prop>
6
8
  <prop name="disabled" type="boolean" default="false">Prevents entering or changing edit mode.</prop>
@@ -31,10 +31,11 @@ export default function controller(host) {
31
31
  input.tabIndex = host.state.internalEdit ? 0 : -1;
32
32
  };
33
33
 
34
- const setEditing = (next, reason, trigger) => {
34
+ const setEditing = (next, reason, trigger, returnFocus = true) => {
35
35
  if (host.state.disabled || Boolean(host.state.internalEdit) === next) return;
36
- // Measured before the input is disabled, which drops focus to the body.
37
- const focusWasInside = element.contains(document.activeElement);
36
+ // Measured before the input is disabled, which drops focus to the body. A press elsewhere has
37
+ // not moved focus yet, so it says not to return it.
38
+ const focusWasInside = returnFocus && element.contains(document.activeElement);
38
39
  if (next) host.state.draft = host.state.internalValue;
39
40
  host.state.internalEdit = next;
40
41
  apply();
@@ -51,11 +52,11 @@ export default function controller(host) {
51
52
  input.select();
52
53
  });
53
54
  };
54
- const commit = (trigger) => {
55
+ const commit = (trigger, returnFocus) => {
55
56
  const previousValue = String(host.state.internalValue ?? "");
56
57
  const value = String(host.state.draft ?? "");
57
58
  host.state.internalValue = value;
58
- setEditing(false, "commit", trigger);
59
+ setEditing(false, "commit", trigger, returnFocus);
59
60
  if (value !== previousValue) host.dispatch("change", { value, previousValue, trigger });
60
61
  };
61
62
  const cancel = (reason, trigger) => {
@@ -87,7 +88,7 @@ export default function controller(host) {
87
88
  };
88
89
  // Leaving the field saves, as with other in-place editors; Escape is the way to discard.
89
90
  const onDocumentPointerdown = (event) => {
90
- if (host.state.internalEdit && !event.composedPath().includes(element)) commit("pointer");
91
+ if (host.state.internalEdit && !event.composedPath().includes(element)) commit("pointer", false);
91
92
  };
92
93
  // Only the input's own blur counts: hiding the display button during the swap also moves focus,
93
94
  // and must not end the edit. Tabbing to another control saves; clicks away are handled above.
@@ -3,7 +3,9 @@
3
3
  <prop name="open" type="boolean" default="false">Whether the menu is open.</prop>
4
4
  <prop name="query" type="string" default="">The text typed after the @.</prop>
5
5
  <prop name="items" type="list(object({ id: string, label: string, detail?: string, initials?: string }))">The people the menu offers.</prop>
6
- <prop name="selectedIndex" type="integer" default="0">The highlighted person.</prop>
6
+ <prop name="selectedIndex" type="integer" default="0"
7
+ >The zero-based index of the highlighted person, capped at the last one shown, and announced through aria-activedescendant. The menu does not handle arrow
8
+ keys itself: the editor sets this as the user moves, while hovering a person highlights them and reports a highlight event.</prop>
7
9
  <prop name="loading" type="boolean" default="false">Whether a search is in progress.</prop>
8
10
  <prop
9
11
  name="anchorRect"
@@ -4,7 +4,9 @@
4
4
  <prop name="open" type="boolean" default="false">Whether the menu is open.</prop>
5
5
  <prop name="query" type="string" default="">The text typed after the slash.</prop>
6
6
  <prop name="items" type="list(object({ title: string, description: string, icon: string }))">The blocks the menu offers.</prop>
7
- <prop name="selectedIndex" type="integer" default="0">The highlighted item.</prop>
7
+ <prop name="selectedIndex" type="integer" default="0"
8
+ >The zero-based index of the highlighted item. The menu does not handle arrow keys itself: the editor sets this as the user moves, while hovering an item
9
+ highlights it and reports a highlight event.</prop>
8
10
  <prop
9
11
  name="anchorRect"
10
12
  type="object({ left?: number, top?: number, right?: number, bottom?: number, x?: number, y?: number, width?: number, height?: number }) | null"
@@ -1,8 +1,10 @@
1
1
  <template component="ui-floating-action-button">
2
2
  <defs>
3
- <prop name="disabled" type="boolean" default="false">disabled token.</prop>
4
- <prop name="mobileOnly" type="boolean" default="false">mobileOnly token.</prop>
5
- <prop name="label" type="string" default="">label token.</prop>
3
+ <prop name="disabled" type="boolean" default="false">Disables the button, so it cannot be pressed or focused, and fades it to 60% opacity.</prop>
4
+ <prop name="mobileOnly" type="boolean" default="false"
5
+ >Hides the button on viewports 768px wide and wider, for a shortcut that only phones need. Make sure the action is reachable another way at those
6
+ widths.</prop>
7
+ <prop name="label" type="string" default="">The button's accessible name. The button usually holds only an icon, so name the action it performs.</prop>
6
8
  </defs>
7
9
  <button type="button" :aria-label="label" :disabled="disabled"><slot></slot></button>
8
10
  <style>
@@ -1,8 +1,14 @@
1
1
  <template component="ui-form-field" controller="./ui-form-field.js">
2
2
  <defs>
3
- <prop name="invalid" type="boolean" default="false">invalid token.</prop>
4
- <prop name="disabled" type="boolean" default="false">disabled token.</prop>
5
- <prop name="required" type="boolean" default="false">required token.</prop>
3
+ <prop name="invalid" type="boolean" default="false"
4
+ >Marks the field's control invalid by setting aria-invalid on it. Pair it with content in the error slot, which the field already links to the control as
5
+ its description.</prop>
6
+ <prop name="disabled" type="boolean" default="false"
7
+ >Disables the field's control: the first input, textarea, or select inside it. The field writes this to the control whenever it updates, so set it here
8
+ rather than on the control.</prop>
9
+ <prop name="required" type="boolean" default="false"
10
+ >Marks the field's control required, for native form validation. Like disabled, it is written to the control, so set it here rather than on the control. It
11
+ adds no visual marker; show one in the label if the form needs it.</prop>
6
12
  </defs>
7
13
  <div>
8
14
  <div class="label"><slot name="label"></slot></div>
@@ -1,7 +1,11 @@
1
1
  <template component="ui-grid">
2
2
  <defs>
3
- <prop name="gap" type="xs | s | m | l | xl">gap token.</prop>
4
- <prop name="min" type="sm | md | lg">min token.</prop>
3
+ <prop name="gap" type="xs | s | m | l | xl"
4
+ >Space between cells, on the spacing scale: xs is 0.5rem, s 0.75rem, m 1rem (also the default), l 1.5rem, and xl 2rem. The --ui-grid-gap custom property,
5
+ when set, overrides it.</prop>
6
+ <prop name="min" type="sm | md | lg"
7
+ >The narrowest a column may get before the grid drops to fewer columns: sm is 12rem (also the default), md is 16rem, and lg is 20rem. The grid fits as many
8
+ equal columns as its width allows, and a single column never overflows a narrower container.</prop>
5
9
  </defs>
6
10
  <div><slot></slot></div>
7
11
  <style>
@@ -1,6 +1,8 @@
1
1
  <template component="ui-icon" controller="./ui-icon.js">
2
2
  <defs>
3
- <prop name="name" type="string" default="">The icon's name.</prop>
3
+ <prop name="name" type="string" default=""
4
+ >Which icon to draw, by its name in Looma's icon set (LOOMA_ICONS), such as "align-left". An unknown or empty name draws nothing. The icon is hidden from
5
+ assistive technology, so name the control it sits in.</prop>
4
6
  <state
5
7
  name="shapes"
6
8
  type="list(object({ tag: path | circle | rect, d?: string, cx?: string, cy?: string, r?: string, x?: string, y?: string, width?: string, height?: string, rx?: string }))"
@@ -1,10 +1,17 @@
1
1
  <template component="ui-icon-button" controller="./ui-icon-button.js">
2
2
  <defs>
3
- <prop name="disabled" type="boolean" default="false">disabled token.</prop>
3
+ <prop name="disabled" type="boolean" default="false"
4
+ >Disables the button, so it cannot be pressed or focused, and greys the icon; outline and solid also take the disabled surface.</prop>
4
5
  <prop name="label" type="string">Accessible name for the icon.</prop>
5
- <prop name="size" type="sm | md | lg" default="md">size token.</prop>
6
- <prop name="variant" type="ghost | outline | solid" default="ghost">variant token.</prop>
7
- <prop name="anticipatory" type="boolean" default="false">anticipatory token.</prop>
6
+ <prop name="size" type="sm | md | lg" default="md"
7
+ >Button and icon size together: sm is 1.75rem with a small icon, md is 2rem, and lg is 2.5rem with a large icon. On touch, every size gets an invisible hit
8
+ area of at least 44px.</prop>
9
+ <prop name="variant" type="ghost | outline | solid" default="ghost"
10
+ >Emphasis. ghost shows only the icon until it is hovered, for toolbars and dense rows; outline adds a border on the default surface; solid fills with the
11
+ accent colour, for the one primary action in a group.</prop>
12
+ <prop name="anticipatory" type="boolean" default="false"
13
+ >Keeps the button out of sight until it is wanted: at rest a fine pointer sees only a small guide dot, and the button appears on hover, on focus, or when
14
+ the pointer comes near inside a ui-affordance-scope. Touch devices and the solid variant always show it.</prop>
8
15
  <prop name="round" type="boolean" default="false">Use a fully circular shape.</prop>
9
16
  </defs>
10
17
  <button :disabled="disabled" type="button" :aria-label="label"><slot></slot></button>