[
  {
    "path": ".eleventy.cjs",
    "content": "let markdownIt = require(\"markdown-it\");\nlet markdownItAnchor = require(\"markdown-it-anchor\");\nlet markdownItAttrs = require(\"markdown-it-attrs\");\n\nmodule.exports = config => {\n\tlet data = {\n\t\t\"layout\": \"page.njk\",\n\t\t\"permalink\": \"{{ page.filePathStem | replace('README', '') }}/index.html\",\n\t\televentyComputed: {\n\t\t\tdefaultTitle: data => {\n\t\t\t\tif (data.id) {\n\t\t\t\t\treturn data.css_only? `.${data.id}` : `<${data.id}>`;\n\t\t\t\t}\n\n\t\t\t\treturn \"Nude UI: A collection of accessible, customizable, ultra-light web components\";\n\t\t\t}\n\t\t}\n\t};\n\n\tfor (let p in data) {\n\t\tconfig.addGlobalData(p, data[p]);\n\t}\n\n\tconfig.setDataDeepMerge(true);\n\n\tconfig.setLibrary(\"md\", markdownIt({\n\t\t\thtml: true,\n\t\t})\n\t\t.disable(\"code\")\n\t\t.use(markdownItAttrs)\n\t\t.use(markdownItAnchor, {\n\t\t\tpermalink: markdownItAnchor.permalink.headerLink(),\n\t\t\tlevel: 2,\n\t\t})\n\t);\n\n\tconfig.addFilter(\n\t\t\"relative\",\n\t\tpage => {\n\t\t\tlet path = page.url.replace(/[^/]+$/, \"\");\n\t\t\tlet ret = require(\"path\").relative(path, \"/\");\n\n\t\t\treturn ret || \".\";\n\t\t}\n\t);\n\n\treturn {\n\t\tmarkdownTemplateEngine: \"njk\",\n\t\ttemplateFormats: [\"md\", \"njk\"],\n\t\tdir: {\n\t\t\toutput: \".\"\n\t\t},\n\t};\n};\n"
  },
  {
    "path": ".gitattributes",
    "content": "# Auto detect text files and perform LF normalization\n* text=auto\n"
  },
  {
    "path": ".gitignore",
    "content": "index.html\nnd-calendar/style.css\nbutton-group/style.css"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Lea Verou\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<header>\n\n# Nude UI\n\nA collection of accessible, customizable, ultra-light web components\n\n- Using built-in controls whenever possible, web components when JS and/or extra elements are needed\n- Highly customizable\n- Tiny (most are ~1KB minified & compressed)\n\nA work in progress. Try them out and [provide feedback](https://github.com/leaverou/nudeui) or move along and check back later.\n\n</header>\n\n\n\n<section id=\"components\" class=\"language-html\">\n\n## Components\n\n| Name | Tag | Description | Type(s) | Status |\n|------|-----|-------------|-------------------|--------|\n| [Switch](elements/nd-switch) | `<nd-switch>` | On/off toggle switch | CSS-only | Mature |\n| [Button Group](elements/button-group) | `<button-group>` | Group of buttons for selecting one or more values out of a set of options | JS | Mature |\n| [Cycle Toggle](elements/cycle-toggle) | `<cycle-toggle>` | Compact way to select one option from a group, click selects the next option | JS | Mature |\n| [Discrete meter](elements/meter-discrete) | `<meter-discrete>` | Meter with discrete values shown as icons | JS | Mature |\n| [Rating](elements/nd-rating) | `<nd-rating>` | Like discrete meter, but editable via hovering and clicking | JS | Mature |\n| [HTML Demo](elements/html-demo) | `<html-demo>` | Display demos of HTML content alongside their source code | JS | Mature |\n| [Image input](elements/img-input) | `<img-input>` | Input an image via URL, file upload, drag-and-drop, or pasting | JS | In incubation |\n| [Freeform text with presets](elements/with-presets) | `<with-presets>` | A combination of a text input and a select element | JS | In incubation |\n| [Calendar](elements/nd-calendar) | `<nd-calendar>` | Show dates on a calendar | JS | In incubation |\n| [Data bind](elements/data-bind) | `<data-bind>` | Declaratively bind data from a source element to a target element | JS | In incubation |\n\n</section>\n\n## Wanna use them all?\n\nThis includes all components marked as mature:\n\n```js\nimport \"https://nudeui.com/elements/index.js\";\n```\n\nComponents still being incubated will need to be included individually.\n\n## Failed experiments\n\nDo not use. These have serious flaws and are likely incomplete.\nThey are included here only in case someone else wants to look into fixing their issues,\nas well as a warning for other wanderers going down the same path.\n\n- [Drop down](elements/drop-down)\n\n"
  },
  {
    "path": "_headers",
    "content": "/*\n\tAccess-Control-Allow-Origin: *\n"
  },
  {
    "path": "_includes/page.njk",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"UTF-8\">\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\t<title>{{ title or defaultTitle }}</title>\n\t<link rel=\"icon\" href=\"{{ page | relative }}/logo.svg\">\n\t<link rel=\"stylesheet\" href=\"{{ page | relative }}/style.css\" />\n\n\t{% if css_only %}\n\t<link rel=\"stylesheet\" href=\"{{ id }}.css\">\n\t{% elseif id %}\n\t<script src=\"{{ id }}.js\" type=\"module\"></script>\n\t{% endif %}\n\t{{ includes | safe }}\n</head>\n<body>\n\n<nav>\n\t<a href=\"#installation\">Installation</a>\n\t<a href=\"https://github.com/LeaVerou/nudeui/tree/main/elements/{{ id }}\">GitHub</a>\n</nav>\n\n{{ content | safe }}\n\n{% if id %}\n<section id=\"installation\">\n\n<h2>Installation</h2>\n\n{% if css_only %}\n\n<p>This is a CSS-only component. You can just import it straight into your CSS file:\n\n<pre class=\"language-css\"><code>@import url('https://nudeui.com/elements/{{ id }}/{{ id }}.css');</code></pre>\n\n<p>Then use <code>class=\"{{ id }}\"</code> on the types of elements described above.</p>\n\n{% else %}\n\n<p>Just include the component's JS file and you're good:\n\n<pre class=\"language-html\"><code>&lt;script src=\"https://nudeui.com/elements/{{ id }}/{{ id }}.js\" type=\"module\">&lt;/script>\n</code></pre>\n\n<p>In case you want to link to local files: CSS is fetched automatically, and assumed to be in the same directory as the JS file.\n{% endif %}\n\n</section>\n{% endif %}\n\n<script src=\"{{ page | relative }}/assets/global.js\" type=\"module\"></script>\n\n</body>\n</html>"
  },
  {
    "path": "_redirects",
    "content": "/button-group/* /elements/button-group/:splat 301\n/meter-discrete/* /elements/meter-discrete/:splat 301\n/with-presets/* /elements/with-presets/:splat 301\n/cycle-toggle/* /elements/cycle-toggle/:splat 301\n/nd-:tag/* /elements/nd-:tag/:splat 301"
  },
  {
    "path": "assets/global.js",
    "content": "// Website scripts\nimport \"../elements/index.js\";\nimport \"https://prismjs.com/prism.js\";\nimport HTMLDemo from \"../elements/html-demo/html-demo.js\";\n\nif (!document.documentElement.matches(\".no-home-link\")) {\n\tlet h1 = document.querySelector(\"h1\");\n\n\tif (h1 && !h1.parentNode.querySelector(\".home\")) {\n\t\th1.insertAdjacentHTML(\"beforebegin\", `<a href=\"/index.html\" class=\"home\">Nude UI</a>`);\n\t}\n}\n\nHTMLDemo.wrapAll();"
  },
  {
    "path": "elements/button-group/README.md",
    "content": "---\nid: button-group\ntitle: <button-group>\nincludes: '<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.81/dist/themes/light.css\" /><script type=\"module\" src=\"https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.0.0-beta.81/dist/shoelace.js\"></script>'\n---\n\n<header>\n\n# `<button-group>`\n\nGroup of exclusive push buttons\n\n</header>\n\n## Features\n\n- Uses existing button styling present in the page\n- Uses [`ElementInternals`](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) to work like a built-in form element\n- Accessible\n- Ultra light (3KB **unminified** and **uncompressed**!)\n\n\n## Examples\n\nBasic, no selected option:\n\n```html\n<button-group>\n\t<button>Design</button>\n\t<button>Preview</button>\n</button-group>\n```\n\nProviding values:\n\n```html\n<button-group id=\"temporal\" oninput=\"out.textContent = this.value\">\n\t<button value=\"\">None</button>\n\t<button value=\"d\">Dates</button>\n\t<button value=\"t\">Times</button>\n\t<button value=\"dt\">Dates & Times</button>\n</button-group>\n<output id=\"out\"></output>\n```\n\nPre-selected state via `aria-pressed`:\n\n```html\n<button-group>\n\t<button>Design</button>\n\t<button aria-pressed=\"true\">Preview</button>\n</button-group>\n```\n\nMultiple:\n\n```html\n<button-group multiple oninput=\"button_multiple_value.textContent = this.value\">\n\t<button value=\"b\"><span style=\"font-weight: bold\">B</span></button>\n\t<button value=\"i\"><span style=\"font-style: italic\">I</span></button>\n\t<button value=\"u\"><span style=\"text-decoration: underline\">U</span></button>\n</button-group>\n<output id=\"button_multiple_value\"></output>\n```\n\nParticipates in form submission (requires [`ElementInternals`](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) support):\n\n```html\n<form action=\"about:blank\" target=\"_blank\">\n\t<button-group name=\"favorite_letter\">\n\t\t<button>A</button>\n\t\t<button aria-pressed=\"true\">B</button>\n\t\t<button>C</button>\n\t\t<button>D</button>\n\t\t<button>E</button>\n\t\t<button>F</button>\n\t\t<button>G</button>\n\t</button-group>\n\t<button type=submit>Submit</button>\n</form>\n```\n\nVertical\n\n```html\n<button-group name=\"type\" vertical>\n\t<button value=\"garlic\" aria-pressed=\"true\">Garlic</button>\n\t<button value=\"msg\">MSG</button>\n\t<button value=\"salt\">Salt</button>\n</button-group>\n```\n\nSeparate\n\n```html\n<button-group name=\"type\" separate>\n\t<button>Salt</button>\n\t<button>Pepper</button>\n\t<button>Garlic</button>\n\t<button>Cumin</button>\n\t<button>Coriander</button>\n\t<button>Dill</button>\n\t<button>Parsley</button>\n\t<button>Turmeric</button>\n</button-group>\n```\n\nDynamically setting `element.value`:\n\n```html\n<button-group id=\"group1\">\n\t<button>A</button>\n\t<button aria-pressed=\"true\">B</button>\n\t<button>C</button>\n</button-group>\n<button onclick=\"group1.value = 'C'\">Select C</button>\n```\n\nDynamically adding `aria-pressed` attribute:\n\n```html\n<button-group id=\"group2\">\n\t<button>A</button>\n\t<button aria-pressed=\"true\">B</button>\n\t<button>C</button>\n</button-group>\n<button onclick=\"group2.children[2].setAttribute('aria-pressed', 'true')\">Select C</button>\n```\n\nDynamically adding options:\n\n```html\n<button-group id=\"group3\">\n\t<button>1</button>\n\t<button>2</button>\n\t<button aria-pressed=\"true\">3</button>\n</button-group>\n<button onclick=\"window.counter ||= 3; group3.insertAdjacentHTML('beforeend', `<button aria-pressed=true>${++counter}</button>`)\">Add option</button>\n```\n\n[WIP](https://twitter.com/LeonieWatson/status/1547544701036888065):\n`<button-group>` has an implicit ARIA Role of `region`, so adding an `aria-label` should make it work as a landmark out of the box\n(requires [`ElementInternals`](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) support):\n\n```html\n<button-group aria-label=\"View switcher\">\n\t<button>Design</button>\n\t<button aria-pressed=\"true\">Preview</button>\n</button-group>\n```\n\nRegular labels should work too (requires [`ElementInternals`](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) support):\n\n```html\n<label for=\"view-switcher\">View:</label>\n<button-group id=\"view-switcher\">\n\t<button>Design</button>\n\t<button aria-pressed=\"true\">Preview</button>\n</button-group>\n```\n\nYou don't even need to use an actual `<button>`, [custom elements](https://shoelace.style/components/button?id=css-parts)\nshould work too\n(presentation needs work, but functionality is there):\n\n```html\n<style>\nsl-button[aria-pressed=\"true\"]::part(base) {\n\tbackground: var(--sl-color-primary-100);\n\tborder-color: var(--sl-color-primary-300);\n}\n</style>\n<button-group>\n\t<sl-button>1</sl-button>\n\t<sl-button aria-pressed=\"true\">2</sl-button>\n\t<sl-button>3</sl-button>\n</button-group>\n```"
  },
  {
    "path": "elements/button-group/button-group.js",
    "content": "export default class ButtonGroup extends HTMLElement {\n\t#internals\n\t#observer\n\n\tconstructor () {\n\t\tsuper();\n\n\t\tthis.attachShadow({ mode: \"open\" });\n\t\tthis.shadowRoot.innerHTML = `<style>@import \"${new URL(\"style.css\", import.meta.url)}\";</style><slot></slot>`;\n\n\t\tthis.#internals = this.attachInternals?.();\n\n\t\tif (this.#internals) {\n\t\t\t// https://twitter.com/LeonieWatson/status/1545788775644667904\n\t\t\tthis.#internals.role = \"region\";\n\t\t}\n\n\t\tthis.addEventListener(\"click\", evt => {\n\t\t\tlet previousValue = this.value + \"\";\n\n\t\t\tlet button = evt.target;\n\n\t\t\twhile (button && button.parentNode !== this) {\n\t\t\t\tbutton = button.parentNode;\n\t\t\t}\n\n\t\t\tif (button) {\n\t\t\t\tthis.#buttonChanged(button);\n\n\t\t\t\tif (previousValue !== this.value + \"\") {\n\t\t\t\t\tlet evt = new InputEvent(\"input\", {bubbles: true});\n\t\t\t\t\tthis.dispatchEvent(evt);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\tthis.#observer = new MutationObserver(mutations => {\n\t\t\tmutations = mutations.filter(m => {\n\t\t\t\tif (m.target === this) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\telse if (m.target.parentNode === this) {\n\t\t\t\t\tif (m.type === \"childList\") {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t\telse if (m.oldValue !== m.target.getAttribute(\"aria-pressed\")) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn false;\n\t\t\t});\n\n\t\t\tif (mutations.length > 0) {\n\t\t\t\tthis.#buttonChanged();\n\t\t\t}\n\t\t});\n\t}\n\n\t#buttonChanged (button) {\n\t\tif (this.multiple) {\n\t\t\tthis.#value ||= [];\n\n\t\t\tif (button) {\n\t\t\t\tlet pressed = button.getAttribute(\"aria-pressed\") === \"true\";\n\t\t\t\tlet value = getValue(button);\n\n\t\t\t\tif (pressed) {\n\t\t\t\t\tthis.#value = this.#value.filter(v => v !== value);\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tthis.#value.push(value);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis.value = this.#value;\n\t\t}\n\t\telse {\n\t\t\tthis.value = getValue(button ?? this.pressedButton);\n\t\t}\n\t}\n\n\tget name () {\n\t\treturn this.getAttribute(\"name\");\n\t}\n\n\tset name (value) {\n\t\tthis.setAttribute(\"name\", value);\n\t}\n\n\tget multiple () {\n\t\treturn this.hasAttribute(\"multiple\");\n\t}\n\n\tset multiple (value) {\n\t\tif (value) {\n\t\t\tthis.setAttribute(\"multiple\", \"\");\n\t\t}\n\t\telse {\n\t\t\tthis.removeAttribute(\"multiple\");\n\t\t}\n\t}\n\n\t#value;\n\n\tget value () {\n\t\treturn this.#value;\n\t}\n\n\tset value (value) {\n\t\tthis.#value = value;\n\n\t\tthis.#internals?.setFormValue(value);\n\n\t\tfor (let button of this.children) {\n\t\t\tif (!button.hasAttribute(\"type\")) {\n\t\t\t\tbutton.type = \"button\";\n\t\t\t}\n\n\t\t\tlet buttonValue = getValue(button);\n\t\t\tlet pressed = this.multiple ? this.#value.includes(buttonValue) : this.#value === buttonValue;\n\n\t\t\tlet ariaPressed = pressed.toString();\n\n\t\t\tif (ariaPressed !== button.getAttribute(\"aria-pressed\")) {\n\t\t\t\tbutton.setAttribute(\"aria-pressed\", ariaPressed);\n\t\t\t}\n\t\t}\n\t}\n\n\tget pressedButtons () {\n\t\treturn [...this.querySelectorAll(`:scope > [aria-pressed=\"true\"]`)];\n\t}\n\n\tget pressedButton () {\n\t\treturn this.pressedButtons.at(-1);\n\t}\n\n\tget labels() {\n\t\treturn this.#internals?.labels;\n\t}\n\n\tconnectedCallback () {\n\t\tthis.#buttonChanged();\n\n\t\tthis.#observer.observe(this, {\n\t\t\tattributeFilter: [\"aria-pressed\"],\n\t\t\tattributeOldValue: true,\n\t\t\tchildList: true,\n\t\t\tsubtree: true,\n\t\t});\n\t}\n\n\tdisconnectedCallback () {\n\t\tthis.#observer.disconnect();\n\t}\n\n\tstatic get formAssociated() {\n\t\treturn true;\n\t}\n}\n\nfunction getValue(button) {\n\tif (!button) {\n\t\treturn null;\n\t}\n\n\tif (button.hasAttribute(\"value\")) {\n\t\treturn button.value;\n\t}\n\telse {\n\t\treturn button.textContent.trim();\n\t}\n}\n\ncustomElements.define(\"button-group\", ButtonGroup);\n"
  },
  {
    "path": "elements/button-group/style.css",
    "content": ":host {\n\tdisplay: inline-flex\n\n\t/* 0 specificity default pressed styles */\n}\n\n:host ::slotted(:where([aria-pressed=\"true\"])) {\n\t\tbackground: hsl(220 10% 90% / .9);\n\t\tbox-shadow: 0 .1em .2em hsl(0 0% 0% / .2) inset, 0 0 0 2em hsl(220 10% 50% / .15) inset;\n\t}\n\n:host([separate]) {\n\tflex-wrap: wrap\n}\n\n:host([separate]) ::slotted(*) {\n\t\tflex: 1;\n\t\tmargin: .2em;\n\t}\n\n:host(:not([separate])) {\n\tmargin: .5em\n}\n\n:host(:not([separate])) ::slotted(*) {\n\t\tmargin: 0;\n\t}\n\n:host(:not([vertical]):not([separate])) {\n\tjustify-content: center;\n\talign-items: stretch\n}\n\n:host(:not([vertical]):not([separate])) slot::slotted(*) {\n\t\tflex: 1;\n\t}\n\n:host(:not([vertical]):not([separate])) ::slotted(:not(:last-of-type)) {\n\t\tborder-top-right-radius: 0 !important;\n\t\tborder-bottom-right-radius: 0 !important;\n\t\tborder-inline-end: none !important;\n\t}\n\n:host(:not([vertical]):not([separate])) ::slotted(:not(:first-of-type)) {\n\t\tborder-top-left-radius: 0 !important;\n\t\tborder-bottom-left-radius: 0 !important;\n\t}\n\n:host([vertical]:not([separate])) {\n\tflex-direction: column;\n\talign-items: stretch\n}\n\n:host([vertical]:not([separate])) ::slotted(*) {\n\t\ttext-align: inline-start;\n\t}\n\n:host([vertical]:not([separate])) ::slotted(:not(:last-of-type)) {\n\t\tborder-bottom-left-radius: 0 !important;\n\t\tborder-bottom-right-radius: 0 !important;\n\t\tborder-block-end: none !important;\n\t}\n\n:host([vertical]:not([separate])) ::slotted(:not(:first-of-type)) {\n\t\tborder-top-left-radius: 0 !important;\n\t\tborder-top-right-radius: 0 !important;\n\t}"
  },
  {
    "path": "elements/button-group/style.postcss",
    "content": ":host {\n\tdisplay: inline-flex;\n\n\t/* 0 specificity default pressed styles */\n\t& ::slotted(:where([aria-pressed=\"true\"])) {\n\t\tbackground: hsl(220 10% 90% / .9);\n\t\tbox-shadow: 0 .1em .2em hsl(0 0% 0% / .2) inset, 0 0 0 2em hsl(220 10% 50% / .15) inset;\n\t}\n}\n\n:host([separate]) {\n\tflex-wrap: wrap;\n\n\t& ::slotted(*) {\n\t\tflex: 1;\n\t\tmargin: .2em;\n\t}\n}\n\n:host(:not([separate])) {\n\tmargin: .5em;\n\n\t& ::slotted(*) {\n\t\tmargin: 0;\n\t}\n}\n\n:host(:not([vertical]):not([separate])) {\n\tjustify-content: center;\n\talign-items: stretch;\n\n\t& slot::slotted(*) {\n\t\tflex: 1;\n\t}\n\n\t& ::slotted(:not(:last-of-type)) {\n\t\tborder-top-right-radius: 0 !important;\n\t\tborder-bottom-right-radius: 0 !important;\n\t\tborder-inline-end: none !important;\n\t}\n\n\t& ::slotted(:not(:first-of-type)) {\n\t\tborder-top-left-radius: 0 !important;\n\t\tborder-bottom-left-radius: 0 !important;\n\t}\n}\n\n:host([vertical]:not([separate])) {\n\tflex-direction: column;\n\talign-items: stretch;\n\n\t& ::slotted(*) {\n\t\ttext-align: inline-start;\n\t}\n\n\t& ::slotted(:not(:last-of-type)) {\n\t\tborder-bottom-left-radius: 0 !important;\n\t\tborder-bottom-right-radius: 0 !important;\n\t\tborder-block-end: none !important;\n\t}\n\n\t& ::slotted(:not(:first-of-type)) {\n\t\tborder-top-left-radius: 0 !important;\n\t\tborder-top-right-radius: 0 !important;\n\t}\n}"
  },
  {
    "path": "elements/cycle-toggle/README.md",
    "content": "---\ntitle: <cycle-toggle>\nid: cycle-toggle\n---\n\n<header>\n\n# `<cycle-toggle>`\n\nClick to cycle through a variety of options\n\n</header>\n\n\n\n## Features\n\n- Uses [`ElementInternals`](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) to work like a built-in form element\n- Accessible (?)\n- Tiny (3K **uncompressed** and **unminified**!)\n\n\n## Examples\n\nBasic, no selected option:\n\n```html\n<label for=\"mood\">Mood:</label>\n<cycle-toggle id=\"mood\">\n\t<span>😔</span>\n\t<span>😕</span>\n\t<span>😐</span>\n\t<span>🙂</span>\n\t<span>😀</span>\n</cycle-toggle>\n```\n\nPre-selected option:\n\n```html\n<label for=\"mood2\">Mood:</label>\n<cycle-toggle id=\"mood2\">\n\t<span>😔</span>\n\t<span>😕</span>\n\t<span>😐</span>\n\t<span aria-selected=\"true\">🙂</span>\n\t<span>😀</span>\n</cycle-toggle>\n```\n\nWith values (any child element works):\n\n```html\n<label for=\"mood3\">Mood:</label>\n<cycle-toggle id=\"mood3\">\n\t<data value=\"sad\">😔</data>\n\t<data value=\"neutral\">😐</data>\n\t<data value=\"happy\" aria-selected=\"true\">🙂</data>\n\t<data value=\"elated\">😀</data>\n</cycle-toggle>\n```\n\nWith styles:\n\n```html\n<cycle-toggle>\n\t<data value=\"\" style=\"opacity: .4\">👍🏼</data>\n\t<data value=\"1\">👍🏼</data>\n</cycle-toggle>\n```\n\nReadonly:\n\n```html\n<cycle-toggle id=\"readonly_toggle\" readonly>\n\t<span>😔</span>\n\t<span>😕</span>\n\t<span>😐</span>\n\t<span aria-selected=\"true\">🙂</span>\n\t<span>😀</span>\n</cycle-toggle>\n<button onclick=\"readonly_toggle.readonly = !readonly_toggle.readonly\">Toggle readonly</button>\n```\n\nSet `element.value`:\n\n```html\n<cycle-toggle id=\"toggle_rate\">\n\t<data value=\"1\">👍🏼</data>\n\t<data value=\"-1\">👎🏼</data>\n</cycle-toggle>\n<button onclick=\"toggle_rate.value = 1\">Select 👍🏼</button>\n<button onclick=\"toggle_rate.value = -1\">Select 👎🏼</button>\n```\n\nDynamic `aria-selected`:\n\n```html\n<cycle-toggle id=\"dynamic_selected\">\n\t<span>😔</span>\n\t<span>😕</span>\n\t<span>😐</span>\n\t<span>🙂</span>\n\t<span>😀</span>\n</cycle-toggle>\n<button onclick=\"dynamic_selected.children[3].setAttribute('aria-selected', 'true')\">Select 🙂</button>\n```\n\n"
  },
  {
    "path": "elements/cycle-toggle/cycle-toggle.js",
    "content": "if (!HTMLSlotElement.prototype.assign) {\n\t// Include Imperative Slot Assignment polyfill\n\tawait import(\"https://unpkg.com/dom-slot-assign\");\n}\n\nexport default class CycleToggle extends HTMLElement {\n\t#internals\n\t#observer\n\t#selectedSlot\n\n\tconstructor () {\n\t\tsuper();\n\n\t\tthis.attachShadow({\n\t\t\tmode: \"open\",\n\t\t\tslotAssignment: \"manual\",\n\t\t\tdelegatesFocus: true,\n\t\t});\n\t\tthis.shadowRoot.innerHTML = `<style>@import \"${new URL(\"style.css\", import.meta.url)}\";</style><button><slot name=\"selected\"></slot></button>`;\n\t\tthis.#selectedSlot = this.shadowRoot.querySelector(\"slot\");\n\n\t\tthis.#internals = this.attachInternals?.();\n\n\t\tthis.addEventListener(\"click\", evt => {\n\t\t\tif (!this.hasAttribute(\"readonly\")) {\n\t\t\t\tthis.cycle();\n\t\t\t}\n\t\t});\n\t}\n\n\tget name () {\n\t\treturn this.getAttribute(\"name\");\n\t}\n\n\tset name (value) {\n\t\tthis.setAttribute(\"name\", value);\n\t}\n\n\tget readonly () {\n\t\treturn this.hasAttribute(\"readonly\");\n\t}\n\n\tset readonly (value) {\n\t\tif (value) {\n\t\t\tthis.setAttribute(\"readonly\", \"\");\n\t\t}\n\t\telse {\n\t\t\tthis.removeAttribute(\"readonly\");\n\t\t}\n\t}\n\n\t#value;\n\n\tget value () {\n\t\treturn this.#value;\n\t}\n\n\tset value (value) {\n\t\tvalue = value + \"\";\n\t\tthis.#value = value;\n\n\t\t// TODO should we reject unrecognized values or be lossless?\n\t\tthis.#internals?.setFormValue(value);\n\n\t\tfor (let option of this.children) {\n\t\t\tthis.#setSelected(option, getValue(option) === value);\n\t\t}\n\t}\n\n\tget selectedOptions () {\n\t\treturn [...this.querySelectorAll(`:scope > [aria-selected=\"true\"]`)];\n\t}\n\n\tget selectedOption () {\n\t\treturn this.selectedOptions?.at(-1) || this.firstElementChild;\n\t}\n\n\tget labels() {\n\t\treturn this.#internals?.labels;\n\t}\n\n\t// Select the next option, or the first if there is no next option.\n\tcycle () {\n\t\tthis.#unobserve();\n\t\tlet selectedOption = this.selectedOption;\n\t\tthis.#setSelected(this.selectedOption, false);\n\n\t\tlet nextOption = selectedOption.nextElementSibling || this.firstElementChild;\n\t\tthis.#setSelected(nextOption, true);\n\t\tthis.dispatchEvent(new InputEvent(\"input\"));\n\t\tthis.#observe();\n\t}\n\n\t#setSelected (option, selected = false) {\n\t\tif (!option) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (selected) {\n\t\t\tif (option.getAttribute(\"aria-selected\") !== \"true\") {\n\t\t\t\toption.setAttribute(\"aria-selected\", \"true\");\n\t\t\t}\n\n\t\t\tthis.#value = getValue(this.selectedOption);\n\t\t}\n\t\telse {\n\t\t\toption.removeAttribute(\"aria-selected\");\n\n\t\t\tif (this.#value === getValue(option)) {\n\t\t\t\tthis.#value = getValue(this.selectedOption);\n\t\t\t}\n\t\t}\n\n\t\tthis.#selectedSlot.assign(this.selectedOption);\n\t}\n\n\tconnectedCallback () {\n\t\tthis.value = getValue(this.selectedOption);\n\n\t\tthis.#observe();\n\t}\n\n\t#observe () {\n\t\tthis.#observer = this.#observer || new MutationObserver(mutations => {\n\t\t\tthis.value = getValue(this.selectedOption);\n\t\t});\n\n\t\tthis.#observer.observe(this, {\n\t\t\tattributeFilter: [\"aria-selected\", \"value\"],\n\t\t\tattributeOldValue: true,\n\t\t\tchildList: true,\n\t\t\tsubtree: true,\n\t\t});\n\t}\n\n\t#unobserve() {\n\t\tif (this.#observer) {\n\t\t\tthis.#observer.takeRecords();\n\t\t\tthis.#observer.disconnect();\n\t\t}\n\t}\n\n\tdisconnectedCallback () {\n\t\tthis.#unobserve();\n\t}\n\n\tstatic get formAssociated() {\n\t\treturn true;\n\t}\n}\n\nfunction getValue(element) {\n\tif (!element) {\n\t\treturn null;\n\t}\n\n\tif (element.hasAttribute(\"value\")) {\n\t\treturn element.getAttribute(\"value\");\n\t}\n\telse {\n\t\treturn element.textContent.trim();\n\t}\n}\n\ncustomElements.define(\"cycle-toggle\", CycleToggle);"
  },
  {
    "path": "elements/cycle-toggle/style.css",
    "content": ":host {\n\tcursor: pointer;\n\tuser-select: none;\n}\n\nbutton {\n\tall: unset;\n\toutline: revert;\n}"
  },
  {
    "path": "elements/data-bind/Observer.js",
    "content": "\nimport {\n\tinterceptPropertyWrites,\n\tflushMutationObserver,\n} from \"./util.js\";\nimport Recipe from \"./Recipe.js\";\n\nlet self = class Observer {\n\tconstructor (element, recipes) {\n\t\tthis.element = element;\n\t\tthis.recipes = recipes.map(property => new Recipe(property));\n\t\tthis.recipe = new Recipe(...this.recipes);\n\t}\n\n\tobserve (fn) {\n\t\tif (this.callback) {\n\t\t\tthis.unobserve();\n\t\t}\n\n\t\tthis.callback = fn;\n\n\t\tif (this.recipe.mutation) {\n\t\t\tthis.mutationObserver ??= new MutationObserver(records => this.changed({type: \"mutation\", records}));\n\t\t\tthis.mutationObserver.observe(this.element, this.recipe.mutation);\n\t\t}\n\n\t\tif (this.recipe.parentMutation) {\n\t\t\tlet parent = this.element.parentElement;\n\t\t\tthis.parentMutationObserver ??= new MutationObserver(records => {\n\t\t\t\tif (parent !== this.element.parentElement) {\n\t\t\t\t\t// Parent changed\n\n\t\t\t\t}\n\t\t\t\tthis.changed({type: \"mutation\", records});\n\t\t\t});\n\t\t\tthis.parentMutationObserver.observe(parent, this.recipe.parentMutation);\n\t\t}\n\n\t\tif (this.recipe.events) {\n\t\t\tfor (let event of this.recipe.events) {\n\t\t\t\tthis.element.addEventListener(event, this.changed);\n\t\t\t}\n\t\t}\n\n\t\tif (this.recipe.resize) {\n\t\t\tthis.resizeObserver ??= new ResizeObserver(entries => this.changed({type: \"resize\", entries}));\n\t\t\tthis.resizeObserver.observe(this.element);\n\t\t}\n\n\t\t// Observe direct property writes\n\t\tthis.descriptors = this.recipe.properties.map(property =>\n\t\t\tinterceptPropertyWrites(\n\t\t\t\tthis.element,\n\t\t\t\tproperty,\n\t\t\t\t(value, oldValue) => this.changed({type: \"set\", property, value, oldValue}),\n\t\t\t)\n\t\t);\n\t}\n\n\tunobserve () {\n\t\tflushMutationObserver(this.mutationObserver, records => this.changed({type: \"mutation\", records}));\n\t\tflushMutationObserver(this.parentMutationObserver, records => this.changed({type: \"mutation\", records}));\n\n\t\tthis.resizeObserver?.disconnect();\n\n\t\tif (this.recipe.events) {\n\t\t\tfor (let event of this.recipe.events) {\n\t\t\t\tthis.element.removeEventListener(event, this.changed);\n\t\t\t}\n\t\t}\n\n\t\tif (this.descriptors?.length) {\n\t\t\tfor (let {property, oldDescriptor} of this.descriptors) {\n\t\t\t\tuninterceptPropertyWrites(this.element, property, oldDescriptor);\n\t\t\t}\n\t\t}\n\t}\n\n\tchanged (change) {\n\t\tthis.callback?.(change);\n\t}\n}\n\nexport default self;"
  },
  {
    "path": "elements/data-bind/README.md",
    "content": "---\nid: data-bind\n---\n\n<header>\n\n# `<data-bind>`\n\nAn element for propagating data changes between elements.\n\n</header>\n\n\n\n## Features\n\n- TBD\n\n\n## Examples\n\n### Basic\n\nDisplay slider value:\n\n```html\n<data-bind>\n\t<input type=\"range\" data-bind-source></textarea>\n\t<span data-bind=\"value\"></span>\n</data-bind>\n```\n\nShow character count:\n\n```html\n<data-bind>\n\t<textarea data-bind-source></textarea>\n\t<span data-bind=\"value.length\"></span>\n</data-bind>\n```\n\n\n\n"
  },
  {
    "path": "elements/data-bind/Recipe.js",
    "content": "import properties from \"./properties.js\";\n\nconst self = class ObserveRecipe {\n\tevents = [];\n\tattributes = [];\n\tproperties = [];\n\ttext = false;\n\tdeep = false;\n\tchildren = false;\n\tsize = false;\n\n\t/**\n\t * @type {Recipe}\n\t */\n\tparent = null;\n\n\tconstructor (...specs) {\n\t\tthis.add(...specs);\n\t}\n\n\tadd (...recipes) {\n\t\tfor (let recipe of recipes) {\n\t\t\tif (typeof recipe === \"string\") {\n\t\t\t\trecipe = getRecipe(recipe);\n\t\t\t}\nconsole.log(recipe)\n\t\t\tif (recipe.property) {\n\t\t\t\tthis.properties.push(recipe.property);\n\t\t\t}\n\n\t\t\tif (this.attributes !== true) {\n\t\t\t\tif (recipe.attributes === true) {\n\t\t\t\t\tthis.attributes = true;\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tif (recipe.attribute) {\n\t\t\t\t\t\tthis.attributes.push(recipe.attribute);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (recipe.attributes?.length > 0) {\n\t\t\t\t\t\tthis.attributes.push(...recipe.attributes);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis.text ||= recipe.text;\n\t\t\tthis.deep ||= recipe.deep;\n\t\t\tthis.children ||= recipe.children;\n\n\t\t\tlet events = recipe.events ?? recipe.event;\n\n\t\t\tif (events) {\n\t\t\t\tevents = Array.isArray(events) ? events : [events];\n\t\t\t}\n\t\t\tif (recipe.event) {\n\t\t\t\tthis.events.push(recipe.event);\n\t\t\t\tthis.events.push(...events);\n\t\t\t}\n\n\t\t\tif (recipe.size) {\n\t\t\t\tthis.size ||= recipe.size;\n\t\t\t}\n\n\t\t\tif (recipe.parent) {\n\t\t\t\tif (this.parent) {\n\t\t\t\t\tthis.parent.add(recipe.parent);\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tthis.parent = new Recipe(recipe.parent);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthis.mutation = this.#getMutation();\n\t}\n\n\t#getMutation () {\n\t\tlet mutation = {};\n\n\t\tif (this.children) {\n\t\t\tmutation.childList = true;\n\t\t}\n\n\t\tif (this.text) {\n\t\t\tmutation.characterData = true;\n\t\t}\n\n\t\tif (this.deep) {\n\t\t\tmutation.subtree = true;\n\t\t}\n\n\t\tif (this.attributes === true || this.attributes?.length > 0) {\n\t\t\tmutation.attributes = true;\n\n\t\t\tif (this.attributes?.length > 0) {\n\t\t\t\tmutation.attributeFilter = this.attributes;\n\t\t\t}\n\t\t}\n\n\t\treturn Object.keys(mutation).length === 0 ? null : mutation;\n\t}\n}\n\nfunction getRecipe (propertyOrAttribute) {\n\tif (propertyOrAttribute.startsWith(\"@\")) {\n\t\t// Only attribute\n\t\treturn { attribute: propertyOrAttribute.slice(1) };\n\t}\n\n\tlet property = propertyOrAttribute.replace(/^\\./, \"\");\n\n\tif (properties[property]) {\n\t\treturn {\n\t\t\tproperty,\n\t\t\t...properties[property]\n\t\t};\n\t}\n\n\t// Search in also fields as well\n\tfor (let key in properties) {\n\t\tif (properties[key].also?.includes(property)) {\n\t\t\treturn {\n\t\t\t\tproperty,\n\t\t\t\t...properties[key]\n\t\t\t};\n\t\t}\n\t}\n\n\t// Still nothing, assume it's an attribute or arbitrary data property\n\tattribute = property.toLowerCase();\n\n\tif (attribute === property) {\n\t\t// Property is all-lowercase, if an attribute exists it will be the same\n\t\treturn { property, attribute };\n\t}\n\n\t// Property is camelCase, there are two possibilities\n\t// 1. The attribute is all-lowercase\n\t// 2. The attribute is kebab-case\n\treturn {\n\t\tproperty,\n\t\tattributes: [\n\t\t\tattribute,\n\t\t\tproperty.replace(/[A-Z]/g, \"-$&\").toLowerCase(),\n\t\t]\n\t}\n}\n\nexport default self;"
  },
  {
    "path": "elements/data-bind/data-bind.js",
    "content": "import Observer from \"./Observer.js\";\n\nconst tagName = \"data-bind\";\n\nlet self = class DataBindlement extends HTMLElement {\n\t_slots = {};\n\n\tconstructor () {\n\t\tsuper();\n\t}\n\n\tconnectedCallback () {\n\t\tthis.configure();\n\t\t// this.update();\n\t}\n\n\tconfigure () {\n\t\tif (this.hasAttribute(\"source\")) {\n\t\t\tthis.source = this.getAttribute(\"source\");\n\n\t\t\tif ([\"window\", \"document\"].includes(this.source)) {\n\t\t\t\tthis.sourceElement = window[this.source];\n\t\t\t}\n\t\t\telse if ([\"body\", \"head\"].includes(this.source)) {\n\t\t\t\tthis.sourceElement = document[this.source];\n\t\t\t}\n\t\t\telse {\n\t\t\t\tlet scope = this;\n\t\t\t\twhile (!this.sourceElement && scope) {\n\t\t\t\t\tthis.sourceElement = scope.querySelector(this.source);\n\t\t\t\t\tscope = scope.parentElement;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tthis.sourceElement = this.querySelector(\":scope > [data-bind-source]\");\n\t\t}\n\n\t\tthis.destElements = [...this.querySelectorAll(\":scope > :not([data-bind-source])\")];\n\n\t\tif (!this.sourceElement || this.destElements.length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tlet paths = this.destElements\n\t\t\t.filter(element => element.dataset.bind !== null) // Only elements with data-bind attribute\n\t\t\t.map(element => element.dataset.bind ?? \"textContent\"); // If data-bind is empty, use textContent (or should it be innerHTML?)\n\t\tlet properties = paths.map(path => path.split(\".\")[0]);\n\n\t\tthis.observer = new Observer(this.sourceElement, properties);\n\n\t\tthis.observer.observe(change => {\n\t\t\tthis.update(change);\n\t\t})\n\t}\n\n\tupdate ({ force, property } = {}) {\n\t\t// debugger;\n\t\t// this.destElements.forEach(element => {\n\t\t// \tif (!property || element.matches(`[data-bind=\"${property}\"], [data-bind^=\"${property}.\"]`)) {\n\t\t// \t\tthis.updateElement(element);\n\t\t// \t}\n\n\t\t// });\n\t}\n\n\tupdateElement (element) {\n\t\tif (!element.dataset.bind) {\n\t\t\treturn;\n\t\t}\n\n\t\tlet path = element.dataset.bind.split(\".\");\n\t\tlet property = path[0];\n\n\t\tif (element.dataset.bind === property) {\n\t\t\t// Single property\n\t\t\telement.textContent = this.sourceElement[property];\n\t\t}\n\t\telse if (element.dataset.bind?.startsWith(`${property}.`)) {\n\n\t\t\tlet obj = this.sourceElement;\n\t\t\tlet i = 0;\n\n\t\t\twhile (obj !== null && obj !== undefined && i < path.length - 1) {\n\t\t\t\tobj = obj?.[path[i++]];\n\t\t\t}\n\n\t\t\telement.textContent = obj[path[i]];\n\t\t}\n\t}\n\n\tstatic observedAttributes = [\"source\"];\n\n\tattributeChangedCallback (name, oldValue, newValue) {\n\t\tif (oldValue !== newValue) {\n\t\t\tthis[name] = newValue;\n\t\t}\n\t}\n}\n\ncustomElements.define(tagName, self);\n\nexport default self;\n\n"
  },
  {
    "path": "elements/data-bind/properties.js",
    "content": "let properties = {\n\ttextContent: {\n\t\tchildren: true,\n\t\ttext: true,\n\t\tdeep: true,\n\t},\n\tinnerHTML: {\n\t\tchildren: true,\n\t\ttext: true,\n\t\tdeep: true,\n\t\tattributes: true,\n\n\t\talso: [\n\t\t\t\"outerHTML\",\n\t\t],\n\t},\n\tvalue: {\n\t\tevent: \"input\",\n\n\t\talso: [\n\t\t\t\"checked\",\n\t\t\t\"valueAsNumber\",\n\t\t\t\"valueAsDate\",\n\t\t],\n\t},\n\tdefaultValue: {\n\t\tattribute: \"value\",\n\t\ttext: true, // for <textarea>\n\t},\n\tdefaultChecked: {\n\t\tattribute: \"checked\",\n\t},\n\tclassName: {\n\t\tattribute: \"class\",\n\t},\n\tclassList: {\n\t\tattribute: \"class\",\n\t},\n\toffsetWidth: {\n\t\tsize: true,\n\n\t\talso: [\n\t\t\t\"offsetHeight\",\n\t\t\t\"clientWidth\",\n\t\t\t\"clientHeight\",\n\t\t],\n\t},\n\tparentNode: {\n\t\tparent: {\n\t\t\tchildren: true,\n\t\t},\n\n\t\talso: [\n\t\t\t\"parentElement\",\n\t\t\t\"nextElementSibling\",\n\t\t\t\"previousElementSibling\",\n\t\t],\n\t},\n\tnextSibling: {\n\t\tparent: {\n\t\t\tchildren: true,\n\t\t\ttext: true,\n\t\t},\n\n\t\talso: [\n\t\t\t\"previousSibling\",\n\t\t],\n\t},\n\tchildNodes: {\n\t\tchildren: true,\n\t\ttext: true,\n\n\t\talso: [\n\t\t\t\"firstChild\",\n\t\t\t\"lastChild\",\n\t\t],\n\t},\n\tchildren: {\n\t\tchildren: true,\n\n\t\talso: [\n\t\t\t\"firstElementChild\",\n\t\t\t\"lastElementChild\",\n\t\t\t\"childElementCount\",\n\t\t],\n\t},\n\tscrollTop: {\n\t\tevent: \"scroll\",\n\n\t\talso: [\n\t\t\t\"scrollLeft\",\n\t\t],\n\t},\n\tattributes: {\n\t\tattributes: true,\n\t},\n};\n\nexport default properties;"
  },
  {
    "path": "elements/data-bind/util.js",
    "content": "/**\n * Get a property descriptor from an object or its prototype chain.\n * @param {*} object\n * @param {*} key\n * @returns\n */\nexport function getPropertyDescriptor (object, key) {\n\twhile (object) {\n\t\tdescriptor = Object.getOwnPropertyDescriptor(object, key);\n\n\t\tif (descriptor) {\n\t\t\treturn {object, descriptor};\n\t\t}\n\n\t\tobject = Object.getPrototypeOf(object);\n\t}\n}\n\nexport function interceptPropertyWrites (obj, property, callback) {\n\tlet {descriptor: d, object} = getPropertyDescriptor(obj, property);\n\n\tlet value = (d && \"value\" in d) ? d.value : obj[property];\n\n\tlet descriptor = {\n\t\tget: d.get ?? function() {\n\t\t\treturn value;\n\t\t},\n\t\tset (newValue) {\n\t\t\tlet oldValue = obj[property];\n\t\t\tif (d?.set) {\n\t\t\t\td.set.call(this, newValue);\n\t\t\t}\n\t\t\telse {\n\t\t\t\tobj[property] = value = newValue;\n\t\t\t}\n\t\t\tcallback({ type: \"set\", property, value, oldValue });\n\t\t},\n\t\tconfigurable: true,\n\t\tenumerable: d.enumerable ?? true,\n\t}\n\n\tObject.defineProperty(obj, property, descriptor);\n\n\treturn { descriptor, originalDescriptor: d, inherited: object !== obj };\n}\n\nexport function uninterceptPropertyWrites (obj, property, descriptor) {\n\tif (!descriptor.get) {\n\t\t// Data property\n\t\tlet currentValue = obj[property];\n\t\tdescriptor = {...descriptor, value: currentValue};\n\t}\n\n\tObject.defineProperty(obj, property, descriptor);\n}\n\nexport function flushMutationObserver (mutationObserver, callback) {\n\tif (!mutationObserver) {\n\t\treturn;\n\t}\n\n\tlet records = mutationObserver.takeRecords();\n\tif (records.length > 0) {\n\t\tcallback(records);\n\t}\n\tmutationObserver.disconnect();\n}"
  },
  {
    "path": "elements/drop-down/README.md",
    "content": "---\ntitle: <drop-down>\nid: drop-down\n---\n<header>\n\n# `<drop-down>`\n\nDrop-down menu that performs actions when items are clicked\n\n</header>\n\n\n\n<section class=\"failed\">\n\n## Failed experiment\n\nThis is a failed experiment. Do not use.\nIt is only posted here, in the hopes that someone else may be able to fix its issues.\n\n### Issues\n\n- It is impossible to reliably detect when the `<select>` is closed,\nso when the menu is closed with no selection, `aria-pressed` lingers until the button is unfocused.\n\n</section>\n\n## Features\n\n- Uses a regular `<select>` menu, ensuring it works well on a variety of devices\n- Accessible (?)\n- Tiny (3K **uncompressed** and **unminified**!)\n\n\n## Examples\n\nBasic:\n\n```html\n<drop-down>\n\t<button>Click me</button>\n\t<select>\n\t\t<option>One</option>\n\t\t<option>Two</option>\n\t\t<option onclick=\"alert('hi')\">Three</option>\n\t</select>\n</drop-down>\n```\n\nWith customized menu label:\n\n```html\n<drop-down>\n\t<button>+</button>\n\t<select aria-label=\"Create new…\">\n\t\t<option>Document</option>\n\t\t<option>Sheet</option>\n\t\t<option onclick=\"alert('hi')\">Picture</option>\n\t</select>\n</drop-down>\n```\n\n"
  },
  {
    "path": "elements/drop-down/drop-down.js",
    "content": "if (!HTMLSlotElement.prototype.assign) {\n\t// Include Imperative Slot Assignment polyfill\n\tawait import(\"https://unpkg.com/dom-slot-assign\");\n}\n\nexport default class DropDown extends HTMLElement {\n\t#internals\n\t#triggerSlot\n\t#menuSlot\n\t#observer\n\t#resizeObserver\n\t#menu\n\t#trigger\n\n\tconstructor () {\n\t\tsuper();\n\n\t\tthis.attachShadow({\n\t\t\tmode: \"open\",\n\t\t\tslotAssignment: \"manual\",\n\t\t\tdelegatesFocus: true,\n\t\t});\n\t\tthis.shadowRoot.innerHTML = `\n\t\t<style>@import \"${new URL(\"style.css\", import.meta.url)}\";</style>\n\t\t<div id=\"trigger-container\">\n\t\t\t<slot name=\"trigger\"></slot>\n\t\t</div>\n\t\t<slot name=\"menu\"></slot>\n\t\t`;\n\t\tthis.#triggerSlot = this.shadowRoot.querySelector(\"slot[name=trigger]\");\n\t\tthis.#menuSlot = this.shadowRoot.querySelector(\"slot[name=menu]\");\n\t\tthis.#trigger = this.shadowRoot.querySelector(\"#trigger\");\n\n\t\tthis.#childrenChanged();\n\n\t\t// this.#internals = this.attachInternals?.();\n\n\t\tthis.#observe();\n\n\t\tthis.addEventListener(\"click\", evt => this.#handleEvent(evt));\n\t\tthis.addEventListener(\"input\", evt => this.#handleEvent(evt));\n\t\tthis.addEventListener(\"focusin\", evt => this.#handleEvent(evt));\n\t\tthis.addEventListener(\"focusout\", evt => this.#handleEvent(evt));\n\t}\n\n\tget labels() {\n\t\treturn this.#internals?.labels;\n\t}\n\n\t#handleEvent (evt) {\n\t\tif (evt.type === \"input\") {\n\t\t\tif (evt.target === this.#menu) {\n\t\t\t\tthis.#trigger.removeAttribute(\"aria-pressed\");\n\t\t\t\tlet item = evt.target.selectedOptions[0];\n\t\t\t\tlet value = evt.target.value;\n\n\t\t\t\tthis.dispatchEvent(new CustomEvent(\"dropdownselect\", {\n\t\t\t\t\tbubbles: true,\n\t\t\t\t\tdetail: { item, value }\n\t\t\t\t}));\n\n\t\t\t\t// Dispatch individual click event to selected option\n\t\t\t\titem.dispatchEvent(new MouseEvent(\"click\"));\n\n\t\t\t\t// Reset selected option\n\t\t\t\tthis.#menu.options[0].selected = true;\n\n\t\t\t\t// Stop input event from propagating further\n\t\t\t\tevt.stopPropagation();\n\t\t\t}\n\t\t}\n\t\telse if (evt.type === \"click\") {\n\t\t\tif (evt.target === this.#menu) {\n\t\t\t\tthis.#trigger.setAttribute(\"aria-pressed\", \"true\");\n\t\t\t\tthis.ownerDocument.addEventListener(\"click\", evt => this.#handleEvent(evt), {once: true});\n\t\t\t}\n\t\t\tif (!this.contains(evt.target)) {\n\t\t\t\tthis.#trigger.removeAttribute(\"aria-pressed\");\n\t\t\t}\n\t\t}\n\t\telse if (evt.type === \"focusin\") {\n\n\t\t}\n\t\telse if (evt.type === \"focusout\") {\n\t\t\tif (evt.target === this.#menu) {\n\t\t\t\tthis.#trigger.removeAttribute(\"aria-pressed\");\n\t\t\t}\n\t\t}\n\t}\n\n\t#observe () {\n\t\tthis.#observer = this.#observer || new MutationObserver(mutations => {\n\t\t\t// An element can't be in both slots at once, so once the <select> is assigned\n\t\t\t// to the select slot, it will be removed from the trigger slot\n\t\t\tthis.#childrenChanged();\n\t\t});\n\n\t\tthis.#observer.observe(this, {\n\t\t\tchildList: true,\n\t\t});\n\t}\n\n\t#childrenChanged () {\n\t\tlet select = this.querySelectorAll(\":scope > select\")[0];\n\t\tlet trigger = this.querySelectorAll(\":scope > :not(select)\")[0];\n\t\tthis.#triggerSlot.assign(trigger);\n\t\tthis.#menuSlot.assign(select);\n\n\t\tthis.#trigger = trigger;\n\t\tthis.#menu = select;\n\n\t\tlet label = this.#menu.ariaLabel || \"Select:\";\n\t\tthis.#menu.insertAdjacentHTML(\"afterbegin\", `<option style=\"opacity: .5\" disabled selected>${label}</option>`);\n\t\tthis.#menu.size = 0; // make sure it has a popup\n\t\tthis.#menu.multiple = false;\n\n\t\tthis.#trigger.ariaHasPopup = \"true\";\n\n\t\t// Observe trigger size so we can set <select> size appropriately\n\t\tthis.#resizeObserver = this.#resizeObserver || new ResizeObserver(entries => {\n\t\t\tfor (let entry of entries) {\n\t\t\t\tthis.style.setProperty(\"--trigger-width\", `${entry.borderBoxSize.width}px`);\n\t\t\t}\n\t\t});\n\n\t\tthis.#resizeObserver.observe(this.#trigger);\n\t}\n\n\t// #unobserve() {\n\t// \tif (this.#observer) {\n\t// \t\tthis.#observer.takeRecords();\n\t// \t\tthis.#observer.disconnect();\n\t// \t}\n\t// }\n\n\t// static get formAssociated() {\n\t// \treturn true;\n\t// }\n}\n\ncustomElements.define(\"drop-down\", DropDown);"
  },
  {
    "path": "elements/drop-down/style.css",
    "content": ":host {\n\tdisplay: inline-grid;\n\tgrid-template: auto / auto;\n\toverflow: hidden;\n}\n\nslot[name=menu]::slotted(select),\n#trigger-container {\n\tgrid-column: 1;\n\tgrid-row: 1;\n}\n\nslot[name=menu]::slotted(select) {\n\topacity: 0 !important;\n\ttransform: scaleY(2);\n\ttransform-origin: top;\n\tmin-width: 0 !important;\n\twidth: var(--trigger-width, 100%) !important;\n}\n\n\nslot[name=trigger]::slotted(button) {\n\t/*padding-right: 1.5rem;\n\t-webkit-appearance: none;\n\tbackground-image: url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 50\">\\\n\t\t<path d=\"m 20 10 L 50 40 L 80 10\" fill=\"none\" stroke=\"black\" stroke-opacity=\".4\" stroke-width=\"13\" stroke-linejoin=\"round\" stroke-linecap=\"round\" />\\\n\t</svg>');\n\tbackground-position: calc(100% + 1rem) 55%;\n\tbackground-size: auto .4rem;\n\tbackground-repeat: no-repeat;\n\tbackground-origin: content-box;*/\n\tlist-style: \"N\"\n}"
  },
  {
    "path": "elements/html-demo/README.md",
    "content": "---\nid: html-demo\n---\n\n<header>\n\n# `<html-demo>`\n\nAn element for displaying HTML content alongside its source code.\nGreat for documenting web components!\n\n</header>\n\n\n\n## Features\n\n- Provide a code snippet and it will create the demo, or provide the demo and it will create the code snippet.\n- Demo inherits page styles but you can optionally isolate\n- Executes `<script>` tags (in code-first mode)\n\n### Roadmap\n\nFrom most to least likely to be implemented:\n\n- More style customization (parts, CSS properties)\n- Option to collapse code by default\n- Open in CodePen button (need a way to specify dependencies)\n- Structured attribute values\n- Work with CSS and JS snippets (without having to include them in HTML markup)\n- Different layouts\n- Editable examples\n\n\n## Examples\n\n### Basic\n\nCode-first:\n\n```html\n<html-demo>\n\t<pre class=\"language-html\"><code>\n\t\t&lt;input type=range>\n\t</code></pre>\n</html-demo>\n```\n\nContent-first:\n\n```html\n<html-demo id=foo>\n\t<input type=range>\n</html-demo>\n```\n\n### Adjusters\n\nOnly `font-size` for now:\n\n```html\n<html-demo adjust=\"font-size\">\n\t<button>Click me</button>\n</html-demo>\n```\n\nUse `--font-size-min` and `--font-size-max` to set the range (default: `50%` to `300%`).\n\n### Style isolation\n\nBy default the demo is rendered in the light DOM, and thus inherits the normal page styles.\nIn most cases, this is what you want.\nIf not, you can use the `isolate` attribute to use the UA’s default styles.\nThis works with both modes:\n\n<table>\n<thead>\n\t<tr>\n\t\t<th>Content-first</th>\n\t\t<th>Code-first</th>\n\t</tr>\n</thead>\n<tr>\n<td>\n\n```html\n<html-demo isolate>\n\t<button>Click me</button>\n</html-demo>\n```\n</td>\n<td>\n\n```html\n<html-demo isolate>\n\t<pre class=\"language-html\"><code>\n\t\t&lt;button>Click me&lt;/button>\n\t</code></pre>\n</html-demo>\n```\n</td>\n</tr>\n</table>\n\n\n\n\n### Execute script\n\nIn code-first mode, any `<script>` elements will also be executed:\n\n```html\n<html-demo>\n\t<pre class=\"language-html\"><code>\n\t\t&lt;button>Click me&lt;/button>\n\t\t&lt;script>{\n\t\t\tlet button = document.currentScript.previousElementSibling;\n\t\t\t// button.onclick = e =>\n\t\t\tbutton.textContent = \"Hi from script!\";\n\t\t}&lt;/script>\n\t</code></pre>\n</html-demo>\n```\n\n#### Executing scripts in isolated mode { #script-isolate }\n\nDo note that there is **limited utility in doing this in isolated mode**, since\nthere is no (easy) way to get a reference to any of the other elements in the demo:\n- [`document.currentScript` is `null` in shadow trees](https://html.spec.whatwg.org/multipage/dom.html#dom-document-currentscript-dev)\n- All `document.querySelector*()` or `document.getElementBy*()` calls will query the light DOM\n- Ids will not create variables\n- `this` will be the global `window` object or `undefined` in module scripts.\n\n\n```html\n<html-demo isolate>\n\t<pre id=\"isolated-demo\" class=\"language-html\"><code>\n\t\t&lt;p>This demo has no actual content, but scroll down a bit 👇🏼 &lt;/p>\n\t\t&lt;script>{\n\t\t\tlet pre = document.getElementById(\"isolated-demo\");\n\t\t\tlet container = pre.closest(\"body > *\");\n\t\t\tcontainer.after(\"Hi from shadow tree script!\");\n\t\t}&lt;/script>\n\t</code></pre>\n</html-demo>\n```\n\n## Auto-wrapping HTML code snippets on a whole page\n\nThe element class provides two helper methods for this very thing:\n\n```js\nimport HTMLDemoElement from \"https://nudeui.com/elements/html-demo/html-demo.js\";\n\nHTMLDemoElement.wrapAll({\n\tcontainer: mySection,\n\tignore: \".no-html-demo, #installation, #some-other-section\",\n});\n```\n\nAll parameters are optional.\n\n| Name | Default value | Description |\n| --- | --- | --- |\n| `container` | `document.body` | The element to search for `<html-demo>` elements. |\n| `ignore` | `\"\"` | A CSS selector for elements to ignore. |\n| `languages` | `[\"html\", \"markup\"]` | The `language-xxx` classes whose code snippets to wrap |\n\n"
  },
  {
    "path": "elements/html-demo/html-demo.css",
    "content": ":host {\n\t--_font-size-min: var(--font-size-min, 50%);\n\t--_font-size-max: var(--font-size-max, 400%);\n\n\t--color-neutral: oklch(50% 0.03 230);\n\t--color-canvas: color-mix(in oklch, canvas, oklch(none 0.002 none) 100%);\n\t--color-neutral-95: color-mix(in oklch, var(--color-neutral), var(--color-canvas) 96%);\n\t--color-neutral-90: color-mix(in oklch, var(--color-neutral), var(--color-canvas) 90%);\n\t--color-neutral-80: color-mix(in oklch, var(--color-neutral), var(--color-canvas) 80%);\n\t--color-neutral-70: color-mix(in oklch, var(--color-neutral), var(--color-canvas) 70%);\n\n\tdisplay: flex;\n\tflex-flow: column;\n\tborder: 1px solid var(--color-neutral-70);\n\tborder-radius: .3rem;\n\tmargin-block: .5em;\n}\n\n::slotted(pre) {\n\tmargin: 0 !important;\n\tpadding: .6em .8em;\n\tbackground: var(--color-neutral-95);\n\tborder-top: 1px solid var(--color-neutral-80);\n\tborder-radius: inherit;\n\tborder-top-left-radius: 0 !important;\n\tborder-top-right-radius: 0 !important;\n\tfont-size: 80%;\n}\n\n#toolbar {\n\tdisplay: flex;\n\tgap: 1em;\n\tborder-bottom: 1px solid var(--color-neutral-80);\n\n\t&:empty,\n\t&:has(#adjusters:empty) {\n\t\tdisplay: none;\n\t}\n\n\t> * {\n\t\tpadding: .3rem .5rem;\n\t\tflex: 1;\n\n\t\t&:not(:first-child) {\n\t\t\tborder-left: 1px solid var(--color-neutral-80);\n\t\t}\n\t}\n}\n\n#adjusters {\n\tdisplay: flex;\n\tflex: 1;\n\tgap: 1em;\n}\n\n.adjuster {\n\tflex: 1;\n\n\t&.font-size {\n\t\tdisplay: flex;\n\t\tgap: .1em;\n\t\talign-items: center;\n\n\t\t.small, .big {\n\t\t\tfont: 100%/1 serif;\n\t\t}\n\n\t\t.small {\n\t\t\tfont-size: var(--_font-size-min);\n\t\t}\n\n\t\t.big {\n\t\t\tfont-size: clamp(var(--_font-size-min), var(--_font-size-max), 200%);\n\t\t}\n\n\t\tinput {\n\t\t\tflex: 1;\n\t\t\theight: .3em;\n\t\t}\n\t}\n}\n\n\n\nslot[name=demo] {\n\tdisplay: block;\n\t--font-size-adjust: var(--adjust-font-size, 0.5);\n\t--font-size-range-low: calc(100% - var(--_font-size-min));\n\t--font-size-range-high: calc(var(--_font-size-max) - 100%);\n\n\tpadding: 1em;\n\n\tfont-size: calc(\n\t\tvar(--_font-size-min)\n\t\t+ clamp(0, var(--font-size-adjust) * 2, 1) * var(--font-size-range-low)\n\t\t+ clamp(0, (var(--font-size-adjust) - 0.5) * 2, 1) * var(--font-size-range-high)\n\t);\n}"
  },
  {
    "path": "elements/html-demo/html-demo.js",
    "content": "let styleURL = new URL(\"./html-demo.css\", import.meta.url);\n\nlet Prism = globalThis.Prism;\nif (!Prism) {\n\tawait import(\"https://prismjs.com/prism.js\");\n}\nPrism = globalThis.Prism;\n\nif (!Prism.plugins.NormalizeWhitespace) {\n\tawait import(\"https://prismjs.com/plugins/normalize-whitespace/prism-normalize-whitespace.min.js\");\n}\n\nlet self = class HTMLDemoElement extends HTMLElement {\n\t#el = {};\n\t#slots = {};\n\tadjust = {};\n\t#observer = new MutationObserver((mutations) => {\n\t\tthis.#assignSlots();\n\t\tthis.#render();\n\t});\n\t#dummy = document.createElement(\"div\");\n\n\tconstructor () {\n\t\tsuper();\n\t\tthis.attachShadow({\n\t\t\tmode: \"open\",\n\t\t\tslotAssignment: \"manual\"\n\t\t});\n\n\t\t// TODO CodePen\n\t\t// https://assets.codepen.io/t-1/codepen-logo.svg\n\n\t\tthis.shadowRoot.innerHTML = `\n\t\t\t<style>@import url(\"${ styleURL }\")</style>\n\t\t\t<div id=\"toolbar\">\n\t\t\t\t<div id=\"adjusters\"></div>\n\t\t\t\t<slot name=\"toolbar\"></slot>\n\t\t\t</div>\n\t\t\t<slot name=\"demo\" data-default></slot>\n\t\t\t<slot name=\"code\" data-assign=\"pre\"></slot>\n\t\t`;\n\n\t\tfor (let slot of this.shadowRoot.querySelectorAll(\"slot\")) {\n\t\t\tthis.#slots[slot.name] = slot;\n\n\t\t\tif (!slot.name || slot.dataset.default !== undefined) {\n\t\t\t\tthis.#slots.default = slot;\n\t\t\t}\n\t\t}\n\n\t\tfor (let el of this.shadowRoot.querySelectorAll(\"[id]\")) {\n\t\t\tthis.#el[el.id] = el;\n\t\t}\n\t}\n\n\tconnectedCallback () {\n\t\tthis.#assignSlots();\n\t\tthis.#render();\n\n\t\tthis.#observe();\n\t}\n\n\t#observe () {\n\t\tthis.#observer.observe(this, { childList: true });\n\t}\n\n\t#unoobserve () {\n\t\tthis.#observer.disconnect();\n\t}\n\n\tdisconnectedCallback () {\n\t\tthis.#unoobserve();\n\t}\n\n\t#assignSlots () {\n\t\tlet children = this.childNodes;\n\t\tlet slotElements = Object.values(this.#slots);\n\t\tlet assignments = new WeakMap();\n\n\t\t// Assign to slots\n\t\tfor (let child of children) {\n\t\t\tlet assignedSlot;\n\n\t\t\tif (child.slot) {\n\t\t\t\t// Explicit slot\n\t\t\t\tassignedSlot = this.#slots[child.slot];\n\t\t\t}\n\t\t\telse if (child.matches) {\n\t\t\t\tassignedSlot = slotElements.find(slot => child.matches(slot.dataset.assign));\n\t\t\t}\n\n\t\t\tassignedSlot ??= this.#slots.default;\n\t\t\tlet all = assignments.get(assignedSlot) ?? new Set();\n\t\t\tall.add(child);\n\t\t\tassignments.set(assignedSlot, all);\n\t\t}\n\n\t\tfor (let slot of slotElements) {\n\t\t\tlet all = assignments.get(slot) ?? new Set();\n\t\t\tslot.assign(...all);\n\t\t}\n\t}\n\n\t#render () {\n\t\tif (this.children.length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#unoobserve(); // avoid mutation cycles\n\n\t\tthis.#el.codeElements = [...this.#slots.code.assignedNodes()];\n\t\tthis.#el.demoNodes = [...this.#slots.demo.assignedNodes()];\n\n\t\t// Once source is determined mutations can't change it\n\t\tthis.source ??= this.getAttribute(\"source\") ?? (this.#el.codeElements.length > 0 ? \"code\" : \"content\");\n\t\tthis.isolate = this.hasAttribute(\"isolate\");\n\n\t\tif (this.source == \"code\") {\n\t\t\t// Code-first\n\t\t\tlet previousCode = this.code;\n\n\t\t\t// TODO handle non-markup code\n\t\t\tthis.code = this.#el.codeElements.map(code => code.textContent).join(\"\\n\");\n\n\t\t\tif (previousCode === this.code) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// TODO handle scripts\n\n\t\t\tif (this.isolate) {\n\t\t\t\t// Remove past demo nodes\n\t\t\t\tthis.#el.demoNodes.forEach(node => node.remove());\n\t\t\t\tthis.#slots.demo.assign();\n\t\t\t\tthis.#slots.demo.innerHTML = this.code;\n\t\t\t\trunScripts(this.#slots.demo.children);\n\t\t\t}\n\t\t\telse {\n\t\t\t\tthis.#dummy.innerHTML = this.code;\n\t\t\t\tlet nodes = [...this.#dummy.childNodes]\n\t\t\t\tthis.append(...nodes);\n\t\t\t\tthis.#slots.demo.assign(...nodes);\n\t\t\t\trunScripts(nodes);\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\t// Get code from content\n\t\t\tthis.code = this.#el.demoNodes.map(el => el.outerHTML ?? el.textContent).join(\"\");\n\n\t\t\t// TODO Clean up markup\n\t\t\tlet pre = document.createElement(\"pre\");\n\t\t\tlet code = document.createElement(\"code\");\n\t\t\tpre.classList.add(\"language-html\", \"html-demo-code\");\n\t\t\tcode.textContent = this.code;\n\t\t\tpre.append(code);\n\n\t\t\tthis.append(pre);\n\t\t\tthis.#slots.code.assign(pre);\n\n\t\t\tif (this.isolate) {\n\t\t\t\t// Move demo nodes to shadow root\n\t\t\t\tlet fragment = document.createDocumentFragment();\n\t\t\t\tfragment.append(...this.#el.demoNodes);\n\t\t\t\tthis.#slots.demo.replaceChildren(...fragment.childNodes);\n\t\t\t}\n\t\t}\n\n\t\tPrism.highlightAllUnder(this);\n\n\t\t// Render adjusters\n\t\tthis.#el.adjusters.innerHTML = \"\";\n\n\t\tif (this.hasAttribute(\"adjust\")) {\n\t\t\tlet adjusters = this.getAttribute(\"adjust\").split(/\\s+/);\n\t\t\tfor (let adjuster of adjusters) {\n\t\t\t\tthis.#renderAdjuster(adjuster);\n\t\t\t}\n\t\t}\n\n\t\tthis.#observe();\n\t}\n\n\t#renderAdjuster (adjuster) {\n\t\tlet template = self.adjusterTemplates[adjuster]?.call(this);\n\n\t\tif (!template) {\n\t\t\treturn null;\n\t\t}\n\n\t\tlet container = appendHTML(this.#el.adjusters, template);\n\t\tlet formControl = container.querySelector(\".main-adjuster, input, select, textarea\");\n\t\tformControl.addEventListener(\"input\", e => {\n\t\t\tlet value = Number(formControl.value);\n\t\t\tthis.adjust[adjuster] = value;\n\t\t\tthis.#slots.demo.style.setProperty(\"--adjust-\" + adjuster, value);\n\t\t});\n\n\t\treturn container;\n\t}\n\n\t/**\n\t * Wrap one or more elements with <html-demo>\n\t * Assumes elements are siblings\n\t * @param  {...any} elements\n\t * @returns\n\t */\n\tstatic wrap (...elements) {\n\t\tlet wrapper = document.createElement(\"html-demo\");\n\t\telements[0].replaceWith(wrapper);\n\t\twrapper.append(...elements);\n\t\treturn wrapper;\n\t}\n\n\t/**\n\t * Wrap <pre> elements under a given container\n\t * @param {object} options\n\t * @param {Node} [options.container=document]\n\t * @param {string[]} [options.languages=[\"html\", \"markup\"]\n\t * @param {string} [options.ignore=\".no-html-demo, #installation\"]\n\t */\n\tstatic wrapAll ({\n\t\tcontainer = document,\n\t\tlanguages = [\"html\", \"markup\"],\n\t\tignore = \".no-html-demo, #installation\",\n\t\tselector = \"\",\n\t} = {}) {\n\t\tlet languageSelector = languages.flatMap(id => `.language-${ id }, .language-${ id } *`).join(\", \");\n\t\tlet ignoreSelector = `html-demo *, :is(${ignore}) *`;\n\n\t\t// TODO wrap adjacent <pre> elements together\n\t\t// TODO handle CSS and JS\n\t\tlet elements = container.querySelectorAll(`pre:has(> code:is(${ languageSelector }):not(${ ignoreSelector }))`);\n\t\telements = Array.from(elements, pre => this.wrap(pre));\n\t\treturn elements;\n\t}\n\n\tstatic adjusterTemplates = {\n\t\t\"font-size\": function() {\n\t\t\treturn `\n\t\t\t<label class=\"font-size adjuster\">\n\t\t\t\t<small class=\"small\">A</small>\n\t\t\t\t<input type=\"range\" min=\"0\" max=\"1\" step=\".01\" value=\"${ this.adjust[\"font-size\"] ?? \"0.5\"}\" aria-label=\"Adjust font size\" />\n\t\t\t\t<strong class=\"big\">A</strong>\n\t\t\t</label>`;\n\t\t}\n\t};\n}\n\nfunction appendHTML (container, html) {\n\tcontainer.insertAdjacentHTML(\"beforeend\", html);\n\treturn container.children[container.children.length - 1];\n}\n\n/**\n * Execute any inline <scripts> in an array of nodes\n * @param {Array<Node>} nodes\n */\nfunction runScripts (nodes) {\n\tfor (let node of nodes) {\n\t\tif (node.matches?.(\"script\")) {\n\t\t\tconst clone = document.createElement(\"script\");\n\t\t\tnode.getAttributeNames().forEach(name => clone.setAttribute(name, node.getAttribute(name)));\n\t\t\tclone.append(node.innerHTML);\n\t\t\tnode.replaceWith(clone);\n\t\t}\n\t}\n}\n\ncustomElements.define(\"html-demo\", self);\n\nexport default self;"
  },
  {
    "path": "elements/img-input/README.md",
    "content": "---\nid: img-input\n---\n\n<header>\n\n# `<img-input>`\n\nForm control for image linking and uploading.\n\n</header>\n\n\n\n## Features\n\n- Paste, drag & drop, upload, or provide a URL, all with the same unified API!\n- Inline preview (`nopreview` attribute to disable)\n- Uses [`ElementInternals`](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) to work like a built-in form element\n- Ultra light\n\n## TODO\n\n- `multiple` attribute?\n- Retargeting of input attributes (`autofocus`, `placeholder` etc)\n\n\n## Examples\n\nBasic\n\n```html\n<img-input></img-input>\n```\n\n## Customizing the preview\n\nBy default, the preview is shown in the same element as the input.\nThere are two ways to customize this: using the `preview` slot, or the `preview` attribute.\n\nYou can set the `preview` attribute to `none` for no preview:\n\n```html\n<img-input preview=\"none\"></img-input>\n```\n\nYou can also set it to a CSS selector pointing to another element:\n\n```html\n<img-input preview=\"#preview\"></img-input>\n<img id=\"preview\">\n```\n\nAlternatively, you can use the `preview` slot to provide your own `<img>` element:\n\n```html\n<img-input>\n  <img slot=\"preview\">\n</img-input>\n```\n\nPlease note that if the `preview` attribute is set, the `preview` slot will be ignored.\n\nThe attribute can be dynamic as well:\n\n```html\n<img-input></img-input>\n<button onclick=\"this.previousElementSibling.preview =\n  this.previousElementSibling.preview === 'none' ? '' : 'none'\">\n\tToggle preview\n</button>\n```\n\n## CSS parts\n\n- `input`, `location` - The input element used for URL or filename\n- `dropzone` The drop zone\n- `button`, `browse-button` - The button used to open the file browser\n- `preview` - The preview image\n\n## Slots\n\n- `input` to replace the default input element\n- `browse` to replace the default “Browse…” button\n- `preview` to replace the default preview image\n\n"
  },
  {
    "path": "elements/img-input/img-input.js",
    "content": "export default class ImageInput extends HTMLElement {\n\t#internals\n\t#el = {}\n\t#inputMethod\n\t#previewURL\n\t#files = []\n\t#initialized = false\n\n\tconstructor () {\n\t\tsuper();\n\n\t\tthis.attachShadow({ mode: \"open\" });\n\t\tthis.shadowRoot.innerHTML = `<style>@import \"${new URL(\"style.css\", import.meta.url)}\";</style>\n\t\t<input type=\"file\" accept=\"image/*\" />\n\t\t<div id=\"drop-zone\" part=\"dropzone\">\n\t\t\t<slot name=\"input\">\n\t\t\t\t<input id=\"url\" part=\"input location\"${ this.hasAttribute(\"autofocus\") ? ' autofocus' : \"\" } />\n\t\t\t</slot>\n\t\t\t<slot name=\"browse\">\n\t\t\t\t<button part=\"button browse-button\">Browse…</button>\n\t\t\t</slot>\n\t\t</div>\n\t\t<slot name=\"preview\">\n\t\t\t<img id=\"preview\" part=\"preview\" />\n\t\t</slot>`;\n\n\t\tthis.#el.input = this.shadowRoot.querySelector(\"input[part~=input]\");\n\t\tthis.#el.fileInput = this.shadowRoot.querySelector(\"input[type=file]\");\n\t\tthis.#el.preview = this.shadowRoot.querySelector(\"img#preview\");\n\t\tthis.#el.previewSlot = this.shadowRoot.querySelector(\"slot[name=preview]\");\n\t\tthis.#el.dropZone = this.shadowRoot.querySelector(\"div#drop-zone\");\n\t\tthis.#el.browseButton = this.shadowRoot.querySelector(\"button[part~=browse-button]\");\n\n\t\tthis.#internals = this.attachInternals?.();\n\n\t\tif (this.#internals) {\n\t\t\t// this.#internals.role = \"region\";\n\t\t}\n\n\t\tthis.attributeChangedCallback();\n\t}\n\n\tconnectedCallback () {\n\t\tif (this.#initialized) {\n\t\t\t// Prevent multiple initializations\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#el.browseButton.addEventListener(\"click\", () => {\n\t\t\tthis.#el.fileInput.click();\n\t\t});\n\n\t\tfor (event of \"drag dragstart dragend dragover dragenter dragleave drop\".split(\" \")) {\n\t\t\tthis.#el.dropZone.addEventListener(event, e => {\n\t\t\t\te.preventDefault();\n\t\t\t\te.stopPropagation();\n\t\t\t});\n\t\t}\n\n\t\tfor (event of \"dragover dragenter\".split(\" \")) {\n\t\t\tthis.#el.dropZone.addEventListener(event, () => {\n\t\t\t\tthis.#el.dropZone.part.add(\"dragover\");\n\t\t\t});\n\t\t}\n\n\t\tfor (event of \"dragleave dragend drop\".split(\" \")) {\n\t\t\tthis.#el.dropZone.addEventListener(event, () => {\n\t\t\t\tthis.#el.dropZone.part.remove(\"dragover\");\n\t\t\t});\n\t\t}\n\n\t\tthis.#el.dropZone.addEventListener(\"drop\", e => {\n\t\t\tthis.#files = [...e.dataTransfer.files];\n\t\t\tthis.#inputMethod = \"drop\";\n\t\t\tthis.#internals?.setFormValue(this.#formValue);\n\t\t\tthis.#render();\n\t\t});\n\n\t\tthis.#el.fileInput.addEventListener(\"change\", e => {\n\t\t\tthis.#files = [...e.target.files];\n\t\t\tthis.#inputMethod = \"browse\";\n\t\t\tthis.#internals?.setFormValue(this.#formValue);\n\t\t\tthis.#render();\n\t\t});\n\n\t\tthis.addEventListener(\"paste\", e => {\n\t\t\tlet files = [...e.clipboardData.items].filter(item => item.kind === \"file\" && /^image\\//.test(item.type));\n\n\t\t\tif (files.length > 0) {\n\t\t\t\t// Images were pasted\n\t\t\t\tthis.#files = files.map(item => item.getAsFile());\n\t\t\t\tconsole.log(this.#files)\n\t\t\t\tthis.#inputMethod = \"paste\";\n\t\t\t\tthis.#internals?.setFormValue(this.#formValue);\n\t\t\t\tthis.#render();\n\t\t\t}\n\t\t});\n\n\t\tthis.#el.input.addEventListener(\"input\", e => {\n\t\t\tlet inputMethod = this.#inputMethod;\n\n\t\t\tif (this.#inputMethod && this.#inputMethod !== \"url\") {\n\t\t\t\tif (/^https?:\\/\\//.test(this.#el.input.value)) {\n\t\t\t\t\t// Back to URL mode, discard the files we have\n\t\t\t\t\tthis.#inputMethod = \"url\";\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (inputMethod !== this.#inputMethod) {\n\t\t\t\t// Input method changed, re-render\n\t\t\t\tthis.#render();\n\t\t\t}\n\n\t\t\tthis.#internals?.setFormValue(this.#formValue);\n\t\t});\n\n\n\n\t\tthis.#initialized = true;\n\t\tthis.#render();\n\t}\n\n\t#render () {\n\t\tif (!this.#inputMethod || this.#inputMethod === \"url\") {\n\t\t\tif (this.#inputMethod === \"url\") {\n\t\t\t\tthis.#previewURL = this.#el.input.value;\n\t\t\t}\n\n\t\t\tObject.assign(this.#el.input, {\n\t\t\t\ttype: \"url\",\n\t\t\t\tariaLabel: \"URL\",\n\t\t\t\tplaceholder: \"https://\"\n\t\t\t});\n\t\t}\n\t\telse {\n\t\t\tthis.#previewURL = URL.createObjectURL(this.files[0]);\n\t\t\tthis.#el.input.value = this.files[0].name;\n\n\t\t\tObject.assign(this.#el.input, {\n\t\t\t\ttype: \"\",\n\t\t\t\tariaLabel: \"Filename\",\n\t\t\t\tplaceholder: \"\"\n\t\t\t});\n\n\t\t\trequestAnimationFrame(() => {\n\t\t\t\t// TODO select only the filename (without the extension)\n\t\t\t\tthis.#el.input.select();\n\t\t\t});\n\t\t}\n\n\t\tthis.#renderPreview();\n\t}\n\n\t#renderPreview () {\n\t\tif (this.preview !== \"none\" && this.#previewURL) {\n\t\t\tthis.#el.preview.src = this.#previewURL;\n\t\t}\n\t}\n\n\tget name () {\n\t\treturn this.getAttribute(\"name\");\n\t}\n\n\tset name (value) {\n\t\tthis.setAttribute(\"name\", value);\n\t}\n\n\tget inputMethod () {\n\t\treturn this.#inputMethod;\n\t}\n\n\tget preview () {\n\t\treturn this.getAttribute(\"preview\") || \"auto\";\n\t}\n\n\tset preview (value) {\n\t\tthis.setAttribute(\"preview\", value);\n\t}\n\n\t// get multiple () {\n\t// \treturn this.hasAttribute(\"multiple\");\n\t// }\n\n\t// set multiple (value) {\n\t// \tif (value) {\n\t// \t\tthis.setAttribute(\"multiple\", \"\");\n\t// \t}\n\t// \telse {\n\t// \t\tthis.removeAttribute(\"multiple\");\n\t// \t}\n\t// }\n\n\tget #formValue () {\n\t\tlet fd = new FormData();\n\t\tfd.set(\"file\", this.files[0]);\n\t\tfd.set(\"url\", this.#inputMethod === \"url\"? this.#el.value : null);\n\t\treturn fd;\n\t}\n\n\tget value () {\n\t\treturn this.#el.value;\n\t}\n\n\tset value (value) {\n\t\tif (!value) {\n\t\t\tthis.#el.value = value;\n\t\t\tthis.#internals?.setFormValue(this.#formValue);\n\t\t\tthis.files = [];\n\t\t}\n\t\telse {\n\t\t\t// This is how the native file input works\n\t\t\t// See https://html.spec.whatwg.org/multipage/input.html#dom-input-value-filename\n\t\t\tthrow new DOMException(\"InvalidStateError\");\n\t\t}\n\t}\n\n\tget files () {\n\t\tif (this.#inputMethod === \"url\") {\n\t\t\treturn [];\n\t\t}\n\t\telse {\n\t\t\tlet files = this.#files.slice(0, 1); // we don't do multiple files yet\n\n\t\t\tif (this.#el.input.value !== this.#files[0].name) {\n\t\t\t\t// Filename edited\n\t\t\t\tfiles[0] = new File([files[0]], this.#el.input.value, files[0]);\n\t\t\t}\n\n\t\t\treturn files;\n\t\t}\n\t}\n\n\tget labels() {\n\t\treturn this.#internals?.labels;\n\t}\n\n\tfocus() {\n\t\tthis.#el.input.focus();\n\t}\n\n\tstatic get observedAttributes() {\n\t\treturn [\"preview\"];\n\t}\n\n\tattributeChangedCallback(name, oldValue) {\n\t\tlet value = this.getAttribute(name);\n\n\t\tif (oldValue === value) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!name || name === \"preview\") {\n\t\t\tthis.#el.preview.style.display = value === \"none\" ? \"none\" : \"\";\n\n\t\t\tif (value && ![\"auto\", \"none\"].includes(value)) {\n\t\t\t\t// Value is a CSS selector\n\t\t\t\tthis.#el.preview = this.ownerDocument.querySelector(value);\n\t\t\t}\n\t\t\telse {\n\t\t\t\tthis.#el.preview = this.#el.previewSlot.assignedElements()[0] // Is an element slotted?\n\t\t\t\t                   || this.shadowRoot.querySelector(\"img[part~=preview]\"); // get default element\n\t\t\t}\n\n\t\t\tthis.#renderPreview();\n\t\t}\n\t}\n\n\tstatic get formAssociated() {\n\t\treturn true;\n\t}\n}\n\ncustomElements.define(\"img-input\", ImageInput);\n"
  },
  {
    "path": "elements/img-input/style.css",
    "content": "#drop-zone {\n\tdisplay: grid;\n\tgap: .3em;\n\tgrid-template: \"url browse\" auto\n\t                \"preview preview\" auto / 1fr auto;\n\n\t&[part~=\"dragover\"] {\n\t\toutline: 2px dashed hsl(0 0% 10% / .5);\n\t\toutline-offset: 4px;\n\t}\n}\n\n#url {\n\tgrid-area: url;\n}\n\ninput[type=file] {\n\tdisplay: none;\n}\n\n#preview {\n\tgrid-area: preview;\n\tmax-width: 100%;\n}\n\n:host([nopreview]) #preview {\n\tdisplay: none;\n}"
  },
  {
    "path": "elements/img-input/test.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"UTF-8\">\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\t<title>Document</title>\n\t<script src=\"img-input.js\" type=\"module\"></script>\n</head>\n<body>\n\t<form id=\"form\">\n\t\t<button>Go</button>\n\t\t<input type=\"file\" name=\"native\">\n\t\t<img-input name=\"imginput\"></img-input>\n\t</form>\n\t<script>\n\t\tform.onsubmit = e => {\n\t\t\tlet o = Object.fromEntries(new FormData(form).entries());\n\t\t\tconsole.log(o);\n\t\t\te.preventDefault();\n\t\t};\n\t</script>\n</body>\n</html>"
  },
  {
    "path": "elements/index.css",
    "content": "/* Import all CSS-only modules */\n\n@import url(\"./nd-switch/nd-switch.css\");"
  },
  {
    "path": "elements/index.js",
    "content": "export { default as ButtonGroup } from \"./button-group/button-group.js\";\nexport { default as CycleToggle } from \"./cycle-toggle/cycle-toggle.js\";\nexport { default as MeterDiscrete } from \"./meter-discrete/meter-discrete.js\";\nexport { default as NudeRating } from \"./nd-rating/nd-rating.js\";\nexport { default as HTMLDemoElement } from \"./html-demo/html-demo.js\";\n\n// CSS-only modules\ndocument.head.insertAdjacentHTML(\"beforeend\", `<link rel=\"stylesheet\" href=\"${new URL(`index.css`, import.meta.url)}\" />`);"
  },
  {
    "path": "elements/meter-discrete/README.md",
    "content": "---\nid: meter-discrete\n---\n\n<header>\n\n# `<meter-discrete>`\n\nLike `<meter>`, but discrete. Useful to display ratings etc.\n\n</header>\n\n\n\n## Features\n\n- Scales with font size\n- Use emoji or custom icons\n- Styleable bar and inactive part\n- Uses [`ElementInternals`](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) for accessibiity\n- Ultra light (3KB **unminified** and **uncompressed**!)\n\n## Examples\n\nNo attributes\n\n```html\n<meter-discrete></meter-discrete>\n```\n\nWithout specifying icon\n\n```html\n<meter-discrete max=\"5\" value=\"2.5\"></meter-discrete>\n```\n\nWith custom icon, and a max of 10\n\n```html\n<meter-discrete max=\"10\" value=\"6.6\" icon=\"❤️\"></meter-discrete>\n```\n\nWith step\n\n```html\n<meter-discrete max=\"10\" value=\"6.6\" step=\"0.5\" icon=\"❤️\"></meter-discrete>\n```\n\nDynamic value\n\n```html\n<meter-discrete max=\"5\" value=\"3.5\" icon=\"💩\"></meter-discrete>\n<button onclick=\"this.previousElementSibling.value = Math.random() * 5\">Random value</button>\n```\n\nDifferent styles\n\n\n```html\n<style>\n#minimal_rating {\n\tfont-size: 200%;\n}\n\n#minimal_rating::part(value),\n#minimal_rating::part(inactive) {\n\tfilter: contrast(0%) sepia() hue-rotate(140deg);\n}\n\n#minimal_rating::part(inactive) {\n\topacity: .5;\n}\n</style>\n<meter-discrete id=\"minimal_rating\" max=\"5\" value=\"2.5\" icon=\"💜\"></meter-discrete>\n```\n\nActual image instead of emoji:\n\n\n```html\n<meter-discrete value=\"3.5\" icon=\"/logo.svg\"></meter-discrete>\n```\n\n## See also\n\n* [`<nd-rating>`](../nd-rating), an editable version of `<meter-discrete>`\n\n"
  },
  {
    "path": "elements/meter-discrete/meter-discrete.js",
    "content": "export const internals = Symbol(\"internals\");\n\nexport default class MeterDiscrete extends HTMLElement {\n\t#internals\n\n\tconstructor() {\n\t\tsuper();\n\n\t\tthis.attachShadow({ mode: \"open\" });\n\t\tthis.shadowRoot.innerHTML = `\n\t\t<style>@import \"${new URL(\"style.css\", import.meta.url)}\";</style>\n\t\t<div id=value part=value></div><div id=inactive part=\"inactive\"></div>`;\n\n\t\tthis[internals] = this.attachInternals?.() ?? {};\n\t\tthis[internals].role = \"meter\";\n\t\tthis[internals].ariaValueMin = this.min;\n\t}\n\n\tget icon () {\n\t\treturn this.getAttribute(\"icon\") ?? \"⭐️\";\n\t}\n\n\t// So it can be handled like a <meter>\n\tget min () {\n\t\treturn 0;\n\t}\n\n\tget max () {\n\t\treturn +this.getAttribute(\"max\") || 5;\n\t}\n\n\tset max (max) {\n\t\tif (max) {\n\t\t\tthis.setAttribute(\"max\", max);\n\t\t}\n\t}\n\n\tget value () {\n\t\tlet value = this.getAttribute(\"value\");\n\n\t\tif (value === null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tvalue = +value;\n\n\t\tlet step = this.step;\n\n\t\tif (step !== null) {\n\t\t\t// Quantize by step\n\t\t\tvalue = quantize(value, step);\n\t\t}\n\n\t\treturn value;\n\t}\n\n\tset value (value) {\n\t\tthis.setAttribute(\"value\", value);\n\t}\n\n\tget step () {\n\t\treturn this.hasAttribute(\"step\") ? +this.getAttribute(\"step\") : null;\n\t}\n\n\tget #iconURL () {\n\t\tlet isURL = this.icon.includes(\".\");\n\n\t\treturn isURL? this.icon : emojiToImage(this.icon);\n\t}\n\n\tstatic get observedAttributes() {\n\t\treturn [\"value\", \"max\", \"icon\"];\n\t}\n\n\tattributeChangedCallback(name, oldValue, newValue) {\n\t\tif (!name || name === \"max\") {\n\t\t\tlet max = this.max;\n\t\t\tthis.style.setProperty(\"aspect-ratio\", `${max} / 1`);\n\t\t\tthis.style.setProperty(\"--max\", max);\n\t\t\tthis[internals].ariaValueMax = max;\n\t\t}\n\n\t\tif (!name || name === \"value\") {\n\t\t\tlet value = this.value;\n\t\t\tthis.style.setProperty(\"--value\", value);\n\t\t\tthis[internals].ariaValueNow = value;\n\t\t}\n\n\t\tif (!name || name === \"icon\") {\n\t\t\tthis.style.setProperty(\"--icon-image\", `url('${ this.#iconURL }')`);\n\t\t}\n\t}\n\n\tconnectedCallback() {\n\t\tthis.attributeChangedCallback();\n\t}\n}\n\nfunction emojiToImage(emoji) {\n\t// For debug: <rect stroke=\"black\" fill=\"none\" stroke-width=\"2\" width=\"100%\" height=\"100%\" />\n\treturn `data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\">`\n\t+ `<text style=\"font-size: 80px\" x=\"50%\" y=\".85em\" dominant-baseline=\"middle\" text-anchor=\"middle\">${emoji}</text></svg>`\n}\n\nfunction quantize (value, step) {\n\treturn Math.round(value / step) * step;\n}\n\n\ncustomElements.define(\"meter-discrete\", MeterDiscrete);"
  },
  {
    "path": "elements/meter-discrete/style.css",
    "content": ":host {\n\tdisplay: inline-flex;\n\tvertical-align: -.25em;\n\theight: 1.2em;\n\tuser-select: none;\n}\n\n#value, #inactive {\n\tbackground: var(--icon-image) 0 / auto 100%;\n}\n\n#value {\n\twidth: calc(var(--value) / var(--max) * 100%);\n}\n\n#inactive {\n\topacity: .5;\n\tfilter: saturate(50%) contrast(.5);\n\tflex: 1;\n\tbackground-position-x: right;\n}"
  },
  {
    "path": "elements/nd-calendar/README.md",
    "content": "---\nid: nd-calendar\n---\n\n<header>\n\n# `<nd-calendar>`\n\nDisplay dates, date ranges, or date/time ranges by day or hour.\n\n</header>\n\n\n\n## Features\n\n- Weekly rows or entire months\n- TODO: Custom colors per date\n\n## Examples\n\nNo attributes\n\n```html\n<nd-calendar>\n\t<time datetime=\"2022-09-05T00:00\"></time> <!-- Times are ignored -->\n\t<time datetime=\"2022-09-07 / 2022-09-10\"></time> <!-- Range -->\n\t<time datetime=\"2022-09-13\"></time>\n</nd-calendar>\n```\n\nCustom max\n```html\n<nd-calendar max=\"2022-09-15\">\n\t<time datetime=\"2022-09-05\"></time>\n\t<time datetime=\"2022-09-07\"></time>\n\t<time datetime=\"2022-09-11\"></time>\n</nd-calendar>\n```\n\nCustom min\n```html\n<nd-calendar min=\"2022-09-01\">\n\t<time datetime=\"2022-09-05\"></time>\n\t<time datetime=\"2022-09-07\"></time>\n\t<time datetime=\"2022-09-11\"></time>\n</nd-calendar>\n```\n\nCustom min and max\n```html\n<nd-calendar min=\"2022-08-01\" max=\"2022-09-30\">\n\t<time datetime=\"2022-09-05\"></time>\n\t<time datetime=\"2022-09-07\"></time>\n\t<time datetime=\"2022-09-11\"></time>\n</nd-calendar>\n```\n\nBy months:\n\n```html\n<nd-calendar rows=\"months\">\n\t<time datetime=\"2022-05-02\"></time>\n\t<time datetime=\"2022-05-12\"></time>\n\t<time datetime=\"2022-06-13T15:00\"></time> <!-- Times are ignored -->\n\t<time datetime=\"2022-07-12\"></time>\n\t<time datetime=\"2022-08-22\"></time>\n\t<time datetime=\"2022-09-05\"></time>\n\t<time datetime=\"2022-09-07\"></time>\n\t<time datetime=\"2022-09-11\"></time>\n</nd-calendar>\n```\n\n"
  },
  {
    "path": "elements/nd-calendar/nd-calendar.js",
    "content": "const DAYS_OF_WEEK = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"];\n\nconst dur = { ms: 1 };\ndur.sec = dur.ms * 1000;\ndur.min = dur.sec * 60;\ndur.hour = dur.min * 60;\ndur.day = dur.hour * 24;\ndur.week = dur.day * 7;\ndur.month = dur.day * 30.4368;\n\nexport default class NudeCalendar extends HTMLElement {\n\t#headers\n\t#calendar\n\t#observer;\n\n\tconstructor() {\n\t\tsuper();\n\n\t\tthis.attachShadow({ mode: \"open\" });\n\t\tthis.shadowRoot.innerHTML = `\n\t\t<style>@import \"${new URL(\"style.css\", import.meta.url)}\";</style>\n\t\t<div id=\"headers\"></div>\n\t\t<div id=\"calendar\"></div>\n\t\t`;\n\t\tthis.#headers = this.shadowRoot.getElementById(\"headers\");\n\t\tthis.#calendar = this.shadowRoot.getElementById(\"calendar\");\n\t\tthis.#observe();\n\t}\n\n\t#createHeaders () {\n\t\tif (this.getAttribute(\"rows\") === \"months\") {\n\t\t\tlet days = Array(31).fill(1).map((a, i) => i + 1);\n\t\t\tthis.#headers.innerHTML = days.map((d, i) => `<div style=\"--day: ${i + 1}\">${d}</div>`).join(\"\\n\");\n\t\t}\n\t\telse {\n\t\t\tthis.#headers.innerHTML = DAYS_OF_WEEK.map((d, i) => `<div style=\"--weekday: ${i + 1}\">${d}</div>`).join(\"\\n\");\n\t\t}\n\n\t}\n\n\t#observe () {\n\t\tthis.#observer ??= new MutationObserver(() => this.#render());\n\t\tthis.#observer.observe(this, { childList: true, subtree: true, attributeFilter: [\"datetime\"] });\n\t}\n\n\t#unobserve () {\n\t\tthis.#observer.takeRecords();\n\t\tthis.#observer.disconnect();\n\t}\n\n\t#render() {\n\t\tthis.#unobserve();\n\n\t\tlet dates = [...this.children].flatMap(time => {\n\t\t\tlet dt = time.getAttribute(\"datetime\");\n\n\t\t\tif (dt.includes(\"/\")) {\n\t\t\t\t// Date range\n\t\t\t\t// add ALL dates between these\n\t\t\t\tlet [low, high] = dt.split(\"/\").map(d => new BetterDate(d.trim()));\n\n\t\t\t\t// Return all dates between low and high\n\t\t\t\tlet dates = [];\n\t\t\t\tlet daysApart = (high - low) / dur.day;\n\n\t\t\t\tif (isNaN(high) || isNaN(low)) {\n\t\t\t\t\treturn [];\n\t\t\t\t}\n\n\t\t\t\tfor (let d = new BetterDate(low), i = 0; d <= new BetterDate(Number(high) + dur.day); d.setDate(d.getDate() + 1)) {\n\t\t\t\t\ti++;\n\t\t\t\t\tdates.push(d.isoDate);\n\t\t\t\t\tif (i > daysApart) break; // failsafe\n\t\t\t\t}\n\n\t\t\t\treturn dates.map(d => new BetterDate(d));\n\t\t\t}\n\n\t\t\treturn new BetterDate(dt);\n\t\t}).filter(d => !isNaN(d)).sort((a, b) => a - b);\n\n\t\tif (dates.length === 0) {\n\t\t\tthis.#observe();\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#calendar.innerHTML = \"\";\n\n\t\tlet hasMin = this.hasAttribute(\"min\");\n\t\tlet hasMax = this.hasAttribute(\"max\");\n\t\tthis.min = hasMin && new BetterDate(this.getAttribute(\"min\")) || dates[0];\n\t\tthis.max = hasMax && new BetterDate(this.getAttribute(\"max\")) || dates.at(-1);\n\n\t\tif (!hasMax) {\n\t\t\tif (this.getAttribute(\"rows\") === \"months\") {\n\t\t\t\tthis.max = new BetterDate(this.max);\n\t\t\t\tthis.max.setMonth(this.max.getMonth() + 1);\n\t\t\t\tthis.max.setDate(0);\n\t\t\t}\n\t\t\telse {\n\t\t\t\tlet now = new BetterDate();\n\t\t\t\tif (now - this.max < dur.month) {\n\t\t\t\t\t// If max is recent use today as the default max\n\t\t\t\t\tthis.max = now;\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\n\t\tif (!hasMin) {\n\t\t\t// If no mix and max is specified, extend min and max in some cases for better presentation\n\t\t\tif (this.getAttribute(\"rows\") === \"months\") {\n\t\t\t\t// Grow ranges to be full months\n\t\t\t\tthis.min = new BetterDate(this.min);\n\t\t\t\tthis.min.setDate(1);\n\n\t\t\t}\n\t\t\telse {\n\t\t\t\tthis.min = new BetterDate(this.max - dur.month);\n\t\t\t}\n\t\t}\n\n\t\tthis.dates = new Set(dates.map(d => d.isoDate));\n\n\t\tlet previousMonth;\n\t\tlet daysApart = (this.max - this.min) / dur.day;\n\t\tfor (let date = this.min, i = 0; !(date > this.max); date.setDate(date.getDate() + 1)) {\n\t\t\tif (isNaN(date)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst dayElement = document.createElement(\"time\");\n\t\t\tdayElement.part = \"day\" + (this.dates.has(date.isoDate) ? \" active\" : \"\");\n\t\t\tdayElement.setAttribute(\"datetime\", date.isoDate);\n\t\t\tdayElement.title = date.toLocaleString(\"en-US\", { dateStyle: \"long\" });\n\t\t\tdayElement.style.setProperty(\"--weekday\", date.weekday);\n\n\t\t\tlet year = date.getComponent(\"year\");\n\t\t\tlet month = date.getComponent(\"month\", \"short\");\n\t\t\tlet day = date.getComponent(\"day\");\n\t\t\tdayElement.style.setProperty(\"--year\", `\"${year}\"`);\n\t\t\tdayElement.style.setProperty(\"--month\", `\"${month}\"`);\n\t\t\tdayElement.style.setProperty(\"--day\", day);\n\n\t\t\tthis.#calendar.appendChild(dayElement);\n\n\t\t\tif (previousMonth !== month) {\n\t\t\t\tdayElement.insertAdjacentHTML(\"beforebegin\", `<div part=\"month\" style=\"--month: \"${month}\";\">${month}</div>`);\n\t\t\t}\n\n\t\t\tif (i > daysApart) break; // failsafe\n\n\t\t\tpreviousMonth = month;\n\t\t\ti++;\n\t\t}\n\n\t\tthis.#observe();\n\t}\n\n\tstatic observedAttributes = [\"rows\"]\n\n\tattributeChangedCallback(name, oldValue, newValue) {\n\t\tif (!name || name === \"rows\") {\n\t\t\tthis.#createHeaders();\n\t\t}\n\t}\n\n\tconnectedCallback() {\n\t\tthis.attributeChangedCallback();\n\t\tthis.#render();\n\t}\n}\n\nclass BetterDate extends Date {\n\tconstructor(...args) {\n\t\tsuper(...args);\n\n\t\tthis.hasTimezone = typeof args[0] === \"string\" && /\\+|Ζ/.test(args[0]) || args[0]?.hasTimezone;\n\n\t\tif (!this.hasTimezone) {\n\t\t\t// Use UTC time if no timezone provided\n\t\t\tthis.setMinutes(this.getMinutes() + this.getTimezoneOffset());\n\t\t}\n\t}\n\n\tget weekday() {\n\t\treturn this.getDay() || 7;\n\t}\n\n\tget isoDate () {\n\t\ttry {\n\t\t\treturn this.toISOString().split(\"T\")[0];\n\t\t}\n\t\tcatch (e) {\n\t\t\treturn \"\";\n\t\t}\n\t}\n\n\tgetComponent(component, format = \"numeric\") {\n\t\treturn this.toLocaleString(\"en-US\", { timeZone: \"UTC\", [component]: format });\n\t}\n}\n\ncustomElements.define(\"nd-calendar\", NudeCalendar);\n"
  },
  {
    "path": "elements/nd-calendar/style.css",
    "content": ":host {\n\t--_active-day-background: var(--active-day-background, var(--accent-color, hsl(220 60% 50%)));\n\t--_inactive-day-background: var(--inactive-day-background, hsl(220 10% 70%));\n\n\tdisplay: grid;\n\tgrid-template-columns: repeat(7, 1fr);\n\tgrid-gap: .2em;\n}\n\n:host([rows=months]) {\n\tgrid-template-columns: auto repeat(31, 1fr)\n}\n\n:host([rows=months]) #headers {\n\t\tfont-weight: normal\n\t}\n\n:host([rows=months]) #headers :first-child {\n\t\t\tgrid-column: 2;\n\t\t}\n\n#calendar {\n\tdisplay: contents;\n}\n\n#headers {\n\tdisplay: contents;\n\tcolor: hsl(var(--gray), 50%);\n\tfont-weight: bold;\n\ttext-align: center;\n}\n\n[part~=\"month\"] {\n\tgrid-column: 1;\n\tfont-weight: bold;\n\ttext-transform: uppercase;\n\tcolor: var(--_inactive-day-background);\n\tfilter: brightness(80%);\n\talign-self: center;\n\tfont-size: 75%;\n}\n\n[part~=\"day\"] {\n\tposition: relative;\n\tborder-radius: .2em;\n\tbackground: var(--_inactive-day-background);\n\tcolor: white;\n\tfont-weight: bold;\n\ttext-decoration: none;\n\toverflow: hidden;\n\tpadding: .2em 0;\n\ttext-align: center;\n\tcontainer-type: inline-size\n}\n\n[part~=\"day\"][part~=\"active\"] {\n\t\tbackground: var(--_active-day-background);\n\t}\n\n[part~=\"day\"]::after {\n\t\tcounter-reset: day var(--day);\n\t\tcontent: counter(day);\n\t}\n\n@container (max-width: 4em) {\n\t\t[part~=\"day\"]::before {\n\t\t\tdisplay: block;\n\t\t\tfont-size: 70%;\n\t\t}\n\n\t\t[part~=\"day\"]::after {\n\t\t\tdisplay: block;\n\t\t}\n\t}\n\n:host(:not([rows=months])) [part~=\"day\"] {\n\t\tgrid-column: var(--weekday)\n}\n\n:host(:not([rows=months])) [part~=\"day\"]::before {\n\t\t\tcontent: var(--month) \" \";\n\t\t\tfont-weight: 300;\n\t\t}\n\n:host([rows=months]) [part~=\"day\"] {\n\t\tfont-size: 90%;\n\t\tletter-spacing: -.03em;\n\t\tgrid-column: calc(var(--day) + 1)\n}\n\n:host(:not([rows=months])) [part~=\"month\"] {\n\t\tdisplay: none;\n\t}\n\n\n\n\n\n"
  },
  {
    "path": "elements/nd-calendar/style.postcss",
    "content": ":host {\n\t--_active-day-background: var(--active-day-background, var(--accent-color, hsl(220 60% 50%)));\n\t--_inactive-day-background: var(--inactive-day-background, hsl(220 10% 70%));\n\n\tdisplay: grid;\n\tgrid-template-columns: repeat(7, 1fr);\n\tgrid-gap: .2em;\n}\n\n:host([rows=months]) {\n\tgrid-template-columns: auto repeat(31, 1fr);\n\n\t& #headers {\n\t\tfont-weight: normal;\n\n\t\t& :first-child {\n\t\t\tgrid-column: 2;\n\t\t}\n\t}\n}\n\n#calendar {\n\tdisplay: contents;\n}\n\n#headers {\n\tdisplay: contents;\n\tcolor: hsl(var(--gray), 50%);\n\tfont-weight: bold;\n\ttext-align: center;\n}\n\n[part~=\"month\"] {\n\tgrid-column: 1;\n\tfont-weight: bold;\n\ttext-transform: uppercase;\n\tcolor: var(--_inactive-day-background);\n\tfilter: brightness(80%);\n\talign-self: center;\n\tfont-size: 75%;\n}\n\n[part~=\"day\"] {\n\tposition: relative;\n\tborder-radius: .2em;\n\tbackground: var(--_inactive-day-background);\n\tcolor: white;\n\tfont-weight: bold;\n\ttext-decoration: none;\n\toverflow: hidden;\n\tpadding: .2em 0;\n\ttext-align: center;\n\tcontainer-type: inline-size;\n\n\t&[part~=\"active\"] {\n\t\tbackground: var(--_active-day-background);\n\t}\n\n\t&::after {\n\t\tcounter-reset: day var(--day);\n\t\tcontent: counter(day);\n\t}\n\n\t@container (max-width: 4em) {\n\t\t&::before {\n\t\t\tdisplay: block;\n\t\t\tfont-size: 70%;\n\t\t}\n\n\t\t&::after {\n\t\t\tdisplay: block;\n\t\t}\n\t}\n\n\t@nest :host(:not([rows=months])) & {\n\t\tgrid-column: var(--weekday);\n\n\t\t&::before {\n\t\t\tcontent: var(--month) \" \";\n\t\t\tfont-weight: 300;\n\t\t}\n\t}\n\n\t@nest :host([rows=months]) & {\n\t\tfont-size: 90%;\n\t\tletter-spacing: -.03em;\n\t\tgrid-column: calc(var(--day) + 1);\n\t}\n}\n\n:host(:not([rows=months])) {\n\t& [part~=\"month\"] {\n\t\tdisplay: none;\n\t}\n}\n\n\n\n\n\n"
  },
  {
    "path": "elements/nd-rating/README.md",
    "content": "---\nid: nd-rating\n---\n\n<header>\n\n# `<nd-rating>`\n\nLike [`<meter-discrete>`](../meter-discrete/), but editable. Useful to display and set ratings etc.\n\n</header>\n\n\n\n## Features\n\n- All features of [`<meter-discrete>`](../meter-discrete/), plus:\n- Uses [`ElementInternals`](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) to work like a built-in form element\n- Keyboard accessible (use arrow keys)\n- Ultra light (3KB **unminified** and **uncompressed** + another 3KB for `<meter-discrete>`)\n\n## Examples\n\nBasic\n\n```html\n<nd-rating max=\"5\" value=\"3.5\"></nd-rating>\n<button onclick=\"this.previousElementSibling.readonly = !this.previousElementSibling.readonly\">Toggle readonly</button>\n```\n\nWith step\n\n```html\n<nd-rating max=\"5\" value=\"3.5\" step=\"0.1\" style=\"font-size: 200%\"></nd-rating>\n```\n\nDifferent styles\n\n\n```html\n<style>\n#minimal_rating {\n\tfont-size: 200%;\n}\n\n#minimal_rating::part(value),\n#minimal_rating::part(inactive) {\n\tfilter: contrast(0%) sepia() hue-rotate(140deg);\n}\n\n#minimal_rating::part(inactive) {\n\topacity: .5;\n}\n</style>\n<nd-rating id=\"minimal_rating\" max=\"5\" value=\"2.5\" step=\"0.5\" icon=\"💜\"></nd-rating>\n```\n\nActual image instead of emoji:\n\n\n```html\n<nd-rating value=\"3.5\" icon=\"../logo.svg\"></nd-rating>\n```\n\nParticipates in form submission (requires [`ElementInternals`](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals) support):\n\n```html\n<form action=\"about:blank\" target=\"_blank\">\n\t<nd-rating name=\"myrating\"></nd-rating>\n\t<button type=submit>Submit</button>\n</form>\n```\n\n"
  },
  {
    "path": "elements/nd-rating/nd-rating.js",
    "content": "import MeterDiscrete, {internals} from \"../meter-discrete/meter-discrete.js\";\n\nexport default class NudeRating extends MeterDiscrete {\n\tconstructor () {\n\t\tsuper();\n\n\t\tif (!this.hasAttribute(\"tabindex\")) {\n\t\t\tthis.tabIndex = 0;\n\t\t}\n\t}\n\n\tget readonly () {\n\t\treturn this.hasAttribute(\"readonly\");\n\t}\n\n\tget value () {\n\t\treturn super.value;\n\t}\n\n\tset value (value) {\n\t\tlet oldValue = super.value;\n\t\tsuper.value = value;\n\n\t\tthis[internals].setFormValue(value);\n\t}\n\n\tset readonly (readonly) {\n\t\tif (readonly) {\n\t\t\tthis.setAttribute(\"readonly\", \"\");\n\t\t}\n\t\telse {\n\t\t\tthis.removeAttribute(\"readonly\");\n\t\t}\n\t}\n\n\tstatic get observedAttributes() {\n\t\treturn [...super.observedAttributes, \"readonly\"];\n\t}\n\n\tattributeChangedCallback(name, oldValue, newValue) {\n\t\tsuper.attributeChangedCallback(name, oldValue, newValue);\n\n\t\tif (name === \"readonly\" || (!name && !this.readonly)) {\n\t\t\tif (name === \"readonly\" && newValue !== null) {\n\t\t\t\tthis.#endEditing();\n\t\t\t}\n\t\t\telse {\n\t\t\t\tthis.#startEditing();\n\t\t\t}\n\t\t}\n\t}\n\n\t#startEditing () {\n\t\tthis.addEventListener(\"mouseenter\", this.edit);\n\t\tthis.addEventListener(\"focus\", this.edit);\n\t}\n\n\t#endEditing () {\n\t\tthis.removeEventListener(\"mouseenter\", this.edit);\n\t\tthis.removeEventListener(\"focus\", this.edit);\n\t}\n\n\tedit () {\n\t\t// Code adapted from Mavo: https://github.com/mavoweb/mavo/blob/master/src/elements.js#L378\n\t\tlet {min = 0, max, step} = this;\n\t\tlet range = max - min;\n\n\t\tstep = step ?? (range > 1? 1 : range/100);\n\n\t\tlet value = this.value;\n\n\t\tlet handlers = {\n\t\t\tmousemove: evt => {\n\t\t\t\t// Change property as mouse moves\n\t\t\t\tlet {left, width} = this.getBoundingClientRect();\n\t\t\t\tlet offset = evt.offsetX / width;\n\t\t\t\tlet newValue = quantize(min + range * offset, step);\n\n\t\t\t\tthis.value = newValue;\n\t\t\t},\n\n\t\t\tmouseleave: evt => {\n\t\t\t\t// Return to actual value\n\t\t\t\tthis.value = value;\n\n\t\t\t\tfor (let event in handlers) {\n\t\t\t\t\tthis.removeEventListener(event, handlers[event]);\n\t\t\t\t}\n\t\t\t},\n\n\t\t\tclick: evt => {\n\t\t\t\t// Register change\n\t\t\t\tvalue = this.value;\n\n\t\t\t\tthis.dispatchEvent(new InputEvent(\"input\", { bubbles: true }));\n\t\t\t},\n\n\t\t\tkeydown: evt => {\n\t\t\t\t// Edit with arrow keys\n\t\t\t\tif ([\"ArrowLeft\", \"ArrowRight\"].includes(evt.key)) {\n\t\t\t\t\tlet increment = step * (evt.key === \"ArrowRight\"? 1 : -1) * (evt.shiftKey? 10 : 1);\n\t\t\t\t\tlet newValue = this.value + increment;\n\t\t\t\t\tnewValue = Math.max(min, Math.min(newValue, max));\n\n\t\t\t\t\tthis.value = newValue;\n\n\t\t\t\t\tthis.dispatchEvent(new InputEvent(\"input\", { bubbles: true }));\n\n\t\t\t\t\tevt.preventDefault();\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\thandlers.blur = handlers.mouseleave;\n\n\t\tfor (let event in handlers) {\n\t\t\tthis.addEventListener(event, handlers[event]);\n\t\t}\n\t}\n\n\tget labels() {\n\t\treturn this[internals]?.labels;\n\t}\n\n\tstatic formAssociated = true;\n}\n\nfunction quantize (value, step) {\n\treturn Math.round(value / step) * step;\n}\n\ncustomElements.define(\"nd-rating\", NudeRating);"
  },
  {
    "path": "elements/nd-slider/README.md",
    "content": "---\nid: nd-slider\n---\n<script type=\"module\" src=\"https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.15.0/cdn/components/format-number/format-number.js\"></script>\n<header>\n\n# `<nd-slider>`\n\nSeveral improvements over the native `<input type=range>`.\n\n</header>\n\n\n\n## Features\n\n- Editable spinner that shows the value or progress of the slider, displayed as a tooltip or inline\n- Convenience methods\n\n## Examples\n\n### Basic\n\nSimplest version:\n\n```html\n<nd-slider></nd-slider>\n```\n\nYou can also *provide* a specific slider element yourself so it can be easier to style or customize:\n\n```html\n<nd-slider>\n\t<input type=range>\n</nd-slider>\n```\n\nYou can also provide a specific element for the value:\n\n```html\n<nd-slider>\n\t<sl-format-number slot=\"value\" type=\"currency\" currency=\"USD\"></sl-format-number>\n</nd-slider>\n```\n\nIf it has a `value` property it will be assumed to be editable, otherwise it will be read-only.\n\n---\n\nAll usual slider attributes work and are simply copied to the slider and spinner elements:\n\n```html\n<nd-slider min=\"-180\" max=\"180\" step=\"0.01\"></nd-slider>\n```\n\nYou are encouraged to provide a slider with the right attributes from the start, to minimize updates.\n\n---\n\nBy default the number is shown as a read/write tooltip, but you can make it display inline:\n\n```html\n<nd-slider style=\"--value-position: end\"></nd-slider>\n```\n\nYes, this is a regular CSS property that you can even set in your stylesheet.\nIt requires [style queries](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_size_and_style_queries#container_style_queries_2),\nwhich means [at the time of this writing, it won’t work in Firefox](https://caniuse.com/css-container-queries-style).\n\nBy default shown is the slider value, but you can switch to showing (and editing) the progress instead:\n\n```html\n<nd-slider show=\"progress\"></nd-slider>\n```\n\n## Properties and methods\n\nIn addition to the usual properties and methods of a slider (`min`, `max`, `step`, `value`, `defaultValue`), the following are available:\n- `show`: Whether to show the value or the progress. Possible values: `value`, `progress`. Default: `value`.\n- `valueElement`: The element that shows the value.\n- `sliderElement`: The element that is the slider.\n- `progress`: The progress of the slider, as a number between 0 and 1.\n- `progressAt(value)`: Returns the progress at a given point, as a number between 0 and 1.\n- `valueAt(progress)`: Returns the value at a given progress, as a number between `min` and `max`.\n\n"
  },
  {
    "path": "elements/nd-slider/nd-slider.css",
    "content": ":host {\n\tdisplay: flex;\n\tgap: .5em;\n\tposition: relative;\n}\n\n.nd-slider,\nslot:not([name])::slotted(*),\nslot:not([name]) > * {\n\twidth: 100%;\n}\n\n.slider-tooltip,\nslot[name=\"value\"] {\n\t--_slider-thumb-width: var(--slider-thumb-width, 16px);\n\t--_tooltip-background: var(--tooltip-background, color-mix(in oklab, canvas 80%, oklab(none none none / 0%)));\n\t--_tooltip-border-radius: var(--tooltip-border-radius, .3em);\n\t--_tooltip-pointer-height: var(--tooltip-pointer-height, .3em);\n\t--_tooltip-pointer-angle: var(--tooltip-pointer-angle, 90deg);\n\n\t@supports (field-sizing: content) {\n\t\t--field-sizing-width: auto;\n\t}\n\n\tposition: absolute;\n\tleft: clamp(-20%,\n\t\t\t100% * var(--progress)\n\t\t\t- (var(--progress) - 0.5) * var(--_slider-thumb-width) / 2 /* center on slider thumb */\n\t\t, 100%);\n\tbottom: calc(100% + 3px);\n\ttranslate: -50%;\n\ttransform-origin: bottom;\n\tdisplay: flex;\n\tpadding-block: .3em;\n\tpadding-inline: .4em;\n\tborder: var(--_tooltip-pointer-height) solid transparent;\n\tborder-radius: calc(var(--_tooltip-border-radius) + var(--_tooltip-pointer-height));\n\ttext-align: center;\n\tcolor: canvastext;\n\tbackground:\n\t\tconic-gradient(from calc(-1 * var(--_tooltip-pointer-angle) / 2) at bottom, var(--_tooltip-background) var(--_tooltip-pointer-angle), transparent 0)\n\t\t\tborder-box bottom / 100% var(--_tooltip-pointer-height) no-repeat,\n\t\tvar(--_tooltip-background) padding-box;\n\tcolor-scheme: dark;\n\ttransition:\n\t\tvisibility 0s 200ms,\n\t\topacity 200ms,\n\t\tscale 200ms,\n\t\twidth 100ms,\n\t\tleft 200ms cubic-bezier(.17,.67,.49,1.48);\n\n\t/* Prevent input from moving all over the place as we type */\n\t&:focus-within {\n\t\ttransition-delay: .5s;\n\t}\n\n\t&::after {\n\t\tcontent: var(--value-suffix);\n\t}\n\n\tinput,\n\t&::slotted(input) {\n\t\tall: unset;\n\t}\n\n\t> input[type=number],\n\t&:is(input[type=number]),\n\t&::slotted(input[type=number]) {\n\t\t--content-width: calc(var(--value-length) * 1ch);\n\t\tfield-sizing: content;\n\t\twidth: var(--field-sizing-width, calc(var(--content-width, 2ch) + 1.2em));\n\t\tmin-width: calc(2ch + 1.2em);\n\t\tbox-sizing: content-box;\n\n\t\t&::-webkit-textfield-decoration-container {\n\t\t\tgap: .2em;\n\t\t}\n\n\t\t@container style(--value-suffix) {\n\t\t\t&::-webkit-textfield-decoration-container {\n\t\t\t\tflex-flow: row-reverse;\n\t\t\t}\n\t\t}\n\n\t\t/* Don’t auto hide the spin buttons */\n\t\t&::-webkit-inner-spin-button {\n\t\t\topacity: 1;\n\t\t}\n\t}\n\n\t&:not(:is(:focus-within, :hover) > *, .color-slider:is(:focus, :hover) + *, :focus, :hover) {\n\t\t/* visibility: hidden;\n\t\topacity: 0;\n\t\tscale: .5; */\n\t}\n\n\t@container style(--value-position) {\n\t\tdisplay: contents;\n\t\tcolor-scheme: inherit;\n\n\t\t@container style(--value-position: start) {\n\t\t\torder: -1;\n\t\t}\n\n\t\tinput,\n\t\t&::slotted(input) {\n\t\t\tall: revert-layer;\n\t\t\tfont: inherit;\n\t\t}\n\t}\n}\n\n:host([show=\"progress\"]) :where(.slider-tooltip, slot[name=\"value\"]) {\n\t--value-suffix: \"%\";\n}"
  },
  {
    "path": "elements/nd-slider/nd-slider.js",
    "content": "let self = class NudeSlider extends HTMLElement {\n\tsliderElement = null;\n\tvalueElement = null;\n\t#slots = {};\n\n\tstatic tagName = \"nd-slider\";\n\tstatic observedAttributes = [\"min\", \"max\", \"step\", \"value\", \"show\"];\n\n\tconstructor () {\n\t\tsuper();\n\n\t\tthis.attachShadow({mode: \"open\"});\n\t\tlet styleURL = new URL(self.tagName + \".css\", import.meta.url);\n\t\tthis.shadowRoot.innerHTML = `\n\t\t\t<style>@import url(\"${ styleURL }\")</style>\n\t\t\t<slot>\n\t\t\t\t<input type=\"range\">\n\t\t\t</slot>\n\t\t\t<slot name=\"value\">\n\t\t\t\t<input type=\"number\">\n\t\t\t</slot>\n\t\t`;\n\n\t\tthis.#slots = Object.fromEntries([...this.shadowRoot.querySelectorAll(\"slot\")].map(slot => [slot.name || \"slider\", slot]));\n\n\t\tthis.#slots.slider.addEventListener(\"slotchange\", this);\n\t\tthis.#slots.value.addEventListener(\"slotchange\", this);\n\n\t\tthis.handleEvent({type: \"slotchange\"});\n\t}\n\n\thandleEvent (event) {\n\t\tif (event.type === \"slotchange\") {\n\t\t\t// What is the source of truth?\n\t\t\t// \"slider\" by default, but becomes \"value\" if no slider is slotted, but a spinner is.\n\t\t\tlet source = \"slider\";\n\n\t\t\tfor (let name in this.#slots) {\n\t\t\t\tlet slot = this.#slots[name];\n\t\t\t\tlet elementProp = name + \"Element\";\n\t\t\t\tlet oldElement = this[elementProp];\n\t\t\t\tlet nodes = slot.assignedNodes();\n\t\t\t\tlet elements = slot.assignedElements();\n\n\t\t\t\tif (elements[0] === oldElement) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (name === \"slider\" && elements.length === 0 && nodes.every(node => !node.nodeValue.trim())) {\n\t\t\t\t\t// Literally every node assigned to this slot is an empty text node. Likely formatting, remove it.\n\t\t\t\t\t// See https://twitter.com/LeaVerou/status/1785904086929346957\n\t\t\t\t\tnodes.forEach(node => node.remove());\n\t\t\t\t}\n\n\t\t\t\tlet element = slot.assignedElements()[0];\n\n\t\t\t\tif (name !== source && element) {\n\t\t\t\t\tsource = name;\n\t\t\t\t}\n\n\t\t\t\telement ??= slot.firstElementChild;\n\t\t\t\tthis[elementProp] = element;\n\n\t\t\t\t[\"min\", \"max\", \"step\"].forEach(prop => this[elementProp][prop] = this[prop]);\n\n\t\t\t\tif (oldElement !== this[elementProp]) {\n\t\t\t\t\toldElement?.removeEventListener(\"input\", this);\n\t\t\t\t\tthis[elementProp]?.addEventListener(\"input\", this);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis[source + \"Element\"].dispatchEvent(new Event(\"input\", {bubbles: true}));\n\t\t}\n\t\telse if (event.type === \"input\") {\n\t\t\tif (event.target === this.sliderElement) {\n\t\t\t\tthis.#valueChanged({source: \"slider\"});\n\t\t\t}\n\t\t\telse if (event.target === this.valueElement) {\n\t\t\t\tthis.#valueChanged({source: \"value\"});\n\t\t\t}\n\n\t\t\tthis.dispatchEvent(new Event(\"input\", {\n\t\t\t\tbubbles: true,\n\t\t\t\toriginalTarget: event.target,\n\t\t\t}));\n\t\t}\n\t}\n\n\t#valueChanged ({source} = {}) {\n\t\tif (source) {\n\t\t\tlet value = this[source + \"Element\"].value;\n\n\t\t\tif (source === \"slider\") {\n\t\t\t\tthis.valueElement.value = this.show === \"progress\" ? +(this.progressAt(value) * 100).toPrecision(4) : value;\n\t\t\t}\n\t\t\telse if (source === \"value\") {\n\t\t\t\tthis.sliderElement.value = this.show === \"progress\" ? this.valueAt(value / 100) : value;\n\t\t\t}\n\t\t}\n\n\t\tthis.style.setProperty(\"--value\", this.value);\n\t\tthis.style.setProperty(\"--progress\", this.progress);\n\n\t\tif (!CSS.supports(\"field-sizing\", \"content\")) {\n\t\t\tlet valueStr = this.value + \"\";\n\t\t\tthis.valueElement.style.setProperty(\"--value-length\", valueStr.length);\n\t\t}\n\t}\n\n\tget show () {\n\t\treturn this.getAttribute(\"show\");\n\t}\n\n\tset show (value) {\n\t\tthis.setAttribute(\"show\", value);\n\t}\n\n\tget progress () {\n\t\treturn this.progressAt(this.value);\n\t}\n\n\tprogressAt (value) {\n\t\treturn (value - this.min) / (this.max - this.min);\n\t}\n\n\tvalueAt (progress) {\n\t\treturn this.min + progress * (this.max - this.min);\n\t}\n\n\tattributeChangedCallback (name, oldValue, newValue) {\n\t\tif (oldValue === newValue) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (name === \"show\") {\n\t\t\tlet values = this;\n\n\t\t\tif (newValue === \"progress\") {\n\t\t\t\tvalues = {min: 0, max: 100, step: 1};\n\t\t\t}\n\n\t\t\t[\"min\", \"max\", \"step\"].forEach(prop => this.valueElement.setAttribute(prop, values[prop]));\n\t\t}\n\t\telse {\n\t\t\tthis.sliderElement.setAttribute(name, newValue);\n\t\t\tthis.valueElement.setAttribute(name, newValue);\n\t\t\tthis.#valueChanged();\n\t\t}\n\t}\n}\n\nlet defaults = {\n\tmin: 0,\n\tmax: 100,\n\tstep: 1,\n\tvalue: 50,\n\tdefaultValue: 50,\n}\n\nfor (let prop of Object.keys(defaults)) {\n\tObject.defineProperty(self.prototype, prop, {\n\t\tget () {\n\t\t\tlet value = this.sliderElement[prop]\n\t\t\treturn value === \"\" ? defaults[prop] : Number(value);\n\t\t},\n\t\tset (value) {\n\t\t\tlet oldValue = this.sliderElement[prop];\n\n\t\t\tif (oldValue !== value) {\n\t\t\t\tthis.sliderElement[prop] = value;\n\t\t\t\tthis.valueElement[prop] = value;\n\t\t\t}\n\t\t},\n\t});\n}\n\ncustomElements.define(self.tagName, self);\n\nexport default self;"
  },
  {
    "path": "elements/nd-switch/README.md",
    "content": "---\ntitle: Nude switch\nid: nd-switch\ncss_only: true\n---\n\n<header>\n\n# Nude switch\n\nCSS-only toggle switch\n\n</header>\n\n\n\n## Examples\n\nBasic:\n\n```html\n<input type=\"checkbox\" class=\"nd-switch\">\n```\n\nBigger:\n\n```html\n<input type=\"checkbox\" class=\"nd-switch\" style=\"font-size: 200%\">\n```\n\nWith larger and smaller thumb:\n\n```html\n<input type=\"checkbox\" class=\"nd-switch\" style=\"--nd-thumb-margin: -.2em\">\n<input type=\"checkbox\" class=\"nd-switch\" style=\"--nd-thumb-margin: .2em\">\n```\n\nDifferent colors:\n```html\n<input type=\"checkbox\" class=\"nd-switch\" style=\"\n\t--nd-thumb-color: black;\n\t--nd-switch-color: white; border: 1px solid black;\n\t--nd-switch-color-checked: red\n\">\n```\n\nRight to left:\n\n```html\n<input type=\"checkbox\" class=\"nd-switch\" dir=\"rtl\">\n```\n\nDisabled:\n\n```html\n<input type=\"checkbox\" class=\"nd-switch\" disabled>\n```\n\n"
  },
  {
    "path": "elements/nd-switch/nd-switch.css",
    "content": "input[type=checkbox].nd-switch {\n\t--nd-_switch-width: var(--nd-switch-width, 3em);\n\t--nd-_switch-height: var(--nd-switch-height, calc(var(--nd-_switch-width) / 2));\n\t--nd-_switch-color: var(--nd-switch-color, hsl(220 10% 60%));\n\t--nd-_switch-color-checked: var(--nd-switch-color-checked, hsl(205 50% 50%));\n\t--nd-_thumb-margin: var(--nd-thumb-margin, .1em);\n\t--nd-_thumb-color: var(--nd-thumb-color, hsl(220 10% 96%));\n\n\t-webkit-appearance: none;\n\tdisplay: inline-flex;\n\tvertical-align: var(--nd-_thumb-margin);\n\talign-items: center;\n\twidth: var(--nd-_switch-width);\n\theight: var(--nd-_switch-height);\n\tbox-sizing: border-box;\n\tborder: 1px solid rgb(0 0 0 / .1);\n\tborder-radius: 1em;\n\t--nd-current-background: var(--nd-_switch-color);\n\tbackground: var(--nd-current-background);\n\tcursor: pointer;\n\ttransition: .3s background;\n\n\t&::before {\n\t\tcontent: \"\";\n\t\tdisplay: block;\n\t\taspect-ratio: 1 / 1;\n\t\tmargin: var(--nd-_thumb-margin);\n\t\theight: calc(100% - 2 * var(--nd-_thumb-margin));\n\t\tbackground: var(--nd-_thumb-color);\n\t\tborder-radius: 50%;\n\t\ttransition: margin;\n\t\ttransition-duration: inherit;\n\t\tbox-shadow: 0 0 0 1px var(--nd-current-background), 0 0 var(--nd-focus-ring, );\n\t}\n\n\t&:checked {\n\t\t--nd-current-background: var(--nd-_switch-color-checked);\n\n\t\t&::before {\n\t\t\tmargin-inline-start: calc(var(--nd-_switch-width) - var(--nd-_switch-height) + var(--nd-_thumb-margin));\n\t\t}\n\t}\n\n\t&:disabled {\n\t\tfilter: grayscale();\n\t\topacity: .6;\n\t\tcursor: not-allowed;\n\t}\n\n\t&:focus {\n\t\toutline: none;\n\n\t\t&::before {\n\t\t\t--nd-focus-ring: .05em .2em hsl(205 80% 50% / .5);\n\t\t}\n\t}\n}"
  },
  {
    "path": "elements/with-presets/README.md",
    "content": "---\nid: with-presets\n---\n\n<header>\n\n# `<with-presets>`\n\nA freeform text field with visible presets\n\n</header>\n\n\n\n## Features\n\n- Freeform text with visible presets, for when you want to display both the label and value of the selected option (unlike `<datalist>`)\n- Use with `<select>` and `<input>` or custom elements!\n- Reactively selects a preset even if entered in the freeform text field\n- Selects the correct preset, even if the preset was created dynamically\n- TODO: How would labels be appropriately used for this?\n\n## Examples\n\nWith select:\n\n```html\n<with-presets id=\"with_select\">\n\t<select>\n\t\t<option value=\"[time(time, 'minutes')]\">HH:ii</option>\n\t\t<option value=\"[time(time, 'hours')]\">HH:00</option>\n\t\t<option value=\"[time(time, 'seconds')]\">HH:ii:ss</option>\n\t\t<option value=\"[time(time, 'ms')]\">HH:ii:ss.ms</option>\n\t</select>\n\t<input />\n</with-presets>\n```\n\nWith [`<button-group>`](../button-group/):\n\n```html\n<button-group>\n\t<button>1</button>\n\t<button value=\"2\">Two</button>\n\t<button value=\"3\">Three</button>\n\t<button value=\"custom\">Custom…</button>\n</button-group>\n```\n\n```html\n<with-presets id=\"with_buttongroup\">\n\t<button-group>\n\t\t<button>1</button>\n\t\t<button value=\"2\">Two</button>\n\t\t<button value=\"3\">Three</button>\n\t\t<button value=\"custom\">Custom…</button>\n\t</button-group>\n\t<input />\n</with-presets>\n<button onclick='with_buttongroup.value = Math.floor(Math.random() * 5)'>Set random number 0-4</button>\n```\n\nWith dynamic select:\n\n```html\n<with-presets vertical id=\"with_dynamic_select\">\n\t<select size=\"4\">\n\t</select>\n\t<input value=\"3\">\n</with-presets>\n<button onclick='with_dynamic_select.children[0].insertAdjacentHTML(\"beforeend\", `<option>${with_dynamic_select.select.children.length}</option>`)'>Add option</button>\n```\n\n"
  },
  {
    "path": "elements/with-presets/style.css",
    "content": ":host {\n\tdisplay: flex;\n\tgap: .2em;\n}\n\n:host([vertical]) {\n\tflex-flow: column;\n\talign-items: stretch;\n}\n\n::slotted(input),\n::slotted(:not(select):nth-child(2)) {\n\tflex: 1;\n}\n\n::slotted(button-group) {\n\tmargin: 0;\n}"
  },
  {
    "path": "elements/with-presets/with-presets.js",
    "content": "export default class WithPresets extends HTMLElement {\n\t#observer\n\n\tconstructor() {\n\t\tsuper();\n\n\t\tthis.attachShadow({ mode: \"open\" });\n\t\tthis.shadowRoot.innerHTML = `<style>@import \"${new URL(\"style.css\", import.meta.url)}\";</style><slot></slot>`;\n\n\t\tthis.customValue = this.getAttribute(\"customvalue\") ?? \"\";\n\n\t\tthis.addEventListener(\"input\", evt => {\n\t\t\tlet value = evt.target.value;\n\n\t\t\tif (value === \"custom\" && evt.target === this.select) {\n\t\t\t\tvalue = this.customValue ?? \"\";\n\t\t\t}\n\n\t\t\tthis.value = value;\n\t\t});\n\t}\n\n\tisPreset (value) {\n\t\t// Is there an option with this value?\n\t\tvalue = value?.replaceAll? value.replaceAll('\"', '\\\\\"') : value;\n\t\treturn !!this.findPreset(value);\n\t}\n\n\tfindPreset (value) {\n\t\tlet options = this.select.options || this.select.children; // might not be a <select>\n\n\t\treturn [...options].find(option => option.value === value);\n\t}\n\n\t#value\n\n\tget value () {\n\t\treturn this.#value;\n\t}\n\n\tset value (value) {\n\t\tvalue ??= \"\";\n\n\t\tif (this.#value && !this.isPreset(this.#value)) {\n\t\t\t// Allow user to toggle between presets and not lose the custom value they entered\n\t\t\tthis.customValue = this.#value;\n\t\t}\n\n\t\tthis.#value = value;\n\n\t\tif (this.input) {\n\t\t\tthis.input.value = this.#value;\n\t\t}\n\n\t\tif (this.select) {\n\t\t\tthis.select.value = this.isPreset(value) ? value : \"custom\";\n\t\t}\n\n\t\tthis[(this.isCustom ? 'set' : 'remove') + \"Attribute\"](\"custom\", \"\");\n\t}\n\n\tget isCustom () {\n\t\treturn this.select?.value === \"custom\";\n\t}\n\n\tconnectedCallback () {\n\t\tthis.input = this.querySelector(\":scope > input\");\n\t\tthis.select = this.querySelector(\":scope > select\");\n\n\t\tif (this.input && !this.select) {\n\t\t\tthis.select = this.querySelector(\":scope > :not(input)\");\n\t\t}\n\t\telse if (this.select && !this.input) {\n\t\t\tthis.input = this.querySelector(\":scope > :not(select)\");\n\t\t}\n\t\telse {\n\t\t\t// Neither an <input> nor a <select> is used, go by child order\n\t\t\tthis.input = this.children[1];\n\t\t\tthis.select = this.children[0];\n\t\t}\n\n\n\t\t// Just in case\n\t\tcustomElements.upgrade(this.input);\n\t\tcustomElements.upgrade(this.select);\n\n\t\tif (this.select && !this.findPreset(\"custom\")) {\n\t\t\t// TODO get tag name from first option\n\t\t\tthis.select.insertAdjacentHTML(\"beforeend\", `<option value=\"custom\">Custom…</option>`);\n\t\t}\n\n\t\tif (this.#value) {\n\t\t\tthis.value = this.#value;\n\t\t}\n\t\telse if (this.input?.value) {\n\t\t\tthis.value = this.input.value;\n\t\t}\n\t\telse if (this.select) {\n\t\t\tthis.value = this.select.value;\n\t\t}\n\n\t\tthis.observer = new MutationObserver(records => {\n\t\t\t// Presets changed. We only need to react in two cases:\n\t\t\t// a) The value was a preset, but the preset is no longer available\n\t\t\t// b) The value was not a preset, but the preset is now available\n\t\t\tlet wasPreset = !this.isCustom;\n\t\t\tlet isPreset = this.isPreset(this.#value);\n\n\t\t\tif (isPreset !== wasPreset) {\n\t\t\t\tthis.value = this.#value;\n\t\t\t}\n\t\t});\n\n\t\tthis.observer.observe(this.select, {\n\t\t\tsubtree: true,\n\t\t\tchildList: true,\n\t\t\tcharacterData: true,\n\t\t\tattributeFilter: [\"value\"]\n\t\t});\n\t}\n}\n\ncustomElements.define(\"with-presets\", WithPresets);"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"nudeui\",\n  \"version\": \"0.0.1\",\n  \"description\": \"\",\n  \"main\": \"index.html\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\",\n    \"build:css\": \"npx postcss \\\"**/*.postcss\\\" --base . --dir . --ext .css --config postcss.config.cjs\",\n    \"build:html\": \"npx @11ty/eleventy --config=.eleventy.cjs\",\n    \"build\": \"npm run build:html && npm run build:css\",\n    \"watch:css\": \"npx postcss \\\"**/*.postcss\\\" --base . --dir . --ext .css --config postcss.config.cjs --watch\",\n    \"watch:html\": \"npx @11ty/eleventy --config=.eleventy.cjs --watch\",\n    \"watch\": \"npx concurrently -n w: npm:watch:*\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/leaverou/nudeui.git\"\n  },\n  \"keywords\": [\n    \"CSS\",\n    \"forms\"\n  ],\n  \"author\": \"Lea Verou\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/leaverou/nudeui/issues\"\n  },\n  \"homepage\": \"https://github.com/leaverou/nudeui#readme\",\n  \"devDependencies\": {\n    \"concurrently\": \"^7.6.0\",\n    \"markdown-it-anchor\": \"^8.6.7\",\n    \"markdown-it-attrs\": \"^4.1.6\",\n    \"postcss\": \"^8.3.9\",\n    \"postcss-cli\": \"^9.0.1\",\n    \"postcss-nesting\": \"^8.0.1\"\n  }\n}\n"
  },
  {
    "path": "postcss.config.cjs",
    "content": "module.exports = {\n\tplugins: [\n\t\trequire('postcss-nesting')({})\n\t],\n};"
  },
  {
    "path": "style/forms.css",
    "content": "@import url('tokens.css');\n\nbutton, input, textarea, select,\n.button,\n::part(button), ::part(input) {\n\tfont: inherit;\n\tbox-sizing: border-box;\n}\n\ninput:where([type=\"number\"]),\n::part(input-number) {\n\twidth: 3em;\n}\n\ninput:where(:not([type]), [type=\"text\"], [type=\"url\"], [type=\"number\"]), textarea, select,\n::part(input), ::part(textarea), ::part(select) {\n\tborder: 1px solid var(--color-neutral-50a);\n\tborder-radius: .2em;\n\tpadding: .2em .4em;\n\tmin-width: 4em;\n}\n\nfieldset {\n\tborder: none;\n\tmargin: .5rem 0;\n\tbackground: var(--color-neutral-10a);\n\tborder-radius: .3em;\n\n\t& > legend {\n\t\tfont-weight: bold;\n\t}\n}\n\nbutton,\n.button,\n::part(button) {\n\tpadding: .6em .8em;\n\tborder: 1px solid hsl(0 0 0 / .2);\n\tborder-radius: calc(.15em + .15rem);\n\tbackground-color: white;\n\tcursor: pointer;\n\tfont-size: var(--font-size-small);\n\ttext-decoration: none;\n\tcolor: inherit;\n\ttransition-duration: .2s;\n\ttransition-property: background-color, border-color, color;\n\tline-height: 1.1;\n\twhite-space: nowrap;\n\n\t&:disabled {\n\t\tcursor: not-allowed;\n\t\topacity: .5;\n\t}\n\n\t&:active,\n\t&[aria-pressed=\"true\"] {\n\t\tbox-shadow: 0 .1em .2em hsl(0 0% 0% / .2) inset, 0 0 0 2em var(--color-neutral-10a) inset;\n\t}\n\n\t&:where(:enabled) {\n\t\t&:hover {\n\t\t\tborder-color: hsl(var(--accent-color-hs) 80%);\n\t\t\tbackground-color: hsl(var(--accent-color-hs) 95%);\n\t\t\tcolor: hsl(var(--accent-color-hs) 15%);\n\t\t}\n\t}\n\n\t/* Icon */\n\t& i {\n\t\tdisplay: inline-block;\n\t\topacity: .7;\n\t}\n\n\t&.icon-prefix:where(:not(.icon-only)) i:first-of-type {\n\t\tmargin-right: .4em;\n\t}\n\n\t&.icon-suffix i:last-of-type {\n\t\tmargin-left: .4em;\n\t}\n\n\t&[type=submit] {\n\t\tbackground: var(--accent-color-2);\n\t\tcolor: white;\n\n\t\t&:where(:enabled) {\n\t\t\t&:hover {\n\t\t\t\tborder-color: hsl(var(--accent-color-2-hs) 80%);\n\t\t\t}\n\t\t}\n\t}\n}"
  },
  {
    "path": "style/tables.css",
    "content": "table {\n\twidth: 100%;\n\twidth: -moz-available;\n\twidth: -webkit-fill-available;\n\twidth: stretch;\n\tborder-collapse: collapse;\n\tborder-spacing: 0;\n}\n\ntd, th {\n\tpadding: .2em .4em;\n\tborder: 1px solid var(--color-neutral-30a);\n}\n\n:where(thead) {\n\tth {\n\t\tbackground-color: var(--color-neutral-90);\n\t}\n}"
  },
  {
    "path": "style/tokens.css",
    "content": ":where(:root) {\n\t--color-magenta-hs: var(--accent-color-hs);\n\n\t--color-blue-hs: var(--accent-color-2-hs);\n\t--color-blue: var(--accent-color-2);\n\t--focus-color: var(--color-blue);\n\n\t--color-neutral: oklch(50% 0.03 230);\n\t--color-canvas: oklch(from canvas l 0.002 none);\n\n\n\n\t--color-neutral-90a: color-mix(in oklch, var(--color-neutral) 90%, oklch(none none none / 0));\n\t--color-neutral-80a: color-mix(in oklch, var(--color-neutral) 80%, oklch(none none none / 0));\n\t--color-neutral-70a: color-mix(in oklch, var(--color-neutral) 70%, oklch(none none none / 0));\n\t--color-neutral-50a: color-mix(in oklch, var(--color-neutral) 50%, oklch(none none none / 0));\n\t--color-neutral-30a: color-mix(in oklch, var(--color-neutral) 30%, oklch(none none none / 0));\n\t--color-neutral-20a: color-mix(in oklch, var(--color-neutral) 20%, oklch(none none none / 0));\n\t--color-neutral-10a: color-mix(in oklch, var(--color-neutral) 10%, oklch(none none none / 0));\n\n\t--color-neutral-95: color-mix(in oklch, var(--color-neutral), var(--color-canvas) 96%);\n\t--color-neutral-90: color-mix(in oklch, var(--color-neutral), var(--color-canvas) 90%);\n\t--color-neutral-80: color-mix(in oklch, var(--color-neutral), var(--color-canvas) 80%);\n\t--color-neutral-70: color-mix(in oklch, var(--color-neutral), var(--color-canvas) 70%);\n\n\t/* Semantic colors */\n\t--accent-color-hs: 335 90%;\n\t--accent-color: hsl(var(--accent-color-hs) 50%);\n\n\t--accent-color-2-hs: 200 90%;\n\t--accent-color-2: hsl(var(--accent-color-2-hs) 50%);\n\n\t/* Fonts */\n\t--font-body: system-ui, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;\n\t--font-mono: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;\n}"
  },
  {
    "path": "style.css",
    "content": "@import url('style/tokens.css');\n@import url('style/forms.css');\n@import url('style/tables.css');\n@import url('https://prismjs.com/themes/prism-solarizedlight.min.css');\n\n:root {\n\t--page-width: 900px;\n\t--page-margin-inline: clamp(1em, 50vw - var(--page-width) / 2, 50vw);\n}\n\nbody {\n\tdisplay: flex;\n\tflex-direction: column;\n\tmargin: 0;\n\tmin-height: 100vh;\n\tfont: 100%/1.5 var(--font-body);\n\tpadding-inline: var(--page-margin-inline);\n\n\t> :is(header, nav, footer) {\n\t\tpadding-inline: var(--page-margin-inline);\n\n\t}\n\n\t> :is(header, nav, footer, .full-width) {\n\t\tmargin-inline: calc(-1 * var(--page-margin-inline));\n\t}\n\n\t> header {\n\t\torder: -2;\n\t\tbackground: var(--accent-color);\n\t\tcolor: white;\n\t\tpadding-block: 1em;\n\n\t\t& h1 {\n\t\t\tmargin: 0;\n\t\t\tfont-size: 300%;\n\n\t\t\t& + p {\n\t\t\t\tmargin: 0;\n\t\t\t\tfont-weight: bold;\n\t\t\t}\n\t\t}\n\n\t\t& a {\n\t\t\tcolor: inherit;\n\t\t}\n\n\t\t& .home {\n\t\t\ttext-transform: uppercase;\n\t\t\tfont-weight: 900;\n\t\t\tfont-size: 75%;\n\n\t\t\t&:not(:hover) {\n\t\t\t\ttext-decoration: none;\n\t\t\t}\n\n\t\t\t&::before {\n\t\t\t\tcontent: \"🏠\";\n\t\t\t\tmargin-right: .3em;\n\t\t\t}\n\t\t}\n\t}\n\n\t> nav {\n\t\torder: -1;\n\n\t\t& + * {\n\t\t\tmargin-top: 1em;\n\t\t}\n\t}\n}\n\nh1, h2, h3, h4, h5, h6 {\n\tline-height: 1.1;\n\tmargin-block: 1.5rem .5rem;\n\n\t.header-anchor {\n\t\tcolor: inherit;\n\n\t\t&:not(:hover, :focus) {\n\t\t\ttext-decoration: none;\n\t\t}\n\t}\n}\n\nh2 {\n\tfont-size: 2.5rem;\n\tfont-weight: 300;\n\tcolor: var(--accent-color);\n}\n\nul, ol {\n\tmargin-block: .5rem 1rem;\n}\n\nnav {\n\torder: 1;\n\tdisplay: flex;\n\tpadding-top: 0;\n\tpadding-bottom: 0;\n\tbackground: hsl(var(--accent-color-hs) 65%);\n\n\t& > a {\n\t\tflex: 1;\n\t\tpadding: .4em .5em;\n\t\tcolor: white;\n\t\tfont-weight: bold;\n\t\tbackground: linear-gradient(to right, hsl(var(--accent-color-hs) 50%), hsl(var(--accent-color-hs) 75%)) no-repeat left / 0 100%;\n\t\ttransition: .3s;\n\n\t\t&:hover {\n\t\t\tbackground-size: 100% 100%;\n\t\t}\n\n\t\t&:where(:not(:hover)) {\n\t\t\ttext-decoration: none;\n\t\t}\n\t}\n}\n\ncode, pre {\n\tfont-family: var(--font-mono);\n}\n\niframe {\n\tborder: 0;\n}\n\n.failed {\n\t--color-hs: 0 50%;\n\tborder: 1px solid hsl(var(--color-hs) 60%);\n\tbackground: hsl(var(--color-hs) 95%);\n\tcolor: hsl(var(--color-hs) 20%);\n\tpadding: 1em;\n\n\t& :is(h2, h3, h4, h5, h6) {\n\t\tcolor: hsl(var(--color-hs) 50%);\n\t}\n\n\t& h2 {\n\t\tmargin-top: 0;\n\n\t\t&::before {\n\t\t\tcontent: \"⚠️\";\n\t\t}\n\t}\n}\n"
  }
]