@ketvietlab/ketsuite 0.1.13 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -1
- package/dist/modules/backend/design/HANDOFF.md +17 -18
- package/dist/modules/backend/design/client/backend-shell.d.mts +2 -2
- package/dist/modules/backend/design/client/backend-shell.d.mts.map +1 -1
- package/dist/modules/backend/design/client/backend-shell.mjs +1 -1
- package/dist/modules/backend/design/client/backend-shell.mjs.map +4 -4
- package/dist/modules/backend/design/content.css +0 -88
- package/dist/modules/backend/design/design-system.css +1 -1
- package/dist/modules/backend/design/foundation.css +8 -195
- package/dist/modules/backend/design/responsive.css +7 -16
- package/dist/modules/backend/messages.d.ts.map +1 -1
- package/dist/modules/backend/messages.js +4 -0
- package/dist/modules/backend/messages.js.map +1 -1
- package/dist/scaffold/index.js +1 -1
- package/dist/ui/client/table-selection-view.d.ts.map +1 -1
- package/dist/ui/client/table-selection-view.js +3 -0
- package/dist/ui/client/table-selection-view.js.map +1 -1
- package/dist/ui/layout.d.ts.map +1 -1
- package/dist/ui/layout.js +2 -2
- package/dist/ui/layout.js.map +1 -1
- package/dist/ui/nav.d.ts +4 -2
- package/dist/ui/nav.d.ts.map +1 -1
- package/dist/ui/nav.js +53 -39
- package/dist/ui/nav.js.map +1 -1
- package/package.json +4 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../../../../ui/client/user-workflow.ts", "../../../../ui/client/table-selection-view.tsx"],
|
|
4
|
-
"sourcesContent": ["export function installUserWorkflow(lifetime: AbortSignal): () => void {\n let abort: AbortController | undefined,\n timer: ReturnType<typeof setTimeout> | undefined,\n sequence = 0\n async function check(form: HTMLFormElement) {\n const input = form.querySelector<HTMLInputElement>('input[type=email]'),\n status = form.querySelector<HTMLElement>('[data-email-status]')\n if (!input || !status) return\n abort?.abort()\n clearTimeout(timer)\n const request = ++sequence\n const value = input.value.trim(),\n next = form.querySelector<HTMLButtonElement>('[data-email-next]'),\n retry = form.querySelector<HTMLButtonElement>('[data-email-retry]')\n const paint = (message: string, valid: boolean) => {\n status.textContent = message\n if (next) next.disabled = !valid\n input.setAttribute('aria-invalid', String(!valid))\n form.dataset.emailAvailable = valid ? 'true' : 'false'\n }\n if (retry) retry.hidden = true\n if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value)) {\n paint(value ? 'Email ch\u01B0a \u0111\u00FAng \u0111\u1ECBnh d\u1EA1ng.' : 'Nh\u1EADp email \u0111\u1EC3 ki\u1EC3m tra.', false)\n return\n }\n paint('\u0110ang ki\u1EC3m tra email\u2026', false)\n timer = setTimeout(async () => {\n abort = new AbortController()\n try {\n const response = await fetch(form.dataset.emailCheck!, {\n method: 'POST',\n cache: 'no-store',\n headers: { 'content-type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({ email: value }),\n signal: AbortSignal.any([abort.signal, AbortSignal.timeout(10000)]),\n })\n if (!response.ok) throw new Error('email-check-failed')\n const result = (await response.json()) as { available: boolean; ok: boolean }\n if (request !== sequence || !form.isConnected) return\n if (!result.ok) throw new Error('email-check-failed')\n paint(\n result.available ? 'Email c\u00F3 th\u1EC3 s\u1EED d\u1EE5ng.' : 'Email \u0111\u00E3 \u0111\u01B0\u1EE3c s\u1EED d\u1EE5ng. Nh\u1EADp email kh\u00E1c.',\n result.available,\n )\n } catch {\n if (request !== sequence || !form.isConnected) return\n paint('Ch\u01B0a ki\u1EC3m tra \u0111\u01B0\u1EE3c email. Vui l\u00F2ng th\u1EED l\u1EA1i.', false)\n if (retry) retry.hidden = false\n }\n }, 400)\n }\n function scope(form: HTMLFormElement, changed = false) {\n const company = form.querySelector<HTMLSelectElement>('select[name=\"companyId\"]'),\n branch = form.querySelector<HTMLSelectElement>('select[name=\"branchId\"]'),\n kind = form.querySelector<HTMLSelectElement>('select[name=\"scopeKind\"]')?.value\n if (branch && company) {\n if (changed) branch.value = ''\n for (const option of branch.options) {\n if (option.value) {\n option.hidden = option.dataset.companyId !== company.value\n option.disabled = option.hidden\n }\n }\n }\n const key =\n kind === 'tenant'\n ? 'tenant'\n : kind === 'branch'\n ? `branch:${company?.value}:${branch?.value}`\n : `company:${company?.value}`\n for (const input of form.querySelectorAll<HTMLInputElement>('[data-assigned-scopes]')) {\n const scopes = JSON.parse(input.dataset.assignedScopes ?? '[]') as string[]\n const assigned = scopes.includes(key)\n input.disabled = input.dataset.roleDisabled === 'true' || assigned\n const row = input.closest('[data-role-option]')\n const assignedLabel = row?.querySelector<HTMLElement>('[data-role-assigned]')\n const description = row?.querySelector<HTMLElement>('[data-role-description]')\n if (assignedLabel) assignedLabel.hidden = !assigned\n if (description) description.hidden = assigned\n if (input.disabled) input.checked = false\n }\n const companyField = company?.closest<HTMLElement>('[data-ui=\"form-field\"]')\n const branchField = branch?.closest<HTMLElement>('[data-ui=\"form-field\"]')\n if (companyField) companyField.hidden = kind === 'tenant'\n if (branchField) branchField.hidden = kind === 'tenant' || kind === 'company'\n }\n function scan() {\n for (const form of document.querySelectorAll<HTMLFormElement>('form[data-user-workflow]'))\n if (!form.dataset.initialized) {\n form.dataset.initialized = 'true'\n scope(form)\n void check(form)\n }\n }\n document.addEventListener(\n 'input',\n (event) => {\n const input = event.target as HTMLInputElement,\n form = input.closest<HTMLFormElement>('form[data-user-workflow]')\n if (!form) return\n if (input.type === 'email') void check(form)\n if (input.matches('[data-role-search]')) {\n let n = 0\n for (const row of form.querySelectorAll<HTMLElement>('[data-role-option]')) {\n row.hidden = !row.textContent?.toLocaleLowerCase('vi').includes(input.value.toLocaleLowerCase('vi'))\n if (!row.hidden) n++\n }\n const empty = form.querySelector<HTMLElement>('[data-role-empty]')\n if (empty) empty.hidden = n > 0\n }\n },\n { signal: lifetime },\n )\n document.addEventListener(\n 'change',\n (event) => {\n const input = event.target as HTMLElement,\n form = input.closest<HTMLFormElement>('form[data-user-workflow]')\n if (form && input.matches('select[name=\"companyId\"],select[name=\"branchId\"],select[name=\"scopeKind\"]'))\n scope(form, input.matches('select[name=\"companyId\"]'))\n },\n { signal: lifetime },\n )\n document.addEventListener(\n 'click',\n (event) => {\n const target = event.target as HTMLElement\n if (target.matches('[data-email-retry]')) void check(target.closest('form')!)\n },\n { signal: lifetime },\n )\n document.addEventListener(\n 'submit',\n (event) => {\n const form = event.target as HTMLFormElement\n if (\n form.matches('form[data-user-workflow]') &&\n form.querySelector('input[type=email]') &&\n form.dataset.emailAvailable !== 'true'\n )\n event.preventDefault()\n },\n { signal: lifetime },\n )\n const observer = new MutationObserver(scan)\n observer.observe(document.documentElement, { childList: true, subtree: true })\n scan()\n return () => {\n abort?.abort()\n clearTimeout(timer)\n observer.disconnect()\n }\n}\n", "import { installUserWorkflow } from './user-workflow.ts'\nimport type { BrowserBehavior, BrowserNavigation } from '@ketvietlab/ketjs'\n\nconst themeStorageKey = 'ket.backend.theme'\nconst dismissibleDropdown = [\n '[data-ui=\"search-menu\"]',\n '[data-ui=\"col-config\"]',\n '[data-ui=\"bulk-actions\"]',\n '[data-ui=\"viewer\"]',\n '[data-ui=\"record-more\"]',\n].join(', ')\n\ntype KetBrowserGlobals = typeof globalThis & {\n confirm: (message?: string) => boolean\n}\n\nconst browserGlobals = globalThis as KetBrowserGlobals\nconst eventElement = (event: Event): Element | null => (event.target instanceof Element ? event.target : null)\n\ntype ThemePreference = 'light' | 'dark'\n\nconst themePreference = (value: string | null | undefined): ThemePreference | null =>\n value === 'light' || value === 'dark' ? value : null\n\nconst storedTheme = (): ThemePreference | null => {\n try {\n return themePreference(localStorage.getItem(themeStorageKey))\n } catch {\n return null\n }\n}\n\nconst saveTheme = (theme: ThemePreference): void => {\n try {\n localStorage.setItem(themeStorageKey, theme)\n } catch {\n // Theme selection still works for this document when storage is unavailable.\n }\n}\n\nconst resolvedTheme = (): ThemePreference =>\n themePreference(document.documentElement.dataset.theme) ??\n (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')\n\nconst syncThemeToggle = (): void => {\n const pressed = String(resolvedTheme() === 'dark')\n for (const control of document.querySelectorAll<HTMLElement>(\n '[data-ui=\"sidebar-tools\"] [data-ui=\"action\"][name=\"theme\"]',\n ))\n control.setAttribute('aria-pressed', pressed)\n}\n\n/** Keep the shell preference explicit only after the reader chooses one. */\nconst installThemeToggle = (signal: AbortSignal): void => {\n const preference = storedTheme()\n if (preference) document.documentElement.dataset.theme = preference\n syncThemeToggle()\n\n document.addEventListener(\n 'click',\n (event) => {\n const control = eventElement(event)?.closest<HTMLButtonElement>(\n '[data-ui=\"sidebar-tools\"] [data-ui=\"action\"][name=\"theme\"]',\n )\n if (!control || control.disabled) return\n const next = resolvedTheme() === 'dark' ? 'light' : 'dark'\n document.documentElement.dataset.theme = next\n saveTheme(next)\n syncThemeToggle()\n },\n { signal },\n )\n\n matchMedia('(prefers-color-scheme: dark)').addEventListener(\n 'change',\n () => {\n if (!themePreference(document.documentElement.dataset.theme)) syncThemeToggle()\n },\n { signal },\n )\n\n addEventListener(\n 'storage',\n (event) => {\n if (event.key !== themeStorageKey) return\n const next = themePreference(event.newValue)\n if (next) document.documentElement.dataset.theme = next\n else delete document.documentElement.dataset.theme\n syncThemeToggle()\n },\n { signal },\n )\n}\n\nconst updateSelection = (table: Element): void => {\n const rows = [...table.querySelectorAll<HTMLInputElement>('[data-ui=\"row-select\"]:not(:disabled)')]\n const checked = rows.filter((input) => input.checked).length\n const all = table.querySelector<HTMLInputElement>('[data-ui=\"select-all\"]')\n if (all) {\n all.checked = rows.length > 0 && checked === rows.length\n all.indeterminate = checked > 0 && checked < rows.length\n }\n if (!checked) {\n const shell = table.closest('[data-ui=\"shell\"]')\n for (const menu of shell?.querySelectorAll<HTMLDetailsElement>('[data-ui=\"bulk-actions\"][open]') ?? [])\n menu.removeAttribute('open')\n }\n}\n\nconst installTableSelection = (signal: AbortSignal, navigation: BrowserNavigation): void => {\n document.addEventListener(\n 'change',\n (event) => {\n const target = eventElement(event)\n if (!(target instanceof HTMLInputElement)) return\n if (!target.matches('[data-ui=\"select-all\"], [data-ui=\"row-select\"]')) return\n const table = target.closest('[data-ui=\"table\"]')\n if (!table) return\n if (target.matches('[data-ui=\"select-all\"]'))\n for (const input of table.querySelectorAll<HTMLInputElement>('[data-ui=\"row-select\"]:not(:disabled)'))\n input.checked = target.checked\n updateSelection(table)\n },\n { signal },\n )\n\n document.addEventListener(\n 'click',\n (event) => {\n if (\n event.defaultPrevented ||\n event.button !== 0 ||\n event.metaKey ||\n event.ctrlKey ||\n event.shiftKey ||\n event.altKey\n )\n return\n const target = eventElement(event)\n if (\n target?.closest(\n 'a, button, input, select, textarea, label, summary, details, [data-ui=\"select-cell\"]',\n )\n )\n return\n const row = target?.closest<HTMLElement>('[data-ui=\"row\"][data-row-href]')\n const href = row?.getAttribute('data-row-href')\n if (!href) return\n event.preventDefault()\n void navigation.navigate(href)\n },\n { signal },\n )\n document.addEventListener(\n 'keydown',\n (event) => {\n if (event.defaultPrevented || (event.key !== 'Enter' && event.key !== ' ')) return\n const target = eventElement(event)\n const row = target?.matches('[data-ui=\"row\"][data-row-href][tabindex=\"0\"]') ? target : null\n const href = row?.getAttribute('data-row-href')\n if (!href) return\n event.preventDefault()\n navigateTo(href, navigation)\n },\n { signal },\n )\n}\n\nconst openDropdowns = (): HTMLDetailsElement[] => [\n ...document.querySelectorAll<HTMLDetailsElement>(`${dismissibleDropdown}[open]`),\n]\n\nconst installDropdownDismiss = (signal: AbortSignal): void => {\n document.addEventListener(\n 'click',\n (event) => {\n const current = eventElement(event)?.closest(dismissibleDropdown)\n for (const dropdown of openDropdowns()) {\n if (dropdown !== current && !dropdown.contains(current ?? null)) dropdown.removeAttribute('open')\n }\n },\n { signal },\n )\n\n document.addEventListener(\n 'keydown',\n (event) => {\n if (event.key !== 'Escape') return\n const dropdowns = openDropdowns()\n if (!dropdowns.length) return\n const focused = document.activeElement?.closest<HTMLDetailsElement>(dismissibleDropdown)\n const current = focused?.open ? focused : dropdowns.at(-1)\n current?.removeAttribute('open')\n current?.querySelector<HTMLElement>('summary')?.focus()\n event.preventDefault()\n },\n { signal },\n )\n}\n\nconst installGlobalFilter = (signal: AbortSignal): void => {\n document.addEventListener(\n 'click',\n (event) => {\n const trigger = eventElement(event)?.closest<HTMLElement>('[data-ui=\"chrome-search-toggle\"]')\n const controls = trigger?.getAttribute('aria-controls')\n if (!trigger || !controls) return\n const dialog = document.getElementById(controls)\n if (!(dialog instanceof HTMLDialogElement) || dialog.open) return\n dialog.showModal()\n trigger.setAttribute('aria-expanded', 'true')\n requestAnimationFrame(() =>\n dialog\n .querySelector<HTMLElement>('[data-presentation=\"modal\"] [data-ui=\"chrome-search-input\"]')\n ?.focus(),\n )\n },\n { signal },\n )\n\n document.addEventListener(\n 'cancel',\n (event) => {\n if (!eventElement(event)?.matches('[data-ui=\"chrome-search-modal\"]')) return\n event.preventDefault()\n },\n { capture: true, signal },\n )\n\n document.addEventListener(\n 'close',\n (event) => {\n const dialog = eventElement(event)\n if (!(dialog instanceof HTMLDialogElement)) return\n if (!dialog.matches('[data-ui=\"chrome-search-modal\"]')) return\n const trigger = document.querySelector<HTMLElement>(\n `[data-ui=\"chrome-search-toggle\"][aria-controls=\"${CSS.escape(dialog.id)}\"]`,\n )\n trigger?.setAttribute('aria-expanded', 'false')\n trigger?.focus()\n },\n { capture: true, signal },\n )\n}\n\nconst focusable = [\n 'a[href]',\n 'button:not(:disabled)',\n 'input:not(:disabled):not([type=\"hidden\"])',\n 'select:not(:disabled)',\n 'textarea:not(:disabled)',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(', ')\n\nconst activeRouteModal = (): HTMLElement | null =>\n document.querySelector<HTMLElement>('[data-ui=\"modal-layer\"][data-route-modal=\"true\"]')\n\nconst modalFocusables = (modal: HTMLElement): HTMLElement[] =>\n [...modal.querySelectorAll<HTMLElement>(focusable)].filter(\n (item) =>\n !item.hidden &&\n item.getAttribute('aria-hidden') !== 'true' &&\n !item.matches('[data-ui=\"modal-backdrop\"]'),\n )\n\nconst navigateTo = (href: string, navigation: BrowserNavigation): void => {\n void navigation.navigate(href)\n}\n\nconst eventStartedInNestedModal = (event: KeyboardEvent, routeModal: HTMLElement): boolean =>\n event\n .composedPath()\n .some(\n (target) =>\n target instanceof HTMLElement &&\n target !== routeModal &&\n target.matches('[data-ui=\"modal-layer\"][data-presentation=\"dialog\"]'),\n )\n\n/**\n * Whether anything in the modal has been typed into since the server rendered it.\n *\n * Comparing each control against its own default is what makes this work without\n * any bookkeeping: the page is server-rendered, so the default *is* the state the\n * reader was given. A modal with no form \u2014 a reader, an inspector \u2014 has nothing\n * to compare and is never dirty, so the guard costs those screens nothing.\n */\nconst modalHasDraft = (modal: HTMLElement): boolean => {\n for (const field of modal.querySelectorAll<HTMLInputElement>('input:not([type=\"hidden\"])')) {\n if (field.disabled) continue\n if (field.type === 'checkbox' || field.type === 'radio') {\n if (field.checked !== field.defaultChecked) return true\n } else if (field.value !== field.defaultValue) return true\n }\n for (const area of modal.querySelectorAll<HTMLTextAreaElement>('textarea'))\n if (!area.disabled && area.value !== area.defaultValue) return true\n for (const select of modal.querySelectorAll<HTMLSelectElement>('select')) {\n if (select.disabled) continue\n for (const option of select.options) if (option.selected !== option.defaultSelected) return true\n }\n return false\n}\n\n/**\n * The guard, and what it is not.\n *\n * Leaving a modal is a link, and a link still works with scripting off \u2014 that is\n * the point of building it as one. So this catches the stray backdrop click, the\n * reflexive Escape and the tab clicked without thinking about the note already\n * typed; it is not a lock, and nothing here should be read as one. The wording\n * comes from the server because this file cannot translate.\n */\nconst mayLeaveModal = (modal: HTMLElement): boolean => {\n if (!modalHasDraft(modal)) return true\n const message = modal.getAttribute('data-unsaved-prompt')\n // No prompt declared means the screen did not ask to be guarded.\n if (!message) return true\n return browserGlobals.confirm(message)\n}\n\n/** Progressive keyboard behavior for URL-owned create/edit workspaces. */\n/**\n * Hear when the thing a screen is waiting for actually changed.\n *\n * The screens this replaces reloaded the whole document on a timer, which threw\n * away scroll, focus and anything typed, on a schedule unrelated to when the\n * work finished. Here the server says so, and the page asks for the fragment it\n * already knows how to swap \u2014 so the reader keeps their place.\n *\n * The connection belongs to the element. A fragment swap replaces it, and the\n * observer opens the new one and closes the old; a screen with nothing to wait\n * for renders no element and opens nothing.\n */\nconst installLiveRegion = (signal: AbortSignal, navigation: BrowserNavigation): (() => void) | undefined => {\n if (typeof EventSource !== 'function') return\n\n let watched: Element | null = null\n let source: EventSource | null = null\n const close = () => {\n source?.close()\n source = null\n watched = null\n }\n\n const sync = (): void => {\n const region = document.querySelector('[data-ui=\"live-region\"][data-stream]')\n if (region === watched) return\n close()\n const id = region instanceof HTMLElement ? (region.dataset.stream ?? '') : ''\n if (!id) return\n watched = region\n source = new EventSource(`/_ket/stream/${encodeURIComponent(id)}`)\n // What the chunk says is the deployment's business. That something was said\n // is the whole signal: the screen is server-rendered, so the way to find out\n // what changed is to ask for it.\n const refresh = () => navigateTo(location.href, navigation)\n source.onmessage = refresh\n source.addEventListener('done', () => {\n close()\n refresh()\n })\n // EventSource reconnects on its own, and a stream that has ended answers 404\n // rather than reopening. Leaving the error alone would retry that forever.\n source.onerror = () => {\n if (source?.readyState === EventSource.CLOSED) close()\n }\n }\n\n sync()\n const observer = new MutationObserver(sync)\n observer.observe(document.body, { childList: true, subtree: true })\n addEventListener('pagehide', close, { signal })\n return () => {\n observer.disconnect()\n close()\n }\n}\n\nconst installRouteModal = (signal: AbortSignal, navigation: BrowserNavigation): (() => void) => {\n const focused = new WeakSet<HTMLElement>()\n const focusModal = (): void => {\n const modal = activeRouteModal()\n if (!modal || focused.has(modal)) return\n focused.add(modal)\n requestAnimationFrame(() => {\n const sheet = modal.querySelector<HTMLElement>('[data-ui=\"modal-sheet\"]')\n const target = modalFocusables(modal).find((item) => !item.matches('[data-ui=\"modal-close\"]'))\n ;(target ?? sheet)?.focus()\n })\n }\n focusModal()\n const observer = new MutationObserver(focusModal)\n observer.observe(document.body, { childList: true, subtree: true })\n\n // The backdrop and the X are ordinary links, so the guard has to run before\n // the navigation layer sees the click rather than inside it.\n document.addEventListener(\n 'click',\n (event) => {\n const modal = activeRouteModal()\n if (!modal) return\n // A tab inside a modal is a third way out of it. It looks like a control\n // that stays on the screen, which is exactly why losing a half-written\n // note to one went unnoticed: the reader never meant to leave. It is a\n // link like the other two, and it leaves the same way.\n const leaving = event\n .composedPath()\n .find(\n (target) =>\n target instanceof HTMLElement &&\n (target.matches('[data-ui=\"modal-close\"], [data-ui=\"modal-backdrop\"], a[data-ui=\"tab\"]') ||\n target.matches('a[data-ui=\"action\"][href]')),\n )\n if (!leaving || !modal.contains(leaving as HTMLElement)) return\n if (!mayLeaveModal(modal)) {\n event.preventDefault()\n event.stopPropagation()\n }\n },\n { capture: true, signal },\n )\n\n document.addEventListener(\n 'click',\n (event) => {\n const container = document.querySelector<HTMLElement>('[data-save-before-navigation]')\n const form =\n container instanceof HTMLFormElement ? container : container?.querySelector<HTMLFormElement>('form')\n const anchor = eventElement(event)?.closest<HTMLAnchorElement>('a[href]')\n if (!container || !form || !anchor || activeRouteModal() || !modalHasDraft(container)) return\n event.preventDefault()\n event.stopPropagation()\n if (!browserGlobals.confirm(container.dataset.saveBeforeNavigation)) return\n let destination = form.querySelector<HTMLInputElement>('input[name=\"returnTo\"]')\n if (!destination) {\n destination = document.createElement('input')\n destination.type = 'hidden'\n destination.name = 'returnTo'\n form.append(destination)\n }\n destination.value = anchor.href\n form.requestSubmit()\n },\n { capture: true, signal },\n )\n document.addEventListener(\n 'keydown',\n (event) => {\n const modal = activeRouteModal()\n if (!modal) return\n // Relation-select removes its dialog synchronously on Escape. The event's\n // composed path is stable even after that DOM subtree disappears, so the\n // route layer must use it instead of querying the now-removed target.\n if (eventStartedInNestedModal(event, modal)) return\n if (event.key === 'Escape') {\n const close = modal.querySelector<HTMLAnchorElement>('[data-ui=\"modal-close\"]')\n if (!close) return\n event.preventDefault()\n if (!mayLeaveModal(modal)) return\n navigateTo(close.getAttribute('href') ?? close.href, navigation)\n return\n }\n if (event.key !== 'Tab') return\n const items = modalFocusables(modal)\n if (!items.length) return\n const first = items[0]\n const last = items.at(-1)!\n if (event.shiftKey && document.activeElement === first) {\n event.preventDefault()\n last.focus()\n } else if (!event.shiftKey && document.activeElement === last) {\n event.preventDefault()\n first.focus()\n }\n },\n { signal },\n )\n return () => observer.disconnect()\n}\n\nexport const backendShell: BrowserBehavior = ({ navigation, lifetime }) => {\n const cleanupUserWorkflow = installUserWorkflow(lifetime)\n installThemeToggle(lifetime)\n installTableSelection(lifetime, navigation)\n installDropdownDismiss(lifetime)\n installGlobalFilter(lifetime)\n const cleanupRouteModal = installRouteModal(lifetime, navigation)\n const cleanupLiveRegion = installLiveRegion(lifetime, navigation)\n return () => {\n cleanupLiveRegion?.()\n cleanupRouteModal()\n cleanupUserWorkflow()\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": ["installUserWorkflow", "lifetime", "abort", "timer", "sequence", "check", "form", "input", "status", "request", "value", "next", "retry", "paint", "message", "valid", "response", "result", "scope", "changed", "company", "branch", "kind", "option", "key", "assigned", "row", "assignedLabel", "description", "companyField", "branchField", "scan", "event", "n", "empty", "target", "observer", "themeStorageKey", "dismissibleDropdown", "browserGlobals", "eventElement", "event", "themePreference", "value", "storedTheme", "saveTheme", "theme", "resolvedTheme", "syncThemeToggle", "pressed", "control", "installThemeToggle", "signal", "preference", "next", "updateSelection", "table", "rows", "checked", "input", "all", "shell", "menu", "installTableSelection", "navigation", "target", "href", "navigateTo", "openDropdowns", "installDropdownDismiss", "current", "dropdown", "dropdowns", "focused", "installGlobalFilter", "trigger", "controls", "dialog", "focusable", "activeRouteModal", "modalFocusables", "modal", "item", "eventStartedInNestedModal", "routeModal", "modalHasDraft", "field", "area", "select", "option", "mayLeaveModal", "message", "installLiveRegion", "watched", "source", "close", "sync", "region", "id", "refresh", "observer", "installRouteModal", "focusModal", "sheet", "leaving", "container", "form", "anchor", "destination", "items", "first", "last", "backendShell", "lifetime", "cleanupUserWorkflow", "installUserWorkflow", "cleanupRouteModal", "cleanupLiveRegion"]
|
|
3
|
+
"sources": ["../../../../ui/client/user-workflow.ts", "../../../../../../design-system/src/primitives/actions/index.tsx", "../../../../../../design-system/src/primitives/status/index.tsx", "../../../../../../design-system/src/primitives/feedback/index.tsx", "../../../../../../design-system/src/primitives/field/index.tsx", "../../../../../../design-system/src/layouts/layout/index.tsx", "../../../../../../design-system/src/layouts/shell/index.tsx", "../../../../../../design-system/src/primitives/field/native-control.tsx", "../../../../../../design-system/src/primitives/navigation/index.tsx", "../../../../../../design-system/src/primitives/progress/index.tsx", "../../../../../../design-system/src/interactions/menu/index.tsx", "../../../../../../design-system/src/interactions/popover/index.tsx", "../../../../../../design-system/src/interactions/tooltip/index.tsx", "../../../../../../design-system/src/patterns/modal-sheet/index.tsx", "../../../../../../design-system/src/interactions/dialog/index.tsx", "../../../../../../design-system/src/interactions/toast/index.tsx", "../../../../../../design-system/src/interactions/spinner/index.tsx", "../../../../../../design-system/src/interactions/skeleton/index.tsx", "../../../../../../design-system/src/runtime/index.js", "../../../../../../design-system/src/forms/shared.tsx", "../../../../../../design-system/src/forms/scalar-fields/index.tsx", "../../../../../../design-system/src/forms/combobox/index.tsx", "../../../../../../design-system/src/forms/date-time/index.tsx", "../../../../../../design-system/src/forms/upload/index.tsx", "../../../../../../design-system/src/forms/relation-picker/index.tsx", "../../../../../../design-system/src/data-operations/list-controls/index.tsx", "../../../../../../design-system/src/data-operations/inline-edit/index.tsx", "../../../../../../design-system/src/data-display/resource-list/index.tsx", "../../../../../../design-system/src/patterns/list-chrome/index.tsx", "../../../../../../design-system/src/data-display/data-grid/index.tsx", "../../../../../../design-system/src/data-display/tree/index.tsx", "../../../../../../design-system/src/record/display/index.tsx", "../../../../../../design-system/src/record/formatted-values/index.tsx", "../../../../../../design-system/src/record/composition/index.tsx", "../../../../../../design-system/src/record/activity/index.tsx", "../../../../../../design-system/src/record/media/index.tsx", "../../../../../../design-system/src/layouts/app-navigation/index.tsx", "../../../../../../design-system/src/patterns/data-table/index.tsx", "../../../../../../design-system/src/patterns/page-shell/index.tsx", "../../../../../../design-system/src/patterns/list-page/index.tsx", "../../../../../../design-system/src/patterns/record-page/index.tsx", "../../../../../../design-system/src/patterns/form-page/index.tsx", "../../../../../../design-system/src/patterns/workspace-page/index.tsx", "../../../../../../design-system/src/patterns/dashboard-page/index.tsx", "../../../../../../design-system/src/patterns/board-page/index.tsx", "../../../../../../design-system/src/patterns/pipeline/index.tsx", "../../../../../../design-system/src/patterns/record-form/index.tsx", "../../../../../../design-system/src/contract/index.ts", "../../../../ui/client/table-selection-view.tsx"],
|
|
4
|
+
"sourcesContent": ["export function installUserWorkflow(lifetime: AbortSignal): () => void {\n let abort: AbortController | undefined,\n timer: ReturnType<typeof setTimeout> | undefined,\n sequence = 0\n async function check(form: HTMLFormElement) {\n const input = form.querySelector<HTMLInputElement>('input[type=email]'),\n status = form.querySelector<HTMLElement>('[data-email-status]')\n if (!input || !status) return\n abort?.abort()\n clearTimeout(timer)\n const request = ++sequence\n const value = input.value.trim(),\n next = form.querySelector<HTMLButtonElement>('[data-email-next]'),\n retry = form.querySelector<HTMLButtonElement>('[data-email-retry]')\n const paint = (message: string, valid: boolean) => {\n status.textContent = message\n if (next) next.disabled = !valid\n input.setAttribute('aria-invalid', String(!valid))\n form.dataset.emailAvailable = valid ? 'true' : 'false'\n }\n if (retry) retry.hidden = true\n if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value)) {\n paint(value ? 'Email ch\u01B0a \u0111\u00FAng \u0111\u1ECBnh d\u1EA1ng.' : 'Nh\u1EADp email \u0111\u1EC3 ki\u1EC3m tra.', false)\n return\n }\n paint('\u0110ang ki\u1EC3m tra email\u2026', false)\n timer = setTimeout(async () => {\n abort = new AbortController()\n try {\n const response = await fetch(form.dataset.emailCheck!, {\n method: 'POST',\n cache: 'no-store',\n headers: { 'content-type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({ email: value }),\n signal: AbortSignal.any([abort.signal, AbortSignal.timeout(10000)]),\n })\n if (!response.ok) throw new Error('email-check-failed')\n const result = (await response.json()) as { available: boolean; ok: boolean }\n if (request !== sequence || !form.isConnected) return\n if (!result.ok) throw new Error('email-check-failed')\n paint(\n result.available ? 'Email c\u00F3 th\u1EC3 s\u1EED d\u1EE5ng.' : 'Email \u0111\u00E3 \u0111\u01B0\u1EE3c s\u1EED d\u1EE5ng. Nh\u1EADp email kh\u00E1c.',\n result.available,\n )\n } catch {\n if (request !== sequence || !form.isConnected) return\n paint('Ch\u01B0a ki\u1EC3m tra \u0111\u01B0\u1EE3c email. Vui l\u00F2ng th\u1EED l\u1EA1i.', false)\n if (retry) retry.hidden = false\n }\n }, 400)\n }\n function scope(form: HTMLFormElement, changed = false) {\n const company = form.querySelector<HTMLSelectElement>('select[name=\"companyId\"]'),\n branch = form.querySelector<HTMLSelectElement>('select[name=\"branchId\"]'),\n kind = form.querySelector<HTMLSelectElement>('select[name=\"scopeKind\"]')?.value\n if (branch && company) {\n if (changed) branch.value = ''\n for (const option of branch.options) {\n if (option.value) {\n option.hidden = option.dataset.companyId !== company.value\n option.disabled = option.hidden\n }\n }\n }\n const key =\n kind === 'tenant'\n ? 'tenant'\n : kind === 'branch'\n ? `branch:${company?.value}:${branch?.value}`\n : `company:${company?.value}`\n for (const input of form.querySelectorAll<HTMLInputElement>('[data-assigned-scopes]')) {\n const scopes = JSON.parse(input.dataset.assignedScopes ?? '[]') as string[]\n const assigned = scopes.includes(key)\n input.disabled = input.dataset.roleDisabled === 'true' || assigned\n const row = input.closest('[data-role-option]')\n const assignedLabel = row?.querySelector<HTMLElement>('[data-role-assigned]')\n const description = row?.querySelector<HTMLElement>('[data-role-description]')\n if (assignedLabel) assignedLabel.hidden = !assigned\n if (description) description.hidden = assigned\n if (input.disabled) input.checked = false\n }\n const companyField = company?.closest<HTMLElement>('[data-ui=\"form-field\"]')\n const branchField = branch?.closest<HTMLElement>('[data-ui=\"form-field\"]')\n if (companyField) companyField.hidden = kind === 'tenant'\n if (branchField) branchField.hidden = kind === 'tenant' || kind === 'company'\n }\n function scan() {\n for (const form of document.querySelectorAll<HTMLFormElement>('form[data-user-workflow]'))\n if (!form.dataset.initialized) {\n form.dataset.initialized = 'true'\n scope(form)\n void check(form)\n }\n }\n document.addEventListener(\n 'input',\n (event) => {\n const input = event.target as HTMLInputElement,\n form = input.closest<HTMLFormElement>('form[data-user-workflow]')\n if (!form) return\n if (input.type === 'email') void check(form)\n if (input.matches('[data-role-search]')) {\n let n = 0\n for (const row of form.querySelectorAll<HTMLElement>('[data-role-option]')) {\n row.hidden = !row.textContent?.toLocaleLowerCase('vi').includes(input.value.toLocaleLowerCase('vi'))\n if (!row.hidden) n++\n }\n const empty = form.querySelector<HTMLElement>('[data-role-empty]')\n if (empty) empty.hidden = n > 0\n }\n },\n { signal: lifetime },\n )\n document.addEventListener(\n 'change',\n (event) => {\n const input = event.target as HTMLElement,\n form = input.closest<HTMLFormElement>('form[data-user-workflow]')\n if (form && input.matches('select[name=\"companyId\"],select[name=\"branchId\"],select[name=\"scopeKind\"]'))\n scope(form, input.matches('select[name=\"companyId\"]'))\n },\n { signal: lifetime },\n )\n document.addEventListener(\n 'click',\n (event) => {\n const target = event.target as HTMLElement\n if (target.matches('[data-email-retry]')) void check(target.closest('form')!)\n },\n { signal: lifetime },\n )\n document.addEventListener(\n 'submit',\n (event) => {\n const form = event.target as HTMLFormElement\n if (\n form.matches('form[data-user-workflow]') &&\n form.querySelector('input[type=email]') &&\n form.dataset.emailAvailable !== 'true'\n )\n event.preventDefault()\n },\n { signal: lifetime },\n )\n const observer = new MutationObserver(scan)\n observer.observe(document.documentElement, { childList: true, subtree: true })\n scan()\n return () => {\n abort?.abort()\n clearTimeout(timer)\n observer.disconnect()\n }\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['action', 'action-leading', 'action-label', 'action-spinner', 'action-group'] as const\n\nexport type ActionVariant = 'primary' | 'secondary' | 'tertiary' | 'destructive'\nexport type ActionSize = 'compact' | 'default' | 'prominent'\n\ntype ActionBase = {\n label: string\n variant?: ActionVariant\n size?: ActionSize\n leading?: JSXChild\n disabled?: boolean\n loading?: boolean\n describedBy?: string | null\n}\n\nexport type ButtonProps = ActionBase & {\n type?: 'button' | 'submit' | 'reset'\n name?: string | null\n value?: string | null\n form?: string | null\n}\n\nexport type LinkButtonProps = ActionBase & { href: string }\nexport type IconButtonProps = Omit<ButtonProps, 'leading'> & {\n icon: JSXChild\n pressed?: boolean\n}\n\nconst ActionContent = (props: ActionBase & { iconOnly?: boolean }): TemplateResult => (\n <>\n {props.loading === true && <span data-ui=\"action-spinner\" aria-hidden=\"true\" />}\n {props.loading !== true && props.leading !== undefined && (\n <span data-ui=\"action-leading\" aria-hidden=\"true\">\n {props.leading}\n </span>\n )}\n {!props.iconOnly && <span data-ui=\"action-label\">{props.label}</span>}\n </>\n)\n\nexport const Button = (props: ButtonProps): TemplateResult => (\n <button\n data-ui=\"action\"\n data-variant={props.variant ?? 'secondary'}\n data-size={props.size ?? 'default'}\n type={props.type ?? 'button'}\n name={props.name ?? null}\n value={props.value ?? null}\n form={props.form ?? null}\n disabled={props.disabled === true || props.loading === true}\n aria-busy={props.loading === true ? 'true' : null}\n aria-describedby={props.describedBy ?? null}\n >\n <ActionContent {...props} />\n </button>\n)\n\n/** An icon-only action with a visible tooltip and accessible name. */\nexport const IconButton = (props: IconButtonProps): TemplateResult => (\n <button\n data-ui=\"action\"\n data-icon-only=\"true\"\n data-variant={props.variant ?? 'tertiary'}\n data-size={props.size ?? 'default'}\n type={props.type ?? 'button'}\n name={props.name ?? null}\n value={props.value ?? null}\n form={props.form ?? null}\n disabled={props.disabled === true || props.loading === true}\n aria-label={props.label}\n aria-pressed={props.pressed === undefined ? null : String(props.pressed)}\n aria-busy={props.loading === true ? 'true' : null}\n aria-describedby={props.describedBy ?? null}\n title={props.label}\n >\n <ActionContent {...props} leading={props.icon} iconOnly />\n </button>\n)\n\nexport const LinkButton = (props: LinkButtonProps): TemplateResult =>\n props.disabled || props.loading ? (\n <button\n data-ui=\"action\"\n data-variant={props.variant ?? 'secondary'}\n data-size={props.size ?? 'default'}\n type=\"button\"\n disabled\n aria-busy={props.loading === true ? 'true' : null}\n aria-describedby={props.describedBy ?? null}\n >\n <ActionContent {...props} />\n </button>\n ) : (\n <a\n data-ui=\"action\"\n data-variant={props.variant ?? 'secondary'}\n data-size={props.size ?? 'default'}\n href={props.href}\n aria-describedby={props.describedBy ?? null}\n >\n <ActionContent {...props} />\n </a>\n )\n\nexport const ActionGroup = (props: {\n actions: readonly JSXChild[]\n label?: string | null\n}): TemplateResult => (\n <div data-ui=\"action-group\" role=\"group\" aria-label={props.label ?? null}>\n {each(\n props.actions,\n (_, index) => index,\n (action) => (\n <>{action}</>\n ),\n )}\n </div>\n)\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['badge', 'tag', 'tag-remove', 'count-badge', 'avatar', 'code'] as const\n\nexport type Tone = 'neutral' | 'info' | 'positive' | 'warning' | 'danger'\n\nexport const Badge = (props: { label: string; tone?: Tone; value?: string }): TemplateResult => (\n <span data-ui=\"badge\" data-tone={props.tone ?? 'neutral'} data-value={props.value ?? ''}>\n {props.label}\n </span>\n)\n\nexport const Tag = (props: {\n label: string\n removeHref?: string | null\n removeLabel?: string | null\n}): TemplateResult => (\n <span data-ui=\"tag\">\n {props.label}\n {!!props.removeHref && (\n <a\n data-ui=\"tag-remove\"\n href={props.removeHref}\n aria-label={props.removeLabel ?? `Remove ${props.label}`}\n >\n \u00D7\n </a>\n )}\n </span>\n)\n\nexport const CountBadge = (props: { count: number; label: string }): TemplateResult => (\n <span data-ui=\"count-badge\" role=\"status\" aria-label={props.label}>\n {String(props.count)}\n </span>\n)\n\nexport const initials = (name: string): string => {\n const parts = name.trim().split(/\\s+/).filter(Boolean)\n if (!parts.length) return '?'\n const first = parts.length > 1 ? parts[parts.length - 1]! : parts[0]!\n const second = parts.length > 2 ? parts[parts.length - 2]! : ''\n return (second.slice(0, 1) + first.slice(0, 1)).toLocaleUpperCase('vi')\n}\n\nexport const Avatar = (props: { name: string; size?: 'small' | 'default' | 'large' }): TemplateResult => (\n <span data-ui=\"avatar\" data-size={props.size ?? 'default'} title={props.name} aria-hidden=\"true\">\n {initials(props.name)}\n </span>\n)\n\nexport const Code = (props: { value: string; context?: string | null }): TemplateResult => (\n <code data-ui=\"code\" data-context={props.context ?? null}>\n {props.value}\n </code>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = [\n 'notice',\n 'notice-mark',\n 'notice-copy',\n 'notice-title',\n 'notice-message',\n 'notice-actions',\n 'empty',\n 'empty-mark',\n 'empty-title',\n 'empty-message',\n 'empty-actions',\n 'loading',\n 'loading-label',\n] as const\n\nexport type NoticeTone = 'info' | 'positive' | 'warning' | 'danger'\n\nexport const Notice = (props: {\n title: string\n message: string\n tone?: NoticeTone\n actions?: JSXChild\n}): TemplateResult => (\n <aside\n data-ui=\"notice\"\n data-tone={props.tone ?? 'info'}\n role={props.tone === 'danger' ? 'alert' : 'status'}\n >\n <span data-ui=\"notice-mark\" aria-hidden=\"true\" />\n <div data-ui=\"notice-copy\">\n <p data-ui=\"notice-title\">{props.title}</p>\n <p data-ui=\"notice-message\">{props.message}</p>\n </div>\n {props.actions !== undefined && <div data-ui=\"notice-actions\">{props.actions}</div>}\n </aside>\n)\n\nexport const EmptyState = (props: { title: string; message: string; actions?: JSXChild }): TemplateResult => (\n <div data-ui=\"empty\" role=\"status\">\n <span data-ui=\"empty-mark\" aria-hidden=\"true\">\n \u25C7\n </span>\n <p data-ui=\"empty-title\">{props.title}</p>\n <p data-ui=\"empty-message\">{props.message}</p>\n {props.actions !== undefined && <div data-ui=\"empty-actions\">{props.actions}</div>}\n </div>\n)\n\nexport const LoadingState = (props: { label: string; lines?: number }): TemplateResult => (\n <div data-ui=\"loading\" role=\"status\" aria-live=\"polite\">\n <span data-ui=\"loading-label\">{props.label}</span>\n <div data-ui=\"skeleton\" aria-hidden=\"true\">\n {each(\n Array.from({ length: Math.max(1, props.lines ?? 3) }),\n (_, index) => index,\n (_, index) => (\n <span data-ui=\"skeleton-line\" data-line={String(index + 1)} />\n ),\n )}\n </div>\n </div>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { Disclosure } from '../../layouts/index.tsx'\nimport { NativeFieldControl } from './native-control.tsx'\n\nexport const HOOKS = [\n 'field',\n 'field-label',\n 'field-required',\n 'field-control',\n 'field-options',\n 'field-option',\n 'field-option-input',\n 'field-help',\n 'field-error',\n] as const\n\nexport type FieldOption = {\n value: string\n label: string\n name?: string\n checked?: boolean\n disabled?: boolean\n}\nexport type FieldProps = {\n /** Nested fields for a collapsible group inside a larger record form. */\n fields?: readonly FieldProps[]\n open?: boolean\n\n id: string\n name: string\n label: string\n /** A trusted, progressively enhanced control such as a relation selector. */\n control?: JSXChild\n type?:\n | 'text'\n | 'email'\n | 'tel'\n | 'number'\n | 'decimal'\n | 'password'\n | 'time'\n | 'color'\n | 'date'\n | 'datetime-local'\n | 'month'\n | 'week'\n | 'select'\n | 'textarea'\n | 'checkbox'\n | 'checkbox-group'\n | 'radio'\n value?: string | number | boolean | null\n placeholder?: string | null\n required?: boolean\n disabled?: boolean\n readOnly?: boolean\n min?: string | number\n max?: string | number\n help?: string | null\n error?: string | null\n options?: readonly FieldOption[]\n span?: 'half' | 'full'\n step?: string | null\n autocomplete?: string | null\n}\n\nconst hasError = (props: FieldProps): boolean => !!props.error || (props.fields?.some(hasError) ?? false)\n\nexport const Field = (props: FieldProps): TemplateResult => {\n const helpId = props.help ? `${props.id}-help` : null\n const errorId = props.error ? `${props.id}-error` : null\n const describedBy = [helpId, errorId].filter(Boolean).join(' ') || null\n const label = (\n <span data-ui=\"field-label\">\n {props.label}\n {props.required && (\n <span data-ui=\"field-required\" aria-hidden=\"true\">\n {' *'}\n </span>\n )}\n </span>\n )\n\n if (props.fields)\n return (\n <div data-ui=\"field\" data-kind=\"group\" data-span=\"full\" data-invalid={String(!!props.error)}>\n <Disclosure\n summary={props.label}\n open={props.open || hasError(props)}\n body={\n <div data-ui=\"form-grid\">\n {each(\n props.fields.map((field) => ({\n ...field,\n disabled: props.disabled || field.disabled,\n readOnly: props.readOnly || field.readOnly,\n })),\n (field) => field.id,\n (field) => (\n <Field {...field} />\n ),\n )}\n </div>\n }\n />\n {!!props.help && (\n <small data-ui=\"field-help\" id={helpId ?? undefined}>\n {props.help}\n </small>\n )}\n {!!props.error && (\n <small data-ui=\"field-error\" id={errorId ?? undefined}>\n {props.error}\n </small>\n )}\n </div>\n )\n\n if (props.type === 'checkbox-group')\n return (\n <div\n data-ui=\"field\"\n data-kind=\"checkbox-group\"\n data-span={props.span ?? 'half'}\n data-invalid={String(!!props.error)}\n >\n <span data-ui=\"field-label\" id={`${props.id}-label`}>\n {props.label}\n {props.required && (\n <span data-ui=\"field-required\" aria-hidden=\"true\">\n {' *'}\n </span>\n )}\n </span>\n <div data-ui=\"field-options\" role=\"group\" aria-labelledby={`${props.id}-label`}>\n {each(\n props.options ?? [],\n (option) => option.name ?? option.value,\n (option) => (\n <label data-ui=\"field-option\">\n <input\n data-ui=\"field-option-input\"\n type=\"checkbox\"\n name={option.name ?? `${props.name}[]`}\n value={option.value}\n checked={option.checked === true}\n disabled={props.disabled === true || option.disabled === true}\n aria-invalid={props.error ? 'true' : null}\n aria-describedby={describedBy}\n autocomplete=\"off\"\n />\n <span>{option.label}</span>\n </label>\n ),\n )}\n </div>\n {!!props.help && (\n <small data-ui=\"field-help\" id={helpId ?? undefined}>\n {props.help}\n </small>\n )}\n {!!props.error && (\n <small data-ui=\"field-error\" id={errorId ?? undefined}>\n {props.error}\n </small>\n )}\n </div>\n )\n\n if (props.type === 'radio')\n return (\n <div\n data-ui=\"field\"\n data-kind=\"radio\"\n data-span={props.span ?? 'half'}\n data-invalid={String(!!props.error)}\n >\n <span data-ui=\"field-label\" id={`${props.id}-label`}>\n {props.label}\n {props.required && (\n <span data-ui=\"field-required\" aria-hidden=\"true\">\n {' *'}\n </span>\n )}\n </span>\n <div data-ui=\"field-options\" role=\"radiogroup\" aria-labelledby={`${props.id}-label`}>\n {each(\n props.options ?? [],\n (option) => option.value,\n (option) => (\n <label data-ui=\"field-option\">\n <input\n data-ui=\"field-option-input\"\n type=\"radio\"\n name={props.name}\n value={option.value}\n checked={String(props.value ?? '') === option.value}\n required={props.required === true}\n disabled={props.disabled === true || option.disabled === true}\n aria-invalid={props.error ? 'true' : null}\n aria-describedby={describedBy}\n autocomplete=\"off\"\n />\n <span>{option.label}</span>\n </label>\n ),\n )}\n </div>\n {!!props.help && (\n <small data-ui=\"field-help\" id={helpId ?? undefined}>\n {props.help}\n </small>\n )}\n {!!props.error && (\n <small data-ui=\"field-error\" id={errorId ?? undefined}>\n {props.error}\n </small>\n )}\n </div>\n )\n\n return (\n <label\n data-ui=\"field\"\n data-kind={props.type ?? 'text'}\n data-span={props.span ?? 'half'}\n data-invalid={String(!!props.error)}\n for={props.id}\n >\n {props.type === 'checkbox' && NativeFieldControl(props, describedBy)}\n {label}\n {props.type !== 'checkbox' && NativeFieldControl(props, describedBy)}\n {!!props.help && (\n <small data-ui=\"field-help\" id={helpId ?? undefined}>\n {props.help}\n </small>\n )}\n {!!props.error && (\n <small data-ui=\"field-error\" id={errorId ?? undefined}>\n {props.error}\n </small>\n )}\n </label>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport type { Tone } from '../../primitives/status.tsx'\n\nexport const HOOKS = [\n 'stack',\n 'inline',\n 'grid',\n 'surface',\n 'surface-head',\n 'surface-title',\n 'surface-actions',\n 'disclosure',\n 'disclosure-summary',\n 'disclosure-body',\n 'section',\n 'section-head',\n 'section-eyebrow',\n 'section-title',\n 'section-description',\n 'section-actions',\n 'section-body',\n 'content-card',\n 'card-head',\n 'card-title',\n 'card-summary',\n 'card-body',\n 'card-meta',\n 'card-actions',\n 'metric',\n 'metric-icon',\n 'metric-label',\n 'metric-value',\n 'metric-detail',\n] as const\n\nexport { AppShell, Page, PageHeader, RecordCanvas, RecordSection } from '../shell.tsx'\n\nconst Items = (props: { items: readonly JSXChild[] }): TemplateResult => (\n <>\n {each(\n props.items,\n (_, index) => index,\n // biome-ignore lint/complexity/noUselessFragments: `each` requires a TemplateResult callback.\n (item) => (typeof item === 'string' || typeof item === 'number' ? <span>{item}</span> : <>{item}</>),\n )}\n </>\n)\n\nexport const Stack = (props: {\n items: readonly JSXChild[]\n gap?: 'compact' | 'default' | 'loose'\n}): TemplateResult => (\n <div data-ui=\"stack\" data-gap={props.gap ?? 'default'}>\n <Items items={props.items} />\n </div>\n)\n\nexport const Inline = (props: { items: readonly JSXChild[] }): TemplateResult => (\n <div data-ui=\"inline\">\n <Items items={props.items} />\n </div>\n)\n\nexport const Grid = (props: { items: readonly JSXChild[]; columns?: 2 | 3 | 4 }): TemplateResult => (\n <div data-ui=\"grid\" data-columns={String(props.columns ?? 3)}>\n <Items items={props.items} />\n </div>\n)\n\nexport const Surface = (props: {\n body: JSXChild\n title?: string\n actions?: JSXChild\n tone?: 'default' | 'subtle' | 'raised'\n padding?: 'none' | 'compact' | 'default'\n}): TemplateResult => (\n <div\n data-ui=\"surface\"\n data-tone={props.tone ?? 'default'}\n data-padding={props.padding ?? 'default'}\n data-has-heading={props.title ? 'true' : null}\n >\n {props.title && (\n <header data-ui=\"surface-head\">\n <h2 data-ui=\"surface-title\">{props.title}</h2>\n {props.actions !== undefined && <div data-ui=\"surface-actions\">{props.actions}</div>}\n </header>\n )}\n {props.body}\n </div>\n)\n\n/** Native, keyboard-accessible progressive disclosure for dense secondary detail. */\nexport const Disclosure = (props: { summary: string; body: JSXChild; open?: boolean }): TemplateResult => (\n <details data-ui=\"disclosure\" open={props.open === true ? true : undefined}>\n <summary data-ui=\"disclosure-summary\">{props.summary}</summary>\n <div data-ui=\"disclosure-body\">{props.body}</div>\n </details>\n)\n\nexport const Section = (props: {\n title: string\n body: JSXChild\n description?: string | null\n eyebrow?: string | null\n actions?: JSXChild\n}): TemplateResult => (\n <section data-ui=\"section\">\n <header data-ui=\"section-head\">\n <div>\n {!!props.eyebrow && <p data-ui=\"section-eyebrow\">{props.eyebrow}</p>}\n <h2 data-ui=\"section-title\">{props.title}</h2>\n {!!props.description && <p data-ui=\"section-description\">{props.description}</p>}\n </div>\n {props.actions !== undefined && <div data-ui=\"section-actions\">{props.actions}</div>}\n </header>\n <div data-ui=\"section-body\">{props.body}</div>\n </section>\n)\n\nexport const ContentCard = (props: {\n title: string\n summary?: string | null\n body?: JSXChild\n meta?: JSXChild\n actions?: JSXChild\n href?: string | null\n selected?: boolean\n}): TemplateResult => (\n <article\n data-ui=\"content-card\"\n data-interactive={String(!!props.href)}\n data-selected={String(props.selected === true)}\n >\n <header data-ui=\"card-head\">\n <h3 data-ui=\"card-title\">{props.href ? <a href={props.href}>{props.title}</a> : props.title}</h3>\n {!!props.summary && <p data-ui=\"card-summary\">{props.summary}</p>}\n </header>\n {props.body !== undefined && <div data-ui=\"card-body\">{props.body}</div>}\n {props.meta !== undefined && <div data-ui=\"card-meta\">{props.meta}</div>}\n {props.actions !== undefined && <div data-ui=\"card-actions\">{props.actions}</div>}\n </article>\n)\n\n/**\n * One number, with the context that makes it mean something.\n *\n * `tone` colours a dot beside the label and nothing else. The number itself stays\n * in the page's ink: a KPI row is read by comparing figures, and four differently\n * coloured figures are compared by colour instead. The dot is drawn only when a\n * tone is passed, so a metric that has no state to report does not grow a grey\n * bullet that means nothing.\n *\n * `href` makes the whole card the link rather than the label, because the card is\n * what the pointer is already over. There is nothing else inside a metric to\n * click, which is exactly why `ContentCard` does the opposite.\n */\nexport const Metric = (props: {\n label: string\n value: string | number\n detail?: string | null\n tone?: Tone\n /**\n * A glyph naming the quantity, in a tile beside it. Decorative: the label is\n * the name of the number, so this is hidden from assistive technology.\n */\n icon?: JSXChild\n href?: string | null\n}): TemplateResult => {\n const attributes = {\n 'data-ui': 'metric',\n 'data-tone': props.tone ?? 'neutral',\n // One marker to a card, never two. The dot reports state for a metric that\n // has no glyph; where there is a glyph, the glyph is already the thing the\n // eye lands on, and a coloured bullet beside the label competes with it.\n 'data-dot': String(props.tone !== undefined && props.icon === undefined),\n }\n const body = (\n <>\n {props.icon !== undefined && (\n <span data-ui=\"metric-icon\" aria-hidden=\"true\">\n {props.icon}\n </span>\n )}\n <p data-ui=\"metric-label\">{props.label}</p>\n <p data-ui=\"metric-value\">{String(props.value)}</p>\n {!!props.detail && <p data-ui=\"metric-detail\">{props.detail}</p>}\n </>\n )\n return props.href ? (\n <a {...attributes} data-interactive=\"true\" href={props.href}>\n {body}\n </a>\n ) : (\n <article {...attributes} data-interactive=\"false\">\n {body}\n </article>\n )\n}\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = [\n 'app-shell',\n 'app-sidebar',\n 'app-main',\n 'app-right-rail',\n 'page',\n 'page-header',\n 'page-heading',\n 'page-title',\n 'page-description',\n 'page-actions',\n 'page-body',\n 'record-canvas',\n 'record-content',\n 'record-section',\n 'record-section-title',\n] as const\n\nexport const AppShell = (props: {\n sidebar: JSXChild\n main: JSXChild\n rightRail?: JSXChild\n mode?: 'viewport' | 'embedded'\n}): TemplateResult => (\n <div\n data-ui=\"app-shell\"\n data-has-right-rail={String(props.rightRail !== undefined)}\n data-mode={props.mode ?? 'viewport'}\n >\n <aside data-ui=\"app-sidebar\">{props.sidebar}</aside>\n {props.mode === 'embedded' ? (\n <div data-ui=\"app-main\">{props.main}</div>\n ) : (\n <main data-ui=\"app-main\">{props.main}</main>\n )}\n {props.rightRail !== undefined && <aside data-ui=\"app-right-rail\">{props.rightRail}</aside>}\n </div>\n)\n\nexport const PageHeader = (props: {\n title: string\n description?: string | null\n actions?: JSXChild\n}): TemplateResult => (\n <header data-ui=\"page-header\">\n <div data-ui=\"page-heading\">\n <h1 data-ui=\"page-title\">{props.title}</h1>\n {!!props.description && <p data-ui=\"page-description\">{props.description}</p>}\n </div>\n {props.actions !== undefined && <div data-ui=\"page-actions\">{props.actions}</div>}\n </header>\n)\n\nexport const Page = (props: {\n title: string\n body: JSXChild\n description?: string | null\n actions?: JSXChild\n}): TemplateResult => (\n <section data-ui=\"page\">\n <PageHeader title={props.title} description={props.description} actions={props.actions} />\n <div data-ui=\"page-body\">{props.body}</div>\n </section>\n)\n\nexport const RecordCanvas = (props: { body: JSXChild }): TemplateResult => (\n <div data-ui=\"record-canvas\">\n <div data-ui=\"record-content\">{props.body}</div>\n </div>\n)\n\nexport const RecordSection = (props: { body: JSXChild; title?: string | null }): TemplateResult => (\n <section data-ui=\"record-section\">\n {!!props.title && <h2 data-ui=\"record-section-title\">{props.title}</h2>}\n {props.body}\n </section>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport type { FieldProps } from './index.tsx'\n\nexport const NativeFieldControl = (props: FieldProps, describedBy: string | null): TemplateResult => {\n if (props.control !== undefined) return <>{props.control}</>\n if (props.type === 'textarea')\n return (\n <textarea\n data-ui=\"field-control\"\n id={props.id}\n name={props.name}\n placeholder={props.placeholder ?? null}\n required={props.required === true}\n disabled={props.disabled === true}\n readonly={props.readOnly === true}\n aria-invalid={props.error ? 'true' : null}\n aria-describedby={describedBy}\n >\n {String(props.value ?? '')}\n </textarea>\n )\n if (props.type === 'select')\n return (\n <select\n data-ui=\"field-control\"\n id={props.id}\n name={props.name}\n required={props.required === true}\n disabled={props.disabled === true}\n aria-invalid={props.error ? 'true' : null}\n aria-describedby={describedBy}\n >\n {each(\n props.options ?? [],\n (option) => option.value,\n (option) => (\n <option\n value={option.value}\n selected={String(props.value ?? '') === option.value}\n disabled={option.disabled === true}\n >\n {option.label}\n </option>\n ),\n )}\n </select>\n )\n return (\n <input\n data-ui=\"field-control\"\n id={props.id}\n type={props.type === 'decimal' ? 'number' : (props.type ?? 'text')}\n name={props.name}\n value={props.type === 'checkbox' ? '1' : String(props.value ?? '')}\n checked={props.type === 'checkbox' && (props.value === true || props.value === '1')}\n placeholder={props.placeholder ?? null}\n required={props.required === true}\n disabled={props.disabled === true}\n readonly={props.readOnly === true}\n min={props.min}\n max={props.max}\n aria-invalid={props.error ? 'true' : null}\n aria-describedby={describedBy}\n autocomplete={props.autocomplete ?? 'off'}\n step={props.type === 'decimal' ? (props.step ?? 'any') : (props.step ?? null)}\n />\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = [\n 'breadcrumbs',\n 'breadcrumb',\n 'nav-list',\n 'nav-item',\n 'nav-item-leading',\n 'nav-item-label',\n 'nav-item-count',\n 'tabs',\n 'tab',\n 'tab-count',\n] as const\n\nexport type BreadcrumbItem = {\n id?: string\n label: string\n href?: string\n}\n\nexport const Breadcrumbs = (props: { label: string; items: readonly BreadcrumbItem[] }): TemplateResult => (\n <nav data-ui=\"breadcrumbs\" aria-label={props.label}>\n <ol>\n {each(\n props.items,\n (item, index) => item.id ?? `${index}:${item.label}`,\n (item, index) => (\n <li data-ui=\"breadcrumb\">\n {item.href !== undefined && index < props.items.length - 1 ? (\n <a href={item.href}>{item.label}</a>\n ) : (\n <span aria-current={index === props.items.length - 1 ? 'page' : null}>{item.label}</span>\n )}\n </li>\n ),\n )}\n </ol>\n </nav>\n)\n\nexport type NavItemProps = {\n label: string\n href: string\n leading?: JSXChild\n count?: number\n active?: boolean\n}\n\nexport const NavItem = (props: NavItemProps): TemplateResult => (\n <a\n data-ui=\"nav-item\"\n data-active={props.active === true ? 'true' : null}\n href={props.href}\n aria-current={props.active === true ? 'page' : null}\n >\n {props.leading !== undefined && (\n <span data-ui=\"nav-item-leading\" aria-hidden=\"true\">\n {props.leading}\n </span>\n )}\n <span data-ui=\"nav-item-label\">{props.label}</span>\n {props.count !== undefined && <span data-ui=\"nav-item-count\">{String(props.count)}</span>}\n </a>\n)\n\nexport const NavList = (props: { label: string; items: readonly NavItemProps[] }): TemplateResult => (\n <nav data-ui=\"nav-list\" aria-label={props.label}>\n {each(\n props.items,\n (item) => `${item.href}:${item.label}`,\n (item) => (\n <NavItem {...item} />\n ),\n )}\n </nav>\n)\n\nexport type TabItem = {\n id: string\n label: string\n href: string\n count?: number\n active?: boolean\n}\n\nexport const Tabs = (props: {\n label: string\n items: readonly TabItem[]\n extension?: JSXChild\n}): TemplateResult => (\n <nav data-ui=\"tabs\" aria-label={props.label}>\n {each(\n props.items,\n (item) => item.id,\n (item) => (\n <a\n data-ui=\"tab\"\n data-active={item.active === true ? 'true' : null}\n href={item.href}\n aria-current={item.active === true ? 'page' : null}\n >\n {item.label}\n {item.count !== undefined && <span data-ui=\"tab-count\">{String(item.count)}</span>}\n </a>\n ),\n )}\n {props.extension}\n </nav>\n)\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = [\n 'progress',\n 'progress-label',\n 'progress-value',\n 'progress-track',\n 'progress-bar',\n] as const\n\nexport type ProgressTone = 'primary' | 'positive' | 'warning' | 'danger'\n\nexport const Progress = (props: {\n value: number\n label: string\n tone?: ProgressTone\n showValue?: boolean\n}): TemplateResult => {\n const value = Number.isFinite(props.value) ? Math.max(0, Math.min(100, props.value)) : 0\n return (\n <div data-ui=\"progress\" data-tone={props.tone ?? 'primary'}>\n {(props.showValue ?? true) && (\n <div data-ui=\"progress-label\">\n <span>{props.label}</span>\n <span data-ui=\"progress-value\">{`${value}%`}</span>\n </div>\n )}\n <div\n data-ui=\"progress-track\"\n role=\"progressbar\"\n aria-label={props.label}\n aria-valuemin=\"0\"\n aria-valuemax=\"100\"\n aria-valuenow={String(value)}\n >\n <span data-ui=\"progress-bar\" style={`width: ${value}%`} />\n </div>\n </div>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['menu', 'menu-trigger', 'menu-panel', 'menu-item', 'menu-item-copy'] as const\n\nexport type MenuItem = {\n id: string\n label: string\n href?: string\n name?: string\n value?: string\n form?: string\n leading?: JSXChild\n description?: string\n disabled?: boolean\n destructive?: boolean\n}\n\nexport type MenuProps = {\n id: string\n label: string\n items: readonly MenuItem[]\n trigger?: JSXChild\n open?: boolean\n align?: 'start' | 'end'\n}\n\nexport const Menu = (props: MenuProps): TemplateResult => (\n <details data-ui=\"menu\" data-align={props.align ?? 'start'} open={props.open === true ? true : undefined}>\n <summary data-ui=\"menu-trigger\" aria-haspopup=\"menu\" aria-controls={`${props.id}-panel`}>\n {props.trigger ?? props.label}\n </summary>\n <div data-ui=\"menu-panel\" id={`${props.id}-panel`} role=\"menu\" aria-label={props.label}>\n {each(\n props.items,\n (item) => item.id,\n (item) => {\n const content = (\n <>\n {item.leading !== undefined && <span aria-hidden=\"true\">{item.leading}</span>}\n <span data-ui=\"menu-item-copy\">\n <span>{item.label}</span>\n {item.description && <small>{item.description}</small>}\n </span>\n </>\n )\n if (item.disabled)\n return (\n <span data-ui=\"menu-item\" role=\"menuitem\" aria-disabled=\"true\" tabIndex=\"-1\">\n {content}\n </span>\n )\n if (item.href)\n return (\n <a\n data-ui=\"menu-item\"\n data-destructive={item.destructive ? 'true' : null}\n role=\"menuitem\"\n href={item.href}\n >\n {content}\n </a>\n )\n return (\n <button\n data-ui=\"menu-item\"\n data-destructive={item.destructive ? 'true' : null}\n role=\"menuitem\"\n type=\"submit\"\n name={item.name ?? 'intent'}\n value={item.value ?? item.id}\n form={item.form ?? null}\n >\n {content}\n </button>\n )\n },\n )}\n </div>\n </details>\n)\n\nexport const ActionMenu = (props: Omit<MenuProps, 'trigger'> & { triggerLabel?: string }): TemplateResult => (\n <Menu\n {...props}\n trigger={<span>{props.triggerLabel ?? props.label} \u00B7\u00B7\u00B7</span>}\n align={props.align ?? 'end'}\n />\n)\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['popover', 'popover-trigger', 'popover-panel', 'popover-close'] as const\n\nexport type PopoverProps = {\n id: string\n label: string\n trigger: JSXChild\n body: JSXChild\n open: boolean\n openHref: string\n closeHref: string\n closeLabel: string\n placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end'\n}\n\nexport const Popover = (props: PopoverProps): TemplateResult => (\n <span data-ui=\"popover\" data-open={String(props.open)} data-placement={props.placement ?? 'bottom-start'}>\n <a\n data-ui=\"popover-trigger\"\n href={props.open ? props.closeHref : props.openHref}\n aria-haspopup=\"dialog\"\n aria-expanded={String(props.open)}\n aria-controls={`${props.id}-panel`}\n >\n {props.trigger}\n </a>\n {props.open && (\n <aside\n data-ui=\"popover-panel\"\n id={`${props.id}-panel`}\n role=\"dialog\"\n aria-label={props.label}\n tabindex=\"-1\"\n >\n <a data-ui=\"popover-close\" href={props.closeHref} aria-label={props.closeLabel}>\n \u00D7\n </a>\n {props.body}\n </aside>\n )}\n </span>\n)\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['tooltip', 'tooltip-trigger', 'tooltip-content'] as const\n\nexport const Tooltip = (props: {\n id: string\n text: string\n trigger: JSXChild\n placement?: 'top' | 'right' | 'bottom' | 'left'\n}): TemplateResult => (\n <span data-ui=\"tooltip\" data-placement={props.placement ?? 'top'}>\n <span data-ui=\"tooltip-trigger\" aria-describedby={props.id}>\n {props.trigger}\n </span>\n <span data-ui=\"tooltip-content\" id={props.id} role=\"tooltip\">\n {props.text}\n </span>\n </span>\n)\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = [\n 'modal-layer',\n 'modal-backdrop',\n 'modal-sheet',\n 'modal-head',\n 'modal-heading',\n 'modal-title',\n 'modal-description',\n 'modal-close',\n 'modal-body',\n 'modal-actions',\n] as const\n\nexport const ModalSheet = (props: {\n id: string\n title: string\n body: JSXChild\n closeHref: string\n closeLabel: string\n description?: string | null\n actions?: JSXChild\n presentation?: 'sheet' | 'dialog'\n size?: 'default' | 'large'\n mode?: 'overlay' | 'embedded'\n unsavedPrompt?: string | null\n}): TemplateResult => (\n <div\n data-ui=\"modal-layer\"\n data-mode={props.mode ?? 'overlay'}\n data-route-modal={props.mode === 'embedded' ? null : 'true'}\n data-presentation={props.presentation ?? 'sheet'}\n data-unsaved-prompt={props.unsavedPrompt ?? null}\n >\n <a data-ui=\"modal-backdrop\" href={props.closeHref} aria-label={props.closeLabel}>\n <span>{props.closeLabel}</span>\n </a>\n <section\n id={props.id}\n data-ui=\"modal-sheet\"\n data-size={props.size ?? 'default'}\n role=\"dialog\"\n aria-modal={props.mode === 'embedded' ? 'false' : 'true'}\n aria-labelledby={`${props.id}-title`}\n aria-describedby={props.description ? `${props.id}-description` : null}\n tabindex={props.mode === 'embedded' ? null : '-1'}\n >\n <header data-ui=\"modal-head\">\n <div data-ui=\"modal-heading\">\n <h2 data-ui=\"modal-title\" id={`${props.id}-title`}>\n {props.title}\n </h2>\n {!!props.description && (\n <p data-ui=\"modal-description\" id={`${props.id}-description`}>\n {props.description}\n </p>\n )}\n </div>\n <a\n data-ui=\"modal-close\"\n href={props.closeHref}\n aria-label={props.closeLabel}\n title={props.closeLabel}\n >\n \u00D7\n </a>\n </header>\n <div data-ui=\"modal-body\">{props.body}</div>\n {props.actions !== undefined && <footer data-ui=\"modal-actions\">{props.actions}</footer>}\n </section>\n </div>\n)\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { ActionGroup, Button, LinkButton } from '../../primitives/actions/index.tsx'\nimport type { ActionVariant } from '../../primitives/actions/index.tsx'\nimport { ModalSheet } from '../../patterns/modal-sheet/index.tsx'\n\nexport const HOOKS = ['dialog', 'confirm-dialog-message'] as const\n\nexport type DialogProps = {\n id: string\n title: string\n body: JSXChild\n closeHref: string\n closeLabel: string\n description?: string\n actions?: JSXChild\n mode?: 'overlay' | 'embedded'\n unsavedPrompt?: string\n}\n\nexport const Dialog = (props: DialogProps): TemplateResult => (\n <div data-ui=\"dialog\">\n <ModalSheet {...props} presentation=\"dialog\" />\n </div>\n)\n\nexport type ConfirmDialogProps = Omit<DialogProps, 'body' | 'actions'> & {\n message: string\n confirmLabel: string\n confirmName?: string\n confirmValue?: string\n confirmForm?: string\n confirmVariant?: ActionVariant\n}\n\nexport const ConfirmDialog = (props: ConfirmDialogProps): TemplateResult => (\n <Dialog\n {...props}\n body={<p data-ui=\"confirm-dialog-message\">{props.message}</p>}\n actions={\n <ActionGroup\n label={props.title}\n actions={[\n <LinkButton label={props.closeLabel} href={props.closeHref} />,\n <Button\n label={props.confirmLabel}\n variant={props.confirmVariant ?? 'destructive'}\n type=\"submit\"\n name={props.confirmName ?? 'intent'}\n value={props.confirmValue ?? 'confirm'}\n form={props.confirmForm ?? null}\n />,\n ]}\n />\n }\n />\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport type { Tone } from '../../primitives/status/index.tsx'\n\nexport const HOOKS = [\n 'toast-region',\n 'toast',\n 'toast-copy',\n 'toast-title',\n 'toast-message',\n 'toast-actions',\n] as const\n\nexport type ToastProps = {\n id: string\n title: string\n message?: string\n tone?: Tone\n actions?: JSXChild\n}\n\nexport const Toast = (props: ToastProps): TemplateResult => (\n <article\n data-ui=\"toast\"\n data-tone={props.tone ?? 'neutral'}\n role={props.tone === 'danger' ? 'alert' : 'status'}\n >\n <div data-ui=\"toast-copy\">\n <strong data-ui=\"toast-title\">{props.title}</strong>\n {props.message && <p data-ui=\"toast-message\">{props.message}</p>}\n </div>\n {props.actions !== undefined && <div data-ui=\"toast-actions\">{props.actions}</div>}\n </article>\n)\n\nexport const ToastRegion = (props: { label: string; toasts: readonly ToastProps[] }): TemplateResult => (\n <section data-ui=\"toast-region\" aria-label={props.label} aria-live=\"polite\" aria-relevant=\"additions text\">\n {each(\n props.toasts,\n (toast) => toast.id,\n (toast) => (\n <Toast {...toast} />\n ),\n )}\n </section>\n)\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['spinner', 'visually-hidden'] as const\n\nexport const Spinner = (props: { label: string; size?: 'small' | 'default' | 'large' }): TemplateResult => (\n <span data-ui=\"spinner\" data-size={props.size ?? 'default'} role=\"status\">\n <span data-ui=\"visually-hidden\">{props.label}</span>\n </span>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['skeleton', 'skeleton-line'] as const\n\nexport const Skeleton = (props: { label: string; lines?: number }): TemplateResult => (\n <div data-ui=\"skeleton\" role=\"status\" aria-label={props.label}>\n {each(\n Array.from({ length: Math.max(1, props.lines ?? 3) }),\n (_, index) => index,\n (_, index) => (\n <span data-ui=\"skeleton-line\" data-line={String(index + 1)} />\n ),\n )}\n </div>\n)\n", "// @ts-check\n\nconst focusableSelector = [\n 'a[href]',\n 'button:not([disabled])',\n 'input:not([disabled])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n 'summary',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(',')\n\n/** @param {ParentNode} root */\nconst focusables = (root) =>\n [...root.querySelectorAll(focusableSelector)].filter(\n (element) =>\n element instanceof HTMLElement &&\n !element.hidden &&\n element.getAttribute('aria-hidden') !== 'true' &&\n (typeof element.checkVisibility !== 'function' || element.checkVisibility()),\n )\n\n/**\n * Attach the small browser contract shared by server-rendered overlays.\n * The returned function restores inert state and removes every listener.\n *\n * @param {ParentNode} [root]\n * @returns {() => void}\n */\nexport const attachDesignSystemInteractions = (root = document) => {\n const activeBeforeOpen = document.activeElement instanceof HTMLElement ? document.activeElement : null\n const modal = root.querySelector('[data-ui=\"modal-layer\"][data-route-modal=\"true\"] [role=\"dialog\"]')\n const appShell = root.querySelector('[data-ui=\"app-shell\"], [data-ui=\"shell\"]')\n const priorInert = appShell instanceof HTMLElement ? appShell.inert : false\n if (modal instanceof HTMLElement) {\n if (appShell instanceof HTMLElement && !appShell.contains(modal)) appShell.inert = true\n const first = focusables(modal)[0]\n ;(first instanceof HTMLElement ? first : modal).focus()\n }\n\n const navigationMedia = window.matchMedia('(max-width: 48rem)')\n const navigationRoot = document.documentElement\n const priorNavigationOpen = navigationRoot.dataset.kvNavigationOpen\n const navigations = /** @type {HTMLDetailsElement[]} */ (\n [...root.querySelectorAll('[data-ui=\"app-navigation\"]')].filter(\n (navigation) => navigation instanceof HTMLDetailsElement,\n )\n )\n const navigationStates = new Map()\n\n /** @param {HTMLDetailsElement} navigation */\n const restoreNavigationTargets = (navigation) => {\n const state = navigationStates.get(navigation)\n if (!state?.targets) return\n for (const { element, inert } of state.targets) element.inert = inert\n state.targets = null\n }\n\n const syncNavigationLock = () => {\n const hasOpenNavigation = navigationMedia.matches && navigations.some((navigation) => navigation.open)\n if (hasOpenNavigation) navigationRoot.dataset.kvNavigationOpen = 'true'\n else if (priorNavigationOpen === undefined) delete navigationRoot.dataset.kvNavigationOpen\n else navigationRoot.dataset.kvNavigationOpen = priorNavigationOpen\n }\n\n /** @param {HTMLDetailsElement} navigation */\n const syncNavigation = (navigation) => {\n const trigger = navigation.querySelector('[data-ui=\"navigation-trigger\"]')\n const drawer = navigation.querySelector('[data-ui=\"navigation-drawer\"]')\n if (!(trigger instanceof HTMLElement) || !(drawer instanceof HTMLElement)) return\n const mobile = navigationMedia.matches\n trigger.dataset.open = String(mobile && navigation.open)\n if (!mobile) {\n drawer.removeAttribute('role')\n drawer.removeAttribute('aria-modal')\n drawer.removeAttribute('aria-label')\n restoreNavigationTargets(navigation)\n syncNavigationLock()\n return\n }\n drawer.setAttribute('role', 'dialog')\n drawer.setAttribute('aria-modal', 'true')\n drawer.setAttribute('aria-label', drawer.dataset.navigationLabel ?? 'Navigation')\n if (!navigation.open) {\n restoreNavigationTargets(navigation)\n syncNavigationLock()\n return\n }\n const state = navigationStates.get(navigation) ?? {}\n if (!state.targets) {\n const shell = navigation.closest('[data-ui=\"app-shell\"], [data-ui=\"shell\"]')\n const targets = shell\n ? [\n ...shell.querySelectorAll(\n ':scope > [data-ui=\"app-main\"], :scope > [data-ui=\"main\"], :scope > [data-ui=\"app-right-rail\"]',\n ),\n ]\n .filter((element) => element instanceof HTMLElement)\n .map((element) => ({ element, inert: element.inert }))\n : []\n for (const { element } of targets) element.inert = true\n state.targets = targets\n state.returnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : trigger\n navigationStates.set(navigation, state)\n queueMicrotask(() => {\n if (!navigation.open || !navigationMedia.matches) return\n const activeItem = drawer.querySelector('[data-ui=\"navigation-item\"][aria-current=\"page\"]')\n const first = focusables(drawer)[0]\n const focusTarget = activeItem instanceof HTMLElement ? activeItem : first\n ;(focusTarget instanceof HTMLElement ? focusTarget : drawer).focus()\n })\n }\n syncNavigationLock()\n }\n\n /** @param {HTMLDetailsElement} navigation @param {boolean} [returnFocus] */\n const closeNavigation = (navigation, returnFocus = true) => {\n const state = navigationStates.get(navigation)\n navigation.open = false\n syncNavigation(navigation)\n if (returnFocus && state?.returnFocus instanceof HTMLElement && state.returnFocus.isConnected)\n state.returnFocus.focus()\n navigationStates.delete(navigation)\n }\n\n const navigationCleanups = navigations.map((navigation) => {\n const onToggle = () => syncNavigation(navigation)\n /** @param {MouseEvent} event */\n const onClick = (event) => {\n const target = event.target\n if (!(target instanceof Element)) return\n const branchTrigger = target.closest('[data-ui=\"navigation-branch-trigger\"]')\n const branch = branchTrigger?.closest('[data-ui=\"navigation-branch\"]')\n if (branch instanceof HTMLDetailsElement && branch.open) {\n event.preventDefault()\n return\n }\n if (!navigationMedia.matches || !navigation.open) return\n if (\n target.closest('[data-ui=\"navigation-close\"]') ||\n target.closest('[data-ui=\"navigation-backdrop\"]') ||\n target.closest('[data-ui=\"navigation-item\"]')\n )\n closeNavigation(navigation)\n }\n navigation.addEventListener('toggle', onToggle)\n navigation.addEventListener('click', onClick)\n syncNavigation(navigation)\n return () => {\n navigation.removeEventListener('toggle', onToggle)\n navigation.removeEventListener('click', onClick)\n restoreNavigationTargets(navigation)\n }\n })\n const onNavigationMediaChange = () => {\n for (const navigation of navigations) {\n if (navigation.open) closeNavigation(navigation, false)\n else syncNavigation(navigation)\n }\n }\n navigationMedia.addEventListener('change', onNavigationMediaChange)\n\n for (const popover of root.querySelectorAll('[data-ui=\"popover\"][data-open=\"true\"]')) {\n const trigger = popover.querySelector('[data-ui=\"popover-trigger\"]')\n const panel = popover.querySelector('[data-ui=\"popover-panel\"]')\n if (!(trigger instanceof HTMLElement) || !(panel instanceof HTMLElement)) continue\n const box = trigger.getBoundingClientRect()\n const gap = 8\n const placement = popover.getAttribute('data-placement') ?? 'bottom-start'\n const prefersTop = placement.startsWith('top')\n const prefersEnd = placement.endsWith('end')\n const topPosition = box.top - panel.offsetHeight - gap\n const bottomPosition = box.bottom + gap\n const topFits = topPosition >= 8\n const bottomFits = bottomPosition + panel.offsetHeight <= window.innerHeight - 8\n const useTop = prefersTop ? topFits || !bottomFits : !bottomFits && topFits\n const requestedLeft = prefersEnd ? box.right - panel.offsetWidth : box.left\n const left = Math.max(8, Math.min(requestedLeft, window.innerWidth - panel.offsetWidth - 8))\n const top = Math.max(\n 8,\n Math.min(useTop ? topPosition : bottomPosition, window.innerHeight - panel.offsetHeight - 8),\n )\n panel.dataset.runtimePositioned = 'true'\n panel.dataset.runtimePlacement = `${useTop ? 'top' : 'bottom'}-${prefersEnd ? 'end' : 'start'}`\n panel.style.left = `${Math.round(left)}px`\n panel.style.top = `${Math.round(top)}px`\n panel.style.right = 'auto'\n panel.style.bottom = 'auto'\n }\n\n /** @param {KeyboardEvent} event */\n const onKeydown = (event) => {\n const openNavigation = navigations.find((navigation) => navigationMedia.matches && navigation.open)\n if (openNavigation instanceof HTMLDetailsElement) {\n const drawer = openNavigation.querySelector('[data-ui=\"navigation-drawer\"]')\n if (event.key === 'Escape') {\n closeNavigation(openNavigation)\n event.preventDefault()\n return\n }\n if (event.key === 'Tab' && drawer instanceof HTMLElement) {\n const items = focusables(drawer)\n const first = items[0]\n const last = items.at(-1)\n if (!items.length) {\n event.preventDefault()\n drawer.focus()\n return\n }\n if (event.shiftKey && (document.activeElement === first || document.activeElement === drawer)) {\n event.preventDefault()\n if (last instanceof HTMLElement) last.focus()\n return\n }\n if (!event.shiftKey && document.activeElement === last) {\n event.preventDefault()\n if (first instanceof HTMLElement) first.focus()\n return\n }\n }\n }\n const treeItem = document.activeElement?.closest('[data-ui=\"tree\"] [role=\"treeitem\"]')\n if (treeItem instanceof HTMLElement) {\n const tree = treeItem.closest('[data-ui=\"tree\"]')\n const items = /** @type {HTMLElement[]} */ (\n tree\n ? [...tree.querySelectorAll('[role=\"treeitem\"]')].filter((item) => item instanceof HTMLElement)\n : []\n )\n const activeIndex = items.indexOf(treeItem)\n let nextIndex = -1\n if (event.key === 'ArrowDown') nextIndex = Math.min(items.length - 1, activeIndex + 1)\n else if (event.key === 'ArrowUp') nextIndex = Math.max(0, activeIndex - 1)\n else if (event.key === 'Home') nextIndex = 0\n else if (event.key === 'End') nextIndex = items.length - 1\n else if (event.key === 'ArrowRight') {\n const level = Number(treeItem.getAttribute('aria-level'))\n if (\n treeItem.getAttribute('aria-expanded') === 'true' &&\n Number(items[activeIndex + 1]?.getAttribute('aria-level')) > level\n )\n nextIndex = activeIndex + 1\n } else if (event.key === 'ArrowLeft') {\n const level = Number(treeItem.getAttribute('aria-level'))\n if (level > 1)\n nextIndex = items.findLastIndex(\n (item, index) => index < activeIndex && Number(item.getAttribute('aria-level')) < level,\n )\n }\n if (nextIndex >= 0 && items[nextIndex]) {\n for (const item of items) item.tabIndex = -1\n items[nextIndex].tabIndex = 0\n items[nextIndex].focus()\n event.preventDefault()\n return\n }\n }\n const treeGridRow = document.activeElement?.closest('[data-ui=\"tree-grid-row\"]')\n if (treeGridRow instanceof HTMLElement) {\n const grid = treeGridRow.closest('[data-ui=\"tree-grid\"]')\n const rows = /** @type {HTMLElement[]} */ (\n grid\n ? [...grid.querySelectorAll('[data-ui=\"tree-grid-row\"]')].filter(\n (row) => row instanceof HTMLElement,\n )\n : []\n )\n const activeIndex = rows.indexOf(treeGridRow)\n const nextIndex =\n event.key === 'ArrowDown'\n ? Math.min(rows.length - 1, activeIndex + 1)\n : event.key === 'ArrowUp'\n ? Math.max(0, activeIndex - 1)\n : event.key === 'Home'\n ? 0\n : event.key === 'End'\n ? rows.length - 1\n : -1\n if (nextIndex >= 0 && rows[nextIndex]) {\n for (const row of rows) row.tabIndex = -1\n rows[nextIndex].tabIndex = 0\n rows[nextIndex].focus()\n event.preventDefault()\n return\n }\n }\n const openMenu = document.activeElement?.closest('[data-ui=\"menu\"][open]')\n if (openMenu instanceof HTMLDetailsElement) {\n const trigger = openMenu.querySelector('[data-ui=\"menu-trigger\"]')\n const items = /** @type {HTMLElement[]} */ (\n [...openMenu.querySelectorAll('[role=\"menuitem\"]')].filter(\n (item) => item instanceof HTMLElement && item.getAttribute('aria-disabled') !== 'true',\n )\n )\n const active = document.activeElement\n const activeIndex = active instanceof HTMLElement ? items.indexOf(active) : -1\n let nextIndex = -1\n if (event.key === 'ArrowDown') nextIndex = activeIndex < 0 ? 0 : (activeIndex + 1) % items.length\n else if (event.key === 'ArrowUp')\n nextIndex = activeIndex < 0 ? items.length - 1 : (activeIndex - 1 + items.length) % items.length\n else if (event.key === 'Home') nextIndex = 0\n else if (event.key === 'End') nextIndex = items.length - 1\n if (nextIndex >= 0 && items[nextIndex] instanceof HTMLElement) {\n items[nextIndex].focus()\n event.preventDefault()\n return\n }\n if ((event.key === 'Enter' || event.key === ' ') && document.activeElement === trigger) {\n if (items[0] instanceof HTMLElement) items[0].focus()\n event.preventDefault()\n return\n }\n }\n if (event.key === 'Escape' && openMenu instanceof HTMLDetailsElement) {\n openMenu.open = false\n const trigger = openMenu.querySelector('[data-ui=\"menu-trigger\"]')\n if (trigger instanceof HTMLElement) trigger.focus()\n event.preventDefault()\n return\n }\n const openPopover = document.activeElement?.closest('[data-ui=\"popover\"][data-open=\"true\"]')\n if (event.key === 'Escape' && openPopover instanceof HTMLElement) {\n const close = openPopover.querySelector('[data-ui=\"popover-close\"]')\n if (close instanceof HTMLElement) close.click()\n event.preventDefault()\n return\n }\n if (!(modal instanceof HTMLElement)) return\n if (event.key === 'Escape') {\n const close = modal.querySelector('[data-ui=\"modal-close\"]')\n if (close instanceof HTMLElement) close.click()\n event.preventDefault()\n return\n }\n if (event.key !== 'Tab') return\n const items = focusables(modal)\n if (!items.length) {\n event.preventDefault()\n modal.focus()\n return\n }\n const first = items[0]\n const last = items.at(-1)\n if (event.shiftKey && (document.activeElement === first || document.activeElement === modal)) {\n event.preventDefault()\n if (last instanceof HTMLElement) last.focus()\n } else if (!event.shiftKey && document.activeElement === last) {\n event.preventDefault()\n if (first instanceof HTMLElement) first.focus()\n }\n }\n /** @param {MouseEvent} event */\n const onDocumentClick = (event) => {\n const target = event.target\n if (!(target instanceof Node)) return\n for (const menu of root.querySelectorAll('[data-ui=\"menu\"][open]')) {\n if (menu instanceof HTMLDetailsElement && !menu.contains(target)) menu.open = false\n }\n }\n document.addEventListener('keydown', onKeydown)\n document.addEventListener('click', onDocumentClick)\n\n return () => {\n document.removeEventListener('keydown', onKeydown)\n document.removeEventListener('click', onDocumentClick)\n navigationMedia.removeEventListener('change', onNavigationMediaChange)\n for (const cleanup of navigationCleanups) cleanup()\n if (priorNavigationOpen === undefined) delete navigationRoot.dataset.kvNavigationOpen\n else navigationRoot.dataset.kvNavigationOpen = priorNavigationOpen\n if (appShell instanceof HTMLElement) appShell.inert = priorInert\n if (activeBeforeOpen?.isConnected) activeBeforeOpen.focus()\n }\n}\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport type FieldIssue = { path: string; message: string }\n\nexport const issueFor = (issues: readonly FieldIssue[] | undefined, name: string): string | null =>\n issues?.find((issue) => issue.path === name)?.message ?? null\n\nexport type FieldFrameProps = {\n id: string\n label: string\n control: JSXChild\n help?: string | null\n error?: string | null\n required?: boolean\n span?: 'half' | 'full'\n kind: string\n}\n\nexport const FieldFrame = (props: FieldFrameProps): TemplateResult => {\n const helpId = props.help ? `${props.id}-help` : null\n const errorId = props.error ? `${props.id}-error` : null\n return (\n <div\n data-ui=\"field\"\n data-kind={props.kind}\n data-span={props.span ?? 'half'}\n data-invalid={String(!!props.error)}\n >\n <label data-ui=\"field-label\" for={props.id}>\n {props.label}\n {props.required && (\n <span data-ui=\"field-required\" aria-hidden=\"true\">\n {' *'}\n </span>\n )}\n </label>\n {props.control}\n {!!props.help && (\n <small data-ui=\"field-help\" id={helpId ?? undefined}>\n {props.help}\n </small>\n )}\n {!!props.error && (\n <small data-ui=\"field-error\" id={errorId ?? undefined}>\n {props.error}\n </small>\n )}\n </div>\n )\n}\n\nexport const describedBy = (id: string, help?: string | null, error?: string | null): string | null =>\n [help ? `${id}-help` : null, error ? `${id}-error` : null].filter(Boolean).join(' ') || null\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport { Field } from '../../primitives/field/index.tsx'\nimport type { FieldOption, FieldProps } from '../../primitives/field/index.tsx'\nimport { describedBy, FieldFrame, issueFor } from '../shared.tsx'\nimport type { FieldIssue } from '../shared.tsx'\n\nexport const HOOKS = ['switch-control', 'switch-track'] as const\n\ntype CommonProps = Omit<FieldProps, 'type' | 'fields' | 'control' | 'error'> & {\n issues?: readonly FieldIssue[]\n error?: string | null\n}\n\nconst normalize = (props: CommonProps): FieldProps => ({\n ...props,\n error: props.error ?? issueFor(props.issues, props.name),\n})\n\nexport const TextField = (props: CommonProps): TemplateResult => <Field {...normalize(props)} type=\"text\" />\nexport const TextArea = (props: CommonProps): TemplateResult => (\n <Field {...normalize(props)} type=\"textarea\" />\n)\nexport const NumberField = (props: CommonProps): TemplateResult => (\n <Field {...normalize(props)} type=\"number\" />\n)\nexport const MoneyField = (props: CommonProps): TemplateResult => (\n <Field {...normalize({ ...props, step: props.step ?? '0.01' })} type=\"decimal\" />\n)\nexport const SearchField = (props: CommonProps): TemplateResult => <Field {...normalize(props)} type=\"text\" />\nexport const Checkbox = (props: CommonProps): TemplateResult => (\n <Field {...normalize(props)} type=\"checkbox\" />\n)\nexport const CheckboxGroup = (props: CommonProps): TemplateResult => (\n <Field {...normalize(props)} type=\"checkbox-group\" />\n)\nexport const RadioGroup = (props: CommonProps): TemplateResult => <Field {...normalize(props)} type=\"radio\" />\nexport const Select = (props: CommonProps): TemplateResult => <Field {...normalize(props)} type=\"select\" />\n\nexport type SwitchProps = Omit<CommonProps, 'options' | 'placeholder' | 'step' | 'min' | 'max'> & {\n checked?: boolean\n value?: string\n}\n\nexport const Switch = (props: SwitchProps): TemplateResult => {\n const error = props.error ?? issueFor(props.issues, props.name)\n const checked = props.checked ?? props.value === '1'\n return (\n <FieldFrame\n id={props.id}\n label={props.label}\n help={props.help}\n error={error}\n required={props.required}\n span={props.span}\n kind=\"switch\"\n control={\n <label data-ui=\"switch-control\">\n <input\n id={props.id}\n type=\"checkbox\"\n role=\"switch\"\n name={props.name}\n value={props.value ?? '1'}\n checked={checked}\n aria-checked={String(checked)}\n disabled={props.disabled === true}\n required={props.required === true}\n aria-invalid={error ? 'true' : null}\n aria-describedby={describedBy(props.id, props.help, error)}\n />\n <span data-ui=\"switch-track\" aria-hidden=\"true\" />\n </label>\n }\n />\n )\n}\n\nexport type { CommonProps as ScalarFieldProps, FieldIssue, FieldOption }\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport { describedBy, FieldFrame, issueFor } from '../shared.tsx'\nimport type { FieldIssue } from '../shared.tsx'\n\nexport const HOOKS = [\n 'combobox',\n 'combobox-input-row',\n 'combobox-toggle',\n 'combobox-listbox',\n 'combobox-option',\n 'combobox-empty',\n 'tag-picker',\n] as const\n\nexport type ComboboxOption = { value: string; label: string; description?: string; disabled?: boolean }\nexport type ComboboxProps = {\n id: string\n name: string\n /** Name used for the human-readable search text; defaults to `${name}Query`. */\n queryName?: string\n label: string\n query: string\n value?: string | null\n options: readonly ComboboxOption[]\n open: boolean\n openHref: string\n closeHref: string\n help?: string | null\n error?: string | null\n issues?: readonly FieldIssue[]\n required?: boolean\n disabled?: boolean\n loading?: boolean\n loadingLabel?: string\n noResultsLabel?: string\n span?: 'half' | 'full'\n}\n\nconst ComboControl = (\n props: ComboboxProps & { resolvedError: string | null; submitValue: boolean },\n): TemplateResult => (\n <div data-ui=\"combobox\" data-open={String(props.open)}>\n {props.submitValue && (\n <input type=\"hidden\" name={props.name} value={props.value ?? ''} disabled={props.disabled === true} />\n )}\n <div data-ui=\"combobox-input-row\">\n <input\n data-ui=\"field-control\"\n id={props.id}\n name={props.queryName ?? `${props.name}Query`}\n value={props.query}\n role=\"combobox\"\n aria-autocomplete=\"list\"\n aria-expanded={String(props.open)}\n aria-controls={`${props.id}-listbox`}\n aria-invalid={props.resolvedError ? 'true' : null}\n aria-describedby={describedBy(props.id, props.help, props.resolvedError)}\n required={props.required === true}\n disabled={props.disabled === true}\n autocomplete=\"off\"\n />\n <a\n data-ui=\"combobox-toggle\"\n href={props.open ? props.closeHref : props.openHref}\n aria-label={props.open ? 'Close options' : 'Open options'}\n >\n \u25BE\n </a>\n </div>\n {props.open && (\n <div data-ui=\"combobox-listbox\" id={`${props.id}-listbox`} role=\"listbox\">\n {props.loading ? (\n <p data-ui=\"combobox-empty\">{props.loadingLabel ?? 'Loading\u2026'}</p>\n ) : props.options.length === 0 ? (\n <p data-ui=\"combobox-empty\">{props.noResultsLabel ?? 'No results'}</p>\n ) : (\n each(\n props.options,\n (option) => option.value,\n (option) => (\n <a\n data-ui=\"combobox-option\"\n href={\n option.disabled\n ? null\n : `${props.closeHref}${props.closeHref.includes('?') ? '&' : '?'}${encodeURIComponent(props.name)}=${encodeURIComponent(option.value)}`\n }\n role=\"option\"\n aria-selected={String(option.value === props.value)}\n aria-disabled={option.disabled ? 'true' : null}\n >\n <strong>{option.label}</strong>\n {option.description && <small>{option.description}</small>}\n </a>\n ),\n )\n )}\n </div>\n )}\n </div>\n)\n\nconst ComboboxField = (props: ComboboxProps & { submitValue: boolean }): TemplateResult => {\n const error = props.error ?? issueFor(props.issues, props.name)\n return (\n <FieldFrame\n {...props}\n kind=\"combobox\"\n error={error}\n control={<ComboControl {...props} resolvedError={error} />}\n />\n )\n}\n\nexport const Combobox = (props: ComboboxProps): TemplateResult => <ComboboxField {...props} submitValue />\n\nexport type MultiComboboxProps = Omit<ComboboxProps, 'value'> & {\n values: readonly string[]\n removeHref: (value: string) => string\n}\n\nexport const MultiCombobox = (props: MultiComboboxProps): TemplateResult => (\n <div data-ui=\"tag-picker\">\n <ComboboxField {...props} value={null} submitValue={false} />\n {each(\n props.values,\n (value) => value,\n (value) => (\n <>\n <input type=\"hidden\" name={props.name} value={value} disabled={props.disabled === true} />\n <a\n href={props.removeHref(value)}\n aria-label={`Remove ${props.options.find((option) => option.value === value)?.label ?? value}`}\n >\n {props.options.find((option) => option.value === value)?.label ?? value} \u00D7\n </a>\n </>\n ),\n )}\n </div>\n)\n\nexport const TagPicker = (props: MultiComboboxProps): TemplateResult => <MultiCombobox {...props} />\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport { Field } from '../../primitives/field/index.tsx'\nimport type { FieldProps } from '../../primitives/field/index.tsx'\nimport { issueFor } from '../shared.tsx'\nimport type { FieldIssue } from '../shared.tsx'\n\nexport const HOOKS = ['date-range'] as const\n\ntype TemporalProps = Omit<FieldProps, 'type' | 'fields' | 'control' | 'error'> & {\n issues?: readonly FieldIssue[]\n error?: string | null\n}\n\nconst temporal = (props: TemporalProps): FieldProps => ({\n ...props,\n error: props.error ?? issueFor(props.issues, props.name),\n})\n\n/** Civil dates are passed through as YYYY-MM-DD text and are never converted through UTC. */\nexport const DatePicker = (props: TemporalProps): TemplateResult => <Field {...temporal(props)} type=\"date\" />\n\nexport type DateRangePickerProps = {\n id: string\n label: string\n start: Omit<TemporalProps, 'label' | 'span'>\n end: Omit<TemporalProps, 'label' | 'span'>\n startLabel: string\n endLabel: string\n span?: 'half' | 'full'\n}\n\nexport const DateRangePicker = (props: DateRangePickerProps): TemplateResult => (\n <fieldset data-ui=\"date-range\" data-span={props.span ?? 'full'}>\n <legend>{props.label}</legend>\n <Field {...temporal({ ...props.start, label: props.startLabel })} type=\"date\" />\n <Field {...temporal({ ...props.end, label: props.endLabel })} type=\"date\" />\n </fieldset>\n)\n\n/** Local date-time text is submitted unchanged; the app adapter owns timezone interpretation. */\nexport const DateTimePicker = (props: TemporalProps): TemplateResult => (\n <Field {...temporal(props)} type=\"datetime-local\" />\n)\n\nexport const TimePicker = (props: TemporalProps): TemplateResult => <Field {...temporal(props)} type=\"time\" />\n\nexport type { TemporalProps }\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { describedBy, FieldFrame, issueFor } from '../shared.tsx'\nimport type { FieldIssue } from '../shared.tsx'\n\nexport const HOOKS = ['file-upload', 'drop-zone', 'upload-status'] as const\n\nexport type FileUploadProps = {\n id: string\n name: string\n label: string\n accept?: string\n multiple?: boolean\n required?: boolean\n disabled?: boolean\n help?: string | null\n error?: string | null\n issues?: readonly FieldIssue[]\n status?: JSXChild\n span?: 'half' | 'full'\n}\n\nconst uploadControl = (props: FileUploadProps, error: string | null, drop: boolean): TemplateResult => (\n <div data-ui={drop ? 'drop-zone' : 'file-upload'}>\n <input\n data-ui=\"field-control\"\n id={props.id}\n type=\"file\"\n name={props.name}\n accept={props.accept}\n multiple={props.multiple === true}\n required={props.required === true}\n disabled={props.disabled === true}\n aria-invalid={error ? 'true' : null}\n aria-describedby={describedBy(props.id, props.help, error)}\n />\n {props.status !== undefined && <div data-ui=\"upload-status\">{props.status}</div>}\n </div>\n)\n\nconst renderUpload = (props: FileUploadProps, drop: boolean): TemplateResult => {\n const error = props.error ?? issueFor(props.issues, props.name)\n return (\n <FieldFrame\n {...props}\n kind={drop ? 'drop-zone' : 'file-upload'}\n error={error}\n control={uploadControl(props, error, drop)}\n />\n )\n}\n\nexport const FileUpload = (props: FileUploadProps): TemplateResult => renderUpload(props, false)\nexport const DropZone = (props: FileUploadProps): TemplateResult => renderUpload(props, true)\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport { Combobox } from '../combobox/index.tsx'\nimport type { ComboboxOption, ComboboxProps } from '../combobox/index.tsx'\n\nexport const HOOKS = ['relation-picker'] as const\n\nexport type RelationPickerProps<T> = Omit<ComboboxProps, 'options'> & {\n results: readonly T[]\n getValue: (result: T) => string\n getLabel: (result: T) => string\n getDescription?: (result: T) => string | undefined\n}\n\n/** The application supplies permission-filtered results; this renderer performs no lookup. */\nexport const RelationPicker = <T,>(props: RelationPickerProps<T>): TemplateResult => {\n const options: ComboboxOption[] = props.results.map((result) => ({\n value: props.getValue(result),\n label: props.getLabel(result),\n description: props.getDescription?.(result),\n }))\n return (\n <div data-ui=\"relation-picker\">\n <Combobox {...props} options={options} />\n </div>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { Button } from '../../primitives/actions/index.tsx'\nimport { Menu } from '../../interactions/menu/index.tsx'\n\nexport const HOOKS = [\n 'search-bar',\n 'filter-bar',\n 'applied-filters',\n 'applied-filter',\n 'saved-views',\n 'saved-view',\n 'view-settings',\n 'view-settings-options',\n] as const\n\nexport const withQueryState = (\n href: string,\n changes: Readonly<Record<string, string | number | boolean | null | undefined>>,\n): string => {\n const url = new URL(href, 'http://ket.local')\n for (const [key, value] of Object.entries(changes)) {\n if (value === null || value === undefined || value === '') url.searchParams.delete(key)\n else url.searchParams.set(key, String(value))\n }\n return `${url.pathname}${url.search}${url.hash}`\n}\n\nexport type SearchBarProps = {\n action: string\n value?: string | null\n name?: string\n id?: string\n label?: string\n placeholder?: string\n submitLabel?: string\n hidden?: Readonly<Record<string, string>>\n}\n\nexport const SearchBar = (props: SearchBarProps): TemplateResult => (\n <form data-ui=\"search-bar\" action={props.action} method=\"get\" role=\"search\">\n {each(\n Object.entries(props.hidden ?? {}),\n ([name]) => name,\n ([name, value]) => (\n <input type=\"hidden\" name={name} value={value} />\n ),\n )}\n <label for={props.id ?? 'resource-search'}>{props.label ?? 'Search'}</label>\n <input\n id={props.id ?? 'resource-search'}\n type=\"search\"\n name={props.name ?? 'q'}\n value={props.value ?? ''}\n placeholder={props.placeholder}\n autocomplete=\"off\"\n />\n <Button type=\"submit\" label={props.submitLabel ?? 'Search'} size=\"compact\" />\n </form>\n)\n\nexport const FilterBar = (props: { label?: string; filters: readonly JSXChild[] }): TemplateResult => (\n <section data-ui=\"filter-bar\" aria-label={props.label ?? 'Filters'}>\n {props.filters}\n </section>\n)\n\nexport type AppliedFilter = { id: string; label: string; value: string; removeHref: string }\nexport const AppliedFilters = (props: {\n label?: string\n filters: readonly AppliedFilter[]\n clearHref?: string\n clearLabel?: string\n}): TemplateResult => (\n <nav data-ui=\"applied-filters\" aria-label={props.label ?? 'Applied filters'}>\n {each(\n props.filters,\n (filter) => filter.id,\n (filter) => (\n <a\n data-ui=\"applied-filter\"\n href={filter.removeHref}\n aria-label={`Remove ${filter.label}: ${filter.value}`}\n >\n <strong>{filter.label}</strong> {filter.value} \u00D7\n </a>\n ),\n )}\n {props.clearHref && <a href={props.clearHref}>{props.clearLabel ?? 'Clear all'}</a>}\n </nav>\n)\n\nexport type SortChoice = { id: string; label: string; href: string; active?: boolean }\nexport const SortMenu = (props: {\n id: string\n label?: string\n choices: readonly SortChoice[]\n}): TemplateResult => (\n <Menu\n id={props.id}\n label={props.label ?? 'Sort'}\n align=\"end\"\n items={props.choices.map((choice) => ({\n id: choice.id,\n label: `${choice.active ? '\u2713 ' : ''}${choice.label}`,\n href: choice.href,\n }))}\n />\n)\n\nexport type SavedView = { id: string; label: string; href: string; active?: boolean; description?: string }\nexport const SavedViews = (props: { label?: string; views: readonly SavedView[] }): TemplateResult => (\n <nav data-ui=\"saved-views\" aria-label={props.label ?? 'Saved views'}>\n {each(\n props.views,\n (view) => view.id,\n (view) => (\n <a data-ui=\"saved-view\" href={view.href} aria-current={view.active ? 'page' : null}>\n <strong>{view.label}</strong>\n {view.description && <small>{view.description}</small>}\n </a>\n ),\n )}\n </nav>\n)\n\nexport type ViewSetting = { id: string; label: string; visible: boolean; disabled?: boolean }\nexport const ViewSettings = (props: {\n id: string\n label?: string\n action: string\n settings: readonly ViewSetting[]\n version?: string\n submitLabel?: string\n}): TemplateResult => (\n <details data-ui=\"view-settings\">\n <summary>{props.label ?? 'View settings'}</summary>\n <form action={props.action} method=\"post\">\n {props.version && <input type=\"hidden\" name=\"version\" value={props.version} />}\n <div data-ui=\"view-settings-options\">\n {each(\n props.settings,\n (setting) => setting.id,\n (setting) => (\n <label>\n <input\n type=\"checkbox\"\n name=\"columns\"\n value={setting.id}\n checked={setting.visible}\n disabled={setting.disabled === true}\n />\n {setting.label}\n </label>\n ),\n )}\n </div>\n <Button type=\"submit\" label={props.submitLabel ?? 'Save view'} size=\"compact\" />\n </form>\n </details>\n)\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { Button, LinkButton } from '../../primitives/actions/index.tsx'\n\nexport const HOOKS = ['inline-edit', 'inline-edit-value', 'inline-edit-form', 'inline-edit-actions'] as const\n\nexport type InlineEditProps = {\n id: string\n label: string\n value: JSXChild\n editing: boolean\n editHref: string\n cancelHref: string\n action: string\n name: string\n inputValue: string\n version?: string\n error?: string | null\n saveLabel?: string\n cancelLabel?: string\n}\n\nexport const InlineEdit = (props: InlineEditProps): TemplateResult => (\n <section data-ui=\"inline-edit\" aria-labelledby={`${props.id}-label`}>\n <strong id={`${props.id}-label`}>{props.label}</strong>\n {!props.editing ? (\n <span data-ui=\"inline-edit-value\">\n {props.value}\n <LinkButton href={props.editHref} label={`Edit ${props.label}`} size=\"compact\" />\n </span>\n ) : (\n <form data-ui=\"inline-edit-form\" action={props.action} method=\"post\">\n {props.version && <input type=\"hidden\" name=\"version\" value={props.version} />}\n <input\n id={`${props.id}-input`}\n name={props.name}\n value={props.inputValue}\n aria-label={props.label}\n aria-invalid={props.error ? 'true' : null}\n aria-describedby={props.error ? `${props.id}-error` : null}\n />\n {props.error && <small id={`${props.id}-error`}>{props.error}</small>}\n <span data-ui=\"inline-edit-actions\">\n <Button type=\"submit\" label={props.saveLabel ?? 'Save'} size=\"compact\" />\n <LinkButton href={props.cancelHref} label={props.cancelLabel ?? 'Cancel'} size=\"compact\" />\n </span>\n </form>\n )}\n </section>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { EmptyState, LoadingState } from '../../primitives/feedback/index.tsx'\nimport { PagerBar } from '../../patterns/list-chrome/index.tsx'\nimport type { PagerBarProps } from '../../patterns/list-chrome/index.tsx'\n\nexport const HOOKS = [\n 'resource-list',\n 'resource-list-item',\n 'resource-list-select',\n 'resource-list-link',\n] as const\n\nexport type ResourceListProps<Row> = {\n label: string\n rows: readonly Row[]\n id: (row: Row) => string\n href: (row: Row) => string\n primary: (row: Row) => JSXChild\n secondary?: (row: Row) => JSXChild\n meta?: (row: Row) => JSXChild\n selectedIds?: readonly string[]\n selectionName?: string\n selectionForm?: string\n loading?: boolean\n loadingLabel?: string\n emptyTitle?: string\n emptyMessage?: string\n pager?: PagerBarProps\n}\n\nexport const ResourceList = <Row,>(props: ResourceListProps<Row>): TemplateResult => {\n if (props.loading) return <LoadingState label={props.loadingLabel ?? 'Loading resources'} />\n if (props.rows.length === 0)\n return (\n <EmptyState\n title={props.emptyTitle ?? 'No resources'}\n message={props.emptyMessage ?? 'No results match this view.'}\n />\n )\n return (\n <div>\n <ul data-ui=\"resource-list\" aria-label={props.label}>\n {each(props.rows, props.id, (row) => {\n const id = props.id(row)\n return (\n <li data-ui=\"resource-list-item\" data-selected={props.selectedIds?.includes(id) ? 'true' : null}>\n {props.selectedIds !== undefined && (\n <input\n data-ui=\"resource-list-select\"\n type=\"checkbox\"\n name={props.selectionName ?? 'ids'}\n value={id}\n form={props.selectionForm}\n checked={props.selectedIds.includes(id)}\n aria-label={`Select ${id}`}\n />\n )}\n <a data-ui=\"resource-list-link\" href={props.href(row)}>\n <strong>{props.primary(row)}</strong>\n {props.secondary && <span>{props.secondary(row)}</span>}\n </a>\n {props.meta && <span>{props.meta(row)}</span>}\n </li>\n )\n })}\n </ul>\n {props.pager && <PagerBar {...props.pager} />}\n </div>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { Button, LinkButton } from '../../primitives/actions.tsx'\nimport type { ActionVariant } from '../../primitives/actions.tsx'\n\nexport type ListFacet = {\n id: string\n label: string\n href: string\n active?: boolean\n count?: number\n}\n\nexport type ListSearch = {\n id?: string\n action: string\n name?: string\n value?: string | null\n placeholder?: string | null\n label?: string\n submitLabel?: string\n hidden?: Readonly<Record<string, string>>\n}\n\nexport type ListSortChoice = {\n value: string\n label: string\n selected?: boolean\n}\n\nexport type ListSort = {\n id?: string\n action: string\n name?: string\n label?: string\n submitLabel?: string\n choices: readonly ListSortChoice[]\n hidden?: Readonly<Record<string, string>>\n}\n\nexport type BulkAction = {\n id: string\n label: string\n name?: string\n value?: string\n variant?: ActionVariant\n disabled?: boolean\n}\n\nexport type BulkActionsProps = {\n form?: string | null\n selectedCount?: number\n summary?: JSXChild\n clearHref?: string | null\n clearLabel?: string\n actions: readonly BulkAction[]\n}\n\nexport type PagerPage = {\n label: string\n href: string\n active?: boolean\n}\n\nexport type PagerBarProps = {\n summary?: JSXChild\n previousHref?: string | null\n nextHref?: string | null\n previousLabel?: string\n nextLabel?: string\n pages?: readonly PagerPage[]\n label?: string\n}\n\nexport type ListChromeProps = {\n filtersLabel?: string\n viewsLabel?: string\n search?: ListSearch\n facets?: readonly ListFacet[]\n views?: readonly ListFacet[]\n sort?: ListSort\n status?: JSXChild\n actions?: JSXChild\n bulk?: BulkActionsProps\n pager?: PagerBarProps\n}\n\nexport const HOOKS = [\n 'list-chrome',\n 'list-chrome-row',\n 'list-search',\n 'list-search-label',\n 'list-search-input',\n 'list-search-submit',\n 'list-facets',\n 'list-facet',\n 'list-facet-count',\n 'list-view-switch',\n 'list-view',\n 'list-sort',\n 'list-sort-label',\n 'list-sort-select',\n 'list-status',\n 'list-actions',\n 'bulk-actions',\n 'bulk-summary',\n 'bulk-action-list',\n 'pager-bar',\n 'pager-summary',\n 'pager-pages',\n 'pager-page',\n 'pager-actions',\n 'pager-link',\n] as const\n\nconst hiddenFields = (fields: Readonly<Record<string, string>> | undefined): JSXChild =>\n fields\n ? each(\n Object.entries(fields),\n ([name]) => name,\n ([name, value]) => <input type=\"hidden\" name={name} value={value} />,\n )\n : ''\n\nconst SearchControl = (props: ListSearch): TemplateResult => (\n <form data-ui=\"list-search\" action={props.action} method=\"get\" role=\"search\">\n {hiddenFields(props.hidden)}\n <label data-ui=\"list-search-label\" for={props.id ?? `${props.name ?? 'q'}-search`}>\n {props.label ?? 'Search'}\n </label>\n <input\n data-ui=\"list-search-input\"\n id={props.id ?? `${props.name ?? 'q'}-search`}\n type=\"search\"\n name={props.name ?? 'q'}\n value={props.value ?? ''}\n placeholder={props.placeholder ?? ''}\n autocomplete=\"off\"\n />\n <span data-ui=\"list-search-submit\">\n <Button type=\"submit\" label={props.submitLabel ?? 'Search'} variant=\"tertiary\" size=\"compact\" />\n </span>\n </form>\n)\n\nconst FacetNav = (props: {\n label: string\n ui: 'list-facets' | 'list-view-switch'\n itemUi: string\n items: readonly ListFacet[]\n}): TemplateResult => (\n <nav data-ui={props.ui} aria-label={props.label}>\n {each(\n props.items,\n (item) => item.id,\n (item) => (\n <a\n data-ui={props.itemUi}\n data-active={item.active === true ? 'true' : null}\n href={item.href}\n aria-current={item.active === true ? 'page' : null}\n >\n <span>{item.label}</span>\n {item.count !== undefined && <span data-ui=\"list-facet-count\">{String(item.count)}</span>}\n </a>\n ),\n )}\n </nav>\n)\n\nconst SortControl = (props: ListSort): TemplateResult => (\n <form data-ui=\"list-sort\" action={props.action} method=\"get\">\n {hiddenFields(props.hidden)}\n <label data-ui=\"list-sort-label\" for={props.id ?? `${props.name ?? 'sort'}-select`}>\n {props.label ?? 'Sort'}\n </label>\n <select\n data-ui=\"list-sort-select\"\n id={props.id ?? `${props.name ?? 'sort'}-select`}\n name={props.name ?? 'sort'}\n >\n {each(\n props.choices,\n (choice) => choice.value,\n (choice) => (\n <option value={choice.value} selected={choice.selected === true}>\n {choice.label}\n </option>\n ),\n )}\n </select>\n <Button type=\"submit\" label={props.submitLabel ?? 'Apply'} variant=\"tertiary\" size=\"compact\" />\n </form>\n)\n\nexport const BulkActions = (props: BulkActionsProps): TemplateResult => (\n <div\n data-ui=\"bulk-actions\"\n data-has-selection={props.selectedCount && props.selectedCount > 0 ? 'true' : null}\n >\n <div data-ui=\"bulk-summary\">\n {props.summary ?? `${props.selectedCount ?? 0} selected`}\n {props.clearHref && (\n <LinkButton\n href={props.clearHref}\n label={props.clearLabel ?? 'Clear'}\n variant=\"tertiary\"\n size=\"compact\"\n />\n )}\n </div>\n <div data-ui=\"bulk-action-list\">\n {each(\n props.actions,\n (action) => action.id,\n (action) => (\n <Button\n type=\"submit\"\n form={props.form ?? null}\n name={action.name}\n value={action.value}\n label={action.label}\n variant={action.variant ?? 'secondary'}\n size=\"compact\"\n disabled={action.disabled === true || !(props.selectedCount && props.selectedCount > 0)}\n />\n ),\n )}\n </div>\n </div>\n)\n\nexport const PagerBar = (props: PagerBarProps): TemplateResult => (\n <nav data-ui=\"pager-bar\" aria-label={props.label ?? 'Pagination'}>\n {props.summary !== undefined && <span data-ui=\"pager-summary\">{props.summary}</span>}\n {props.pages && props.pages.length > 0 && (\n <span data-ui=\"pager-pages\">\n {each(\n props.pages,\n (page) => page.href,\n (page) => (\n <a\n data-ui=\"pager-page\"\n data-active={page.active === true ? 'true' : null}\n href={page.href}\n aria-current={page.active === true ? 'page' : null}\n >\n {page.label}\n </a>\n ),\n )}\n </span>\n )}\n <span data-ui=\"pager-actions\">\n {props.previousHref ? (\n <a\n data-ui=\"pager-link\"\n href={props.previousHref}\n rel=\"prev\"\n aria-label={props.previousLabel ?? 'Previous page'}\n >\n \u2039\n </a>\n ) : (\n <span data-ui=\"pager-link\" data-disabled=\"true\" aria-hidden=\"true\">\n \u2039\n </span>\n )}\n {props.nextHref ? (\n <a data-ui=\"pager-link\" href={props.nextHref} rel=\"next\" aria-label={props.nextLabel ?? 'Next page'}>\n \u203A\n </a>\n ) : (\n <span data-ui=\"pager-link\" data-disabled=\"true\" aria-hidden=\"true\">\n \u203A\n </span>\n )}\n </span>\n </nav>\n)\n\nexport const ListChrome = (props: ListChromeProps): TemplateResult => {\n const filters =\n (props.facets?.length ?? 0) > 0 || (props.views?.length ?? 0) > 0 || props.sort ? (\n <div data-ui=\"list-chrome-row\" data-row=\"filters\">\n {props.facets && props.facets.length > 0 && (\n <FacetNav\n label={props.filtersLabel ?? 'Filters'}\n ui=\"list-facets\"\n itemUi=\"list-facet\"\n items={props.facets}\n />\n )}\n {props.views && props.views.length > 0 && (\n <FacetNav\n label={props.viewsLabel ?? 'Views'}\n ui=\"list-view-switch\"\n itemUi=\"list-view\"\n items={props.views}\n />\n )}\n {props.sort && <SortControl {...props.sort} />}\n </div>\n ) : null\n const meta =\n props.status !== undefined || props.actions !== undefined || props.pager ? (\n <div data-ui=\"list-chrome-row\" data-row=\"meta\">\n {props.status !== undefined && <div data-ui=\"list-status\">{props.status}</div>}\n {props.actions !== undefined && <div data-ui=\"list-actions\">{props.actions}</div>}\n {props.pager && <PagerBar {...props.pager} />}\n </div>\n ) : null\n const tail =\n filters || meta ? (\n <div data-ui=\"list-chrome-row\" data-row=\"tail\">\n {filters}\n {meta}\n </div>\n ) : null\n return (\n <div data-ui=\"list-chrome\">\n <div data-ui=\"list-chrome-row\" data-row=\"query\">\n {props.search && <SearchControl {...props.search} />}\n {tail}\n </div>\n {props.bulk && <BulkActions {...props.bulk} />}\n </div>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['data-grid-scroll', 'data-grid', 'data-grid-cap', 'data-grid-cell'] as const\n\nexport type DataGridColumn<Row> = {\n key: string\n label: string\n cell: (row: Row) => JSXChild\n visible?: boolean\n pinned?: 'start' | 'end'\n width?: string\n}\n\nexport type DataGridProps<Row> = {\n label: string\n rows: readonly Row[]\n id: (row: Row) => string\n columns: readonly DataGridColumn<Row>[]\n columnOrder?: readonly string[]\n maxRows?: number\n capLabel?: (shown: number, total: number) => string\n density?: 'compact' | 'default' | 'comfortable'\n}\n\nexport const DataGrid = <Row,>(props: DataGridProps<Row>): TemplateResult => {\n const maxRows = Math.max(1, props.maxRows ?? 500)\n const rows = props.rows.slice(0, maxRows)\n const order = props.columnOrder ?? props.columns.map((column) => column.key)\n const byKey = new Map(props.columns.map((column) => [column.key, column]))\n const columns = order\n .map((key) => byKey.get(key))\n .filter((column): column is DataGridColumn<Row> => !!column && column.visible !== false)\n return (\n <div data-ui=\"data-grid-scroll\" data-density={props.density ?? 'default'}>\n {props.rows.length > rows.length && (\n <p data-ui=\"data-grid-cap\" role=\"status\">\n {props.capLabel?.(rows.length, props.rows.length) ??\n `Showing ${rows.length} of ${props.rows.length} rows`}\n </p>\n )}\n <table\n data-ui=\"data-grid\"\n aria-label={props.label}\n style={`--kv-grid-columns: ${columns.map((column) => column.width ?? 'minmax(9rem, 1fr)').join(' ')}`}\n >\n <thead>\n <tr>\n {each(\n columns,\n (column) => column.key,\n (column) => (\n <th scope=\"col\" data-pinned={column.pinned}>\n {column.label}\n </th>\n ),\n )}\n </tr>\n </thead>\n <tbody>\n {each(rows, props.id, (row) => (\n <tr>\n {each(\n columns,\n (column) => column.key,\n (column) => (\n <td data-ui=\"data-grid-cell\" data-pinned={column.pinned}>\n {column.cell(row)}\n </td>\n ),\n )}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['tree', 'tree-item', 'tree-link', 'tree-grid', 'tree-grid-row'] as const\n\nexport type TreeNode = {\n id: string\n label: JSXChild\n href?: string\n active?: boolean\n expanded?: boolean\n children?: readonly TreeNode[]\n}\n\nconst activeNode = (items: readonly TreeNode[]): TreeNode | undefined => {\n for (const item of items) {\n if (item.active) return item\n if (item.children?.length && item.expanded !== false) {\n const nested = activeNode(item.children)\n if (nested) return nested\n }\n }\n return undefined\n}\n\nconst nodes = (items: readonly TreeNode[], level: number, tabbableId: string | undefined): TemplateResult => (\n <ul role={level === 1 ? 'tree' : 'group'}>\n {each(\n items,\n (item) => item.id,\n (item) => (\n <li data-ui=\"tree-item\" role=\"none\">\n {item.href ? (\n <a\n data-ui=\"tree-link\"\n href={item.href}\n role=\"treeitem\"\n tabindex={item.id === tabbableId ? '0' : '-1'}\n aria-level={String(level)}\n aria-expanded={item.children?.length ? String(item.expanded !== false) : null}\n aria-current={item.active ? 'page' : null}\n >\n {item.label}\n </a>\n ) : (\n // biome-ignore lint/a11y/useFocusableInteractive: the serialized lowercase tabindex is the explicit focus contract for this treeitem.\n <span\n data-ui=\"tree-link\"\n role=\"treeitem\"\n tabindex={item.id === tabbableId ? '0' : '-1'}\n aria-level={String(level)}\n aria-expanded={item.children?.length ? String(item.expanded !== false) : null}\n aria-current={item.active ? 'page' : null}\n >\n {item.label}\n </span>\n )}\n {item.children?.length && item.expanded !== false\n ? nodes(item.children, level + 1, tabbableId)\n : null}\n </li>\n ),\n )}\n </ul>\n)\n\nexport const Tree = (props: { label: string; nodes: readonly TreeNode[] }): TemplateResult => (\n <nav data-ui=\"tree\" aria-label={props.label}>\n {nodes(props.nodes, 1, activeNode(props.nodes)?.id ?? props.nodes[0]?.id)}\n </nav>\n)\n\nexport type TreeGridColumn<Row> = { key: string; label: string; cell: (row: Row) => JSXChild }\nexport type TreeGridRow<Row> = { row: Row; level: number; expanded?: boolean; hasChildren?: boolean }\nexport const TreeGrid = <Row,>(props: {\n label: string\n rows: readonly TreeGridRow<Row>[]\n id: (row: Row) => string\n primary: (row: Row) => JSXChild\n columns: readonly TreeGridColumn<Row>[]\n}): TemplateResult => (\n // biome-ignore lint/a11y/noNoninteractiveElementToInteractiveRole: ARIA treegrid intentionally augments the native table model with expandable hierarchical rows.\n <table data-ui=\"tree-grid\" role=\"treegrid\" aria-label={props.label}>\n <thead>\n <tr>\n <th scope=\"col\">Name</th>\n {each(\n props.columns,\n (column) => column.key,\n (column) => (\n <th scope=\"col\">{column.label}</th>\n ),\n )}\n </tr>\n </thead>\n <tbody>\n {each(\n props.rows,\n (item) => props.id(item.row),\n (item) => (\n <tr\n data-ui=\"tree-grid-row\"\n tabindex={\n props.id(item.row) === (props.rows[0] ? props.id(props.rows[0].row) : null) ? '0' : '-1'\n }\n aria-level={String(item.level)}\n aria-expanded={item.hasChildren ? String(item.expanded !== false) : null}\n >\n <th scope=\"row\" style={`--kv-tree-level: ${item.level}`}>\n {props.primary(item.row)}\n </th>\n {each(\n props.columns,\n (column) => column.key,\n (column) => (\n <td>{column.cell(item.row)}</td>\n ),\n )}\n </tr>\n ),\n )}\n </tbody>\n </table>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { Avatar, Badge } from '../../primitives/status/index.tsx'\nimport type { Tone } from '../../primitives/status/index.tsx'\n\nexport const HOOKS = [\n 'description-list',\n 'key-value',\n 'key-value-label',\n 'key-value-value',\n 'person',\n 'person-body',\n 'avatar-group',\n 'status',\n] as const\n\nexport type KeyValueProps = { label: JSXChild; value: JSXChild; emptyLabel?: string }\nexport const KeyValue = (props: KeyValueProps): TemplateResult => (\n <div data-ui=\"key-value\">\n <dt data-ui=\"key-value-label\">{props.label}</dt>\n <dd data-ui=\"key-value-value\">{props.value ?? props.emptyLabel ?? '\u2014'}</dd>\n </div>\n)\n\nexport const DescriptionList = (props: {\n label?: string\n items: readonly (KeyValueProps & { id: string })[]\n columns?: 1 | 2 | 3\n}): TemplateResult => (\n <dl data-ui=\"description-list\" aria-label={props.label} data-columns={String(props.columns ?? 2)}>\n {each(\n props.items,\n (item) => item.id,\n (item) => (\n <KeyValue {...item} />\n ),\n )}\n </dl>\n)\n\nexport type PersonProps = {\n name: string\n detail?: JSXChild\n href?: string\n size?: 'small' | 'default' | 'large'\n}\nexport const Person = (props: PersonProps): TemplateResult => (\n <span data-ui=\"person\">\n <Avatar name={props.name} size={props.size} />\n <span data-ui=\"person-body\">\n {props.href ? <a href={props.href}>{props.name}</a> : <strong>{props.name}</strong>}\n {props.detail !== undefined && <small>{props.detail}</small>}\n </span>\n </span>\n)\n\nexport const AvatarGroup = (props: {\n label: string\n people: readonly { id: string; name: string }[]\n max?: number\n}): TemplateResult => {\n const visible = props.people.slice(0, props.max ?? 4)\n const remaining = props.people.length - visible.length\n return (\n <span data-ui=\"avatar-group\" role=\"group\" aria-label={props.label}>\n {each(\n visible,\n (person) => person.id,\n (person) => (\n <Avatar name={person.name} />\n ),\n )}\n {remaining > 0 && (\n <span role=\"img\" aria-label={`${remaining} more people`} title={`${remaining} more people`}>\n +{String(remaining)}\n </span>\n )}\n </span>\n )\n}\n\nexport const Status = (props: { label: string; tone?: Tone; detail?: string }): TemplateResult => (\n <span data-ui=\"status\" title={props.detail}>\n <Badge label={props.label} tone={props.tone} />\n </span>\n)\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['formatted-date', 'formatted-number', 'formatted-money'] as const\n\nexport const FormattedDate = (props: {\n value: string\n locale?: string\n emptyLabel?: string\n}): TemplateResult => {\n const match = /^(\\d{4})-(\\d{2})-(\\d{2})$/u.exec(props.value)\n const date = match ? new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]))) : null\n const valid =\n !!match &&\n !!date &&\n date.getUTCFullYear() === Number(match[1]) &&\n date.getUTCMonth() === Number(match[2]) - 1 &&\n date.getUTCDate() === Number(match[3])\n const formatted = valid\n ? new Intl.DateTimeFormat(props.locale ?? 'vi-VN', { dateStyle: 'medium', timeZone: 'UTC' }).format(date)\n : (props.emptyLabel ?? '\u2014')\n return (\n <time data-ui=\"formatted-date\" datetime={valid ? props.value : null}>\n {formatted}\n </time>\n )\n}\n\nexport const FormattedNumber = (props: {\n value: number\n locale?: string\n minimumFractionDigits?: number\n maximumFractionDigits?: number\n}): TemplateResult => (\n <data data-ui=\"formatted-number\" value={String(props.value)}>\n {new Intl.NumberFormat(props.locale ?? 'vi-VN', {\n minimumFractionDigits: props.minimumFractionDigits,\n maximumFractionDigits: props.maximumFractionDigits,\n }).format(props.value)}\n </data>\n)\n\nexport const FormattedMoney = (props: {\n value: number\n currency: string\n locale?: string\n}): TemplateResult => (\n <data data-ui=\"formatted-money\" value={String(props.value)} data-currency={props.currency}>\n {new Intl.NumberFormat(props.locale ?? 'vi-VN', { style: 'currency', currency: props.currency }).format(\n props.value,\n )}\n </data>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { ActionGroup } from '../../primitives/actions/index.tsx'\nimport { Section } from '../../layouts/layout/index.tsx'\nimport { DescriptionList, Person, Status } from '../display/index.tsx'\nimport type { KeyValueProps, PersonProps } from '../display/index.tsx'\nimport type { Tone } from '../../primitives/status/index.tsx'\n\nexport const HOOKS = ['record-summary', 'record-summary-main', 'record-actions', 'record-rail'] as const\n\nexport type RecordSummaryProps = {\n title: JSXChild\n subtitle?: JSXChild\n person?: PersonProps\n status?: { label: string; tone?: Tone; detail?: string }\n facts?: readonly (KeyValueProps & { id: string })[]\n}\n\nexport const RecordSummary = (props: RecordSummaryProps): TemplateResult => (\n <header data-ui=\"record-summary\">\n <div data-ui=\"record-summary-main\">\n <h2>{props.title}</h2>\n {props.subtitle !== undefined && <p>{props.subtitle}</p>}\n {props.person && <Person {...props.person} />}\n </div>\n {props.status && <Status {...props.status} />}\n {props.facts && <DescriptionList items={props.facts} columns={3} />}\n </header>\n)\n\nexport const RecordActions = (props: { label?: string; actions: readonly JSXChild[] }): TemplateResult => (\n <nav data-ui=\"record-actions\" aria-label={props.label ?? 'Record actions'}>\n <ActionGroup actions={props.actions} />\n </nav>\n)\n\nexport type RecordRailSection = { id: string; title: string; body: JSXChild; actions?: JSXChild }\nexport const RecordRail = (props: {\n label?: string\n sections: readonly RecordRailSection[]\n}): TemplateResult => (\n <aside data-ui=\"record-rail\" aria-label={props.label ?? 'Record context'}>\n {each(\n props.sections,\n (section) => section.id,\n (section) => (\n <Section title={section.title} body={section.body} actions={section.actions} />\n ),\n )}\n </aside>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { EmptyState, LoadingState, Notice } from '../../primitives/feedback/index.tsx'\nimport { Person } from '../display/index.tsx'\n\nexport const HOOKS = [\n 'activity-timeline',\n 'activity-item',\n 'activity-marker',\n 'audit-log',\n 'audit-item',\n] as const\n\nexport type ActivityItem = {\n id: string\n actor: string\n actorDetail?: string\n action: JSXChild\n datetime: string\n timeLabel: string\n detail?: JSXChild\n redacted?: boolean\n}\n\ntype FeedProps = {\n label: string\n items: readonly ActivityItem[]\n loading?: boolean\n loadingLabel?: string\n emptyTitle?: string\n emptyMessage?: string\n error?: string | null\n redactedLabel?: string\n}\n\nconst state = (props: FeedProps): TemplateResult | null => {\n if (props.loading) return <LoadingState label={props.loadingLabel ?? 'Loading activity'} />\n if (props.error) return <Notice title=\"Activity unavailable\" message={props.error} tone=\"danger\" />\n if (!props.items.length)\n return (\n <EmptyState\n title={props.emptyTitle ?? 'No activity'}\n message={props.emptyMessage ?? 'Nothing has happened yet.'}\n />\n )\n return null\n}\n\nexport const ActivityTimeline = (props: FeedProps): TemplateResult => {\n const placeholder = state(props)\n if (placeholder) return placeholder\n return (\n <ol data-ui=\"activity-timeline\" aria-label={props.label}>\n {each(\n props.items,\n (item) => item.id,\n (item) => (\n <li data-ui=\"activity-item\">\n <span data-ui=\"activity-marker\" aria-hidden=\"true\" />\n <div>\n <Person name={item.actor} detail={item.actorDetail} size=\"small\" />\n <p>{item.redacted ? (props.redactedLabel ?? 'Details redacted') : item.action}</p>\n {item.detail !== undefined && !item.redacted && <div>{item.detail}</div>}\n <time datetime={item.datetime}>{item.timeLabel}</time>\n </div>\n </li>\n ),\n )}\n </ol>\n )\n}\n\nexport const AuditLog = (props: FeedProps): TemplateResult => {\n const placeholder = state(props)\n if (placeholder) return placeholder\n return (\n <ol data-ui=\"audit-log\" aria-label={props.label}>\n {each(\n props.items,\n (item) => item.id,\n (item) => (\n <li data-ui=\"audit-item\">\n <time datetime={item.datetime}>{item.timeLabel}</time>\n <strong>{item.actor}</strong>\n <span>{item.redacted ? (props.redactedLabel ?? 'Change redacted') : item.action}</span>\n </li>\n ),\n )}\n </ol>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { EmptyState, LoadingState, Notice } from '../../primitives/feedback/index.tsx'\n\nexport const HOOKS = ['attachments', 'attachment', 'media-gallery', 'media-item'] as const\n\nexport type AttachmentItem = {\n id: string\n name: string\n href?: string\n meta?: JSXChild\n redacted?: boolean\n}\n\nexport const Attachments = (props: {\n label: string\n items: readonly AttachmentItem[]\n loading?: boolean\n error?: string | null\n redactedLabel?: string\n}): TemplateResult => {\n if (props.loading) return <LoadingState label=\"Loading attachments\" />\n if (props.error) return <Notice title=\"Attachments unavailable\" message={props.error} tone=\"danger\" />\n if (!props.items.length) return <EmptyState title=\"No attachments\" message=\"No files have been attached.\" />\n return (\n <ul data-ui=\"attachments\" aria-label={props.label}>\n {each(\n props.items,\n (item) => item.id,\n (item) => (\n <li data-ui=\"attachment\">\n {item.redacted || !item.href ? (\n <span>{props.redactedLabel ?? item.name}</span>\n ) : (\n <a href={item.href}>{item.name}</a>\n )}\n {item.meta !== undefined && <small>{item.meta}</small>}\n </li>\n ),\n )}\n </ul>\n )\n}\n\nexport type MediaItem = {\n id: string\n src?: string\n alt: string\n caption?: JSXChild\n href?: string\n redacted?: boolean\n}\nexport const MediaGallery = (props: {\n label: string\n items: readonly MediaItem[]\n emptyTitle?: string\n redactedLabel?: string\n}): TemplateResult => {\n if (!props.items.length)\n return <EmptyState title={props.emptyTitle ?? 'No media'} message=\"No media is available.\" />\n return (\n <div data-ui=\"media-gallery\" role=\"group\" aria-label={props.label}>\n {each(\n props.items,\n (item) => item.id,\n (item) => (\n <figure data-ui=\"media-item\">\n {item.redacted || !item.src ? (\n <div role=\"img\" aria-label={props.redactedLabel ?? 'Media redacted'}>\n Restricted\n </div>\n ) : item.href ? (\n <a href={item.href}>\n <img src={item.src} alt={item.alt} loading=\"lazy\" />\n </a>\n ) : (\n <img src={item.src} alt={item.alt} loading=\"lazy\" />\n )}\n {item.caption !== undefined && <figcaption>{item.caption}</figcaption>}\n </figure>\n ),\n )}\n </div>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = [\n 'app-navigation',\n 'navigation-trigger',\n 'navigation-trigger-icon',\n 'navigation-trigger-identity',\n 'navigation-trigger-label',\n 'navigation-layer',\n 'navigation-backdrop',\n 'navigation-drawer',\n 'navigation-header',\n 'navigation-identity',\n 'navigation-context',\n 'navigation-close',\n 'navigation-groups',\n 'navigation-group',\n 'navigation-group-label',\n 'navigation-items',\n 'navigation-supplementary',\n 'navigation-item',\n 'navigation-branch',\n 'navigation-branch-trigger',\n 'navigation-children',\n 'navigation-item-leading',\n 'navigation-item-copy',\n 'navigation-item-label',\n 'navigation-item-description',\n 'navigation-item-count',\n 'navigation-footer',\n] as const\n\ntype NavigationItemBase = {\n id: string\n label: string\n leading?: JSXChild\n description?: string\n count?: number\n}\n\nexport type NavigationItemData = NavigationItemBase &\n (\n | { href: string; active?: boolean; children?: never; expanded?: never }\n | { href?: never; active?: never; children: readonly NavigationItemData[]; expanded?: boolean }\n )\n\nexport type NavigationGroupData = {\n id: string\n label?: string\n items: readonly NavigationItemData[]\n}\n\nexport type AppNavigationProps = {\n id: string\n label: string\n identity: JSXChild\n groups: readonly NavigationGroupData[]\n context?: JSXChild\n supplementary?: JSXChild\n footer?: JSXChild\n navigationSlot?: string\n menuLabel?: string\n closeLabel?: string\n open?: boolean\n}\n\nexport const NavigationTrigger = (props: {\n controls: string\n label: string\n identity: JSXChild\n open?: boolean\n}): TemplateResult => (\n <summary\n data-ui=\"navigation-trigger\"\n aria-controls={props.controls}\n data-open={props.open === true ? 'true' : 'false'}\n >\n <span data-ui=\"navigation-trigger-icon\" aria-hidden=\"true\">\n <span />\n <span />\n <span />\n </span>\n <span data-ui=\"navigation-trigger-identity\">{props.identity}</span>\n <span data-ui=\"navigation-trigger-label\">{props.label}</span>\n </summary>\n)\n\nexport const NavigationHeader = (props: {\n identity: JSXChild\n context?: JSXChild\n closeLabel: string\n}): TemplateResult => (\n <header data-ui=\"navigation-header\">\n <div>\n <div data-ui=\"navigation-identity\">{props.identity}</div>\n {props.context !== undefined && <div data-ui=\"navigation-context\">{props.context}</div>}\n </div>\n <button data-ui=\"navigation-close\" type=\"button\" aria-label={props.closeLabel}>\n <span aria-hidden=\"true\" />\n </button>\n </header>\n)\n\nconst NavigationItemContent = (props: { item: NavigationItemData }): TemplateResult => (\n <>\n {props.item.leading !== undefined && (\n <span data-ui=\"navigation-item-leading\" aria-hidden=\"true\">\n {props.item.leading}\n </span>\n )}\n <span data-ui=\"navigation-item-copy\">\n <span data-ui=\"navigation-item-label\">{props.item.label}</span>\n {props.item.description !== undefined && (\n <span data-ui=\"navigation-item-description\">{props.item.description}</span>\n )}\n </span>\n {props.item.count !== undefined && (\n <span data-ui=\"navigation-item-count\">{String(props.item.count)}</span>\n )}\n </>\n)\n\nconst hasActiveItem = (item: NavigationItemData): boolean =>\n item.active === true || item.children?.some(hasActiveItem) === true\n\nconst renderNavigationItem = (\n props: NavigationItemData,\n branchGroup: string,\n level: number,\n): TemplateResult => {\n if (props.children !== undefined) {\n const open = props.expanded === true || hasActiveItem(props)\n return (\n <details data-ui=\"navigation-branch\" data-level={String(level)} name={branchGroup} open={open}>\n <summary data-ui=\"navigation-branch-trigger\">\n <NavigationItemContent item={props} />\n </summary>\n <div data-ui=\"navigation-children\" data-level={String(level + 1)}>\n {each(\n props.children,\n (item) => item.id,\n (item) => renderNavigationItem(item, `${props.id}-branches`, level + 1),\n )}\n </div>\n </details>\n )\n }\n return (\n <a\n data-ui=\"navigation-item\"\n data-active={props.active === true ? 'true' : null}\n data-level={String(level)}\n href={props.href}\n aria-current={props.active === true ? 'page' : null}\n >\n <NavigationItemContent item={props} />\n </a>\n )\n}\n\nexport const NavigationItem = (props: NavigationItemData): TemplateResult => (\n <>{renderNavigationItem(props, `${props.id}-root-branches`, 1)}</>\n)\n\nexport const NavigationGroup = (props: NavigationGroupData & { branchGroup?: string }): TemplateResult => (\n <section data-ui=\"navigation-group\" aria-labelledby={props.label ? `${props.id}-label` : null}>\n {props.label !== undefined && (\n <h2 data-ui=\"navigation-group-label\" id={`${props.id}-label`}>\n {props.label}\n </h2>\n )}\n <div data-ui=\"navigation-items\">\n {each(\n props.items,\n (item) => item.id,\n (item) => renderNavigationItem(item, props.branchGroup ?? `${props.id}-branches`, 1),\n )}\n </div>\n </section>\n)\n\nexport const NavigationDrawer = (props: {\n id: string\n label: string\n identity: JSXChild\n groups: readonly NavigationGroupData[]\n context?: JSXChild\n supplementary?: JSXChild\n footer?: JSXChild\n navigationSlot?: string\n closeLabel: string\n}): TemplateResult => (\n <div data-ui=\"navigation-layer\">\n <button data-ui=\"navigation-backdrop\" type=\"button\" aria-label={props.closeLabel} tabIndex={-1} />\n <div data-ui=\"navigation-drawer\" id={props.id} data-navigation-label={props.label} tabIndex={-1}>\n <NavigationHeader identity={props.identity} context={props.context} closeLabel={props.closeLabel} />\n <nav data-ui=\"navigation-groups\" aria-label={props.label} data-ket-slot={props.navigationSlot}>\n {each(\n props.groups,\n (group) => group.id,\n (group) => (\n <NavigationGroup {...group} id={`${props.id}-${group.id}`} branchGroup={`${props.id}-branches`} />\n ),\n )}\n {props.supplementary !== undefined && (\n <div data-ui=\"navigation-supplementary\">{props.supplementary}</div>\n )}\n </nav>\n {props.footer !== undefined && <footer data-ui=\"navigation-footer\">{props.footer}</footer>}\n </div>\n </div>\n)\n\nexport const AppNavigation = (props: AppNavigationProps): TemplateResult => {\n const drawerId = `${props.id}-drawer`\n const closeLabel = props.closeLabel ?? 'Close navigation'\n return (\n <details data-ui=\"app-navigation\" data-navigation-id={props.id} open={props.open === true}>\n <NavigationTrigger\n controls={drawerId}\n label={props.menuLabel ?? 'Menu'}\n identity={props.identity}\n open={props.open}\n />\n <NavigationDrawer\n id={drawerId}\n label={props.label}\n identity={props.identity}\n groups={props.groups}\n context={props.context}\n supplementary={props.supplementary}\n footer={props.footer}\n navigationSlot={props.navigationSlot}\n closeLabel={closeLabel}\n />\n </details>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { EmptyState } from '../../primitives/feedback.tsx'\nimport { Surface } from '../../layouts/index.tsx'\n\nexport type Cell = JSXChild\nexport type SortDirection = 'ascending' | 'descending'\nexport type TablePager = {\n label: string\n previousHref?: string | null\n nextHref?: string | null\n}\n\nexport type Column<Row> = {\n key: string\n label: string\n cell: (row: Row) => Cell\n align?: 'start' | 'end'\n kind?: 'text' | 'number' | 'currency' | 'date' | 'status' | 'identifier' | 'person' | 'media'\n priority?: 'primary' | 'secondary' | 'tertiary'\n width?: 'narrow' | 'medium' | 'wide'\n hidden?: boolean\n optional?: boolean\n sort?: {\n href: string\n direction?: SortDirection | null\n label?: string\n } | null\n}\n\nexport type TableSelection = {\n /** Associates row checkboxes with an external bulk-action form. */\n form?: string\n name?: string\n selectAllName?: string\n selectedIds?: readonly string[]\n disabled?: boolean\n}\n\nexport type TableGroup<Row> = {\n id: string\n label: JSXChild\n count?: number\n href?: string | null\n rows?: readonly Row[]\n children?: readonly TableGroup<Row>[]\n pager?: TablePager | null\n}\n\nexport type DataTableLabels<Row> = {\n selectAll?: string\n selectRow?: (row: Row, id: string) => string\n sortedAscending?: string\n sortedDescending?: string\n previousPage?: string\n nextPage?: string\n}\n\nexport type DataTableProps<Row> = {\n /** A shared surface contains the heading and an unframed table viewport. */\n title?: string\n actions?: JSXChild\n columns: readonly Column<Row>[]\n rows: readonly Row[]\n id: (row: Row) => string\n caption?: string | null\n rowHref?: (row: Row) => string\n selected?: (row: Row) => boolean\n emptyTitle?: string\n emptyMessage?: string\n emptyActions?: JSXChild\n /** Stack labelled cells on narrow screens, or retain horizontal scrolling. */\n responsive?: 'scroll' | 'stack'\n /** Use a denser gutter for high-volume operational lists. */\n gutter?: 'default' | 'compact'\n selection?: TableSelection | null\n groups?: readonly TableGroup<Row>[]\n shownColumns?: readonly string[]\n labels?: DataTableLabels<Row>\n}\n\nexport const HOOKS = [\n 'table-scroll',\n 'table',\n 'table-caption',\n 'select-col',\n 'select-all',\n 'col',\n 'sort-link',\n 'sort-icon',\n 'row',\n 'group-row',\n 'group-cell',\n 'group-link',\n 'group-indent',\n 'group-count',\n 'group-pager',\n 'group-pager-actions',\n 'cell',\n 'select-cell',\n 'row-select',\n 'row-link',\n] as const\n\nconst selectedColumnKeys = <Row,>(props: DataTableProps<Row>): readonly string[] =>\n props.shownColumns ?? props.columns.filter((column) => column.hidden !== true).map((column) => column.key)\n\nconst visibleColumns = <Row,>(props: DataTableProps<Row>): readonly Column<Row>[] => {\n const selected = new Set(selectedColumnKeys(props))\n return props.columns.filter((column) => selected.has(column.key))\n}\n\nconst colSpan = <Row,>(props: DataTableProps<Row>, columns: readonly Column<Row>[]): number =>\n columns.length + (props.selection ? 1 : 0)\n\nconst sortLabel = <Row,>(column: Column<Row>, labels: DataTableLabels<Row> | undefined): string | null => {\n if (!column.sort?.direction) return null\n return column.sort.direction === 'ascending'\n ? (labels?.sortedAscending ?? 'Sorted ascending')\n : (labels?.sortedDescending ?? 'Sorted descending')\n}\n\nconst columnHeader = <Row,>(\n column: Column<Row>,\n labels: DataTableLabels<Row> | undefined,\n): TemplateResult => (\n <th\n data-ui=\"col\"\n data-col={column.key}\n data-align={column.align ?? 'start'}\n data-kind={column.kind ?? 'text'}\n data-width={column.width ?? null}\n data-optional={column.optional === true ? 'true' : null}\n data-sort={column.sort?.direction ?? null}\n aria-sort={column.sort?.direction ?? null}\n scope=\"col\"\n >\n {column.sort ? (\n <a data-ui=\"sort-link\" href={column.sort.href} aria-label={column.sort.label ?? column.label}>\n <span>{column.label}</span>\n {!!column.sort.direction && (\n <span data-ui=\"sort-icon\" aria-hidden=\"true\" title={sortLabel(column, labels) ?? undefined}>\n {column.sort.direction === 'ascending' ? '\u2191' : '\u2193'}\n </span>\n )}\n </a>\n ) : (\n column.label\n )}\n </th>\n)\n\nconst selectionHeader = <Row,>(props: DataTableProps<Row>): TemplateResult | null =>\n props.selection ? (\n <th data-ui=\"select-col\" scope=\"col\">\n <input\n data-ui=\"select-all\"\n type=\"checkbox\"\n name={props.selection.selectAllName ?? 'select-all'}\n aria-label={props.labels?.selectAll ?? 'Select all rows'}\n disabled={props.selection.disabled === true}\n />\n </th>\n ) : null\n\nconst isSelected = <Row,>(props: DataTableProps<Row>, row: Row): boolean => {\n if (props.selected?.(row) === true) return true\n return props.selection?.selectedIds?.includes(props.id(row)) === true\n}\n\nconst rowCells = <Row,>(\n props: DataTableProps<Row>,\n columns: readonly Column<Row>[],\n row: Row,\n): TemplateResult => (\n <>\n {props.selection && (\n <td data-ui=\"select-cell\">\n <input\n data-ui=\"row-select\"\n type=\"checkbox\"\n name={props.selection.name ?? 'ids'}\n form={props.selection.form}\n value={props.id(row)}\n checked={isSelected(props, row)}\n aria-label={props.labels?.selectRow?.(row, props.id(row)) ?? `Select row ${props.id(row)}`}\n disabled={props.selection.disabled === true}\n />\n </td>\n )}\n {each(\n columns,\n (column) => column.key,\n (column) => (\n <td\n data-ui=\"cell\"\n data-col={column.key}\n data-linked={props.rowHref && column === columns[0] ? 'true' : null}\n data-align={column.align ?? 'start'}\n data-kind={column.kind ?? 'text'}\n data-priority={column.priority ?? 'secondary'}\n data-label={props.responsive === 'stack' ? column.label : null}\n >\n {props.rowHref && column === columns[0] ? (\n <a data-ui=\"row-link\" href={props.rowHref(row)}>\n {column.cell(row)}\n </a>\n ) : (\n column.cell(row)\n )}\n </td>\n ),\n )}\n </>\n)\n\nconst tableRow = <Row,>(\n props: DataTableProps<Row>,\n columns: readonly Column<Row>[],\n row: Row,\n): TemplateResult => (\n <tr data-ui=\"row\" data-row={props.id(row)} data-selected={isSelected(props, row) ? 'true' : null}>\n {rowCells(props, columns, row)}\n </tr>\n)\n\nconst groupPager = <Row,>(\n props: DataTableProps<Row>,\n columns: readonly Column<Row>[],\n pager: TablePager | null | undefined,\n): TemplateResult | null =>\n pager ? (\n <tr data-ui=\"group-pager\">\n <td colSpan={String(colSpan(props, columns))}>\n <span>{pager.label}</span>\n <span data-ui=\"group-pager-actions\">\n {pager.previousHref && (\n <a href={pager.previousHref} aria-label={props.labels?.previousPage ?? 'Previous page'}>\n \u2039\n </a>\n )}\n {pager.nextHref && (\n <a href={pager.nextHref} aria-label={props.labels?.nextPage ?? 'Next page'}>\n \u203A\n </a>\n )}\n </span>\n </td>\n </tr>\n ) : null\n\nconst groupRows = <Row,>(\n props: DataTableProps<Row>,\n columns: readonly Column<Row>[],\n group: TableGroup<Row>,\n depth: number,\n): TemplateResult => (\n <>\n <tr data-ui=\"group-row\" data-group={group.id} data-depth={String(depth)}>\n <th data-ui=\"group-cell\" colSpan={String(colSpan(props, columns))} scope=\"rowgroup\">\n <span data-ui=\"group-indent\" style={`--kv-group-depth: ${depth}`} aria-hidden=\"true\" />\n {group.href ? (\n <a data-ui=\"group-link\" href={group.href}>\n {group.label}\n </a>\n ) : (\n <span data-ui=\"group-link\">{group.label}</span>\n )}\n {group.count !== undefined && <span data-ui=\"group-count\">{String(group.count)}</span>}\n </th>\n </tr>\n {each(group.rows ?? [], props.id, (row) => tableRow(props, columns, row))}\n {each(\n group.children ?? [],\n (child) => child.id,\n (child) => groupRows(props, columns, child, depth + 1),\n )}\n {groupPager(props, columns, group.pager)}\n </>\n)\n\nexport const DataTable = <Row,>(props: DataTableProps<Row>): TemplateResult => {\n const columns = visibleColumns(props)\n const groups = props.groups ?? []\n const hasRows = props.rows.length > 0 || groups.length > 0\n\n const content = hasRows ? (\n <div\n data-ui=\"table-scroll\"\n data-framed={props.title ? 'false' : null}\n data-responsive={props.responsive ?? 'scroll'}\n data-gutter={props.gutter === 'compact' ? 'compact' : null}\n >\n <table data-ui=\"table\" aria-label={props.caption ? null : (props.title ?? null)}>\n {!!props.caption && <caption data-ui=\"table-caption\">{props.caption}</caption>}\n <thead>\n <tr>\n {selectionHeader(props)}\n {each(\n columns,\n (column) => column.key,\n (column) => columnHeader(column, props.labels),\n )}\n </tr>\n </thead>\n <tbody>\n {groups.length > 0\n ? each(\n groups,\n (group) => group.id,\n (group) => groupRows(props, columns, group, 0),\n )\n : each(props.rows, props.id, (row) => tableRow(props, columns, row))}\n </tbody>\n </table>\n </div>\n ) : (\n <EmptyState\n title={props.emptyTitle ?? 'No records'}\n message={props.emptyMessage ?? 'There is nothing to show yet.'}\n actions={props.emptyActions}\n />\n )\n return props.title ? (\n <Surface title={props.title} actions={props.actions} padding=\"none\" body={content} />\n ) : (\n content\n )\n}\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport type PageIdentityKind =\n | 'list-page'\n | 'record-page'\n | 'form-page'\n | 'workspace-page'\n | 'dashboard-page'\n | 'board-page'\n\nexport type PageIdentityProps = {\n context?: JSXChild\n eyebrow?: string | null\n title: string\n description?: string | null\n status?: JSXChild\n actions?: JSXChild\n meta?: JSXChild\n}\n\ntype PageIdentityContentProps = PageIdentityProps & {\n groupDescription?: boolean\n}\n\n/**\n * The single identity band shared by every full-page pattern.\n *\n * The pattern name remains in each hook so consumers can style the content\n * contract without reimplementing breadcrumbs, title hierarchy or actions.\n */\nexport const pageIdentityContent = (\n kind: PageIdentityKind,\n props: PageIdentityContentProps,\n): TemplateResult => (\n <>\n <div data-ui={`${kind}-heading`}>\n {!!props.eyebrow && <p data-ui={`${kind}-eyebrow`}>{props.eyebrow}</p>}\n <div data-ui={`${kind}-title-row`}>\n <h1 data-ui={`${kind}-title`}>{props.title}</h1>\n {props.actions !== undefined && <div data-ui={`${kind}-actions`}>{props.actions}</div>}\n </div>\n {(props.groupDescription || props.status !== undefined) &&\n (!!props.description || props.status !== undefined) ? (\n <div data-ui={`${kind}-subline`}>\n {!!props.description && <p data-ui={`${kind}-description`}>{props.description}</p>}\n {props.status !== undefined && <span data-ui={`${kind}-status`}>{props.status}</span>}\n </div>\n ) : (\n !!props.description && <p data-ui={`${kind}-description`}>{props.description}</p>\n )}\n </div>\n {props.meta !== undefined && <div data-ui={`${kind}-meta`}>{props.meta}</div>}\n </>\n)\n\nexport const pageIdentity = (kind: PageIdentityKind, props: PageIdentityProps): TemplateResult => (\n <>\n {props.context !== undefined && <div data-ui={`${kind}-context`}>{props.context}</div>}\n <header data-ui={`${kind}-header`}>{pageIdentityContent(kind, props)}</header>\n </>\n)\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { pageIdentity } from '../page-shell.tsx'\n\nexport const HOOKS = [\n 'list-page',\n 'list-page-context',\n 'list-page-header',\n 'list-page-heading',\n 'list-page-title-row',\n 'list-page-eyebrow',\n 'list-page-title',\n 'list-page-description',\n 'list-page-actions',\n 'list-page-toolbar',\n 'list-page-controls',\n 'list-page-status',\n 'list-page-body',\n 'list-page-footer',\n] as const\n\nexport type ListPageProps = {\n title: string\n body: JSXChild\n /**\n * A narrow application context strip above the page identity. The application\n * owns its breadcrumbs and organisation switcher; the pattern owns the slot.\n */\n context?: JSXChild\n /**\n * `operational` is the full workspace composition used by application collection\n * screens. The unadorned variant remains available for embedded specimens.\n */\n variant?: 'operational'\n eyebrow?: string | null\n description?: string | null\n actions?: JSXChild\n controls?: JSXChild\n status?: JSXChild\n footer?: JSXChild\n}\n\n/**\n * The baseline for an operational collection screen.\n *\n * Applications own the translated copy, URL-driven controls and records. This\n * pattern owns their order and rhythm: identity and primary action first, query\n * controls second, then the result status and the collection itself.\n */\nexport const ListPage = (props: ListPageProps): TemplateResult => {\n const operational = props.variant === 'operational'\n // Result summaries close the collection in an operational workspace. Keeping\n // the legacy `status` prop as the input lets existing screens migrate without\n // duplicating their translated count as a separate footer value.\n const toolbarStatus = operational ? undefined : props.status\n const footer = props.footer ?? (operational ? props.status : undefined)\n\n return (\n <section data-ui=\"list-page\" data-variant={props.variant ?? null} data-pattern=\"list\">\n {pageIdentity('list-page', {\n context: props.context,\n eyebrow: props.eyebrow,\n title: props.title,\n description: props.description,\n actions: props.actions,\n })}\n {(props.controls !== undefined || toolbarStatus !== undefined) && (\n <div data-ui=\"list-page-toolbar\">\n {toolbarStatus !== undefined && <div data-ui=\"list-page-status\">{toolbarStatus}</div>}\n {props.controls !== undefined && <div data-ui=\"list-page-controls\">{props.controls}</div>}\n </div>\n )}\n <div data-ui=\"list-page-body\">{props.body}</div>\n {footer !== undefined && <footer data-ui=\"list-page-footer\">{footer}</footer>}\n </section>\n )\n}\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { pageIdentityContent, type PageIdentityKind } from '../page-shell.tsx'\n\nexport const HOOKS = [\n 'record-page',\n 'record-page-context',\n 'record-page-header',\n 'record-page-title-row',\n 'record-page-heading',\n 'record-page-title',\n 'record-page-subline',\n 'record-page-status',\n 'record-page-description',\n 'record-page-actions',\n 'record-page-meta',\n 'record-page-controller',\n 'record-page-navigation',\n 'record-page-layout',\n 'record-page-body',\n 'record-page-aside',\n] as const\n\nexport type RecordPageSlots = {\n header: string\n body: string\n fragmentTitle?: string\n}\n\nexport type RecordPageProps = {\n title: string\n body: JSXChild\n context?: JSXChild\n variant?: 'operational'\n width?: 'default' | 'wide'\n scope?: string | null\n description?: string | null\n status?: JSXChild\n actions?: JSXChild\n meta?: JSXChild\n controller?: JSXChild\n navigation?: JSXChild\n aside?: JSXChild\n asideLabel?: string | null\n slots?: RecordPageSlots\n}\n\ntype RecordPageKind = Extract<PageIdentityKind, 'record-page' | 'form-page'>\n\nexport const recordPage = (props: RecordPageProps, compatibilityKind?: RecordPageKind): TemplateResult => {\n const kind = compatibilityKind ?? 'record-page'\n if (props.slots?.fragmentTitle !== undefined)\n return (\n <ket-fragments data-title={props.slots.fragmentTitle}>\n <template data-ket-slot={props.slots.header}>\n {pageIdentityContent(kind, { ...props, groupDescription: true })}\n </template>\n <template data-ket-slot={props.slots.body}>{props.body}</template>\n </ket-fragments>\n )\n return (\n <section\n data-ui={kind}\n data-scope={props.scope ?? null}\n data-has-aside={String(props.aside !== undefined)}\n data-variant={props.variant ?? null}\n data-width={props.width ?? 'default'}\n data-pattern=\"record\"\n >\n {props.context !== undefined && <div data-ui={`${kind}-context`}>{props.context}</div>}\n <header data-ui={`${kind}-header`} data-ket-slot={props.slots?.header}>\n {pageIdentityContent(kind, { ...props, groupDescription: true })}\n </header>\n {props.controller !== undefined && <div data-ui={`${kind}-controller`}>{props.controller}</div>}\n {props.navigation !== undefined && <div data-ui={`${kind}-navigation`}>{props.navigation}</div>}\n <div data-ui={`${kind}-layout`}>\n <div data-ui={`${kind}-body`} data-ket-slot={props.slots?.body}>\n {props.body}\n </div>\n {props.aside !== undefined && (\n <aside data-ui={`${kind}-aside`} aria-label={props.asideLabel ?? null}>\n {props.aside}\n </aside>\n )}\n </div>\n </section>\n )\n}\n\n/** One durable subject: create, edit, inspect or advance its workflow. */\nexport const RecordPage = (props: RecordPageProps): TemplateResult => recordPage(props)\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport { recordPage, type RecordPageProps, type RecordPageSlots } from '../record-page.tsx'\n\nexport const HOOKS = [\n 'form-page',\n 'form-page-context',\n 'form-page-header',\n 'form-page-title-row',\n 'form-page-heading',\n 'form-page-title',\n 'form-page-subline',\n 'form-page-status',\n 'form-page-description',\n 'form-page-actions',\n 'form-page-meta',\n 'form-page-controller',\n 'form-page-navigation',\n 'form-page-layout',\n 'form-page-body',\n 'form-page-aside',\n] as const\n\nexport type FormPageSlots = RecordPageSlots\nexport type FormPageProps = RecordPageProps\n\n/** @deprecated Use RecordPage. Retained while application call sites migrate. */\nexport const FormPage = (props: FormPageProps): TemplateResult => recordPage(props, 'form-page')\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { pageIdentity, type PageIdentityKind } from '../page-shell.tsx'\n\nexport const HOOKS = [\n 'workspace-page',\n 'workspace-page-context',\n 'workspace-page-header',\n 'workspace-page-heading',\n 'workspace-page-eyebrow',\n 'workspace-page-title-row',\n 'workspace-page-title',\n 'workspace-page-subline',\n 'workspace-page-status',\n 'workspace-page-description',\n 'workspace-page-actions',\n 'workspace-page-meta',\n 'workspace-page-toolbar',\n 'workspace-page-controls',\n 'workspace-page-body',\n] as const\n\nexport type WorkspacePageProps = {\n title: string\n body: JSXChild\n context?: JSXChild\n variant?: 'operational'\n /** Vertical work surfaces use flow; boards, timelines and maps use canvas. */\n layout?: 'flow' | 'canvas'\n eyebrow?: string | null\n description?: string | null\n status?: JSXChild\n meta?: JSXChild\n actions?: JSXChild\n controls?: JSXChild\n}\n\ntype CompatibilityKind = Extract<PageIdentityKind, 'dashboard-page' | 'board-page'>\n\nexport const workspacePage = (\n props: WorkspacePageProps,\n compatibilityKind?: CompatibilityKind,\n): TemplateResult => {\n const kind = compatibilityKind ?? (props.layout === 'canvas' ? 'board-page' : 'dashboard-page')\n return (\n <section\n data-ui={kind}\n data-variant={props.variant ?? null}\n data-layout={props.layout ?? 'flow'}\n data-pattern=\"workspace\"\n >\n {pageIdentity(kind, props)}\n {props.controls !== undefined && (\n <div data-ui={`${kind}-toolbar`}>\n <div data-ui={`${kind}-controls`}>{props.controls}</div>\n </div>\n )}\n <div data-ui={`${kind}-body`}>{props.body}</div>\n </section>\n )\n}\n\n/** One operational canvas, with flow and horizontal-canvas layout modes. */\nexport const WorkspacePage = (props: WorkspacePageProps): TemplateResult => workspacePage(props)\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport { workspacePage, type WorkspacePageProps } from '../workspace-page.tsx'\n\nexport const HOOKS = [\n 'dashboard-page',\n 'dashboard-page-context',\n 'dashboard-page-header',\n 'dashboard-page-heading',\n 'dashboard-page-eyebrow',\n 'dashboard-page-title-row',\n 'dashboard-page-title',\n 'dashboard-page-subline',\n 'dashboard-page-status',\n 'dashboard-page-description',\n 'dashboard-page-actions',\n 'dashboard-page-meta',\n 'dashboard-page-body',\n] as const\n\nexport type DashboardPageProps = Omit<WorkspacePageProps, 'layout' | 'controls'>\n\n/**\n * The baseline for KPI, workflow and analytical overview screens.\n *\n * Applications own the translated copy, business actions and dashboard blocks.\n * This pattern keeps location, identity and the overview canvas in a stable,\n * compact hierarchy without pretending that the dashboard is a business record.\n *\n * @deprecated Compose the overview recipe with WorkspacePage layout=\"flow\".\n */\nexport const DashboardPage = (props: DashboardPageProps): TemplateResult =>\n workspacePage({ ...props, layout: 'flow' }, 'dashboard-page')\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport { workspacePage, type WorkspacePageProps } from '../workspace-page.tsx'\n\nexport const HOOKS = [\n 'board-page',\n 'board-page-context',\n 'board-page-header',\n 'board-page-heading',\n 'board-page-eyebrow',\n 'board-page-title-row',\n 'board-page-title',\n 'board-page-subline',\n 'board-page-status',\n 'board-page-description',\n 'board-page-actions',\n 'board-page-meta',\n 'board-page-toolbar',\n 'board-page-controls',\n 'board-page-body',\n] as const\n\nexport type BoardPageProps = Omit<WorkspacePageProps, 'layout'>\n\n/**\n * The baseline for kanban, schedule and timeline workspaces.\n *\n * A board needs more horizontal room than a collection or form, but it still\n * belongs to the same application hierarchy. The pattern keeps location,\n * identity and global controls in stable compact bands while leaving scrolling,\n * columns and interaction to the specialised board supplied by the application.\n *\n * @deprecated Compose canvas recipes with WorkspacePage layout=\"canvas\".\n */\nexport const BoardPage = (props: BoardPageProps): TemplateResult =>\n workspacePage({ ...props, layout: 'canvas' }, 'board-page')\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport type { Tone } from '../../primitives/status.tsx'\n\nexport const HOOKS = [\n 'pipeline',\n 'pipeline-step',\n 'pipeline-arrow',\n 'pipeline-step-label',\n 'pipeline-step-value',\n 'pipeline-step-node',\n] as const\n\nexport type PipelineStep = {\n id: string\n label: string\n value: string | number\n href?: string | null\n tone?: Tone\n}\n\n/**\n * A process read left to right: the stages of a thing, and how many are sitting\n * in each one.\n *\n * Not a `Progress`, which answers \"how far along is this one record\". This\n * answers \"where is the work piling up\", so every stage carries its own count\n * and none of them is a percentage of the others \u2014 a stage can hold more than\n * the one before it, and a bar that normalised them would say the opposite.\n *\n * An ordered list, because the order is the meaning. The connecting rail and the\n * arrows between stages are drawn by the stylesheet from `pipeline-step-node`\n * and `pipeline-arrow`; both are decoration for an order the markup already\n * states, which is why the arrow carries no text to announce.\n */\nexport const Pipeline = (props: { label: string; steps: readonly PipelineStep[] }): TemplateResult => (\n <ol data-ui=\"pipeline\" aria-label={props.label}>\n {each(\n props.steps,\n (step) => step.id,\n (step) => (\n <li data-ui=\"pipeline-step\" data-tone={step.tone ?? 'neutral'}>\n <span data-ui=\"pipeline-arrow\" aria-hidden=\"true\" />\n {step.href ? (\n <a data-ui=\"pipeline-step-label\" href={step.href}>\n {step.label}\n </a>\n ) : (\n <span data-ui=\"pipeline-step-label\">{step.label}</span>\n )}\n <span data-ui=\"pipeline-step-value\">{String(step.value)}</span>\n <span data-ui=\"pipeline-step-node\" aria-hidden=\"true\" />\n </li>\n ),\n )}\n </ol>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport { ActionGroup, Button, LinkButton } from '../../primitives/actions.tsx'\nimport type { ActionVariant } from '../../primitives/actions.tsx'\nimport { Field } from '../../primitives/field.tsx'\nimport type { FieldProps } from '../../primitives/field.tsx'\n\nexport const HOOKS = ['record-form', 'form-errors', 'form-grid', 'form-actions'] as const\n\nexport type RecordFormProps = {\n id?: string | null\n action: string\n method?: 'get' | 'post'\n fields: readonly FieldProps[]\n submitLabel: string\n submitVariant?: ActionVariant\n cancelHref?: string | null\n cancelLabel?: string | null\n errors?: readonly string[]\n hidden?: Record<string, string>\n}\n\nexport const RecordForm = (props: RecordFormProps): TemplateResult => (\n <form\n data-ui=\"record-form\"\n id={props.id ?? undefined}\n action={props.action}\n method={props.method ?? 'post'}\n >\n {Object.entries(props.hidden ?? {}).map(([name, value]) => (\n <input type=\"hidden\" name={name} value={value} />\n ))}\n {!!props.errors?.length && (\n <ul data-ui=\"form-errors\" role=\"alert\">\n {each(\n props.errors,\n (error, index) => `${index}:${error}`,\n (error) => (\n <li>{error}</li>\n ),\n )}\n </ul>\n )}\n <div data-ui=\"form-grid\">\n {each(\n props.fields,\n (field) => field.id,\n (field) => (\n <Field {...field} />\n ),\n )}\n </div>\n <div data-ui=\"form-actions\">\n <ActionGroup\n actions={[\n <Button label={props.submitLabel} variant={props.submitVariant ?? 'primary'} type=\"submit\" />,\n ...(props.cancelHref\n ? [\n <LinkButton\n label={props.cancelLabel ?? 'Cancel'}\n href={props.cancelHref}\n variant=\"tertiary\"\n />,\n ]\n : []),\n ]}\n />\n </div>\n </form>\n)\n", "import { HOOKS as actionHooks } from '../primitives/actions/index.tsx'\nimport { HOOKS as feedbackHooks } from '../primitives/feedback/index.tsx'\nimport { HOOKS as fieldHooks } from '../primitives/field/index.tsx'\nimport { HOOKS as statusHooks } from '../primitives/status/index.tsx'\nimport { HOOKS as navigationHooks } from '../primitives/navigation/index.tsx'\nimport { HOOKS as progressHooks } from '../primitives/progress/index.tsx'\nimport { HOOKS as menuHooks } from '../interactions/menu/index.tsx'\nimport { HOOKS as popoverHooks } from '../interactions/popover/index.tsx'\nimport { HOOKS as tooltipHooks } from '../interactions/tooltip/index.tsx'\nimport { HOOKS as dialogHooks } from '../interactions/dialog/index.tsx'\nimport { HOOKS as toastHooks } from '../interactions/toast/index.tsx'\nimport { HOOKS as spinnerHooks } from '../interactions/spinner/index.tsx'\nimport { HOOKS as skeletonHooks } from '../interactions/skeleton/index.tsx'\nimport { HOOKS as scalarFieldHooks } from '../forms/scalar-fields/index.tsx'\nimport { HOOKS as comboboxHooks } from '../forms/combobox/index.tsx'\nimport { HOOKS as dateTimeHooks } from '../forms/date-time/index.tsx'\nimport { HOOKS as uploadHooks } from '../forms/upload/index.tsx'\nimport { HOOKS as relationPickerHooks } from '../forms/relation-picker/index.tsx'\nimport { HOOKS as listControlHooks } from '../data-operations/list-controls/index.tsx'\nimport { HOOKS as inlineEditHooks } from '../data-operations/inline-edit/index.tsx'\nimport { HOOKS as resourceListHooks } from '../data-display/resource-list/index.tsx'\nimport { HOOKS as dataGridHooks } from '../data-display/data-grid/index.tsx'\nimport { HOOKS as treeHooks } from '../data-display/tree/index.tsx'\nimport { HOOKS as recordDisplayHooks } from '../record/display/index.tsx'\nimport { HOOKS as formattedValueHooks } from '../record/formatted-values/index.tsx'\nimport { HOOKS as recordCompositionHooks } from '../record/composition/index.tsx'\nimport { HOOKS as activityHooks } from '../record/activity/index.tsx'\nimport { HOOKS as mediaHooks } from '../record/media/index.tsx'\nimport { HOOKS as layoutHooks } from '../layouts/layout/index.tsx'\nimport { HOOKS as shellHooks } from '../layouts/shell/index.tsx'\nimport { HOOKS as appNavigationHooks } from '../layouts/app-navigation/index.tsx'\nimport { HOOKS as tableHooks } from '../patterns/data-table/index.tsx'\nimport { HOOKS as listChromeHooks } from '../patterns/list-chrome/index.tsx'\nimport { HOOKS as listPageHooks } from '../patterns/list-page/index.tsx'\nimport { HOOKS as formPageHooks } from '../patterns/form-page/index.tsx'\nimport { HOOKS as recordPageHooks } from '../patterns/record-page/index.tsx'\nimport { HOOKS as dashboardPageHooks } from '../patterns/dashboard-page/index.tsx'\nimport { HOOKS as boardPageHooks } from '../patterns/board-page/index.tsx'\nimport { HOOKS as modalHooks } from '../patterns/modal-sheet/index.tsx'\nimport { HOOKS as pipelineHooks } from '../patterns/pipeline/index.tsx'\nimport { HOOKS as formHooks } from '../patterns/record-form/index.tsx'\n\nconst GROUPS = {\n actions: actionHooks,\n feedback: feedbackHooks,\n fields: fieldHooks,\n status: statusHooks,\n navigation: navigationHooks,\n progress: progressHooks,\n menu: menuHooks,\n popover: popoverHooks,\n tooltip: tooltipHooks,\n dialog: dialogHooks,\n toast: toastHooks,\n spinner: spinnerHooks,\n skeleton: skeletonHooks,\n scalarFields: scalarFieldHooks,\n combobox: comboboxHooks,\n dateTime: dateTimeHooks,\n upload: uploadHooks,\n relationPicker: relationPickerHooks,\n listControls: listControlHooks,\n inlineEdit: inlineEditHooks,\n resourceList: resourceListHooks,\n dataGrid: dataGridHooks,\n tree: treeHooks,\n recordDisplay: recordDisplayHooks,\n formattedValues: formattedValueHooks,\n recordComposition: recordCompositionHooks,\n activity: activityHooks,\n media: mediaHooks,\n layouts: layoutHooks,\n shell: shellHooks,\n appNavigation: appNavigationHooks,\n table: tableHooks,\n listChrome: listChromeHooks,\n listPage: listPageHooks,\n formPage: formPageHooks,\n recordPage: recordPageHooks,\n dashboardPage: dashboardPageHooks,\n boardPage: boardPageHooks,\n modal: modalHooks,\n pipeline: pipelineHooks,\n form: formHooks,\n} as const\n\nexport const HOOKS: readonly string[] = [...new Set(Object.values(GROUPS).flat())].sort()\n\nexport const OWNERS: Readonly<Record<string, string[]>> = Object.freeze(\n Object.fromEntries(Object.entries(GROUPS).map(([owner, hooks]) => [owner, [...hooks]])),\n)\n", "import { installUserWorkflow } from './user-workflow.ts'\nimport type { BrowserBehavior, BrowserNavigation } from '@ketvietlab/ketjs'\nimport { attachDesignSystemInteractions } from '@ketvietlab/design-system'\n\nconst themeStorageKey = 'ket.backend.theme'\nconst dismissibleDropdown = [\n '[data-ui=\"search-menu\"]',\n '[data-ui=\"col-config\"]',\n '[data-ui=\"bulk-actions\"]',\n '[data-ui=\"viewer\"]',\n '[data-ui=\"record-more\"]',\n].join(', ')\n\ntype KetBrowserGlobals = typeof globalThis & {\n confirm: (message?: string) => boolean\n}\n\nconst browserGlobals = globalThis as KetBrowserGlobals\nconst eventElement = (event: Event): Element | null => (event.target instanceof Element ? event.target : null)\n\ntype ThemePreference = 'light' | 'dark'\n\nconst themePreference = (value: string | null | undefined): ThemePreference | null =>\n value === 'light' || value === 'dark' ? value : null\n\nconst storedTheme = (): ThemePreference | null => {\n try {\n return themePreference(localStorage.getItem(themeStorageKey))\n } catch {\n return null\n }\n}\n\nconst saveTheme = (theme: ThemePreference): void => {\n try {\n localStorage.setItem(themeStorageKey, theme)\n } catch {\n // Theme selection still works for this document when storage is unavailable.\n }\n}\n\nconst resolvedTheme = (): ThemePreference =>\n themePreference(document.documentElement.dataset.theme) ??\n (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')\n\nconst syncThemeToggle = (): void => {\n const pressed = String(resolvedTheme() === 'dark')\n for (const control of document.querySelectorAll<HTMLElement>(\n '[data-ui=\"sidebar-tools\"] [data-ui=\"action\"][name=\"theme\"]',\n ))\n control.setAttribute('aria-pressed', pressed)\n}\n\n/** Keep the shell preference explicit only after the reader chooses one. */\nconst installThemeToggle = (signal: AbortSignal): void => {\n const preference = storedTheme()\n if (preference) document.documentElement.dataset.theme = preference\n syncThemeToggle()\n\n document.addEventListener(\n 'click',\n (event) => {\n const control = eventElement(event)?.closest<HTMLButtonElement>(\n '[data-ui=\"sidebar-tools\"] [data-ui=\"action\"][name=\"theme\"]',\n )\n if (!control || control.disabled) return\n const next = resolvedTheme() === 'dark' ? 'light' : 'dark'\n document.documentElement.dataset.theme = next\n saveTheme(next)\n syncThemeToggle()\n },\n { signal },\n )\n\n matchMedia('(prefers-color-scheme: dark)').addEventListener(\n 'change',\n () => {\n if (!themePreference(document.documentElement.dataset.theme)) syncThemeToggle()\n },\n { signal },\n )\n\n addEventListener(\n 'storage',\n (event) => {\n if (event.key !== themeStorageKey) return\n const next = themePreference(event.newValue)\n if (next) document.documentElement.dataset.theme = next\n else delete document.documentElement.dataset.theme\n syncThemeToggle()\n },\n { signal },\n )\n}\n\nconst updateSelection = (table: Element): void => {\n const rows = [...table.querySelectorAll<HTMLInputElement>('[data-ui=\"row-select\"]:not(:disabled)')]\n const checked = rows.filter((input) => input.checked).length\n const all = table.querySelector<HTMLInputElement>('[data-ui=\"select-all\"]')\n if (all) {\n all.checked = rows.length > 0 && checked === rows.length\n all.indeterminate = checked > 0 && checked < rows.length\n }\n if (!checked) {\n const shell = table.closest('[data-ui=\"shell\"]')\n for (const menu of shell?.querySelectorAll<HTMLDetailsElement>('[data-ui=\"bulk-actions\"][open]') ?? [])\n menu.removeAttribute('open')\n }\n}\n\nconst installTableSelection = (signal: AbortSignal, navigation: BrowserNavigation): void => {\n document.addEventListener(\n 'change',\n (event) => {\n const target = eventElement(event)\n if (!(target instanceof HTMLInputElement)) return\n if (!target.matches('[data-ui=\"select-all\"], [data-ui=\"row-select\"]')) return\n const table = target.closest('[data-ui=\"table\"]')\n if (!table) return\n if (target.matches('[data-ui=\"select-all\"]'))\n for (const input of table.querySelectorAll<HTMLInputElement>('[data-ui=\"row-select\"]:not(:disabled)'))\n input.checked = target.checked\n updateSelection(table)\n },\n { signal },\n )\n\n document.addEventListener(\n 'click',\n (event) => {\n if (\n event.defaultPrevented ||\n event.button !== 0 ||\n event.metaKey ||\n event.ctrlKey ||\n event.shiftKey ||\n event.altKey\n )\n return\n const target = eventElement(event)\n if (\n target?.closest(\n 'a, button, input, select, textarea, label, summary, details, [data-ui=\"select-cell\"]',\n )\n )\n return\n const row = target?.closest<HTMLElement>('[data-ui=\"row\"][data-row-href]')\n const href = row?.getAttribute('data-row-href')\n if (!href) return\n event.preventDefault()\n void navigation.navigate(href)\n },\n { signal },\n )\n document.addEventListener(\n 'keydown',\n (event) => {\n if (event.defaultPrevented || (event.key !== 'Enter' && event.key !== ' ')) return\n const target = eventElement(event)\n const row = target?.matches('[data-ui=\"row\"][data-row-href][tabindex=\"0\"]') ? target : null\n const href = row?.getAttribute('data-row-href')\n if (!href) return\n event.preventDefault()\n navigateTo(href, navigation)\n },\n { signal },\n )\n}\n\nconst openDropdowns = (): HTMLDetailsElement[] => [\n ...document.querySelectorAll<HTMLDetailsElement>(`${dismissibleDropdown}[open]`),\n]\n\nconst installDropdownDismiss = (signal: AbortSignal): void => {\n document.addEventListener(\n 'click',\n (event) => {\n const current = eventElement(event)?.closest(dismissibleDropdown)\n for (const dropdown of openDropdowns()) {\n if (dropdown !== current && !dropdown.contains(current ?? null)) dropdown.removeAttribute('open')\n }\n },\n { signal },\n )\n\n document.addEventListener(\n 'keydown',\n (event) => {\n if (event.key !== 'Escape') return\n const dropdowns = openDropdowns()\n if (!dropdowns.length) return\n const focused = document.activeElement?.closest<HTMLDetailsElement>(dismissibleDropdown)\n const current = focused?.open ? focused : dropdowns.at(-1)\n current?.removeAttribute('open')\n current?.querySelector<HTMLElement>('summary')?.focus()\n event.preventDefault()\n },\n { signal },\n )\n}\n\nconst installGlobalFilter = (signal: AbortSignal): void => {\n document.addEventListener(\n 'click',\n (event) => {\n const trigger = eventElement(event)?.closest<HTMLElement>('[data-ui=\"chrome-search-toggle\"]')\n const controls = trigger?.getAttribute('aria-controls')\n if (!trigger || !controls) return\n const dialog = document.getElementById(controls)\n if (!(dialog instanceof HTMLDialogElement) || dialog.open) return\n dialog.showModal()\n trigger.setAttribute('aria-expanded', 'true')\n requestAnimationFrame(() =>\n dialog\n .querySelector<HTMLElement>('[data-presentation=\"modal\"] [data-ui=\"chrome-search-input\"]')\n ?.focus(),\n )\n },\n { signal },\n )\n\n document.addEventListener(\n 'cancel',\n (event) => {\n if (!eventElement(event)?.matches('[data-ui=\"chrome-search-modal\"]')) return\n event.preventDefault()\n },\n { capture: true, signal },\n )\n\n document.addEventListener(\n 'close',\n (event) => {\n const dialog = eventElement(event)\n if (!(dialog instanceof HTMLDialogElement)) return\n if (!dialog.matches('[data-ui=\"chrome-search-modal\"]')) return\n const trigger = document.querySelector<HTMLElement>(\n `[data-ui=\"chrome-search-toggle\"][aria-controls=\"${CSS.escape(dialog.id)}\"]`,\n )\n trigger?.setAttribute('aria-expanded', 'false')\n trigger?.focus()\n },\n { capture: true, signal },\n )\n}\n\nconst focusable = [\n 'a[href]',\n 'button:not(:disabled)',\n 'input:not(:disabled):not([type=\"hidden\"])',\n 'select:not(:disabled)',\n 'textarea:not(:disabled)',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(', ')\n\nconst activeRouteModal = (): HTMLElement | null =>\n document.querySelector<HTMLElement>('[data-ui=\"modal-layer\"][data-route-modal=\"true\"]')\n\nconst modalFocusables = (modal: HTMLElement): HTMLElement[] =>\n [...modal.querySelectorAll<HTMLElement>(focusable)].filter(\n (item) =>\n !item.hidden &&\n item.getAttribute('aria-hidden') !== 'true' &&\n !item.matches('[data-ui=\"modal-backdrop\"]'),\n )\n\nconst navigateTo = (href: string, navigation: BrowserNavigation): void => {\n void navigation.navigate(href)\n}\n\nconst eventStartedInNestedModal = (event: KeyboardEvent, routeModal: HTMLElement): boolean =>\n event\n .composedPath()\n .some(\n (target) =>\n target instanceof HTMLElement &&\n target !== routeModal &&\n target.matches('[data-ui=\"modal-layer\"][data-presentation=\"dialog\"]'),\n )\n\n/**\n * Whether anything in the modal has been typed into since the server rendered it.\n *\n * Comparing each control against its own default is what makes this work without\n * any bookkeeping: the page is server-rendered, so the default *is* the state the\n * reader was given. A modal with no form \u2014 a reader, an inspector \u2014 has nothing\n * to compare and is never dirty, so the guard costs those screens nothing.\n */\nconst modalHasDraft = (modal: HTMLElement): boolean => {\n for (const field of modal.querySelectorAll<HTMLInputElement>('input:not([type=\"hidden\"])')) {\n if (field.disabled) continue\n if (field.type === 'checkbox' || field.type === 'radio') {\n if (field.checked !== field.defaultChecked) return true\n } else if (field.value !== field.defaultValue) return true\n }\n for (const area of modal.querySelectorAll<HTMLTextAreaElement>('textarea'))\n if (!area.disabled && area.value !== area.defaultValue) return true\n for (const select of modal.querySelectorAll<HTMLSelectElement>('select')) {\n if (select.disabled) continue\n for (const option of select.options) if (option.selected !== option.defaultSelected) return true\n }\n return false\n}\n\n/**\n * The guard, and what it is not.\n *\n * Leaving a modal is a link, and a link still works with scripting off \u2014 that is\n * the point of building it as one. So this catches the stray backdrop click, the\n * reflexive Escape and the tab clicked without thinking about the note already\n * typed; it is not a lock, and nothing here should be read as one. The wording\n * comes from the server because this file cannot translate.\n */\nconst mayLeaveModal = (modal: HTMLElement): boolean => {\n if (!modalHasDraft(modal)) return true\n const message = modal.getAttribute('data-unsaved-prompt')\n // No prompt declared means the screen did not ask to be guarded.\n if (!message) return true\n return browserGlobals.confirm(message)\n}\n\n/** Progressive keyboard behavior for URL-owned create/edit workspaces. */\n/**\n * Hear when the thing a screen is waiting for actually changed.\n *\n * The screens this replaces reloaded the whole document on a timer, which threw\n * away scroll, focus and anything typed, on a schedule unrelated to when the\n * work finished. Here the server says so, and the page asks for the fragment it\n * already knows how to swap \u2014 so the reader keeps their place.\n *\n * The connection belongs to the element. A fragment swap replaces it, and the\n * observer opens the new one and closes the old; a screen with nothing to wait\n * for renders no element and opens nothing.\n */\nconst installLiveRegion = (signal: AbortSignal, navigation: BrowserNavigation): (() => void) | undefined => {\n if (typeof EventSource !== 'function') return\n\n let watched: Element | null = null\n let source: EventSource | null = null\n const close = () => {\n source?.close()\n source = null\n watched = null\n }\n\n const sync = (): void => {\n const region = document.querySelector('[data-ui=\"live-region\"][data-stream]')\n if (region === watched) return\n close()\n const id = region instanceof HTMLElement ? (region.dataset.stream ?? '') : ''\n if (!id) return\n watched = region\n source = new EventSource(`/_ket/stream/${encodeURIComponent(id)}`)\n // What the chunk says is the deployment's business. That something was said\n // is the whole signal: the screen is server-rendered, so the way to find out\n // what changed is to ask for it.\n const refresh = () => navigateTo(location.href, navigation)\n source.onmessage = refresh\n source.addEventListener('done', () => {\n close()\n refresh()\n })\n // EventSource reconnects on its own, and a stream that has ended answers 404\n // rather than reopening. Leaving the error alone would retry that forever.\n source.onerror = () => {\n if (source?.readyState === EventSource.CLOSED) close()\n }\n }\n\n sync()\n const observer = new MutationObserver(sync)\n observer.observe(document.body, { childList: true, subtree: true })\n addEventListener('pagehide', close, { signal })\n return () => {\n observer.disconnect()\n close()\n }\n}\n\nconst installRouteModal = (signal: AbortSignal, navigation: BrowserNavigation): (() => void) => {\n const focused = new WeakSet<HTMLElement>()\n const focusModal = (): void => {\n const modal = activeRouteModal()\n if (!modal || focused.has(modal)) return\n focused.add(modal)\n requestAnimationFrame(() => {\n const sheet = modal.querySelector<HTMLElement>('[data-ui=\"modal-sheet\"]')\n const target = modalFocusables(modal).find((item) => !item.matches('[data-ui=\"modal-close\"]'))\n ;(target ?? sheet)?.focus()\n })\n }\n focusModal()\n const observer = new MutationObserver(focusModal)\n observer.observe(document.body, { childList: true, subtree: true })\n\n // The backdrop and the X are ordinary links, so the guard has to run before\n // the navigation layer sees the click rather than inside it.\n document.addEventListener(\n 'click',\n (event) => {\n const modal = activeRouteModal()\n if (!modal) return\n // A tab inside a modal is a third way out of it. It looks like a control\n // that stays on the screen, which is exactly why losing a half-written\n // note to one went unnoticed: the reader never meant to leave. It is a\n // link like the other two, and it leaves the same way.\n const leaving = event\n .composedPath()\n .find(\n (target) =>\n target instanceof HTMLElement &&\n (target.matches('[data-ui=\"modal-close\"], [data-ui=\"modal-backdrop\"], a[data-ui=\"tab\"]') ||\n target.matches('a[data-ui=\"action\"][href]')),\n )\n if (!leaving || !modal.contains(leaving as HTMLElement)) return\n if (!mayLeaveModal(modal)) {\n event.preventDefault()\n event.stopPropagation()\n }\n },\n { capture: true, signal },\n )\n\n document.addEventListener(\n 'click',\n (event) => {\n const container = document.querySelector<HTMLElement>('[data-save-before-navigation]')\n const form =\n container instanceof HTMLFormElement ? container : container?.querySelector<HTMLFormElement>('form')\n const anchor = eventElement(event)?.closest<HTMLAnchorElement>('a[href]')\n if (!container || !form || !anchor || activeRouteModal() || !modalHasDraft(container)) return\n event.preventDefault()\n event.stopPropagation()\n if (!browserGlobals.confirm(container.dataset.saveBeforeNavigation)) return\n let destination = form.querySelector<HTMLInputElement>('input[name=\"returnTo\"]')\n if (!destination) {\n destination = document.createElement('input')\n destination.type = 'hidden'\n destination.name = 'returnTo'\n form.append(destination)\n }\n destination.value = anchor.href\n form.requestSubmit()\n },\n { capture: true, signal },\n )\n document.addEventListener(\n 'keydown',\n (event) => {\n const modal = activeRouteModal()\n if (!modal) return\n // Relation-select removes its dialog synchronously on Escape. The event's\n // composed path is stable even after that DOM subtree disappears, so the\n // route layer must use it instead of querying the now-removed target.\n if (eventStartedInNestedModal(event, modal)) return\n if (event.key === 'Escape') {\n const close = modal.querySelector<HTMLAnchorElement>('[data-ui=\"modal-close\"]')\n if (!close) return\n event.preventDefault()\n if (!mayLeaveModal(modal)) return\n navigateTo(close.getAttribute('href') ?? close.href, navigation)\n return\n }\n if (event.key !== 'Tab') return\n const items = modalFocusables(modal)\n if (!items.length) return\n const first = items[0]\n const last = items.at(-1)!\n if (event.shiftKey && document.activeElement === first) {\n event.preventDefault()\n last.focus()\n } else if (!event.shiftKey && document.activeElement === last) {\n event.preventDefault()\n first.focus()\n }\n },\n { signal },\n )\n return () => observer.disconnect()\n}\n\nexport const backendShell: BrowserBehavior = ({ navigation, lifetime }) => {\n const cleanupDesignSystem = attachDesignSystemInteractions(document)\n const cleanupUserWorkflow = installUserWorkflow(lifetime)\n installThemeToggle(lifetime)\n installTableSelection(lifetime, navigation)\n installDropdownDismiss(lifetime)\n installGlobalFilter(lifetime)\n const cleanupRouteModal = installRouteModal(lifetime, navigation)\n const cleanupLiveRegion = installLiveRegion(lifetime, navigation)\n return () => {\n cleanupDesignSystem()\n cleanupLiveRegion?.()\n cleanupRouteModal()\n cleanupUserWorkflow()\n }\n}\n"],
|
|
5
|
+
"mappings": "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F,ECkCM,OACE,OAAAC,GADF,QAAAC,OAAA,4BAjDC,IAAMC,GAAQ,CACnB,cACA,sBACA,qBACA,wBACA,sBACA,oBACA,sBACA,qBACA,0BACA,sBACA,mBACA,yBACA,yBACA,qBACA,mBACA,mBACF,ECjBO,IAAMC,GAAQ,CACnB,YACA,oBACA,mBACA,sBACA,oBACA,kBACA,oBACA,mBACA,wBACA,oBACA,iBACA,uBACA,uBACA,mBACA,iBACA,iBACF,ECwBI,OASM,OAAAC,GATN,QAAAC,OAAA,4BCzCG,IAAMC,GAAQ,CACnB,iBACA,yBACA,wBACA,yBACA,yBACA,2BACA,uBACA,yBACA,wBACA,6BACA,yBACA,sBACA,qBACF,ECdO,IAAMC,GAAQ,CACnB,aACA,qBACA,oBACA,qBACA,qBACA,uBACA,mBACA,qBACA,oBACA,yBACA,qBACA,kBACA,qBACA,sBACA,iBACF,ECnBA,OAAS,QAAAC,OAAY,sBAyCb,OACE,OAAAC,GADF,QAAAC,OAAA,4BArCD,IAAMC,GAAQ,CACnB,WACA,gBACA,iBACA,sBACA,sBACA,oBACF,ECXA,OAAS,QAAAC,OAAY,sBAuBnB,OAOI,OAAAC,GAPJ,QAAAC,OAAA,4BAhBK,IAAMC,GAAQ,CAAC,cAAe,cAAe,YAAa,cAAc,ECmC/E,IAAMC,GAAS,CACb,QAASC,EACT,SAAUA,EACV,OAAQA,GACR,OAAQA,EACR,WAAYA,GACZ,SAAUA,GACV,KAAMA,GACN,QAASA,GACT,QAASA,GACT,OAAQA,GACR,MAAOA,GACP,QAASA,GACT,SAAUA,GACV,aAAcA,GACd,SAAUA,GACV,SAAUA,GACV,OAAQA,GACR,eAAgBA,GAChB,aAAcA,GACd,WAAYA,GACZ,aAAcA,GACd,SAAUA,GACV,KAAMA,GACN,cAAeA,GACf,gBAAiBA,GACjB,kBAAmBA,GACnB,SAAUA,GACV,MAAOA,GACP,QAASA,GACT,MAAOA,GACP,cAAeA,GACf,MAAOA,GACP,WAAYA,GACZ,SAAUA,GACV,SAAUA,GACV,WAAYA,GACZ,cAAeA,GACf,UAAWA,GACX,MAAOA,GACP,SAAUA,GA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",
|
|
6
|
+
"names": ["installUserWorkflow", "lifetime", "abort", "timer", "sequence", "check", "form", "input", "status", "request", "value", "next", "retry", "paint", "message", "valid", "response", "result", "scope", "changed", "company", "branch", "kind", "option", "key", "assigned", "row", "assignedLabel", "description", "companyField", "branchField", "scan", "event", "n", "empty", "target", "observer", "each", "Fragment", "jsx", "jsxs", "HOOKS", "jsx", "jsxs", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "each", "each", "jsx", "jsxs", "HOOKS", "Fragment", "jsx", "jsxs", "HOOKS", "each", "Fragment", "jsx", "jsx", "jsxs", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "jsx", "jsxs", "HOOKS", "each", "Fragment", "jsx", "jsxs", "HOOKS", "jsx", "jsxs", "HOOKS", "jsx", "jsxs", "HOOKS", "jsx", "jsxs", "HOOKS", "jsx", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "jsx", "HOOKS", "each", "jsx", "HOOKS", "focusableSelector", "focusables", "root", "element", "attachDesignSystemInteractions", "activeBeforeOpen", "modal", "appShell", "priorInert", "first", "navigationMedia", "navigationRoot", "priorNavigationOpen", "navigations", "navigation", "navigationStates", "restoreNavigationTargets", "state", "inert", "syncNavigationLock", "syncNavigation", "trigger", "drawer", "mobile", "shell", "targets", "activeItem", "focusTarget", "closeNavigation", "returnFocus", "navigationCleanups", "onToggle", "onClick", "event", "target", "branch", "onNavigationMediaChange", "popover", "panel", "box", "gap", "placement", "prefersTop", "prefersEnd", "topPosition", "bottomPosition", "topFits", "bottomFits", "useTop", "requestedLeft", "left", "top", "onKeydown", "openNavigation", "items", "last", "treeItem", "tree", "item", "activeIndex", "nextIndex", "level", "index", "treeGridRow", "grid", "rows", "row", "openMenu", "active", "openPopover", "close", "onDocumentClick", "menu", "cleanup", "jsx", "jsxs", "jsx", "jsxs", "HOOKS", "each", "Fragment", "jsx", "jsxs", "HOOKS", "jsx", "jsxs", "HOOKS", "jsx", "jsxs", "HOOKS", "jsx", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "jsx", "jsxs", "HOOKS", "each", "each", "jsx", "jsxs", "HOOKS", "jsx", "jsxs", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "jsx", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "each", "Fragment", "jsx", "jsxs", "HOOKS", "each", "Fragment", "jsx", "jsxs", "HOOKS", "Fragment", "jsx", "jsxs", "jsx", "jsxs", "HOOKS", "jsx", "jsxs", "HOOKS", "HOOKS", "jsx", "jsxs", "HOOKS", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "GROUPS", "HOOKS", "OWNERS", "owner", "hooks", "themeStorageKey", "dismissibleDropdown", "browserGlobals", "eventElement", "event", "themePreference", "value", "storedTheme", "saveTheme", "theme", "resolvedTheme", "syncThemeToggle", "pressed", "control", "installThemeToggle", "signal", "preference", "next", "updateSelection", "table", "rows", "checked", "input", "all", "shell", "menu", "installTableSelection", "navigation", "target", "href", "navigateTo", "openDropdowns", "installDropdownDismiss", "current", "dropdown", "dropdowns", "focused", "installGlobalFilter", "trigger", "controls", "dialog", "focusable", "activeRouteModal", "modalFocusables", "modal", "item", "eventStartedInNestedModal", "routeModal", "modalHasDraft", "field", "area", "select", "option", "mayLeaveModal", "message", "installLiveRegion", "watched", "source", "close", "sync", "region", "id", "refresh", "observer", "installRouteModal", "focusModal", "sheet", "leaving", "container", "form", "anchor", "destination", "items", "first", "last", "backendShell", "lifetime", "cleanupDesignSystem", "attachDesignSystemInteractions", "cleanupUserWorkflow", "installUserWorkflow", "cleanupRouteModal", "cleanupLiveRegion"]
|
|
7
7
|
}
|