@vielzeug/refine 2.2.5 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/carousel.cjs +1 -1
  2. package/dist/carousel.cjs.map +1 -1
  3. package/dist/carousel.js +1 -1
  4. package/dist/carousel.js.map +1 -1
  5. package/dist/chat-message.cjs +9 -9
  6. package/dist/chat-message.cjs.map +1 -1
  7. package/dist/chat-message.js +40 -37
  8. package/dist/chat-message.js.map +1 -1
  9. package/dist/command-palette.cjs +1 -1
  10. package/dist/command-palette.cjs.map +1 -1
  11. package/dist/command-palette.js +1 -1
  12. package/dist/command-palette.js.map +1 -1
  13. package/dist/core/announcer.cjs +1 -1
  14. package/dist/core/announcer.cjs.map +1 -1
  15. package/dist/core/announcer.d.ts +13 -3
  16. package/dist/core/announcer.d.ts.map +1 -1
  17. package/dist/core/announcer.js +16 -9
  18. package/dist/core/announcer.js.map +1 -1
  19. package/dist/custom-elements.json +4328 -4328
  20. package/dist/feedback/typing-indicator/typing-indicator.d.ts +2 -2
  21. package/dist/file-input.cjs +49 -49
  22. package/dist/file-input.cjs.map +1 -1
  23. package/dist/file-input.js +128 -127
  24. package/dist/file-input.js.map +1 -1
  25. package/dist/inputs/file-input/file-input-upload.cjs +1 -1
  26. package/dist/inputs/file-input/file-input-upload.cjs.map +1 -1
  27. package/dist/inputs/file-input/file-input-upload.d.ts +2 -0
  28. package/dist/inputs/file-input/file-input-upload.d.ts.map +1 -1
  29. package/dist/inputs/file-input/file-input-upload.js +5 -2
  30. package/dist/inputs/file-input/file-input-upload.js.map +1 -1
  31. package/dist/inputs/file-input/file-input.d.ts.map +1 -1
  32. package/dist/refine.cjs +62 -62
  33. package/dist/refine.cjs.map +1 -1
  34. package/dist/refine.iife.js +62 -62
  35. package/dist/refine.iife.js.map +1 -1
  36. package/dist/refine.js +62 -62
  37. package/dist/refine.js.map +1 -1
  38. package/dist/typing-indicator.cjs +1 -1
  39. package/dist/typing-indicator.cjs.map +1 -1
  40. package/dist/typing-indicator.js +8 -7
  41. package/dist/typing-indicator.js.map +1 -1
  42. package/package.json +4 -4
@@ -1 +1 @@
1
- {"version":3,"file":"chat-message.js","names":[],"sources":["../src/content/chat-message/chat-message.ts"],"sourcesContent":["import { define, html, onEvent, onMounted, prop, ref, useEmit, useSlots } from '@vielzeug/ore';\nimport { computed, watch } from '@vielzeug/ripple';\n\nimport { announce } from '../../core';\nimport { reducedMotionMixin } from '../../styles';\nimport '../icon/icon';\nimport componentStyles from './chat-message.css?inline';\n\nexport type ChatMessageSender = 'assistant' | 'system' | 'user';\nexport type ChatMessageStatus = 'error' | 'sending' | 'sent';\n\nconst SENDER_LABELS: Record<ChatMessageSender, string> = {\n assistant: 'Assistant',\n system: 'System',\n user: 'You',\n};\n\n/** Events emitted by the chat-message component */\nexport type OreChatMessageEvents = {\n /** Fired when the retry action is activated on a `status=\"error\"` message */\n retry: { originalEvent?: Event };\n};\n\n/** Chat message component properties */\nexport type OreChatMessageProps = {\n /** Error detail shown beneath the bubble; only visible when `status=\"error\"` */\n error?: string;\n /** Display name shown above the bubble (falls back to a generic label per `sender`) */\n name?: string;\n /** Who sent the message — controls alignment and bubble styling */\n sender?: ChatMessageSender;\n /** Delivery status for outgoing messages — shows an inline indicator and, for `\"error\"`, a retry action */\n status?: ChatMessageStatus;\n /** Append a blinking cursor after the content, for a message still streaming in */\n streaming?: boolean;\n /** ISO 8601 timestamp; rendered as a localized short time in a semantic `<time>` element */\n timestamp?: string;\n};\n\n/**\n * A single message bubble for chat/conversation UIs — sender-aware alignment, an optional\n * avatar slot, delivery status (sending/sent/error with retry), and a streaming cursor for\n * assistant messages still generating. Content is provided via the default slot, so any\n * markdown-to-HTML rendering stays the consumer's choice.\n *\n * The default slot's leading/trailing whitespace-only text nodes are trimmed in place once\n * per slot assignment (mount, and again on `slotchange`) — pretty-printed HTML's\n * indentation would otherwise render as blank lines, since the bubble preserves line breaks\n * (`white-space: pre-wrap`) for genuine multi-paragraph replies. This mutates those specific\n * text nodes' `textContent` directly; it never touches nodes appended afterward (e.g. by\n * `el.textContent += token` while streaming), only the ones present at assignment time.\n *\n * @element ore-chat-message\n *\n * @attr {string} sender - Who sent the message: 'user' | 'assistant' | 'system' (default 'assistant')\n * @attr {string} name - Display name shown above the bubble\n * @attr {string} timestamp - ISO 8601 timestamp, rendered as a localized short time\n * @attr {string} status - Delivery status: 'sending' | 'sent' | 'error'\n * @attr {string} error - Error detail shown beneath the bubble when status=\"error\"\n * @attr {boolean} streaming - Append a blinking cursor after the content\n *\n * @fires retry - Fired when the retry action is activated. detail: { originalEvent }\n *\n * @slot - Message content\n * @slot avatar - Avatar element (e.g. `<ore-avatar>`)\n * @slot actions - Action buttons shown beneath the message (copy, regenerate, feedback, …)\n *\n * @cssprop --chat-message-bg - Bubble background color (assistant/system)\n * @cssprop --chat-message-color - Bubble text color (assistant/system)\n * @cssprop --chat-message-user-bg - Bubble background color (user)\n * @cssprop --chat-message-user-color - Bubble text color (user)\n * @cssprop --chat-message-radius - Bubble border radius\n * @cssprop --chat-message-max-width - Maximum bubble width\n *\n * @part row - Root row container (avatar + column)\n * @part avatar - Avatar slot container\n * @part column - Column containing name, bubble, meta, and actions\n * @part name - Sender display name\n * @part bubble - Message bubble container\n * @part content - Content container inside the bubble\n * @part cursor - Blinking streaming cursor\n * @part meta - Row of timestamp, status, and error text below the bubble\n * @part timestamp - Timestamp `<time>` element\n * @part status - Status indicator\n * @part error - Error text\n * @part retry - Retry button (status=\"error\" only)\n * @part actions - Actions slot container\n *\n * @example\n * ```html\n * <ore-chat-message sender=\"user\" timestamp=\"2024-01-01T12:00:00Z\" status=\"sent\">\n * What's the weather like today?\n * </ore-chat-message>\n *\n * <ore-chat-message sender=\"assistant\" name=\"Assistant\" streaming>\n * Let me check that for you\n * </ore-chat-message>\n * ```\n */\nexport const CHAT_MESSAGE_TAG = 'ore-chat-message' as const;\ndefine<OreChatMessageProps>(CHAT_MESSAGE_TAG, {\n props: {\n error: prop.string(),\n name: prop.string(),\n sender: prop.oneOf(['user', 'assistant', 'system'] as const, 'assistant'),\n status: prop.string<ChatMessageStatus>(),\n streaming: prop.bool(false),\n timestamp: prop.string(),\n },\n setup(props) {\n const emit = useEmit<OreChatMessageEvents>();\n const slots = useSlots();\n\n const senderLabel = () => SENDER_LABELS[props.sender.value ?? 'assistant'];\n\n const bubbleLabel = computed(() => `Message from ${props.name.value || senderLabel()}`);\n\n const formattedTime = computed(() => {\n const raw = props.timestamp.value;\n\n if (!raw) return '';\n\n const date = new Date(raw);\n\n if (Number.isNaN(date.getTime())) return '';\n\n return new Intl.DateTimeFormat(undefined, { hour: 'numeric', minute: '2-digit' }).format(date);\n });\n\n const hasMeta = computed(\n () => Boolean(formattedTime.value) || Boolean(props.status.value) || Boolean(props.error.value),\n );\n\n // Announce delivery failures — the visible retry button and error text are always present\n // for sighted users, but a failed send is exactly the kind of state change a screen\n // reader user could otherwise miss entirely. Keyed on `error` text too (not just\n // `status`) so a retry that fails again with a *different* reason re-announces even\n // though `status` never left `\"error\"` in between; a second failure with the identical\n // message doesn't re-fire, since the source value hasn't actually changed.\n watch(\n () => (props.status.value === 'error' ? (props.error.value ?? '') : null),\n (errorText) => {\n if (errorText !== null) {\n announce(`Message failed to send${errorText ? `: ${errorText}` : ''}`, { politeness: 'assertive' });\n }\n },\n );\n\n function handleRetry(e: Event): void {\n emit('retry', { originalEvent: e });\n }\n\n // ── Trim author-time indentation whitespace from the default slot ──\n // `.content` uses `white-space: pre-wrap` so genuine multi-paragraph replies keep their\n // line breaks — but that also preserves the leading/trailing newline + indentation from\n // pretty-printed HTML (`<ore-chat-message>\\n Hello\\n</ore-chat-message>`), rendering as\n // visible blank lines around the text. Trim only the outermost edges once per slot\n // assignment; appending tokens to an existing text node for streaming doesn't re-fire\n // `slotchange`, so this never interferes with in-progress streaming updates.\n //\n // A named-slotted sibling (e.g. `<ore-avatar slot=\"avatar\">` between the opening tag and\n // the message text) splits the default slot's light-DOM text into *multiple* text nodes\n // — text nodes can't target a named slot, so each run on either side of the element is\n // assigned separately. The \"real\" content can start on a later node than index 0 (it's\n // still preceded by its own leading indentation), so this walks inward from each end,\n // fully clearing whitespace-only nodes and stopping at the first node with real content\n // on each side — rather than only touching the very first/last assigned node.\n const contentSlotRef = ref<HTMLSlotElement>();\n\n function trimSlotEdgeWhitespace(): void {\n const nodes = contentSlotRef.value?.assignedNodes({ flatten: true });\n\n if (!nodes || nodes.length === 0) return;\n\n for (const node of nodes) {\n if (node.nodeType !== Node.TEXT_NODE) break;\n\n const trimmed = (node.textContent ?? '').replace(/^\\s+/, '');\n\n node.textContent = trimmed;\n\n if (trimmed !== '') break;\n }\n\n for (let i = nodes.length - 1; i >= 0; i--) {\n const node = nodes[i];\n\n if (node.nodeType !== Node.TEXT_NODE) break;\n\n const trimmed = (node.textContent ?? '').replace(/\\s+$/, '');\n\n node.textContent = trimmed;\n\n if (trimmed !== '') break;\n }\n }\n\n onMounted(() => {\n trimSlotEdgeWhitespace();\n\n const slotEl = contentSlotRef.value;\n\n if (slotEl) onEvent(slotEl, 'slotchange', trimSlotEdgeWhitespace);\n });\n\n return html`\n <div class=\"row\" part=\"row\">\n <span class=\"avatar\" part=\"avatar\" ?hidden=\"${() => !slots.has('avatar').value}\">\n <slot name=\"avatar\"></slot>\n </span>\n <div class=\"column\" part=\"column\">\n <span class=\"name\" part=\"name\" ?hidden=\"${() => !props.name.value}\">${props.name}</span>\n <div class=\"bubble\" part=\"bubble\" role=\"article\" aria-label=\"${bubbleLabel}\">\n <div class=\"content\" part=\"content\">\n <slot class=\"content-slot\" ref=\"${contentSlotRef}\"></slot>\n <span class=\"cursor\" part=\"cursor\" aria-hidden=\"true\" ?hidden=\"${() => !props.streaming.value}\"></span>\n </div>\n </div>\n <div class=\"meta\" part=\"meta\" ?hidden=\"${() => !hasMeta.value}\">\n <time\n class=\"timestamp\"\n part=\"timestamp\"\n datetime=\"${props.timestamp}\"\n ?hidden=\"${() => !formattedTime.value}\">\n ${formattedTime}\n </time>\n <span class=\"status\" part=\"status\" data-status=\"${props.status}\" ?hidden=\"${() => !props.status.value}\">\n ${() => {\n switch (props.status.value) {\n case 'error':\n return html`\n <ore-icon name=\"alert-circle\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n `;\n case 'sending':\n return html`\n <span class=\"spinner\" aria-hidden=\"true\"></span>\n `;\n case 'sent':\n return html`\n <ore-icon name=\"check\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n `;\n default:\n return '';\n }\n }}\n </span>\n <span class=\"error-text\" part=\"error\" role=\"alert\" ?hidden=\"${() => !props.error.value}\">\n ${props.error}\n </span>\n <button\n class=\"retry\"\n part=\"retry\"\n type=\"button\"\n ?hidden=\"${() => props.status.value !== 'error'}\"\n @click=\"${handleRetry}\">\n Retry\n </button>\n </div>\n <div class=\"actions\" part=\"actions\" ?hidden=\"${() => !slots.has('actions').value}\">\n <slot name=\"actions\"></slot>\n </div>\n </div>\n </div>\n `;\n },\n styles: [reducedMotionMixin, componentStyles],\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"chat-message.js","names":[],"sources":["../src/content/chat-message/chat-message.ts"],"sourcesContent":["import { define, getHost, html, onEvent, onMounted, prop, ref, useEmit, useSlots } from '@vielzeug/ore';\nimport { computed, watch } from '@vielzeug/ripple';\n\nimport { announce } from '../../core';\nimport { reducedMotionMixin } from '../../styles';\nimport '../icon/icon';\nimport componentStyles from './chat-message.css?inline';\n\nexport type ChatMessageSender = 'assistant' | 'system' | 'user';\nexport type ChatMessageStatus = 'error' | 'sending' | 'sent';\n\nconst SENDER_LABELS: Record<ChatMessageSender, string> = {\n assistant: 'Assistant',\n system: 'System',\n user: 'You',\n};\n\n/** Events emitted by the chat-message component */\nexport type OreChatMessageEvents = {\n /** Fired when the retry action is activated on a `status=\"error\"` message */\n retry: { originalEvent?: Event };\n};\n\n/** Chat message component properties */\nexport type OreChatMessageProps = {\n /** Error detail shown beneath the bubble; only visible when `status=\"error\"` */\n error?: string;\n /** Display name shown above the bubble (falls back to a generic label per `sender`) */\n name?: string;\n /** Who sent the message — controls alignment and bubble styling */\n sender?: ChatMessageSender;\n /** Delivery status for outgoing messages — shows an inline indicator and, for `\"error\"`, a retry action */\n status?: ChatMessageStatus;\n /** Append a blinking cursor after the content, for a message still streaming in */\n streaming?: boolean;\n /** ISO 8601 timestamp; rendered as a localized short time in a semantic `<time>` element */\n timestamp?: string;\n};\n\n/**\n * A single message bubble for chat/conversation UIs — sender-aware alignment, an optional\n * avatar slot, delivery status (sending/sent/error with retry), and a streaming cursor for\n * assistant messages still generating. Content is provided via the default slot, so any\n * markdown-to-HTML rendering stays the consumer's choice.\n *\n * The default slot's leading/trailing whitespace-only text nodes are trimmed in place once\n * per slot assignment (mount, and again on `slotchange`) — pretty-printed HTML's\n * indentation would otherwise render as blank lines, since the bubble preserves line breaks\n * (`white-space: pre-wrap`) for genuine multi-paragraph replies. This mutates those specific\n * text nodes' `textContent` directly; it never touches nodes appended afterward (e.g. by\n * `el.textContent += token` while streaming), only the ones present at assignment time.\n *\n * @element ore-chat-message\n *\n * @attr {string} sender - Who sent the message: 'user' | 'assistant' | 'system' (default 'assistant')\n * @attr {string} name - Display name shown above the bubble\n * @attr {string} timestamp - ISO 8601 timestamp, rendered as a localized short time\n * @attr {string} status - Delivery status: 'sending' | 'sent' | 'error'\n * @attr {string} error - Error detail shown beneath the bubble when status=\"error\"\n * @attr {boolean} streaming - Append a blinking cursor after the content\n *\n * @fires retry - Fired when the retry action is activated. detail: { originalEvent }\n *\n * @slot - Message content\n * @slot avatar - Avatar element (e.g. `<ore-avatar>`)\n * @slot actions - Action buttons shown beneath the message (copy, regenerate, feedback, …)\n *\n * @cssprop --chat-message-bg - Bubble background color (assistant/system)\n * @cssprop --chat-message-color - Bubble text color (assistant/system)\n * @cssprop --chat-message-user-bg - Bubble background color (user)\n * @cssprop --chat-message-user-color - Bubble text color (user)\n * @cssprop --chat-message-radius - Bubble border radius\n * @cssprop --chat-message-max-width - Maximum bubble width\n *\n * @part row - Root row container (avatar + column)\n * @part avatar - Avatar slot container\n * @part column - Column containing name, bubble, meta, and actions\n * @part name - Sender display name\n * @part bubble - Message bubble container\n * @part content - Content container inside the bubble\n * @part cursor - Blinking streaming cursor\n * @part meta - Row of timestamp, status, and error text below the bubble\n * @part timestamp - Timestamp `<time>` element\n * @part status - Status indicator\n * @part error - Error text\n * @part retry - Retry button (status=\"error\" only)\n * @part actions - Actions slot container\n *\n * @example\n * ```html\n * <ore-chat-message sender=\"user\" timestamp=\"2024-01-01T12:00:00Z\" status=\"sent\">\n * What's the weather like today?\n * </ore-chat-message>\n *\n * <ore-chat-message sender=\"assistant\" name=\"Assistant\" streaming>\n * Let me check that for you\n * </ore-chat-message>\n * ```\n */\nexport const CHAT_MESSAGE_TAG = 'ore-chat-message' as const;\ndefine<OreChatMessageProps>(CHAT_MESSAGE_TAG, {\n props: {\n error: prop.string(),\n name: prop.string(),\n sender: prop.oneOf(['user', 'assistant', 'system'] as const, 'assistant'),\n status: prop.string<ChatMessageStatus>(),\n streaming: prop.bool(false),\n timestamp: prop.string(),\n },\n setup(props) {\n const el = getHost();\n const emit = useEmit<OreChatMessageEvents>();\n const slots = useSlots();\n\n const senderLabel = () => SENDER_LABELS[props.sender.value ?? 'assistant'];\n\n const bubbleLabel = computed(() => `Message from ${props.name.value || senderLabel()}`);\n\n const formattedTime = computed(() => {\n const raw = props.timestamp.value;\n\n if (!raw) return '';\n\n const date = new Date(raw);\n\n if (Number.isNaN(date.getTime())) return '';\n\n return new Intl.DateTimeFormat(undefined, { hour: 'numeric', minute: '2-digit' }).format(date);\n });\n\n const hasMeta = computed(\n () => Boolean(formattedTime.value) || Boolean(props.status.value) || Boolean(props.error.value),\n );\n\n // Announce delivery failures — the visible retry button and error text are always present\n // for sighted users, but a failed send is exactly the kind of state change a screen\n // reader user could otherwise miss entirely. Keyed on `error` text too (not just\n // `status`) so a retry that fails again with a *different* reason re-announces even\n // though `status` never left `\"error\"` in between; a second failure with the identical\n // message doesn't re-fire, since the source value hasn't actually changed.\n watch(\n () => (props.status.value === 'error' ? (props.error.value ?? '') : null),\n (errorText) => {\n if (errorText !== null) {\n announce(`Message failed to send${errorText ? `: ${errorText}` : ''}`, {\n document: el.ownerDocument,\n politeness: 'assertive',\n });\n }\n },\n );\n\n function handleRetry(e: Event): void {\n emit('retry', { originalEvent: e });\n }\n\n // ── Trim author-time indentation whitespace from the default slot ──\n // `.content` uses `white-space: pre-wrap` so genuine multi-paragraph replies keep their\n // line breaks — but that also preserves the leading/trailing newline + indentation from\n // pretty-printed HTML (`<ore-chat-message>\\n Hello\\n</ore-chat-message>`), rendering as\n // visible blank lines around the text. Trim only the outermost edges once per slot\n // assignment; appending tokens to an existing text node for streaming doesn't re-fire\n // `slotchange`, so this never interferes with in-progress streaming updates.\n //\n // A named-slotted sibling (e.g. `<ore-avatar slot=\"avatar\">` between the opening tag and\n // the message text) splits the default slot's light-DOM text into *multiple* text nodes\n // — text nodes can't target a named slot, so each run on either side of the element is\n // assigned separately. The \"real\" content can start on a later node than index 0 (it's\n // still preceded by its own leading indentation), so this walks inward from each end,\n // fully clearing whitespace-only nodes and stopping at the first node with real content\n // on each side — rather than only touching the very first/last assigned node.\n const contentSlotRef = ref<HTMLSlotElement>();\n\n function trimSlotEdgeWhitespace(): void {\n const nodes = contentSlotRef.value?.assignedNodes({ flatten: true });\n\n if (!nodes || nodes.length === 0) return;\n\n for (const node of nodes) {\n if (node.nodeType !== Node.TEXT_NODE) break;\n\n const trimmed = (node.textContent ?? '').replace(/^\\s+/, '');\n\n node.textContent = trimmed;\n\n if (trimmed !== '') break;\n }\n\n for (let i = nodes.length - 1; i >= 0; i--) {\n const node = nodes[i];\n\n if (node.nodeType !== Node.TEXT_NODE) break;\n\n const trimmed = (node.textContent ?? '').replace(/\\s+$/, '');\n\n node.textContent = trimmed;\n\n if (trimmed !== '') break;\n }\n }\n\n onMounted(() => {\n trimSlotEdgeWhitespace();\n\n const slotEl = contentSlotRef.value;\n\n if (slotEl) onEvent(slotEl, 'slotchange', trimSlotEdgeWhitespace);\n });\n\n return html`\n <div class=\"row\" part=\"row\">\n <span class=\"avatar\" part=\"avatar\" ?hidden=\"${() => !slots.has('avatar').value}\">\n <slot name=\"avatar\"></slot>\n </span>\n <div class=\"column\" part=\"column\">\n <span class=\"name\" part=\"name\" ?hidden=\"${() => !props.name.value}\">${props.name}</span>\n <div class=\"bubble\" part=\"bubble\" role=\"article\" aria-label=\"${bubbleLabel}\">\n <div class=\"content\" part=\"content\">\n <slot class=\"content-slot\" ref=\"${contentSlotRef}\"></slot>\n <span class=\"cursor\" part=\"cursor\" aria-hidden=\"true\" ?hidden=\"${() => !props.streaming.value}\"></span>\n </div>\n </div>\n <div class=\"meta\" part=\"meta\" ?hidden=\"${() => !hasMeta.value}\">\n <time\n class=\"timestamp\"\n part=\"timestamp\"\n datetime=\"${props.timestamp}\"\n ?hidden=\"${() => !formattedTime.value}\">\n ${formattedTime}\n </time>\n <span class=\"status\" part=\"status\" data-status=\"${props.status}\" ?hidden=\"${() => !props.status.value}\">\n ${() => {\n switch (props.status.value) {\n case 'error':\n return html`\n <ore-icon name=\"alert-circle\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n `;\n case 'sending':\n return html`\n <span class=\"spinner\" aria-hidden=\"true\"></span>\n `;\n case 'sent':\n return html`\n <ore-icon name=\"check\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n `;\n default:\n return '';\n }\n }}\n </span>\n <span class=\"error-text\" part=\"error\" role=\"alert\" ?hidden=\"${() => !props.error.value}\">\n ${props.error}\n </span>\n <button\n class=\"retry\"\n part=\"retry\"\n type=\"button\"\n ?hidden=\"${() => props.status.value !== 'error'}\"\n @click=\"${handleRetry}\">\n Retry\n </button>\n </div>\n <div class=\"actions\" part=\"actions\" ?hidden=\"${() => !slots.has('actions').value}\">\n <slot name=\"actions\"></slot>\n </div>\n </div>\n </div>\n `;\n },\n styles: [reducedMotionMixin, componentStyles],\n});\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./styles/mixins/accessibility.css.cjs"),t=require("./core/announcer.cjs");require("./_dev.cjs");const n=require("./core/ore.cjs"),r=require("./core/nav.cjs"),i=require("./overlay/shared/use-dialog.cjs"),a=require("./overlay/command-palette/command-palette-items.cjs"),o=require("./overlay/command-palette/command-palette.css.cjs");let s=require("@vielzeug/ore"),c=require("@vielzeug/ripple"),l=require("@vielzeug/keymap");var u=e=>e==null?void 0:e===``||e===`true`,d=`ore-command-palette-item`;(0,s.define)(d,{setup(){return s.html``}});var f=`ore-command-palette`;(0,s.define)(f,{props:{"default-open":s.prop.bool(!1),"empty-text":s.prop.string(`No results found.`),items:s.prop.data(),"keep-open-on-select":s.prop.bool(!1),label:s.prop.string(`Command palette`),loading:s.prop.bool(!1),"no-filter":s.prop.bool(!1),open:{default:void 0,parse:u},placeholder:s.prop.string(`Type a command or search…`),shortcut:s.prop.string(`mod+k`)},setup(e){let o=(0,s.getHost)(),u=(0,s.useEmit)(),d=n.lifecycleSignal(s.onCleanup),f=(0,s.ref)(),p=(0,s.ref)(),m=`cmdk-listbox`,h=null,g=(0,c.signal)(``),_=(0,c.signal)([]),v=()=>{_.value=a.parseSlottedItems([...o.children])};v();let y=new MutationObserver(v);y.observe(o,{characterData:!0,childList:!0,subtree:!0}),(0,s.onCleanup)(()=>y.disconnect());let b=(0,c.computed)(()=>{let t=e.items.value;return Array.isArray(t)?t.map(a.normalizeItem):_.value}),x=(0,c.computed)(()=>a.filterItems(b.value,g.value,!!e[`no-filter`].value)),S=(0,c.computed)(()=>a.buildRows(x.value)),C=r.createListControl({getItems:()=>x.value,isItemDisabled:e=>e.disabled,onNavigate:()=>{h?.querySelector(`[data-focused]`)?.scrollIntoView({block:`nearest`})},signal:d}),{focusedIndex:w}=C,T=()=>{g.value=``,C.reset()},{closeWithAnimation:E,overlay:D,requestClose:O,setupNativeListeners:k}=i.useDialogControl({defaultOpen:e[`default-open`],dialogRef:f,getPanelEl:()=>f.value?.querySelector(`.panel`),host:o,initialFocus:(0,c.computed)(()=>`.search-input`),isPersistent:()=>!1,onCleanup:s.onCleanup,onEvent:s.onEvent,onNativeClose:e=>{u(`open-change`,{open:!1,reason:e}),T()},onOpen:e=>{u(`open-change`,{open:!0,reason:e}),C.navigate(`first`)},openProp:e.open,performClose:()=>E(),returnFocus:(0,c.computed)(()=>!0)}),A=(0,l.createKeymap)();A.mount(window),(0,s.onCleanup)(()=>A.dispose());let j=null;(0,c.watch)(e.shortcut,e=>{j?.(),j=null;let t=(e??``).trim();if(t)try{j=A.bind(t,()=>{D.toggle(`keyboard`,`trigger`)})}catch(e){`${t}${e instanceof Error?e.message:String(e)}`}},{immediate:!0}),(0,s.onMounted)(()=>{k()});function M(t){t.disabled||(u(`select`,{item:t,label:t.label,value:t.value}),e[`keep-open-on-select`].value?p.value?.focus():O(`trigger`))}function N(n){let r=n.target.value;g.value=r,C.navigate(`first`),u(`search`,{query:r});let i=x.value.length;t.announce(i===0?e[`empty-text`].value:`${i} result${i===1?``:`s`} found`)}function P(e){if(!C.handleKeydown(e)&&e.key===`Enter`){let t=C.getActiveItem()??x.value.find(e=>!e.disabled);t&&(e.preventDefault(),M(t))}}return s.html`
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./styles/mixins/accessibility.css.cjs"),t=require("./core/announcer.cjs");require("./_dev.cjs");const n=require("./core/ore.cjs"),r=require("./core/nav.cjs"),i=require("./overlay/shared/use-dialog.cjs"),a=require("./overlay/command-palette/command-palette-items.cjs"),o=require("./overlay/command-palette/command-palette.css.cjs");let s=require("@vielzeug/ore"),c=require("@vielzeug/ripple"),l=require("@vielzeug/keymap");var u=e=>e==null?void 0:e===``||e===`true`,d=`ore-command-palette-item`;(0,s.define)(d,{setup(){return s.html``}});var f=`ore-command-palette`;(0,s.define)(f,{props:{"default-open":s.prop.bool(!1),"empty-text":s.prop.string(`No results found.`),items:s.prop.data(),"keep-open-on-select":s.prop.bool(!1),label:s.prop.string(`Command palette`),loading:s.prop.bool(!1),"no-filter":s.prop.bool(!1),open:{default:void 0,parse:u},placeholder:s.prop.string(`Type a command or search…`),shortcut:s.prop.string(`mod+k`)},setup(e){let o=(0,s.getHost)(),u=(0,s.useEmit)(),d=n.lifecycleSignal(s.onCleanup),f=(0,s.ref)(),p=(0,s.ref)(),m=`cmdk-listbox`,h=null,g=(0,c.signal)(``),_=(0,c.signal)([]),v=()=>{_.value=a.parseSlottedItems([...o.children])};v();let y=new MutationObserver(v);y.observe(o,{characterData:!0,childList:!0,subtree:!0}),(0,s.onCleanup)(()=>y.disconnect());let b=(0,c.computed)(()=>{let t=e.items.value;return Array.isArray(t)?t.map(a.normalizeItem):_.value}),x=(0,c.computed)(()=>a.filterItems(b.value,g.value,!!e[`no-filter`].value)),S=(0,c.computed)(()=>a.buildRows(x.value)),C=r.createListControl({getItems:()=>x.value,isItemDisabled:e=>e.disabled,onNavigate:()=>{h?.querySelector(`[data-focused]`)?.scrollIntoView({block:`nearest`})},signal:d}),{focusedIndex:w}=C,T=()=>{g.value=``,C.reset()},{closeWithAnimation:E,overlay:D,requestClose:O,setupNativeListeners:k}=i.useDialogControl({defaultOpen:e[`default-open`],dialogRef:f,getPanelEl:()=>f.value?.querySelector(`.panel`),host:o,initialFocus:(0,c.computed)(()=>`.search-input`),isPersistent:()=>!1,onCleanup:s.onCleanup,onEvent:s.onEvent,onNativeClose:e=>{u(`open-change`,{open:!1,reason:e}),T()},onOpen:e=>{u(`open-change`,{open:!0,reason:e}),C.navigate(`first`)},openProp:e.open,performClose:()=>E(),returnFocus:(0,c.computed)(()=>!0)}),A=(0,l.createKeymap)();A.mount(window),(0,s.onCleanup)(()=>A.dispose());let j=null;(0,c.watch)(e.shortcut,e=>{j?.(),j=null;let t=(e??``).trim();if(t)try{j=A.bind(t,()=>{D.toggle(`keyboard`,`trigger`)})}catch(e){`${t}${e instanceof Error?e.message:String(e)}`}},{immediate:!0}),(0,s.onMounted)(()=>{k()});function M(t){t.disabled||(u(`select`,{item:t,label:t.label,value:t.value}),e[`keep-open-on-select`].value?p.value?.focus():O(`trigger`))}function N(n){let r=n.target.value;g.value=r,C.navigate(`first`),u(`search`,{query:r});let i=x.value.length;t.announce(i===0?e[`empty-text`].value:`${i} result${i===1?``:`s`} found`,{document:o.ownerDocument})}function P(e){if(!C.handleKeydown(e)&&e.key===`Enter`){let t=C.getActiveItem()??x.value.find(e=>!e.disabled);t&&(e.preventDefault(),M(t))}}return s.html`
2
2
  <dialog ref="${f}" class="dialog" part="dialog" aria-label="${e.label}" aria-modal="true">
3
3
  <div class="panel" part="panel">
4
4
  <div class="search" part="search">
@@ -1 +1 @@
1
- {"version":3,"file":"command-palette.cjs","names":[],"sources":["../src/overlay/command-palette/command-palette.ts"],"sourcesContent":["import { createKeymap } from '@vielzeug/keymap';\nimport { define, getHost, html, onCleanup, onEvent, onMounted, prop, ref, useEmit } from '@vielzeug/ore';\nimport { computed, signal, watch } from '@vielzeug/ripple';\nimport { warn } from '../../_dev';\nimport { announce, createListControl, lifecycleSignal } from '../../core';\nimport { reducedMotionMixin } from '../../styles';\nimport { useDialogControl } from '../shared/use-dialog';\nimport type { CommandPaletteItem, OreCommandPaletteEvents, OreCommandPaletteProps } from './command-palette.types';\nimport { buildRows, filterItems, normalizeItem, parseSlottedItems, splitShortcutKeys } from './command-palette-items';\nimport '../../content/icon/icon';\nimport componentStyles from './command-palette.css?inline';\n\nexport type { OreCommandPaletteEvents, OreCommandPaletteProps } from './command-palette.types';\n\nconst parseOptionalBool = (value: string | null): boolean | undefined =>\n value == null ? undefined : value === '' || value === 'true';\n\n/**\n * A pure data node describing one command. Never rendered directly — `ore-command-palette`\n * reads its attributes/text content and renders the visible row itself.\n *\n * @element ore-command-palette-item\n *\n * @attr {string} value - Value emitted by the `select` event and matched against the search query\n * @attr {string} label - Explicit label text; falls back to the element's text content\n * @attr {string} group - Group heading the item is clustered under\n * @attr {string} shortcut - Display-only keyboard hint rendered at the end of the row, one `<kbd>` per `+`-separated key (e.g. `\"⌘+S\"` renders two keycaps; a literal `+` key isn't representable — spell it out, e.g. `\"Ctrl+Plus\"`)\n * @attr {boolean} disabled - Excludes the item from keyboard navigation and selection\n *\n * @slot icon - Optional leading icon content\n *\n * @example\n * ```html\n * <ore-command-palette-item value=\"new-file\" group=\"File\" shortcut=\"⌘+N\">New File</ore-command-palette-item>\n * ```\n */\nexport const COMMAND_PALETTE_ITEM_TAG = 'ore-command-palette-item' as const;\ndefine(COMMAND_PALETTE_ITEM_TAG, {\n setup() {\n return html``;\n },\n});\n\n/**\n * A searchable, keyboard-driven list of commands presented in a centered modal —\n * the \"⌘K\" pattern popularized by editors and productivity apps. Built on the\n * native `<dialog>` element (focus trap, top-layer stacking, `Escape`-to-close)\n * and `@vielzeug/keymap` for the global shortcut that opens it.\n *\n * @element ore-command-palette\n * @element ore-command-palette-item - Slotted command definition (place in default slot)\n *\n * @attr {boolean} open - Controls the open state of the palette\n * @attr {boolean} default-open - Initial uncontrolled open state\n * @attr {string} label - Accessible label for the dialog (screen-reader-only heading)\n * @attr {string} placeholder - Placeholder text for the search input\n * @attr {string} shortcut - Global keyboard shortcut (keymap syntax) that toggles the palette. Default `\"mod+k\"`; set to `\"\"` to disable\n * @attr {boolean} no-filter - Disable built-in client-side filtering (for server-driven search)\n * @attr {boolean} loading - Shows a loading row below the search input\n * @attr {string} empty-text - Message shown when no item matches the query\n * @attr {boolean} keep-open-on-select - Keep the palette open after an item is selected\n *\n * @fires open-change - Fired when the palette state changes. detail: `{ open, reason }`\n * @fires search - Fired on every keystroke in the search input. detail: `{ query }`\n * @fires select - Fired when a command is chosen (click or `Enter`). detail: `{ value, label, item }`\n *\n * @slot - `<ore-command-palette-item>` elements (alternative/supplement to the `items` prop)\n *\n * @cssprop --command-palette-bg - Panel background color\n * @cssprop --command-palette-border-color - Panel border color\n * @cssprop --command-palette-radius - Panel border radius\n * @cssprop --command-palette-shadow - Panel drop shadow\n * @cssprop --command-palette-max-width - Maximum panel width\n * @cssprop --command-palette-backdrop - Backdrop overlay color\n * @cssprop --command-palette-option-hover-bg - Item background on hover\n * @cssprop --command-palette-option-focus-bg - Item background when keyboard-focused\n *\n * @part dialog - Dialog root container\n * @part panel - Panel container\n * @part input - Search input\n * @part listbox - Listbox of matching commands\n *\n * @example\n * ```html\n * <ore-command-palette label=\"Command palette\" placeholder=\"Type a command…\">\n * <ore-command-palette-item value=\"new-file\" group=\"File\" shortcut=\"⌘+N\">New File</ore-command-palette-item>\n * <ore-command-palette-item value=\"open-file\" group=\"File\" shortcut=\"⌘+O\">Open File…</ore-command-palette-item>\n * <ore-command-palette-item value=\"toggle-theme\" group=\"View\">Toggle Theme</ore-command-palette-item>\n * </ore-command-palette>\n * ```\n */\nexport const COMMAND_PALETTE_TAG = 'ore-command-palette' as const;\ndefine<OreCommandPaletteProps>(COMMAND_PALETTE_TAG, {\n props: {\n 'default-open': prop.bool(false),\n 'empty-text': prop.string('No results found.'),\n items: prop.data<OreCommandPaletteProps['items']>(),\n 'keep-open-on-select': prop.bool(false),\n label: prop.string('Command palette'),\n loading: prop.bool(false),\n 'no-filter': prop.bool(false),\n open: { default: undefined as boolean | undefined, parse: parseOptionalBool },\n placeholder: prop.string('Type a command or search…'),\n shortcut: prop.string('mod+k'),\n },\n setup(props) {\n const el = getHost();\n const emit = useEmit<OreCommandPaletteEvents>();\n const abortSignal = lifecycleSignal(onCleanup);\n\n const dialogRef = ref<HTMLDialogElement>();\n const searchInputRef = ref<HTMLInputElement>();\n const listboxId = 'cmdk-listbox';\n\n let listboxEl: HTMLElement | null = null;\n\n const query = signal('');\n\n // ── Items: slotted <ore-command-palette-item> elements, merged with the `items` prop ──\n // Items are pure data nodes (see `ore-command-palette-item`'s own doc comment) — they're\n // never projected through a `<slot>`, just read directly off the host's light DOM. That\n // sidesteps `useSlots()`/`slotchange` entirely: a plain MutationObserver on the host\n // already covers everything `slotchange` would (an item added or removed) *and* the case\n // it can't — an already-assigned item's own text/attributes changing. For items written\n // as static, inline HTML, the browser's parser can insert the last item before appending\n // its text-node child; without watching `characterData`, that would permanently cache an\n // empty label, since nothing else re-triggers once the element itself stops changing.\n const slottedItems = signal<CommandPaletteItem[]>([]);\n\n const reparseSlottedItems = (): void => {\n slottedItems.value = parseSlottedItems([...el.children]);\n };\n\n reparseSlottedItems();\n\n const lightDomObserver = new MutationObserver(reparseSlottedItems);\n\n lightDomObserver.observe(el, { characterData: true, childList: true, subtree: true });\n onCleanup(() => lightDomObserver.disconnect());\n\n const allItems = computed<CommandPaletteItem[]>(() => {\n const explicit = props.items.value;\n\n return Array.isArray(explicit) ? explicit.map(normalizeItem) : slottedItems.value;\n });\n\n const filteredItems = computed(() => filterItems(allItems.value, query.value, Boolean(props['no-filter'].value)));\n const rows = computed(() => buildRows(filteredItems.value));\n\n // ── Keyboard navigation over the filtered list (Up/Down/Home/End) ──\n const scrollFocusedIntoView = (): void => {\n listboxEl?.querySelector<HTMLElement>('[data-focused]')?.scrollIntoView({ block: 'nearest' });\n };\n\n const list = createListControl<CommandPaletteItem>({\n getItems: () => filteredItems.value,\n isItemDisabled: (item) => item.disabled,\n onNavigate: scrollFocusedIntoView,\n signal: abortSignal,\n });\n const { focusedIndex } = list;\n\n // ── Dialog chrome: native <dialog>, focus trap, Escape, backdrop click ──\n const resetSearch = (): void => {\n query.value = '';\n list.reset();\n };\n\n const { closeWithAnimation, overlay, requestClose, setupNativeListeners } = useDialogControl({\n defaultOpen: props['default-open'],\n dialogRef,\n getPanelEl: () => dialogRef.value?.querySelector<HTMLElement>('.panel'),\n host: el,\n initialFocus: computed(() => '.search-input'),\n isPersistent: () => false,\n onCleanup,\n onEvent,\n onNativeClose: (reason) => {\n emit('open-change', { open: false, reason });\n resetSearch();\n },\n onOpen: (reason) => {\n emit('open-change', { open: true, reason });\n list.navigate('first');\n },\n openProp: props.open,\n performClose: () => closeWithAnimation(),\n returnFocus: computed(() => true),\n });\n\n // ── Global keyboard shortcut (opens/closes the palette from anywhere) ──\n const globalKeymap = createKeymap();\n\n globalKeymap.mount(window);\n onCleanup(() => globalKeymap.dispose());\n\n let unbindShortcut: (() => void) | null = null;\n\n watch(\n props.shortcut,\n (shortcutStr) => {\n unbindShortcut?.();\n unbindShortcut = null;\n\n const trimmed = (shortcutStr ?? '').trim();\n\n if (!trimmed) return;\n\n try {\n unbindShortcut = globalKeymap.bind(trimmed, () => {\n overlay.toggle('keyboard', 'trigger');\n });\n } catch (error) {\n warn(`invalid \"shortcut\" value \"${trimmed}\" — ${error instanceof Error ? error.message : String(error)}`);\n }\n },\n { immediate: true },\n );\n\n onMounted(() => {\n setupNativeListeners();\n });\n\n // ── Selection ────────────────────────────────────────────────────────────\n function selectItem(item: CommandPaletteItem): void {\n if (item.disabled) return;\n\n emit('select', { item, label: item.label, value: item.value });\n\n if (props['keep-open-on-select'].value) {\n searchInputRef.value?.focus();\n } else {\n requestClose('trigger');\n }\n }\n\n function handleInput(e: Event): void {\n const value = (e.target as HTMLInputElement).value;\n\n query.value = value;\n list.navigate('first');\n emit('search', { query: value });\n\n const count = filteredItems.value.length;\n\n announce(count === 0 ? props['empty-text'].value! : `${count} result${count === 1 ? '' : 's'} found`);\n }\n\n function handleKeydown(e: KeyboardEvent): void {\n if (list.handleKeydown(e)) return;\n\n if (e.key === 'Enter') {\n // `list.navigate('first')` (called on open and on every keystroke) already keeps\n // the focused row off a disabled item, so this fallback is only ever reached when\n // nothing has been focused yet — but it mirrors `selectItem`'s own disabled guard\n // rather than blindly grabbing index 0, in case that navigate-on-open wiring ever\n // changes.\n const active = list.getActiveItem() ?? filteredItems.value.find((item) => !item.disabled);\n\n if (active) {\n e.preventDefault();\n selectItem(active);\n }\n }\n }\n\n return html`\n <dialog ref=\"${dialogRef}\" class=\"dialog\" part=\"dialog\" aria-label=\"${props.label}\" aria-modal=\"true\">\n <div class=\"panel\" part=\"panel\">\n <div class=\"search\" part=\"search\">\n <span class=\"search-icon\" aria-hidden=\"true\"><ore-icon name=\"search\" size=\"18\"></ore-icon></span>\n <input\n ref=\"${searchInputRef}\"\n class=\"search-input\"\n part=\"input\"\n type=\"text\"\n role=\"combobox\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n aria-expanded=\"true\"\n aria-autocomplete=\"list\"\n aria-controls=\"${listboxId}\"\n aria-activedescendant=\"${() => (focusedIndex.value >= 0 ? `${listboxId}-opt-${focusedIndex.value}` : '')}\"\n placeholder=\"${props.placeholder}\"\n value=\"${query}\"\n @input=\"${handleInput}\"\n @keydown=\"${handleKeydown}\" />\n <span class=\"search-loader\" ?hidden=\"${() => !props.loading.value}\" aria-hidden=\"true\"></span>\n </div>\n <div\n class=\"listbox\"\n part=\"listbox\"\n role=\"listbox\"\n id=\"${listboxId}\"\n aria-label=\"Commands\"\n ref=\"${(listEl: HTMLElement) => {\n listboxEl = listEl;\n }}\">\n <div class=\"empty\" role=\"presentation\" ?hidden=\"${() => props.loading.value || rows.value.length > 0}\">\n ${() => props['empty-text'].value}\n </div>\n ${() =>\n rows.value.map((row) =>\n row.type === 'group'\n ? html`\n <div class=\"group-heading\" role=\"presentation\">${row.group}</div>\n `\n : html`\n <div\n class=\"item\"\n part=\"item\"\n role=\"option\"\n id=\"${`${listboxId}-opt-${row.idx}`}\"\n aria-selected=\"${() => String(focusedIndex.value === row.idx)}\"\n aria-disabled=\"${() => String(row.item.disabled)}\"\n ?data-focused=\"${() => focusedIndex.value === row.idx}\"\n ?data-disabled=\"${() => row.item.disabled}\"\n @click=\"${(e: MouseEvent) => {\n e.stopPropagation();\n selectItem(row.item);\n }}\"\n @pointerenter=\"${() => {\n list.set(row.idx);\n }}\">\n ${\n row.item.icon\n ? html`\n <span class=\"item-icon\" aria-hidden=\"true\">\n <ore-icon name=\"${row.item.icon}\" size=\"16\"></ore-icon>\n </span>\n `\n : ''\n }\n <span class=\"item-label\">${row.item.label}</span>\n ${\n row.item.shortcut\n ? html`\n <span class=\"item-shortcut\">\n ${splitShortcutKeys(row.item.shortcut).map(\n (key) => html`\n <kbd>${key}</kbd>\n `,\n )}\n </span>\n `\n : ''\n }\n </div>\n `,\n )}\n </div>\n <div class=\"footer\" part=\"footer\">\n <span class=\"footer-hint\">\n <kbd>↑</kbd>\n <kbd>↓</kbd>\n Navigate\n </span>\n <span class=\"footer-hint\">\n <kbd>Enter</kbd>\n Select\n </span>\n <span class=\"footer-hint\">\n <kbd>Esc</kbd>\n Close\n </span>\n </div>\n </div>\n </dialog>\n `;\n },\n styles: [reducedMotionMixin, componentStyles],\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"command-palette.cjs","names":[],"sources":["../src/overlay/command-palette/command-palette.ts"],"sourcesContent":["import { createKeymap } from '@vielzeug/keymap';\nimport { define, getHost, html, onCleanup, onEvent, onMounted, prop, ref, useEmit } from '@vielzeug/ore';\nimport { computed, signal, watch } from '@vielzeug/ripple';\nimport { warn } from '../../_dev';\nimport { announce, createListControl, lifecycleSignal } from '../../core';\nimport { reducedMotionMixin } from '../../styles';\nimport { useDialogControl } from '../shared/use-dialog';\nimport type { CommandPaletteItem, OreCommandPaletteEvents, OreCommandPaletteProps } from './command-palette.types';\nimport { buildRows, filterItems, normalizeItem, parseSlottedItems, splitShortcutKeys } from './command-palette-items';\nimport '../../content/icon/icon';\nimport componentStyles from './command-palette.css?inline';\n\nexport type { OreCommandPaletteEvents, OreCommandPaletteProps } from './command-palette.types';\n\nconst parseOptionalBool = (value: string | null): boolean | undefined =>\n value == null ? undefined : value === '' || value === 'true';\n\n/**\n * A pure data node describing one command. Never rendered directly — `ore-command-palette`\n * reads its attributes/text content and renders the visible row itself.\n *\n * @element ore-command-palette-item\n *\n * @attr {string} value - Value emitted by the `select` event and matched against the search query\n * @attr {string} label - Explicit label text; falls back to the element's text content\n * @attr {string} group - Group heading the item is clustered under\n * @attr {string} shortcut - Display-only keyboard hint rendered at the end of the row, one `<kbd>` per `+`-separated key (e.g. `\"⌘+S\"` renders two keycaps; a literal `+` key isn't representable — spell it out, e.g. `\"Ctrl+Plus\"`)\n * @attr {boolean} disabled - Excludes the item from keyboard navigation and selection\n *\n * @slot icon - Optional leading icon content\n *\n * @example\n * ```html\n * <ore-command-palette-item value=\"new-file\" group=\"File\" shortcut=\"⌘+N\">New File</ore-command-palette-item>\n * ```\n */\nexport const COMMAND_PALETTE_ITEM_TAG = 'ore-command-palette-item' as const;\ndefine(COMMAND_PALETTE_ITEM_TAG, {\n setup() {\n return html``;\n },\n});\n\n/**\n * A searchable, keyboard-driven list of commands presented in a centered modal —\n * the \"⌘K\" pattern popularized by editors and productivity apps. Built on the\n * native `<dialog>` element (focus trap, top-layer stacking, `Escape`-to-close)\n * and `@vielzeug/keymap` for the global shortcut that opens it.\n *\n * @element ore-command-palette\n * @element ore-command-palette-item - Slotted command definition (place in default slot)\n *\n * @attr {boolean} open - Controls the open state of the palette\n * @attr {boolean} default-open - Initial uncontrolled open state\n * @attr {string} label - Accessible label for the dialog (screen-reader-only heading)\n * @attr {string} placeholder - Placeholder text for the search input\n * @attr {string} shortcut - Global keyboard shortcut (keymap syntax) that toggles the palette. Default `\"mod+k\"`; set to `\"\"` to disable\n * @attr {boolean} no-filter - Disable built-in client-side filtering (for server-driven search)\n * @attr {boolean} loading - Shows a loading row below the search input\n * @attr {string} empty-text - Message shown when no item matches the query\n * @attr {boolean} keep-open-on-select - Keep the palette open after an item is selected\n *\n * @fires open-change - Fired when the palette state changes. detail: `{ open, reason }`\n * @fires search - Fired on every keystroke in the search input. detail: `{ query }`\n * @fires select - Fired when a command is chosen (click or `Enter`). detail: `{ value, label, item }`\n *\n * @slot - `<ore-command-palette-item>` elements (alternative/supplement to the `items` prop)\n *\n * @cssprop --command-palette-bg - Panel background color\n * @cssprop --command-palette-border-color - Panel border color\n * @cssprop --command-palette-radius - Panel border radius\n * @cssprop --command-palette-shadow - Panel drop shadow\n * @cssprop --command-palette-max-width - Maximum panel width\n * @cssprop --command-palette-backdrop - Backdrop overlay color\n * @cssprop --command-palette-option-hover-bg - Item background on hover\n * @cssprop --command-palette-option-focus-bg - Item background when keyboard-focused\n *\n * @part dialog - Dialog root container\n * @part panel - Panel container\n * @part input - Search input\n * @part listbox - Listbox of matching commands\n *\n * @example\n * ```html\n * <ore-command-palette label=\"Command palette\" placeholder=\"Type a command…\">\n * <ore-command-palette-item value=\"new-file\" group=\"File\" shortcut=\"⌘+N\">New File</ore-command-palette-item>\n * <ore-command-palette-item value=\"open-file\" group=\"File\" shortcut=\"⌘+O\">Open File…</ore-command-palette-item>\n * <ore-command-palette-item value=\"toggle-theme\" group=\"View\">Toggle Theme</ore-command-palette-item>\n * </ore-command-palette>\n * ```\n */\nexport const COMMAND_PALETTE_TAG = 'ore-command-palette' as const;\ndefine<OreCommandPaletteProps>(COMMAND_PALETTE_TAG, {\n props: {\n 'default-open': prop.bool(false),\n 'empty-text': prop.string('No results found.'),\n items: prop.data<OreCommandPaletteProps['items']>(),\n 'keep-open-on-select': prop.bool(false),\n label: prop.string('Command palette'),\n loading: prop.bool(false),\n 'no-filter': prop.bool(false),\n open: { default: undefined as boolean | undefined, parse: parseOptionalBool },\n placeholder: prop.string('Type a command or search…'),\n shortcut: prop.string('mod+k'),\n },\n setup(props) {\n const el = getHost();\n const emit = useEmit<OreCommandPaletteEvents>();\n const abortSignal = lifecycleSignal(onCleanup);\n\n const dialogRef = ref<HTMLDialogElement>();\n const searchInputRef = ref<HTMLInputElement>();\n const listboxId = 'cmdk-listbox';\n\n let listboxEl: HTMLElement | null = null;\n\n const query = signal('');\n\n // ── Items: slotted <ore-command-palette-item> elements, merged with the `items` prop ──\n // Items are pure data nodes (see `ore-command-palette-item`'s own doc comment) — they're\n // never projected through a `<slot>`, just read directly off the host's light DOM. That\n // sidesteps `useSlots()`/`slotchange` entirely: a plain MutationObserver on the host\n // already covers everything `slotchange` would (an item added or removed) *and* the case\n // it can't — an already-assigned item's own text/attributes changing. For items written\n // as static, inline HTML, the browser's parser can insert the last item before appending\n // its text-node child; without watching `characterData`, that would permanently cache an\n // empty label, since nothing else re-triggers once the element itself stops changing.\n const slottedItems = signal<CommandPaletteItem[]>([]);\n\n const reparseSlottedItems = (): void => {\n slottedItems.value = parseSlottedItems([...el.children]);\n };\n\n reparseSlottedItems();\n\n const lightDomObserver = new MutationObserver(reparseSlottedItems);\n\n lightDomObserver.observe(el, { characterData: true, childList: true, subtree: true });\n onCleanup(() => lightDomObserver.disconnect());\n\n const allItems = computed<CommandPaletteItem[]>(() => {\n const explicit = props.items.value;\n\n return Array.isArray(explicit) ? explicit.map(normalizeItem) : slottedItems.value;\n });\n\n const filteredItems = computed(() => filterItems(allItems.value, query.value, Boolean(props['no-filter'].value)));\n const rows = computed(() => buildRows(filteredItems.value));\n\n // ── Keyboard navigation over the filtered list (Up/Down/Home/End) ──\n const scrollFocusedIntoView = (): void => {\n listboxEl?.querySelector<HTMLElement>('[data-focused]')?.scrollIntoView({ block: 'nearest' });\n };\n\n const list = createListControl<CommandPaletteItem>({\n getItems: () => filteredItems.value,\n isItemDisabled: (item) => item.disabled,\n onNavigate: scrollFocusedIntoView,\n signal: abortSignal,\n });\n const { focusedIndex } = list;\n\n // ── Dialog chrome: native <dialog>, focus trap, Escape, backdrop click ──\n const resetSearch = (): void => {\n query.value = '';\n list.reset();\n };\n\n const { closeWithAnimation, overlay, requestClose, setupNativeListeners } = useDialogControl({\n defaultOpen: props['default-open'],\n dialogRef,\n getPanelEl: () => dialogRef.value?.querySelector<HTMLElement>('.panel'),\n host: el,\n initialFocus: computed(() => '.search-input'),\n isPersistent: () => false,\n onCleanup,\n onEvent,\n onNativeClose: (reason) => {\n emit('open-change', { open: false, reason });\n resetSearch();\n },\n onOpen: (reason) => {\n emit('open-change', { open: true, reason });\n list.navigate('first');\n },\n openProp: props.open,\n performClose: () => closeWithAnimation(),\n returnFocus: computed(() => true),\n });\n\n // ── Global keyboard shortcut (opens/closes the palette from anywhere) ──\n const globalKeymap = createKeymap();\n\n globalKeymap.mount(window);\n onCleanup(() => globalKeymap.dispose());\n\n let unbindShortcut: (() => void) | null = null;\n\n watch(\n props.shortcut,\n (shortcutStr) => {\n unbindShortcut?.();\n unbindShortcut = null;\n\n const trimmed = (shortcutStr ?? '').trim();\n\n if (!trimmed) return;\n\n try {\n unbindShortcut = globalKeymap.bind(trimmed, () => {\n overlay.toggle('keyboard', 'trigger');\n });\n } catch (error) {\n warn(`invalid \"shortcut\" value \"${trimmed}\" — ${error instanceof Error ? error.message : String(error)}`);\n }\n },\n { immediate: true },\n );\n\n onMounted(() => {\n setupNativeListeners();\n });\n\n // ── Selection ────────────────────────────────────────────────────────────\n function selectItem(item: CommandPaletteItem): void {\n if (item.disabled) return;\n\n emit('select', { item, label: item.label, value: item.value });\n\n if (props['keep-open-on-select'].value) {\n searchInputRef.value?.focus();\n } else {\n requestClose('trigger');\n }\n }\n\n function handleInput(e: Event): void {\n const value = (e.target as HTMLInputElement).value;\n\n query.value = value;\n list.navigate('first');\n emit('search', { query: value });\n\n const count = filteredItems.value.length;\n\n announce(count === 0 ? props['empty-text'].value! : `${count} result${count === 1 ? '' : 's'} found`, {\n document: el.ownerDocument,\n });\n }\n\n function handleKeydown(e: KeyboardEvent): void {\n if (list.handleKeydown(e)) return;\n\n if (e.key === 'Enter') {\n // `list.navigate('first')` (called on open and on every keystroke) already keeps\n // the focused row off a disabled item, so this fallback is only ever reached when\n // nothing has been focused yet — but it mirrors `selectItem`'s own disabled guard\n // rather than blindly grabbing index 0, in case that navigate-on-open wiring ever\n // changes.\n const active = list.getActiveItem() ?? filteredItems.value.find((item) => !item.disabled);\n\n if (active) {\n e.preventDefault();\n selectItem(active);\n }\n }\n }\n\n return html`\n <dialog ref=\"${dialogRef}\" class=\"dialog\" part=\"dialog\" aria-label=\"${props.label}\" aria-modal=\"true\">\n <div class=\"panel\" part=\"panel\">\n <div class=\"search\" part=\"search\">\n <span class=\"search-icon\" aria-hidden=\"true\"><ore-icon name=\"search\" size=\"18\"></ore-icon></span>\n <input\n ref=\"${searchInputRef}\"\n class=\"search-input\"\n part=\"input\"\n type=\"text\"\n role=\"combobox\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n aria-expanded=\"true\"\n aria-autocomplete=\"list\"\n aria-controls=\"${listboxId}\"\n aria-activedescendant=\"${() => (focusedIndex.value >= 0 ? `${listboxId}-opt-${focusedIndex.value}` : '')}\"\n placeholder=\"${props.placeholder}\"\n value=\"${query}\"\n @input=\"${handleInput}\"\n @keydown=\"${handleKeydown}\" />\n <span class=\"search-loader\" ?hidden=\"${() => !props.loading.value}\" aria-hidden=\"true\"></span>\n </div>\n <div\n class=\"listbox\"\n part=\"listbox\"\n role=\"listbox\"\n id=\"${listboxId}\"\n aria-label=\"Commands\"\n ref=\"${(listEl: HTMLElement) => {\n listboxEl = listEl;\n }}\">\n <div class=\"empty\" role=\"presentation\" ?hidden=\"${() => props.loading.value || rows.value.length > 0}\">\n ${() => props['empty-text'].value}\n </div>\n ${() =>\n rows.value.map((row) =>\n row.type === 'group'\n ? html`\n <div class=\"group-heading\" role=\"presentation\">${row.group}</div>\n `\n : html`\n <div\n class=\"item\"\n part=\"item\"\n role=\"option\"\n id=\"${`${listboxId}-opt-${row.idx}`}\"\n aria-selected=\"${() => String(focusedIndex.value === row.idx)}\"\n aria-disabled=\"${() => String(row.item.disabled)}\"\n ?data-focused=\"${() => focusedIndex.value === row.idx}\"\n ?data-disabled=\"${() => row.item.disabled}\"\n @click=\"${(e: MouseEvent) => {\n e.stopPropagation();\n selectItem(row.item);\n }}\"\n @pointerenter=\"${() => {\n list.set(row.idx);\n }}\">\n ${\n row.item.icon\n ? html`\n <span class=\"item-icon\" aria-hidden=\"true\">\n <ore-icon name=\"${row.item.icon}\" size=\"16\"></ore-icon>\n </span>\n `\n : ''\n }\n <span class=\"item-label\">${row.item.label}</span>\n ${\n row.item.shortcut\n ? html`\n <span class=\"item-shortcut\">\n ${splitShortcutKeys(row.item.shortcut).map(\n (key) => html`\n <kbd>${key}</kbd>\n `,\n )}\n </span>\n `\n : ''\n }\n </div>\n `,\n )}\n </div>\n <div class=\"footer\" part=\"footer\">\n <span class=\"footer-hint\">\n <kbd>↑</kbd>\n <kbd>↓</kbd>\n Navigate\n </span>\n <span class=\"footer-hint\">\n <kbd>Enter</kbd>\n Select\n </span>\n <span class=\"footer-hint\">\n <kbd>Esc</kbd>\n Close\n </span>\n </div>\n </div>\n </dialog>\n `;\n },\n styles: [reducedMotionMixin, componentStyles],\n});\n"],"mappings":"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"}
@@ -105,7 +105,7 @@ d(E, {
105
105
  let r = n.target.value;
106
106
  O.value = r, F.navigate("first"), d("search", { query: r });
107
107
  let i = N.value.length;
108
- t(i === 0 ? e["empty-text"].value : `${i} result${i === 1 ? "" : "s"} found`);
108
+ t(i === 0 ? e["empty-text"].value : `${i} result${i === 1 ? "" : "s"} found`, { document: u.ownerDocument });
109
109
  }
110
110
  function K(e) {
111
111
  if (!F.handleKeydown(e) && e.key === "Enter") {
@@ -1 +1 @@
1
- {"version":3,"file":"command-palette.js","names":[],"sources":["../src/overlay/command-palette/command-palette.ts"],"sourcesContent":["import { createKeymap } from '@vielzeug/keymap';\nimport { define, getHost, html, onCleanup, onEvent, onMounted, prop, ref, useEmit } from '@vielzeug/ore';\nimport { computed, signal, watch } from '@vielzeug/ripple';\nimport { warn } from '../../_dev';\nimport { announce, createListControl, lifecycleSignal } from '../../core';\nimport { reducedMotionMixin } from '../../styles';\nimport { useDialogControl } from '../shared/use-dialog';\nimport type { CommandPaletteItem, OreCommandPaletteEvents, OreCommandPaletteProps } from './command-palette.types';\nimport { buildRows, filterItems, normalizeItem, parseSlottedItems, splitShortcutKeys } from './command-palette-items';\nimport '../../content/icon/icon';\nimport componentStyles from './command-palette.css?inline';\n\nexport type { OreCommandPaletteEvents, OreCommandPaletteProps } from './command-palette.types';\n\nconst parseOptionalBool = (value: string | null): boolean | undefined =>\n value == null ? undefined : value === '' || value === 'true';\n\n/**\n * A pure data node describing one command. Never rendered directly — `ore-command-palette`\n * reads its attributes/text content and renders the visible row itself.\n *\n * @element ore-command-palette-item\n *\n * @attr {string} value - Value emitted by the `select` event and matched against the search query\n * @attr {string} label - Explicit label text; falls back to the element's text content\n * @attr {string} group - Group heading the item is clustered under\n * @attr {string} shortcut - Display-only keyboard hint rendered at the end of the row, one `<kbd>` per `+`-separated key (e.g. `\"⌘+S\"` renders two keycaps; a literal `+` key isn't representable — spell it out, e.g. `\"Ctrl+Plus\"`)\n * @attr {boolean} disabled - Excludes the item from keyboard navigation and selection\n *\n * @slot icon - Optional leading icon content\n *\n * @example\n * ```html\n * <ore-command-palette-item value=\"new-file\" group=\"File\" shortcut=\"⌘+N\">New File</ore-command-palette-item>\n * ```\n */\nexport const COMMAND_PALETTE_ITEM_TAG = 'ore-command-palette-item' as const;\ndefine(COMMAND_PALETTE_ITEM_TAG, {\n setup() {\n return html``;\n },\n});\n\n/**\n * A searchable, keyboard-driven list of commands presented in a centered modal —\n * the \"⌘K\" pattern popularized by editors and productivity apps. Built on the\n * native `<dialog>` element (focus trap, top-layer stacking, `Escape`-to-close)\n * and `@vielzeug/keymap` for the global shortcut that opens it.\n *\n * @element ore-command-palette\n * @element ore-command-palette-item - Slotted command definition (place in default slot)\n *\n * @attr {boolean} open - Controls the open state of the palette\n * @attr {boolean} default-open - Initial uncontrolled open state\n * @attr {string} label - Accessible label for the dialog (screen-reader-only heading)\n * @attr {string} placeholder - Placeholder text for the search input\n * @attr {string} shortcut - Global keyboard shortcut (keymap syntax) that toggles the palette. Default `\"mod+k\"`; set to `\"\"` to disable\n * @attr {boolean} no-filter - Disable built-in client-side filtering (for server-driven search)\n * @attr {boolean} loading - Shows a loading row below the search input\n * @attr {string} empty-text - Message shown when no item matches the query\n * @attr {boolean} keep-open-on-select - Keep the palette open after an item is selected\n *\n * @fires open-change - Fired when the palette state changes. detail: `{ open, reason }`\n * @fires search - Fired on every keystroke in the search input. detail: `{ query }`\n * @fires select - Fired when a command is chosen (click or `Enter`). detail: `{ value, label, item }`\n *\n * @slot - `<ore-command-palette-item>` elements (alternative/supplement to the `items` prop)\n *\n * @cssprop --command-palette-bg - Panel background color\n * @cssprop --command-palette-border-color - Panel border color\n * @cssprop --command-palette-radius - Panel border radius\n * @cssprop --command-palette-shadow - Panel drop shadow\n * @cssprop --command-palette-max-width - Maximum panel width\n * @cssprop --command-palette-backdrop - Backdrop overlay color\n * @cssprop --command-palette-option-hover-bg - Item background on hover\n * @cssprop --command-palette-option-focus-bg - Item background when keyboard-focused\n *\n * @part dialog - Dialog root container\n * @part panel - Panel container\n * @part input - Search input\n * @part listbox - Listbox of matching commands\n *\n * @example\n * ```html\n * <ore-command-palette label=\"Command palette\" placeholder=\"Type a command…\">\n * <ore-command-palette-item value=\"new-file\" group=\"File\" shortcut=\"⌘+N\">New File</ore-command-palette-item>\n * <ore-command-palette-item value=\"open-file\" group=\"File\" shortcut=\"⌘+O\">Open File…</ore-command-palette-item>\n * <ore-command-palette-item value=\"toggle-theme\" group=\"View\">Toggle Theme</ore-command-palette-item>\n * </ore-command-palette>\n * ```\n */\nexport const COMMAND_PALETTE_TAG = 'ore-command-palette' as const;\ndefine<OreCommandPaletteProps>(COMMAND_PALETTE_TAG, {\n props: {\n 'default-open': prop.bool(false),\n 'empty-text': prop.string('No results found.'),\n items: prop.data<OreCommandPaletteProps['items']>(),\n 'keep-open-on-select': prop.bool(false),\n label: prop.string('Command palette'),\n loading: prop.bool(false),\n 'no-filter': prop.bool(false),\n open: { default: undefined as boolean | undefined, parse: parseOptionalBool },\n placeholder: prop.string('Type a command or search…'),\n shortcut: prop.string('mod+k'),\n },\n setup(props) {\n const el = getHost();\n const emit = useEmit<OreCommandPaletteEvents>();\n const abortSignal = lifecycleSignal(onCleanup);\n\n const dialogRef = ref<HTMLDialogElement>();\n const searchInputRef = ref<HTMLInputElement>();\n const listboxId = 'cmdk-listbox';\n\n let listboxEl: HTMLElement | null = null;\n\n const query = signal('');\n\n // ── Items: slotted <ore-command-palette-item> elements, merged with the `items` prop ──\n // Items are pure data nodes (see `ore-command-palette-item`'s own doc comment) — they're\n // never projected through a `<slot>`, just read directly off the host's light DOM. That\n // sidesteps `useSlots()`/`slotchange` entirely: a plain MutationObserver on the host\n // already covers everything `slotchange` would (an item added or removed) *and* the case\n // it can't — an already-assigned item's own text/attributes changing. For items written\n // as static, inline HTML, the browser's parser can insert the last item before appending\n // its text-node child; without watching `characterData`, that would permanently cache an\n // empty label, since nothing else re-triggers once the element itself stops changing.\n const slottedItems = signal<CommandPaletteItem[]>([]);\n\n const reparseSlottedItems = (): void => {\n slottedItems.value = parseSlottedItems([...el.children]);\n };\n\n reparseSlottedItems();\n\n const lightDomObserver = new MutationObserver(reparseSlottedItems);\n\n lightDomObserver.observe(el, { characterData: true, childList: true, subtree: true });\n onCleanup(() => lightDomObserver.disconnect());\n\n const allItems = computed<CommandPaletteItem[]>(() => {\n const explicit = props.items.value;\n\n return Array.isArray(explicit) ? explicit.map(normalizeItem) : slottedItems.value;\n });\n\n const filteredItems = computed(() => filterItems(allItems.value, query.value, Boolean(props['no-filter'].value)));\n const rows = computed(() => buildRows(filteredItems.value));\n\n // ── Keyboard navigation over the filtered list (Up/Down/Home/End) ──\n const scrollFocusedIntoView = (): void => {\n listboxEl?.querySelector<HTMLElement>('[data-focused]')?.scrollIntoView({ block: 'nearest' });\n };\n\n const list = createListControl<CommandPaletteItem>({\n getItems: () => filteredItems.value,\n isItemDisabled: (item) => item.disabled,\n onNavigate: scrollFocusedIntoView,\n signal: abortSignal,\n });\n const { focusedIndex } = list;\n\n // ── Dialog chrome: native <dialog>, focus trap, Escape, backdrop click ──\n const resetSearch = (): void => {\n query.value = '';\n list.reset();\n };\n\n const { closeWithAnimation, overlay, requestClose, setupNativeListeners } = useDialogControl({\n defaultOpen: props['default-open'],\n dialogRef,\n getPanelEl: () => dialogRef.value?.querySelector<HTMLElement>('.panel'),\n host: el,\n initialFocus: computed(() => '.search-input'),\n isPersistent: () => false,\n onCleanup,\n onEvent,\n onNativeClose: (reason) => {\n emit('open-change', { open: false, reason });\n resetSearch();\n },\n onOpen: (reason) => {\n emit('open-change', { open: true, reason });\n list.navigate('first');\n },\n openProp: props.open,\n performClose: () => closeWithAnimation(),\n returnFocus: computed(() => true),\n });\n\n // ── Global keyboard shortcut (opens/closes the palette from anywhere) ──\n const globalKeymap = createKeymap();\n\n globalKeymap.mount(window);\n onCleanup(() => globalKeymap.dispose());\n\n let unbindShortcut: (() => void) | null = null;\n\n watch(\n props.shortcut,\n (shortcutStr) => {\n unbindShortcut?.();\n unbindShortcut = null;\n\n const trimmed = (shortcutStr ?? '').trim();\n\n if (!trimmed) return;\n\n try {\n unbindShortcut = globalKeymap.bind(trimmed, () => {\n overlay.toggle('keyboard', 'trigger');\n });\n } catch (error) {\n warn(`invalid \"shortcut\" value \"${trimmed}\" — ${error instanceof Error ? error.message : String(error)}`);\n }\n },\n { immediate: true },\n );\n\n onMounted(() => {\n setupNativeListeners();\n });\n\n // ── Selection ────────────────────────────────────────────────────────────\n function selectItem(item: CommandPaletteItem): void {\n if (item.disabled) return;\n\n emit('select', { item, label: item.label, value: item.value });\n\n if (props['keep-open-on-select'].value) {\n searchInputRef.value?.focus();\n } else {\n requestClose('trigger');\n }\n }\n\n function handleInput(e: Event): void {\n const value = (e.target as HTMLInputElement).value;\n\n query.value = value;\n list.navigate('first');\n emit('search', { query: value });\n\n const count = filteredItems.value.length;\n\n announce(count === 0 ? props['empty-text'].value! : `${count} result${count === 1 ? '' : 's'} found`);\n }\n\n function handleKeydown(e: KeyboardEvent): void {\n if (list.handleKeydown(e)) return;\n\n if (e.key === 'Enter') {\n // `list.navigate('first')` (called on open and on every keystroke) already keeps\n // the focused row off a disabled item, so this fallback is only ever reached when\n // nothing has been focused yet — but it mirrors `selectItem`'s own disabled guard\n // rather than blindly grabbing index 0, in case that navigate-on-open wiring ever\n // changes.\n const active = list.getActiveItem() ?? filteredItems.value.find((item) => !item.disabled);\n\n if (active) {\n e.preventDefault();\n selectItem(active);\n }\n }\n }\n\n return html`\n <dialog ref=\"${dialogRef}\" class=\"dialog\" part=\"dialog\" aria-label=\"${props.label}\" aria-modal=\"true\">\n <div class=\"panel\" part=\"panel\">\n <div class=\"search\" part=\"search\">\n <span class=\"search-icon\" aria-hidden=\"true\"><ore-icon name=\"search\" size=\"18\"></ore-icon></span>\n <input\n ref=\"${searchInputRef}\"\n class=\"search-input\"\n part=\"input\"\n type=\"text\"\n role=\"combobox\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n aria-expanded=\"true\"\n aria-autocomplete=\"list\"\n aria-controls=\"${listboxId}\"\n aria-activedescendant=\"${() => (focusedIndex.value >= 0 ? `${listboxId}-opt-${focusedIndex.value}` : '')}\"\n placeholder=\"${props.placeholder}\"\n value=\"${query}\"\n @input=\"${handleInput}\"\n @keydown=\"${handleKeydown}\" />\n <span class=\"search-loader\" ?hidden=\"${() => !props.loading.value}\" aria-hidden=\"true\"></span>\n </div>\n <div\n class=\"listbox\"\n part=\"listbox\"\n role=\"listbox\"\n id=\"${listboxId}\"\n aria-label=\"Commands\"\n ref=\"${(listEl: HTMLElement) => {\n listboxEl = listEl;\n }}\">\n <div class=\"empty\" role=\"presentation\" ?hidden=\"${() => props.loading.value || rows.value.length > 0}\">\n ${() => props['empty-text'].value}\n </div>\n ${() =>\n rows.value.map((row) =>\n row.type === 'group'\n ? html`\n <div class=\"group-heading\" role=\"presentation\">${row.group}</div>\n `\n : html`\n <div\n class=\"item\"\n part=\"item\"\n role=\"option\"\n id=\"${`${listboxId}-opt-${row.idx}`}\"\n aria-selected=\"${() => String(focusedIndex.value === row.idx)}\"\n aria-disabled=\"${() => String(row.item.disabled)}\"\n ?data-focused=\"${() => focusedIndex.value === row.idx}\"\n ?data-disabled=\"${() => row.item.disabled}\"\n @click=\"${(e: MouseEvent) => {\n e.stopPropagation();\n selectItem(row.item);\n }}\"\n @pointerenter=\"${() => {\n list.set(row.idx);\n }}\">\n ${\n row.item.icon\n ? html`\n <span class=\"item-icon\" aria-hidden=\"true\">\n <ore-icon name=\"${row.item.icon}\" size=\"16\"></ore-icon>\n </span>\n `\n : ''\n }\n <span class=\"item-label\">${row.item.label}</span>\n ${\n row.item.shortcut\n ? html`\n <span class=\"item-shortcut\">\n ${splitShortcutKeys(row.item.shortcut).map(\n (key) => html`\n <kbd>${key}</kbd>\n `,\n )}\n </span>\n `\n : ''\n }\n </div>\n `,\n )}\n </div>\n <div class=\"footer\" part=\"footer\">\n <span class=\"footer-hint\">\n <kbd>↑</kbd>\n <kbd>↓</kbd>\n Navigate\n </span>\n <span class=\"footer-hint\">\n <kbd>Enter</kbd>\n Select\n </span>\n <span class=\"footer-hint\">\n <kbd>Esc</kbd>\n Close\n </span>\n </div>\n </div>\n </dialog>\n `;\n },\n styles: [reducedMotionMixin, componentStyles],\n});\n"],"mappings":";;;;;;;;;;;;AAcA,IAAM,KAAqB,MACzB,KAAS,OAAO,KAAA,IAAY,MAAU,MAAM,MAAU,QAqB3C,IAA2B;AACxC,EAAO,GAA0B,EAC/B,QAAQ;CACN,OAAO,CAAI;AACb,EACF,CAAC;AAkDD,IAAa,IAAsB;AACnC,EAA+B,GAAqB;CAClD,OAAO;EACL,gBAAgB,EAAK,KAAK,EAAK;EAC/B,cAAc,EAAK,OAAO,mBAAmB;EAC7C,OAAO,EAAK,KAAsC;EAClD,uBAAuB,EAAK,KAAK,EAAK;EACtC,OAAO,EAAK,OAAO,iBAAiB;EACpC,SAAS,EAAK,KAAK,EAAK;EACxB,aAAa,EAAK,KAAK,EAAK;EAC5B,MAAM;GAAE,SAAS,KAAA;GAAkC,OAAO;EAAkB;EAC5E,aAAa,EAAK,OAAO,2BAA2B;EACpD,UAAU,EAAK,OAAO,OAAO;CAC/B;CACA,MAAM,GAAO;EACX,IAAM,IAAK,EAAQ,GACb,IAAO,EAAiC,GACxC,IAAc,EAAgB,CAAS,GAEvC,IAAY,EAAuB,GACnC,IAAiB,EAAsB,GACvC,IAAY,gBAEd,IAAgC,MAE9B,IAAQ,EAAO,EAAE,GAWjB,IAAe,EAA6B,CAAC,CAAC,GAE9C,UAAkC;GACtC,EAAa,QAAQ,EAAkB,CAAC,GAAG,EAAG,QAAQ,CAAC;EACzD;EAEA,EAAoB;EAEpB,IAAM,IAAmB,IAAI,iBAAiB,CAAmB;EAGjE,AADA,EAAiB,QAAQ,GAAI;GAAE,eAAe;GAAM,WAAW;GAAM,SAAS;EAAK,CAAC,GACpF,QAAgB,EAAiB,WAAW,CAAC;EAE7C,IAAM,IAAW,QAAqC;GACpD,IAAM,IAAW,EAAM,MAAM;GAE7B,OAAO,MAAM,QAAQ,CAAQ,IAAI,EAAS,IAAI,CAAa,IAAI,EAAa;EAC9E,CAAC,GAEK,IAAgB,QAAe,EAAY,EAAS,OAAO,EAAM,OAAO,EAAQ,EAAM,YAAY,CAAC,KAAM,CAAC,GAC1G,IAAO,QAAe,EAAU,EAAc,KAAK,CAAC,GAOpD,IAAO,EAAsC;GACjD,gBAAgB,EAAc;GAC9B,iBAAiB,MAAS,EAAK;GAC/B,kBAPwC;IACxC,GAAW,cAA2B,gBAAgB,CAAC,EAAE,eAAe,EAAE,OAAO,UAAU,CAAC;GAC9F;GAME,QAAQ;EACV,CAAC,GACK,EAAE,oBAAiB,GAGnB,UAA0B;GAE9B,AADA,EAAM,QAAQ,IACd,EAAK,MAAM;EACb,GAEM,EAAE,uBAAoB,YAAS,iBAAc,4BAAyB,EAAiB;GAC3F,aAAa,EAAM;GACnB;GACA,kBAAkB,EAAU,OAAO,cAA2B,QAAQ;GACtE,MAAM;GACN,cAAc,QAAe,eAAe;GAC5C,oBAAoB;GACpB;GACA;GACA,gBAAgB,MAAW;IAEzB,AADA,EAAK,eAAe;KAAE,MAAM;KAAO;IAAO,CAAC,GAC3C,EAAY;GACd;GACA,SAAS,MAAW;IAElB,AADA,EAAK,eAAe;KAAE,MAAM;KAAM;IAAO,CAAC,GAC1C,EAAK,SAAS,OAAO;GACvB;GACA,UAAU,EAAM;GAChB,oBAAoB,EAAmB;GACvC,aAAa,QAAe,EAAI;EAClC,CAAC,GAGK,IAAe,EAAa;EAGlC,AADA,EAAa,MAAM,MAAM,GACzB,QAAgB,EAAa,QAAQ,CAAC;EAEtC,IAAI,IAAsC;EAuB1C,AArBA,EACE,EAAM,WACL,MAAgB;GAEf,AADA,IAAiB,GACjB,IAAiB;GAEjB,IAAM,KAAW,KAAe,GAAA,CAAI,KAAK;GAEpC,OAEL,IAAI;IACF,IAAiB,EAAa,KAAK,SAAe;KAChD,EAAQ,OAAO,YAAY,SAAS;IACtC,CAAC;GACH,SAAS,GAAO;IACd,AAAK,GAA6B,EAA7B,EAA2C,aAAiB,QAAQ,EAAM,UAAU,OAAO,CAAK,EAAhG;GACP;EACF,GACA,EAAE,WAAW,GAAK,CACpB,GAEA,QAAgB;GACd,EAAqB;EACvB,CAAC;EAGD,SAAS,EAAW,GAAgC;GAC9C,EAAK,aAET,EAAK,UAAU;IAAE;IAAM,OAAO,EAAK;IAAO,OAAO,EAAK;GAAM,CAAC,GAEzD,EAAM,sBAAsB,CAAC,QAC/B,EAAe,OAAO,MAAM,IAE5B,EAAa,SAAS;EAE1B;EAEA,SAAS,EAAY,GAAgB;GACnC,IAAM,IAAS,EAAE,OAA4B;GAI7C,AAFA,EAAM,QAAQ,GACd,EAAK,SAAS,OAAO,GACrB,EAAK,UAAU,EAAE,OAAO,EAAM,CAAC;GAE/B,IAAM,IAAQ,EAAc,MAAM;GAElC,EAAS,MAAU,IAAI,EAAM,aAAa,CAAC,QAAS,GAAG,EAAM,SAAS,MAAU,IAAI,KAAK,IAAI,OAAO;EACtG;EAEA,SAAS,EAAc,GAAwB;GACzC,OAAK,cAAc,CAAC,KAEpB,EAAE,QAAQ,SAAS;IAMrB,IAAM,IAAS,EAAK,cAAc,KAAK,EAAc,MAAM,MAAM,MAAS,CAAC,EAAK,QAAQ;IAExF,AAAI,MACF,EAAE,eAAe,GACjB,EAAW,CAAM;GAErB;EACF;EAEA,OAAO,CAAI;qBACM,EAAU,6CAA6C,EAAM,MAAM;;;;;qBAKnE,EAAe;;;;;;;;;+BASL,EAAU;6CACK,EAAa,SAAS,IAAI,GAAG,EAAU,OAAO,EAAa,UAAU,GAAI;6BAC1F,EAAM,YAAY;uBACxB,EAAM;wBACL,EAAY;0BACV,EAAc;yDACiB,CAAC,EAAM,QAAQ,MAAM;;;;;;kBAM5D,EAAU;;oBAER,MAAwB;GAC9B,IAAY;EACd,EAAE;oEACsD,EAAM,QAAQ,SAAS,EAAK,MAAM,SAAS,EAAE;sBAC3F,EAAM,aAAa,CAAC,MAAM;;oBAGlC,EAAK,MAAM,KAAK,MACd,EAAI,SAAS,UACT,CAAI;uEAC+C,EAAI,MAAM;wBAE7D,CAAI;;;;;8BAKM,GAAG,EAAU,OAAO,EAAI,MAAM;+CACb,OAAO,EAAa,UAAU,EAAI,GAAG,EAAE;+CACvC,OAAO,EAAI,KAAK,QAAQ,EAAE;+CAC1B,EAAa,UAAU,EAAI,IAAI;gDAC9B,EAAI,KAAK,SAAS;mCAC/B,MAAkB;GAE3B,AADA,EAAE,gBAAgB,GAClB,EAAW,EAAI,IAAI;EACrB,EAAE;+CACqB;GACrB,EAAK,IAAI,EAAI,GAAG;EAClB,EAAE;0BAEA,EAAI,KAAK,OACL,CAAI;;oDAEkB,EAAI,KAAK,KAAK;;kCAGpC,GACL;mDAC0B,EAAI,KAAK,MAAM;0BAExC,EAAI,KAAK,WACL,CAAI;;oCAEE,EAAkB,EAAI,KAAK,QAAQ,CAAC,CAAC,KACpC,MAAQ,CAAI;6CACJ,EAAI;qCAEf,EAAE;;kCAGN,GACL;;qBAGX,EAAE;;;;;;;;;;;;;;;;;;;;CAoBd;CACA,QAAQ,CAAC,GAAoB,CAAe;AAC9C,CAAC"}
1
+ {"version":3,"file":"command-palette.js","names":[],"sources":["../src/overlay/command-palette/command-palette.ts"],"sourcesContent":["import { createKeymap } from '@vielzeug/keymap';\nimport { define, getHost, html, onCleanup, onEvent, onMounted, prop, ref, useEmit } from '@vielzeug/ore';\nimport { computed, signal, watch } from '@vielzeug/ripple';\nimport { warn } from '../../_dev';\nimport { announce, createListControl, lifecycleSignal } from '../../core';\nimport { reducedMotionMixin } from '../../styles';\nimport { useDialogControl } from '../shared/use-dialog';\nimport type { CommandPaletteItem, OreCommandPaletteEvents, OreCommandPaletteProps } from './command-palette.types';\nimport { buildRows, filterItems, normalizeItem, parseSlottedItems, splitShortcutKeys } from './command-palette-items';\nimport '../../content/icon/icon';\nimport componentStyles from './command-palette.css?inline';\n\nexport type { OreCommandPaletteEvents, OreCommandPaletteProps } from './command-palette.types';\n\nconst parseOptionalBool = (value: string | null): boolean | undefined =>\n value == null ? undefined : value === '' || value === 'true';\n\n/**\n * A pure data node describing one command. Never rendered directly — `ore-command-palette`\n * reads its attributes/text content and renders the visible row itself.\n *\n * @element ore-command-palette-item\n *\n * @attr {string} value - Value emitted by the `select` event and matched against the search query\n * @attr {string} label - Explicit label text; falls back to the element's text content\n * @attr {string} group - Group heading the item is clustered under\n * @attr {string} shortcut - Display-only keyboard hint rendered at the end of the row, one `<kbd>` per `+`-separated key (e.g. `\"⌘+S\"` renders two keycaps; a literal `+` key isn't representable — spell it out, e.g. `\"Ctrl+Plus\"`)\n * @attr {boolean} disabled - Excludes the item from keyboard navigation and selection\n *\n * @slot icon - Optional leading icon content\n *\n * @example\n * ```html\n * <ore-command-palette-item value=\"new-file\" group=\"File\" shortcut=\"⌘+N\">New File</ore-command-palette-item>\n * ```\n */\nexport const COMMAND_PALETTE_ITEM_TAG = 'ore-command-palette-item' as const;\ndefine(COMMAND_PALETTE_ITEM_TAG, {\n setup() {\n return html``;\n },\n});\n\n/**\n * A searchable, keyboard-driven list of commands presented in a centered modal —\n * the \"⌘K\" pattern popularized by editors and productivity apps. Built on the\n * native `<dialog>` element (focus trap, top-layer stacking, `Escape`-to-close)\n * and `@vielzeug/keymap` for the global shortcut that opens it.\n *\n * @element ore-command-palette\n * @element ore-command-palette-item - Slotted command definition (place in default slot)\n *\n * @attr {boolean} open - Controls the open state of the palette\n * @attr {boolean} default-open - Initial uncontrolled open state\n * @attr {string} label - Accessible label for the dialog (screen-reader-only heading)\n * @attr {string} placeholder - Placeholder text for the search input\n * @attr {string} shortcut - Global keyboard shortcut (keymap syntax) that toggles the palette. Default `\"mod+k\"`; set to `\"\"` to disable\n * @attr {boolean} no-filter - Disable built-in client-side filtering (for server-driven search)\n * @attr {boolean} loading - Shows a loading row below the search input\n * @attr {string} empty-text - Message shown when no item matches the query\n * @attr {boolean} keep-open-on-select - Keep the palette open after an item is selected\n *\n * @fires open-change - Fired when the palette state changes. detail: `{ open, reason }`\n * @fires search - Fired on every keystroke in the search input. detail: `{ query }`\n * @fires select - Fired when a command is chosen (click or `Enter`). detail: `{ value, label, item }`\n *\n * @slot - `<ore-command-palette-item>` elements (alternative/supplement to the `items` prop)\n *\n * @cssprop --command-palette-bg - Panel background color\n * @cssprop --command-palette-border-color - Panel border color\n * @cssprop --command-palette-radius - Panel border radius\n * @cssprop --command-palette-shadow - Panel drop shadow\n * @cssprop --command-palette-max-width - Maximum panel width\n * @cssprop --command-palette-backdrop - Backdrop overlay color\n * @cssprop --command-palette-option-hover-bg - Item background on hover\n * @cssprop --command-palette-option-focus-bg - Item background when keyboard-focused\n *\n * @part dialog - Dialog root container\n * @part panel - Panel container\n * @part input - Search input\n * @part listbox - Listbox of matching commands\n *\n * @example\n * ```html\n * <ore-command-palette label=\"Command palette\" placeholder=\"Type a command…\">\n * <ore-command-palette-item value=\"new-file\" group=\"File\" shortcut=\"⌘+N\">New File</ore-command-palette-item>\n * <ore-command-palette-item value=\"open-file\" group=\"File\" shortcut=\"⌘+O\">Open File…</ore-command-palette-item>\n * <ore-command-palette-item value=\"toggle-theme\" group=\"View\">Toggle Theme</ore-command-palette-item>\n * </ore-command-palette>\n * ```\n */\nexport const COMMAND_PALETTE_TAG = 'ore-command-palette' as const;\ndefine<OreCommandPaletteProps>(COMMAND_PALETTE_TAG, {\n props: {\n 'default-open': prop.bool(false),\n 'empty-text': prop.string('No results found.'),\n items: prop.data<OreCommandPaletteProps['items']>(),\n 'keep-open-on-select': prop.bool(false),\n label: prop.string('Command palette'),\n loading: prop.bool(false),\n 'no-filter': prop.bool(false),\n open: { default: undefined as boolean | undefined, parse: parseOptionalBool },\n placeholder: prop.string('Type a command or search…'),\n shortcut: prop.string('mod+k'),\n },\n setup(props) {\n const el = getHost();\n const emit = useEmit<OreCommandPaletteEvents>();\n const abortSignal = lifecycleSignal(onCleanup);\n\n const dialogRef = ref<HTMLDialogElement>();\n const searchInputRef = ref<HTMLInputElement>();\n const listboxId = 'cmdk-listbox';\n\n let listboxEl: HTMLElement | null = null;\n\n const query = signal('');\n\n // ── Items: slotted <ore-command-palette-item> elements, merged with the `items` prop ──\n // Items are pure data nodes (see `ore-command-palette-item`'s own doc comment) — they're\n // never projected through a `<slot>`, just read directly off the host's light DOM. That\n // sidesteps `useSlots()`/`slotchange` entirely: a plain MutationObserver on the host\n // already covers everything `slotchange` would (an item added or removed) *and* the case\n // it can't — an already-assigned item's own text/attributes changing. For items written\n // as static, inline HTML, the browser's parser can insert the last item before appending\n // its text-node child; without watching `characterData`, that would permanently cache an\n // empty label, since nothing else re-triggers once the element itself stops changing.\n const slottedItems = signal<CommandPaletteItem[]>([]);\n\n const reparseSlottedItems = (): void => {\n slottedItems.value = parseSlottedItems([...el.children]);\n };\n\n reparseSlottedItems();\n\n const lightDomObserver = new MutationObserver(reparseSlottedItems);\n\n lightDomObserver.observe(el, { characterData: true, childList: true, subtree: true });\n onCleanup(() => lightDomObserver.disconnect());\n\n const allItems = computed<CommandPaletteItem[]>(() => {\n const explicit = props.items.value;\n\n return Array.isArray(explicit) ? explicit.map(normalizeItem) : slottedItems.value;\n });\n\n const filteredItems = computed(() => filterItems(allItems.value, query.value, Boolean(props['no-filter'].value)));\n const rows = computed(() => buildRows(filteredItems.value));\n\n // ── Keyboard navigation over the filtered list (Up/Down/Home/End) ──\n const scrollFocusedIntoView = (): void => {\n listboxEl?.querySelector<HTMLElement>('[data-focused]')?.scrollIntoView({ block: 'nearest' });\n };\n\n const list = createListControl<CommandPaletteItem>({\n getItems: () => filteredItems.value,\n isItemDisabled: (item) => item.disabled,\n onNavigate: scrollFocusedIntoView,\n signal: abortSignal,\n });\n const { focusedIndex } = list;\n\n // ── Dialog chrome: native <dialog>, focus trap, Escape, backdrop click ──\n const resetSearch = (): void => {\n query.value = '';\n list.reset();\n };\n\n const { closeWithAnimation, overlay, requestClose, setupNativeListeners } = useDialogControl({\n defaultOpen: props['default-open'],\n dialogRef,\n getPanelEl: () => dialogRef.value?.querySelector<HTMLElement>('.panel'),\n host: el,\n initialFocus: computed(() => '.search-input'),\n isPersistent: () => false,\n onCleanup,\n onEvent,\n onNativeClose: (reason) => {\n emit('open-change', { open: false, reason });\n resetSearch();\n },\n onOpen: (reason) => {\n emit('open-change', { open: true, reason });\n list.navigate('first');\n },\n openProp: props.open,\n performClose: () => closeWithAnimation(),\n returnFocus: computed(() => true),\n });\n\n // ── Global keyboard shortcut (opens/closes the palette from anywhere) ──\n const globalKeymap = createKeymap();\n\n globalKeymap.mount(window);\n onCleanup(() => globalKeymap.dispose());\n\n let unbindShortcut: (() => void) | null = null;\n\n watch(\n props.shortcut,\n (shortcutStr) => {\n unbindShortcut?.();\n unbindShortcut = null;\n\n const trimmed = (shortcutStr ?? '').trim();\n\n if (!trimmed) return;\n\n try {\n unbindShortcut = globalKeymap.bind(trimmed, () => {\n overlay.toggle('keyboard', 'trigger');\n });\n } catch (error) {\n warn(`invalid \"shortcut\" value \"${trimmed}\" — ${error instanceof Error ? error.message : String(error)}`);\n }\n },\n { immediate: true },\n );\n\n onMounted(() => {\n setupNativeListeners();\n });\n\n // ── Selection ────────────────────────────────────────────────────────────\n function selectItem(item: CommandPaletteItem): void {\n if (item.disabled) return;\n\n emit('select', { item, label: item.label, value: item.value });\n\n if (props['keep-open-on-select'].value) {\n searchInputRef.value?.focus();\n } else {\n requestClose('trigger');\n }\n }\n\n function handleInput(e: Event): void {\n const value = (e.target as HTMLInputElement).value;\n\n query.value = value;\n list.navigate('first');\n emit('search', { query: value });\n\n const count = filteredItems.value.length;\n\n announce(count === 0 ? props['empty-text'].value! : `${count} result${count === 1 ? '' : 's'} found`, {\n document: el.ownerDocument,\n });\n }\n\n function handleKeydown(e: KeyboardEvent): void {\n if (list.handleKeydown(e)) return;\n\n if (e.key === 'Enter') {\n // `list.navigate('first')` (called on open and on every keystroke) already keeps\n // the focused row off a disabled item, so this fallback is only ever reached when\n // nothing has been focused yet — but it mirrors `selectItem`'s own disabled guard\n // rather than blindly grabbing index 0, in case that navigate-on-open wiring ever\n // changes.\n const active = list.getActiveItem() ?? filteredItems.value.find((item) => !item.disabled);\n\n if (active) {\n e.preventDefault();\n selectItem(active);\n }\n }\n }\n\n return html`\n <dialog ref=\"${dialogRef}\" class=\"dialog\" part=\"dialog\" aria-label=\"${props.label}\" aria-modal=\"true\">\n <div class=\"panel\" part=\"panel\">\n <div class=\"search\" part=\"search\">\n <span class=\"search-icon\" aria-hidden=\"true\"><ore-icon name=\"search\" size=\"18\"></ore-icon></span>\n <input\n ref=\"${searchInputRef}\"\n class=\"search-input\"\n part=\"input\"\n type=\"text\"\n role=\"combobox\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n aria-expanded=\"true\"\n aria-autocomplete=\"list\"\n aria-controls=\"${listboxId}\"\n aria-activedescendant=\"${() => (focusedIndex.value >= 0 ? `${listboxId}-opt-${focusedIndex.value}` : '')}\"\n placeholder=\"${props.placeholder}\"\n value=\"${query}\"\n @input=\"${handleInput}\"\n @keydown=\"${handleKeydown}\" />\n <span class=\"search-loader\" ?hidden=\"${() => !props.loading.value}\" aria-hidden=\"true\"></span>\n </div>\n <div\n class=\"listbox\"\n part=\"listbox\"\n role=\"listbox\"\n id=\"${listboxId}\"\n aria-label=\"Commands\"\n ref=\"${(listEl: HTMLElement) => {\n listboxEl = listEl;\n }}\">\n <div class=\"empty\" role=\"presentation\" ?hidden=\"${() => props.loading.value || rows.value.length > 0}\">\n ${() => props['empty-text'].value}\n </div>\n ${() =>\n rows.value.map((row) =>\n row.type === 'group'\n ? html`\n <div class=\"group-heading\" role=\"presentation\">${row.group}</div>\n `\n : html`\n <div\n class=\"item\"\n part=\"item\"\n role=\"option\"\n id=\"${`${listboxId}-opt-${row.idx}`}\"\n aria-selected=\"${() => String(focusedIndex.value === row.idx)}\"\n aria-disabled=\"${() => String(row.item.disabled)}\"\n ?data-focused=\"${() => focusedIndex.value === row.idx}\"\n ?data-disabled=\"${() => row.item.disabled}\"\n @click=\"${(e: MouseEvent) => {\n e.stopPropagation();\n selectItem(row.item);\n }}\"\n @pointerenter=\"${() => {\n list.set(row.idx);\n }}\">\n ${\n row.item.icon\n ? html`\n <span class=\"item-icon\" aria-hidden=\"true\">\n <ore-icon name=\"${row.item.icon}\" size=\"16\"></ore-icon>\n </span>\n `\n : ''\n }\n <span class=\"item-label\">${row.item.label}</span>\n ${\n row.item.shortcut\n ? html`\n <span class=\"item-shortcut\">\n ${splitShortcutKeys(row.item.shortcut).map(\n (key) => html`\n <kbd>${key}</kbd>\n `,\n )}\n </span>\n `\n : ''\n }\n </div>\n `,\n )}\n </div>\n <div class=\"footer\" part=\"footer\">\n <span class=\"footer-hint\">\n <kbd>↑</kbd>\n <kbd>↓</kbd>\n Navigate\n </span>\n <span class=\"footer-hint\">\n <kbd>Enter</kbd>\n Select\n </span>\n <span class=\"footer-hint\">\n <kbd>Esc</kbd>\n Close\n </span>\n </div>\n </div>\n </dialog>\n `;\n },\n styles: [reducedMotionMixin, componentStyles],\n});\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- const e=require("../styles/mixins/accessibility.css.cjs");var t=new WeakMap,n=new WeakMap,r=(t,n)=>{let r=n.createElement(`div`);return r.setAttribute(`aria-live`,t),r.setAttribute(`aria-atomic`,`true`),r.setAttribute(`data-block-announcer`,t),r.style.cssText=e.SR_ONLY_INLINE_STYLE,n.body.appendChild(r),r},i=(e,n)=>{let i=t.get(n);return i?.[e]&&!n.body.contains(i[e])&&(i[e]=void 0),i?.[e]||(i||(i={},t.set(n,i)),i[e]=r(e,n)),i[e]??null},a=(e,t={})=>{if(typeof document>`u`)return;let r=i(t.politeness??`polite`,document);if(!r)return;r.textContent=``;let a=n.get(r);a!==void 0&&clearTimeout(a);let o=setTimeout(()=>{r.textContent=e,n.delete(r)},50);n.set(r,o)};exports.announce=a;
1
+ const e=require("../styles/mixins/accessibility.css.cjs");var t=new WeakMap,n=new WeakMap,r=(t,n)=>{if(!n.body)return null;let r=n.createElement(`div`);return r.setAttribute(`aria-live`,t),r.setAttribute(`aria-atomic`,`true`),r.setAttribute(`data-block-announcer`,t),r.style.cssText=e.SR_ONLY_INLINE_STYLE,n.body.appendChild(r),r},i=(e,n)=>{let i=t.get(n);if(i?.[e]&&!n.body?.contains(i[e])&&(i[e]=void 0),!i?.[e]){let a=r(e,n);if(!a)return null;i||(i={},t.set(n,i)),i[e]=a}return i[e]??null},a=(e,t={})=>{let r=t.document??(typeof document<`u`?document:null);if(!r)return;let a=i(t.politeness??`polite`,r);if(!a)return;a.textContent=``;let o=n.get(a);o!==void 0&&clearTimeout(o);let s=setTimeout(()=>{a.textContent=e,n.delete(a)},50);n.set(a,s)};exports.announce=a;
2
2
  //# sourceMappingURL=announcer.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"announcer.cjs","names":[],"sources":["../../src/core/announcer.ts"],"sourcesContent":["// ── Live-Region Announcer ─────────────────────────────────────────────────────\n// Programmatic announcements to screen readers without requiring a visible\n// DOM element. Used for status messages, selection feedback, and count updates\n// that live in components where the visible element is hidden or absent.\n//\n// Two visually-hidden live regions are lazily created in `<body>` — one polite,\n// one assertive. The clear-then-set pattern forces AT to re-read even identical\n// consecutive messages.\n//\n// Usage:\n// ```ts\n// import { announce } from './announcer';\n// announce(`${selectedCount} items selected`);\n// announce('Error: field is required', { politeness: 'assertive' });\n// ```\n\nimport { SR_ONLY_INLINE_STYLE } from '../styles';\n\n// ── Visually-hidden style ─────────────────────────────────────────────────────\n// Shared with the CSS-side `.sr-only` mixin — see `src/styles/mixins/accessibility.css.ts`.\n\n// ── Per-document live regions (WeakMap for automatic GC + test isolation) ────\n\ntype DocumentRegions = { assertive?: HTMLElement; polite?: HTMLElement };\n\nconst _regions = new WeakMap<Document, DocumentRegions>();\nconst _timers = new WeakMap<HTMLElement, ReturnType<typeof setTimeout>>();\n\n/** Creates and mounts a visually-hidden live region with the given politeness. */\nconst createRegion = (politeness: 'assertive' | 'polite', doc: Document): HTMLElement => {\n const el = doc.createElement('div');\n\n el.setAttribute('aria-live', politeness);\n el.setAttribute('aria-atomic', 'true');\n el.setAttribute('data-block-announcer', politeness);\n el.style.cssText = SR_ONLY_INLINE_STYLE;\n\n doc.body.appendChild(el);\n\n return el;\n};\n\n/**\n * Returns the live region for the requested politeness in the given document,\n * creating it on first access. Returns `null` outside a browser context.\n */\nconst getRegion = (politeness: 'assertive' | 'polite', doc: Document): HTMLElement | null => {\n let regions = _regions.get(doc);\n\n // Re-create if this specific region was removed from the DOM (e.g. by test teardown).\n if (regions?.[politeness] && !doc.body.contains(regions[politeness])) {\n regions[politeness] = undefined;\n }\n\n if (!regions?.[politeness]) {\n if (!regions) {\n regions = {};\n _regions.set(doc, regions);\n }\n\n regions[politeness] = createRegion(politeness, doc);\n }\n\n return regions[politeness] ?? null;\n};\n\n// ── Public API ────────────────────────────────────────────────────────────────\n\nexport type AnnouncePoliteness = 'assertive' | 'polite';\n\nexport type AnnounceOptions = {\n /**\n * `'polite'` — queues the announcement; does not interrupt ongoing speech.\n * `'assertive'` — interrupts immediately. Use sparingly; reserve for errors.\n *\n * Default: `'polite'`.\n */\n politeness?: AnnouncePoliteness;\n};\n\n/**\n * Announces a message to screen readers via a singleton visually-hidden\n * live region. Safe to call outside a component lifecycle (no cleanup needed).\n *\n * The clear-then-set pattern (50 ms delay) forces AT to re-read even when the\n * message text hasn't changed.\n *\n * @example\n * ```ts\n * announce('3 results found');\n * announce('Session expired', { politeness: 'assertive' });\n * ```\n */\nexport const announce = (message: string, options: AnnounceOptions = {}): void => {\n if (typeof document === 'undefined') return;\n\n const politeness = options.politeness ?? 'polite';\n const region = getRegion(politeness, document);\n\n if (!region) return;\n\n // Clear immediately so AT registers a content change, then set after a short\n // delay. The 50 ms window is the established minimum for most AT/browser combos.\n region.textContent = '';\n\n // Use a per-element timer so concurrent polite + assertive announcements don't\n // clobber each other's pending writes.\n const existing = _timers.get(region);\n\n if (existing !== undefined) clearTimeout(existing);\n\n const timer = setTimeout(() => {\n region.textContent = message;\n _timers.delete(region);\n }, 50);\n\n _timers.set(region, timer);\n};\n"],"mappings":"0DAyBA,IAAM,EAAW,IAAI,QACf,EAAU,IAAI,QAGd,GAAgB,EAAoC,IAA+B,CACvF,IAAM,EAAK,EAAI,cAAc,KAAK,EASlC,OAPA,EAAG,aAAa,YAAa,CAAU,EACvC,EAAG,aAAa,cAAe,MAAM,EACrC,EAAG,aAAa,uBAAwB,CAAU,EAClD,EAAG,MAAM,QAAU,EAAA,qBAEnB,EAAI,KAAK,YAAY,CAAE,EAEhB,CACT,EAMM,GAAa,EAAoC,IAAsC,CAC3F,IAAI,EAAU,EAAS,IAAI,CAAG,EAgB9B,OAbI,IAAU,IAAe,CAAC,EAAI,KAAK,SAAS,EAAQ,EAAW,IACjE,EAAQ,GAAc,IAAA,IAGnB,IAAU,KACR,IACH,EAAU,CAAC,EACX,EAAS,IAAI,EAAK,CAAO,GAG3B,EAAQ,GAAc,EAAa,EAAY,CAAG,GAG7C,EAAQ,IAAe,IAChC,EA6Ba,GAAY,EAAiB,EAA2B,CAAC,IAAY,CAChF,GAAI,OAAO,SAAa,IAAa,OAGrC,IAAM,EAAS,EADI,EAAQ,YAAc,SACJ,QAAQ,EAE7C,GAAI,CAAC,EAAQ,OAIb,EAAO,YAAc,GAIrB,IAAM,EAAW,EAAQ,IAAI,CAAM,EAE/B,IAAa,IAAA,IAAW,aAAa,CAAQ,EAEjD,IAAM,EAAQ,eAAiB,CAC7B,EAAO,YAAc,EACrB,EAAQ,OAAO,CAAM,CACvB,EAAG,EAAE,EAEL,EAAQ,IAAI,EAAQ,CAAK,CAC3B"}
1
+ {"version":3,"file":"announcer.cjs","names":[],"sources":["../../src/core/announcer.ts"],"sourcesContent":["// ── Live-Region Announcer ─────────────────────────────────────────────────────\n// Programmatic announcements to screen readers without requiring a visible\n// DOM element. Used for status messages, selection feedback, and count updates\n// that live in components where the visible element is hidden or absent.\n//\n// Two visually-hidden live regions are lazily created in the target document's\n// `<body>` — one polite, one assertive. The clear-then-set pattern forces AT to\n// re-read even identical consecutive messages.\n//\n// Behavior is latest-value replacement (debounce), not queueing: rapid\n// consecutive calls to the same politeness cancel pending writes so only the\n// last message survives. JavaScript cannot know when assistive technology\n// finishes speaking, so a queue would be dishonest.\n//\n// Usage:\n// ```ts\n// import { announce } from './announcer';\n// announce(`${selectedCount} items selected`);\n// announce('Error: field is required', { politeness: 'assertive' });\n// announce('Saved', { document: getHost().ownerDocument });\n// ```\n\nimport { SR_ONLY_INLINE_STYLE } from '../styles';\n\n// ── Per-document live regions (WeakMap for automatic GC + test isolation) ────\n\ntype DocumentRegions = { assertive?: HTMLElement; polite?: HTMLElement };\n\nconst _regions = new WeakMap<Document, DocumentRegions>();\nconst _timers = new WeakMap<HTMLElement, ReturnType<typeof setTimeout>>();\n\n/** Creates and mounts a visually-hidden live region with the given politeness. */\nconst createRegion = (politeness: 'assertive' | 'polite', doc: Document): HTMLElement | null => {\n if (!doc.body) return null;\n\n const el = doc.createElement('div');\n\n el.setAttribute('aria-live', politeness);\n el.setAttribute('aria-atomic', 'true');\n el.setAttribute('data-block-announcer', politeness);\n el.style.cssText = SR_ONLY_INLINE_STYLE;\n\n doc.body.appendChild(el);\n\n return el;\n};\n\n/**\n * Returns the live region for the requested politeness in the given document,\n * creating it on first access. Returns `null` outside a browser context or\n * when the document has no `<body>`.\n */\nconst getRegion = (politeness: 'assertive' | 'polite', doc: Document): HTMLElement | null => {\n let regions = _regions.get(doc);\n\n // Re-create if this specific region was removed from the DOM (e.g. by test teardown).\n if (regions?.[politeness] && !doc.body?.contains(regions[politeness])) {\n regions[politeness] = undefined;\n }\n\n if (!regions?.[politeness]) {\n const el = createRegion(politeness, doc);\n\n if (!el) return null;\n\n if (!regions) {\n regions = {};\n _regions.set(doc, regions);\n }\n\n regions[politeness] = el;\n }\n\n return regions[politeness] ?? null;\n};\n\n// ── Public API ────────────────────────────────────────────────────────────────\n\nexport type AnnouncePoliteness = 'assertive' | 'polite';\n\nexport type AnnounceOptions = {\n /**\n * The document whose `<body>` will host the visually-hidden live region.\n * Defaults to the global `document`. Pass `getHost().ownerDocument` from\n * inside a component that may live in an iframe to announce through the\n * correct accessibility tree.\n */\n document?: Document;\n /**\n * `'polite'` — latest-value replacement; does not interrupt ongoing speech.\n * `'assertive'` — interrupts immediately. Use sparingly; reserve for errors.\n *\n * Default: `'polite'`.\n */\n politeness?: AnnouncePoliteness;\n};\n\n/**\n * Announces a message to screen readers via a singleton visually-hidden\n * live region. Safe to call outside a component lifecycle (no cleanup needed).\n *\n * Uses latest-value replacement: rapid consecutive calls to the same politeness\n * cancel pending writes so only the last message is spoken. The clear-then-set\n * pattern (short delay) forces AT to re-read even when the message text hasn't\n * changed.\n *\n * @example\n * ```ts\n * announce('3 results found');\n * announce('Session expired', { politeness: 'assertive' });\n * announce('Saved', { document: getHost().ownerDocument });\n * ```\n */\nexport const announce = (message: string, options: AnnounceOptions = {}): void => {\n const doc = options.document ?? (typeof document !== 'undefined' ? document : null);\n\n if (!doc) return;\n\n const politeness = options.politeness ?? 'polite';\n const region = getRegion(politeness, doc);\n\n if (!region) return;\n\n // Clear immediately so AT registers a content change, then set after a short\n // delay. This is a debounce window, not a guaranteed speech timing — AT speech\n // timing varies across browsers and screen readers.\n region.textContent = '';\n\n // Use a per-element timer so concurrent polite + assertive announcements don't\n // clobber each other's pending writes.\n const existing = _timers.get(region);\n\n if (existing !== undefined) clearTimeout(existing);\n\n const timer = setTimeout(() => {\n region.textContent = message;\n _timers.delete(region);\n }, 50);\n\n _timers.set(region, timer);\n};\n"],"mappings":"0DA4BA,IAAM,EAAW,IAAI,QACf,EAAU,IAAI,QAGd,GAAgB,EAAoC,IAAsC,CAC9F,GAAI,CAAC,EAAI,KAAM,OAAO,KAEtB,IAAM,EAAK,EAAI,cAAc,KAAK,EASlC,OAPA,EAAG,aAAa,YAAa,CAAU,EACvC,EAAG,aAAa,cAAe,MAAM,EACrC,EAAG,aAAa,uBAAwB,CAAU,EAClD,EAAG,MAAM,QAAU,EAAA,qBAEnB,EAAI,KAAK,YAAY,CAAE,EAEhB,CACT,EAOM,GAAa,EAAoC,IAAsC,CAC3F,IAAI,EAAU,EAAS,IAAI,CAAG,EAO9B,GAJI,IAAU,IAAe,CAAC,EAAI,MAAM,SAAS,EAAQ,EAAW,IAClE,EAAQ,GAAc,IAAA,IAGpB,CAAC,IAAU,GAAa,CAC1B,IAAM,EAAK,EAAa,EAAY,CAAG,EAEvC,GAAI,CAAC,EAAI,OAAO,KAEX,IACH,EAAU,CAAC,EACX,EAAS,IAAI,EAAK,CAAO,GAG3B,EAAQ,GAAc,CACxB,CAEA,OAAO,EAAQ,IAAe,IAChC,EAuCa,GAAY,EAAiB,EAA2B,CAAC,IAAY,CAChF,IAAM,EAAM,EAAQ,WAAa,OAAO,SAAa,IAAc,SAAW,MAE9E,GAAI,CAAC,EAAK,OAGV,IAAM,EAAS,EADI,EAAQ,YAAc,SACJ,CAAG,EAExC,GAAI,CAAC,EAAQ,OAKb,EAAO,YAAc,GAIrB,IAAM,EAAW,EAAQ,IAAI,CAAM,EAE/B,IAAa,IAAA,IAAW,aAAa,CAAQ,EAEjD,IAAM,EAAQ,eAAiB,CAC7B,EAAO,YAAc,EACrB,EAAQ,OAAO,CAAM,CACvB,EAAG,EAAE,EAEL,EAAQ,IAAI,EAAQ,CAAK,CAC3B"}
@@ -1,7 +1,14 @@
1
1
  export type AnnouncePoliteness = 'assertive' | 'polite';
2
2
  export type AnnounceOptions = {
3
3
  /**
4
- * `'polite'` — queues the announcement; does not interrupt ongoing speech.
4
+ * The document whose `<body>` will host the visually-hidden live region.
5
+ * Defaults to the global `document`. Pass `getHost().ownerDocument` from
6
+ * inside a component that may live in an iframe to announce through the
7
+ * correct accessibility tree.
8
+ */
9
+ document?: Document;
10
+ /**
11
+ * `'polite'` — latest-value replacement; does not interrupt ongoing speech.
5
12
  * `'assertive'` — interrupts immediately. Use sparingly; reserve for errors.
6
13
  *
7
14
  * Default: `'polite'`.
@@ -12,13 +19,16 @@ export type AnnounceOptions = {
12
19
  * Announces a message to screen readers via a singleton visually-hidden
13
20
  * live region. Safe to call outside a component lifecycle (no cleanup needed).
14
21
  *
15
- * The clear-then-set pattern (50 ms delay) forces AT to re-read even when the
16
- * message text hasn't changed.
22
+ * Uses latest-value replacement: rapid consecutive calls to the same politeness
23
+ * cancel pending writes so only the last message is spoken. The clear-then-set
24
+ * pattern (short delay) forces AT to re-read even when the message text hasn't
25
+ * changed.
17
26
  *
18
27
  * @example
19
28
  * ```ts
20
29
  * announce('3 results found');
21
30
  * announce('Session expired', { politeness: 'assertive' });
31
+ * announce('Saved', { document: getHost().ownerDocument });
22
32
  * ```
23
33
  */
24
34
  export declare const announce: (message: string, options?: AnnounceOptions) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"announcer.d.ts","sourceRoot":"","sources":["../../src/core/announcer.ts"],"names":[],"mappings":"AAoEA,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,QAAQ,CAAC;AAExD,MAAM,MAAM,eAAe,GAAG;IAC5B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,kBAAkB,CAAC;CACjC,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,QAAQ,GAAI,SAAS,MAAM,EAAE,UAAS,eAAoB,KAAG,IAwBzE,CAAC"}
1
+ {"version":3,"file":"announcer.d.ts","sourceRoot":"","sources":["../../src/core/announcer.ts"],"names":[],"mappings":"AA8EA,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,QAAQ,CAAC;AAExD,MAAM,MAAM,eAAe,GAAG;IAC5B;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,kBAAkB,CAAC;CACjC,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,QAAQ,GAAI,SAAS,MAAM,EAAE,UAAS,eAAoB,KAAG,IA2BzE,CAAC"}
@@ -1,22 +1,29 @@
1
1
  import { SR_ONLY_INLINE_STYLE as e } from "../styles/mixins/accessibility.css.js";
2
2
  //#region src/core/announcer.ts
3
3
  var t = /* @__PURE__ */ new WeakMap(), n = /* @__PURE__ */ new WeakMap(), r = (t, n) => {
4
+ if (!n.body) return null;
4
5
  let r = n.createElement("div");
5
6
  return r.setAttribute("aria-live", t), r.setAttribute("aria-atomic", "true"), r.setAttribute("data-block-announcer", t), r.style.cssText = e, n.body.appendChild(r), r;
6
7
  }, i = (e, n) => {
7
8
  let i = t.get(n);
8
- return i?.[e] && !n.body.contains(i[e]) && (i[e] = void 0), i?.[e] || (i || (i = {}, t.set(n, i)), i[e] = r(e, n)), i[e] ?? null;
9
+ if (i?.[e] && !n.body?.contains(i[e]) && (i[e] = void 0), !i?.[e]) {
10
+ let a = r(e, n);
11
+ if (!a) return null;
12
+ i || (i = {}, t.set(n, i)), i[e] = a;
13
+ }
14
+ return i[e] ?? null;
9
15
  }, a = (e, t = {}) => {
10
- if (typeof document > "u") return;
11
- let r = i(t.politeness ?? "polite", document);
16
+ let r = t.document ?? (typeof document < "u" ? document : null);
12
17
  if (!r) return;
13
- r.textContent = "";
14
- let a = n.get(r);
15
- a !== void 0 && clearTimeout(a);
16
- let o = setTimeout(() => {
17
- r.textContent = e, n.delete(r);
18
+ let a = i(t.politeness ?? "polite", r);
19
+ if (!a) return;
20
+ a.textContent = "";
21
+ let o = n.get(a);
22
+ o !== void 0 && clearTimeout(o);
23
+ let s = setTimeout(() => {
24
+ a.textContent = e, n.delete(a);
18
25
  }, 50);
19
- n.set(r, o);
26
+ n.set(a, s);
20
27
  };
21
28
  //#endregion
22
29
  export { a as announce };
@@ -1 +1 @@
1
- {"version":3,"file":"announcer.js","names":[],"sources":["../../src/core/announcer.ts"],"sourcesContent":["// ── Live-Region Announcer ─────────────────────────────────────────────────────\n// Programmatic announcements to screen readers without requiring a visible\n// DOM element. Used for status messages, selection feedback, and count updates\n// that live in components where the visible element is hidden or absent.\n//\n// Two visually-hidden live regions are lazily created in `<body>` — one polite,\n// one assertive. The clear-then-set pattern forces AT to re-read even identical\n// consecutive messages.\n//\n// Usage:\n// ```ts\n// import { announce } from './announcer';\n// announce(`${selectedCount} items selected`);\n// announce('Error: field is required', { politeness: 'assertive' });\n// ```\n\nimport { SR_ONLY_INLINE_STYLE } from '../styles';\n\n// ── Visually-hidden style ─────────────────────────────────────────────────────\n// Shared with the CSS-side `.sr-only` mixin — see `src/styles/mixins/accessibility.css.ts`.\n\n// ── Per-document live regions (WeakMap for automatic GC + test isolation) ────\n\ntype DocumentRegions = { assertive?: HTMLElement; polite?: HTMLElement };\n\nconst _regions = new WeakMap<Document, DocumentRegions>();\nconst _timers = new WeakMap<HTMLElement, ReturnType<typeof setTimeout>>();\n\n/** Creates and mounts a visually-hidden live region with the given politeness. */\nconst createRegion = (politeness: 'assertive' | 'polite', doc: Document): HTMLElement => {\n const el = doc.createElement('div');\n\n el.setAttribute('aria-live', politeness);\n el.setAttribute('aria-atomic', 'true');\n el.setAttribute('data-block-announcer', politeness);\n el.style.cssText = SR_ONLY_INLINE_STYLE;\n\n doc.body.appendChild(el);\n\n return el;\n};\n\n/**\n * Returns the live region for the requested politeness in the given document,\n * creating it on first access. Returns `null` outside a browser context.\n */\nconst getRegion = (politeness: 'assertive' | 'polite', doc: Document): HTMLElement | null => {\n let regions = _regions.get(doc);\n\n // Re-create if this specific region was removed from the DOM (e.g. by test teardown).\n if (regions?.[politeness] && !doc.body.contains(regions[politeness])) {\n regions[politeness] = undefined;\n }\n\n if (!regions?.[politeness]) {\n if (!regions) {\n regions = {};\n _regions.set(doc, regions);\n }\n\n regions[politeness] = createRegion(politeness, doc);\n }\n\n return regions[politeness] ?? null;\n};\n\n// ── Public API ────────────────────────────────────────────────────────────────\n\nexport type AnnouncePoliteness = 'assertive' | 'polite';\n\nexport type AnnounceOptions = {\n /**\n * `'polite'` — queues the announcement; does not interrupt ongoing speech.\n * `'assertive'` — interrupts immediately. Use sparingly; reserve for errors.\n *\n * Default: `'polite'`.\n */\n politeness?: AnnouncePoliteness;\n};\n\n/**\n * Announces a message to screen readers via a singleton visually-hidden\n * live region. Safe to call outside a component lifecycle (no cleanup needed).\n *\n * The clear-then-set pattern (50 ms delay) forces AT to re-read even when the\n * message text hasn't changed.\n *\n * @example\n * ```ts\n * announce('3 results found');\n * announce('Session expired', { politeness: 'assertive' });\n * ```\n */\nexport const announce = (message: string, options: AnnounceOptions = {}): void => {\n if (typeof document === 'undefined') return;\n\n const politeness = options.politeness ?? 'polite';\n const region = getRegion(politeness, document);\n\n if (!region) return;\n\n // Clear immediately so AT registers a content change, then set after a short\n // delay. The 50 ms window is the established minimum for most AT/browser combos.\n region.textContent = '';\n\n // Use a per-element timer so concurrent polite + assertive announcements don't\n // clobber each other's pending writes.\n const existing = _timers.get(region);\n\n if (existing !== undefined) clearTimeout(existing);\n\n const timer = setTimeout(() => {\n region.textContent = message;\n _timers.delete(region);\n }, 50);\n\n _timers.set(region, timer);\n};\n"],"mappings":";;AAyBA,IAAM,oBAAW,IAAI,QAAmC,GAClD,oBAAU,IAAI,QAAoD,GAGlE,KAAgB,GAAoC,MAA+B;CACvF,IAAM,IAAK,EAAI,cAAc,KAAK;CASlC,OAPA,EAAG,aAAa,aAAa,CAAU,GACvC,EAAG,aAAa,eAAe,MAAM,GACrC,EAAG,aAAa,wBAAwB,CAAU,GAClD,EAAG,MAAM,UAAU,GAEnB,EAAI,KAAK,YAAY,CAAE,GAEhB;AACT,GAMM,KAAa,GAAoC,MAAsC;CAC3F,IAAI,IAAU,EAAS,IAAI,CAAG;CAgB9B,OAbI,IAAU,MAAe,CAAC,EAAI,KAAK,SAAS,EAAQ,EAAW,MACjE,EAAQ,KAAc,KAAA,IAGnB,IAAU,OACR,MACH,IAAU,CAAC,GACX,EAAS,IAAI,GAAK,CAAO,IAG3B,EAAQ,KAAc,EAAa,GAAY,CAAG,IAG7C,EAAQ,MAAe;AAChC,GA6Ba,KAAY,GAAiB,IAA2B,CAAC,MAAY;CAChF,IAAI,OAAO,WAAa,KAAa;CAGrC,IAAM,IAAS,EADI,EAAQ,cAAc,UACJ,QAAQ;CAE7C,IAAI,CAAC,GAAQ;CAIb,EAAO,cAAc;CAIrB,IAAM,IAAW,EAAQ,IAAI,CAAM;CAEnC,AAAI,MAAa,KAAA,KAAW,aAAa,CAAQ;CAEjD,IAAM,IAAQ,iBAAiB;EAE7B,AADA,EAAO,cAAc,GACrB,EAAQ,OAAO,CAAM;CACvB,GAAG,EAAE;CAEL,EAAQ,IAAI,GAAQ,CAAK;AAC3B"}
1
+ {"version":3,"file":"announcer.js","names":[],"sources":["../../src/core/announcer.ts"],"sourcesContent":["// ── Live-Region Announcer ─────────────────────────────────────────────────────\n// Programmatic announcements to screen readers without requiring a visible\n// DOM element. Used for status messages, selection feedback, and count updates\n// that live in components where the visible element is hidden or absent.\n//\n// Two visually-hidden live regions are lazily created in the target document's\n// `<body>` — one polite, one assertive. The clear-then-set pattern forces AT to\n// re-read even identical consecutive messages.\n//\n// Behavior is latest-value replacement (debounce), not queueing: rapid\n// consecutive calls to the same politeness cancel pending writes so only the\n// last message survives. JavaScript cannot know when assistive technology\n// finishes speaking, so a queue would be dishonest.\n//\n// Usage:\n// ```ts\n// import { announce } from './announcer';\n// announce(`${selectedCount} items selected`);\n// announce('Error: field is required', { politeness: 'assertive' });\n// announce('Saved', { document: getHost().ownerDocument });\n// ```\n\nimport { SR_ONLY_INLINE_STYLE } from '../styles';\n\n// ── Per-document live regions (WeakMap for automatic GC + test isolation) ────\n\ntype DocumentRegions = { assertive?: HTMLElement; polite?: HTMLElement };\n\nconst _regions = new WeakMap<Document, DocumentRegions>();\nconst _timers = new WeakMap<HTMLElement, ReturnType<typeof setTimeout>>();\n\n/** Creates and mounts a visually-hidden live region with the given politeness. */\nconst createRegion = (politeness: 'assertive' | 'polite', doc: Document): HTMLElement | null => {\n if (!doc.body) return null;\n\n const el = doc.createElement('div');\n\n el.setAttribute('aria-live', politeness);\n el.setAttribute('aria-atomic', 'true');\n el.setAttribute('data-block-announcer', politeness);\n el.style.cssText = SR_ONLY_INLINE_STYLE;\n\n doc.body.appendChild(el);\n\n return el;\n};\n\n/**\n * Returns the live region for the requested politeness in the given document,\n * creating it on first access. Returns `null` outside a browser context or\n * when the document has no `<body>`.\n */\nconst getRegion = (politeness: 'assertive' | 'polite', doc: Document): HTMLElement | null => {\n let regions = _regions.get(doc);\n\n // Re-create if this specific region was removed from the DOM (e.g. by test teardown).\n if (regions?.[politeness] && !doc.body?.contains(regions[politeness])) {\n regions[politeness] = undefined;\n }\n\n if (!regions?.[politeness]) {\n const el = createRegion(politeness, doc);\n\n if (!el) return null;\n\n if (!regions) {\n regions = {};\n _regions.set(doc, regions);\n }\n\n regions[politeness] = el;\n }\n\n return regions[politeness] ?? null;\n};\n\n// ── Public API ────────────────────────────────────────────────────────────────\n\nexport type AnnouncePoliteness = 'assertive' | 'polite';\n\nexport type AnnounceOptions = {\n /**\n * The document whose `<body>` will host the visually-hidden live region.\n * Defaults to the global `document`. Pass `getHost().ownerDocument` from\n * inside a component that may live in an iframe to announce through the\n * correct accessibility tree.\n */\n document?: Document;\n /**\n * `'polite'` — latest-value replacement; does not interrupt ongoing speech.\n * `'assertive'` — interrupts immediately. Use sparingly; reserve for errors.\n *\n * Default: `'polite'`.\n */\n politeness?: AnnouncePoliteness;\n};\n\n/**\n * Announces a message to screen readers via a singleton visually-hidden\n * live region. Safe to call outside a component lifecycle (no cleanup needed).\n *\n * Uses latest-value replacement: rapid consecutive calls to the same politeness\n * cancel pending writes so only the last message is spoken. The clear-then-set\n * pattern (short delay) forces AT to re-read even when the message text hasn't\n * changed.\n *\n * @example\n * ```ts\n * announce('3 results found');\n * announce('Session expired', { politeness: 'assertive' });\n * announce('Saved', { document: getHost().ownerDocument });\n * ```\n */\nexport const announce = (message: string, options: AnnounceOptions = {}): void => {\n const doc = options.document ?? (typeof document !== 'undefined' ? document : null);\n\n if (!doc) return;\n\n const politeness = options.politeness ?? 'polite';\n const region = getRegion(politeness, doc);\n\n if (!region) return;\n\n // Clear immediately so AT registers a content change, then set after a short\n // delay. This is a debounce window, not a guaranteed speech timing — AT speech\n // timing varies across browsers and screen readers.\n region.textContent = '';\n\n // Use a per-element timer so concurrent polite + assertive announcements don't\n // clobber each other's pending writes.\n const existing = _timers.get(region);\n\n if (existing !== undefined) clearTimeout(existing);\n\n const timer = setTimeout(() => {\n region.textContent = message;\n _timers.delete(region);\n }, 50);\n\n _timers.set(region, timer);\n};\n"],"mappings":";;AA4BA,IAAM,oBAAW,IAAI,QAAmC,GAClD,oBAAU,IAAI,QAAoD,GAGlE,KAAgB,GAAoC,MAAsC;CAC9F,IAAI,CAAC,EAAI,MAAM,OAAO;CAEtB,IAAM,IAAK,EAAI,cAAc,KAAK;CASlC,OAPA,EAAG,aAAa,aAAa,CAAU,GACvC,EAAG,aAAa,eAAe,MAAM,GACrC,EAAG,aAAa,wBAAwB,CAAU,GAClD,EAAG,MAAM,UAAU,GAEnB,EAAI,KAAK,YAAY,CAAE,GAEhB;AACT,GAOM,KAAa,GAAoC,MAAsC;CAC3F,IAAI,IAAU,EAAS,IAAI,CAAG;CAO9B,IAJI,IAAU,MAAe,CAAC,EAAI,MAAM,SAAS,EAAQ,EAAW,MAClE,EAAQ,KAAc,KAAA,IAGpB,CAAC,IAAU,IAAa;EAC1B,IAAM,IAAK,EAAa,GAAY,CAAG;EAEvC,IAAI,CAAC,GAAI,OAAO;EAOhB,AALK,MACH,IAAU,CAAC,GACX,EAAS,IAAI,GAAK,CAAO,IAG3B,EAAQ,KAAc;CACxB;CAEA,OAAO,EAAQ,MAAe;AAChC,GAuCa,KAAY,GAAiB,IAA2B,CAAC,MAAY;CAChF,IAAM,IAAM,EAAQ,aAAa,OAAO,WAAa,MAAc,WAAW;CAE9E,IAAI,CAAC,GAAK;CAGV,IAAM,IAAS,EADI,EAAQ,cAAc,UACJ,CAAG;CAExC,IAAI,CAAC,GAAQ;CAKb,EAAO,cAAc;CAIrB,IAAM,IAAW,EAAQ,IAAI,CAAM;CAEnC,AAAI,MAAa,KAAA,KAAW,aAAa,CAAQ;CAEjD,IAAM,IAAQ,iBAAiB;EAE7B,AADA,EAAO,cAAc,GACrB,EAAQ,OAAO,CAAM;CACvB,GAAG,EAAE;CAEL,EAAQ,IAAI,GAAQ,CAAK;AAC3B"}