{"name":"select","files":[{"path":"ui/Icon.svelte","content":"<script lang=\"ts\">\n\timport type { SVGAttributes } from 'svelte/elements';\n\n\ttype IconNode = readonly (readonly [string, Readonly<Record<string, string | number>>])[];\n\n\tinterface Props extends SVGAttributes<SVGSVGElement> {\n\t\t/** Any icon from @hugeicons/core-free-icons. */\n\t\ticon: IconNode;\n\t\tsize?: number | string;\n\t\t/** Accessible name. Without it the icon is decorative and hidden from screen readers. */\n\t\tlabel?: string;\n\t\t/** Overrides the icon's stroke width (Hugeicons default 1.5). */\n\t\tstrokeWidth?: number;\n\t}\n\n\tlet { icon, size = 18, label, strokeWidth, ...rest }: Props = $props();\n\n\t// Rendered in markup, not onMount: icons are in the server HTML, so nothing pops in after hydration.\n\tconst kebab = (k: string) => k.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase());\n\tconst nodes = $derived(\n\t\ticon.map(([tag, attrs]) => ({\n\t\t\ttag,\n\t\t\tattrs: Object.fromEntries(\n\t\t\t\tObject.entries(attrs)\n\t\t\t\t\t.filter(([k]) => k !== 'key')\n\t\t\t\t\t.map(([k, v]) => [kebab(k), k === 'strokeWidth' && strokeWidth ? strokeWidth : v])\n\t\t\t)\n\t\t}))\n\t);\n</script>\n\n<svg\n\txmlns=\"http://www.w3.org/2000/svg\"\n\tviewBox=\"0 0 24 24\"\n\twidth={size}\n\theight={size}\n\tfill=\"none\"\n\trole={label ? 'img' : undefined}\n\taria-label={label}\n\taria-hidden={label ? undefined : 'true'}\n\tfocusable=\"false\"\n\t{...rest}\n>\n\t{#each nodes as { tag, attrs }, i (i)}\n\t\t<svelte:element this={tag} xmlns=\"http://www.w3.org/2000/svg\" {...attrs} />\n\t{/each}\n</svg>\n\n<style>\n\tsvg {\n\t\tflex: none;\n\t\tdisplay: block;\n\t}\n</style>\n"},{"path":"ui/Select.svelte","content":"<script lang=\"ts\">\n\timport type { Snippet } from 'svelte';\n\timport type { HTMLSelectAttributes } from 'svelte/elements';\n\timport { ArrowDown01Icon } from '@hugeicons/core-free-icons';\n\timport Icon from './Icon.svelte';\n\n\tinterface Props extends Omit<HTMLSelectAttributes, 'value' | 'children'> {\n\t\tlabel: string;\n\t\tvalue?: string;\n\t\t/** Shown until a choice is made; it can't be chosen back. */\n\t\tplaceholder?: string;\n\t\thint?: string;\n\t\terror?: string;\n\t\t/** The <option> and <optgroup> elements. */\n\t\tchildren: Snippet;\n\t}\n\n\tconst uid = $props.id();\n\tlet {\n\t\tlabel,\n\t\tvalue = $bindable(''),\n\t\tplaceholder,\n\t\thint,\n\t\terror,\n\t\tchildren,\n\t\tid = uid,\n\t\tclass: className,\n\t\t...rest\n\t}: Props = $props();\n\n\tconst describedby = $derived(\n\t\t[hint && `${id}-hint`, error && `${id}-error`].filter(Boolean).join(' ') || undefined\n\t);\n</script>\n\n<!-- Native <select>: the platform picker (a sheet on phones), keyboard, typeahead and forms for free. -->\n<div class={['field', className]}>\n\t<label for={id}>{label}</label>\n\t<div class=\"control\">\n\t\t<select\n\t\t\t{id}\n\t\t\tbind:value\n\t\t\taria-invalid={error ? true : undefined}\n\t\t\taria-describedby={describedby}\n\t\t\t{...rest}\n\t\t>\n\t\t\t{#if placeholder}<option value=\"\" disabled>{placeholder}</option>{/if}\n\t\t\t{@render children()}\n\t\t</select>\n\t\t<span class=\"chev\" aria-hidden=\"true\"><Icon icon={ArrowDown01Icon} size={16} /></span>\n\t</div>\n\t{#if hint}<p id=\"{id}-hint\" class=\"hint\">{hint}</p>{/if}\n\t{#if error}<p id=\"{id}-error\" class=\"error\">{error}</p>{/if}\n</div>\n\n<style>\n\t.field {\n\t\tdisplay: grid;\n\t\tgap: 0.25rem;\n\t\tfont: 0.9375rem/1.5 var(--ui-font);\n\t\tcolor: var(--ui-fg);\n\t}\n\tlabel {\n\t\tfont-weight: 500;\n\t}\n\t.control {\n\t\tposition: relative;\n\t}\n\t/* Same box as Input: border, radius and 40px height, so they line up in a form. */\n\tselect {\n\t\tappearance: none;\n\t\tbox-sizing: border-box;\n\t\tinline-size: 100%;\n\t\tmin-block-size: 2.5rem;\n\t\tmargin: 0;\n\t\tpadding-block: 0.375rem;\n\t\tpadding-inline: 0.75rem 2.25rem;\n\t\tborder: 1px solid var(--ui-field-line);\n\t\tborder-radius: var(--ui-radius-control);\n\t\tbackground: var(--ui-surface);\n\t\tcolor: inherit;\n\t\t/* 16px minimum: iOS Safari zooms the page into any smaller field on focus. */\n\t\tfont: inherit;\n\t\tfont-size: max(1rem, 16px);\n\t\tbox-shadow: none;\n\t\tcursor: pointer;\n\t}\n\t@media (pointer: coarse) {\n\t\tselect {\n\t\t\tmin-block-size: 2.75rem;\n\t\t}\n\t}\n\t/* The placeholder reads as a hint, not a value. */\n\tselect:has(option[value='']:checked) {\n\t\tcolor: var(--ui-muted);\n\t}\n\t/* ...but the open list shows every option in normal text (options come from the parent's markup). */\n\tselect :global(option) {\n\t\tcolor: var(--ui-fg);\n\t}\n\tselect:focus-visible {\n\t\toutline: var(--ui-ring-width) solid var(--ui-ring);\n\t\toutline-offset: var(--ui-ring-offset);\n\t}\n\tselect[aria-invalid='true'] {\n\t\tborder-color: var(--ui-danger);\n\t}\n\tselect[aria-invalid='true']:focus-visible {\n\t\toutline-color: var(--ui-danger);\n\t}\n\tselect:disabled {\n\t\tbackground: var(--ui-subtle);\n\t\topacity: 0.55;\n\t\tcursor: not-allowed;\n\t}\n\t.chev {\n\t\tposition: absolute;\n\t\tinset-block: 0;\n\t\tinset-inline-end: 0.75rem;\n\t\tdisplay: grid;\n\t\tplace-items: center;\n\t\tcolor: var(--ui-muted);\n\t\tpointer-events: none;\n\t}\n\t.hint,\n\t.error {\n\t\tmargin: 0;\n\t\tfont-size: 0.8125rem;\n\t}\n\t.hint {\n\t\tcolor: var(--ui-muted);\n\t}\n\t.error {\n\t\tcolor: var(--ui-danger);\n\t}\n</style>\n"}],"dependencies":{"@hugeicons/core-free-icons":"^4.3.5"}}