{
 "name": "xoong",
 "version": "0.0.54",
 "note": "Generated by tools/site/reference-data.mjs from packages/core/src/syntax.js, packages/cli/src/html-data.js, the index.d.ts files and tools/site/reference-supplement.mjs. Do not edit.",
 "groups": [
  {
   "id": "elements",
   "label": "Elements",
   "summary": "Every xo-* element: what it does, its attributes, its events and an example.",
   "count": 47,
   "entries": [
    {
     "id": "xo-view",
     "name": "xo-view",
     "kind": "element",
     "package": "@xoong/data",
     "import": "import '@xoong/data/view';",
     "summary": "Declare a screen: `get` names the GET operation to read.",
     "description": "Declare a screen: `get` names the GET operation to read. Children are bound to the result. Add `live` to apply server-sent updates.",
     "attributes": [
      {
       "name": "get",
       "type": "operation",
       "default": null,
       "values": null,
       "description": "A path (\"/users\") or operationId."
      },
      {
       "name": "live",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Apply live updates from the event stream."
      },
      {
       "name": "url-params",
       "type": "list",
       "default": null,
       "values": null,
       "description": "Comma-separated param names kept in the page URL (shareable, survive reload and Back): `status,q,sort,offset`."
      },
      {
       "name": "p-<name>",
       "type": "text",
       "default": null,
       "values": null,
       "description": "The value of the path parameter \"name\" of the <xo-view get> operation (p-id=\"42\"; p-id=\"?id\" reads ?id= from the page URL; p-id=\":id\" reads a route parameter)."
      }
     ],
     "events": [
      {
       "name": "xo-done",
       "id": "event-xo-done",
       "summary": "An action succeeded."
      },
      {
       "name": "xo-error",
       "id": "event-xo-error",
       "summary": "A load or an action failed."
      },
      {
       "name": "xo-invalid",
       "id": "event-xo-invalid",
       "summary": "A form was not sent because it is invalid."
      },
      {
       "name": "xo-notice",
       "id": "event-xo-notice",
       "summary": "A message for the toast."
      },
      {
       "name": "xo-go",
       "id": "event-xo-go",
       "summary": "An action asked to go to another page."
      }
     ],
     "example": {
      "lang": "html",
      "code": "<xo-view get=\"listPeople\" url-params=\"name,role\">\n  <p xo-if=\"$loading\">Loading…</p>\n  <p xo-if=\"$error\">{$error.detail}</p>\n  <ul>\n    <li xo-each=\"items\">{name} · {email}</li>\n  </ul>\n</xo-view>"
     },
     "guide": "data.html",
     "keywords": "screen page read fetch load data list query live stream"
    },
    {
     "id": "xo-table",
     "name": "xo-table",
     "kind": "element",
     "package": "@xoong/ui",
     "import": "import '@xoong/ui';",
     "summary": "A table for the list in the enclosing <xo-view get>.",
     "description": "A table for the list in the enclosing <xo-view get>. Children become the actions cell of each row.",
     "attributes": [
      {
       "name": "of",
       "type": "path",
       "default": "items",
       "values": null,
       "description": "List field of the response (default `items`)."
      },
      {
       "name": "columns",
       "type": "list",
       "default": null,
       "values": null,
       "description": "Comma-separated fields, optionally `field:Label`."
      },
      {
       "name": "sortable",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Header buttons sort on the server through the `sort` query parameter."
      },
      {
       "name": "selectable",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "A checkbox per row and a select-all box; slot=\"bulk\" children act on the ticked rows."
      },
      {
       "name": "filters",
       "type": "list",
       "default": null,
       "values": null,
       "description": "Comma-separated fields that get a search box above the table; typing sets the query parameter of the same name."
      },
      {
       "name": "page-size",
       "type": "number",
       "default": null,
       "values": null,
       "description": "Rows per page: sets `limit`, Previous/Next change `offset`. The response needs a `total`."
      },
      {
       "name": "cards",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Draw each row as a card in a narrow pane (a theme does it; the table gets the class xo-cards). Cells carry data-label."
      },
      {
       "name": "title",
       "type": "text",
       "default": "the first",
       "values": null,
       "description": "The column that is the card title (default: the first)."
      },
      {
       "name": "status",
       "type": "text",
       "default": null,
       "values": null,
       "description": "The column that is the status badge of the card (data-status)."
      },
      {
       "name": "empty",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Text shown when the list has no rows."
      },
      {
       "name": "page-sizes",
       "type": "list",
       "default": null,
       "values": null,
       "description": "Comma-separated rows-per-page choices: a select beside the pager (needs `page-size`)."
      },
      {
       "name": "column-picker",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "A \"Columns\" dropdown at the right end of the search row: tick a column to show it, untick to hide it (the last visible one stays). Fires `xo-columns-change` with `{ visible, hidden }`."
      },
      {
       "name": "hidden-columns",
       "type": "list",
       "default": null,
       "values": null,
       "description": "With `column-picker`: comma-separated fields that start hidden."
      },
      {
       "name": "columns-key",
       "type": "text",
       "default": null,
       "values": null,
       "description": "With `column-picker`: a name under which the choice is remembered in this browser (localStorage)."
      }
     ],
     "events": [
      {
       "name": "xo-columns-change",
       "id": "event-xo-columns-change",
       "summary": "The person showed or hid a column of the table."
      }
     ],
     "example": {
      "lang": "html",
      "code": "<xo-view get=\"listPeople\">\n  <xo-table columns=\"name,email:E-mail,role\" sortable filters=\"name:Search by name\" selectable page-size=\"10\" empty=\"Nobody here\">\n    <span slot=\"role\" class=\"badge\">{role}</span>\n    <button xo-click=\"deletePerson\" xo-confirm=\"Delete {name}?\" xo-notice=\"Deleted\">Delete</button>\n    <button slot=\"bulk\" xo-click=\"deletePerson\" xo-selected>Delete ticked</button>\n  </xo-table>\n</xo-view>"
     },
     "guide": "data.html",
     "keywords": "grid list rows columns sort filter search page pagination select bulk cards"
    },
    {
     "id": "xo-form",
     "name": "xo-form",
     "kind": "element",
     "package": "@xoong/ui",
     "import": "import '@xoong/ui';",
     "summary": "A form generated from the request schema of an operation.",
     "description": "A form generated from the request schema of an operation.",
     "attributes": [
      {
       "name": "op",
       "type": "operation",
       "default": null,
       "values": null,
       "description": "The operation to submit to."
      },
      {
       "name": "submit",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Text of the submit button."
      }
     ],
     "events": [
      {
       "name": "xo-done",
       "id": "event-xo-done",
       "summary": "An action succeeded."
      },
      {
       "name": "xo-error",
       "id": "event-xo-error",
       "summary": "A load or an action failed."
      },
      {
       "name": "xo-invalid",
       "id": "event-xo-invalid",
       "summary": "A form was not sent because it is invalid."
      },
      {
       "name": "xo-dirty",
       "id": "event-xo-dirty",
       "summary": "A form gained or lost unsaved changes."
      },
      {
       "name": "xo-reveal",
       "id": "event-xo-reveal",
       "summary": "The form is about to focus an invalid field."
      },
      {
       "name": "xo-form-ready",
       "id": "event-xo-form-ready",
       "summary": "The form model exists."
      }
     ],
     "example": {
      "lang": "html",
      "code": "<xo-form op=\"createPerson\" submit=\"Add person\" xo-notice=\"Person added\"></xo-form>"
     },
     "guide": "forms.html",
     "keywords": "input fields validation submit schema generate create edit"
    },
    {
     "id": "xo-dialog",
     "name": "xo-dialog",
     "kind": "element",
     "package": "@xoong/ui",
     "import": "import '@xoong/ui';",
     "summary": "A modal on the native <dialog>.",
     "description": "A modal on the native <dialog>. Open it with xo-open, close it with xo-close or by submitting a form inside. With `heading` it is a titled modal: a header with the title and a Close button, the content as the body, and the elements marked `slot=\"footer\"` as the footer.",
     "attributes": [
      {
       "name": "heading",
       "type": "text",
       "default": null,
       "values": null,
       "description": "The title: makes it a titled modal (header, body, footer)."
      },
      {
       "name": "size",
       "type": "choice",
       "default": "40rem",
       "values": [
        "sm",
        "wide",
        "full"
       ],
       "description": "`sm`, `wide` or `full` (the default is 40rem)."
      },
      {
       "name": "dismiss",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Close it on a click outside the dialog."
      }
     ],
     "events": [],
     "example": {
      "lang": "html",
      "code": "<button xo-open=\"#add\">Add person</button>\n<xo-dialog id=\"add\" heading=\"Add person\" size=\"wide\" dismiss>\n  <xo-form op=\"createPerson\" submit=\"Add\"></xo-form>\n  <button slot=\"footer\" xo-close>Cancel</button>\n</xo-dialog>"
     },
     "guide": "dialogs.html",
     "keywords": "modal popup window overlay confirm sheet"
    },
    {
     "id": "xo-toast",
     "name": "xo-toast",
     "kind": "element",
     "package": "@xoong/ui",
     "import": "import '@xoong/ui';",
     "summary": "Shows the message of the latest xo-error event on the page (not when the event was prevented with `preventDefault()`, or its `handled` is true because a view shows `$error` itself); hides on the next xo-done or after `timeout` seconds.",
     "description": "Shows the message of the latest xo-error event on the page (not when the event was prevented with `preventDefault()`, or its `handled` is true because a view shows `$error` itself); hides on the next xo-done or after `timeout` seconds. Set `detail.action` (`{ label, run }`, for example Retry) and `detail.timeout` on the error and the toast shows that button, the same way an `xo-notice` does.",
     "attributes": [
      {
       "name": "timeout",
       "type": "number",
       "default": "6",
       "values": null,
       "description": "Seconds before it hides (default 6, 0 = until the next success)."
      }
     ],
     "events": [],
     "example": {
      "lang": "html",
      "code": "<xo-toast timeout=\"8\"></xo-toast>"
     },
     "guide": "dialogs.html",
     "keywords": "notification snackbar message notice alert undo error success"
    },
    {
     "id": "xo-router",
     "name": "xo-router",
     "kind": "element",
     "package": "@xoong/ui",
     "import": "import '@xoong/ui';",
     "summary": "A History-API router declared in markup: one <template xo-route> per route, optional <template xo-layout> shells.",
     "description": "A History-API router declared in markup: one <template xo-route> per route, optional <template xo-layout> shells. Links are intercepted, aria-current marks the active one. Guards and leave-vetoes are JavaScript (`router.guard`, `router.beforeNavigate`).",
     "attributes": [
      {
       "name": "base",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Path prefix the app lives under, for example `/admin`."
      },
      {
       "name": "base-links",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "A root-relative link such as `/users` inside the router means `/admin/users`, so the same screens mount under any base. `rel=\"external\"` leaves the app."
      }
     ],
     "events": [
      {
       "name": "xo-navigate",
       "id": "event-xo-navigate",
       "summary": "The router moved to another location."
      },
      {
       "name": "xo-error",
       "id": "event-xo-error",
       "summary": "A load or an action failed."
      }
     ],
     "example": {
      "lang": "html",
      "code": "<xo-router base=\"/admin\" base-links>\n  <template xo-layout=\"shell\">\n    <nav><a href=\"/users\">Users</a></nav>\n    <main xo-outlet></main>\n  </template>\n  <template xo-route=\"/users/:id\" layout=\"shell\" title=\"User\">\n    <xo-view get=\"getPerson\" p-id=\":id\"><h1>{name}</h1></xo-view>\n  </template>\n  <template xo-route=\"*\">Not found</template>\n</xo-router>"
     },
     "guide": "arch-routing.html",
     "keywords": "route navigation spa history links layout guard base url"
    },
    {
     "id": "xo-pager",
     "name": "xo-pager",
     "kind": "element",
     "package": "@xoong/ui",
     "import": "import '@xoong/ui';",
     "summary": "Previous and Next (and the \"1–12 of 48\" text) for any list in the enclosing <xo-view>: a tile grid, a thread list.",
     "description": "Previous and Next (and the \"1–12 of 48\" text) for any list in the enclosing <xo-view>: a tile grid, a thread list. The same pager <xo-table page-size> draws.",
     "attributes": [
      {
       "name": "page-size",
       "type": "number",
       "default": null,
       "values": null,
       "description": "Rows per page: sets `limit`; the buttons change `offset`. The response needs a `total`."
      },
      {
       "name": "page-sizes",
       "type": "list",
       "default": null,
       "values": null,
       "description": "Comma-separated rows-per-page choices."
      }
     ],
     "events": [],
     "example": {
      "lang": "html",
      "code": "<xo-view get=\"listPeople\">\n  <ul><li xo-each=\"items\">{name}</li></ul>\n  <xo-pager page-size=\"12\" page-sizes=\"12,24,48\"></xo-pager>\n</xo-view>"
     },
     "guide": "recipes.html",
     "keywords": "pagination page next previous paging offset limit total"
    },
    {
     "id": "xo-tabs",
     "name": "xo-tabs",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "Accessible tabs from the sections that have xo-tab.",
     "description": "Accessible tabs from the sections that have xo-tab. Panels stay in the DOM, so form controls on other tabs still count; xo-reveal selects the tab of an invalid field.",
     "attributes": [
      {
       "name": "value",
       "type": "text",
       "default": "the first",
       "values": null,
       "description": "Key of the selected tab (default: the first)."
      },
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name of the tab list."
      }
     ],
     "events": [
      {
       "name": "xo-tab-change",
       "id": "event-xo-tab-change",
       "summary": "Another tab was selected."
      }
     ],
     "example": {
      "lang": "html",
      "code": "<xo-tabs label=\"Person\">\n  <section xo-tab=\"General\" id=\"general\"><label>Name <input name=\"name\"></label></section>\n  <section xo-tab=\"Topics\" id=\"topics\">…</section>\n</xo-tabs>"
     },
     "guide": "tabs.html",
     "keywords": "tab panel section"
    },
    {
     "id": "xo-menu",
     "name": "xo-menu",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "An action menu: a trigger button and items (buttons or links) with arrow keys, Home/End, type-ahead and Escape.",
     "description": "An action menu: a trigger button and items (buttons or links) with arrow keys, Home/End, type-ahead and Escape. Drawn in the top layer where the browser supports the Popover API. Items with a `value` make it a choice menu: `xo-menu-select` fires with the value.",
     "attributes": [
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name of the menu."
      },
      {
       "name": "value",
       "type": "text",
       "default": null,
       "values": null,
       "description": "The chosen value of a choice menu (the items that have a `value` attribute are radio items)."
      }
     ],
     "events": [
      {
       "name": "xo-menu-select",
       "id": "event-xo-menu-select",
       "summary": "An item of a choice menu was chosen."
      },
      {
       "name": "xo-menu-open",
       "id": "event-xo-menu-open",
       "summary": "A menu opened."
      },
      {
       "name": "xo-menu-close",
       "id": "event-xo-menu-close",
       "summary": "A menu closed."
      }
     ],
     "example": {
      "lang": "html",
      "code": "<xo-menu label=\"Actions\">\n  <button xo-menu-trigger>Actions</button>\n  <button>Duplicate</button>\n  <a href=\"/people/1\">Open</a>\n</xo-menu>"
     },
     "guide": "menu.html",
     "keywords": "dropdown actions context popup choice radio"
    },
    {
     "id": "xo-combobox",
     "name": "xo-combobox",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A form control to pick from a list you can filter by typing.",
     "description": "A form control to pick from a list you can filter by typing. <option> children are the choices; `multiple` shows chips; the value is a string, or an array with `multiple`.",
     "attributes": [
      {
       "name": "name",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Form field path."
      },
      {
       "name": "multiple",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Pick several values (chips)."
      },
      {
       "name": "creatable",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Allow a value that is not in the list."
      },
      {
       "name": "remote",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Do not filter locally: listen to xo-search and set `el.options`."
      },
      {
       "name": "options-op",
       "type": "operation",
       "default": null,
       "values": null,
       "description": "A GET operation whose list is the options (needs @xoong/data/view). With `option-value` and `option-label` (paths in each item, default id and name) and `p-<name>` parameters; a mutation of the list refreshes the options."
      },
      {
       "name": "option-value",
       "type": "path",
       "default": "id",
       "values": null,
       "description": "With options-op: the path in each item that is the value (default `id`)."
      },
      {
       "name": "option-label",
       "type": "path",
       "default": "name",
       "values": null,
       "description": "With options-op: the path in each item that is the label (default `name`)."
      },
      {
       "name": "placeholder",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Hint text of the empty field."
      },
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name when there is no <label>."
      },
      {
       "name": "required",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "The field must be filled before the form can be sent."
      },
      {
       "name": "disabled",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Not available: not editable, not sent with the form."
      },
      {
       "name": "readonly",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Shown and sent, but not changeable."
      }
     ],
     "events": [
      {
       "name": "xo-search",
       "id": "event-xo-search",
       "summary": "The person typed in a combobox that does not filter by itself."
      }
     ],
     "example": {
      "lang": "html",
      "code": "<xo-combobox name=\"topics\" label=\"Topics\" multiple>\n  <option value=\"web\">Web</option>\n  <option value=\"data\">Data</option>\n  <option value=\"design\">Design</option>\n</xo-combobox>"
     },
     "guide": "combobox.html",
     "keywords": "select dropdown autocomplete typeahead picker chips multiple options"
    },
    {
     "id": "xo-tags",
     "name": "xo-tags",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A free-text tag input: type and press Enter or a comma to add a chip; Backspace removes the last.",
     "description": "A free-text tag input: type and press Enter or a comma to add a chip; Backspace removes the last. Value: an array of strings.",
     "attributes": [
      {
       "name": "name",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Form field path."
      },
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name when there is no <label>."
      },
      {
       "name": "placeholder",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Hint text of the empty field."
      },
      {
       "name": "required",
       "type": "text",
       "default": null,
       "values": null,
       "description": "The field must be filled before the form can be sent."
      },
      {
       "name": "disabled",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Not available: not editable, not sent with the form."
      },
      {
       "name": "readonly",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Shown and sent, but not changeable."
      }
     ],
     "events": [],
     "example": {
      "lang": "html",
      "code": "<xo-tags name=\"keywords\" label=\"Keywords\" placeholder=\"Add a keyword\"></xo-tags>"
     },
     "guide": "combobox.html",
     "keywords": "chips keywords labels free text input"
    },
    {
     "id": "xo-richtext",
     "name": "xo-richtext",
     "kind": "element",
     "package": "@xoong/richtext",
     "import": "import '@xoong/richtext';",
     "summary": "A rich text editor form control: toolbar, headings, bold, italic, lists, quote, links.",
     "description": "A rich text editor form control: toolbar, headings, bold, italic, lists, quote, links. The value is sanitised HTML (p, h2, h3, strong, em, ul, ol, li, blockquote, br, a, img). `xo-pick-media` (cancelable) asks the app for an image.",
     "attributes": [
      {
       "name": "name",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Form field path."
      },
      {
       "name": "tools",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Space-separated tools: undo redo style bold italic underline ul ol quote link image (h2 as a toggle)."
      },
      {
       "name": "toolbar",
       "type": "choice",
       "default": null,
       "values": [
        "full",
        "compact"
       ],
       "description": "`full` or `compact` forces a toolbar size; by default the editor's own width decides (full from 580px)."
      },
      {
       "name": "media",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Show the image tool; it fires xo-pick-media."
      },
      {
       "name": "placeholder",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Hint text of the empty editor."
      },
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name of the editor."
      },
      {
       "name": "maxlength",
       "type": "number",
       "default": null,
       "values": null,
       "description": "Maximum number of text characters."
      },
      {
       "name": "required",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "The field must be filled before the form can be sent."
      },
      {
       "name": "disabled",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Not available: not editable, not sent with the form."
      },
      {
       "name": "readonly",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Shown and sent, but not changeable."
      }
     ],
     "events": [
      {
       "name": "xo-pick-media",
       "id": "event-xo-pick-media",
       "summary": "The image tool of the editor was used."
      },
      {
       "name": "xo-richtext-expand",
       "id": "event-xo-richtext-expand",
       "summary": "The editor opened in its large dialog."
      },
      {
       "name": "xo-richtext-done",
       "id": "event-xo-richtext-done",
       "summary": "The large editor dialog was closed."
      }
     ],
     "example": {
      "lang": "html",
      "code": "<xo-richtext name=\"bio\" label=\"Bio\" toolbar=\"compact\" maxlength=\"2000\" media></xo-richtext>"
     },
     "guide": "richtext.html",
     "keywords": "editor wysiwyg html formatting bold italic image toolbar"
    },
    {
     "id": "xo-carousel",
     "name": "xo-carousel",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A row of slides you move through: cards, a banner, a gallery.",
     "description": "A row of slides you move through: cards, a banner, a gallery. The children are the slides; they sit in a scroll container with scroll snap, so swiping is native. WAI-ARIA carousel pattern, slides out of view are inert.",
     "attributes": [
      {
       "name": "items",
       "type": "number",
       "default": "1",
       "values": null,
       "description": "Slides visible at once (default 1; 1.15 lets the next peek). Also --xo-carousel-items in CSS."
      },
      {
       "name": "dots",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "A button per position; a slide with data-thumb=\"url\" shows that picture instead."
      },
      {
       "name": "counter",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Show \"1 / 5\"."
      },
      {
       "name": "no-arrows",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Hide the previous and next buttons."
      },
      {
       "name": "loop",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Next at the end returns to the first slide."
      },
      {
       "name": "autoplay",
       "type": "number",
       "default": null,
       "values": null,
       "description": "Move on by itself every n milliseconds (pauses on hover and focus; off with reduced motion; adds a pause button)."
      },
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name of the carousel."
      }
     ],
     "events": [
      {
       "name": "xo-carousel-change",
       "id": "event-xo-carousel-change",
       "summary": "The carousel moved to another slide."
      }
     ],
     "example": {
      "lang": "html",
      "code": "<xo-carousel label=\"Services\" items=\"3\" dots counter loop>\n  <article>First</article>\n  <article>Second</article>\n  <article>Third</article>\n  <article>Fourth</article>\n</xo-carousel>"
     },
     "guide": "carousel.html",
     "keywords": "slider slides gallery banner cards swipe slideshow"
    },
    {
     "id": "xo-switch",
     "name": "xo-switch",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "One setting on or off (WAI-ARIA switch).",
     "description": "One setting on or off (WAI-ARIA switch). A form control with a boolean value; the text inside is the visible label (`label.xo-switch-row`); `change` fires when the person toggles it.",
     "attributes": [
      {
       "name": "name",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Form field path."
      },
      {
       "name": "checked",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Start on."
      },
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name."
      },
      {
       "name": "size",
       "type": "choice",
       "default": null,
       "values": [
        "sm"
       ],
       "description": "`sm` for the compact switch."
      },
      {
       "name": "busy",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "A save is running: aria-busy, clicks ignored."
      },
      {
       "name": "disabled",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Not available."
      },
      {
       "name": "readonly",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Shown but not changeable."
      },
      {
       "name": "required",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "The field must be filled before the form can be sent."
      }
     ],
     "events": [],
     "example": {
      "lang": "html",
      "code": "<xo-switch name=\"notify\" checked>Email me about new comments</xo-switch>"
     },
     "guide": "more-controls.html#choose",
     "keywords": "toggle checkbox on off boolean setting"
    },
    {
     "id": "xo-date",
     "name": "xo-date",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A date field with a calendar popover.",
     "description": "A date field with a calendar popover. The value is ISO `YYYY-MM-DD` (with `range`: `{ start, end }`); the field shows the locale format and accepts typing.",
     "attributes": [
      {
       "name": "name",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Form field path."
      },
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name."
      },
      {
       "name": "min",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Earliest date, `YYYY-MM-DD`."
      },
      {
       "name": "max",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Latest date, `YYYY-MM-DD`."
      },
      {
       "name": "range",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Pick a start and an end (Clear and Apply in the calendar)."
      },
      {
       "name": "week-start",
       "type": "number",
       "default": "the locale",
       "values": null,
       "description": "First day of the week, 1 = Monday ... 7 = Sunday (default: the locale)."
      },
      {
       "name": "placeholder",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Hint text of the empty field."
      },
      {
       "name": "required",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "The field must be filled before the form can be sent."
      },
      {
       "name": "disabled",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Not available: not editable, not sent with the form."
      },
      {
       "name": "readonly",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Shown and sent, but not changeable."
      }
     ],
     "events": [],
     "example": {
      "lang": "html",
      "code": "<xo-date name=\"publishedAt\" label=\"Publish on\" value=\"2026-11-25\"></xo-date>"
     },
     "guide": "more-controls.html#dates",
     "keywords": "calendar datepicker day range"
    },
    {
     "id": "xo-time",
     "name": "xo-time",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A time of day as two selects, hour and minute.",
     "description": "A time of day as two selects, hour and minute. The value is `HH:mm` or empty.",
     "attributes": [
      {
       "name": "name",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Form field path."
      },
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name of the group."
      },
      {
       "name": "step",
       "type": "number",
       "default": "5",
       "values": null,
       "description": "Minutes between choices (default 5)."
      },
      {
       "name": "min",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Earliest time, `HH:mm`."
      },
      {
       "name": "max",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Latest time, `HH:mm`."
      },
      {
       "name": "required",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "The field must be filled before the form can be sent."
      },
      {
       "name": "disabled",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Not available: not editable, not sent with the form."
      },
      {
       "name": "readonly",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Shown and sent, but not changeable."
      }
     ],
     "events": [],
     "example": {
      "lang": "html",
      "code": "<xo-time name=\"at\" label=\"Time\" step=\"15\" value=\"09:30\"></xo-time>"
     },
     "guide": "more-controls.html#dates",
     "keywords": "hour minute clock"
    },
    {
     "id": "xo-slider",
     "name": "xo-slider",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A number on a track (a native range input).",
     "description": "A number on a track (a native range input). `input` while dragging, `change` on release.",
     "attributes": [
      {
       "name": "name",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Form field path."
      },
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name."
      },
      {
       "name": "min",
       "type": "number",
       "default": "0",
       "values": null,
       "description": "Lowest value (default 0)."
      },
      {
       "name": "max",
       "type": "number",
       "default": "100",
       "values": null,
       "description": "Highest value (default 100)."
      },
      {
       "name": "step",
       "type": "number",
       "default": "1",
       "values": null,
       "description": "Step (default 1)."
      },
      {
       "name": "labels",
       "type": "list",
       "default": null,
       "values": null,
       "description": "Comma-separated names of the stops, read by screen readers (`Low,Medium,High`)."
      },
      {
       "name": "required",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "The field must be filled before the form can be sent."
      },
      {
       "name": "disabled",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Not available: not editable, not sent with the form."
      },
      {
       "name": "readonly",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Shown and sent, but not changeable."
      }
     ],
     "events": [],
     "example": {
      "lang": "html",
      "code": "<xo-slider name=\"quality\" label=\"Image quality\" min=\"0\" max=\"100\" step=\"5\" value=\"80\"></xo-slider>"
     },
     "guide": "more-controls.html#numbers",
     "keywords": "range track number"
    },
    {
     "id": "xo-stepper",
     "name": "xo-stepper",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A number with minus and plus buttons; arrows change by step, Page by ten steps, the value is clamped.",
     "description": "A number with minus and plus buttons; arrows change by step, Page by ten steps, the value is clamped.",
     "attributes": [
      {
       "name": "name",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Form field path."
      },
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name."
      },
      {
       "name": "min",
       "type": "number",
       "default": null,
       "values": null,
       "description": "Lowest value."
      },
      {
       "name": "max",
       "type": "number",
       "default": null,
       "values": null,
       "description": "Highest value."
      },
      {
       "name": "step",
       "type": "number",
       "default": "1",
       "values": null,
       "description": "Step (default 1)."
      },
      {
       "name": "required",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "The field must be filled before the form can be sent."
      },
      {
       "name": "disabled",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Not available: not editable, not sent with the form."
      },
      {
       "name": "readonly",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Shown and sent, but not changeable."
      }
     ],
     "events": [],
     "example": {
      "lang": "html",
      "code": "<xo-stepper name=\"perPage\" label=\"Items per page\" min=\"1\" max=\"100\" step=\"5\" value=\"25\"></xo-stepper>"
     },
     "guide": "more-controls.html#numbers",
     "keywords": "number plus minus quantity spinner increment"
    },
    {
     "id": "xo-password",
     "name": "xo-password",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A password field with a show/hide button.",
     "description": "A password field with a show/hide button.",
     "attributes": [
      {
       "name": "name",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Form field path."
      },
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name."
      },
      {
       "name": "autocomplete",
       "type": "text",
       "default": null,
       "values": null,
       "description": "`new-password` or `current-password`, for password managers."
      },
      {
       "name": "placeholder",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Hint text of the empty field."
      },
      {
       "name": "required",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "The field must be filled before the form can be sent."
      },
      {
       "name": "disabled",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Not available: not editable, not sent with the form."
      },
      {
       "name": "readonly",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Shown and sent, but not changeable."
      }
     ],
     "events": [],
     "example": {
      "lang": "html",
      "code": "<xo-password name=\"password\" label=\"Password\" autocomplete=\"new-password\"></xo-password>"
     },
     "guide": "more-controls.html#text",
     "keywords": "secret show hide reveal credential"
    },
    {
     "id": "xo-copy",
     "name": "xo-copy",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A read-only value with a Copy button; fires `copied`.",
     "description": "A read-only value with a Copy button; fires `copied`.",
     "attributes": [
      {
       "name": "value",
       "type": "text",
       "default": null,
       "values": null,
       "description": "The text to show and copy."
      },
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name of the field."
      },
      {
       "name": "required",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "The field must be filled before the form can be sent."
      },
      {
       "name": "disabled",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Not available: not editable, not sent with the form."
      },
      {
       "name": "readonly",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Shown and sent, but not changeable."
      }
     ],
     "events": [
      {
       "name": "copied",
       "id": "event-copied",
       "summary": "The text was copied to the clipboard."
      },
      {
       "name": "copy-failed",
       "id": "event-copy-failed",
       "summary": "The browser refused the clipboard."
      }
     ],
     "example": {
      "lang": "html",
      "code": "<xo-copy label=\"API key\" value=\"xo_live_4f9a7f3a\"></xo-copy>"
     },
     "guide": "more-controls.html#text",
     "keywords": "clipboard copy button api key"
    },
    {
     "id": "xo-image",
     "name": "xo-image",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "An image that loads lazily, keeps its space (no layout jump), shows a placeholder while loading and a fallback on error.",
     "description": "An image that loads lazily, keeps its space (no layout jump), shows a placeholder while loading and a fallback on error. `data-state`: empty, loading, loaded, error.",
     "attributes": [
      {
       "name": "src",
       "type": "text",
       "default": null,
       "values": null,
       "description": "The image URL (in an xo-view: `src=\"{cover.url}\"`)."
      },
      {
       "name": "alt",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Required: the text for the image, or `alt=\"\"` when it is decorative."
      },
      {
       "name": "width",
       "type": "number",
       "default": null,
       "values": null,
       "description": "Intrinsic width, reserves the space with height."
      },
      {
       "name": "height",
       "type": "number",
       "default": null,
       "values": null,
       "description": "Intrinsic height."
      },
      {
       "name": "ratio",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Aspect ratio to reserve instead of width/height: `16/9`, `1.5`."
      },
      {
       "name": "fit",
       "type": "text",
       "default": "cover",
       "values": null,
       "description": "`cover` (default) or `contain`."
      },
      {
       "name": "position",
       "type": "text",
       "default": null,
       "values": null,
       "description": "object-position, e.g. `top`."
      },
      {
       "name": "srcset",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Responsive sources, passed to the img."
      },
      {
       "name": "sizes",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Display sizes for srcset."
      },
      {
       "name": "priority",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "The image above the fold: loads at once with high priority (not lazy)."
      },
      {
       "name": "loading",
       "type": "choice",
       "default": "lazy",
       "values": [
        "lazy",
        "eager"
       ],
       "description": "`lazy` (default) or `eager`."
      },
      {
       "name": "fallback",
       "type": "text",
       "default": null,
       "values": null,
       "description": "A src to use once when the image fails."
      }
     ],
     "events": [],
     "example": {
      "lang": "html",
      "code": "<xo-image src=\"img/landscape.svg\" alt=\"A landscape\" ratio=\"16/9\" fallback=\"img/placeholder.svg\"></xo-image>"
     },
     "guide": "more-controls.html#page",
     "keywords": "picture photo lazy placeholder fallback ratio responsive"
    },
    {
     "id": "xo-theme-toggle",
     "name": "xo-theme-toggle",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A button that switches between the light and the dark theme: sets `data-theme` on <html>, saves the choice, follows the system until chosen.",
     "description": "A button that switches between the light and the dark theme: sets `data-theme` on <html>, saves the choice, follows the system until chosen. Fires `xo-theme-change`.",
     "attributes": [
      {
       "name": "storage",
       "type": "text",
       "default": "xoong-theme",
       "values": null,
       "description": "localStorage key (default `xoong-theme`)."
      }
     ],
     "events": [
      {
       "name": "xo-theme-change",
       "id": "event-xo-theme-change",
       "summary": "The theme was switched."
      }
     ],
     "example": {
      "lang": "html",
      "code": "<xo-theme-toggle></xo-theme-toggle>"
     },
     "guide": "more-controls.html#page",
     "keywords": "dark mode light theme switch colour scheme"
    },
    {
     "id": "xo-accordion",
     "name": "xo-accordion",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A stack of native <details> as an accordion; `single` allows one open at a time; arrows move between summaries.",
     "description": "A stack of native <details> as an accordion; `single` allows one open at a time; arrows move between summaries. Fires `xo-accordion-change`.",
     "attributes": [
      {
       "name": "single",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Only one section open at a time."
      }
     ],
     "events": [
      {
       "name": "xo-accordion-change",
       "id": "event-xo-accordion-change",
       "summary": "A section of an accordion opened or closed."
      }
     ],
     "example": {
      "lang": "html",
      "code": "<xo-accordion single>\n  <details><summary>Does it need a build?</summary><div class=\"xo-accordion__body\">No.</div></details>\n  <details><summary>Can I use my own styles?</summary><div class=\"xo-accordion__body\">Yes.</div></details>\n</xo-accordion>"
     },
     "guide": "more-controls.html#structure",
     "keywords": "collapse expand details faq sections"
    },
    {
     "id": "xo-popover",
     "name": "xo-popover",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A small panel opened by a button: Escape or an outside click closes it and the focus returns to the trigger.",
     "description": "A small panel opened by a button: Escape or an outside click closes it and the focus returns to the trigger. `xo-popover-open` / `xo-popover-close`.",
     "attributes": [
      {
       "name": "anchor",
       "type": "selector",
       "default": "the element before it",
       "values": null,
       "description": "Selector of the trigger (default: the element before it)."
      },
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name of the panel."
      },
      {
       "name": "open",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Open at the start."
      }
     ],
     "events": [
      {
       "name": "xo-popover-open",
       "id": "event-xo-popover-open",
       "summary": "A popover opened."
      },
      {
       "name": "xo-popover-close",
       "id": "event-xo-popover-close",
       "summary": "A popover closed."
      }
     ],
     "example": {
      "lang": "html",
      "code": "<div class=\"xo-notify\">\n  <button aria-label=\"Notifications\">Bell</button>\n  <xo-popover label=\"Notifications\"><p>Nothing new.</p></xo-popover>\n</div>"
     },
     "guide": "more-controls.html#structure",
     "keywords": "panel popup dropdown notifications overlay"
    },
    {
     "id": "xo-consent",
     "name": "xo-consent",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A cookie consent bar that remembers the choice.",
     "description": "A cookie consent bar that remembers the choice. The text inside is the message. Fires `xo-consent` with the choices.",
     "attributes": [
      {
       "name": "categories",
       "type": "list",
       "default": null,
       "values": null,
       "description": "Comma-separated categories: `analytics,marketing`."
      },
      {
       "name": "storage",
       "type": "text",
       "default": "xoong-consent",
       "values": null,
       "description": "localStorage key (default `xoong-consent`)."
      },
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name of the bar."
      }
     ],
     "events": [
      {
       "name": "xo-consent",
       "id": "event-xo-consent",
       "summary": "The visitor made a choice about cookies."
      }
     ],
     "example": {
      "lang": "html",
      "code": "<xo-consent categories=\"analytics,marketing\">\n  <b>We use two cookies.</b> One keeps you signed in, one measures anonymous visits.\n</xo-consent>\n<button xo-consent-open>Cookie settings</button>"
     },
     "guide": "more-controls.html#page",
     "keywords": "cookies privacy gdpr banner analytics marketing"
    },
    {
     "id": "xo-file",
     "name": "xo-file",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A file field with a drop zone.",
     "description": "A file field with a drop zone. The value is a File (an array with `multiple`). Refused files fire `xo-file-reject`.",
     "attributes": [
      {
       "name": "name",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Form field path."
      },
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name."
      },
      {
       "name": "accept",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Allowed types: `image/*`, `.pdf`."
      },
      {
       "name": "max-size",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Largest file: bytes or `2MB`."
      },
      {
       "name": "multiple",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Several files."
      },
      {
       "name": "hint",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Text under the prompt."
      },
      {
       "name": "required",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "The field must be filled before the form can be sent."
      },
      {
       "name": "disabled",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Not available: not editable, not sent with the form."
      },
      {
       "name": "readonly",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Shown and sent, but not changeable."
      }
     ],
     "events": [
      {
       "name": "xo-file-reject",
       "id": "event-xo-file-reject",
       "summary": "A file was refused."
      }
     ],
     "example": {
      "lang": "html",
      "code": "<xo-file name=\"logo\" label=\"Logo\" accept=\"image/*\" max-size=\"2MB\" hint=\"PNG or SVG, up to 2 MB\"></xo-file>"
     },
     "guide": "more-controls.html#file",
     "keywords": "upload drop zone attachment image pdf"
    },
    {
     "id": "xo-command",
     "name": "xo-command",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A command palette: Ctrl/Cmd+K, type to filter, arrows, Enter.",
     "description": "A command palette: Ctrl/Cmd+K, type to filter, arrows, Enter. The children (links, buttons with `data-group`) are the commands. Fires `xo-command-run`.",
     "attributes": [
      {
       "name": "shortcut",
       "type": "text",
       "default": "k",
       "values": null,
       "description": "The key used with Ctrl/Cmd (default `k`)."
      },
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name of the dialog."
      }
     ],
     "events": [
      {
       "name": "xo-command-run",
       "id": "event-xo-command-run",
       "summary": "A command of the palette was run."
      }
     ],
     "example": {
      "lang": "html",
      "code": "<xo-command shortcut=\"k\" label=\"Search commands\">\n  <a href=\"/articles\" data-group=\"Go to\">Articles</a>\n  <button data-group=\"Actions\" data-value=\"new-article\">New article</button>\n</xo-command>"
     },
     "guide": "more-controls.html#page",
     "keywords": "palette search commands shortcut ctrl k spotlight"
    },
    {
     "id": "xo-chart",
     "name": "xo-chart",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A bar or line chart drawn from data, with a table of the same numbers.",
     "description": "A bar or line chart drawn from data, with a table of the same numbers.",
     "attributes": [
      {
       "name": "type",
       "type": "choice",
       "default": "bar",
       "values": [
        "bar",
        "line"
       ],
       "description": "`bar` (default) or `line`."
      },
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name of the chart."
      },
      {
       "name": "data",
       "type": "JSON",
       "default": null,
       "values": null,
       "description": "JSON: `[{\"label\":\"Mon\",\"value\":12}]` or `[12, 18]`; or set `el.rows`."
      },
      {
       "name": "unit",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Appended to values: `k`, `%`."
      },
      {
       "name": "heading",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Label column of the table."
      },
      {
       "name": "value-heading",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Value column of the table."
      }
     ],
     "events": [],
     "example": {
      "lang": "html",
      "code": "<xo-chart type=\"bar\" label=\"Visits per day\" unit=\"k\" data='[{\"label\":\"Mon\",\"value\":12},{\"label\":\"Tue\",\"value\":18},{\"label\":\"Wed\",\"value\":9}]'></xo-chart>"
     },
     "guide": "more-controls.html#structure",
     "keywords": "graph bar line plot data visualisation table"
    },
    {
     "id": "xo-range-slider",
     "name": "xo-range-slider",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A minimum and a maximum on one track: two native range inputs, thumbs cannot cross.",
     "description": "A minimum and a maximum on one track: two native range inputs, thumbs cannot cross. The value is `[min, max]`; the attribute is `value=\"20,70\"`.",
     "attributes": [
      {
       "name": "name",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Form field path."
      },
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name of the group."
      },
      {
       "name": "min",
       "type": "number",
       "default": "0",
       "values": null,
       "description": "Lowest value (default 0)."
      },
      {
       "name": "max",
       "type": "number",
       "default": "100",
       "values": null,
       "description": "Highest value (default 100)."
      },
      {
       "name": "step",
       "type": "number",
       "default": "1",
       "values": null,
       "description": "Step (default 1)."
      },
      {
       "name": "min-label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name of the minimum thumb."
      },
      {
       "name": "max-label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name of the maximum thumb."
      },
      {
       "name": "required",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "The field must be filled before the form can be sent."
      },
      {
       "name": "disabled",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Not available: not editable, not sent with the form."
      },
      {
       "name": "readonly",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Shown and sent, but not changeable."
      }
     ],
     "events": [],
     "example": {
      "lang": "html",
      "code": "<xo-range-slider name=\"price\" label=\"Price\" min=\"0\" max=\"100\" value=\"20,70\" min-label=\"Minimum price\" max-label=\"Maximum price\"></xo-range-slider>"
     },
     "guide": "more-controls.html#numbers",
     "keywords": "minimum maximum two thumbs price range"
    },
    {
     "id": "xo-segmented",
     "name": "xo-segmented",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A group of buttons where one is chosen.",
     "description": "A group of buttons where one is chosen. The children `<button value>` are the segments; `aria-pressed` on each; arrows move the focus.",
     "attributes": [
      {
       "name": "name",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Form field path."
      },
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name of the group."
      },
      {
       "name": "multiple",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Each segment toggles on its own; the value is an array."
      },
      {
       "name": "clearable",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "A click on the chosen segment unchooses it."
      },
      {
       "name": "required",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "The field must be filled before the form can be sent."
      },
      {
       "name": "disabled",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Not available: not editable, not sent with the form."
      },
      {
       "name": "readonly",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Shown and sent, but not changeable."
      }
     ],
     "events": [],
     "example": {
      "lang": "html",
      "code": "<xo-segmented name=\"period\" label=\"Billing period\" value=\"month\">\n  <button value=\"month\">Monthly</button>\n  <button value=\"year\">Yearly</button>\n</xo-segmented>"
     },
     "guide": "more-controls.html#choose",
     "keywords": "buttons choice toggle group radio"
    },
    {
     "id": "xo-split-button",
     "name": "xo-split-button",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A main action with a menu of related actions.",
     "description": "A main action with a menu of related actions. The first child is the main button; the others are the menu items.",
     "attributes": [
      {
       "name": "more",
       "type": "text",
       "default": "More options",
       "values": null,
       "description": "Accessible name of the arrow button (default \"More options\")."
      }
     ],
     "events": [],
     "example": {
      "lang": "html",
      "code": "<xo-split-button more=\"More publish options\">\n  <button class=\"primary\">Publish</button>\n  <button>Schedule</button>\n  <button>Save as draft</button>\n</xo-split-button>"
     },
     "guide": "more-controls.html#choose",
     "keywords": "button menu dropdown main action more"
    },
    {
     "id": "xo-textarea",
     "name": "xo-textarea",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A textarea with a character counter and a thin progress bar toward `maxlength`; over the limit it is invalid and the text is kept.",
     "description": "A textarea with a character counter and a thin progress bar toward `maxlength`; over the limit it is invalid and the text is kept.",
     "attributes": [
      {
       "name": "name",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Form field path."
      },
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name."
      },
      {
       "name": "maxlength",
       "type": "number",
       "default": null,
       "values": null,
       "description": "Character limit."
      },
      {
       "name": "rows",
       "type": "number",
       "default": "3",
       "values": null,
       "description": "Visible rows (default 3)."
      },
      {
       "name": "placeholder",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Hint text of the empty field."
      },
      {
       "name": "required",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "The field must be filled before the form can be sent."
      },
      {
       "name": "disabled",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Not available: not editable, not sent with the form."
      },
      {
       "name": "readonly",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Shown and sent, but not changeable."
      }
     ],
     "events": [],
     "example": {
      "lang": "html",
      "code": "<xo-textarea name=\"excerpt\" label=\"Excerpt\" maxlength=\"160\" rows=\"3\"></xo-textarea>"
     },
     "guide": "more-controls.html#text",
     "keywords": "multiline text counter limit characters"
    },
    {
     "id": "xo-stat",
     "name": "xo-stat",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A KPI tile: label, value, a change against before and a sparkline from a series.",
     "description": "A KPI tile: label, value, a change against before and a sparkline from a series.",
     "attributes": [
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "What is measured."
      },
      {
       "name": "value",
       "type": "text",
       "default": null,
       "values": null,
       "description": "The number as text (formatted by you)."
      },
      {
       "name": "delta",
       "type": "number",
       "default": null,
       "values": null,
       "description": "Change in percent; positive shows an up arrow, negative a down arrow."
      },
      {
       "name": "delta-label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Text after the change, e.g. \"vs last week\"."
      },
      {
       "name": "series",
       "type": "list",
       "default": null,
       "values": null,
       "description": "Comma-separated numbers for the sparkline."
      },
      {
       "name": "trend",
       "type": "text",
       "default": null,
       "values": null,
       "description": "`down-is-good` when a fall is an improvement."
      }
     ],
     "events": [],
     "example": {
      "lang": "html",
      "code": "<xo-stat label=\"Visits\" value=\"48,210\" delta=\"12.4\" delta-label=\"vs last week\" series=\"20,22,18,26,24,32,29\"></xo-stat>"
     },
     "guide": "more-controls.html#structure",
     "keywords": "kpi metric number sparkline delta trend tile"
    },
    {
     "id": "xo-otp",
     "name": "xo-otp",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "One-time code: one box per digit; the value is the digit string; fires `complete` when all are filled.",
     "description": "One-time code: one box per digit; the value is the digit string; fires `complete` when all are filled. Paste splits across the boxes.",
     "attributes": [
      {
       "name": "name",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Form field path."
      },
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name of the group."
      },
      {
       "name": "length",
       "type": "number",
       "default": "6",
       "values": null,
       "description": "Number of digits (default 6)."
      },
      {
       "name": "value",
       "type": "text",
       "default": null,
       "values": null,
       "description": "The start value."
      },
      {
       "name": "required",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Must be filled."
      },
      {
       "name": "disabled",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Not available."
      },
      {
       "name": "readonly",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Shown but not changeable."
      }
     ],
     "events": [
      {
       "name": "complete",
       "id": "event-complete",
       "summary": "Every box of a one-time code is filled."
      }
     ],
     "example": {
      "lang": "html",
      "code": "<xo-otp name=\"code\" length=\"6\" label=\"Verification code\"></xo-otp>"
     },
     "guide": "more-controls.html#feedback",
     "keywords": "code pin verification digits sms"
    },
    {
     "id": "xo-avatar",
     "name": "xo-avatar",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A round badge with initials from the name, or a picture; `aria-label` is the name and the status.",
     "description": "A round badge with initials from the name, or a picture; `aria-label` is the name and the status.",
     "attributes": [
      {
       "name": "name",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Full name (initials and accessible name)."
      },
      {
       "name": "src",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Picture URL; the initials stay when it fails."
      },
      {
       "name": "size",
       "type": "choice",
       "default": null,
       "values": [
        "sm",
        "lg"
       ],
       "description": "`sm` or `lg`."
      },
      {
       "name": "status",
       "type": "choice",
       "default": null,
       "values": [
        "ok",
        "warn",
        "muted"
       ],
       "description": "`ok`, `warn` or `muted`."
      }
     ],
     "events": [],
     "example": {
      "lang": "html",
      "code": "<xo-avatar name=\"Linh Phạm\" status=\"ok\"></xo-avatar>"
     },
     "guide": "more-controls.html#feedback",
     "keywords": "profile picture initials user badge photo"
    },
    {
     "id": "xo-avatars",
     "name": "xo-avatars",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A stack of avatars with a +N badge for the rest; a list.",
     "description": "A stack of avatars with a +N badge for the rest; a list.",
     "attributes": [
      {
       "name": "max",
       "type": "number",
       "default": null,
       "values": null,
       "description": "How many to show."
      },
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name of the list."
      }
     ],
     "events": [],
     "example": {
      "lang": "html",
      "code": "<xo-avatars max=\"2\" label=\"Reviewers\">\n  <xo-avatar name=\"An Lê\"></xo-avatar>\n  <xo-avatar name=\"Binh Vo\"></xo-avatar>\n  <xo-avatar name=\"Chi Do\"></xo-avatar>\n</xo-avatars>"
     },
     "guide": "more-controls.html#feedback",
     "keywords": "stack group users team people"
    },
    {
     "id": "xo-progress",
     "name": "xo-progress",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A progress bar; no `value` means indeterminate.",
     "description": "A progress bar; no `value` means indeterminate.",
     "attributes": [
      {
       "name": "value",
       "type": "number",
       "default": null,
       "values": null,
       "description": "Current amount."
      },
      {
       "name": "max",
       "type": "number",
       "default": "100",
       "values": null,
       "description": "Full amount (default 100)."
      },
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name."
      },
      {
       "name": "thin",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "The thin variant."
      }
     ],
     "events": [],
     "example": {
      "lang": "html",
      "code": "<xo-progress value=\"62\" label=\"Upload\"></xo-progress>"
     },
     "guide": "more-controls.html#feedback",
     "keywords": "bar loading percent upload indeterminate"
    },
    {
     "id": "xo-spinner",
     "name": "xo-spinner",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A loading spinner, `role=status`; `label=\"\"` makes it decorative.",
     "description": "A loading spinner, `role=status`; `label=\"\"` makes it decorative.",
     "attributes": [
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "What is loading."
      },
      {
       "name": "size",
       "type": "choice",
       "default": null,
       "values": [
        "md",
        "lg"
       ],
       "description": "`md` or `lg`."
      }
     ],
     "events": [],
     "example": {
      "lang": "html",
      "code": "<xo-spinner label=\"Loading\" size=\"lg\"></xo-spinner>"
     },
     "guide": "more-controls.html#feedback",
     "keywords": "loading busy wait indicator"
    },
    {
     "id": "xo-steps",
     "name": "xo-steps",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "Progress through a flow of steps: the children are the step titles; `aria-current=step` marks the current one; done steps show a check.",
     "description": "Progress through a flow of steps: the children are the step titles; `aria-current=step` marks the current one; done steps show a check.",
     "attributes": [
      {
       "name": "current",
       "type": "number",
       "default": null,
       "values": null,
       "description": "Index of the current step (from 0)."
      },
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name of the list."
      }
     ],
     "events": [],
     "example": {
      "lang": "html",
      "code": "<xo-steps current=\"1\" label=\"Checkout\">\n  <span>Account</span><span>Plan</span><span>Payment</span>\n</xo-steps>"
     },
     "guide": "more-controls.html#feedback",
     "keywords": "wizard stepper progress flow checkout"
    },
    {
     "id": "xo-inline-edit",
     "name": "xo-inline-edit",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "Text you rename in place: select it or press the Rename button; Enter saves, Escape cancels.",
     "description": "Text you rename in place: select it or press the Rename button; Enter saves, Escape cancels. Fires `save` and `cancel`.",
     "attributes": [
      {
       "name": "name",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Form field path."
      },
      {
       "name": "value",
       "type": "text",
       "default": null,
       "values": null,
       "description": "The text."
      },
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name of the Rename button."
      },
      {
       "name": "disabled",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Not available."
      },
      {
       "name": "readonly",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Not editable."
      },
      {
       "name": "required",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "The field must be filled before the form can be sent."
      }
     ],
     "events": [
      {
       "name": "save",
       "id": "event-save",
       "summary": "The new text of an inline edit was saved."
      },
      {
       "name": "cancel",
       "id": "event-cancel",
       "summary": "An inline edit was cancelled."
      }
     ],
     "example": {
      "lang": "html",
      "code": "<xo-inline-edit name=\"title\" value=\"Greece Golden Visa\" label=\"Title\"></xo-inline-edit>"
     },
     "guide": "more-controls.html#feedback",
     "keywords": "rename edit in place title text"
    },
    {
     "id": "xo-drawer",
     "name": "xo-drawer",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A side sheet on a native modal <dialog>: open it with `xo-open=\"#id\"` like xo-dialog, close it with `xo-close`, Escape or (with `dismiss`) a click outside.",
     "description": "A side sheet on a native modal <dialog>: open it with `xo-open=\"#id\"` like xo-dialog, close it with `xo-close`, Escape or (with `dismiss`) a click outside. The header and the footer (`slot=\"footer\"`) stay put, the body scrolls; full screen on a phone.",
     "attributes": [
      {
       "name": "heading",
       "type": "text",
       "default": null,
       "values": null,
       "description": "The title in the header (or give `label`)."
      },
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name when there is no heading."
      },
      {
       "name": "side",
       "type": "choice",
       "default": "end",
       "values": [
        "end",
        "start"
       ],
       "description": "`end` (default) or `start`: logical, follows the writing direction."
      },
      {
       "name": "size",
       "type": "choice",
       "default": "md",
       "values": [
        "sm",
        "md",
        "lg"
       ],
       "description": "`sm`, `md` (default) or `lg`."
      },
      {
       "name": "dismiss",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Close it on a click outside."
      }
     ],
     "events": [],
     "example": {
      "lang": "html",
      "code": "<button xo-open=\"#filters\">Filters</button>\n<xo-drawer id=\"filters\" heading=\"Filters\" side=\"end\" dismiss>\n  <p>Choose what to show.</p>\n  <button slot=\"footer\" type=\"button\" xo-close>Close</button>\n</xo-drawer>"
     },
     "guide": "more-controls.html#overlays",
     "keywords": "side sheet panel slide filter offcanvas modal"
    },
    {
     "id": "xo-breadcrumb",
     "name": "xo-breadcrumb",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "The trail of pages that led here: `<a>` children or JSON `items`, the last is `aria-current=page`; the middle collapses into an \"…\" menu when `max` is set or the space is short.",
     "description": "The trail of pages that led here: `<a>` children or JSON `items`, the last is `aria-current=page`; the middle collapses into an \"…\" menu when `max` is set or the space is short.",
     "attributes": [
      {
       "name": "label",
       "type": "text",
       "default": "Breadcrumb",
       "values": null,
       "description": "Accessible name of the navigation (default \"Breadcrumb\")."
      },
      {
       "name": "items",
       "type": "JSON",
       "default": null,
       "values": null,
       "description": "JSON: `[{\"label\":\"Home\",\"href\":\"/\"},{\"label\":\"Now\"}]`; or write `<a>` children."
      },
      {
       "name": "max",
       "type": "number",
       "default": null,
       "values": null,
       "description": "Most items to show (the first and the last ones); the rest go into the \"…\" menu."
      }
     ],
     "events": [],
     "example": {
      "lang": "html",
      "code": "<xo-breadcrumb max=\"3\" items='[{\"label\":\"Home\",\"href\":\"/\"},{\"label\":\"Docs\",\"href\":\"/docs\"},{\"label\":\"Controls\",\"href\":\"/docs/controls\"},{\"label\":\"Tree\"}]'></xo-breadcrumb>"
     },
     "guide": "more-controls.html#overlays",
     "keywords": "trail path navigation hierarchy location"
    },
    {
     "id": "xo-alert",
     "name": "xo-alert",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A message that stays in the page: an icon by `kind`, an optional heading and action (`slot=\"action\"`), a Close button with `dismissible`.",
     "description": "A message that stays in the page: an icon by `kind`, an optional heading and action (`slot=\"action\"`), a Close button with `dismissible`. `role=alert` for warning and danger, `status` otherwise. Fires `xo-dismiss`.",
     "attributes": [
      {
       "name": "kind",
       "type": "choice",
       "default": "info",
       "values": [
        "info",
        "success",
        "warning",
        "danger"
       ],
       "description": "`info` (default), `success`, `warning` or `danger`."
      },
      {
       "name": "heading",
       "type": "text",
       "default": null,
       "values": null,
       "description": "A bold first line."
      },
      {
       "name": "dismissible",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Show a Close button that hides the alert."
      },
      {
       "name": "live",
       "type": "choice",
       "default": null,
       "values": [
        "polite",
        "assertive",
        "off"
       ],
       "description": "`polite`, `assertive` or `off`: overrides the role chosen by `kind`."
      }
     ],
     "events": [
      {
       "name": "xo-dismiss",
       "id": "event-xo-dismiss",
       "summary": "The Close button of an alert was pressed."
      }
     ],
     "example": {
      "lang": "html",
      "code": "<xo-alert kind=\"warning\" heading=\"Your trial ends in 3 days\" dismissible>Add a payment method to keep your pages.</xo-alert>"
     },
     "guide": "more-controls.html#messages",
     "keywords": "banner notice message warning error success info callout"
    },
    {
     "id": "xo-skeleton",
     "name": "xo-skeleton",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A grey placeholder in the shape of what is loading: lines of text, a circle or a rectangle.",
     "description": "A grey placeholder in the shape of what is loading: lines of text, a circle or a rectangle. Hidden from screen readers, with one visually hidden \"Loading\" status; the shimmer stops under reduced motion.",
     "attributes": [
      {
       "name": "lines",
       "type": "number",
       "default": null,
       "values": null,
       "description": "Number of text lines (the last is shorter)."
      },
      {
       "name": "shape",
       "type": "choice",
       "default": "text",
       "values": [
        "text",
        "circle",
        "rect"
       ],
       "description": "`text` (default), `circle` or `rect`."
      },
      {
       "name": "width",
       "type": "text",
       "default": null,
       "values": null,
       "description": "A CSS length; a bare number is pixels."
      },
      {
       "name": "height",
       "type": "text",
       "default": null,
       "values": null,
       "description": "A CSS length; a bare number is pixels."
      },
      {
       "name": "label",
       "type": "text",
       "default": "Loading",
       "values": null,
       "description": "The status text (default \"Loading\"); `label=\"\"` leaves it out."
      }
     ],
     "events": [],
     "example": {
      "lang": "html",
      "code": "<xo-skeleton lines=\"3\"></xo-skeleton>"
     },
     "guide": "more-controls.html#messages",
     "keywords": "loading placeholder shimmer shape"
    },
    {
     "id": "xo-tree",
     "name": "xo-tree",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A tree view (the WAI-ARIA tree pattern): nested `<ul>` or JSON `data`; arrows, Home, End, `*` and type-ahead; `selectable` or `multiple` (checkboxes, a parent is true, false or mixed).",
     "description": "A tree view (the WAI-ARIA tree pattern): nested `<ul>` or JSON `data`; arrows, Home, End, `*` and type-ahead; `selectable` or `multiple` (checkboxes, a parent is true, false or mixed). Fires `xo-tree-select`, `xo-tree-toggle` and, for lazy items, `xo-tree-load`.",
     "attributes": [
      {
       "name": "data",
       "type": "JSON",
       "default": null,
       "values": null,
       "description": "JSON: `[{\"id\":\"a\",\"label\":\"A\",\"children\":[...]}]`; or write nested `<ul><li>`."
      },
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name of the tree."
      },
      {
       "name": "selectable",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "One item can be chosen (`aria-selected`)."
      },
      {
       "name": "multiple",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Every item has a checkbox; a parent follows its children."
      },
      {
       "name": "expanded",
       "type": "list",
       "default": null,
       "values": null,
       "description": "Ids of the items open at the start, separated by commas."
      },
      {
       "name": "selected",
       "type": "list",
       "default": null,
       "values": null,
       "description": "Ids chosen at the start, separated by commas."
      }
     ],
     "events": [
      {
       "name": "xo-tree-select",
       "id": "event-xo-tree-select",
       "summary": "The choice in a tree changed."
      },
      {
       "name": "xo-tree-toggle",
       "id": "event-xo-tree-toggle",
       "summary": "A tree item opened or closed."
      },
      {
       "name": "xo-tree-load",
       "id": "event-xo-tree-load",
       "summary": "A lazy tree item was opened for the first time."
      }
     ],
     "example": {
      "lang": "html",
      "code": "<xo-tree label=\"Files\" selectable>\n  <ul><li>src<ul><li>index.js</li><li>util.js</li></ul></li><li>README.md</li></ul>\n</xo-tree>"
     },
     "guide": "more-controls.html#overlays",
     "keywords": "hierarchy folders outline nested expand collapse checkbox"
    },
    {
     "id": "xo-color",
     "name": "xo-color",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A color field: a swatch button opens a panel with the native color picker, a validated hex field and preset swatches.",
     "description": "A color field: a swatch button opens a panel with the native color picker, a validated hex field and preset swatches. The value is `#rrggbb` (`#rrggbbaa` with `alpha`).",
     "attributes": [
      {
       "name": "name",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Form field path."
      },
      {
       "name": "label",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Accessible name."
      },
      {
       "name": "alpha",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Add an opacity slider; the value then has two more digits."
      },
      {
       "name": "swatches",
       "type": "list",
       "default": null,
       "values": null,
       "description": "Preset colors: `#0b7285,#c92a2a`."
      },
      {
       "name": "required",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "The field must be filled before the form can be sent."
      },
      {
       "name": "disabled",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Not available: not editable, not sent with the form."
      },
      {
       "name": "readonly",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Shown and sent, but not changeable."
      }
     ],
     "events": [],
     "example": {
      "lang": "html",
      "code": "<xo-color name=\"brand\" label=\"Brand color\" value=\"#0b7285\" swatches=\"#0b7285,#c92a2a,#2b8a3e\"></xo-color>"
     },
     "guide": "more-controls.html#pick",
     "keywords": "colour picker hex swatch palette brand"
    },
    {
     "id": "xo-rating",
     "name": "xo-rating",
     "kind": "element",
     "package": "@xoong/controls",
     "import": "import '@xoong/controls';",
     "summary": "A star rating: one native radio per star (two with `half`), so arrows, Tab and screen readers work; the pointer previews; the value is a number (0 is nothing).",
     "description": "A star rating: one native radio per star (two with `half`), so arrows, Tab and screen readers work; the pointer previews; the value is a number (0 is nothing).",
     "attributes": [
      {
       "name": "name",
       "type": "text",
       "default": null,
       "values": null,
       "description": "Form field path."
      },
      {
       "name": "label",
       "type": "text",
       "default": "Rating",
       "values": null,
       "description": "Accessible name of the group (default \"Rating\")."
      },
      {
       "name": "max",
       "type": "number",
       "default": "5",
       "values": null,
       "description": "Number of stars (default 5)."
      },
      {
       "name": "half",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Allow half stars."
      },
      {
       "name": "clearable",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "A click on the chosen star, or Delete, sets it back to nothing."
      },
      {
       "name": "required",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "The field must be filled before the form can be sent."
      },
      {
       "name": "disabled",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Not available: not editable, not sent with the form."
      },
      {
       "name": "readonly",
       "type": "flag",
       "default": "off",
       "values": null,
       "description": "Shown and sent, but not changeable."
      }
     ],
     "events": [],
     "example": {
      "lang": "html",
      "code": "<xo-rating name=\"score\" label=\"Your rating\" value=\"4\" half clearable></xo-rating>"
     },
     "guide": "more-controls.html#pick",
     "keywords": "stars review score rate"
    }
   ]
  },
  {
   "id": "directives",
   "label": "Directives",
   "summary": "The xo-* attributes you put on your own markup: data, actions, form rows, routing, dialogs.",
   "count": 35,
   "entries": [
    {
     "id": "xo-each",
     "name": "xo-each",
     "kind": "directive",
     "category": "Read data",
     "package": "@xoong/data",
     "summary": "Repeat this element for each item of the list at the given path (keyed by entity id).",
     "description": "Repeat this element for each item of the list at the given path (keyed by entity id).",
     "applies": "Any element inside `<xo-view>`. On a `<template>` it repeats the template's children.",
     "value": "The path of a list: `items`, `tags`, `blocks`.",
     "values": null,
     "notes": [
      "Inside it `{path}` reads the current item, `$index` is its position from 0 and `$n` from 1.",
      "Rows are keyed by entity id, so a changed list updates the rows that changed."
     ],
     "example": {
      "lang": "html",
      "code": "<xo-view get=\"listPeople\">\n  <ul><li xo-each=\"items\">{$n}. {name}</li></ul>\n</xo-view>"
     },
     "guide": "data.html",
     "keywords": "loop repeat list foreach iterate rows items"
    },
    {
     "id": "xo-if",
     "name": "xo-if",
     "kind": "directive",
     "category": "Read data",
     "package": "@xoong/data",
     "summary": "Render this element only when the path is truthy.",
     "description": "Render this element only when the path is truthy. Prefix with `!` to negate. An empty array is false.",
     "applies": "Any element inside `<xo-view>`.",
     "value": "A path (`$loading`, `items`), `!path` to negate, or a comparison with a text: `type=image`, `status!=draft`.",
     "values": null,
     "notes": [],
     "example": {
      "lang": "html",
      "code": "<xo-view get=\"listPeople\">\n  <p xo-if=\"$loading\">Loading…</p>\n  <p xo-if=\"!items\">Nobody here yet.</p>\n</xo-view>"
     },
     "guide": "data-states.html",
     "keywords": "condition show hide when conditional loading empty error"
    },
    {
     "id": "xo-param",
     "name": "xo-param",
     "kind": "directive",
     "category": "Read data",
     "package": "@xoong/data",
     "summary": "On an <input> or <select>: set the view parameter of that name to its value.",
     "description": "On an <input> or <select>: set the view parameter of that name to its value. On other elements, on click, set a view parameter and refetch: `name=value`, `name+=n`, `name-=n`, or `name~=value` (toggle value / -value, for sorting).",
     "applies": "An `<input>` or `<select>` (its value), or any element that is clicked (`name=value`), inside `<xo-view>`.",
     "value": "On a field: the parameter name. On click: `name=value`, `name+=n`, `name-=n` (never below 0) or `name~=value` (toggle `value` and `-value`, for sorting).",
     "values": null,
     "notes": [
      "A button whose `xo-param` matches the current value gets `aria-current`."
     ],
     "example": {
      "lang": "html",
      "code": "<xo-view get=\"listPeople\" url-params=\"name,role\">\n  <input type=\"search\" xo-param=\"name\" aria-label=\"Search by name\">\n  <button xo-param=\"role=admin\">Admins</button>\n  <button xo-param=\"role=editor\">Editors</button>\n  <ul><li xo-each=\"items\">{name}</li></ul>\n</xo-view>"
     },
     "guide": "data.html",
     "keywords": "filter search sort page query parameter refetch toggle"
    },
    {
     "id": "xo-refresh",
     "name": "xo-refresh",
     "kind": "directive",
     "category": "Read data",
     "package": "@xoong/data",
     "summary": "On a button inside an <xo-view>: on click, fetch the view's data again (a \"Try again\" button after an error, or a refresh button).",
     "description": "On a button inside an <xo-view>: on click, fetch the view's data again (a \"Try again\" button after an error, or a refresh button).",
     "applies": "A button inside `<xo-view>`.",
     "value": "None.",
     "values": null,
     "notes": [],
     "example": {
      "lang": "html",
      "code": "<xo-view get=\"listPeople\">\n  <p xo-if=\"$error\">Could not load. <button xo-refresh>Try again</button></p>\n</xo-view>"
     },
     "guide": "data-states.html",
     "keywords": "reload retry try again refetch update"
    },
    {
     "id": "xo-click",
     "name": "xo-click",
     "kind": "directive",
     "category": "Actions",
     "package": "@xoong/data",
     "summary": "Call an operation when clicked: \"METHOD /path/{param}\" or an operationId.",
     "description": "Call an operation when clicked: \"METHOD /path/{param}\" or an operationId. Params come from the current item, then the view's p-* params.",
     "applies": "A `<button>` or link inside `<xo-view>`. Another element works, but it is not reachable by keyboard.",
     "value": "An operationId (`deletePerson`) or `METHOD /path/{param}`. `{param}` comes from the current item, then the view's `p-*` parameters.",
     "values": null,
     "notes": [
      "An operation that reads (GET) belongs on `<xo-view get>`, not on `xo-click`.",
      "After success the queries that overlap the path are refetched."
     ],
     "example": {
      "lang": "html",
      "code": "<xo-view get=\"listPeople\">\n  <ul>\n    <li xo-each=\"items\">{name}\n      <button xo-click=\"deletePerson\" xo-confirm=\"Delete {name}?\" xo-notice=\"Deleted\">Delete</button>\n    </li>\n  </ul>\n</xo-view>"
     },
     "guide": "data.html",
     "keywords": "button call operation mutation delete post put request action"
    },
    {
     "id": "xo-submit",
     "name": "xo-submit",
     "kind": "directive",
     "category": "Actions",
     "package": "@xoong/data",
     "summary": "On a <form>: send the form fields as the request body of the operation.",
     "description": "On a <form>: send the form fields as the request body of the operation. The form is reset on success unless xo-keep is present. Field errors from the server appear beside the inputs.",
     "applies": "A `<form>` inside `<xo-view>`.",
     "value": "An operationId or `METHOD /path/{param}`.",
     "values": null,
     "notes": [
      "The field names are paths (`name`, `title.en`, `items[0].x`). The form is reset after success unless `xo-keep` is present."
     ],
     "example": {
      "lang": "html",
      "code": "<xo-view>\n  <form xo-submit=\"createPerson\" xo-notice=\"Person added\">\n    <label>Name <input name=\"name\" required></label>\n    <label>E-mail <input name=\"email\" type=\"email\"></label>\n    <button>Add</button>\n  </form>\n</xo-view>"
     },
     "guide": "forms.html",
     "keywords": "form send create save post body validate"
    },
    {
     "id": "xo-body",
     "name": "xo-body",
     "kind": "directive",
     "category": "Actions",
     "package": "@xoong/data",
     "summary": "With xo-click: take the request body from the item at this path.",
     "description": "With xo-click: take the request body from the item at this path.",
     "applies": "An element with `xo-click`.",
     "value": "The path of the object to send: `.` (the current item), `draft`, `$selected`.",
     "values": null,
     "notes": [],
     "example": {
      "lang": "html",
      "code": "<xo-view get=\"listPeople\">\n  <ul><li xo-each=\"items\">{name}\n    <button xo-click=\"updatePerson\" xo-body=\".\">Save</button>\n  </li></ul>\n</xo-view>"
     },
     "guide": null,
     "keywords": "request body payload item send whole"
    },
    {
     "id": "xo-keep",
     "name": "xo-keep",
     "kind": "directive",
     "category": "Actions",
     "package": "@xoong/data",
     "summary": "With xo-submit: keep the field values after a successful send.",
     "description": "With xo-submit: keep the field values after a successful send.",
     "applies": "A `<form>` that has `xo-submit`.",
     "value": "None.",
     "values": null,
     "notes": [],
     "example": {
      "lang": "html",
      "code": "<xo-view>\n  <form xo-submit=\"createPerson\" xo-keep>\n    <input name=\"name\" aria-label=\"Name\">\n    <button>Add</button>\n  </form>\n</xo-view>"
     },
     "guide": null,
     "keywords": "preserve reset clear fields after submit"
    },
    {
     "id": "xo-optimistic",
     "name": "xo-optimistic",
     "kind": "directive",
     "category": "Actions",
     "package": "@xoong/data",
     "summary": "On xo-click: set these fields (`field=value,field2={path}`) on the current item immediately, and undo them if the call fails.",
     "description": "On xo-click: set these fields (`field=value,field2={path}`) on the current item immediately, and undo them if the call fails.",
     "applies": "An element with `xo-click`, inside `xo-each`.",
     "value": "A list `field=value,field2={path}`.",
     "values": null,
     "notes": [],
     "example": {
      "lang": "html",
      "code": "<xo-view get=\"listPeople\">\n  <ul><li xo-each=\"items\">{name} ({role})\n    <button xo-click=\"updatePerson\" xo-with=\"role=viewer\" xo-optimistic=\"role=viewer\">Make viewer</button>\n  </li></ul>\n</xo-view>"
     },
     "guide": "data.html",
     "keywords": "instant immediately undo rollback fast ui"
    },
    {
     "id": "xo-select",
     "name": "xo-select",
     "kind": "directive",
     "category": "Actions",
     "package": "@xoong/data",
     "summary": "On a checkbox in a row (inside xo-each): tick that row.",
     "description": "On a checkbox in a row (inside xo-each): tick that row. `all:items` in the header ticks every row of the list. <xo-table selectable> does this for you.",
     "applies": "A checkbox in a row (inside `xo-each`), or in the header.",
     "value": "None on a row; `all:items` (`all:` and the list path) on the header checkbox ticks every row.",
     "values": null,
     "notes": [
      "`<xo-table selectable>` adds these checkboxes for you."
     ],
     "example": {
      "lang": "html",
      "code": "<xo-view get=\"listPeople\">\n  <label><input type=\"checkbox\" xo-select=\"all:items\"> All</label>\n  <ul><li xo-each=\"items\"><label><input type=\"checkbox\" xo-select> {name}</label></li></ul>\n  <button xo-click=\"deletePerson\" xo-selected xo-if=\"$selectedCount\">Delete {$selectedCount}</button>\n</xo-view>"
     },
     "guide": "data.html",
     "keywords": "checkbox tick row bulk select all"
    },
    {
     "id": "xo-selected",
     "name": "xo-selected",
     "kind": "directive",
     "category": "Actions",
     "package": "@xoong/data",
     "summary": "On an xo-click button: run the operation once for every ticked row, one after another; the first failure stops the rest.",
     "description": "On an xo-click button: run the operation once for every ticked row, one after another; the first failure stops the rest. With a value (`xo-selected=\"ids\"`): ONE request whose body has the ticked ids in that field.",
     "applies": "An element with `xo-click`.",
     "value": "None: run the operation once per ticked row. With a field name (`ids`): one request whose body has the ticked ids in that field.",
     "values": null,
     "notes": [],
     "example": {
      "lang": "html",
      "code": "<xo-view get=\"listPeople\">\n  <ul><li xo-each=\"items\"><label><input type=\"checkbox\" xo-select> {name}</label></li></ul>\n  <button xo-click=\"deletePerson\" xo-selected>Delete ticked</button>\n</xo-view>"
     },
     "guide": "data.html",
     "keywords": "bulk batch ticked rows selected many"
    },
    {
     "id": "xo-with",
     "name": "xo-with",
     "kind": "directive",
     "category": "Actions",
     "package": "@xoong/data",
     "summary": "On xo-click/xo-submit: fixed fields added to the request body, `field=value,other={path}` (typed from the request schema).",
     "description": "On xo-click/xo-submit: fixed fields added to the request body, `field=value,other={path}` (typed from the request schema).",
     "applies": "An element with `xo-click` or `xo-submit`, or `xo-add` in a form.",
     "value": "A list `field=value,other={path}`. Values are typed from the request schema.",
     "values": null,
     "notes": [],
     "example": {
      "lang": "html",
      "code": "<xo-view get=\"listPeople\">\n  <ul><li xo-each=\"items\">{name}\n    <button xo-click=\"updatePerson\" xo-with=\"role=admin\">Make admin</button>\n  </li></ul>\n</xo-view>"
     },
     "guide": "data.html",
     "keywords": "fixed fields body extra values constant"
    },
    {
     "id": "xo-confirm",
     "name": "xo-confirm",
     "kind": "directive",
     "category": "Actions",
     "package": "@xoong/data",
     "summary": "On xo-click/xo-submit: ask before running, with a native dialog.",
     "description": "On xo-click/xo-submit: ask before running, with a native dialog. `{path}` names the item: `xo-confirm=\"Delete {title}?\"`.",
     "applies": "An element with `xo-click` or `xo-submit`.",
     "value": "The question; `{path}` names the item: `Delete {name}?`.",
     "values": null,
     "notes": [
      "The dialog is native. With @xoong/ui imported it is the built-in confirm dialog; `xo-confirm-title`, `xo-confirm-ok`, `xo-confirm-cancel` and `xo-confirm-kind` tune it.",
      "In JavaScript the same question is `confirm()`."
     ],
     "example": {
      "lang": "html",
      "code": "<xo-view get=\"listPeople\">\n  <ul><li xo-each=\"items\">{name}\n    <button xo-click=\"deletePerson\" xo-confirm=\"Delete {name}?\" xo-confirm-title=\"Delete person\" xo-confirm-ok=\"Delete\" xo-confirm-kind=\"danger\">Delete</button>\n  </li></ul>\n</xo-view>"
     },
     "guide": "dialogs.html",
     "keywords": "ask question dialog modal sure delete danger yes no confirm"
    },
    {
     "id": "xo-notice",
     "name": "xo-notice",
     "kind": "directive",
     "category": "Actions",
     "package": "@xoong/data",
     "summary": "On xo-click/xo-submit: after success, show this message in <xo-toast> (`{path}` and `{$selectedCount}` work).",
     "description": "On xo-click/xo-submit: after success, show this message in <xo-toast> (`{path}` and `{$selectedCount}` work).",
     "applies": "An element with `xo-click` or `xo-submit`.",
     "value": "The message; `{path}` and `{$selectedCount}` work.",
     "values": null,
     "notes": [
      "The message is shown by `<xo-toast>`. It travels as the `xo-notice` event."
     ],
     "example": {
      "lang": "html",
      "code": "<button xo-click=\"deletePerson\" xo-notice=\"Deleted {name}\">Delete</button>"
     },
     "guide": "dialogs.html",
     "keywords": "toast message success saved feedback snackbar"
    },
    {
     "id": "xo-then",
     "name": "xo-then",
     "kind": "directive",
     "category": "Actions",
     "package": "@xoong/data",
     "summary": "On xo-click/xo-submit: after success, go to this path; `{id}` comes from the result (a created record), then from the item acted on: `xo-then=\"/articles/{id}\"`, or `xo-then=\"/articles\"` after a delete.",
     "description": "On xo-click/xo-submit: after success, go to this path; `{id}` comes from the result (a created record), then from the item acted on: `xo-then=\"/articles/{id}\"`, or `xo-then=\"/articles\"` after a delete. <xo-router> navigates.",
     "applies": "An element with `xo-click` or `xo-submit`.",
     "value": "A path of this site. `{id}` comes from the result (a created record), then from the item acted on.",
     "values": null,
     "notes": [
      "`<xo-router>` navigates. Without a router the page is loaded with `location.assign`. Only a relative path or a same-origin URL is followed."
     ],
     "example": {
      "lang": "html",
      "code": "<xo-view>\n  <form xo-submit=\"createPerson\" xo-then=\"/people/{id}\">\n    <input name=\"name\" aria-label=\"Name\">\n    <button>Create</button>\n  </form>\n</xo-view>"
     },
     "guide": null,
     "keywords": "redirect navigate go after success next page"
    },
    {
     "id": "xo-confirm-title",
     "name": "xo-confirm-title",
     "kind": "directive",
     "category": "Actions",
     "package": "@xoong/ui",
     "summary": "With xo-confirm: a heading for the question (`xo-confirm-title=\"Delete person\"`).",
     "description": "With xo-confirm: a heading for the question (`xo-confirm-title=\"Delete person\"`).",
     "applies": "An element that has `xo-confirm`.",
     "value": "The heading text.",
     "values": null,
     "notes": [],
     "example": {
      "lang": "html",
      "code": "<button xo-click=\"deletePerson\" xo-confirm=\"This cannot be undone.\" xo-confirm-title=\"Delete person\">Delete</button>"
     },
     "guide": "dialogs.html",
     "keywords": "heading dialog confirm title"
    },
    {
     "id": "xo-confirm-ok",
     "name": "xo-confirm-ok",
     "kind": "directive",
     "category": "Actions",
     "package": "@xoong/ui",
     "summary": "With xo-confirm: the text of the confirming button (`xo-confirm-ok=\"Delete\"`) instead of \"Confirm\".",
     "description": "With xo-confirm: the text of the confirming button (`xo-confirm-ok=\"Delete\"`) instead of \"Confirm\".",
     "applies": "An element that has `xo-confirm`.",
     "value": "The text of the confirming button.",
     "values": null,
     "notes": [],
     "example": {
      "lang": "html",
      "code": "<button xo-click=\"deletePerson\" xo-confirm=\"Delete {name}?\" xo-confirm-ok=\"Delete\">Delete</button>"
     },
     "guide": "dialogs.html",
     "keywords": "button label confirm ok text"
    },
    {
     "id": "xo-confirm-cancel",
     "name": "xo-confirm-cancel",
     "kind": "directive",
     "category": "Actions",
     "package": "@xoong/ui",
     "summary": "With xo-confirm: the text of the cancelling button instead of \"Cancel\".",
     "description": "With xo-confirm: the text of the cancelling button instead of \"Cancel\".",
     "applies": "An element that has `xo-confirm`.",
     "value": "The text of the cancelling button.",
     "values": null,
     "notes": [],
     "example": {
      "lang": "html",
      "code": "<button xo-click=\"deletePerson\" xo-confirm=\"Delete {name}?\" xo-confirm-cancel=\"Keep it\">Delete</button>"
     },
     "guide": null,
     "keywords": "button label confirm cancel text"
    },
    {
     "id": "xo-confirm-kind",
     "name": "xo-confirm-kind",
     "kind": "directive",
     "category": "Actions",
     "package": "@xoong/ui",
     "summary": "With xo-confirm: `danger`, `warning`, `info` or `success`: an icon and the matching colour; the confirming button is red unless a kind says otherwise.",
     "description": "With xo-confirm: `danger`, `warning`, `info` or `success`: an icon and the matching colour; the confirming button is red unless a kind says otherwise.",
     "applies": "An element that has `xo-confirm`.",
     "value": "One of `danger`, `warning`, `info`, `success`.",
     "values": [
      "danger",
      "warning",
      "info",
      "success"
     ],
     "notes": [],
     "example": {
      "lang": "html",
      "code": "<button xo-click=\"updatePerson\" xo-confirm=\"Publish now?\" xo-confirm-kind=\"info\">Publish</button>"
     },
     "guide": "dialogs.html",
     "keywords": "icon colour danger warning info success severity"
    },
    {
     "id": "xo-add",
     "name": "xo-add",
     "kind": "directive",
     "category": "Form rows",
     "package": "@xoong/forms",
     "summary": "In a form: append an item to the array at this path (`blocks`); `xo-with=\"type=image\"` gives a union item its tag.",
     "description": "In a form: append an item to the array at this path (`blocks`); `xo-with=\"type=image\"` gives a union item its tag.",
     "applies": "A button inside a `<form xo-submit>`.",
     "value": "The path of the array: `keywords`, `blocks`. Add `xo-with=\"type=image\"` to give a union item its tag.",
     "values": null,
     "notes": [],
     "example": {
      "lang": "html",
      "code": "<xo-view get=\"getPerson\" p-id=\"1\">\n  <form xo-submit=\"updatePerson\">\n    <div xo-each=\"keywords\"><input name=\"keywords[{$index}]\" aria-label=\"Keyword\"></div>\n    <button type=\"button\" xo-add=\"keywords\">Add keyword</button>\n  </form>\n</xo-view>"
     },
     "guide": "forms.html#written-by-hand",
     "keywords": "array list row append item add union"
    },
    {
     "id": "xo-remove",
     "name": "xo-remove",
     "kind": "directive",
     "category": "Form rows",
     "package": "@xoong/forms",
     "summary": "In a form row: remove the array item at this path (`blocks[{$index}]`).",
     "description": "In a form row: remove the array item at this path (`blocks[{$index}]`).",
     "applies": "A button inside a row of an array in a form.",
     "value": "The path of the item: `keywords[{$index}]`.",
     "values": null,
     "notes": [],
     "example": {
      "lang": "html",
      "code": "<xo-view get=\"getPerson\" p-id=\"1\">\n  <form xo-submit=\"updatePerson\">\n    <div xo-each=\"keywords\"><input name=\"keywords[{$index}]\" aria-label=\"Keyword\">\n      <button type=\"button\" xo-remove=\"keywords[{$index}]\" aria-label=\"Remove\">✕</button></div>\n  </form>\n</xo-view>"
     },
     "guide": "forms.html#written-by-hand",
     "keywords": "array list row delete item"
    },
    {
     "id": "xo-move",
     "name": "xo-move",
     "kind": "directive",
     "category": "Form rows",
     "package": "@xoong/forms",
     "summary": "In a form row: move the array item at this path, `blocks[{$index}]:up` or `:down`.",
     "description": "In a form row: move the array item at this path, `blocks[{$index}]:up` or `:down`.",
     "applies": "A button inside a row of an array in a form.",
     "value": "The path of the item and a direction: `keywords[{$index}]:up` or `:down`.",
     "values": null,
     "notes": [],
     "example": {
      "lang": "html",
      "code": "<xo-view get=\"getPerson\" p-id=\"1\">\n  <form xo-submit=\"updatePerson\">\n    <div xo-each=\"keywords\"><input name=\"keywords[{$index}]\" aria-label=\"Keyword\">\n      <button type=\"button\" xo-move=\"keywords[{$index}]:up\" aria-label=\"Move up\">↑</button>\n      <button type=\"button\" xo-move=\"keywords[{$index}]:down\" aria-label=\"Move down\">↓</button></div>\n  </form>\n</xo-view>"
     },
     "guide": "forms.html#written-by-hand",
     "keywords": "array list row reorder up down order"
    },
    {
     "id": "xo-drag",
     "name": "xo-drag",
     "kind": "directive",
     "category": "Form rows",
     "package": "@xoong/forms",
     "summary": "On a handle inside an array row of a form: drag the row onto another row of the same list to move it (`blocks[{$index}]`).",
     "description": "On a handle inside an array row of a form: drag the row onto another row of the same list to move it (`blocks[{$index}]`). The xo-move buttons stay for keyboards.",
     "applies": "A handle inside a row of an array in a form.",
     "value": "The path of the item: `keywords[{$index}]`.",
     "values": null,
     "notes": [],
     "example": {
      "lang": "html",
      "code": "<xo-view get=\"getPerson\" p-id=\"1\">\n  <form xo-submit=\"updatePerson\">\n    <div xo-each=\"keywords\"><span class=\"handle\" xo-drag=\"keywords[{$index}]\" title=\"Drag to reorder\">⠿</span>\n      <input name=\"keywords[{$index}]\" aria-label=\"Keyword\"></div>\n  </form>\n</xo-view>"
     },
     "guide": "forms.html#written-by-hand",
     "keywords": "array list row reorder drag drop handle"
    },
    {
     "id": "xo-guard",
     "name": "xo-guard",
     "kind": "directive",
     "category": "Form rows",
     "package": "@xoong/ui",
     "summary": "On a <form xo-submit>: with unsaved changes the router asks before the page is left.",
     "description": "On a <form xo-submit>: with unsaved changes the router asks before the page is left. `xo-guard=\"off\"` disables it; any other text is the question to ask.",
     "applies": "A `<form xo-submit>`.",
     "value": "`off` disables the question; any other text is the question to ask.",
     "values": null,
     "notes": [],
     "example": {
      "lang": "html",
      "code": "<xo-view get=\"getPerson\" p-id=\"1\">\n  <form xo-submit=\"updatePerson\" xo-guard=\"Leave without saving?\">\n    <input name=\"name\" aria-label=\"Name\"> <button>Save</button>\n  </form>\n</xo-view>"
     },
     "guide": null,
     "keywords": "unsaved changes leave page dirty warn navigation"
    },
    {
     "id": "xo-route",
     "name": "xo-route",
     "kind": "directive",
     "category": "Routing",
     "package": "@xoong/ui",
     "summary": "On a <template> inside <xo-router>: the path this route answers, for example `/users/:id`; `*` is the not-found route.",
     "description": "On a <template> inside <xo-router>: the path this route answers, for example `/users/:id`; `*` is the not-found route. The content is inert until the route matches. An empty template with `src` fetches that URL the first time the route (or a layout) is shown.",
     "applies": "A `<template>` inside `<xo-router>`.",
     "value": "The path the route answers: `/users/:id`; `*` is the not-found route.",
     "values": null,
     "notes": [],
     "example": {
      "lang": "html",
      "code": "<xo-router>\n  <template xo-route=\"/users/:id\" title=\"User\"><xo-view get=\"getPerson\" p-id=\":id\"><h1>{name}</h1></xo-view></template>\n  <template xo-route=\"*\">Not found</template>\n</xo-router>"
     },
     "guide": "arch-routing.html",
     "keywords": "path url page screen router template"
    },
    {
     "id": "xo-layout",
     "name": "xo-layout",
     "kind": "directive",
     "category": "Routing",
     "package": "@xoong/ui",
     "summary": "On a <template> inside <xo-router>: a layout (shell) that routes use with `layout=\"name\"`.",
     "description": "On a <template> inside <xo-router>: a layout (shell) that routes use with `layout=\"name\"`. It must contain one element with xo-outlet.",
     "applies": "A `<template>` inside `<xo-router>`.",
     "value": "The name of the layout. Routes use it with `layout=\"name\"`.",
     "values": null,
     "notes": [],
     "example": {
      "lang": "html",
      "code": "<xo-router>\n  <template xo-layout=\"shell\"><nav><a href=\"/users\">Users</a></nav><main xo-outlet></main></template>\n  <template xo-route=\"/users\" layout=\"shell\">Users</template>\n</xo-router>"
     },
     "guide": "arch-routing.html",
     "keywords": "shell frame navigation sidebar wrapper"
    },
    {
     "id": "xo-outlet",
     "name": "xo-outlet",
     "kind": "directive",
     "category": "Routing",
     "package": "@xoong/ui",
     "summary": "In a layout: the element where the matched route content is shown.",
     "description": "In a layout: the element where the matched route content is shown. The layout stays while only this content changes.",
     "applies": "One element in a layout.",
     "value": "None.",
     "values": null,
     "notes": [],
     "example": {
      "lang": "html",
      "code": "<xo-router>\n  <template xo-layout=\"shell\"><header>My app</header><main xo-outlet></main></template>\n  <template xo-route=\"/\" layout=\"shell\">Home</template>\n</xo-router>"
     },
     "guide": "arch-routing.html",
     "keywords": "slot content area route placeholder"
    },
    {
     "id": "xo-current",
     "name": "xo-current",
     "kind": "directive",
     "category": "Routing",
     "package": "@xoong/ui",
     "summary": "On a link, a nav, or the router: leave aria-current as the author set it.",
     "description": "On a link, a nav, or the router: leave aria-current as the author set it. The router then does not mark that link current.",
     "applies": "A link, a nav, or `<xo-router>`.",
     "value": "Present. No value.",
     "values": null,
     "notes": [],
     "example": {
      "lang": "html",
      "code": "<nav xo-current>\n  <a href=\"/studio/pages\" aria-current=\"page\">Pages</a>\n</nav>"
     },
     "guide": "arch-routing.html",
     "keywords": "aria-current active nav current page author"
    },
    {
     "id": "xo-open",
     "name": "xo-open",
     "kind": "directive",
     "category": "Dialogs",
     "package": "@xoong/ui",
     "summary": "Open the <xo-dialog> matched by this selector, for example `#add`.",
     "description": "Open the <xo-dialog> matched by this selector, for example `#add`.",
     "applies": "A button anywhere in the page.",
     "value": "A selector for an `<xo-dialog>`: `#add`.",
     "values": null,
     "notes": [],
     "example": {
      "lang": "html",
      "code": "<button xo-open=\"#add\">Add person</button>\n<xo-dialog id=\"add\" heading=\"Add person\"><xo-form op=\"createPerson\"></xo-form></xo-dialog>"
     },
     "guide": "dialogs.html",
     "keywords": "show modal dialog button trigger open window"
    },
    {
     "id": "xo-close",
     "name": "xo-close",
     "kind": "directive",
     "category": "Dialogs",
     "package": "@xoong/ui",
     "summary": "Close the <xo-dialog> this element is in.",
     "description": "Close the <xo-dialog> this element is in.",
     "applies": "A button inside an `<xo-dialog>`.",
     "value": "None.",
     "values": null,
     "notes": [],
     "example": {
      "lang": "html",
      "code": "<xo-dialog id=\"info\" heading=\"Info\">\n  <p>Saved.</p>\n  <button slot=\"footer\" xo-close>Close</button>\n</xo-dialog>"
     },
     "guide": "dialogs.html",
     "keywords": "dismiss cancel hide modal dialog button"
    },
    {
     "id": "xo-tab",
     "name": "xo-tab",
     "kind": "directive",
     "category": "Controls",
     "package": "@xoong/controls",
     "summary": "On a section inside <xo-tabs>: the tab label.",
     "description": "On a section inside <xo-tabs>: the tab label. The section id is the tab key (its position when it has none).",
     "applies": "A `<section>` inside `<xo-tabs>`.",
     "value": "The tab label. The section's `id` is the key of the tab (its position without one).",
     "values": null,
     "notes": [],
     "example": {
      "lang": "html",
      "code": "<xo-tabs label=\"Page\"><section xo-tab=\"General\">…</section><section xo-tab=\"SEO\" id=\"seo\">…</section></xo-tabs>"
     },
     "guide": "tabs.html",
     "keywords": "tab label panel section"
    },
    {
     "id": "xo-menu-trigger",
     "name": "xo-menu-trigger",
     "kind": "directive",
     "category": "Controls",
     "package": "@xoong/controls",
     "summary": "On the button of <xo-menu> that opens it (the first button when none is marked).",
     "description": "On the button of <xo-menu> that opens it (the first button when none is marked).",
     "applies": "A button inside `<xo-menu>`.",
     "value": "None.",
     "values": null,
     "notes": [],
     "example": {
      "lang": "html",
      "code": "<xo-menu label=\"Actions\"><button xo-menu-trigger>Actions</button><button>Duplicate</button></xo-menu>"
     },
     "guide": "menu.html",
     "keywords": "button open dropdown actions"
    },
    {
     "id": "xo-tooltip",
     "name": "xo-tooltip",
     "kind": "directive",
     "category": "Controls",
     "package": "@xoong/controls",
     "summary": "On any element: a short description shown on hover and focus (and hidden on Escape).",
     "description": "On any element: a short description shown on hover and focus (and hidden on Escape). It is a description, not the accessible name.",
     "applies": "Any element.",
     "value": "The text of the tooltip.",
     "values": null,
     "notes": [
      "It describes the element; it is not its accessible name, so a button without text still needs `aria-label`."
     ],
     "example": {
      "lang": "html",
      "code": "<button xo-tooltip=\"Copy the link\" aria-label=\"Copy link\">Copy</button>"
     },
     "guide": "menu.html",
     "keywords": "hint hover description help title"
    },
    {
     "id": "xo-consent-open",
     "name": "xo-consent-open",
     "kind": "directive",
     "category": "Controls",
     "package": "@xoong/controls",
     "summary": "On any element: a click shows the <xo-consent> bar again (cookie settings).",
     "description": "On any element: a click shows the <xo-consent> bar again (cookie settings).",
     "applies": "Any element (a button).",
     "value": "None.",
     "values": null,
     "notes": [],
     "example": {
      "lang": "html",
      "code": "<button xo-consent-open>Cookie settings</button>"
     },
     "guide": "more-controls.html#page",
     "keywords": "cookies settings privacy reopen banner"
    },
    {
     "id": "xo-t",
     "name": "xo-t",
     "kind": "directive",
     "category": "Language",
     "package": "@xoong/i18n",
     "summary": "Translate this element with @xoong/i18n: `xo-t=\"key\"` for its text, `xo-t=\"attr:key; attr2:key2\"` for attributes.",
     "description": "Translate this element with @xoong/i18n: `xo-t=\"key\"` for its text, `xo-t=\"attr:key; attr2:key2\"` for attributes. The text written in the HTML is the default. `{placeholders}` come from the enclosing xo-view.",
     "applies": "Any element.",
     "value": "A message key for the text (`home.title`), or `attr:key; attr2:key2` for attributes.",
     "values": null,
     "notes": [
      "The text written in the HTML is the default. Inside an `<xo-view>`, `{placeholders}` come from the view."
     ],
     "example": {
      "lang": "html",
      "code": "<h1 xo-t=\"home.title\">Welcome</h1>\n<input xo-t=\"placeholder:search.placeholder\" placeholder=\"Search\" aria-label=\"Search\">"
     },
     "guide": "recipes.html",
     "keywords": "translate translation localise language locale catalog"
    }
   ]
  },
  {
   "id": "events",
   "label": "Events",
   "summary": "The events xoong elements fire, what is in `detail`, and who listens.",
   "count": 36,
   "entries": [
    {
     "id": "event-xo-done",
     "name": "xo-done",
     "kind": "event",
     "summary": "An action succeeded.",
     "description": "An action succeeded. Fires on the element that ran the action, after the server answered, and bubbles. `<xo-dialog>` closes when a form inside it fires it; `<xo-toast>` hides its error.",
     "from": "`<xo-view>` (xo-click, xo-submit, `<xo-form>`)",
     "detail": "The result of the operation.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-view",
      "xo-form"
     ],
     "keywords": "success saved finished complete",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-done', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-error",
     "name": "xo-error",
     "kind": "event",
     "summary": "A load or an action failed.",
     "description": "A load or an action failed. Fires on the element that ran the action (or the view that could not load) and bubbles. Cancelable: call `preventDefault()` when your own code handles the failure, and `<xo-toast>` stays quiet. `event.handled` is true when the view shows `$error` itself, or the error is a 401 that `onUnauthorized` deals with: the toast skips those too. `detail.action` (`{ label, run }`, for example Retry) and `detail.timeout` are drawn when the toast is not skipped.",
     "from": "`<xo-view>`, `<xo-router>`",
     "detail": "The `XoongError` (status, code, detail, fields). Set `action` (`{ label, run }`) and `timeout` on it and `<xo-toast>` shows that button (it reads them one microtask later).",
     "bubbles": true,
     "cancelable": true,
     "firedBy": [
      "xo-view",
      "xo-form",
      "xo-router"
     ],
     "keywords": "failure fail problem exception handled prevent toast retry",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-error', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-invalid",
     "name": "xo-invalid",
     "kind": "event",
     "summary": "A form was not sent because it is invalid.",
     "description": "A form was not sent because it is invalid. Nothing was sent; the form already shows why beside each field.",
     "from": "`<form xo-submit>`",
     "detail": "`{ errors }`: the messages by field path.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-view",
      "xo-form"
     ],
     "keywords": "validation errors fields",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-invalid', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-notice",
     "name": "xo-notice",
     "kind": "event",
     "summary": "A message for the toast.",
     "description": "A message for the toast. Fires after an action with `xo-notice` succeeded, and when you call `toast.show()`. `<xo-toast>` listens on the document.",
     "from": "`xo-notice`, `toast.show()`",
     "detail": "`{ message, kind }`; `toast.show()` adds `action` and `timeout`.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-view"
     ],
     "keywords": "toast message snackbar",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-notice', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-go",
     "name": "xo-go",
     "kind": "event",
     "summary": "An action asked to go to another page.",
     "description": "An action asked to go to another page. Cancelable. `<xo-router>` handles it and cancels the default; without a router the page is loaded with `location.assign`.",
     "from": "`xo-then`",
     "detail": "`{ path }`: where to go.",
     "bubbles": true,
     "cancelable": true,
     "firedBy": [
      "xo-view"
     ],
     "keywords": "redirect navigate then",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-go', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-navigate",
     "name": "xo-navigate",
     "kind": "event",
     "summary": "The router moved to another location.",
     "description": "The router moved to another location. Fires after a navigation, also when only the query or the hash changed.",
     "from": "`<xo-router>`",
     "detail": "`{ to, from }`: two route locations (`path`, `params`, `query`, `url`, `route`).",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-router"
     ],
     "keywords": "route change page url history",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-navigate', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-columns-change",
     "name": "xo-columns-change",
     "kind": "event",
     "summary": "The person showed or hid a column of the table.",
     "description": "The person showed or hid a column of the table. Fires from the Columns picker. With `columns-key` the choice is also remembered in this browser.",
     "from": "`<xo-table column-picker>`",
     "detail": "`{ visible, hidden }`: the field names that are shown and those that are hidden.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-table"
     ],
     "keywords": "columns show hide picker table toggle",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-columns-change', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-dirty",
     "name": "xo-dirty",
     "kind": "event",
     "summary": "A form gained or lost unsaved changes.",
     "description": "A form gained or lost unsaved changes. The router listens to it to ask before the page is left.",
     "from": "`<form xo-submit>`",
     "detail": "`{ dirty }`: true when there are unsaved changes.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-form"
     ],
     "keywords": "unsaved changes modified",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-dirty', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-reveal",
     "name": "xo-reveal",
     "kind": "event",
     "summary": "The form is about to focus an invalid field.",
     "description": "The form is about to focus an invalid field. Cancelable. `<xo-tabs>` listens and selects the tab of the field first, so the field is visible when it gets the focus.",
     "from": "`<form xo-submit>`",
     "detail": "`{ path }`: the invalid field.",
     "bubbles": true,
     "cancelable": true,
     "firedBy": [
      "xo-form"
     ],
     "keywords": "invalid focus tab field error",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-reveal', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-array-change",
     "name": "xo-array-change",
     "kind": "event",
     "summary": "A row of an array in a form was added, removed or moved.",
     "description": "A row of an array in a form was added, removed or moved. Fires for the add, remove and move buttons, a drag, and code that changes the array.",
     "from": "`<form xo-submit>`",
     "detail": "`{ path, op, from, to }`: `op` is `add`, `remove` or `move`.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [],
     "keywords": "row list add remove move reorder",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-array-change', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-form-ready",
     "name": "xo-form-ready",
     "kind": "event",
     "summary": "The form model exists.",
     "description": "The form model exists. From here `form.model` is available. A child control that needs the model listens for this event.",
     "from": "`<form xo-submit>`",
     "detail": "`{ model }`: the form model.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-form"
     ],
     "keywords": "model attach",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-form-ready', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-pick-media",
     "name": "xo-pick-media",
     "kind": "event",
     "summary": "The image tool of the editor was used.",
     "description": "The image tool of the editor was used. Cancelable. Open your media library in a listener, call `preventDefault()`, and call `detail.insert` now or later. Without `preventDefault()` nothing is inserted.",
     "from": "`<xo-richtext>`",
     "detail": "`{ insert }`: call `insert({ src, alt })` when the person has chosen.",
     "bubbles": true,
     "cancelable": true,
     "firedBy": [
      "xo-richtext"
     ],
     "keywords": "image upload media library editor",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-pick-media', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-richtext-expand",
     "name": "xo-richtext-expand",
     "kind": "event",
     "summary": "The editor opened in its large dialog.",
     "description": "The editor opened in its large dialog.",
     "from": "`<xo-richtext>`",
     "detail": "None.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-richtext"
     ],
     "keywords": "fullscreen expand editor",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-richtext-expand', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-richtext-done",
     "name": "xo-richtext-done",
     "kind": "event",
     "summary": "The large editor dialog was closed.",
     "description": "The large editor dialog was closed. Fires for Done and for Escape.",
     "from": "`<xo-richtext>`",
     "detail": "None.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-richtext"
     ],
     "keywords": "collapse close editor",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-richtext-done', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-search",
     "name": "xo-search",
     "kind": "event",
     "summary": "The person typed in a combobox that does not filter by itself.",
     "description": "The person typed in a combobox that does not filter by itself. Set `el.options` in the listener.",
     "from": "`<xo-combobox remote>`",
     "detail": "`{ query }`: what the person typed.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-combobox"
     ],
     "keywords": "autocomplete remote filter typeahead",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-search', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-tab-change",
     "name": "xo-tab-change",
     "kind": "event",
     "summary": "Another tab was selected.",
     "description": "Another tab was selected.",
     "from": "`<xo-tabs>`",
     "detail": "`{ value }`: the key of the selected tab.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-tabs"
     ],
     "keywords": "tabs selected",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-tab-change', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-menu-select",
     "name": "xo-menu-select",
     "kind": "event",
     "summary": "An item of a choice menu was chosen.",
     "description": "An item of a choice menu was chosen. Only for items that have a `value` attribute.",
     "from": "`<xo-menu>`",
     "detail": "`{ value }`: the `value` of the chosen item.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-menu"
     ],
     "keywords": "choice radio dropdown",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-menu-select', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-menu-open",
     "name": "xo-menu-open",
     "kind": "event",
     "summary": "A menu opened.",
     "description": "A menu opened.",
     "from": "`<xo-menu>`",
     "detail": "None.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-menu"
     ],
     "keywords": "dropdown",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-menu-open', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-menu-close",
     "name": "xo-menu-close",
     "kind": "event",
     "summary": "A menu closed.",
     "description": "A menu closed.",
     "from": "`<xo-menu>`",
     "detail": "None.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-menu"
     ],
     "keywords": "dropdown",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-menu-close', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-carousel-change",
     "name": "xo-carousel-change",
     "kind": "event",
     "summary": "The carousel moved to another slide.",
     "description": "The carousel moved to another slide.",
     "from": "`<xo-carousel>`",
     "detail": "`{ index, count }`: the first visible slide and the number of slides.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-carousel"
     ],
     "keywords": "slide slider",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-carousel-change', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-file-reject",
     "name": "xo-file-reject",
     "kind": "event",
     "summary": "A file was refused.",
     "description": "A file was refused.",
     "from": "`<xo-file>`",
     "detail": "`{ file, reason }`: `reason` is `type` or `size`.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-file"
     ],
     "keywords": "upload drop invalid",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-file-reject', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-command-run",
     "name": "xo-command-run",
     "kind": "event",
     "summary": "A command of the palette was run.",
     "description": "A command of the palette was run.",
     "from": "`<xo-command>`",
     "detail": "`{ value, label }`: `data-value` (or the href), and the text of the command.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-command"
     ],
     "keywords": "palette search",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-command-run', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-consent",
     "name": "xo-consent",
     "kind": "event",
     "summary": "The visitor made a choice about cookies.",
     "description": "The visitor made a choice about cookies.",
     "from": "`<xo-consent>`",
     "detail": "The choices: one boolean per category (`{ analytics: true, marketing: false }`).",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-consent"
     ],
     "keywords": "cookies privacy gdpr",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-consent', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-theme-change",
     "name": "xo-theme-change",
     "kind": "event",
     "summary": "The theme was switched.",
     "description": "The theme was switched.",
     "from": "`<xo-theme-toggle>`",
     "detail": "`{ theme }`: `light` or `dark`.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-theme-toggle"
     ],
     "keywords": "dark mode light colour",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-theme-change', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-popover-open",
     "name": "xo-popover-open",
     "kind": "event",
     "summary": "A popover opened.",
     "description": "A popover opened.",
     "from": "`<xo-popover>`",
     "detail": "None.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-popover"
     ],
     "keywords": "panel",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-popover-open', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-popover-close",
     "name": "xo-popover-close",
     "kind": "event",
     "summary": "A popover closed.",
     "description": "A popover closed.",
     "from": "`<xo-popover>`",
     "detail": "None.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-popover"
     ],
     "keywords": "panel",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-popover-close', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-accordion-change",
     "name": "xo-accordion-change",
     "kind": "event",
     "summary": "A section of an accordion opened or closed.",
     "description": "A section of an accordion opened or closed.",
     "from": "`<xo-accordion>`",
     "detail": "`{ index, open }`: which section and whether it is open now.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-accordion"
     ],
     "keywords": "collapse expand details",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-accordion-change', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-copied",
     "name": "copied",
     "kind": "event",
     "summary": "The text was copied to the clipboard.",
     "description": "The text was copied to the clipboard.",
     "from": "`<xo-copy>`",
     "detail": "`{ text }`: what was copied.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-copy"
     ],
     "keywords": "clipboard copy",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('copied', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-copy-failed",
     "name": "copy-failed",
     "kind": "event",
     "summary": "The browser refused the clipboard.",
     "description": "The browser refused the clipboard.",
     "from": "`<xo-copy>`",
     "detail": "`{ text }`: what could not be copied.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-copy"
     ],
     "keywords": "clipboard copy",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('copy-failed', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-complete",
     "name": "complete",
     "kind": "event",
     "summary": "Every box of a one-time code is filled.",
     "description": "Every box of a one-time code is filled.",
     "from": "`<xo-otp>`",
     "detail": "`{ value }`: the digits.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-otp"
     ],
     "keywords": "code pin otp digits",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('complete', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-save",
     "name": "save",
     "kind": "event",
     "summary": "The new text of an inline edit was saved.",
     "description": "The new text of an inline edit was saved. Enter saves.",
     "from": "`<xo-inline-edit>`",
     "detail": "`{ value }`: the new text.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-inline-edit"
     ],
     "keywords": "rename inline edit",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('save', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-dismiss",
     "name": "xo-dismiss",
     "kind": "event",
     "summary": "The Close button of an alert was pressed.",
     "description": "The Close button of an alert was pressed.",
     "from": "`<xo-alert>`",
     "detail": "None. `preventDefault()` keeps the alert.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-alert"
     ],
     "keywords": "close banner",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-dismiss', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-tree-select",
     "name": "xo-tree-select",
     "kind": "event",
     "summary": "The choice in a tree changed.",
     "description": "The choice in a tree changed.",
     "from": "`<xo-tree>`",
     "detail": "`{ id, selected }`: the item acted on and the chosen ids.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-tree"
     ],
     "keywords": "checkbox selection",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-tree-select', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-tree-toggle",
     "name": "xo-tree-toggle",
     "kind": "event",
     "summary": "A tree item opened or closed.",
     "description": "A tree item opened or closed.",
     "from": "`<xo-tree>`",
     "detail": "`{ id, expanded }`: the item and whether it is open now.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-tree"
     ],
     "keywords": "expand collapse",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-tree-toggle', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-xo-tree-load",
     "name": "xo-tree-load",
     "kind": "event",
     "summary": "A lazy tree item was opened for the first time.",
     "description": "A lazy tree item was opened for the first time.",
     "from": "`<xo-tree>`",
     "detail": "`{ id }`: the lazy item; answer with `tree.setChildren(id, children)`.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-tree"
     ],
     "keywords": "lazy children load",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-tree-load', (event) => console.log(event.detail));"
     }
    },
    {
     "id": "event-cancel",
     "name": "cancel",
     "kind": "event",
     "summary": "An inline edit was cancelled.",
     "description": "An inline edit was cancelled. Escape cancels.",
     "from": "`<xo-inline-edit>`",
     "detail": "None.",
     "bubbles": true,
     "cancelable": false,
     "firedBy": [
      "xo-inline-edit"
     ],
     "keywords": "rename inline edit",
     "example": {
      "lang": "js",
      "code": "document.addEventListener('cancel', (event) => console.log(event.detail));"
     }
    }
   ]
  },
  {
   "id": "api",
   "label": "JavaScript API",
   "summary": "The functions and objects you import when HTML is not enough.",
   "count": 46,
   "entries": [
    {
     "id": "api-configure",
     "name": "configure",
     "kind": "api",
     "package": "@xoong/data",
     "import": "@xoong/data",
     "type": "function",
     "signature": "configure(options)",
     "returns": "Client",
     "declaration": "function configure<Ops extends Record<keyof Ops, OperationType> = any>(options: ClientOptions): Client<Ops>;",
     "summary": "Create the app-wide client once, at start-up.",
     "description": "Every `<xo-view>`, `xo-click` and `xo-submit` uses this client. Call it before the views start; views that started earlier wait for it.",
     "params": [
      {
       "name": "api",
       "type": "object",
       "description": "Required. The contents of `.xoong/operations.json` (written by `xoong sync`)."
      },
      {
       "name": "baseUrl",
       "type": "string",
       "description": "Prefix of every request URL, for example `/api`."
      },
      {
       "name": "headers",
       "type": "function",
       "description": "Returns the headers of a request, or a promise of them: authentication."
      },
      {
       "name": "credentials",
       "type": "string",
       "description": "The `fetch` credentials mode (`same-origin`, `include`, `omit`)."
      },
      {
       "name": "staleTime",
       "type": "number",
       "description": "Milliseconds a GET result stays fresh. Default 30000."
      },
      {
       "name": "gcTime",
       "type": "number",
       "description": "Milliseconds an unobserved query is kept. Default 300000."
      },
      {
       "name": "evictEntities",
       "type": "boolean",
       "description": "Drop the entities that no cached query refers to when a query is collected. Default true."
      },
      {
       "name": "retry",
       "type": "number",
       "description": "Retries of a GET after a network error or a 502, 503 or 504. Default 2."
      },
      {
       "name": "retryDelay",
       "type": "number",
       "description": "Base delay in milliseconds, doubled per retry. Default 300."
      },
      {
       "name": "invalidate",
       "type": "object",
       "description": "Which queries a mutation refreshes, by operation: `{ shipOrder: ['getDashboard'] }`."
      },
      {
       "name": "onError",
       "type": "function",
       "description": "Called with every `XoongError`."
      },
      {
       "name": "onUnauthorized",
       "type": "function",
       "description": "Called with a 401 error (`onError` is called too)."
      },
      {
       "name": "streamUrl",
       "type": "string",
       "description": "The server-sent events endpoint that `<xo-view live>` listens to."
      },
      {
       "name": "fetch",
       "type": "function",
       "description": "Your own `fetch`, for tests or a custom transport."
      }
     ],
     "example": {
      "lang": "js",
      "code": "import { configure } from '@xoong/data';\nimport '@xoong/data/view';\nimport operations from '/.xoong/operations.json' with { type: 'json' };\n\nconfigure({ api: operations, baseUrl: '/api', streamUrl: '/api/events' });"
     },
     "guide": "install.html",
     "keywords": "setup start init client api baseUrl headers auth retry cache"
    },
    {
     "id": "api-get-client",
     "name": "getClient",
     "kind": "api",
     "package": "@xoong/data",
     "import": "@xoong/data",
     "type": "function",
     "signature": "getClient()",
     "returns": "Client",
     "declaration": "function getClient<Ops extends Record<keyof Ops, OperationType> = any>(): Client<Ops>;",
     "summary": "The client that `configure()` created.",
     "description": "Use it when HTML is not enough: `getClient().load(...)`, `.mutate(...)`, `.reset()`.",
     "params": [],
     "example": {
      "lang": "js",
      "code": "import { getClient } from '@xoong/data';\n\nconst people = await getClient().load('listPeople', { limit: 5 });"
     },
     "guide": "install.html",
     "keywords": "client instance fetch"
    },
    {
     "id": "api-create-client",
     "name": "createClient",
     "kind": "api",
     "package": "@xoong/data",
     "import": "@xoong/data",
     "type": "function",
     "signature": "createClient(options)",
     "returns": "Client",
     "declaration": "function createClient<Ops extends Record<keyof Ops, OperationType> = any>(options: ClientOptions): Client<Ops>;",
     "summary": "Build a client without making it the app-wide one.",
     "description": "Takes the same options as `configure()`. Useful in tests and for a second API.",
     "params": [
      {
       "name": "api",
       "type": "object",
       "description": "Required. The contents of `.xoong/operations.json` (written by `xoong sync`)."
      },
      {
       "name": "baseUrl",
       "type": "string",
       "description": "Prefix of every request URL, for example `/api`."
      },
      {
       "name": "headers",
       "type": "function",
       "description": "Returns the headers of a request, or a promise of them: authentication."
      },
      {
       "name": "credentials",
       "type": "string",
       "description": "The `fetch` credentials mode (`same-origin`, `include`, `omit`)."
      },
      {
       "name": "staleTime",
       "type": "number",
       "description": "Milliseconds a GET result stays fresh. Default 30000."
      },
      {
       "name": "gcTime",
       "type": "number",
       "description": "Milliseconds an unobserved query is kept. Default 300000."
      },
      {
       "name": "evictEntities",
       "type": "boolean",
       "description": "Drop the entities that no cached query refers to when a query is collected. Default true."
      },
      {
       "name": "retry",
       "type": "number",
       "description": "Retries of a GET after a network error or a 502, 503 or 504. Default 2."
      },
      {
       "name": "retryDelay",
       "type": "number",
       "description": "Base delay in milliseconds, doubled per retry. Default 300."
      },
      {
       "name": "invalidate",
       "type": "object",
       "description": "Which queries a mutation refreshes, by operation: `{ shipOrder: ['getDashboard'] }`."
      },
      {
       "name": "onError",
       "type": "function",
       "description": "Called with every `XoongError`."
      },
      {
       "name": "onUnauthorized",
       "type": "function",
       "description": "Called with a 401 error (`onError` is called too)."
      },
      {
       "name": "streamUrl",
       "type": "string",
       "description": "The server-sent events endpoint that `<xo-view live>` listens to."
      },
      {
       "name": "fetch",
       "type": "function",
       "description": "Your own `fetch`, for tests or a custom transport."
      }
     ],
     "example": {
      "lang": "js",
      "code": "import { createClient } from '@xoong/data';\n\nconst client = createClient({ api: operations, baseUrl: '/api' });"
     },
     "guide": null,
     "keywords": "client instance test second api typed"
    },
    {
     "id": "api-client-load",
     "name": "client.load",
     "kind": "api",
     "package": "@xoong/data",
     "import": "@xoong/data",
     "type": "method",
     "signature": "client.load(operation, params?)",
     "returns": "Promise<response>",
     "declaration": "load<K extends Id<Ops>>(op: K, ...params: ParamsArg<Ops[K]>): Promise<Ops[K]['response']>;",
     "summary": "Read a GET operation and resolve with the response.",
     "description": "Uses the cache while the result is fresh (`staleTime`). Throws a `XoongError` when the request fails.",
     "params": [
      {
       "name": "operation",
       "type": "string",
       "description": "An operationId or `GET /path`."
      },
      {
       "name": "params",
       "type": "object",
       "description": "Path and query parameters."
      }
     ],
     "example": {
      "lang": "js",
      "code": "const page = await getClient().load('listPeople', { limit: 10, sort: 'name' });"
     },
     "guide": "data.html",
     "keywords": "fetch get read request data"
    },
    {
     "id": "api-client-query",
     "name": "client.query",
     "kind": "api",
     "package": "@xoong/data",
     "import": "@xoong/data",
     "type": "method",
     "signature": "client.query(operation, params?)",
     "returns": "{ data, status, error, retain(), release(), invalidate(), fetch() }",
     "declaration": "query<K extends Id<Ops>>(op: K, ...params: ParamsArg<Ops[K]>): TypedQuery<Ops[K]>;",
     "summary": "A GET operation as signals.",
     "description": "`data`, `status` (`idle`, `loading`, `success`, `error`) and `error` are signals: read them inside an `effect` or a template. `retain()` and `release()` keep the query alive while you observe it.",
     "params": [
      {
       "name": "operation",
       "type": "string",
       "description": "An operationId or `GET /path`."
      },
      {
       "name": "params",
       "type": "object",
       "description": "Path and query parameters."
      }
     ],
     "example": {
      "lang": "js",
      "code": "const query = getClient().query('listPeople', { limit: 5 });\nquery.retain();\neffect(() => console.log(query.status.value, query.data.value));"
     },
     "guide": null,
     "keywords": "signals reactive observe status loading error"
    },
    {
     "id": "api-client-mutate",
     "name": "client.mutate",
     "kind": "api",
     "package": "@xoong/data",
     "import": "@xoong/data",
     "type": "method",
     "signature": "client.mutate(operation, params, body?)",
     "returns": "Promise<response>",
     "declaration": "mutate<K extends Id<Ops>>(op: K, params: Ops[K]['params'], ...body: [Ops[K]['body']] extends [never] ? [] : [body: Ops[K]['body']]): Promise<Ops[K]['response']>;",
     "summary": "Run a write operation (POST, PUT, PATCH, DELETE).",
     "description": "Resolves with the response. The queries whose path overlaps the mutation are refetched afterwards (see `invalidate` in `configure`).",
     "params": [
      {
       "name": "operation",
       "type": "string",
       "description": "An operationId or `METHOD /path`."
      },
      {
       "name": "params",
       "type": "object",
       "description": "Path parameters."
      },
      {
       "name": "body",
       "type": "object",
       "description": "The request body."
      }
     ],
     "example": {
      "lang": "js",
      "code": "await getClient().mutate('createPerson', {}, { name: 'Ada Lovelace', email: 'ada@example.org' });"
     },
     "guide": "data.html",
     "keywords": "post put patch delete create update write save"
    },
    {
     "id": "api-client-seed",
     "name": "client.seed",
     "kind": "api",
     "package": "@xoong/data",
     "import": "@xoong/data",
     "type": "method",
     "signature": "client.seed(operation, params, data)",
     "returns": "the data",
     "declaration": "seed<K extends Id<Ops>>(op: K, params: Ops[K] extends { params: infer P } ? P : never, data: Ops[K]['response']): Ops[K]['response'];",
     "summary": "Put data the server already has into the cache.",
     "description": "As if the GET had run: no request is made until the result is stale. For data embedded in the page.",
     "params": [
      {
       "name": "operation",
       "type": "string",
       "description": "An operationId."
      },
      {
       "name": "params",
       "type": "object",
       "description": "The parameters the GET would have had."
      },
      {
       "name": "data",
       "type": "object",
       "description": "The response."
      }
     ],
     "example": {
      "lang": "js",
      "code": "getClient().seed('listPeople', { limit: 5 }, { items: [], total: 0 });"
     },
     "guide": null,
     "keywords": "cache preload hydrate prerender initial data"
    },
    {
     "id": "api-client-stream",
     "name": "client.stream",
     "kind": "api",
     "package": "@xoong/data",
     "import": "@xoong/data",
     "type": "method",
     "signature": "client.stream(operation, params, body?, { signal }?)",
     "returns": "AsyncGenerator<SseEvent>",
     "declaration": "stream<K extends Id<Ops>>(op: K, params: Ops[K]['params'], body?: Ops[K]['body'], options?: { signal?: AbortSignal }): AsyncGenerator<SseEvent>;",
     "summary": "Call an operation that answers with server-sent events.",
     "description": "Each event is `{ event, data, json, id }`; `json` is `data` parsed when it is JSON. Pass an `AbortSignal` to stop.",
     "params": [
      {
       "name": "operation",
       "type": "string",
       "description": "An operationId."
      },
      {
       "name": "params",
       "type": "object",
       "description": "Path parameters."
      },
      {
       "name": "body",
       "type": "object",
       "description": "The request body."
      }
     ],
     "example": {
      "lang": "js",
      "code": "for await (const event of getClient().stream('sendMessage', {}, { text: 'Hello' })) {\n  console.log(event.json ?? event.data);\n}"
     },
     "guide": null,
     "keywords": "sse server sent events chat streaming tokens"
    },
    {
     "id": "api-client-invalidate-all",
     "name": "client.invalidateAll",
     "kind": "api",
     "package": "@xoong/data",
     "import": "@xoong/data",
     "type": "method",
     "signature": "client.invalidateAll()",
     "returns": "void",
     "declaration": "invalidateAll(): void;",
     "summary": "Mark every cached query stale.",
     "description": "Views on screen fetch again.",
     "params": [],
     "example": {
      "lang": "js",
      "code": "getClient().invalidateAll();"
     },
     "guide": null,
     "keywords": "refresh reload stale cache"
    },
    {
     "id": "api-client-reset",
     "name": "client.reset",
     "kind": "api",
     "package": "@xoong/data",
     "import": "@xoong/data",
     "type": "method",
     "signature": "client.reset()",
     "returns": "void",
     "declaration": "reset(): void;",
     "summary": "Forget everything that was loaded.",
     "description": "For a logout or another user: requests in flight are cancelled, cached queries and entities are dropped, and what a mounted view shows is fetched again. Mutations already sent are not cancelled.",
     "params": [],
     "example": {
      "lang": "js",
      "code": "configure({ api: operations, onUnauthorized: () => { getClient().reset(); router.navigate('/login'); } });"
     },
     "guide": null,
     "keywords": "logout sign out clear cache user switch"
    },
    {
     "id": "api-derived",
     "name": "derived",
     "kind": "api",
     "package": "@xoong/data",
     "import": "@xoong/data",
     "type": "function",
     "signature": "derived(({ all, get }) => value, client?)",
     "returns": "Signal",
     "declaration": "function derived<T>(fn: (ctx: { all(name: string): any[]; get(name: string, id: string | number): any }) => T, client?: Client<any>): Signal<T>;",
     "summary": "A value computed from the entities that are loaded.",
     "description": "`all('Order')` lists every loaded entity of a type, `get(name, id)` returns one. It recomputes when they change and never makes a request: fetching is declared on a view.",
     "params": [],
     "example": {
      "lang": "js",
      "code": "import { derived } from '@xoong/data';\n\nconst openOrders = derived(({ all }) => all('Order').filter((o) => o.status === 'open').length);"
     },
     "guide": null,
     "keywords": "computed total sum count entities store signal"
    },
    {
     "id": "api-connect-stream",
     "name": "connectStream",
     "kind": "api",
     "package": "@xoong/data",
     "import": "@xoong/data",
     "type": "function",
     "signature": "connectStream({ client, url, EventSource?, onStatus?, onError? })",
     "returns": "{ close() }",
     "declaration": "function connectStream(options: { client: Client<any>; url: string; EventSource?: typeof EventSource; onStatus?: (s: 'open' | 'error' | 'closed') => void; onError?: (e: Error) => void }): { close(): void };",
     "summary": "Apply a stream of entity events to a client.",
     "description": "The events are `patch`, `upsert`, `delete` and `resync`. `configure({ streamUrl })` does this for you when a view has `live`.",
     "params": [],
     "example": {
      "lang": "js",
      "code": "import { connectStream, getClient } from '@xoong/data';\n\nconst stream = connectStream({ client: getClient(), url: '/api/events' });"
     },
     "guide": null,
     "keywords": "sse live realtime push updates events"
    },
    {
     "id": "api-xoong-error",
     "name": "XoongError",
     "kind": "api",
     "package": "@xoong/data",
     "import": "@xoong/data",
     "type": "class",
     "signature": "class XoongError extends Error",
     "returns": null,
     "declaration": "class XoongError extends Error {\n  status: number;\n  title?: string;\n  detail?: string;\n  code?: string;\n  retryAfterS?: number;\n  problem?: any;\n  operation?: string;\n  /** Per-field messages when the server reports them (`{ email: 'is not valid' }`). */\n  fields: Record<string, string>;\n}",
     "summary": "The error of a failed request.",
     "description": "Built from problem+json or `{ error: { code, message } }`. Fields: `status`, `title`, `detail`, `code`, `retryAfterS`, `operation`, and `fields`, the messages by field (`{ email: 'is not valid' }`).",
     "params": [],
     "example": {
      "lang": "js",
      "code": "document.addEventListener('xo-error', (event) => console.warn(event.detail.status, event.detail.detail));"
     },
     "guide": null,
     "keywords": "error exception failure problem json 401 validation"
    },
    {
     "id": "api-set-format",
     "name": "setFormat",
     "kind": "api",
     "package": "@xoong/data",
     "import": "@xoong/data/view",
     "type": "function",
     "signature": "setFormat(name, fn)",
     "returns": "void",
     "declaration": "export function setFormat(name: string, fn: (value: any, locale?: string) => string): void",
     "summary": "Add or replace a display format.",
     "description": "Then `{price | currency}` works in any view. The function receives the value and the active language. A format that throws shows the plain value.",
     "params": [
      {
       "name": "name",
       "type": "string",
       "description": "The name used after `|`."
      },
      {
       "name": "fn",
       "type": "function",
       "description": "`(value, locale) => string`."
      }
     ],
     "example": {
      "lang": "js",
      "code": "import { setFormat } from '@xoong/data/view';\n\nsetFormat('currency', (value, locale) => new Intl.NumberFormat(locale, { style: 'currency', currency: 'EUR' }).format(value));"
     },
     "guide": "recipes.html",
     "keywords": "format currency money date number custom pipe filter"
    },
    {
     "id": "api-confirm",
     "name": "confirm",
     "kind": "api",
     "package": "@xoong/ui",
     "import": "@xoong/ui",
     "type": "function",
     "signature": "confirm(message, options?)",
     "returns": "Promise<boolean>",
     "declaration": "function confirm(message: string, options?: DialogLook & { ok?: string; cancel?: string }): Promise<boolean>;",
     "summary": "Ask a yes or no question.",
     "description": "The built-in dialog, a native modal. Without a `kind` the confirming button is red, as for something that cannot be undone. Escape answers false.",
     "params": [
      {
       "name": "title",
       "type": "string",
       "description": "A heading above the message."
      },
      {
       "name": "kind",
       "type": "string",
       "description": "`info`, `success`, `warning` or `danger`: an icon and the matching colour."
      },
      {
       "name": "detail",
       "type": "string",
       "description": "A second, quieter line."
      },
      {
       "name": "ok",
       "type": "string",
       "description": "The text of the confirming button."
      },
      {
       "name": "cancel",
       "type": "string",
       "description": "The text of the cancelling button."
      }
     ],
     "example": {
      "lang": "js",
      "code": "import { confirm } from '@xoong/ui';\n\nif (await confirm('Delete 3 items?', { kind: 'danger', ok: 'Delete' })) remove();"
     },
     "guide": "dialogs.html",
     "keywords": "dialog modal question yes no sure delete ask"
    },
    {
     "id": "api-alert",
     "name": "alert",
     "kind": "api",
     "package": "@xoong/ui",
     "import": "@xoong/ui",
     "type": "function",
     "signature": "alert(message, options?)",
     "returns": "Promise<void>",
     "declaration": "function alert(message: string, options?: DialogLook & { ok?: string }): Promise<void>;",
     "summary": "Tell the person something they must see.",
     "description": "One button; resolves when the dialog is closed. `kind` defaults to `info`.",
     "params": [
      {
       "name": "title",
       "type": "string",
       "description": "A heading above the message."
      },
      {
       "name": "kind",
       "type": "string",
       "description": "`info`, `success`, `warning` or `danger`: an icon and the matching colour."
      },
      {
       "name": "detail",
       "type": "string",
       "description": "A second, quieter line."
      },
      {
       "name": "ok",
       "type": "string",
       "description": "The text of the button."
      }
     ],
     "example": {
      "lang": "js",
      "code": "await alert('Saved', { kind: 'success' });"
     },
     "guide": "dialogs.html",
     "keywords": "dialog modal message info success warning error notify"
    },
    {
     "id": "api-prompt",
     "name": "prompt",
     "kind": "api",
     "package": "@xoong/ui",
     "import": "@xoong/ui",
     "type": "function",
     "signature": "prompt(message, options?)",
     "returns": "Promise<string | null>",
     "declaration": "function prompt(message: string, options?: DialogLook & { value?: string; label?: string; placeholder?: string; type?: string; required?: boolean; pattern?: string; ok?: string; cancel?: string }): Promise<string | null>;",
     "summary": "Ask for one line of text.",
     "description": "Resolves with the text, or null when cancelled. The field is native, so `required`, `pattern` and `type` are checked by the browser.",
     "params": [
      {
       "name": "title",
       "type": "string",
       "description": "A heading above the message."
      },
      {
       "name": "kind",
       "type": "string",
       "description": "`info`, `success`, `warning` or `danger`: an icon and the matching colour."
      },
      {
       "name": "detail",
       "type": "string",
       "description": "A second, quieter line."
      },
      {
       "name": "value",
       "type": "string",
       "description": "The starting text."
      },
      {
       "name": "label",
       "type": "string",
       "description": "The accessible name of the field."
      },
      {
       "name": "placeholder",
       "type": "string",
       "description": "Hint text of the empty field."
      },
      {
       "name": "type",
       "type": "string",
       "description": "The input type."
      },
      {
       "name": "required",
       "type": "boolean",
       "description": "The field must be filled."
      },
      {
       "name": "pattern",
       "type": "string",
       "description": "A pattern the text must match."
      },
      {
       "name": "ok",
       "type": "string",
       "description": "The text of the confirming button."
      },
      {
       "name": "cancel",
       "type": "string",
       "description": "The text of the cancelling button."
      }
     ],
     "example": {
      "lang": "js",
      "code": "const name = await prompt('Name this view', { value: 'My view', required: true });"
     },
     "guide": "dialogs.html",
     "keywords": "dialog modal input text name rename ask"
    },
    {
     "id": "api-ask",
     "name": "ask",
     "kind": "api",
     "package": "@xoong/ui",
     "import": "@xoong/ui",
     "type": "function",
     "signature": "ask(message, { buttons, ...look })",
     "returns": "Promise<unknown>",
     "declaration": "function ask(message: string, options: DialogLook & { buttons: DialogChoice[] }): Promise<unknown>;",
     "summary": "Offer any set of choices.",
     "description": "Resolves with the `value` of the button pressed; Escape answers with the `value` of the one marked `cancel`, or null when none is.",
     "params": [
      {
       "name": "buttons",
       "type": "array",
       "description": "Required. Each button is `{ label, value?, primary?, danger?, cancel? }`."
      },
      {
       "name": "title",
       "type": "string",
       "description": "A heading above the message."
      },
      {
       "name": "kind",
       "type": "string",
       "description": "`info`, `success`, `warning` or `danger`: an icon and the matching colour."
      },
      {
       "name": "detail",
       "type": "string",
       "description": "A second, quieter line."
      }
     ],
     "example": {
      "lang": "js",
      "code": "const answer = await ask('Save changes?', {\n  buttons: [{ label: 'Discard', value: 'no' }, { label: 'Save', value: 'yes', primary: true }],\n});"
     },
     "guide": "dialogs.html",
     "keywords": "dialog modal choices save discard keep editing buttons"
    },
    {
     "id": "api-confirm-labels",
     "name": "confirmLabels",
     "kind": "api",
     "package": "@xoong/ui",
     "import": "@xoong/ui",
     "type": "object",
     "signature": "confirmLabels.ok, confirmLabels.cancel",
     "returns": null,
     "declaration": "const confirmLabels: { ok: string; cancel: string };",
     "summary": "The default button texts of `confirm()`.",
     "description": "Replace them for another wording or another language: `confirmLabels.ok = 'Delete'`.",
     "params": [],
     "example": {
      "lang": "js",
      "code": "import { confirmLabels } from '@xoong/ui';\n\nconfirmLabels.ok = 'Yes, delete';"
     },
     "guide": "dialogs.html",
     "keywords": "translate wording text labels buttons"
    },
    {
     "id": "api-set-confirm",
     "name": "setConfirm",
     "kind": "api",
     "package": "@xoong/ui",
     "import": "@xoong/ui",
     "type": "function",
     "signature": "setConfirm(fn)",
     "returns": "void",
     "declaration": "function setConfirm(fn: (message: string, context?: ConfirmContext) => boolean | Promise<boolean>): void;",
     "summary": "Replace the confirmation dialog.",
     "description": "For `xo-confirm` and the question asked when a form with unsaved changes is left. `fn(message, context)` returns true (or a promise of it) to go on.",
     "params": [],
     "example": {
      "lang": "js",
      "code": "import { setConfirm } from '@xoong/ui';\n\nsetConfirm((message) => myDialog.ask(message));"
     },
     "guide": "dialogs.html",
     "keywords": "custom dialog override confirm own"
    },
    {
     "id": "api-toast-show",
     "name": "toast.show",
     "kind": "api",
     "package": "@xoong/ui",
     "import": "@xoong/ui",
     "type": "method",
     "signature": "toast.show({ message, kind?, action?, timeout? })",
     "returns": "void",
     "declaration": "show(notice: { message: string; kind?: string; action?: { label: string; run: () => unknown }; timeout?: number }): void;",
     "summary": "Show a notice in `<xo-toast>`.",
     "description": "A message that asks nothing and may offer one action, for example Undo. It is the same notice `xo-notice` shows.",
     "params": [
      {
       "name": "message",
       "type": "string",
       "description": "The text."
      },
      {
       "name": "kind",
       "type": "string",
       "description": "`success` or `info` (the default)."
      },
      {
       "name": "action",
       "type": "object",
       "description": "`{ label, run }`: a button, for example Undo."
      },
      {
       "name": "timeout",
       "type": "number",
       "description": "Seconds before it hides: 6 unless `<xo-toast timeout>` says otherwise; 0 keeps it until it is dismissed."
      }
     ],
     "example": {
      "lang": "js",
      "code": "import { toast } from '@xoong/ui';\n\ntoast.show({ message: 'Published 3', action: { label: 'Undo', run: undo } });"
     },
     "guide": "dialogs.html",
     "keywords": "notification snackbar message undo feedback"
    },
    {
     "id": "api-router-navigate",
     "name": "router.navigate",
     "kind": "api",
     "package": "@xoong/ui",
     "import": "@xoong/ui",
     "type": "method",
     "signature": "router.navigate(path, { replace? }?)",
     "returns": "Promise<boolean>",
     "declaration": "navigate(path: string, options?: { replace?: boolean }): Promise<boolean>;",
     "summary": "Go to a path of the app.",
     "description": "The path is relative to the router's `base`. Resolves with true when the navigation happened (a guard or a veto may stop it).",
     "params": [
      {
       "name": "path",
       "type": "string",
       "description": "For example `/users/3`."
      },
      {
       "name": "replace",
       "type": "boolean",
       "description": "Replace the history entry instead of adding one."
      }
     ],
     "example": {
      "lang": "js",
      "code": "import { router } from '@xoong/ui';\n\nawait router.navigate('/users/3');"
     },
     "guide": "arch-routing.html",
     "keywords": "redirect go link route url"
    },
    {
     "id": "api-router-guard",
     "name": "router.guard",
     "kind": "api",
     "package": "@xoong/ui",
     "import": "@xoong/ui",
     "type": "method",
     "signature": "router.guard((to, from) => result)",
     "returns": "function",
     "declaration": "guard(fn: (to: RouteLocation, from: RouteLocation | null) => GuardResult | Promise<GuardResult>): () => void;",
     "summary": "Run a check before every navigation.",
     "description": "Return true (or nothing) to continue, false to cancel, or a path to redirect there. Returns a function that removes the guard.",
     "params": [],
     "example": {
      "lang": "js",
      "code": "const stop = router.guard((to) => (loggedIn() || to.path === '/login' ? true : '/login'));"
     },
     "guide": "arch-routing.html",
     "keywords": "auth login protect redirect permission"
    },
    {
     "id": "api-router-before-navigate",
     "name": "router.beforeNavigate",
     "kind": "api",
     "package": "@xoong/ui",
     "import": "@xoong/ui",
     "type": "method",
     "signature": "router.beforeNavigate((to, from, type) => boolean)",
     "returns": "function",
     "declaration": "beforeNavigate(fn: (to: RouteLocation, from: RouteLocation | null, type: 'link' | 'api' | 'popstate') => boolean | void | Promise<boolean | void>): () => void;",
     "summary": "Veto leaving the current location.",
     "description": "Runs for links, back and forward, and `navigate()`; `type` is `link`, `api` or `popstate`. Return false to stay. Returns a function that removes it.",
     "params": [],
     "example": {
      "lang": "js",
      "code": "router.beforeNavigate(() => !editor.dirty);"
     },
     "guide": "arch-routing.html",
     "keywords": "unsaved dirty leave warn veto"
    },
    {
     "id": "api-router-current",
     "name": "router.current",
     "kind": "api",
     "package": "@xoong/ui",
     "import": "@xoong/ui",
     "type": "property",
     "signature": "router.current",
     "returns": null,
     "declaration": "readonly current: RouteLocation | null;",
     "summary": "The current location.",
     "description": "`{ path, params, query, url, route }`, or null before the first navigation.",
     "params": [],
     "example": {
      "lang": "js",
      "code": "console.log(router.current?.params.id);"
     },
     "guide": "arch-routing.html",
     "keywords": "location path params query url"
    },
    {
     "id": "api-configure-i18n",
     "name": "configureI18n",
     "kind": "api",
     "package": "@xoong/i18n",
     "import": "@xoong/i18n",
     "type": "function",
     "signature": "configureI18n({ locale?, fallback?, messages?, load? })",
     "returns": "Promise<void>",
     "declaration": "function configureI18n(options?: { locale?: string; fallback?: string; messages?: Record<string, Record<string, string>>; load?: (code: string) => Promise<any> | any }): void;",
     "summary": "Set up languages.",
     "description": "Resolves when the catalogs of the language are loaded.",
     "params": [
      {
       "name": "locale",
       "type": "string",
       "description": "The active language."
      },
      {
       "name": "fallback",
       "type": "string",
       "description": "The language to use for a missing message."
      },
      {
       "name": "messages",
       "type": "object",
       "description": "Catalogs by language: `{ vi: { key: 'message' } }`."
      },
      {
       "name": "load",
       "type": "function",
       "description": "`(code) => catalog`: loads a catalog on demand."
      }
     ],
     "example": {
      "lang": "js",
      "code": "import { configureI18n } from '@xoong/i18n';\n\nconfigureI18n({ locale: 'en', fallback: 'en', load: (code) => import(`/i18n/${code}.json`) });"
     },
     "guide": "recipes.html",
     "keywords": "language translation locale catalog messages"
    },
    {
     "id": "api-set-locale",
     "name": "setLocale",
     "kind": "api",
     "package": "@xoong/i18n",
     "import": "@xoong/i18n",
     "type": "function",
     "signature": "setLocale(code)",
     "returns": "Promise<void>",
     "declaration": "function setLocale(code: string): Promise<void>;",
     "summary": "Change the language.",
     "description": "Loads its catalog first, so the page switches once, then updates `<html lang dir>`. The last call wins.",
     "params": [],
     "example": {
      "lang": "js",
      "code": "await setLocale('vi');"
     },
     "guide": "recipes.html",
     "keywords": "language switch translate locale rtl"
    },
    {
     "id": "api-t",
     "name": "t",
     "kind": "api",
     "package": "@xoong/i18n",
     "import": "@xoong/i18n",
     "type": "function",
     "signature": "t(key, args?, fallbackText?)",
     "returns": "string",
     "declaration": "function t(key: string, args?: Record<string, any>, fallbackText?: string): string;",
     "summary": "Translate a message.",
     "description": "The message in the active language; `fallbackText` is shown when the key is unknown, otherwise the key.",
     "params": [
      {
       "name": "key",
       "type": "string",
       "description": "The message key."
      },
      {
       "name": "args",
       "type": "object",
       "description": "Values for `{placeholders}` and plural forms."
      },
      {
       "name": "fallbackText",
       "type": "string",
       "description": "Shown when no language has the key."
      }
     ],
     "example": {
      "lang": "js",
      "code": "t('articles.count', { count: 3 });"
     },
     "guide": "recipes.html",
     "keywords": "translate message plural language"
    },
    {
     "id": "api-locale",
     "name": "locale",
     "kind": "api",
     "package": "@xoong/i18n",
     "import": "@xoong/i18n",
     "type": "signal",
     "signature": "locale.value",
     "returns": null,
     "declaration": "const locale: Signal<string>;",
     "summary": "The active language, a signal.",
     "description": "Read it inside an `effect` or a template; set it with `setLocale`.",
     "params": [],
     "example": {
      "lang": "js",
      "code": "effect(() => console.log('language:', locale.value));"
     },
     "guide": null,
     "keywords": "language current signal"
    },
    {
     "id": "api-add-messages",
     "name": "addMessages",
     "kind": "api",
     "package": "@xoong/i18n",
     "import": "@xoong/i18n",
     "type": "function",
     "signature": "addMessages(code, messages)",
     "returns": "void",
     "declaration": "function addMessages(code: string, messages: Record<string, string>): void;",
     "summary": "Add messages for a language.",
     "description": "Merged over what is there.",
     "params": [],
     "example": {
      "lang": "js",
      "code": "addMessages('vi', { 'home.title': 'Chào mừng' });"
     },
     "guide": null,
     "keywords": "catalog translation messages"
    },
    {
     "id": "api-on-locale-change",
     "name": "onLocaleChange",
     "kind": "api",
     "package": "@xoong/i18n",
     "import": "@xoong/i18n",
     "type": "function",
     "signature": "onLocaleChange(fn)",
     "returns": "function",
     "declaration": "function onLocaleChange(fn: (code: string) => void): () => void;",
     "summary": "Run a function after each language change.",
     "description": "Not for the current language. Returns a function that stops it.",
     "params": [],
     "example": {
      "lang": "js",
      "code": "const stop = onLocaleChange((code) => console.log('now', code));"
     },
     "guide": null,
     "keywords": "language listener"
    },
    {
     "id": "api-detect-locale",
     "name": "detectLocale",
     "kind": "api",
     "package": "@xoong/i18n",
     "import": "@xoong/i18n",
     "type": "function",
     "signature": "detectLocale(supported, { default?, query?, storageKey? }?)",
     "returns": "string",
     "declaration": "function detectLocale(supported: string[], options?: { default?: string; query?: string; storageKey?: string }): string;",
     "summary": "Pick a supported language.",
     "description": "The one in `?lang=`, a saved choice, then the browser's preferences, then `default` (the first supported one).",
     "params": [],
     "example": {
      "lang": "js",
      "code": "const code = detectLocale(['en', 'vi']);"
     },
     "guide": null,
     "keywords": "language browser preference"
    },
    {
     "id": "api-signal",
     "name": "signal",
     "kind": "api",
     "package": "@xoong/core",
     "import": "@xoong/core",
     "type": "function",
     "signature": "signal(value)",
     "returns": "Signal",
     "declaration": "function signal<T>(value: T): Signal<T>;",
     "summary": "A reactive value.",
     "description": "`.value` reads (tracked inside an effect, a computed or a template function) and writes; `.peek()` reads without tracking.",
     "params": [],
     "example": {
      "lang": "js",
      "code": "import { signal } from '@xoong/core';\n\nconst count = signal(0);\ncount.value++;"
     },
     "guide": null,
     "keywords": "reactive state store observable"
    },
    {
     "id": "api-computed",
     "name": "computed",
     "kind": "api",
     "package": "@xoong/core",
     "import": "@xoong/core",
     "type": "function",
     "signature": "computed(fn)",
     "returns": "ReadonlySignal",
     "declaration": "function computed<T>(fn: () => T): ReadonlySignal<T>;",
     "summary": "A value derived from other signals.",
     "description": "It recomputes lazily, when something it read has changed.",
     "params": [],
     "example": {
      "lang": "js",
      "code": "const double = computed(() => count.value * 2);"
     },
     "guide": null,
     "keywords": "derived reactive formula"
    },
    {
     "id": "api-effect",
     "name": "effect",
     "kind": "api",
     "package": "@xoong/core",
     "import": "@xoong/core",
     "type": "function",
     "signature": "effect(fn)",
     "returns": "function",
     "declaration": "function effect(fn: () => void | (() => void)): () => void;",
     "summary": "Run a function now and whenever a signal it read changes.",
     "description": "`fn` may return a cleanup function. For side effects only: never fetch data in an effect, declare the operation on a view. Returns a function that stops it.",
     "params": [],
     "example": {
      "lang": "js",
      "code": "const stop = effect(() => { document.title = `${count.value} items`; });"
     },
     "guide": null,
     "keywords": "reactive side effect subscribe"
    },
    {
     "id": "api-batch",
     "name": "batch",
     "kind": "api",
     "package": "@xoong/core",
     "import": "@xoong/core",
     "type": "function",
     "signature": "batch(fn)",
     "returns": "what fn returns",
     "declaration": "function batch<T>(fn: () => T): T;",
     "summary": "Group writes.",
     "description": "The effects run once, after the outermost batch ends.",
     "params": [],
     "example": {
      "lang": "js",
      "code": "batch(() => { first.value = 'Ada'; last.value = 'Lovelace'; });"
     },
     "guide": null,
     "keywords": "reactive transaction group"
    },
    {
     "id": "api-untracked",
     "name": "untracked",
     "kind": "api",
     "package": "@xoong/core",
     "import": "@xoong/core",
     "type": "function",
     "signature": "untracked(fn)",
     "returns": "what fn returns",
     "declaration": "function untracked<T>(fn: () => T): T;",
     "summary": "Read signals without subscribing to them.",
     "description": "Inside `fn` a read is not tracked by the running effect or computed.",
     "params": [],
     "example": {
      "lang": "js",
      "code": "effect(() => { const n = untracked(() => count.value); });"
     },
     "guide": null,
     "keywords": "reactive peek"
    },
    {
     "id": "api-html",
     "name": "html",
     "kind": "api",
     "package": "@xoong/core",
     "import": "@xoong/core",
     "type": "function",
     "signature": "html`...`",
     "returns": "TemplateResult",
     "declaration": "function html(strings: TemplateStringsArray, ...values: unknown[]): TemplateResult;",
     "summary": "A template with reactive bindings.",
     "description": "A function in a child or attribute position is reactive. `name=${v}` sets an attribute, `?name=${v}` a boolean attribute, `.name=${v}` a property, `@event=${fn}` a listener and `style:--x=${v}` a CSS custom property.",
     "params": [],
     "example": {
      "lang": "js",
      "code": "html`<button class=${() => cls.value} @click=${save}>${() => label.value}</button>`"
     },
     "guide": null,
     "keywords": "template render lit bindings"
    },
    {
     "id": "api-render",
     "name": "render",
     "kind": "api",
     "package": "@xoong/core",
     "import": "@xoong/core",
     "type": "function",
     "signature": "render(result, container)",
     "returns": "function",
     "declaration": "function render(result: TemplateResult, container: Element | DocumentFragment): () => void;",
     "summary": "Render a template into an element.",
     "description": "Replaces the children of `container`. Returns the dispose function.",
     "params": [],
     "example": {
      "lang": "js",
      "code": "const dispose = render(html`<p>${() => count.value}</p>`, document.body);"
     },
     "guide": null,
     "keywords": "template mount dom"
    },
    {
     "id": "api-xo-element",
     "name": "XoElement",
     "kind": "api",
     "package": "@xoong/core",
     "import": "@xoong/core",
     "type": "class",
     "signature": "class XoElement extends HTMLElement",
     "returns": null,
     "declaration": "class XoElement extends HTMLElement {\n  /** Attributes <-> signals. The attribute is the kebab-case of the name (`maxSize` is `max-size`). */\n  static props: Record<string, XoPropType>;\n  static get observedAttributes(): string[];\n\n  constructor();\n\n  /** The signal behind a prop (reading `this.name` already tracks it). */\n  _sig(name: string): Signal<any>;\n\n  attributeChangedCallback(attr: string, oldValue: string | null, value: string | null): void;\n  connectedCallback(): void;\n  disconnectedCallback(): void;\n\n  /**\n   * Implement it only if the element OWNS its content: the result is rendered into the element when it connects.\n   * To enhance server-rendered HTML in place, implement `connected()` instead.\n   */\n  render?(): TemplateResult | null | undefined | void;\n  /** Runs after `render()` when the element is connected. */\n  connected?(): void;\n  /** Runs after the element's effects and rendering were disposed, when it is disconnected. */\n  disconnected?(): void;\n\n  /** Resolves with the element when its children are there: at once once the document is parsed, else after the parser reached the end. Await it in `connected()`. */\n  ready(): Promise<this>;\n\n  /** A side effect bound to this element's lifetime: disposed on disconnect. */\n  effect(fn: () => void | (() => void)): void;\n\n  /** Dispatch a bubbling, composed CustomEvent. */\n  emit<T = unknown>(name: string, detail?: T): void;\n}",
     "summary": "The base of a custom element in the Light DOM.",
     "description": "`static props` declares attributes that are also properties and signals. Implement `render()` when the element owns its content, or `connected()` to enhance markup that is already there.",
     "params": [],
     "example": {
      "lang": "js",
      "code": "class UserCard extends XoElement {\n  static props = { name: String, age: Number };\n  render() { return html`<h3>${() => this.name}</h3>`; }\n}\ndefine('user-card', UserCard);"
     },
     "guide": "own-control.html",
     "keywords": "custom element component web component base class props"
    },
    {
     "id": "api-define",
     "name": "define",
     "kind": "api",
     "package": "@xoong/core",
     "import": "@xoong/core",
     "type": "function",
     "signature": "define(tag, class)",
     "returns": "void",
     "declaration": "function define(tag: string, cls: typeof XoElement): void;",
     "summary": "Register a custom element.",
     "description": "A tag that is already defined is left alone. The tag must contain a hyphen.",
     "params": [],
     "example": {
      "lang": "js",
      "code": "define('user-card', UserCard);"
     },
     "guide": "own-control.html",
     "keywords": "custom element register customElements"
    },
    {
     "id": "api-xo-control",
     "name": "XoControl",
     "kind": "api",
     "package": "@xoong/core",
     "import": "@xoong/core",
     "type": "class",
     "signature": "class XoControl extends XoElement",
     "returns": null,
     "declaration": "class XoControl extends XoElement {\n  static formAssociated: boolean;\n  static props: { name: StringConstructor; required: BooleanConstructor; disabled: BooleanConstructor; readonly: BooleanConstructor };\n\n  constructor();\n\n  /** The form field name. */\n  name: string;\n  required: boolean;\n  disabled: boolean;\n  readonly: boolean;\n\n  // ---- value\n  /** The value. A string by default; a subclass may hold anything and override `serialize`. Reading it is tracked. */\n  get value(): any;\n  set value(v: any);\n  /** Set the value programmatically (like `input.value = x`: no events). Before the control has started, it also becomes the initial value. */\n  setValue(v: any): void;\n  /** The person changed the value: sets it and fires `input`. */\n  userInput(v: any): void;\n  /** The person finished a change: fires `change`. */\n  userChange(): void;\n  /** What the form submits for a value: a string, a File, FormData, or null for nothing. Override for structured values. */\n  serialize(v: any): string | File | FormData | null;\n  /** Whether a value counts as \"nothing entered\" for `required`. Override for structured values. */\n  isEmpty(v: any): boolean;\n  /** Validation hook: return an error message for an invalid value, a falsy value when it is valid. Runs on every change. */\n  validate(value: any): string | null | undefined | false;\n\n  // ---- state (reading these inside a template or an effect is tracked)\n  /** The value the control started with (what `reset` goes back to and `dirty` compares with). */\n  get initialValue(): any;\n  get dirty(): boolean;\n  get pristine(): boolean;\n  get touched(): boolean;\n  /** True when there is no validation error. */\n  get valid(): boolean;\n  get invalid(): boolean;\n  /** Mark the control as visited (blur does this). */\n  markTouched(): void;\n\n  // ---- validation\n  /** The message to show, or '' when valid. */\n  get validationMessage(): string;\n  /** Disabled by its own attribute or by a disabled fieldset. */\n  get isDisabled(): boolean;\n  get willValidate(): boolean;\n  get validity(): XoValidity;\n  /** Set your own error (cross-field rules, server errors). An empty string clears it. */\n  setCustomValidity(message: string | null | undefined): void;\n  /** True when valid (or not validated); otherwise fires a cancelable `invalid` event and returns false. */\n  checkValidity(): boolean;\n  /** Like `checkValidity`, and on failure marks the control touched and focuses `focusTarget`. */\n  reportValidity(): boolean;\n  /** The element inside that takes focus (and anchors the validation bubble). */\n  get focusTarget(): HTMLElement | null;\n\n  // ---- reset\n  /** Back to the initial value, untouched. The form calls this on reset. */\n  reset(): void;\n  /** Make the current value the new baseline, as after a successful save: `dirty` becomes false. */\n  markPristine(): void;\n\n  // ---- form-associated callbacks\n  formResetCallback(): void;\n  formDisabledCallback(disabled: boolean): void;\n  formStateRestoreCallback(state: any): void;\n  /** The form this control belongs to (through ElementInternals, else the closest `<form>`). */\n  get form(): HTMLFormElement | null;\n}",
     "summary": "The base of a form control.",
     "description": "Gives the element `name`, `required`, `disabled` and `readonly`, a `value`, validation and a place in the form model. Every field control of @xoong/controls extends it.",
     "params": [],
     "example": {
      "lang": "js",
      "code": "class RatingInput extends XoControl { static props = { ...XoControl.props, max: Number }; }"
     },
     "guide": "own-control.html",
     "keywords": "form field input custom element validation"
    },
    {
     "id": "api-create-form",
     "name": "createForm",
     "kind": "api",
     "package": "@xoong/forms",
     "import": "@xoong/forms",
     "type": "function",
     "signature": "createForm({ schema?, value?, rules?, serialize? })",
     "returns": "FormModel",
     "declaration": "function createForm(options?: { schema?: SchemaNode; value?: any; rules?: Rules; serialize?: (draft: any, body: any) => any }): FormModel;",
     "summary": "The draft of one form, without a DOM.",
     "description": "Holds the value, the errors and the dirty state by path. `<form xo-submit>` creates one for you; use this in tests and for a form that is not in a view.",
     "params": [
      {
       "name": "schema",
       "type": "object",
       "description": "A request schema node."
      },
      {
       "name": "value",
       "type": "object",
       "description": "The starting record."
      },
      {
       "name": "rules",
       "type": "object",
       "description": "Rules by field path; `*` gets the whole record."
      },
      {
       "name": "serialize",
       "type": "function",
       "description": "`(draft, body) => body`: change what is sent."
      }
     ],
     "example": {
      "lang": "js",
      "code": "import { createForm } from '@xoong/forms';\n\nconst form = createForm({ rules: { slug: (v) => (/^[a-z-]+$/.test(v) ? '' : 'Lower case letters only') } });\nform.set('slug', 'My Page');"
     },
     "guide": "forms.html",
     "keywords": "form model validation draft state"
    },
    {
     "id": "api-attach-form",
     "name": "attach",
     "kind": "api",
     "package": "@xoong/forms",
     "import": "@xoong/forms",
     "type": "function",
     "signature": "attach(form, { schema, record?, hadRecord? })",
     "returns": "{ model, submit, detach, rescan }",
     "declaration": "function attach(form: HTMLFormElement, options: { schema: SchemaNode; record?: () => any; hadRecord?: boolean }): { model: FormModel; submit(send: (body: any) => Promise<any>): Promise<any>; detach(): void; rescan(): void };",
     "summary": "Attach a form model to a `<form>`.",
     "description": "The binder that `<form xo-submit>` uses, for a form you handle yourself.",
     "params": [],
     "example": {
      "lang": "js",
      "code": "const { model } = attach(document.querySelector('form'), { schema });"
     },
     "guide": "forms.html",
     "keywords": "form binder dom model"
    },
    {
     "id": "api-set-messages",
     "name": "setMessages",
     "kind": "api",
     "package": "@xoong/forms",
     "import": "@xoong/forms",
     "type": "function",
     "signature": "setMessages({ code: (params) => message })",
     "returns": "void",
     "declaration": "function setMessages(partial: Record<string, (params: Record<string, any>) => string>): void;",
     "summary": "Replace the texts of the built-in validation messages.",
     "description": "For another language or your own wording. Each message receives its parameters, for example `{ min: 3 }`. The codes are `required`, `type`, `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf`, `minLength`, `maxLength`, `pattern`, `format`, `enum`, `minItems`, `maxItems`, `uniqueItems` and `variant`. `resetMessages()` brings the English ones back.",
     "params": [],
     "example": {
      "lang": "js",
      "code": "import { setMessages } from '@xoong/forms';\n\nsetMessages({ required: () => 'Bắt buộc', minLength: ({ min }) => `Ít nhất ${min} ký tự` });"
     },
     "guide": "forms.html",
     "keywords": "validation errors messages translate wording required language"
    },
    {
     "id": "api-sanitize-html",
     "name": "sanitizeHtml",
     "kind": "api",
     "package": "@xoong/richtext",
     "import": "@xoong/richtext",
     "type": "function",
     "signature": "sanitizeHtml(html, { images? }?)",
     "returns": "string",
     "declaration": "function sanitizeHtml(html: string, options?: { images?: boolean }): string;",
     "summary": "Reduce HTML to the editor's safe subset.",
     "description": "Keeps p, h2, h3, strong, em, u, ul, ol, li, blockquote, br, a (safe links only) and img (safe sources only). `images: false` removes `<img>`. It needs a DOM.",
     "params": [],
     "example": {
      "lang": "js",
      "code": "import { sanitizeHtml } from '@xoong/richtext';\n\nconst clean = sanitizeHtml(untrusted, { images: false });"
     },
     "guide": "richtext.html",
     "keywords": "security xss clean html sanitize editor"
    }
   ]
  },
  {
   "id": "template",
   "label": "Template syntax",
   "summary": "What goes between braces inside an <xo-view>: paths, formats and view variables.",
   "count": 11,
   "entries": [
    {
     "id": "tpl-path",
     "name": "{path}",
     "kind": "syntax",
     "summary": "Text or attribute interpolation.",
     "description": "A path reads a field of the current item (`.` is the item itself, `a.b` is nested, `a[0].b` indexes) and then the view's `p-*` parameters. The text is escaped. A `javascript:` URL becomes `about:blank`, and `on*` attributes and `srcdoc` are not interpolated. `{{name}}` and `\\{name}` stay as written, and braces around anything that is not a path (JSON, CSS) are left alone.",
     "table": null,
     "example": {
      "lang": "html",
      "code": "<xo-view get=\"getPerson\" p-id=\"1\">\n  <h2>{name}</h2>\n  <a href=\"mailto:{email}\">{email}</a>\n</xo-view>"
     },
     "guide": "data.html",
     "keywords": "interpolate binding mustache braces value field expression"
    },
    {
     "id": "tpl-format",
     "name": "{path | format}",
     "kind": "syntax",
     "summary": "Show a value in a display format.",
     "description": "The formats are `date`, `datetime`, `time`, `relative`, `number`, `json` and `localized`. They use `Intl` and the page language. A value that cannot be formatted is shown as it is. Add your own with `setFormat()`. An unknown name is reported once on the console and the plain value is shown.",
     "table": [
      [
       "date",
       "A calendar date in the page language: `Nov 25, 2026`."
      ],
      [
       "datetime",
       "Date and time."
      ],
      [
       "time",
       "Time of day."
      ],
      [
       "relative",
       "\"3 days ago\", \"tomorrow\"."
      ],
      [
       "number",
       "A number with the language's separators."
      ],
      [
       "json",
       "The value as JSON text."
      ],
      [
       "localized",
       "A value that is an object by language (`{ en, vi }`): the active language, then its base language, then English, then any filled one."
      ]
     ],
     "example": {
      "lang": "html",
      "code": "<xo-view get=\"listPeople\">\n  <p xo-each=\"items\">{name}: {createdAt | relative}, {total | number}</p>\n</xo-view>"
     },
     "guide": "data.html",
     "keywords": "format date time number relative json localized currency pipe filter"
    },
    {
     "id": "tpl-loading",
     "name": "$loading, $refreshing",
     "kind": "syntax",
     "summary": "Whether the view is fetching.",
     "description": "`$loading` is true while a request runs and there is nothing to show yet. `$refreshing` is true while a request runs and the previous result is still on screen (a sort or a page loads), so the view does not collapse. The view also carries `data-refreshing` for CSS.",
     "table": null,
     "example": {
      "lang": "html",
      "code": "<xo-view get=\"listPeople\">\n  <p xo-if=\"$loading\">Loading…</p>\n  <p xo-if=\"$refreshing\">Updating…</p>\n</xo-view>"
     },
     "guide": "data-states.html",
     "keywords": "state skeleton spinner busy wait"
    },
    {
     "id": "tpl-error",
     "name": "$error",
     "kind": "syntax",
     "summary": "The error of the failed load.",
     "description": "A `XoongError` with `status`, `code` and `detail`, or nothing when the load worked.",
     "table": null,
     "example": {
      "lang": "html",
      "code": "<xo-view get=\"listPeople\">\n  <p xo-if=\"$error\">{$error.code}: {$error.detail}</p>\n</xo-view>"
     },
     "guide": "data-states.html",
     "keywords": "failure problem message detail code status"
    },
    {
     "id": "tpl-index",
     "name": "$index, $n",
     "kind": "syntax",
     "summary": "The position of the row in `xo-each`.",
     "description": "`$index` counts from 0 and `$n` from 1. In nested `xo-each` loops `$i0` (the outer one), `$i1`, ... name the position at each depth.",
     "table": null,
     "example": {
      "lang": "html",
      "code": "<li xo-each=\"items\">{$n}. {name}</li>"
     },
     "guide": "data.html",
     "keywords": "number counter row position loop"
    },
    {
     "id": "tpl-paging",
     "name": "$hasPrev, $hasNext, $from, $to",
     "kind": "syntax",
     "summary": "Paging state of a list.",
     "description": "For lists that take `limit` and `offset` and answer with `total`. `$from` and `$to` are the numbers of the first and the last row shown (the \"1–12 of 48\" text).",
     "table": null,
     "example": {
      "lang": "html",
      "code": "<xo-view get=\"listPeople\" p-limit=\"10\">\n  <button xo-param=\"offset-=10\" xo-if=\"$hasPrev\">Previous</button>\n  <span>{$from}–{$to}</span>\n  <button xo-param=\"offset+=10\" xo-if=\"$hasNext\">Next</button>\n</xo-view>"
     },
     "guide": "data.html",
     "keywords": "pagination page offset limit total next previous"
    },
    {
     "id": "tpl-selection",
     "name": "$selectedCount, $selected",
     "kind": "syntax",
     "summary": "The ticked rows.",
     "description": "`$selectedCount` is the number of ticked rows (`xo-select`), `$selected` the rows themselves, for `xo-body=\"$selected\"`.",
     "table": null,
     "example": {
      "lang": "html",
      "code": "<button xo-click=\"deletePerson\" xo-selected xo-if=\"$selectedCount\">Delete {$selectedCount}</button>"
     },
     "guide": "data.html",
     "keywords": "checkbox bulk rows selection count"
    },
    {
     "id": "tpl-stream",
     "name": "$stream, $streaming",
     "kind": "syntax",
     "summary": "The events of a streamed call.",
     "description": "`$stream` holds the events of the latest streamed call (an operation that answers with server-sent events) and `$streaming` whether it still runs.",
     "table": null,
     "example": {
      "lang": "html",
      "code": "<p xo-if=\"$streaming\">Receiving…</p>"
     },
     "guide": null,
     "keywords": "sse events chat tokens live"
    },
    {
     "id": "tpl-form",
     "name": "$form",
     "kind": "syntax",
     "summary": "The state of the form around you.",
     "description": "Inside a `<form xo-submit>`: `$form.dirty`, `$form.valid`, `$form.submitting`, `$form.stale`, and by field path `$form.errors['title.vi']`, `$form.touched[...]`, `$form.filled[...]`.",
     "table": null,
     "example": {
      "lang": "html",
      "code": "<xo-view get=\"getPerson\" p-id=\"1\">\n  <form xo-submit=\"updatePerson\">\n    <input name=\"name\" aria-label=\"Name\">\n    <span xo-if=\"$form.dirty\">Unsaved changes</span>\n    <button>Save</button>\n  </form>\n</xo-view>"
     },
     "guide": "forms.html",
     "keywords": "dirty valid submitting errors touched filled unsaved"
    },
    {
     "id": "tpl-params",
     "name": "p-<name>",
     "kind": "syntax",
     "summary": "A parameter of the view's operation.",
     "description": "On `<xo-view>`: `p-id=\"42\"` is the path parameter `id`. `p-id=\"?id\"` reads `?id=` from the page URL and `p-id=\":id\"` reads a route parameter. Inside the view `{id}` reads it.",
     "table": null,
     "example": {
      "lang": "html",
      "code": "<xo-view get=\"getPerson\" p-id=\"?id\"><h1>{name}</h1></xo-view>"
     },
     "guide": "data.html",
     "keywords": "parameter path id route url query"
    },
    {
     "id": "tpl-boolean-attributes",
     "name": "hidden=\"{flag}\"",
     "kind": "syntax",
     "summary": "A boolean attribute that follows the data.",
     "description": "When `{flag}` is the whole value of `hidden`, `disabled`, `checked`, `selected`, `readonly`, `required`, `open`, `multiple`, `inert`, `autofocus`, `controls`, `loop`, `muted` or `reversed`, the attribute is present while the value is true (an empty list is false).",
     "table": null,
     "example": {
      "lang": "html",
      "code": "<button disabled=\"{$loading}\" xo-refresh>Refresh</button>"
     },
     "guide": "data.html",
     "keywords": "disabled checked selected readonly required attribute toggle"
    }
   ]
  }
 ]
}
