@vielzeug/refine 2.2.5 → 2.4.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.
- package/dist/accordion-item.cjs +1 -1
- package/dist/accordion-item.cjs.map +1 -1
- package/dist/accordion-item.js +1 -1
- package/dist/accordion-item.js.map +1 -1
- package/dist/card.cjs +1 -1
- package/dist/card.cjs.map +1 -1
- package/dist/card.js +1 -1
- package/dist/card.js.map +1 -1
- package/dist/carousel.cjs +1 -1
- package/dist/carousel.cjs.map +1 -1
- package/dist/carousel.js +1 -1
- package/dist/carousel.js.map +1 -1
- package/dist/chat-message.cjs +9 -9
- package/dist/chat-message.cjs.map +1 -1
- package/dist/chat-message.js +41 -38
- package/dist/chat-message.js.map +1 -1
- package/dist/combobox.cjs +9 -7
- package/dist/combobox.cjs.map +1 -1
- package/dist/combobox.js +55 -48
- package/dist/combobox.js.map +1 -1
- package/dist/command-palette.cjs +1 -1
- package/dist/command-palette.cjs.map +1 -1
- package/dist/command-palette.js +1 -1
- package/dist/command-palette.js.map +1 -1
- package/dist/content/index.d.ts +2 -0
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/list/list-item.css.cjs +1 -1
- package/dist/content/list/list-item.css.cjs.map +1 -1
- package/dist/content/list/list-item.css.js +1 -1
- package/dist/content/list/list-item.css.js.map +1 -1
- package/dist/content/list/list-item.d.ts +9 -0
- package/dist/content/list/list-item.d.ts.map +1 -1
- package/dist/content/stats/stats.css.cjs +2 -0
- package/dist/content/stats/stats.css.cjs.map +1 -0
- package/dist/content/stats/stats.css.js +6 -0
- package/dist/content/stats/stats.css.js.map +1 -0
- package/dist/content/stats/stats.d.ts +72 -0
- package/dist/content/stats/stats.d.ts.map +1 -0
- package/dist/core/announcer.cjs +1 -1
- package/dist/core/announcer.cjs.map +1 -1
- package/dist/core/announcer.d.ts +13 -3
- package/dist/core/announcer.d.ts.map +1 -1
- package/dist/core/announcer.js +16 -9
- package/dist/core/announcer.js.map +1 -1
- package/dist/core/auto-resize.cjs +1 -1
- package/dist/core/auto-resize.cjs.map +1 -1
- package/dist/core/auto-resize.js +1 -1
- package/dist/core/auto-resize.js.map +1 -1
- package/dist/custom-elements.json +4457 -4161
- package/dist/datagrid.cjs +199 -179
- package/dist/datagrid.cjs.map +1 -1
- package/dist/datagrid.js +435 -321
- package/dist/datagrid.js.map +1 -1
- package/dist/disclosure/tab-item/tab-item.css.cjs +1 -1
- package/dist/disclosure/tab-item/tab-item.css.cjs.map +1 -1
- package/dist/disclosure/tab-item/tab-item.css.js +1 -1
- package/dist/disclosure/tab-item/tab-item.css.js.map +1 -1
- package/dist/disclosure/tab-item/tab-item.d.ts +3 -0
- package/dist/disclosure/tab-item/tab-item.d.ts.map +1 -1
- package/dist/disclosure/tabs/tabs.css.cjs +1 -1
- package/dist/disclosure/tabs/tabs.css.cjs.map +1 -1
- package/dist/disclosure/tabs/tabs.css.js +1 -1
- package/dist/disclosure/tabs/tabs.css.js.map +1 -1
- package/dist/disclosure/tabs/tabs.d.ts +4 -0
- package/dist/disclosure/tabs/tabs.d.ts.map +1 -1
- package/dist/feedback/typing-indicator/typing-indicator.d.ts +2 -2
- package/dist/file-input.cjs +49 -49
- package/dist/file-input.cjs.map +1 -1
- package/dist/file-input.js +128 -127
- package/dist/file-input.js.map +1 -1
- package/dist/frameworks/elements.d.ts +2 -1
- package/dist/icon.cjs +1 -1
- package/dist/icon.cjs.map +1 -1
- package/dist/icon.js +1 -1
- package/dist/icon.js.map +1 -1
- package/dist/input.cjs +5 -4
- package/dist/input.cjs.map +1 -1
- package/dist/input.js +14 -8
- package/dist/input.js.map +1 -1
- package/dist/inputs/combobox/combobox-options.cjs +1 -1
- package/dist/inputs/combobox/combobox-options.cjs.map +1 -1
- package/dist/inputs/combobox/combobox-options.d.ts.map +1 -1
- package/dist/inputs/combobox/combobox-options.js +1 -0
- package/dist/inputs/combobox/combobox-options.js.map +1 -1
- package/dist/inputs/combobox/combobox.css.cjs +1 -1
- package/dist/inputs/combobox/combobox.css.cjs.map +1 -1
- package/dist/inputs/combobox/combobox.css.js +1 -1
- package/dist/inputs/combobox/combobox.css.js.map +1 -1
- package/dist/inputs/combobox/combobox.d.ts +1 -0
- package/dist/inputs/combobox/combobox.d.ts.map +1 -1
- package/dist/inputs/combobox/combobox.types.d.ts +7 -0
- package/dist/inputs/combobox/combobox.types.d.ts.map +1 -1
- package/dist/inputs/datagrid/datagrid-model.cjs +1 -1
- package/dist/inputs/datagrid/datagrid-model.cjs.map +1 -1
- package/dist/inputs/datagrid/datagrid-model.d.ts +17 -3
- package/dist/inputs/datagrid/datagrid-model.d.ts.map +1 -1
- package/dist/inputs/datagrid/datagrid-model.js +93 -70
- package/dist/inputs/datagrid/datagrid-model.js.map +1 -1
- package/dist/inputs/datagrid/datagrid.css.cjs +1 -1
- package/dist/inputs/datagrid/datagrid.css.cjs.map +1 -1
- package/dist/inputs/datagrid/datagrid.css.js +1 -1
- package/dist/inputs/datagrid/datagrid.css.js.map +1 -1
- package/dist/inputs/datagrid/datagrid.d.ts +54 -4
- package/dist/inputs/datagrid/datagrid.d.ts.map +1 -1
- package/dist/inputs/file-input/file-input-upload.cjs +1 -1
- package/dist/inputs/file-input/file-input-upload.cjs.map +1 -1
- package/dist/inputs/file-input/file-input-upload.d.ts +2 -0
- package/dist/inputs/file-input/file-input-upload.d.ts.map +1 -1
- package/dist/inputs/file-input/file-input-upload.js +5 -2
- package/dist/inputs/file-input/file-input-upload.js.map +1 -1
- package/dist/inputs/file-input/file-input.d.ts.map +1 -1
- package/dist/inputs/index.d.ts +1 -1
- package/dist/inputs/index.d.ts.map +1 -1
- package/dist/inputs/input/input.css.cjs +1 -1
- package/dist/inputs/input/input.css.cjs.map +1 -1
- package/dist/inputs/input/input.css.js +1 -1
- package/dist/inputs/input/input.css.js.map +1 -1
- package/dist/inputs/input/input.d.ts +3 -0
- package/dist/inputs/input/input.d.ts.map +1 -1
- package/dist/inputs/select/select.css.cjs +1 -1
- package/dist/inputs/select/select.css.cjs.map +1 -1
- package/dist/inputs/select/select.css.js +1 -1
- package/dist/inputs/select/select.css.js.map +1 -1
- package/dist/inputs/select/select.d.ts +5 -0
- package/dist/inputs/select/select.d.ts.map +1 -1
- package/dist/layout/navbar/navbar.d.ts +2 -0
- package/dist/layout/navbar/navbar.d.ts.map +1 -1
- package/dist/layout/sidebar/sidebar.css.cjs +1 -1
- package/dist/layout/sidebar/sidebar.css.cjs.map +1 -1
- package/dist/layout/sidebar/sidebar.css.js +1 -1
- package/dist/layout/sidebar/sidebar.css.js.map +1 -1
- package/dist/layout/sidebar/sidebar.d.ts +8 -0
- package/dist/layout/sidebar/sidebar.d.ts.map +1 -1
- package/dist/list-item.cjs +6 -5
- package/dist/list-item.cjs.map +1 -1
- package/dist/list-item.js +23 -17
- package/dist/list-item.js.map +1 -1
- package/dist/list.cjs +1 -1
- package/dist/list.cjs.map +1 -1
- package/dist/list.js +1 -1
- package/dist/list.js.map +1 -1
- package/dist/menu.cjs +1 -1
- package/dist/menu.cjs.map +1 -1
- package/dist/menu.js +1 -1
- package/dist/menu.js.map +1 -1
- package/dist/navbar.cjs +2 -2
- package/dist/navbar.cjs.map +1 -1
- package/dist/navbar.js +7 -5
- package/dist/navbar.js.map +1 -1
- package/dist/navigation-menu.cjs +1 -1
- package/dist/navigation-menu.cjs.map +1 -1
- package/dist/navigation-menu.js +1 -1
- package/dist/navigation-menu.js.map +1 -1
- package/dist/overlay/popover/popover.css.cjs +1 -1
- package/dist/overlay/popover/popover.css.cjs.map +1 -1
- package/dist/overlay/popover/popover.css.js +1 -1
- package/dist/overlay/popover/popover.css.js.map +1 -1
- package/dist/overlay/popover/popover.d.ts +1 -0
- package/dist/overlay/popover/popover.d.ts.map +1 -1
- package/dist/overlay/shared/use-dialog.cjs +1 -1
- package/dist/overlay/shared/use-dialog.cjs.map +1 -1
- package/dist/overlay/shared/use-dialog.js +1 -1
- package/dist/overlay/shared/use-dialog.js.map +1 -1
- package/dist/overlay/shared/use-floating-trigger.cjs +1 -1
- package/dist/overlay/shared/use-floating-trigger.cjs.map +1 -1
- package/dist/overlay/shared/use-floating-trigger.js +1 -1
- package/dist/overlay/shared/use-floating-trigger.js.map +1 -1
- package/dist/popover.cjs.map +1 -1
- package/dist/popover.js.map +1 -1
- package/dist/radio.cjs +1 -1
- package/dist/radio.cjs.map +1 -1
- package/dist/radio.js +10 -8
- package/dist/radio.js.map +1 -1
- package/dist/rating.cjs +1 -1
- package/dist/rating.cjs.map +1 -1
- package/dist/rating.js +1 -1
- package/dist/rating.js.map +1 -1
- package/dist/refine.cjs +428 -379
- package/dist/refine.cjs.map +1 -1
- package/dist/refine.iife.js +428 -379
- package/dist/refine.iife.js.map +1 -1
- package/dist/refine.js +458 -409
- package/dist/refine.js.map +1 -1
- package/dist/select.cjs +16 -14
- package/dist/select.cjs.map +1 -1
- package/dist/select.js +118 -112
- package/dist/select.js.map +1 -1
- package/dist/sidebar.cjs +28 -23
- package/dist/sidebar.cjs.map +1 -1
- package/dist/sidebar.js +112 -77
- package/dist/sidebar.js.map +1 -1
- package/dist/slider.cjs +1 -1
- package/dist/slider.cjs.map +1 -1
- package/dist/slider.js +1 -1
- package/dist/slider.js.map +1 -1
- package/dist/stats.cjs +18 -0
- package/dist/stats.cjs.map +1 -0
- package/dist/stats.js +88 -0
- package/dist/stats.js.map +1 -0
- package/dist/stepper.cjs +1 -1
- package/dist/stepper.cjs.map +1 -1
- package/dist/stepper.js +1 -1
- package/dist/stepper.js.map +1 -1
- package/dist/styles/mixins/animation.css.cjs +2 -0
- package/dist/styles/mixins/animation.css.cjs.map +1 -1
- package/dist/styles/mixins/animation.css.d.ts.map +1 -1
- package/dist/styles/mixins/animation.css.js +2 -0
- package/dist/styles/mixins/animation.css.js.map +1 -1
- package/dist/tab-item.cjs +1 -1
- package/dist/tab-item.cjs.map +1 -1
- package/dist/tab-item.js +3 -2
- package/dist/tab-item.js.map +1 -1
- package/dist/tabs.cjs +1 -1
- package/dist/tabs.cjs.map +1 -1
- package/dist/tabs.js +3 -1
- package/dist/tabs.js.map +1 -1
- package/dist/toast.cjs.map +1 -1
- package/dist/toast.js.map +1 -1
- package/dist/typing-indicator.cjs +1 -1
- package/dist/typing-indicator.cjs.map +1 -1
- package/dist/typing-indicator.js +8 -7
- package/dist/typing-indicator.js.map +1 -1
- package/package.json +66 -58
package/dist/file-input.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-input.js","names":[],"sources":["../src/inputs/file-input/file-input.ts"],"sourcesContent":["import { createDropZone } from '@vielzeug/dnd';\nimport {\n bind,\n createStableId,\n define,\n html,\n onCleanup,\n onElement,\n onEvent,\n onMounted,\n prop,\n ref,\n useEmit,\n useField,\n when,\n} from '@vielzeug/ore';\nimport { computed, signal, watch } from '@vielzeug/ripple';\n\nimport '../../content/icon/icon';\nimport '../../feedback/progress/progress';\nimport { bindRefCallback, createInteraction } from '../../core';\nimport { FILE_INPUT_SIZE_PRESET } from '../../shared';\nimport {\n coarsePointerMixin,\n colorThemeMixin,\n disabledLoadingMixin,\n forcedColorsFocusMixin,\n reducedMotionMixin,\n roundedVariantMixin,\n sizeVariantMixin,\n} from '../../styles';\nimport componentStyles from './file-input.css?inline';\nimport { createFileQueue, type FileUploadFn, formatBytes } from './file-input-upload';\n\nexport type { FileUploadFn, FileUploadState, FileUploadStatus } from './file-input-upload';\n\nconst isImageFile = (file: File): boolean => file.type.startsWith('image/');\n\n/** File input component properties */\nexport type OreFileInputProps = {\n /** Accepted file types (comma-separated, e.g. '.jpg, .png, image/*') */\n accept?: string;\n /** Theme color tint */\n color?: string;\n /** Disabled state */\n disabled?: boolean;\n /** Error message text */\n error?: string;\n /**\n * Render selected files as a grid of image thumbnails/previews instead of the default\n * single-column list. Non-image files fall back to a generic file icon in the same grid.\n */\n gallery?: boolean;\n /** Helper text displayed below the input */\n helper?: string;\n /** Input label text */\n label?: string;\n /** Max number of files allowed (only used if multiple is true) */\n 'max-files'?: number;\n /** Max size of a single file in bytes */\n 'max-size'?: number;\n /** Allow multiple files selection */\n multiple?: boolean;\n /** Form field name */\n name?: string;\n /**\n * JS-only callback fired with the inner `<input type=\"file\">` element when it\n * mounts, and with `null` when it unmounts. Intended for composed components\n * that need imperative access to the raw element.\n * Set as a JS property: `bitFileInput.ref = (el) => { ... }`.\n */\n ref?: ((el: HTMLInputElement | null) => void) | null;\n /** Required field */\n required?: boolean;\n /** Field size preset */\n size?: string;\n /**\n * JS-only upload transport (see `FileUploadFn`). When set, every newly added file starts\n * uploading immediately and independently — progress/speed/ETA, retry-on-failure, and a\n * success confirmation card are all driven from here. Omit it to keep the component in its\n * original picker-only mode (select files, no upload lifecycle).\n * Set as a JS property: `fileInput.upload = async (file, { onProgress, signal }) => { ... }`.\n */\n upload?: FileUploadFn | null;\n};\n\n/** Events emitted by the file-input component */\nexport type OreFileInputEvents = {\n /** Emitted when files are added or removed */\n change: { files: File[]; originalEvent?: Event; value: File[] };\n /** Emitted when a specific file is removed */\n remove: { file: File; files: File[]; originalEvent?: Event; value: File[] };\n /** Emitted when `upload` rejects for a file (after a fresh attempt or a retry) */\n 'upload-error': { error: unknown; file: File };\n /** Emitted whenever `upload`'s `onProgress` reports new bytes for a file */\n 'upload-progress': { file: File; loaded: number; total: number };\n /** Emitted when `upload` resolves for a file */\n 'upload-success': { file: File };\n};\n\n/**\n * A file upload field with drag-and-drop support, built-in validation messaging, and — once a\n * `upload` transport is wired up — a full per-file upload lifecycle: live progress/speed/ETA,\n * retry-on-failure, a success confirmation card, and fully independent handling of concurrent\n * files (one failing never blocks or cancels the others). Picking files with no `upload` set\n * keeps the original, upload-free selection-only behavior.\n *\n * @element ore-file-input\n *\n * @attr {string} accept - Comma-separated file extensions or MIME types\n * @attr {boolean} multiple - Enable multiple files selection\n * @attr {boolean} gallery - Show selected files as an image thumbnail/preview grid\n * @attr {number} max-files - Max number of files allowed\n * @attr {number} max-size - Max size of each file in bytes\n * @attr {boolean} disabled - Disable interaction\n * @attr {string} error - Show an error state/message\n * @attr {string} helper - Provide helper context below the dropzone\n *\n * @fires change - detail: { files: File[], value: File[] }\n * @fires remove - detail: { file: File, files: File[] }\n * @fires upload-progress - detail: { file: File, loaded: number, total: number }\n * @fires upload-success - detail: { file: File }\n * @fires upload-error - detail: { file: File, error: unknown }\n *\n * @cssprop --file-input-bg - Dropzone background color\n * @cssprop --file-input-border-color - Dropzone border color\n * @cssprop --file-input-font-size - Font size\n * @cssprop --file-input-radius - Dropzone border radius\n * @cssprop --file-input-min-height - Minimum dropzone height\n * @cssprop --file-input-hover-bg - Dropzone background on hover (flat/ghost variants)\n * @cssprop --file-input-hover-border-color - Dropzone border on hover (flat/bordered variants)\n * @cssprop --file-input-focus-bg - Dropzone background when focused/drag-over (flat variant)\n * @cssprop --file-input-focus-border-color - Dropzone border when focused/drag-over (flat variant)\n * @cssprop --file-input-thumb-size - Gallery thumbnail width/height\n * @part wrapper - Root wrapper around the file input field\n * @part label - Visible label rendered above the dropzone\n * @part dropzone - Interactive drag-and-drop target\n * @part input - Native file input element\n * @part gallery - Gallery grid container (rendered instead of `file-list` when `gallery` is set)\n * @part helper - Helper text shown beneath the dropzone\n * @part error - Error message shown beneath the field\n * @example\n * ```html\n * <ore-file-input label=\"Upload files\" accept=\"image/*\" multiple />\n * <ore-file-input label=\"Photos\" accept=\"image/*\" multiple gallery />\n * <ore-file-input label=\"Resume\" accept=\".pdf,.doc,.docx\" max-size=\"5242880\" />\n * <ore-file-input variant=\"bordered\" color=\"primary\" />\n * ```\n * ```ts\n * // Wire up a real upload transport — progress/retry/success are then handled automatically.\n * // `XMLHttpRequest` is used here (not `fetch`) because it's the only browser API that reports\n * // upload progress; swap in whatever transport the app already uses.\n * const input = document.querySelector('ore-file-input');\n * input.upload = (file, { onProgress, signal, resumeFrom }) =>\n * new Promise((resolve, reject) => {\n * const xhr = new XMLHttpRequest();\n *\n * xhr.upload.addEventListener('progress', (e) => onProgress(resumeFrom + e.loaded, file.size));\n * xhr.addEventListener('load', () => (xhr.status < 400 ? resolve() : reject(new Error(xhr.statusText))));\n * xhr.addEventListener('error', () => reject(new Error('Network error')));\n * signal.addEventListener('abort', () => xhr.abort());\n *\n * xhr.open('PUT', '/uploads');\n * if (resumeFrom > 0) xhr.setRequestHeader('Content-Range', `bytes ${resumeFrom}-/${file.size}`);\n * xhr.send(file);\n * });\n * ```\n */\nexport const FILE_INPUT_TAG = 'ore-file-input' as const;\ndefine<OreFileInputProps>(FILE_INPUT_TAG, {\n formAssociated: true,\n props: {\n accept: prop.string(),\n color: prop.string(),\n disabled: prop.bool(false),\n error: prop.string(),\n gallery: prop.bool(false),\n helper: prop.string(),\n label: prop.string(),\n 'max-files': prop.number(0),\n 'max-size': prop.number(0),\n multiple: prop.bool(false),\n name: prop.string(),\n ref: prop.data<((el: HTMLInputElement | null) => void) | null>(),\n required: prop.bool(false),\n size: prop.string(),\n upload: prop.data<FileUploadFn>(),\n },\n setup(props) {\n const emit = useEmit<OreFileInputEvents>();\n\n // ============================================\n // State\n // ============================================\n\n const isDragging = signal(false);\n // Set while a \"Replace\" action is waiting for the (shared) native file input's next\n // `change` — routes that selection into `queue.replaceFile()` instead of `queue.addFiles()`.\n const replaceTarget = signal<File | null>(null);\n\n const isDisabled = computed(() => Boolean(props.disabled.value));\n const maxFilesLimit = computed(() => props['max-files'].value ?? 0);\n const maxSizeLimit = computed(() => props['max-size'].value ?? 0);\n\n // File selection + the opt-in upload lifecycle (progress, retry, replace, per-file\n // isolation) live in `createFileQueue` — see file-input-upload.ts for why they're one unit.\n const queue = createFileQueue({\n accept: props.accept,\n disabled: isDisabled,\n maxFiles: maxFilesLimit,\n maxSize: maxSizeLimit,\n multiple: computed(() => Boolean(props.multiple.value)),\n onChange: (changedFiles, originalEvent) =>\n emit('change', { files: changedFiles, originalEvent, value: changedFiles }),\n onRemove: (file, remainingFiles, originalEvent) =>\n emit('remove', { file, files: remainingFiles, originalEvent, value: remainingFiles }),\n onUploadError: (file, error) => emit('upload-error', { error, file }),\n onUploadProgress: (file, loaded, total) => emit('upload-progress', { file, loaded, total }),\n onUploadSuccess: (file) => emit('upload-success', { file }),\n upload: computed(() => props.upload.value ?? undefined),\n });\n\n onCleanup(() => queue.dispose());\n\n // ============================================\n // Form Integration\n // ============================================\n\n useField({\n disabled: isDisabled,\n toFormValue: (fi: File[]) => {\n if (fi.length === 0) return null;\n\n const name = props.name.value || 'file';\n const fd = new FormData();\n\n for (const file of fi) fd.append(name, file);\n\n return fd;\n },\n value: queue.files,\n });\n\n // Sync host attributes for CSS selectors\n const isInvalid = computed(() => Boolean(props.error.value));\n\n bind({\n attr: {\n 'drag-over': () => (isDragging.value ? true : undefined),\n invalid: () => (isInvalid.value ? true : undefined),\n size: props.size,\n },\n });\n\n // ============================================\n // IDs\n // ============================================\n const fileInputId = createStableId('file-input');\n const labelId = `label-${fileInputId}`;\n const helperId = `helper-${fileInputId}`;\n const errorId = `error-${fileInputId}`;\n\n // ============================================\n // Refs\n // ============================================\n const dropzoneRef = ref<HTMLDivElement>();\n const inputRef = ref<HTMLInputElement>();\n const hintText = computed(() => {\n const parts: string[] = [];\n\n if (props.accept.value) {\n parts.push(\n props.accept.value\n .split(',')\n .map((s: string) => s.trim())\n .join(', '),\n );\n }\n\n const maxSize = maxSizeLimit.value;\n\n if (maxSize > 0) parts.push(`max ${formatBytes(maxSize)}`);\n\n const maxFiles = maxFilesLimit.value;\n\n if (maxFiles > 0) parts.push(`up to ${maxFiles} file${maxFiles !== 1 ? 's' : ''}`);\n\n return parts.join(' · ');\n });\n\n // ============================================\n // Replace (\"swap this one file\" — see file-input-upload.ts's `replaceFile`)\n // ============================================\n function beginReplace(file: File): void {\n replaceTarget.value = file;\n inputRef.value?.click();\n }\n\n // ============================================\n // Gallery Preview URLs\n // ============================================\n // Object URLs are created lazily (only while `gallery` is enabled) and revoked as soon\n // as their file is no longer selected, plus unconditionally on disconnect — otherwise\n // each preview leaks its backing blob for the life of the page.\n const previewUrls = new Map<File, string>();\n\n function getPreviewUrl(file: File): string {\n let url = previewUrls.get(file);\n\n if (!url) {\n url = URL.createObjectURL(file);\n previewUrls.set(file, url);\n }\n\n return url;\n }\n\n function revokeStalePreviewUrls(activeFiles: File[]): void {\n const keep = new Set(activeFiles);\n\n for (const [file, url] of previewUrls) {\n if (!keep.has(file)) {\n URL.revokeObjectURL(url);\n previewUrls.delete(file);\n }\n }\n }\n\n watch(queue.files, revokeStalePreviewUrls);\n\n onCleanup(() => {\n for (const url of previewUrls.values()) URL.revokeObjectURL(url);\n\n previewUrls.clear();\n });\n\n // ============================================\n // Mount\n // ============================================\n // ============================================\n // Template\n // ============================================\n onElement(inputRef, (inp) => bindRefCallback(props.ref, inp));\n\n onMounted(() => {\n const inp = inputRef.value!;\n const dz = dropzoneRef.value!;\n let skipNextClick = false;\n const pressControl = createInteraction({\n disabled: isDisabled,\n onPress: () => {\n inp.click();\n },\n });\n\n // Native input → add files, or — if a \"Replace\" action opened the picker — swap the one\n // file it targeted instead of appending a new entry.\n onEvent(inp, 'change', (e: Event) => {\n const input = e.target as HTMLInputElement;\n const target = replaceTarget.value;\n\n replaceTarget.value = null;\n\n if (target && input.files?.[0]) queue.replaceFile(target, input.files[0], e);\n else if (input.files?.length) queue.addFiles(Array.from(input.files), e);\n\n input.value = ''; // reset so the same file triggers change again\n });\n // Click dropzone → open file picker\n onEvent(dz, 'click', (e: MouseEvent) => {\n if (e.target === inp) return;\n\n if (skipNextClick) {\n skipNextClick = false;\n\n return;\n }\n\n if (!isDisabled.value) inp.click();\n });\n // Keyboard: Enter / Space → open picker\n onEvent(dz, 'keydown', (e: KeyboardEvent) => {\n skipNextClick = pressControl.handleKeydown(e) && e.key === 'Enter';\n });\n\n // `createDropZone` has no way to update `disabled` after creation — recreate the zone\n // whenever the prop changes instead of capturing a stale snapshot from this one `onMounted`\n // run. `watch`'s own returned cleanup (not a second `onCleanup`) disposes the current zone\n // both on the next toggle and on final teardown.\n watch(\n isDisabled,\n (disabled) => {\n const dropZone = createDropZone({\n disabled,\n element: dz,\n onDrop: (droppedFiles) => queue.addFiles(droppedFiles),\n onHoverChange: (hovered) => {\n isDragging.value = hovered;\n },\n });\n\n return () => dropZone.dispose();\n },\n { immediate: true },\n );\n });\n\n return html`\n <div class=\"file-input-wrapper\" part=\"wrapper\">\n <label class=\"label-outside\" id=\"${labelId}\" part=\"label\" ?hidden=${() => !props.label.value}>\n ${props.label}\n </label>\n <div\n class=\"dropzone\"\n part=\"dropzone\"\n ref=${dropzoneRef}\n role=\"button\"\n tabindex=\"${() => (isDisabled.value ? '-1' : '0')}\"\n aria-disabled=\"${() => String(isDisabled.value)}\"\n aria-label=\"${() => (!props.label.value ? 'File upload drop zone' : null)}\"\n aria-labelledby=\"${() => (props.label.value ? labelId : null)}\"\n aria-describedby=\"${helperId}\">\n <input\n type=\"file\"\n ref=${inputRef}\n part=\"input\"\n id=\"${fileInputId}\"\n accept=\"${props.accept}\"\n ?multiple=\"${props.multiple}\"\n ?required=\"${props.required}\"\n ?disabled=\"${isDisabled}\"\n name=\"${props.name}\"\n hidden\n inert\n tabindex=\"-1\" />\n <div class=\"dropzone-content\">\n <span class=\"dropzone-icon\" aria-hidden=\"true\" data-status=\"${() => (isInvalid.value ? 'error' : null)}\">\n ${() =>\n isInvalid.value\n ? html`\n <ore-icon name=\"alert-circle\" size=\"36\" stroke-width=\"1.5\" aria-hidden=\"true\"></ore-icon>\n `\n : html`\n <ore-icon name=\"upload\" size=\"36\" stroke-width=\"1.5\" aria-hidden=\"true\"></ore-icon>\n `}\n </span>\n <!-- Signal 01: the copy itself shifts on drag-entry (not just border/glow) — a\n static \"Drop files here\" during an active drag reads as if the drop target\n hasn't noticed the file yet. -->\n ${when(\n () => isDragging.value,\n () => html`\n <span class=\"dropzone-title dropzone-title-active\">Release to upload</span>\n `,\n )}\n ${when(\n () => !isDragging.value,\n () => html`\n <span class=\"dropzone-title\">\n Drop files here or\n <u>click to browse</u>\n </span>\n `,\n )}\n <span class=\"dropzone-hint\" ?hidden=${() => !hintText.value}>${hintText}</span>\n </div>\n </div>\n <ul\n class=\"${() => (props.gallery.value ? 'file-grid' : 'file-list')}\"\n part=\"${() => (props.gallery.value ? 'gallery' : null)}\"\n role=\"list\"\n aria-label=\"Selected files\"\n ?hidden=${() => queue.files.value.length === 0}>\n ${() =>\n queue.files.value.map((file: File) =>\n props.gallery.value\n ? html`\n <li class=\"file-card\" data-status=${() => queue.fileState(file).status}>\n <span class=\"file-thumb-frame\">\n ${\n // Decorative: the file name is already announced via the visible\n // `.file-card-name` caption below — repeating it as `alt` text would\n // duplicate it (and often trips redundant-alt checks, since real\n // filenames commonly contain words like \"photo\" or \"image\").\n isImageFile(file)\n ? // Object URLs use the `blob:` scheme, which ore's attribute-level\n // XSS guard blocks unconditionally on `src` (and other\n // URL-accepting attributes) — set it as a DOM property via `ref`\n // instead, bypassing that string-based check. Safe here: the value\n // comes from `URL.createObjectURL(file)` on a real `File` object we\n // control, never from untrusted text.\n html`\n <img\n class=\"file-thumb\"\n alt=\"\"\n ref=\"${(el: HTMLImageElement | null) => {\n if (el) el.src = getPreviewUrl(file);\n }}\" />\n `\n : html`\n <span class=\"file-thumb file-thumb-generic\" aria-hidden=\"true\">\n <ore-icon name=\"file\" size=\"28\" stroke-width=\"1.5\" aria-hidden=\"true\"></ore-icon>\n </span>\n `\n }\n ${when(\n () => queue.fileState(file).status === 'success',\n () => html`\n <span class=\"file-status-badge file-status-badge-success\" aria-hidden=\"true\">\n <ore-icon name=\"check\" size=\"11\" stroke-width=\"3\" aria-hidden=\"true\"></ore-icon>\n </span>\n `,\n )}\n ${when(\n () => queue.fileState(file).status === 'error',\n () => html`\n <span class=\"file-status-badge file-status-badge-error\" aria-hidden=\"true\">\n <ore-icon name=\"alert-circle\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n </span>\n `,\n )}\n <span class=\"file-card-actions\">\n ${when(\n () => queue.fileState(file).status === 'error',\n () => html`\n <button\n class=\"file-card-action\"\n type=\"button\"\n aria-label=\"${`Retry uploading ${file.name}`}\"\n @click=${() => queue.retryUpload(file)}>\n <ore-icon name=\"refresh-cw\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n </button>\n `,\n )}\n ${when(\n () =>\n queue.fileState(file).status === 'success' &&\n Boolean(props.multiple.value) &&\n Boolean(props.upload.value),\n () => html`\n <button\n class=\"file-card-action\"\n type=\"button\"\n aria-label=\"${`Replace ${file.name}`}\"\n @click=${() => beginReplace(file)}>\n <ore-icon name=\"upload\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n </button>\n `,\n )}\n <button\n class=\"file-card-action file-card-remove\"\n type=\"button\"\n aria-label=\"${`Remove ${file.name}`}\"\n @click=${(e: Event) => queue.removeFile(file, e)}>\n <ore-icon name=\"x\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n </button>\n </span>\n </span>\n <span class=\"file-card-name\" title=\"${file.name}\">${file.name}</span>\n ${when(\n () => queue.fileState(file).status === 'uploading',\n () => html`\n <ore-progress\n type=\"linear\"\n size=\"sm\"\n value=${() => queue.uploadPercent(file)}\n label=${() => `${queue.uploadPercent(file)}%`}></ore-progress>\n <span class=\"file-progress-meta\">${() => queue.uploadMetaText(file)}</span>\n `,\n )}\n ${when(\n () => queue.fileState(file).status === 'error',\n () => html`\n <span class=\"file-error-text\">${() => queue.fileState(file).error}</span>\n `,\n )}\n </li>\n `\n : html`\n <li class=\"file-item\" data-status=${() => queue.fileState(file).status}>\n <span class=\"file-icon\" aria-hidden=\"true\">\n <ore-icon\n name=${() => queue.statusIconName(file)}\n size=\"18\"\n stroke-width=\"1.75\"\n aria-hidden=\"true\"></ore-icon>\n </span>\n <span class=\"file-meta\">\n <span class=\"file-name\" title=\"${file.name}\">${file.name}</span>\n ${when(\n () => queue.fileState(file).status === 'uploading',\n () => html`\n <ore-progress\n type=\"linear\"\n size=\"sm\"\n value=${() => queue.uploadPercent(file)}\n label=${() => `${queue.uploadPercent(file)}%`}></ore-progress>\n <span class=\"file-progress-meta\">${() => queue.uploadMetaText(file)}</span>\n `,\n )}\n ${when(\n () => queue.fileState(file).status === 'error',\n () => html`\n <span class=\"file-error-text\">${() => queue.fileState(file).error}</span>\n `,\n )}\n ${when(\n () => queue.fileState(file).status === 'idle' || queue.fileState(file).status === 'success',\n () => html`\n <span class=\"file-size\">${formatBytes(file.size)}</span>\n `,\n )}\n </span>\n <span class=\"file-actions\">\n ${when(\n () => queue.fileState(file).status === 'error',\n () => html`\n <button\n class=\"file-action\"\n type=\"button\"\n aria-label=\"${`Retry uploading ${file.name}`}\"\n @click=${() => queue.retryUpload(file)}>\n <ore-icon name=\"refresh-cw\" size=\"14\" stroke-width=\"2\" aria-hidden=\"true\"></ore-icon>\n </button>\n `,\n )}\n ${when(\n () =>\n queue.fileState(file).status === 'success' &&\n Boolean(props.multiple.value) &&\n Boolean(props.upload.value),\n () => html`\n <button\n class=\"file-action\"\n type=\"button\"\n aria-label=\"${`Replace ${file.name}`}\"\n @click=${() => beginReplace(file)}>\n <ore-icon name=\"upload\" size=\"14\" stroke-width=\"2\" aria-hidden=\"true\"></ore-icon>\n </button>\n `,\n )}\n <button\n class=\"file-remove\"\n type=\"button\"\n aria-label=\"${`Remove ${file.name}`}\"\n @click=${(e: Event) => queue.removeFile(file, e)}>\n <ore-icon name=\"x\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n </button>\n </span>\n </li>\n `,\n )}\n </ul>\n <div class=\"helper-text\" id=\"${helperId}\" part=\"helper\" ?hidden=${() => isInvalid.value || !props.helper.value}>\n ${props.helper}\n </div>\n <div\n class=\"helper-text helper-text-error\"\n id=\"${errorId}\"\n role=\"alert\"\n part=\"error\"\n ?hidden=${() => !isInvalid.value}>\n ${() => props.error.value ?? ''}\n </div>\n </div>\n `;\n },\n shadow: { delegatesFocus: true },\n styles: [\n colorThemeMixin,\n coarsePointerMixin,\n reducedMotionMixin,\n roundedVariantMixin,\n disabledLoadingMixin,\n sizeVariantMixin(FILE_INPUT_SIZE_PRESET),\n forcedColorsFocusMixin('.dropzone'),\n componentStyles,\n ],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"file-input.js","names":[],"sources":["../src/inputs/file-input/file-input.ts"],"sourcesContent":["import { createDropZone } from '@vielzeug/dnd';\nimport {\n bind,\n createStableId,\n define,\n getHost,\n html,\n onCleanup,\n onElement,\n onEvent,\n onMounted,\n prop,\n ref,\n useEmit,\n useField,\n when,\n} from '@vielzeug/ore';\nimport { computed, signal, watch } from '@vielzeug/ripple';\n\nimport '../../content/icon/icon';\nimport '../../feedback/progress/progress';\nimport { bindRefCallback, createInteraction } from '../../core';\nimport { FILE_INPUT_SIZE_PRESET } from '../../shared';\nimport {\n coarsePointerMixin,\n colorThemeMixin,\n disabledLoadingMixin,\n forcedColorsFocusMixin,\n reducedMotionMixin,\n roundedVariantMixin,\n sizeVariantMixin,\n} from '../../styles';\nimport componentStyles from './file-input.css?inline';\nimport { createFileQueue, type FileUploadFn, formatBytes } from './file-input-upload';\n\nexport type { FileUploadFn, FileUploadState, FileUploadStatus } from './file-input-upload';\n\nconst isImageFile = (file: File): boolean => file.type.startsWith('image/');\n\n/** File input component properties */\nexport type OreFileInputProps = {\n /** Accepted file types (comma-separated, e.g. '.jpg, .png, image/*') */\n accept?: string;\n /** Theme color tint */\n color?: string;\n /** Disabled state */\n disabled?: boolean;\n /** Error message text */\n error?: string;\n /**\n * Render selected files as a grid of image thumbnails/previews instead of the default\n * single-column list. Non-image files fall back to a generic file icon in the same grid.\n */\n gallery?: boolean;\n /** Helper text displayed below the input */\n helper?: string;\n /** Input label text */\n label?: string;\n /** Max number of files allowed (only used if multiple is true) */\n 'max-files'?: number;\n /** Max size of a single file in bytes */\n 'max-size'?: number;\n /** Allow multiple files selection */\n multiple?: boolean;\n /** Form field name */\n name?: string;\n /**\n * JS-only callback fired with the inner `<input type=\"file\">` element when it\n * mounts, and with `null` when it unmounts. Intended for composed components\n * that need imperative access to the raw element.\n * Set as a JS property: `bitFileInput.ref = (el) => { ... }`.\n */\n ref?: ((el: HTMLInputElement | null) => void) | null;\n /** Required field */\n required?: boolean;\n /** Field size preset */\n size?: string;\n /**\n * JS-only upload transport (see `FileUploadFn`). When set, every newly added file starts\n * uploading immediately and independently — progress/speed/ETA, retry-on-failure, and a\n * success confirmation card are all driven from here. Omit it to keep the component in its\n * original picker-only mode (select files, no upload lifecycle).\n * Set as a JS property: `fileInput.upload = async (file, { onProgress, signal }) => { ... }`.\n */\n upload?: FileUploadFn | null;\n};\n\n/** Events emitted by the file-input component */\nexport type OreFileInputEvents = {\n /** Emitted when files are added or removed */\n change: { files: File[]; originalEvent?: Event; value: File[] };\n /** Emitted when a specific file is removed */\n remove: { file: File; files: File[]; originalEvent?: Event; value: File[] };\n /** Emitted when `upload` rejects for a file (after a fresh attempt or a retry) */\n 'upload-error': { error: unknown; file: File };\n /** Emitted whenever `upload`'s `onProgress` reports new bytes for a file */\n 'upload-progress': { file: File; loaded: number; total: number };\n /** Emitted when `upload` resolves for a file */\n 'upload-success': { file: File };\n};\n\n/**\n * A file upload field with drag-and-drop support, built-in validation messaging, and — once a\n * `upload` transport is wired up — a full per-file upload lifecycle: live progress/speed/ETA,\n * retry-on-failure, a success confirmation card, and fully independent handling of concurrent\n * files (one failing never blocks or cancels the others). Picking files with no `upload` set\n * keeps the original, upload-free selection-only behavior.\n *\n * @element ore-file-input\n *\n * @attr {string} accept - Comma-separated file extensions or MIME types\n * @attr {boolean} multiple - Enable multiple files selection\n * @attr {boolean} gallery - Show selected files as an image thumbnail/preview grid\n * @attr {number} max-files - Max number of files allowed\n * @attr {number} max-size - Max size of each file in bytes\n * @attr {boolean} disabled - Disable interaction\n * @attr {string} error - Show an error state/message\n * @attr {string} helper - Provide helper context below the dropzone\n *\n * @fires change - detail: { files: File[], value: File[] }\n * @fires remove - detail: { file: File, files: File[] }\n * @fires upload-progress - detail: { file: File, loaded: number, total: number }\n * @fires upload-success - detail: { file: File }\n * @fires upload-error - detail: { file: File, error: unknown }\n *\n * @cssprop --file-input-bg - Dropzone background color\n * @cssprop --file-input-border-color - Dropzone border color\n * @cssprop --file-input-font-size - Font size\n * @cssprop --file-input-radius - Dropzone border radius\n * @cssprop --file-input-min-height - Minimum dropzone height\n * @cssprop --file-input-hover-bg - Dropzone background on hover (flat/ghost variants)\n * @cssprop --file-input-hover-border-color - Dropzone border on hover (flat/bordered variants)\n * @cssprop --file-input-focus-bg - Dropzone background when focused/drag-over (flat variant)\n * @cssprop --file-input-focus-border-color - Dropzone border when focused/drag-over (flat variant)\n * @cssprop --file-input-thumb-size - Gallery thumbnail width/height\n * @part wrapper - Root wrapper around the file input field\n * @part label - Visible label rendered above the dropzone\n * @part dropzone - Interactive drag-and-drop target\n * @part input - Native file input element\n * @part gallery - Gallery grid container (rendered instead of `file-list` when `gallery` is set)\n * @part helper - Helper text shown beneath the dropzone\n * @part error - Error message shown beneath the field\n * @example\n * ```html\n * <ore-file-input label=\"Upload files\" accept=\"image/*\" multiple />\n * <ore-file-input label=\"Photos\" accept=\"image/*\" multiple gallery />\n * <ore-file-input label=\"Resume\" accept=\".pdf,.doc,.docx\" max-size=\"5242880\" />\n * <ore-file-input variant=\"bordered\" color=\"primary\" />\n * ```\n * ```ts\n * // Wire up a real upload transport — progress/retry/success are then handled automatically.\n * // `XMLHttpRequest` is used here (not `fetch`) because it's the only browser API that reports\n * // upload progress; swap in whatever transport the app already uses.\n * const input = document.querySelector('ore-file-input');\n * input.upload = (file, { onProgress, signal, resumeFrom }) =>\n * new Promise((resolve, reject) => {\n * const xhr = new XMLHttpRequest();\n *\n * xhr.upload.addEventListener('progress', (e) => onProgress(resumeFrom + e.loaded, file.size));\n * xhr.addEventListener('load', () => (xhr.status < 400 ? resolve() : reject(new Error(xhr.statusText))));\n * xhr.addEventListener('error', () => reject(new Error('Network error')));\n * signal.addEventListener('abort', () => xhr.abort());\n *\n * xhr.open('PUT', '/uploads');\n * if (resumeFrom > 0) xhr.setRequestHeader('Content-Range', `bytes ${resumeFrom}-/${file.size}`);\n * xhr.send(file);\n * });\n * ```\n */\nexport const FILE_INPUT_TAG = 'ore-file-input' as const;\ndefine<OreFileInputProps>(FILE_INPUT_TAG, {\n formAssociated: true,\n props: {\n accept: prop.string(),\n color: prop.string(),\n disabled: prop.bool(false),\n error: prop.string(),\n gallery: prop.bool(false),\n helper: prop.string(),\n label: prop.string(),\n 'max-files': prop.number(0),\n 'max-size': prop.number(0),\n multiple: prop.bool(false),\n name: prop.string(),\n ref: prop.data<((el: HTMLInputElement | null) => void) | null>(),\n required: prop.bool(false),\n size: prop.string(),\n upload: prop.data<FileUploadFn>(),\n },\n setup(props) {\n const el = getHost();\n const emit = useEmit<OreFileInputEvents>();\n\n // ============================================\n // State\n // ============================================\n\n const isDragging = signal(false);\n // Set while a \"Replace\" action is waiting for the (shared) native file input's next\n // `change` — routes that selection into `queue.replaceFile()` instead of `queue.addFiles()`.\n const replaceTarget = signal<File | null>(null);\n\n const isDisabled = computed(() => Boolean(props.disabled.value));\n const maxFilesLimit = computed(() => props['max-files'].value ?? 0);\n const maxSizeLimit = computed(() => props['max-size'].value ?? 0);\n\n // File selection + the opt-in upload lifecycle (progress, retry, replace, per-file\n // isolation) live in `createFileQueue` — see file-input-upload.ts for why they're one unit.\n const queue = createFileQueue({\n accept: props.accept,\n disabled: isDisabled,\n document: el.ownerDocument,\n maxFiles: maxFilesLimit,\n maxSize: maxSizeLimit,\n multiple: computed(() => Boolean(props.multiple.value)),\n onChange: (changedFiles, originalEvent) =>\n emit('change', { files: changedFiles, originalEvent, value: changedFiles }),\n onRemove: (file, remainingFiles, originalEvent) =>\n emit('remove', { file, files: remainingFiles, originalEvent, value: remainingFiles }),\n onUploadError: (file, error) => emit('upload-error', { error, file }),\n onUploadProgress: (file, loaded, total) => emit('upload-progress', { file, loaded, total }),\n onUploadSuccess: (file) => emit('upload-success', { file }),\n upload: computed(() => props.upload.value ?? undefined),\n });\n\n onCleanup(() => queue.dispose());\n\n // ============================================\n // Form Integration\n // ============================================\n\n useField({\n disabled: isDisabled,\n toFormValue: (fi: File[]) => {\n if (fi.length === 0) return null;\n\n const name = props.name.value || 'file';\n const fd = new FormData();\n\n for (const file of fi) fd.append(name, file);\n\n return fd;\n },\n value: queue.files,\n });\n\n // Sync host attributes for CSS selectors\n const isInvalid = computed(() => Boolean(props.error.value));\n\n bind({\n attr: {\n 'drag-over': () => (isDragging.value ? true : undefined),\n invalid: () => (isInvalid.value ? true : undefined),\n size: props.size,\n },\n });\n\n // ============================================\n // IDs\n // ============================================\n const fileInputId = createStableId('file-input');\n const labelId = `label-${fileInputId}`;\n const helperId = `helper-${fileInputId}`;\n const errorId = `error-${fileInputId}`;\n\n // ============================================\n // Refs\n // ============================================\n const dropzoneRef = ref<HTMLDivElement>();\n const inputRef = ref<HTMLInputElement>();\n const hintText = computed(() => {\n const parts: string[] = [];\n\n if (props.accept.value) {\n parts.push(\n props.accept.value\n .split(',')\n .map((s: string) => s.trim())\n .join(', '),\n );\n }\n\n const maxSize = maxSizeLimit.value;\n\n if (maxSize > 0) parts.push(`max ${formatBytes(maxSize)}`);\n\n const maxFiles = maxFilesLimit.value;\n\n if (maxFiles > 0) parts.push(`up to ${maxFiles} file${maxFiles !== 1 ? 's' : ''}`);\n\n return parts.join(' · ');\n });\n\n // ============================================\n // Replace (\"swap this one file\" — see file-input-upload.ts's `replaceFile`)\n // ============================================\n function beginReplace(file: File): void {\n replaceTarget.value = file;\n inputRef.value?.click();\n }\n\n // ============================================\n // Gallery Preview URLs\n // ============================================\n // Object URLs are created lazily (only while `gallery` is enabled) and revoked as soon\n // as their file is no longer selected, plus unconditionally on disconnect — otherwise\n // each preview leaks its backing blob for the life of the page.\n const previewUrls = new Map<File, string>();\n\n function getPreviewUrl(file: File): string {\n let url = previewUrls.get(file);\n\n if (!url) {\n url = URL.createObjectURL(file);\n previewUrls.set(file, url);\n }\n\n return url;\n }\n\n function revokeStalePreviewUrls(activeFiles: File[]): void {\n const keep = new Set(activeFiles);\n\n for (const [file, url] of previewUrls) {\n if (!keep.has(file)) {\n URL.revokeObjectURL(url);\n previewUrls.delete(file);\n }\n }\n }\n\n watch(queue.files, revokeStalePreviewUrls);\n\n onCleanup(() => {\n for (const url of previewUrls.values()) URL.revokeObjectURL(url);\n\n previewUrls.clear();\n });\n\n // ============================================\n // Mount\n // ============================================\n // ============================================\n // Template\n // ============================================\n onElement(inputRef, (inp) => bindRefCallback(props.ref, inp));\n\n onMounted(() => {\n const inp = inputRef.value!;\n const dz = dropzoneRef.value!;\n let skipNextClick = false;\n const pressControl = createInteraction({\n disabled: isDisabled,\n onPress: () => {\n inp.click();\n },\n });\n\n // Native input → add files, or — if a \"Replace\" action opened the picker — swap the one\n // file it targeted instead of appending a new entry.\n onEvent(inp, 'change', (e: Event) => {\n const input = e.target as HTMLInputElement;\n const target = replaceTarget.value;\n\n replaceTarget.value = null;\n\n if (target && input.files?.[0]) queue.replaceFile(target, input.files[0], e);\n else if (input.files?.length) queue.addFiles(Array.from(input.files), e);\n\n input.value = ''; // reset so the same file triggers change again\n });\n // Click dropzone → open file picker\n onEvent(dz, 'click', (e: MouseEvent) => {\n if (e.target === inp) return;\n\n if (skipNextClick) {\n skipNextClick = false;\n\n return;\n }\n\n if (!isDisabled.value) inp.click();\n });\n // Keyboard: Enter / Space → open picker\n onEvent(dz, 'keydown', (e: KeyboardEvent) => {\n skipNextClick = pressControl.handleKeydown(e) && e.key === 'Enter';\n });\n\n // `createDropZone` has no way to update `disabled` after creation — recreate the zone\n // whenever the prop changes instead of capturing a stale snapshot from this one `onMounted`\n // run. `watch`'s own returned cleanup (not a second `onCleanup`) disposes the current zone\n // both on the next toggle and on final teardown.\n watch(\n isDisabled,\n (disabled) => {\n const dropZone = createDropZone({\n disabled,\n element: dz,\n onDrop: (droppedFiles) => queue.addFiles(droppedFiles),\n onHoverChange: (hovered) => {\n isDragging.value = hovered;\n },\n });\n\n return () => dropZone.dispose();\n },\n { immediate: true },\n );\n });\n\n return html`\n <div class=\"file-input-wrapper\" part=\"wrapper\">\n <label class=\"label-outside\" id=\"${labelId}\" part=\"label\" ?hidden=${() => !props.label.value}>\n ${props.label}\n </label>\n <div\n class=\"dropzone\"\n part=\"dropzone\"\n ref=${dropzoneRef}\n role=\"button\"\n tabindex=\"${() => (isDisabled.value ? '-1' : '0')}\"\n aria-disabled=\"${() => String(isDisabled.value)}\"\n aria-label=\"${() => (!props.label.value ? 'File upload drop zone' : null)}\"\n aria-labelledby=\"${() => (props.label.value ? labelId : null)}\"\n aria-describedby=\"${helperId}\">\n <input\n type=\"file\"\n ref=${inputRef}\n part=\"input\"\n id=\"${fileInputId}\"\n accept=\"${props.accept}\"\n ?multiple=\"${props.multiple}\"\n ?required=\"${props.required}\"\n ?disabled=\"${isDisabled}\"\n name=\"${props.name}\"\n hidden\n inert\n tabindex=\"-1\" />\n <div class=\"dropzone-content\">\n <span class=\"dropzone-icon\" aria-hidden=\"true\" data-status=\"${() => (isInvalid.value ? 'error' : null)}\">\n ${() =>\n isInvalid.value\n ? html`\n <ore-icon name=\"alert-circle\" size=\"36\" stroke-width=\"1.5\" aria-hidden=\"true\"></ore-icon>\n `\n : html`\n <ore-icon name=\"upload\" size=\"36\" stroke-width=\"1.5\" aria-hidden=\"true\"></ore-icon>\n `}\n </span>\n <!-- Signal 01: the copy itself shifts on drag-entry (not just border/glow) — a\n static \"Drop files here\" during an active drag reads as if the drop target\n hasn't noticed the file yet. -->\n ${when(\n () => isDragging.value,\n () => html`\n <span class=\"dropzone-title dropzone-title-active\">Release to upload</span>\n `,\n )}\n ${when(\n () => !isDragging.value,\n () => html`\n <span class=\"dropzone-title\">\n Drop files here or\n <u>click to browse</u>\n </span>\n `,\n )}\n <span class=\"dropzone-hint\" ?hidden=${() => !hintText.value}>${hintText}</span>\n </div>\n </div>\n <ul\n class=\"${() => (props.gallery.value ? 'file-grid' : 'file-list')}\"\n part=\"${() => (props.gallery.value ? 'gallery' : null)}\"\n role=\"list\"\n aria-label=\"Selected files\"\n ?hidden=${() => queue.files.value.length === 0}>\n ${() =>\n queue.files.value.map((file: File) =>\n props.gallery.value\n ? html`\n <li class=\"file-card\" data-status=${() => queue.fileState(file).status}>\n <span class=\"file-thumb-frame\">\n ${\n // Decorative: the file name is already announced via the visible\n // `.file-card-name` caption below — repeating it as `alt` text would\n // duplicate it (and often trips redundant-alt checks, since real\n // filenames commonly contain words like \"photo\" or \"image\").\n isImageFile(file)\n ? // Object URLs use the `blob:` scheme, which ore's attribute-level\n // XSS guard blocks unconditionally on `src` (and other\n // URL-accepting attributes) — set it as a DOM property via `ref`\n // instead, bypassing that string-based check. Safe here: the value\n // comes from `URL.createObjectURL(file)` on a real `File` object we\n // control, never from untrusted text.\n html`\n <img\n class=\"file-thumb\"\n alt=\"\"\n ref=\"${(el: HTMLImageElement | null) => {\n if (el) el.src = getPreviewUrl(file);\n }}\" />\n `\n : html`\n <span class=\"file-thumb file-thumb-generic\" aria-hidden=\"true\">\n <ore-icon name=\"file\" size=\"28\" stroke-width=\"1.5\" aria-hidden=\"true\"></ore-icon>\n </span>\n `\n }\n ${when(\n () => queue.fileState(file).status === 'success',\n () => html`\n <span class=\"file-status-badge file-status-badge-success\" aria-hidden=\"true\">\n <ore-icon name=\"check\" size=\"11\" stroke-width=\"3\" aria-hidden=\"true\"></ore-icon>\n </span>\n `,\n )}\n ${when(\n () => queue.fileState(file).status === 'error',\n () => html`\n <span class=\"file-status-badge file-status-badge-error\" aria-hidden=\"true\">\n <ore-icon name=\"alert-circle\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n </span>\n `,\n )}\n <span class=\"file-card-actions\">\n ${when(\n () => queue.fileState(file).status === 'error',\n () => html`\n <button\n class=\"file-card-action\"\n type=\"button\"\n aria-label=\"${`Retry uploading ${file.name}`}\"\n @click=${() => queue.retryUpload(file)}>\n <ore-icon name=\"refresh-cw\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n </button>\n `,\n )}\n ${when(\n () =>\n queue.fileState(file).status === 'success' &&\n Boolean(props.multiple.value) &&\n Boolean(props.upload.value),\n () => html`\n <button\n class=\"file-card-action\"\n type=\"button\"\n aria-label=\"${`Replace ${file.name}`}\"\n @click=${() => beginReplace(file)}>\n <ore-icon name=\"upload\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n </button>\n `,\n )}\n <button\n class=\"file-card-action file-card-remove\"\n type=\"button\"\n aria-label=\"${`Remove ${file.name}`}\"\n @click=${(e: Event) => queue.removeFile(file, e)}>\n <ore-icon name=\"x\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n </button>\n </span>\n </span>\n <span class=\"file-card-name\" title=\"${file.name}\">${file.name}</span>\n ${when(\n () => queue.fileState(file).status === 'uploading',\n () => html`\n <ore-progress\n type=\"linear\"\n size=\"sm\"\n value=${() => queue.uploadPercent(file)}\n label=${() => `${queue.uploadPercent(file)}%`}></ore-progress>\n <span class=\"file-progress-meta\">${() => queue.uploadMetaText(file)}</span>\n `,\n )}\n ${when(\n () => queue.fileState(file).status === 'error',\n () => html`\n <span class=\"file-error-text\">${() => queue.fileState(file).error}</span>\n `,\n )}\n </li>\n `\n : html`\n <li class=\"file-item\" data-status=${() => queue.fileState(file).status}>\n <span class=\"file-icon\" aria-hidden=\"true\">\n <ore-icon\n name=${() => queue.statusIconName(file)}\n size=\"18\"\n stroke-width=\"1.75\"\n aria-hidden=\"true\"></ore-icon>\n </span>\n <span class=\"file-meta\">\n <span class=\"file-name\" title=\"${file.name}\">${file.name}</span>\n ${when(\n () => queue.fileState(file).status === 'uploading',\n () => html`\n <ore-progress\n type=\"linear\"\n size=\"sm\"\n value=${() => queue.uploadPercent(file)}\n label=${() => `${queue.uploadPercent(file)}%`}></ore-progress>\n <span class=\"file-progress-meta\">${() => queue.uploadMetaText(file)}</span>\n `,\n )}\n ${when(\n () => queue.fileState(file).status === 'error',\n () => html`\n <span class=\"file-error-text\">${() => queue.fileState(file).error}</span>\n `,\n )}\n ${when(\n () => queue.fileState(file).status === 'idle' || queue.fileState(file).status === 'success',\n () => html`\n <span class=\"file-size\">${formatBytes(file.size)}</span>\n `,\n )}\n </span>\n <span class=\"file-actions\">\n ${when(\n () => queue.fileState(file).status === 'error',\n () => html`\n <button\n class=\"file-action\"\n type=\"button\"\n aria-label=\"${`Retry uploading ${file.name}`}\"\n @click=${() => queue.retryUpload(file)}>\n <ore-icon name=\"refresh-cw\" size=\"14\" stroke-width=\"2\" aria-hidden=\"true\"></ore-icon>\n </button>\n `,\n )}\n ${when(\n () =>\n queue.fileState(file).status === 'success' &&\n Boolean(props.multiple.value) &&\n Boolean(props.upload.value),\n () => html`\n <button\n class=\"file-action\"\n type=\"button\"\n aria-label=\"${`Replace ${file.name}`}\"\n @click=${() => beginReplace(file)}>\n <ore-icon name=\"upload\" size=\"14\" stroke-width=\"2\" aria-hidden=\"true\"></ore-icon>\n </button>\n `,\n )}\n <button\n class=\"file-remove\"\n type=\"button\"\n aria-label=\"${`Remove ${file.name}`}\"\n @click=${(e: Event) => queue.removeFile(file, e)}>\n <ore-icon name=\"x\" size=\"12\" stroke-width=\"2.5\" aria-hidden=\"true\"></ore-icon>\n </button>\n </span>\n </li>\n `,\n )}\n </ul>\n <div class=\"helper-text\" id=\"${helperId}\" part=\"helper\" ?hidden=${() => isInvalid.value || !props.helper.value}>\n ${props.helper}\n </div>\n <div\n class=\"helper-text helper-text-error\"\n id=\"${errorId}\"\n role=\"alert\"\n part=\"error\"\n ?hidden=${() => !isInvalid.value}>\n ${() => props.error.value ?? ''}\n </div>\n </div>\n `;\n },\n shadow: { delegatesFocus: true },\n styles: [\n colorThemeMixin,\n coarsePointerMixin,\n reducedMotionMixin,\n roundedVariantMixin,\n disabledLoadingMixin,\n sizeVariantMixin(FILE_INPUT_SIZE_PRESET),\n forcedColorsFocusMixin('.dropzone'),\n componentStyles,\n ],\n});\n"],"mappings":";;;;;;;;;;;;;AAqCA,IAAM,KAAe,MAAwB,EAAK,KAAK,WAAW,QAAQ,GAoI7D,IAAiB;AAC9B,EAA0B,GAAgB;CACxC,gBAAgB;CAChB,OAAO;EACL,QAAQ,EAAK,OAAO;EACpB,OAAO,EAAK,OAAO;EACnB,UAAU,EAAK,KAAK,EAAK;EACzB,OAAO,EAAK,OAAO;EACnB,SAAS,EAAK,KAAK,EAAK;EACxB,QAAQ,EAAK,OAAO;EACpB,OAAO,EAAK,OAAO;EACnB,aAAa,EAAK,OAAO,CAAC;EAC1B,YAAY,EAAK,OAAO,CAAC;EACzB,UAAU,EAAK,KAAK,EAAK;EACzB,MAAM,EAAK,OAAO;EAClB,KAAK,EAAK,KAAqD;EAC/D,UAAU,EAAK,KAAK,EAAK;EACzB,MAAM,EAAK,OAAO;EAClB,QAAQ,EAAK,KAAmB;CAClC;CACA,MAAM,GAAO;EACX,IAAM,IAAK,EAAQ,GACb,IAAO,EAA4B,GAMnC,IAAa,EAAO,EAAK,GAGzB,IAAgB,EAAoB,IAAI,GAExC,IAAa,QAAe,EAAQ,EAAM,SAAS,KAAM,GACzD,IAAgB,QAAe,EAAM,YAAY,CAAC,SAAS,CAAC,GAC5D,IAAe,QAAe,EAAM,WAAW,CAAC,SAAS,CAAC,GAI1D,IAAQ,EAAgB;GAC5B,QAAQ,EAAM;GACd,UAAU;GACV,UAAU,EAAG;GACb,UAAU;GACV,SAAS;GACT,UAAU,QAAe,EAAQ,EAAM,SAAS,KAAM;GACtD,WAAW,GAAc,MACvB,EAAK,UAAU;IAAE,OAAO;IAAc;IAAe,OAAO;GAAa,CAAC;GAC5E,WAAW,GAAM,GAAgB,MAC/B,EAAK,UAAU;IAAE;IAAM,OAAO;IAAgB;IAAe,OAAO;GAAe,CAAC;GACtF,gBAAgB,GAAM,MAAU,EAAK,gBAAgB;IAAE;IAAO;GAAK,CAAC;GACpE,mBAAmB,GAAM,GAAQ,MAAU,EAAK,mBAAmB;IAAE;IAAM;IAAQ;GAAM,CAAC;GAC1F,kBAAkB,MAAS,EAAK,kBAAkB,EAAE,QAAK,CAAC;GAC1D,QAAQ,QAAe,EAAM,OAAO,SAAS,KAAA,CAAS;EACxD,CAAC;EAQD,AANA,QAAgB,EAAM,QAAQ,CAAC,GAM/B,EAAS;GACP,UAAU;GACV,cAAc,MAAe;IAC3B,IAAI,EAAG,WAAW,GAAG,OAAO;IAE5B,IAAM,IAAO,EAAM,KAAK,SAAS,QAC3B,IAAK,IAAI,SAAS;IAExB,KAAK,IAAM,KAAQ,GAAI,EAAG,OAAO,GAAM,CAAI;IAE3C,OAAO;GACT;GACA,OAAO,EAAM;EACf,CAAC;EAGD,IAAM,IAAY,QAAe,EAAQ,EAAM,MAAM,KAAM;EAE3D,EAAK,EACH,MAAM;GACJ,mBAAoB,EAAW,QAAQ,KAAO,KAAA;GAC9C,eAAgB,EAAU,QAAQ,KAAO,KAAA;GACzC,MAAM,EAAM;EACd,EACF,CAAC;EAKD,IAAM,IAAc,EAAe,YAAY,GACzC,IAAU,SAAS,KACnB,IAAW,UAAU,KACrB,IAAU,SAAS,KAKnB,IAAc,EAAoB,GAClC,IAAW,EAAsB,GACjC,IAAW,QAAe;GAC9B,IAAM,IAAkB,CAAC;GAEzB,AAAI,EAAM,OAAO,SACf,EAAM,KACJ,EAAM,OAAO,MACV,MAAM,GAAG,CAAC,CACV,KAAK,MAAc,EAAE,KAAK,CAAC,CAAC,CAC5B,KAAK,IAAI,CACd;GAGF,IAAM,IAAU,EAAa;GAE7B,AAAI,IAAU,KAAG,EAAM,KAAK,OAAO,EAAY,CAAO,GAAG;GAEzD,IAAM,IAAW,EAAc;GAI/B,OAFI,IAAW,KAAG,EAAM,KAAK,SAAS,EAAS,OAAO,MAAa,IAAU,KAAN,KAAU,GAE1E,EAAM,KAAK,KAAK;EACzB,CAAC;EAKD,SAAS,EAAa,GAAkB;GAEtC,AADA,EAAc,QAAQ,GACtB,EAAS,OAAO,MAAM;EACxB;EAQA,IAAM,oBAAc,IAAI,IAAkB;EAE1C,SAAS,EAAc,GAAoB;GACzC,IAAI,IAAM,EAAY,IAAI,CAAI;GAO9B,OALK,MACH,IAAM,IAAI,gBAAgB,CAAI,GAC9B,EAAY,IAAI,GAAM,CAAG,IAGpB;EACT;EAEA,SAAS,EAAuB,GAA2B;GACzD,IAAM,IAAO,IAAI,IAAI,CAAW;GAEhC,KAAK,IAAM,CAAC,GAAM,MAAQ,GACxB,AAAK,EAAK,IAAI,CAAI,MAChB,IAAI,gBAAgB,CAAG,GACvB,EAAY,OAAO,CAAI;EAG7B;EAiFA,OA/EA,EAAM,EAAM,OAAO,CAAsB,GAEzC,QAAgB;GACd,KAAK,IAAM,KAAO,EAAY,OAAO,GAAG,IAAI,gBAAgB,CAAG;GAE/D,EAAY,MAAM;EACpB,CAAC,GAQD,EAAU,IAAW,MAAQ,EAAgB,EAAM,KAAK,CAAG,CAAC,GAE5D,QAAgB;GACd,IAAM,IAAM,EAAS,OACf,IAAK,EAAY,OACnB,IAAgB,IACd,IAAe,EAAkB;IACrC,UAAU;IACV,eAAe;KACb,EAAI,MAAM;IACZ;GACF,CAAC;GAoCD,AAhCA,EAAQ,GAAK,WAAW,MAAa;IACnC,IAAM,IAAQ,EAAE,QACV,IAAS,EAAc;IAO7B,AALA,EAAc,QAAQ,MAElB,KAAU,EAAM,QAAQ,KAAI,EAAM,YAAY,GAAQ,EAAM,MAAM,IAAI,CAAC,IAClE,EAAM,OAAO,UAAQ,EAAM,SAAS,MAAM,KAAK,EAAM,KAAK,GAAG,CAAC,GAEvE,EAAM,QAAQ;GAChB,CAAC,GAED,EAAQ,GAAI,UAAU,MAAkB;IAClC,MAAE,WAAW,GAEjB;SAAI,GAAe;MACjB,IAAgB;MAEhB;KACF;KAEA,AAAK,EAAW,SAAO,EAAI,MAAM;IAFjC;GAGF,CAAC,GAED,EAAQ,GAAI,YAAY,MAAqB;IAC3C,IAAgB,EAAa,cAAc,CAAC,KAAK,EAAE,QAAQ;GAC7D,CAAC,GAMD,EACE,IACC,MAAa;IACZ,IAAM,IAAW,EAAe;KAC9B;KACA,SAAS;KACT,SAAS,MAAiB,EAAM,SAAS,CAAY;KACrD,gBAAgB,MAAY;MAC1B,EAAW,QAAQ;KACrB;IACF,CAAC;IAED,aAAa,EAAS,QAAQ;GAChC,GACA,EAAE,WAAW,GAAK,CACpB;EACF,CAAC,GAEM,CAAI;;2CAE4B,EAAQ,+BAA+B,CAAC,EAAM,MAAM,MAAM;YACzF,EAAM,MAAM;;;;;gBAKR,EAAY;;4BAEC,EAAW,QAAQ,OAAO,IAAK;iCAC3B,OAAO,EAAW,KAAK,EAAE;8BAC1B,EAAM,MAAM,QAAkC,OAA1B,wBAAgC;mCAChD,EAAM,MAAM,QAAQ,IAAU,KAAM;8BAC1C,EAAS;;;kBAGrB,EAAS;;kBAET,EAAY;sBACR,EAAM,OAAO;yBACV,EAAM,SAAS;yBACf,EAAM,SAAS;yBACf,EAAW;oBAChB,EAAM,KAAK;;;;;gFAKkD,EAAU,QAAQ,UAAU,KAAM;sBAEnG,EAAU,QACN,CAAI;;wBAGJ,CAAI;;sBAEF;;;;;cAKR,QACM,EAAW,aACX,CAAI;;eAGZ,EAAE;cACA,QACM,CAAC,EAAW,aACZ,CAAI;;;;;eAMZ,EAAE;wDAC0C,CAAC,EAAS,MAAM,GAAG,EAAS;;;;yBAI1D,EAAM,QAAQ,QAAQ,cAAc,YAAa;wBAClD,EAAM,QAAQ,QAAQ,YAAY,KAAM;;;0BAGvC,EAAM,MAAM,MAAM,WAAW,EAAE;kBAE7C,EAAM,MAAM,MAAM,KAAK,MACrB,EAAM,QAAQ,QACV,CAAI;8DACwC,EAAM,UAAU,CAAI,CAAC,CAAC,OAAO;;0BAOjE,EAAY,CAAI,IAOZ,CAAI;;;;0CAIQ,MAAgC;GACtC,AAAI,MAAI,EAAG,MAAM,EAAc,CAAI;EACrC,EAAE;kCAEN,CAAI;;;;gCAKT;0BACC,QACM,EAAM,UAAU,CAAI,CAAC,CAAC,WAAW,iBACjC,CAAI;;;;2BAKZ,EAAE;0BACA,QACM,EAAM,UAAU,CAAI,CAAC,CAAC,WAAW,eACjC,CAAI;;;;2BAKZ,EAAE;;4BAEE,QACM,EAAM,UAAU,CAAI,CAAC,CAAC,WAAW,eACjC,CAAI;;;;8CAIQ,mBAAmB,EAAK,OAAO;+CAC9B,EAAM,YAAY,CAAI,EAAE;;;6BAI7C,EAAE;4BACA,QAEE,EAAM,UAAU,CAAI,CAAC,CAAC,WAAW,aACjC,EAAQ,EAAM,SAAS,SACvB,EAAQ,EAAM,OAAO,aACjB,CAAI;;;;8CAIQ,WAAW,EAAK,OAAO;+CACtB,EAAa,CAAI,EAAE;;;6BAIxC,EAAE;;;;0CAIc,UAAU,EAAK,OAAO;sCAC1B,MAAa,EAAM,WAAW,GAAM,CAAC,EAAE;;;;;4DAKjB,EAAK,KAAK,IAAI,EAAK,KAAK;wBAC5D,QACM,EAAM,UAAU,CAAI,CAAC,CAAC,WAAW,mBACjC,CAAI;;;;0CAIQ,EAAM,cAAc,CAAI,EAAE;0CAC1B,GAAG,EAAM,cAAc,CAAI,EAAE,GAAG;mEACP,EAAM,eAAe,CAAI,EAAE;yBAExE,EAAE;wBACA,QACM,EAAM,UAAU,CAAI,CAAC,CAAC,WAAW,eACjC,CAAI;gEAC8B,EAAM,UAAU,CAAI,CAAC,CAAC,MAAM;yBAEtE,EAAE;;sBAGN,CAAI;8DACwC,EAAM,UAAU,CAAI,CAAC,CAAC,OAAO;;;uCAGpD,EAAM,eAAe,CAAI,EAAE;;;;;;yDAMT,EAAK,KAAK,IAAI,EAAK,KAAK;0BACvD,QACM,EAAM,UAAU,CAAI,CAAC,CAAC,WAAW,mBACjC,CAAI;;;;4CAIQ,EAAM,cAAc,CAAI,EAAE;4CAC1B,GAAG,EAAM,cAAc,CAAI,EAAE,GAAG;qEACP,EAAM,eAAe,CAAI,EAAE;2BAExE,EAAE;0BACA,QACM,EAAM,UAAU,CAAI,CAAC,CAAC,WAAW,eACjC,CAAI;kEAC8B,EAAM,UAAU,CAAI,CAAC,CAAC,MAAM;2BAEtE,EAAE;0BACA,QACM,EAAM,UAAU,CAAI,CAAC,CAAC,WAAW,UAAU,EAAM,UAAU,CAAI,CAAC,CAAC,WAAW,iBAC5E,CAAI;sDACkB,EAAY,EAAK,IAAI,EAAE;2BAErD,EAAE;;;0BAGA,QACM,EAAM,UAAU,CAAI,CAAC,CAAC,WAAW,eACjC,CAAI;;;;4CAIQ,mBAAmB,EAAK,OAAO;6CAC9B,EAAM,YAAY,CAAI,EAAE;;;2BAI7C,EAAE;0BACA,QAEE,EAAM,UAAU,CAAI,CAAC,CAAC,WAAW,aACjC,EAAQ,EAAM,SAAS,SACvB,EAAQ,EAAM,OAAO,aACjB,CAAI;;;;4CAIQ,WAAW,EAAK,OAAO;6CACtB,EAAa,CAAI,EAAE;;;2BAIxC,EAAE;;;;wCAIc,UAAU,EAAK,OAAO;oCAC1B,MAAa,EAAM,WAAW,GAAM,CAAC,EAAE;;;;;mBAM/D,EAAE;;uCAEyB,EAAS,gCAAgC,EAAU,SAAS,CAAC,EAAM,OAAO,MAAM;YAC3G,EAAM,OAAO;;;;gBAIT,EAAQ;;;0BAGE,CAAC,EAAU,MAAM;kBACzB,EAAM,MAAM,SAAS,GAAG;;;;CAIxC;CACA,QAAQ,EAAE,gBAAgB,GAAK;CAC/B,QAAQ;EACN;EACA;EACA;EACA;EACA;EACA,EAAiB,CAAsB;EACvC,EAAuB,WAAW;EAClC;CACF;AACF,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Generated from dist/custom-elements.json. Do not edit directly.
|
|
2
|
-
export type RefineElementTag = 'ore-accordion' | 'ore-accordion-item' | 'ore-alert' | 'ore-async' | 'ore-avatar' | 'ore-avatar-group' | 'ore-badge' | 'ore-box' | 'ore-breadcrumb' | 'ore-breadcrumb-item' | 'ore-button' | 'ore-button-group' | 'ore-calendar' | 'ore-card' | 'ore-carousel' | 'ore-chat-message' | 'ore-checkbox' | 'ore-checkbox-group' | 'ore-chip' | 'ore-code-window' | 'ore-command-palette' | 'ore-command-palette-item' | 'ore-copy-command' | 'ore-date-picker' | 'ore-dialog' | 'ore-drawer' | 'ore-file-input' | 'ore-grid' | 'ore-grid-item' | 'ore-icon' | 'ore-input' | 'ore-list' | 'ore-list-item' | 'ore-marquee' | 'ore-menu' | 'ore-menu-item' | 'ore-menu-separator' | 'ore-message-composer' | 'ore-navbar' | 'ore-navbar-item' | 'ore-navigation-menu' | 'ore-navigation-menu-item' | 'ore-navigation-menu-panel' | 'ore-number-input' | 'ore-otp-input' | 'ore-pagination' | 'ore-password-strength' | 'ore-popover' | 'ore-progress' | 'ore-radio' | 'ore-radio-group' | 'ore-rating' | 'ore-select' | 'ore-separator' | 'ore-sidebar' | 'ore-sidebar-group' | 'ore-sidebar-item' | 'ore-skeleton' | 'ore-slider' | 'ore-step' | 'ore-stepper' | 'ore-switch' | 'ore-tab-item' | 'ore-tab-panel' | 'ore-table' | 'ore-tabs' | 'ore-td' | 'ore-text' | 'ore-textarea' | 'ore-th' | 'ore-time-picker' | 'ore-toast' | 'ore-tooltip' | 'ore-tr' | 'ore-typing-indicator';
|
|
2
|
+
export type RefineElementTag = 'ore-accordion' | 'ore-accordion-item' | 'ore-alert' | 'ore-async' | 'ore-avatar' | 'ore-avatar-group' | 'ore-badge' | 'ore-box' | 'ore-breadcrumb' | 'ore-breadcrumb-item' | 'ore-button' | 'ore-button-group' | 'ore-calendar' | 'ore-card' | 'ore-carousel' | 'ore-chat-message' | 'ore-checkbox' | 'ore-checkbox-group' | 'ore-chip' | 'ore-code-window' | 'ore-command-palette' | 'ore-command-palette-item' | 'ore-copy-command' | 'ore-date-picker' | 'ore-dialog' | 'ore-drawer' | 'ore-file-input' | 'ore-grid' | 'ore-grid-item' | 'ore-icon' | 'ore-input' | 'ore-list' | 'ore-list-item' | 'ore-marquee' | 'ore-menu' | 'ore-menu-item' | 'ore-menu-separator' | 'ore-message-composer' | 'ore-navbar' | 'ore-navbar-item' | 'ore-navigation-menu' | 'ore-navigation-menu-item' | 'ore-navigation-menu-panel' | 'ore-number-input' | 'ore-otp-input' | 'ore-pagination' | 'ore-password-strength' | 'ore-popover' | 'ore-progress' | 'ore-radio' | 'ore-radio-group' | 'ore-rating' | 'ore-select' | 'ore-separator' | 'ore-sidebar' | 'ore-sidebar-group' | 'ore-sidebar-item' | 'ore-skeleton' | 'ore-slider' | 'ore-stats' | 'ore-step' | 'ore-stepper' | 'ore-switch' | 'ore-tab-item' | 'ore-tab-panel' | 'ore-table' | 'ore-tabs' | 'ore-td' | 'ore-text' | 'ore-textarea' | 'ore-th' | 'ore-time-picker' | 'ore-toast' | 'ore-tooltip' | 'ore-tr' | 'ore-typing-indicator';
|
|
3
3
|
|
|
4
4
|
export interface RefineElementMap {
|
|
5
5
|
'ore-accordion': HTMLElement;
|
|
@@ -61,6 +61,7 @@ export interface RefineElementMap {
|
|
|
61
61
|
'ore-sidebar-item': HTMLElement;
|
|
62
62
|
'ore-skeleton': HTMLElement;
|
|
63
63
|
'ore-slider': HTMLElement;
|
|
64
|
+
'ore-stats': HTMLElement;
|
|
64
65
|
'ore-step': HTMLElement;
|
|
65
66
|
'ore-stepper': HTMLElement;
|
|
66
67
|
'ore-switch': HTMLElement;
|
package/dist/icon.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./_virtual/_rolldown/runtime.cjs");require("./_dev.cjs");const t=require("./content/icon/icon.css.cjs");let n=require("@vielzeug/ore"),r=require("@vielzeug/ripple"),i=require("lucide");i=e.__toESM(i,1);var a=16,o=2,s=24,c=`http://www.w3.org/2000/svg`,l=new Set([`circle`,`clipPath`,`defs`,`ellipse`,`g`,`line`,`linearGradient`,`mask`,`path`,`polygon`,`polyline`,`radialGradient`,`rect`,`stop`,`symbol`,`use`]),u=/^[a-zA-Z][a-zA-Z0-9:_-]*$/,d=/^(on|xlink:|xml:|href$)/i,f=new Map(Object.entries(i.icons));function p(e){for(let[t,n]of Object.entries(e))f.set(t,n)}var m=e=>e.replace(/([a-z0-9])([A-Z])/g,`$1-$2`).split(/[^a-zA-Z0-9]+/).filter(Boolean).map(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()).join(``),h=e=>{if(e==null||e===``)return a;let t=Number(e);return Number.isFinite(t)&&t>0&&/^\d+(\.\d+)?$/.test(e.trim())?t:e},g=e=>f.get(e)??f.get(m(e)),_=(e,t)=>{if(!l.has(e))return null;let n=document.createElementNS(c,e);for(let[e,r]of Object.entries(t))r!==void 0&&
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./_virtual/_rolldown/runtime.cjs");require("./_dev.cjs");const t=require("./content/icon/icon.css.cjs");let n=require("@vielzeug/ore"),r=require("@vielzeug/ripple"),i=require("lucide");i=e.__toESM(i,1);var a=16,o=2,s=24,c=`http://www.w3.org/2000/svg`,l=new Set([`circle`,`clipPath`,`defs`,`ellipse`,`g`,`line`,`linearGradient`,`mask`,`path`,`polygon`,`polyline`,`radialGradient`,`rect`,`stop`,`symbol`,`use`]),u=/^[a-zA-Z][a-zA-Z0-9:_-]*$/,d=/^(on|xlink:|xml:|href$)/i,f=new Map(Object.entries(i.icons));function p(e){for(let[t,n]of Object.entries(e))f.set(t,n)}var m=e=>e.replace(/([a-z0-9])([A-Z])/g,`$1-$2`).split(/[^a-zA-Z0-9]+/).filter(Boolean).map(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()).join(``),h=e=>{if(e==null||e===``)return a;let t=Number(e);return Number.isFinite(t)&&t>0&&/^\d+(\.\d+)?$/.test(e.trim())?t:e},g=e=>f.get(e)??f.get(m(e)),_=(e,t)=>{if(!l.has(e))return null;let n=document.createElementNS(c,e);for(let[e,r]of Object.entries(t))r!==void 0&&u.test(e)&&!d.test(e)&&n.setAttribute(e,String(r));return n},v=`ore-icon`;(0,n.define)(v,{props:{absoluteStrokeWidth:n.prop.bool(),label:n.prop.string(),name:n.prop.string(),size:{default:a,parse:h,reflect:!1},solid:n.prop.bool(),strokeWidth:n.prop.number(o)},setup(e){(0,n.bind)({attr:{"aria-hidden":()=>(e.label.value??``).trim()?null:`true`,"aria-label":()=>(e.label.value??``).trim()||null,role:()=>(e.label.value??``).trim()?`img`:null},style:{"--_size-px":()=>{let t=e.size.value;return typeof t==`number`?`${t}px`:String(t)}}});let t=(0,n.ref)(),i=(0,r.computed)(()=>{let t=(e.name.value??``).trim();if(!t)return;let n=g(t);return n||`${t.slice(0,64)}`,n});return(0,n.watchEffect)(()=>{let n=t.value,r=i.value;if(!n||!r)return;let a=e.size.value,o=typeof a==`number`?a:s,c=e.absoluteStrokeWidth.value&&!e.solid.value?e.strokeWidth.value*(s/o):e.strokeWidth.value;n.setAttribute(`fill`,e.solid.value?`currentColor`:`none`),n.setAttribute(`stroke`,e.solid.value?`none`:`currentColor`),n.setAttribute(`stroke-linecap`,`round`),n.setAttribute(`stroke-linejoin`,`round`),n.setAttribute(`stroke-width`,String(e.solid.value?0:c)),n.setAttribute(`viewBox`,`0 0 24 24`);let l=r.map(([e,t])=>_(e,t)).filter(e=>!!e);n.replaceChildren(...l)}),n.html`
|
|
2
2
|
${(0,n.when)(()=>!!i.value,()=>n.html`
|
|
3
3
|
<svg ref="${t}" part="svg" style="width:100%;height:100%"></svg>
|
|
4
4
|
`)}
|
package/dist/icon.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"icon.cjs","names":[],"sources":["../src/content/icon/icon.ts"],"sourcesContent":["import { bind, define, html, prop, ref, watchEffect, when } from '@vielzeug/ore';\nimport { computed } from '@vielzeug/ripple';\nimport * as lucideModule from 'lucide';\n\nimport { warn } from '../../_dev';\nimport styles from './icon.css?inline';\n\nexport type IconNode = Array<[string, Record<string, string | number | undefined>]>;\n\nconst DEFAULT_SIZE = 16;\nconst DEFAULT_STROKE_WIDTH = 2;\nconst LUCIDE_VIEWBOX_SIZE = 24;\nconst SVG_NS = 'http://www.w3.org/2000/svg';\n\n/**\n * Allowlisted SVG shape/structure element names. Only these tags are permitted\n * when building the SVG DOM from an IconNode, preventing arbitrary element\n * injection (e.g. <script>, event-handler-bearing elements).\n */\nconst ALLOWED_SVG_TAGS = new Set([\n 'circle',\n 'clipPath',\n 'defs',\n 'ellipse',\n 'g',\n 'line',\n 'linearGradient',\n 'mask',\n 'path',\n 'polygon',\n 'polyline',\n 'radialGradient',\n 'rect',\n 'stop',\n 'symbol',\n 'use',\n]);\n\n/**\n * Allowlisted SVG presentation / structural attribute names.\n * Event handler attributes (on*) and \"xlink:href\" are excluded.\n */\nconst ATTR_KEY_RE = /^[a-zA-Z][a-zA-Z0-9:_-]*$/;\nconst BLOCKED_ATTR_RE = /^(on|xlink:|xml:|href$)/i;\n\n// Sync registry seeded from lucide at module load; extend at any time via registerIcons()\nconst registry = new Map<string, IconNode>(\n Object.entries((lucideModule as unknown as { icons: Record<string, IconNode> }).icons),\n);\n\n/**\n * Register additional icons (or override existing ones) by name.\n * Keys may be kebab-case or PascalCase — both are accepted by `ore-icon`.\n */\nexport function registerIcons(icons: Record<string, IconNode>): void {\n for (const [name, node] of Object.entries(icons)) {\n registry.set(name, node);\n }\n}\n\nconst toPascalCase = (value: string): string =>\n value\n .replace(/([a-z0-9])([A-Z])/g, '$1-$2')\n .split(/[^a-zA-Z0-9]+/)\n .filter(Boolean)\n .map((p) => p.charAt(0).toUpperCase() + p.slice(1).toLowerCase())\n .join('');\n\nconst parseSize = (value: string | null): number | string => {\n if (value == null || value === '') return DEFAULT_SIZE;\n\n const n = Number(value);\n\n return Number.isFinite(n) && n > 0 && /^\\d+(\\.\\d+)?$/.test(value.trim()) ? n : value;\n};\n\nconst resolveIcon = (name: string): IconNode | undefined => registry.get(name) ?? registry.get(toPascalCase(name));\n\n/**\n * Builds an allowlisted SVG child element via `createElementNS` — real DOM construction, not\n * HTML-string parsing, so this never touches Ore's `unsafeHtml()` directive (and its \"unsanitized\n * HTML\" dev warning) at all. `iconNode` entries come from the bundled Lucide icon set or\n * `registerIcons()` (a developer/build-time API, never runtime user input), but the tag/attr\n * allowlist stays as defense in depth regardless of where the data originated.\n */\nconst createSvgChild = (tag: string, attrs: Record<string, string | number | undefined>): SVGElement | null => {\n if (!ALLOWED_SVG_TAGS.has(tag)) return null;\n\n const el = document.createElementNS(SVG_NS, tag);\n\n for (const [key, value] of Object.entries(attrs)) {\n if (value === undefined) continue;\n\n if (!ATTR_KEY_RE.test(key) || BLOCKED_ATTR_RE.test(key)) continue;\n\n el.setAttribute(key, String(value));\n }\n\n return el;\n};\n\n/** Icon component properties */\nexport type OreIconProps = {\n /** Keep stroke width visually consistent when icon size changes */\n absoluteStrokeWidth?: boolean;\n /** Accessible text label. Decorative icons should omit this. */\n label?: string;\n /** Lucide icon name, e.g. `search` or `chevron-right` */\n name?: string;\n /** Icon width/height in px by default. Accepts CSS lengths. */\n size?: number | string;\n /** Render as a filled/solid shape instead of a stroked outline */\n solid?: boolean;\n /** SVG stroke width */\n strokeWidth?: number;\n};\n\n/**\n * Icon wrapper for consistent Lucide rendering across Block.\n *\n * @element ore-icon\n *\n * @attr {string} name - Lucide icon name, e.g. `search` or `chevron-right`\n * @attr {number|string} size - Width/height (default: 16)\n * @attr {number} stroke-width - SVG stroke width (default: 2)\n * @attr {boolean} absolute-stroke-width - Keep stroke width visually stable across icon sizes\n * @attr {boolean} solid - Render as filled shape instead of stroked outline\n * @attr {string} label - Accessible label; when omitted the icon is decorative\n *\n * @csspart svg - Internal SVG element\n *\n * @example\n * ```html\n * <ore-icon name=\"search\"></ore-icon>\n * <ore-icon name=\"chevron-right\" size=\"18\"></ore-icon>\n * <ore-icon name=\"trash-2\" label=\"Delete\"></ore-icon>\n * <ore-icon name=\"star\" solid></ore-icon>\n * ```\n */\nexport const ICON_TAG = 'ore-icon' as const;\ndefine<OreIconProps>(ICON_TAG, {\n props: {\n absoluteStrokeWidth: prop.bool(),\n label: prop.string(),\n name: prop.string(),\n size: { default: DEFAULT_SIZE as number | string, parse: parseSize, reflect: false },\n solid: prop.bool(),\n strokeWidth: prop.number(DEFAULT_STROKE_WIDTH),\n },\n setup(props) {\n bind({\n attr: {\n 'aria-hidden': () => ((props.label.value ?? '').trim() ? null : 'true'),\n 'aria-label': () => (props.label.value ?? '').trim() || null,\n role: () => ((props.label.value ?? '').trim() ? 'img' : null),\n },\n style: {\n '--_size-px': () => {\n const s = props.size.value;\n\n return typeof s === 'number' ? `${s}px` : String(s);\n },\n },\n });\n\n const svgRef = ref<SVGSVGElement>();\n\n // Memoized so an unknown icon name only warns once per change, not once per reactive read.\n const iconNode = computed(() => {\n const name = (props.name.value ?? '').trim();\n\n if (!name) return undefined;\n\n const node = resolveIcon(name);\n\n if (!node) warn(`ore-icon: icon not found: \"${name.slice(0, 64)}\"`);\n\n return node;\n });\n\n // Real DOM construction (createElementNS), not the unsafeHtml() directive — see createSvgChild's\n // doc comment. Rebuilds the <svg>'s attributes and children whenever the icon name, size,\n // or presentation props change, or when `svgRef` first attaches (a fresh element mounts\n // every time `when()`'s branch below flips truthy).\n watchEffect(() => {\n const svg = svgRef.value;\n const node = iconNode.value;\n\n if (!svg || !node) return;\n\n const size = props.size.value;\n const numericSize = typeof size === 'number' ? size : LUCIDE_VIEWBOX_SIZE;\n const strokeWidth =\n props.absoluteStrokeWidth.value && !props.solid.value\n ? props.strokeWidth.value! * (LUCIDE_VIEWBOX_SIZE / numericSize)\n : props.strokeWidth.value!;\n\n svg.setAttribute('fill', props.solid.value ? 'currentColor' : 'none');\n svg.setAttribute('stroke', props.solid.value ? 'none' : 'currentColor');\n svg.setAttribute('stroke-linecap', 'round');\n svg.setAttribute('stroke-linejoin', 'round');\n svg.setAttribute('stroke-width', String(props.solid.value ? 0 : strokeWidth));\n svg.setAttribute('viewBox', '0 0 24 24');\n\n const children = node.map(([tag, attrs]) => createSvgChild(tag, attrs)).filter((el): el is SVGElement => !!el);\n\n svg.replaceChildren(...children);\n });\n\n return html`\n ${when(\n () => !!iconNode.value,\n () => html`\n <svg ref=\"${svgRef}\" part=\"svg\" style=\"width:100%;height:100%\"></svg>\n `,\n )}\n `;\n },\n styles: [styles],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"icon.cjs","names":[],"sources":["../src/content/icon/icon.ts"],"sourcesContent":["import { bind, define, html, prop, ref, watchEffect, when } from '@vielzeug/ore';\nimport { computed } from '@vielzeug/ripple';\nimport * as lucideModule from 'lucide';\n\nimport { warn } from '../../_dev';\nimport styles from './icon.css?inline';\n\nexport type IconNode = Array<[string, Record<string, string | number | undefined>]>;\n\nconst DEFAULT_SIZE = 16;\nconst DEFAULT_STROKE_WIDTH = 2;\nconst LUCIDE_VIEWBOX_SIZE = 24;\nconst SVG_NS = 'http://www.w3.org/2000/svg';\n\n/**\n * Allowlisted SVG shape/structure element names. Only these tags are permitted\n * when building the SVG DOM from an IconNode, preventing arbitrary element\n * injection (e.g. <script>, event-handler-bearing elements).\n */\nconst ALLOWED_SVG_TAGS = new Set([\n 'circle',\n 'clipPath',\n 'defs',\n 'ellipse',\n 'g',\n 'line',\n 'linearGradient',\n 'mask',\n 'path',\n 'polygon',\n 'polyline',\n 'radialGradient',\n 'rect',\n 'stop',\n 'symbol',\n 'use',\n]);\n\n/**\n * Allowlisted SVG presentation / structural attribute names.\n * Event handler attributes (on*) and \"xlink:href\" are excluded.\n */\nconst ATTR_KEY_RE = /^[a-zA-Z][a-zA-Z0-9:_-]*$/;\nconst BLOCKED_ATTR_RE = /^(on|xlink:|xml:|href$)/i;\n\n// Sync registry seeded from lucide at module load; extend at any time via registerIcons()\nconst registry = new Map<string, IconNode>(\n Object.entries((lucideModule as unknown as { icons: Record<string, IconNode> }).icons),\n);\n\n/**\n * Register additional icons (or override existing ones) by name.\n * Keys may be kebab-case or PascalCase — both are accepted by `ore-icon`.\n */\nexport function registerIcons(icons: Record<string, IconNode>): void {\n for (const [name, node] of Object.entries(icons)) {\n registry.set(name, node);\n }\n}\n\nconst toPascalCase = (value: string): string =>\n value\n .replace(/([a-z0-9])([A-Z])/g, '$1-$2')\n .split(/[^a-zA-Z0-9]+/)\n .filter(Boolean)\n .map((p) => p.charAt(0).toUpperCase() + p.slice(1).toLowerCase())\n .join('');\n\nconst parseSize = (value: string | null): number | string => {\n if (value == null || value === '') return DEFAULT_SIZE;\n\n const n = Number(value);\n\n return Number.isFinite(n) && n > 0 && /^\\d+(\\.\\d+)?$/.test(value.trim()) ? n : value;\n};\n\nconst resolveIcon = (name: string): IconNode | undefined => registry.get(name) ?? registry.get(toPascalCase(name));\n\n/**\n * Builds an allowlisted SVG child element via `createElementNS` — real DOM construction, not\n * HTML-string parsing, so this never touches Ore's `unsafeHtml()` directive (and its \"unsanitized\n * HTML\" dev warning) at all. `iconNode` entries come from the bundled Lucide icon set or\n * `registerIcons()` (a developer/build-time API, never runtime user input), but the tag/attr\n * allowlist stays as defense in depth regardless of where the data originated.\n */\nconst createSvgChild = (tag: string, attrs: Record<string, string | number | undefined>): SVGElement | null => {\n if (!ALLOWED_SVG_TAGS.has(tag)) return null;\n\n const el = document.createElementNS(SVG_NS, tag);\n\n for (const [key, value] of Object.entries(attrs)) {\n if (value === undefined) continue;\n\n if (!ATTR_KEY_RE.test(key) || BLOCKED_ATTR_RE.test(key)) continue;\n\n el.setAttribute(key, String(value));\n }\n\n return el;\n};\n\n/** Icon component properties */\nexport type OreIconProps = {\n /** Keep stroke width visually consistent when icon size changes */\n absoluteStrokeWidth?: boolean;\n /** Accessible text label. Decorative icons should omit this. */\n label?: string;\n /** Lucide icon name, e.g. `search` or `chevron-right` */\n name?: string;\n /** Icon width/height in px by default. Accepts CSS lengths. */\n size?: number | string;\n /** Render as a filled/solid shape instead of a stroked outline */\n solid?: boolean;\n /** SVG stroke width */\n strokeWidth?: number;\n};\n\n/**\n * Icon wrapper for consistent Lucide rendering across Block.\n *\n * @element ore-icon\n *\n * @attr {string} name - Lucide icon name, e.g. `search` or `chevron-right`\n * @attr {number|string} size - Width/height (default: 16)\n * @attr {number} stroke-width - SVG stroke width (default: 2)\n * @attr {boolean} absolute-stroke-width - Keep stroke width visually stable across icon sizes\n * @attr {boolean} solid - Render as filled shape instead of stroked outline\n * @attr {string} label - Accessible label; when omitted the icon is decorative\n *\n * @csspart svg - Internal SVG element\n *\n * @example\n * ```html\n * <ore-icon name=\"search\"></ore-icon>\n * <ore-icon name=\"chevron-right\" size=\"18\"></ore-icon>\n * <ore-icon name=\"trash-2\" label=\"Delete\"></ore-icon>\n * <ore-icon name=\"star\" solid></ore-icon>\n * ```\n */\nexport const ICON_TAG = 'ore-icon' as const;\ndefine<OreIconProps>(ICON_TAG, {\n props: {\n absoluteStrokeWidth: prop.bool(),\n label: prop.string(),\n name: prop.string(),\n size: { default: DEFAULT_SIZE as number | string, parse: parseSize, reflect: false },\n solid: prop.bool(),\n strokeWidth: prop.number(DEFAULT_STROKE_WIDTH),\n },\n setup(props) {\n bind({\n attr: {\n 'aria-hidden': () => ((props.label.value ?? '').trim() ? null : 'true'),\n 'aria-label': () => (props.label.value ?? '').trim() || null,\n role: () => ((props.label.value ?? '').trim() ? 'img' : null),\n },\n style: {\n '--_size-px': () => {\n const s = props.size.value;\n\n return typeof s === 'number' ? `${s}px` : String(s);\n },\n },\n });\n\n const svgRef = ref<SVGSVGElement>();\n\n // Memoized so an unknown icon name only warns once per change, not once per reactive read.\n const iconNode = computed(() => {\n const name = (props.name.value ?? '').trim();\n\n if (!name) return undefined;\n\n const node = resolveIcon(name);\n\n if (!node) warn(`ore-icon: icon not found: \"${name.slice(0, 64)}\"`);\n\n return node;\n });\n\n // Real DOM construction (createElementNS), not the unsafeHtml() directive — see createSvgChild's\n // doc comment. Rebuilds the <svg>'s attributes and children whenever the icon name, size,\n // or presentation props change, or when `svgRef` first attaches (a fresh element mounts\n // every time `when()`'s branch below flips truthy).\n watchEffect(() => {\n const svg = svgRef.value;\n const node = iconNode.value;\n\n if (!svg || !node) return;\n\n const size = props.size.value;\n const numericSize = typeof size === 'number' ? size : LUCIDE_VIEWBOX_SIZE;\n const strokeWidth =\n props.absoluteStrokeWidth.value && !props.solid.value\n ? props.strokeWidth.value! * (LUCIDE_VIEWBOX_SIZE / numericSize)\n : props.strokeWidth.value!;\n\n svg.setAttribute('fill', props.solid.value ? 'currentColor' : 'none');\n svg.setAttribute('stroke', props.solid.value ? 'none' : 'currentColor');\n svg.setAttribute('stroke-linecap', 'round');\n svg.setAttribute('stroke-linejoin', 'round');\n svg.setAttribute('stroke-width', String(props.solid.value ? 0 : strokeWidth));\n svg.setAttribute('viewBox', '0 0 24 24');\n\n const children = node.map(([tag, attrs]) => createSvgChild(tag, attrs)).filter((el): el is SVGElement => !!el);\n\n svg.replaceChildren(...children);\n });\n\n return html`\n ${when(\n () => !!iconNode.value,\n () => html`\n <svg ref=\"${svgRef}\" part=\"svg\" style=\"width:100%;height:100%\"></svg>\n `,\n )}\n `;\n },\n styles: [styles],\n});\n"],"mappings":"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"}
|
package/dist/icon.js
CHANGED
|
@@ -32,7 +32,7 @@ var y = (e) => e.replace(/([a-z0-9])([A-Z])/g, "$1-$2").split(/[^a-zA-Z0-9]+/).f
|
|
|
32
32
|
}, x = (e) => _.get(e) ?? _.get(y(e)), S = (e, t) => {
|
|
33
33
|
if (!m.has(e)) return null;
|
|
34
34
|
let n = document.createElementNS(p, e);
|
|
35
|
-
for (let [e, r] of Object.entries(t)) r !== void 0 &&
|
|
35
|
+
for (let [e, r] of Object.entries(t)) r !== void 0 && h.test(e) && !g.test(e) && n.setAttribute(e, String(r));
|
|
36
36
|
return n;
|
|
37
37
|
}, C = "ore-icon";
|
|
38
38
|
n(C, {
|
package/dist/icon.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"icon.js","names":[],"sources":["../src/content/icon/icon.ts"],"sourcesContent":["import { bind, define, html, prop, ref, watchEffect, when } from '@vielzeug/ore';\nimport { computed } from '@vielzeug/ripple';\nimport * as lucideModule from 'lucide';\n\nimport { warn } from '../../_dev';\nimport styles from './icon.css?inline';\n\nexport type IconNode = Array<[string, Record<string, string | number | undefined>]>;\n\nconst DEFAULT_SIZE = 16;\nconst DEFAULT_STROKE_WIDTH = 2;\nconst LUCIDE_VIEWBOX_SIZE = 24;\nconst SVG_NS = 'http://www.w3.org/2000/svg';\n\n/**\n * Allowlisted SVG shape/structure element names. Only these tags are permitted\n * when building the SVG DOM from an IconNode, preventing arbitrary element\n * injection (e.g. <script>, event-handler-bearing elements).\n */\nconst ALLOWED_SVG_TAGS = new Set([\n 'circle',\n 'clipPath',\n 'defs',\n 'ellipse',\n 'g',\n 'line',\n 'linearGradient',\n 'mask',\n 'path',\n 'polygon',\n 'polyline',\n 'radialGradient',\n 'rect',\n 'stop',\n 'symbol',\n 'use',\n]);\n\n/**\n * Allowlisted SVG presentation / structural attribute names.\n * Event handler attributes (on*) and \"xlink:href\" are excluded.\n */\nconst ATTR_KEY_RE = /^[a-zA-Z][a-zA-Z0-9:_-]*$/;\nconst BLOCKED_ATTR_RE = /^(on|xlink:|xml:|href$)/i;\n\n// Sync registry seeded from lucide at module load; extend at any time via registerIcons()\nconst registry = new Map<string, IconNode>(\n Object.entries((lucideModule as unknown as { icons: Record<string, IconNode> }).icons),\n);\n\n/**\n * Register additional icons (or override existing ones) by name.\n * Keys may be kebab-case or PascalCase — both are accepted by `ore-icon`.\n */\nexport function registerIcons(icons: Record<string, IconNode>): void {\n for (const [name, node] of Object.entries(icons)) {\n registry.set(name, node);\n }\n}\n\nconst toPascalCase = (value: string): string =>\n value\n .replace(/([a-z0-9])([A-Z])/g, '$1-$2')\n .split(/[^a-zA-Z0-9]+/)\n .filter(Boolean)\n .map((p) => p.charAt(0).toUpperCase() + p.slice(1).toLowerCase())\n .join('');\n\nconst parseSize = (value: string | null): number | string => {\n if (value == null || value === '') return DEFAULT_SIZE;\n\n const n = Number(value);\n\n return Number.isFinite(n) && n > 0 && /^\\d+(\\.\\d+)?$/.test(value.trim()) ? n : value;\n};\n\nconst resolveIcon = (name: string): IconNode | undefined => registry.get(name) ?? registry.get(toPascalCase(name));\n\n/**\n * Builds an allowlisted SVG child element via `createElementNS` — real DOM construction, not\n * HTML-string parsing, so this never touches Ore's `unsafeHtml()` directive (and its \"unsanitized\n * HTML\" dev warning) at all. `iconNode` entries come from the bundled Lucide icon set or\n * `registerIcons()` (a developer/build-time API, never runtime user input), but the tag/attr\n * allowlist stays as defense in depth regardless of where the data originated.\n */\nconst createSvgChild = (tag: string, attrs: Record<string, string | number | undefined>): SVGElement | null => {\n if (!ALLOWED_SVG_TAGS.has(tag)) return null;\n\n const el = document.createElementNS(SVG_NS, tag);\n\n for (const [key, value] of Object.entries(attrs)) {\n if (value === undefined) continue;\n\n if (!ATTR_KEY_RE.test(key) || BLOCKED_ATTR_RE.test(key)) continue;\n\n el.setAttribute(key, String(value));\n }\n\n return el;\n};\n\n/** Icon component properties */\nexport type OreIconProps = {\n /** Keep stroke width visually consistent when icon size changes */\n absoluteStrokeWidth?: boolean;\n /** Accessible text label. Decorative icons should omit this. */\n label?: string;\n /** Lucide icon name, e.g. `search` or `chevron-right` */\n name?: string;\n /** Icon width/height in px by default. Accepts CSS lengths. */\n size?: number | string;\n /** Render as a filled/solid shape instead of a stroked outline */\n solid?: boolean;\n /** SVG stroke width */\n strokeWidth?: number;\n};\n\n/**\n * Icon wrapper for consistent Lucide rendering across Block.\n *\n * @element ore-icon\n *\n * @attr {string} name - Lucide icon name, e.g. `search` or `chevron-right`\n * @attr {number|string} size - Width/height (default: 16)\n * @attr {number} stroke-width - SVG stroke width (default: 2)\n * @attr {boolean} absolute-stroke-width - Keep stroke width visually stable across icon sizes\n * @attr {boolean} solid - Render as filled shape instead of stroked outline\n * @attr {string} label - Accessible label; when omitted the icon is decorative\n *\n * @csspart svg - Internal SVG element\n *\n * @example\n * ```html\n * <ore-icon name=\"search\"></ore-icon>\n * <ore-icon name=\"chevron-right\" size=\"18\"></ore-icon>\n * <ore-icon name=\"trash-2\" label=\"Delete\"></ore-icon>\n * <ore-icon name=\"star\" solid></ore-icon>\n * ```\n */\nexport const ICON_TAG = 'ore-icon' as const;\ndefine<OreIconProps>(ICON_TAG, {\n props: {\n absoluteStrokeWidth: prop.bool(),\n label: prop.string(),\n name: prop.string(),\n size: { default: DEFAULT_SIZE as number | string, parse: parseSize, reflect: false },\n solid: prop.bool(),\n strokeWidth: prop.number(DEFAULT_STROKE_WIDTH),\n },\n setup(props) {\n bind({\n attr: {\n 'aria-hidden': () => ((props.label.value ?? '').trim() ? null : 'true'),\n 'aria-label': () => (props.label.value ?? '').trim() || null,\n role: () => ((props.label.value ?? '').trim() ? 'img' : null),\n },\n style: {\n '--_size-px': () => {\n const s = props.size.value;\n\n return typeof s === 'number' ? `${s}px` : String(s);\n },\n },\n });\n\n const svgRef = ref<SVGSVGElement>();\n\n // Memoized so an unknown icon name only warns once per change, not once per reactive read.\n const iconNode = computed(() => {\n const name = (props.name.value ?? '').trim();\n\n if (!name) return undefined;\n\n const node = resolveIcon(name);\n\n if (!node) warn(`ore-icon: icon not found: \"${name.slice(0, 64)}\"`);\n\n return node;\n });\n\n // Real DOM construction (createElementNS), not the unsafeHtml() directive — see createSvgChild's\n // doc comment. Rebuilds the <svg>'s attributes and children whenever the icon name, size,\n // or presentation props change, or when `svgRef` first attaches (a fresh element mounts\n // every time `when()`'s branch below flips truthy).\n watchEffect(() => {\n const svg = svgRef.value;\n const node = iconNode.value;\n\n if (!svg || !node) return;\n\n const size = props.size.value;\n const numericSize = typeof size === 'number' ? size : LUCIDE_VIEWBOX_SIZE;\n const strokeWidth =\n props.absoluteStrokeWidth.value && !props.solid.value\n ? props.strokeWidth.value! * (LUCIDE_VIEWBOX_SIZE / numericSize)\n : props.strokeWidth.value!;\n\n svg.setAttribute('fill', props.solid.value ? 'currentColor' : 'none');\n svg.setAttribute('stroke', props.solid.value ? 'none' : 'currentColor');\n svg.setAttribute('stroke-linecap', 'round');\n svg.setAttribute('stroke-linejoin', 'round');\n svg.setAttribute('stroke-width', String(props.solid.value ? 0 : strokeWidth));\n svg.setAttribute('viewBox', '0 0 24 24');\n\n const children = node.map(([tag, attrs]) => createSvgChild(tag, attrs)).filter((el): el is SVGElement => !!el);\n\n svg.replaceChildren(...children);\n });\n\n return html`\n ${when(\n () => !!iconNode.value,\n () => html`\n <svg ref=\"${svgRef}\" part=\"svg\" style=\"width:100%;height:100%\"></svg>\n `,\n )}\n `;\n },\n styles: [styles],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"icon.js","names":[],"sources":["../src/content/icon/icon.ts"],"sourcesContent":["import { bind, define, html, prop, ref, watchEffect, when } from '@vielzeug/ore';\nimport { computed } from '@vielzeug/ripple';\nimport * as lucideModule from 'lucide';\n\nimport { warn } from '../../_dev';\nimport styles from './icon.css?inline';\n\nexport type IconNode = Array<[string, Record<string, string | number | undefined>]>;\n\nconst DEFAULT_SIZE = 16;\nconst DEFAULT_STROKE_WIDTH = 2;\nconst LUCIDE_VIEWBOX_SIZE = 24;\nconst SVG_NS = 'http://www.w3.org/2000/svg';\n\n/**\n * Allowlisted SVG shape/structure element names. Only these tags are permitted\n * when building the SVG DOM from an IconNode, preventing arbitrary element\n * injection (e.g. <script>, event-handler-bearing elements).\n */\nconst ALLOWED_SVG_TAGS = new Set([\n 'circle',\n 'clipPath',\n 'defs',\n 'ellipse',\n 'g',\n 'line',\n 'linearGradient',\n 'mask',\n 'path',\n 'polygon',\n 'polyline',\n 'radialGradient',\n 'rect',\n 'stop',\n 'symbol',\n 'use',\n]);\n\n/**\n * Allowlisted SVG presentation / structural attribute names.\n * Event handler attributes (on*) and \"xlink:href\" are excluded.\n */\nconst ATTR_KEY_RE = /^[a-zA-Z][a-zA-Z0-9:_-]*$/;\nconst BLOCKED_ATTR_RE = /^(on|xlink:|xml:|href$)/i;\n\n// Sync registry seeded from lucide at module load; extend at any time via registerIcons()\nconst registry = new Map<string, IconNode>(\n Object.entries((lucideModule as unknown as { icons: Record<string, IconNode> }).icons),\n);\n\n/**\n * Register additional icons (or override existing ones) by name.\n * Keys may be kebab-case or PascalCase — both are accepted by `ore-icon`.\n */\nexport function registerIcons(icons: Record<string, IconNode>): void {\n for (const [name, node] of Object.entries(icons)) {\n registry.set(name, node);\n }\n}\n\nconst toPascalCase = (value: string): string =>\n value\n .replace(/([a-z0-9])([A-Z])/g, '$1-$2')\n .split(/[^a-zA-Z0-9]+/)\n .filter(Boolean)\n .map((p) => p.charAt(0).toUpperCase() + p.slice(1).toLowerCase())\n .join('');\n\nconst parseSize = (value: string | null): number | string => {\n if (value == null || value === '') return DEFAULT_SIZE;\n\n const n = Number(value);\n\n return Number.isFinite(n) && n > 0 && /^\\d+(\\.\\d+)?$/.test(value.trim()) ? n : value;\n};\n\nconst resolveIcon = (name: string): IconNode | undefined => registry.get(name) ?? registry.get(toPascalCase(name));\n\n/**\n * Builds an allowlisted SVG child element via `createElementNS` — real DOM construction, not\n * HTML-string parsing, so this never touches Ore's `unsafeHtml()` directive (and its \"unsanitized\n * HTML\" dev warning) at all. `iconNode` entries come from the bundled Lucide icon set or\n * `registerIcons()` (a developer/build-time API, never runtime user input), but the tag/attr\n * allowlist stays as defense in depth regardless of where the data originated.\n */\nconst createSvgChild = (tag: string, attrs: Record<string, string | number | undefined>): SVGElement | null => {\n if (!ALLOWED_SVG_TAGS.has(tag)) return null;\n\n const el = document.createElementNS(SVG_NS, tag);\n\n for (const [key, value] of Object.entries(attrs)) {\n if (value === undefined) continue;\n\n if (!ATTR_KEY_RE.test(key) || BLOCKED_ATTR_RE.test(key)) continue;\n\n el.setAttribute(key, String(value));\n }\n\n return el;\n};\n\n/** Icon component properties */\nexport type OreIconProps = {\n /** Keep stroke width visually consistent when icon size changes */\n absoluteStrokeWidth?: boolean;\n /** Accessible text label. Decorative icons should omit this. */\n label?: string;\n /** Lucide icon name, e.g. `search` or `chevron-right` */\n name?: string;\n /** Icon width/height in px by default. Accepts CSS lengths. */\n size?: number | string;\n /** Render as a filled/solid shape instead of a stroked outline */\n solid?: boolean;\n /** SVG stroke width */\n strokeWidth?: number;\n};\n\n/**\n * Icon wrapper for consistent Lucide rendering across Block.\n *\n * @element ore-icon\n *\n * @attr {string} name - Lucide icon name, e.g. `search` or `chevron-right`\n * @attr {number|string} size - Width/height (default: 16)\n * @attr {number} stroke-width - SVG stroke width (default: 2)\n * @attr {boolean} absolute-stroke-width - Keep stroke width visually stable across icon sizes\n * @attr {boolean} solid - Render as filled shape instead of stroked outline\n * @attr {string} label - Accessible label; when omitted the icon is decorative\n *\n * @csspart svg - Internal SVG element\n *\n * @example\n * ```html\n * <ore-icon name=\"search\"></ore-icon>\n * <ore-icon name=\"chevron-right\" size=\"18\"></ore-icon>\n * <ore-icon name=\"trash-2\" label=\"Delete\"></ore-icon>\n * <ore-icon name=\"star\" solid></ore-icon>\n * ```\n */\nexport const ICON_TAG = 'ore-icon' as const;\ndefine<OreIconProps>(ICON_TAG, {\n props: {\n absoluteStrokeWidth: prop.bool(),\n label: prop.string(),\n name: prop.string(),\n size: { default: DEFAULT_SIZE as number | string, parse: parseSize, reflect: false },\n solid: prop.bool(),\n strokeWidth: prop.number(DEFAULT_STROKE_WIDTH),\n },\n setup(props) {\n bind({\n attr: {\n 'aria-hidden': () => ((props.label.value ?? '').trim() ? null : 'true'),\n 'aria-label': () => (props.label.value ?? '').trim() || null,\n role: () => ((props.label.value ?? '').trim() ? 'img' : null),\n },\n style: {\n '--_size-px': () => {\n const s = props.size.value;\n\n return typeof s === 'number' ? `${s}px` : String(s);\n },\n },\n });\n\n const svgRef = ref<SVGSVGElement>();\n\n // Memoized so an unknown icon name only warns once per change, not once per reactive read.\n const iconNode = computed(() => {\n const name = (props.name.value ?? '').trim();\n\n if (!name) return undefined;\n\n const node = resolveIcon(name);\n\n if (!node) warn(`ore-icon: icon not found: \"${name.slice(0, 64)}\"`);\n\n return node;\n });\n\n // Real DOM construction (createElementNS), not the unsafeHtml() directive — see createSvgChild's\n // doc comment. Rebuilds the <svg>'s attributes and children whenever the icon name, size,\n // or presentation props change, or when `svgRef` first attaches (a fresh element mounts\n // every time `when()`'s branch below flips truthy).\n watchEffect(() => {\n const svg = svgRef.value;\n const node = iconNode.value;\n\n if (!svg || !node) return;\n\n const size = props.size.value;\n const numericSize = typeof size === 'number' ? size : LUCIDE_VIEWBOX_SIZE;\n const strokeWidth =\n props.absoluteStrokeWidth.value && !props.solid.value\n ? props.strokeWidth.value! * (LUCIDE_VIEWBOX_SIZE / numericSize)\n : props.strokeWidth.value!;\n\n svg.setAttribute('fill', props.solid.value ? 'currentColor' : 'none');\n svg.setAttribute('stroke', props.solid.value ? 'none' : 'currentColor');\n svg.setAttribute('stroke-linecap', 'round');\n svg.setAttribute('stroke-linejoin', 'round');\n svg.setAttribute('stroke-width', String(props.solid.value ? 0 : strokeWidth));\n svg.setAttribute('viewBox', '0 0 24 24');\n\n const children = node.map(([tag, attrs]) => createSvgChild(tag, attrs)).filter((el): el is SVGElement => !!el);\n\n svg.replaceChildren(...children);\n });\n\n return html`\n ${when(\n () => !!iconNode.value,\n () => html`\n <svg ref=\"${svgRef}\" part=\"svg\" style=\"width:100%;height:100%\"></svg>\n `,\n )}\n `;\n },\n styles: [styles],\n});\n"],"mappings":"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"}
|
package/dist/input.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./styles/mixins/accessibility.css.cjs"),t=require("./styles/mixins/field-variants.css.cjs"),n=require("./styles/mixins/shape.css.cjs"),r=require("./styles/mixins/states.css.cjs"),i=require("./styles/mixins/theme.css.cjs"),a=require("./core/ore.cjs"),o=require("./core/ref-callback.cjs"),s=require("./core/text-field.cjs"),c=require("./shared/prop-bundles.cjs"),l=require("./shared/size-presets.cjs"),u=require("./inputs/shared/field-binding.cjs"),d=require("./inputs/shared/templates.cjs"),f=require("./inputs/shared/native-field-event.cjs"),p=require("./inputs/input/input.css.cjs");let m=require("@vielzeug/ore"),h=require("@vielzeug/ripple");var g=[`text`,`email`,`password`,`search`,`url`,`tel`,`number`,`date`,`time`,`datetime-local`,`month`,`week`],_=`ore-input`;(0,m.define)(_,{formAssociated:!0,props:{...c.themableBundle,...c.sizableBundle,...c.disablableBundle,...c.roundableBundle,autocomplete:m.prop.string(),clearable:m.prop.bool(!1),error:m.prop.string(),fullwidth:m.prop.bool(!1),helper:m.prop.string(),inputmode:m.prop.string(),label:m.prop.string(),"label-placement":m.prop.oneOf([`inset`,`outside`],`inset`),loading:m.prop.bool(!1),maxlength:m.prop.json(void 0),minlength:m.prop.json(void 0),name:m.prop.string(),pattern:m.prop.string(),placeholder:m.prop.string(),readonly:m.prop.bool(!1),ref:m.prop.data(),required:m.prop.bool(!1),success:m.prop.bool(!1),type:m.prop.oneOf(g,`text`),value:m.prop.string(),variant:m.prop.string()},setup(e){let t=(0,m.getHost)(),n=(0,m.useSlots)(),r=(0,h.signal)(!1),i=(0,m.ref)(),c=(0,h.computed)(()=>!!e.label.value||n.has(`label`).value),l=(0,h.computed)(()=>e.disabled.value||e.loading.value),p=a.lifecycleSignal(m.onCleanup),g=s.createTextField({disabled:l,error:e.error,hasLabel:c,helper:e.helper,label:e.label,labelPlacement:e[`label-placement`],maxLength:e.maxlength,onChange:(e,n)=>{f.setFieldValue(t,n),f.dispatchNativeFieldEvent(t,`change`)},onInput:(e,n)=>{f.setFieldValue(t,n),f.dispatchNativeFieldEvent(t,`input`)},prefix:`input`,readonly:e.readonly,required:e.required,signal:p,value:e.value});f.defineFieldValue(t,()=>g.value.value,e=>{g.value.value=e}),g.attachFormField((0,m.useField)({disabled:g.disabled,onReset:g.reset,toFormValue:e=>e,validationMessage:g.validationMessage,validity:g.validity,value:g.value}));let{ariaDescribedBy:_,ariaErrorMessage:v,ariaInvalid:y,ariaLabelledBy:b,assistiveId:x,clear:S,counter:C,errorId:w,errorText:T,fieldId:E,helperText:D,labelId:O,labelVisible:k,value:A,wire:j}=g;
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./styles/mixins/accessibility.css.cjs"),t=require("./styles/mixins/field-variants.css.cjs"),n=require("./styles/mixins/shape.css.cjs"),r=require("./styles/mixins/states.css.cjs"),i=require("./styles/mixins/theme.css.cjs"),a=require("./core/ore.cjs"),o=require("./core/ref-callback.cjs"),s=require("./core/text-field.cjs"),c=require("./shared/prop-bundles.cjs"),l=require("./shared/size-presets.cjs"),u=require("./inputs/shared/field-binding.cjs"),d=require("./inputs/shared/templates.cjs"),f=require("./inputs/shared/native-field-event.cjs"),p=require("./inputs/input/input.css.cjs");let m=require("@vielzeug/ore"),h=require("@vielzeug/ripple");var g=[`text`,`email`,`password`,`search`,`url`,`tel`,`number`,`date`,`time`,`datetime-local`,`month`,`week`],_=`ore-input`;(0,m.define)(_,{formAssociated:!0,props:{...c.themableBundle,...c.sizableBundle,...c.disablableBundle,...c.roundableBundle,"aria-label":m.prop.string(),autocomplete:m.prop.string(),clearable:m.prop.bool(!1),error:m.prop.string(),fullwidth:m.prop.bool(!1),helper:m.prop.string(),inputmode:m.prop.string(),label:m.prop.string(),"label-placement":m.prop.oneOf([`inset`,`outside`],`inset`),loading:m.prop.bool(!1),maxlength:m.prop.json(void 0),minlength:m.prop.json(void 0),name:m.prop.string(),pattern:m.prop.string(),placeholder:m.prop.string(),readonly:m.prop.bool(!1),ref:m.prop.data(),required:m.prop.bool(!1),success:m.prop.bool(!1),type:m.prop.oneOf(g,`text`),value:m.prop.string(),variant:m.prop.string()},setup(e){let t=(0,m.getHost)(),n=(0,m.useSlots)(),r=(0,h.signal)(!1),i=(0,m.ref)(),c=(0,h.computed)(()=>!!e.label.value||n.has(`label`).value),l=(0,h.computed)(()=>e.disabled.value||e.loading.value),p=a.lifecycleSignal(m.onCleanup),g=s.createTextField({disabled:l,error:e.error,hasLabel:c,helper:e.helper,label:e.label,labelPlacement:e[`label-placement`],maxLength:e.maxlength,onChange:(e,n)=>{f.setFieldValue(t,n),f.dispatchNativeFieldEvent(t,`change`)},onInput:(e,n)=>{f.setFieldValue(t,n),f.dispatchNativeFieldEvent(t,`input`)},prefix:`input`,readonly:e.readonly,required:e.required,signal:p,value:e.value});f.defineFieldValue(t,()=>g.value.value,e=>{g.value.value=e}),g.attachFormField((0,m.useField)({disabled:g.disabled,onReset:g.reset,toFormValue:e=>e,validationMessage:g.validationMessage,validity:g.validity,value:g.value}));let{ariaDescribedBy:_,ariaErrorMessage:v,ariaInvalid:y,ariaLabelledBy:b,assistiveId:x,clear:S,counter:C,errorId:w,errorText:T,fieldId:E,helperText:D,labelId:O,labelVisible:k,value:A,wire:j}=g;(0,m.onElement)(i,t=>{j(t,p);let n=o.bindRefCallback(e.ref,t);return()=>{n()}});let M=e=>{S(e),i.value?.focus()},N=()=>e.type.value===`password`&&r.value?`text`:e.type.value??`text`;(0,m.bind)({attr:{error:u.errorAttr(T),"has-value":()=>A.value?!0:void 0,size:e.size,success:()=>e.success.value&&!T.value?!0:void 0,variant:e.variant}});let P=()=>!k.value;return m.html`
|
|
2
2
|
<div class="input-wrapper" part="wrapper">
|
|
3
|
-
<label class="label" for="${E}" id="${O}" part="label" ?hidden="${
|
|
3
|
+
<label class="label" for="${()=>P()?null:E}" id="${O}" part="label" ?hidden="${P}">
|
|
4
4
|
<slot name="label">${e.label}</slot>
|
|
5
5
|
</label>
|
|
6
6
|
<div class="field" part="field">
|
|
@@ -9,7 +9,7 @@ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=requi
|
|
|
9
9
|
<input
|
|
10
10
|
part="input"
|
|
11
11
|
id="${E}"
|
|
12
|
-
type="${
|
|
12
|
+
type="${N}"
|
|
13
13
|
name="${e.name}"
|
|
14
14
|
placeholder="${e.placeholder}"
|
|
15
15
|
autocomplete="${e.autocomplete}"
|
|
@@ -21,6 +21,7 @@ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=requi
|
|
|
21
21
|
?readonly="${e.readonly}"
|
|
22
22
|
?required="${e.required}"
|
|
23
23
|
value="${(0,m.live)(A)}"
|
|
24
|
+
aria-label="${e[`aria-label`]}"
|
|
24
25
|
aria-labelledby="${b}"
|
|
25
26
|
aria-describedby="${_}"
|
|
26
27
|
aria-errormessage="${v}"
|
|
@@ -50,7 +51,7 @@ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=requi
|
|
|
50
51
|
part="clear"
|
|
51
52
|
type="button"
|
|
52
53
|
tabindex="${()=>A.value?`0`:`-1`}"
|
|
53
|
-
@click="${
|
|
54
|
+
@click="${M}">
|
|
54
55
|
<ore-icon aria-hidden="true" name="x" size="12" stroke-width="2.5"></ore-icon>
|
|
55
56
|
</button>
|
|
56
57
|
</div>
|
package/dist/input.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input.cjs","names":[],"sources":["../src/inputs/input/input.ts"],"sourcesContent":["import { bind, define, getHost, html, live, onCleanup, onElement, prop, ref, useField, useSlots } from '@vielzeug/ore';\nimport { computed, signal } from '@vielzeug/ripple';\nimport { bindRefCallback, createTextField, lifecycleSignal } from '../../core';\nimport type { TextFieldProps } from '../../shared';\nimport { disablableBundle, FIELD_SIZE_PRESET, roundableBundle, sizableBundle, themableBundle } from '../../shared';\nimport type { InputType, VisualVariant } from '../../types';\nimport '../../content/icon/icon';\nimport {\n coarsePointerMixin,\n colorThemeMixin,\n disabledLoadingMixin,\n fieldVariantMixin,\n forcedColorsFocusMixin,\n reducedMotionMixin,\n roundedVariantMixin,\n sizeVariantMixin,\n} from '../../styles';\nimport { errorAttr } from '../shared/field-binding';\nimport { defineFieldValue, dispatchNativeFieldEvent, setFieldValue } from '../shared/native-field-event';\nimport { renderStatusIcon } from '../shared/templates';\nimport componentStyles from './input.css?inline';\n\n/** Input component properties */\n\nexport type OreInputEvents = {\n change: Event;\n input: InputEvent;\n};\n\nexport type OreInputProps = TextFieldProps<Exclude<VisualVariant, 'frost'>> & {\n /** Autocomplete hint */\n autocomplete?: string;\n /** Show a clear (×) button when the field has a value */\n clearable?: boolean;\n /** Virtual keyboard hint for mobile devices */\n inputmode?: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url';\n /**\n * Shows an inline spinner inside the field and forces the inner `<input>` into\n * `disabled` for the duration — use while an async validation/submission request\n * is in flight (e.g. checking username availability) to prevent double-submits.\n */\n loading?: boolean;\n /** Maximum character length — shows a counter below the input */\n maxlength?: number;\n /** Minimum character length */\n minlength?: number;\n /** HTML pattern attribute for client-side validation */\n pattern?: string;\n /**\n * JS-only callback fired with the inner `<input>` element when it mounts,\n * and with `null` when it unmounts. Intended for composed components that\n * need imperative access to the raw input element.\n * Set as a JS property: `bitInput.ref = (el) => { ... }`.\n */\n ref?: ((el: HTMLInputElement | null) => void) | null;\n /** HTML input type */\n type?: InputType;\n};\n\nconst VALID_INPUT_TYPES = [\n 'text',\n 'email',\n 'password',\n 'search',\n 'url',\n 'tel',\n 'number',\n 'date',\n 'time',\n 'datetime-local',\n 'month',\n 'week',\n] as const;\n\n/**\n * A customizable text input component with multiple variants, label placements, and form features.\n *\n * @element ore-input\n *\n * @attr {string} label - Label text\n * @attr {string} label-placement - Label placement: 'inset' | 'outside'\n * @attr {string} type - HTML input type: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'search'\n * @attr {string} value - Current input value\n * @attr {string} placeholder - Placeholder text\n * @attr {string} name - Form field name\n * @attr {string} helper - Helper text displayed below the input (fallback when the `helper` slot is empty)\n * @attr {string} error - Error message — marks the field as invalid (fallback when the `error` slot is empty)\n * @attr {boolean} disabled - Disable input interaction\n * @attr {boolean} readonly - Make the input read-only\n * @attr {boolean} required - Mark the field as required\n * @attr {boolean} loading - Show an inline spinner and force the field disabled\n * @attr {boolean} success - Show an inline success check icon (suppressed while `error` is set)\n * @attr {string} color - Theme color: 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'\n * @attr {string} variant - Visual variant: 'solid' | 'flat' | 'bordered' | 'outline' | 'ghost' | 'text'\n * @attr {string} size - Input size: 'sm' | 'md' | 'lg'\n * @attr {string} rounded - Border radius: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full'\n *\n * @fires input - Emitted when input value changes (on every keystroke).\n * @fires change - Emitted when input loses focus with changed value.\n *\n * @slot prefix - Content before the input (e.g., icons)\n * @slot suffix - Content after the input (e.g., clear button, validation icon)\n * @slot label - Replaces the label text — slotted content takes precedence over the `label` prop\n * @slot helper - Replaces the helper text — slotted content takes precedence over the `helper` prop\n * @slot error - Replaces the error text — slotted content takes precedence over the `error` prop\n *\n * @part wrapper - The input wrapper element\n * @part label - The label element (inset or outside)\n * @part field - The field container element\n * @part input-row - The input row container element\n * @part input - The input element\n * @part status-icon - The inline error/success icon shown inside the field\n * @part spinner - The inline loading spinner shown inside the field while `loading`\n * @part helper - The helper text element\n *\n * @cssprop --input-bg - Background color\n * @cssprop --input-color - Text color\n * @cssprop --input-border-color - Border color\n * @cssprop --input-placeholder-color - Placeholder text color\n * @cssprop --input-radius - Border radius\n * @cssprop --input-padding - Inner padding (block inline)\n * @cssprop --input-label-block-inset - Inset from the field block start for an inset label\n * @cssprop --input-label-inset - Inset from the field inline start for an inset label\n * @cssprop --input-gap - Gap between prefix/suffix icons and input text\n * @cssprop --input-font-size - Font size\n * @cssprop --input-height - Field height\n * @cssprop --input-hover-bg - Field background on hover (flat/ghost variants)\n * @cssprop --input-hover-border-color - Field border on hover (flat/bordered variants)\n * @cssprop --input-focus-bg - Field background when focused (flat variant)\n * @cssprop --input-focus-border-color - Field border when focused (flat/text variants)\n *\n * @example\n * ```html\n * <ore-input type=\"email\" label=\"Email\" placeholder=\"you@example.com\" />\n * <ore-input label=\"Name\" variant=\"bordered\" color=\"primary\" />\n * <ore-input label=\"Username\" error=\"That username is taken\" />\n * <ore-input label=\"Username\" success />\n * <ore-input label=\"Username\" loading />\n * ```\n */\nexport const INPUT_TAG = 'ore-input' as const;\ndefine<OreInputProps>(INPUT_TAG, {\n formAssociated: true,\n props: {\n ...themableBundle,\n ...sizableBundle,\n ...disablableBundle,\n ...roundableBundle,\n autocomplete: prop.string(),\n clearable: prop.bool(false),\n error: prop.string(),\n fullwidth: prop.bool(false),\n helper: prop.string(),\n inputmode: prop.string<'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url'>(),\n label: prop.string(),\n 'label-placement': prop.oneOf(['inset', 'outside'] as const, 'inset'),\n loading: prop.bool(false),\n maxlength: prop.json(undefined as number | undefined),\n minlength: prop.json(undefined as number | undefined),\n name: prop.string(),\n pattern: prop.string(),\n placeholder: prop.string(),\n readonly: prop.bool(false),\n ref: prop.data<((el: HTMLInputElement | null) => void) | null>(),\n required: prop.bool(false),\n success: prop.bool(false),\n type: prop.oneOf(VALID_INPUT_TYPES, 'text'),\n value: prop.string(),\n variant: prop.string<'flat' | 'text' | 'solid' | 'bordered' | 'outline' | 'ghost'>(),\n },\n setup(props) {\n const el = getHost();\n const slots = useSlots();\n\n const showPassword = signal(false);\n const inputRef = ref<HTMLInputElement>();\n\n const hasLabel = computed(() => !!props.label.value || slots.has('label').value);\n // `loading` behaves like a temporary `disabled` — forces the inner <input> out of\n // constraint validation and interaction for the duration, layered on top of any real\n // `loading` is layered on top of the consumer's explicit `disabled` prop.\n const isDisabled = computed(() => props.disabled.value || props.loading.value);\n\n const abortSignal = lifecycleSignal(onCleanup);\n const tf = createTextField({\n disabled: isDisabled,\n error: props.error,\n hasLabel,\n helper: props.helper,\n label: props.label,\n labelPlacement: props['label-placement'],\n maxLength: props.maxlength,\n onChange: (_event: Event, value: string) => {\n setFieldValue(el, value);\n dispatchNativeFieldEvent(el, 'change');\n },\n onInput: (_event: Event, value: string) => {\n setFieldValue(el, value);\n dispatchNativeFieldEvent(el, 'input');\n },\n prefix: 'input',\n readonly: props.readonly,\n required: props.required,\n signal: abortSignal,\n value: props.value,\n });\n\n defineFieldValue(\n el,\n () => tf.value.value,\n (value) => {\n tf.value.value = value;\n },\n );\n\n tf.attachFormField(\n useField<string>({\n disabled: tf.disabled,\n onReset: tf.reset,\n toFormValue: (v) => v,\n validationMessage: tf.validationMessage,\n validity: tf.validity,\n value: tf.value,\n }),\n );\n\n const {\n ariaDescribedBy,\n ariaErrorMessage,\n ariaInvalid,\n ariaLabelledBy,\n assistiveId,\n clear: clearValue,\n counter,\n errorId,\n errorText,\n fieldId: inputId,\n helperText,\n labelId,\n labelVisible,\n value: fieldValue,\n wire,\n } = tf;\n\n onElement(inputRef, (el) => {\n wire(el, abortSignal);\n\n const unwireRef = bindRefCallback(props.ref, el);\n\n return () => {\n unwireRef();\n };\n });\n\n const clear = (event?: Event): void => {\n clearValue(event);\n inputRef.value?.focus();\n };\n\n const resolvedInputType = (): string =>\n props.type.value === 'password' && showPassword.value ? 'text' : (props.type.value ?? 'text');\n\n bind({\n attr: {\n error: errorAttr(errorText),\n 'has-value': () => (fieldValue.value ? true : undefined),\n size: props.size,\n // Reflects `success` only once `error` is confirmed empty — keeps the two host\n // attributes mutually exclusive even if a consumer sets both props at once.\n success: () => (props.success.value && !errorText.value ? true : undefined),\n variant: props.variant,\n },\n });\n\n const labelHidden = () => !labelVisible.value;\n const passwordToggleLabel = () => (showPassword.value ? 'Hide password' : 'Show password');\n const passwordTogglePressed = () => String(showPassword.value);\n const passwordToggleIcon = () =>\n showPassword.value\n ? html`\n <ore-icon name=\"eye-off\" size=\"14\" stroke-width=\"2\" aria-hidden=\"true\"></ore-icon>\n `\n : html`\n <ore-icon name=\"eye\" size=\"14\" stroke-width=\"2\" aria-hidden=\"true\"></ore-icon>\n `;\n const helperHidden = () => !!errorText.value || !helperText.value;\n const errorHidden = () => !errorText.value;\n const counterNearLimit = () => (counter?.value.counterNearLimit && !counter?.value.counterAtLimit ? '' : null);\n const counterAtLimit = () => (counter?.value.counterAtLimit ? '' : null);\n const counterHidden = () => !counter;\n const counterText = () => counter?.value.counterText ?? '';\n\n const clearTabIndex = () => (fieldValue.value ? '0' : '-1');\n const pwdToggleTabIndex = () => (props.type.value === 'password' ? '0' : '-1');\n\n const togglePassword = () => {\n showPassword.value = !showPassword.value;\n inputRef.value?.focus();\n };\n\n return html`\n <div class=\"input-wrapper\" part=\"wrapper\">\n <label class=\"label\" for=\"${inputId}\" id=\"${labelId}\" part=\"label\" ?hidden=\"${labelHidden}\">\n <slot name=\"label\">${props.label}</slot>\n </label>\n <div class=\"field\" part=\"field\">\n <div class=\"input-row\" part=\"input-row\">\n <slot name=\"prefix\"></slot>\n <input\n part=\"input\"\n id=\"${inputId}\"\n type=\"${resolvedInputType}\"\n name=\"${props.name}\"\n placeholder=\"${props.placeholder}\"\n autocomplete=\"${props.autocomplete}\"\n inputmode=\"${props.inputmode}\"\n maxlength=\"${props.maxlength}\"\n minlength=\"${props.minlength}\"\n pattern=\"${props.pattern}\"\n ?disabled=\"${isDisabled}\"\n ?readonly=\"${props.readonly}\"\n ?required=\"${props.required}\"\n value=\"${live(fieldValue)}\"\n aria-labelledby=\"${ariaLabelledBy}\"\n aria-describedby=\"${ariaDescribedBy}\"\n aria-errormessage=\"${ariaErrorMessage}\"\n aria-invalid=\"${ariaInvalid}\"\n aria-busy=\"${() => (props.loading.value ? 'true' : null)}\"\n ref=\"${inputRef}\" />\n <slot name=\"suffix\"></slot>\n ${renderStatusIcon(errorText)}\n <span class=\"field-spinner\" part=\"spinner\" role=\"status\" aria-label=\"Loading\"></span>\n <button\n class=\"pwd-toggle-btn\"\n part=\"pwd-toggle\"\n type=\"button\"\n aria-label=\"${passwordToggleLabel}\"\n aria-pressed=\"${passwordTogglePressed}\"\n tabindex=\"${pwdToggleTabIndex}\"\n @click=\"${togglePassword}\">\n ${passwordToggleIcon}\n </button>\n <button\n aria-label=\"Clear\"\n class=\"clear-btn\"\n part=\"clear\"\n type=\"button\"\n tabindex=\"${clearTabIndex}\"\n @click=\"${clear}\">\n <ore-icon aria-hidden=\"true\" name=\"x\" size=\"12\" stroke-width=\"2.5\"></ore-icon>\n </button>\n </div>\n </div>\n <div class=\"helper-text\" aria-live=\"polite\" id=\"${assistiveId}\" part=\"helper\" ?hidden=\"${helperHidden}\">\n <slot name=\"helper\">${() => helperText.value}</slot>\n </div>\n <div class=\"helper-text\" id=\"${errorId}\" role=\"alert\" part=\"error\" ?hidden=\"${errorHidden}\">\n <slot name=\"error\">${() => errorText.value}</slot>\n </div>\n <div\n class=\"char-counter\"\n part=\"char-counter\"\n data-near-limit=\"${counterNearLimit}\"\n data-at-limit=\"${counterAtLimit}\"\n ?hidden=\"${counterHidden}\">\n ${counterText}\n </div>\n </div>\n `;\n },\n shadow: { delegatesFocus: true },\n styles: [\n colorThemeMixin,\n coarsePointerMixin,\n reducedMotionMixin,\n roundedVariantMixin,\n disabledLoadingMixin,\n sizeVariantMixin(FIELD_SIZE_PRESET),\n forcedColorsFocusMixin('input'),\n componentStyles,\n // Must come after `componentStyles` — `@layer` precedence is fixed by which layer name is\n // *first* referenced across this whole array, and `componentStyles` is what establishes\n // `refine.base` (the unconditional `--_bg`/`--_border-color` defaults this mixin's\n // `refine.variants` rules need to win over). Placed earlier, `refine.variants` would end up\n // registered as the *lower*-priority layer, and every variant would silently render as the\n // base default — exactly the bug this ordering fixes.\n fieldVariantMixin({ container: '.field', text: 'input', tokenPrefix: 'input' }),\n ],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"input.cjs","names":[],"sources":["../src/inputs/input/input.ts"],"sourcesContent":["import { bind, define, getHost, html, live, onCleanup, onElement, prop, ref, useField, useSlots } from '@vielzeug/ore';\nimport { computed, signal } from '@vielzeug/ripple';\nimport { bindRefCallback, createTextField, lifecycleSignal } from '../../core';\nimport type { TextFieldProps } from '../../shared';\nimport { disablableBundle, FIELD_SIZE_PRESET, roundableBundle, sizableBundle, themableBundle } from '../../shared';\nimport type { InputType, VisualVariant } from '../../types';\nimport '../../content/icon/icon';\nimport {\n coarsePointerMixin,\n colorThemeMixin,\n disabledLoadingMixin,\n fieldVariantMixin,\n forcedColorsFocusMixin,\n reducedMotionMixin,\n roundedVariantMixin,\n sizeVariantMixin,\n} from '../../styles';\nimport { errorAttr } from '../shared/field-binding';\nimport { defineFieldValue, dispatchNativeFieldEvent, setFieldValue } from '../shared/native-field-event';\nimport { renderStatusIcon } from '../shared/templates';\nimport componentStyles from './input.css?inline';\n\n/** Input component properties */\n\nexport type OreInputEvents = {\n change: Event;\n input: InputEvent;\n};\n\nexport type OreInputProps = TextFieldProps<Exclude<VisualVariant, 'frost'>> & {\n /** Accessible name used when no visible label is rendered. */\n 'aria-label'?: string;\n /** Autocomplete hint */\n autocomplete?: string;\n /** Show a clear (×) button when the field has a value */\n clearable?: boolean;\n /** Virtual keyboard hint for mobile devices */\n inputmode?: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url';\n /**\n * Shows an inline spinner inside the field and forces the inner `<input>` into\n * `disabled` for the duration — use while an async validation/submission request\n * is in flight (e.g. checking username availability) to prevent double-submits.\n */\n loading?: boolean;\n /** Maximum character length — shows a counter below the input */\n maxlength?: number;\n /** Minimum character length */\n minlength?: number;\n /** HTML pattern attribute for client-side validation */\n pattern?: string;\n /**\n * JS-only callback fired with the inner `<input>` element when it mounts,\n * and with `null` when it unmounts. Intended for composed components that\n * need imperative access to the raw input element.\n * Set as a JS property: `bitInput.ref = (el) => { ... }`.\n */\n ref?: ((el: HTMLInputElement | null) => void) | null;\n /** HTML input type */\n type?: InputType;\n};\n\nconst VALID_INPUT_TYPES = [\n 'text',\n 'email',\n 'password',\n 'search',\n 'url',\n 'tel',\n 'number',\n 'date',\n 'time',\n 'datetime-local',\n 'month',\n 'week',\n] as const;\n\n/**\n * A customizable text input component with multiple variants, label placements, and form features.\n *\n * @element ore-input\n *\n * @attr {string} aria-label - Accessible name used when no visible label is rendered\n * @attr {string} label - Label text\n * @attr {string} label-placement - Label placement: 'inset' | 'outside'\n * @attr {string} type - HTML input type: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'search'\n * @attr {string} value - Current input value\n * @attr {string} placeholder - Placeholder text\n * @attr {string} name - Form field name\n * @attr {string} helper - Helper text displayed below the input (fallback when the `helper` slot is empty)\n * @attr {string} error - Error message — marks the field as invalid (fallback when the `error` slot is empty)\n * @attr {boolean} disabled - Disable input interaction\n * @attr {boolean} readonly - Make the input read-only\n * @attr {boolean} required - Mark the field as required\n * @attr {boolean} loading - Show an inline spinner and force the field disabled\n * @attr {boolean} success - Show an inline success check icon (suppressed while `error` is set)\n * @attr {string} color - Theme color: 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'\n * @attr {string} variant - Visual variant: 'solid' | 'flat' | 'bordered' | 'outline' | 'ghost' | 'text'\n * @attr {string} size - Input size: 'sm' | 'md' | 'lg'\n * @attr {string} rounded - Border radius: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full'\n *\n * @fires input - Emitted when input value changes (on every keystroke).\n * @fires change - Emitted when input loses focus with changed value.\n *\n * @slot prefix - Content before the input (e.g., icons)\n * @slot suffix - Content after the input (e.g., clear button, validation icon)\n * @slot label - Replaces the label text — slotted content takes precedence over the `label` prop\n * @slot helper - Replaces the helper text — slotted content takes precedence over the `helper` prop\n * @slot error - Replaces the error text — slotted content takes precedence over the `error` prop\n *\n * @part wrapper - The input wrapper element\n * @part label - The label element (inset or outside)\n * @part field - The field container element\n * @part input-row - The input row container element\n * @part input - The input element\n * @part status-icon - The inline error/success icon shown inside the field\n * @part spinner - The inline loading spinner shown inside the field while `loading`\n * @part helper - The helper text element\n *\n * @cssprop --input-bg - Background color\n * @cssprop --input-color - Text color\n * @cssprop --input-border-color - Border color\n * @cssprop --input-placeholder-color - Placeholder text color\n * @cssprop --input-radius - Border radius\n * @cssprop --input-padding - Inner padding (block inline)\n * @cssprop --input-label-block-inset - Inset from the field block start for an inset label\n * @cssprop --input-label-inset - Inset from the field inline start for an inset label\n * @cssprop --input-gap - Gap between prefix/suffix icons and input text\n * @cssprop --input-font-size - Font size\n * @cssprop --input-height - Field height\n * @cssprop --input-hover-bg - Field background on hover (flat/ghost variants)\n * @cssprop --input-hover-border-color - Field border on hover (flat/bordered variants)\n * @cssprop --input-focus-bg - Field background when focused (flat variant)\n * @cssprop --input-focus-border-color - Field border when focused (flat/text variants)\n *\n * @example\n * ```html\n * <ore-input type=\"email\" label=\"Email\" placeholder=\"you@example.com\" />\n * <ore-input label=\"Name\" variant=\"bordered\" color=\"primary\" />\n * <ore-input label=\"Username\" error=\"That username is taken\" />\n * <ore-input label=\"Username\" success />\n * <ore-input label=\"Username\" loading />\n * ```\n */\nexport const INPUT_TAG = 'ore-input' as const;\ndefine<OreInputProps>(INPUT_TAG, {\n formAssociated: true,\n props: {\n ...themableBundle,\n ...sizableBundle,\n ...disablableBundle,\n ...roundableBundle,\n 'aria-label': prop.string(),\n autocomplete: prop.string(),\n clearable: prop.bool(false),\n error: prop.string(),\n fullwidth: prop.bool(false),\n helper: prop.string(),\n inputmode: prop.string<'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url'>(),\n label: prop.string(),\n 'label-placement': prop.oneOf(['inset', 'outside'] as const, 'inset'),\n loading: prop.bool(false),\n maxlength: prop.json(undefined as number | undefined),\n minlength: prop.json(undefined as number | undefined),\n name: prop.string(),\n pattern: prop.string(),\n placeholder: prop.string(),\n readonly: prop.bool(false),\n ref: prop.data<((el: HTMLInputElement | null) => void) | null>(),\n required: prop.bool(false),\n success: prop.bool(false),\n type: prop.oneOf(VALID_INPUT_TYPES, 'text'),\n value: prop.string(),\n variant: prop.string<'flat' | 'text' | 'solid' | 'bordered' | 'outline' | 'ghost'>(),\n },\n setup(props) {\n const el = getHost();\n const slots = useSlots();\n\n const showPassword = signal(false);\n const inputRef = ref<HTMLInputElement>();\n\n const hasLabel = computed(() => !!props.label.value || slots.has('label').value);\n // `loading` behaves like a temporary `disabled` — forces the inner <input> out of\n // constraint validation and interaction for the duration, layered on top of any real\n // `loading` is layered on top of the consumer's explicit `disabled` prop.\n const isDisabled = computed(() => props.disabled.value || props.loading.value);\n\n const abortSignal = lifecycleSignal(onCleanup);\n const tf = createTextField({\n disabled: isDisabled,\n error: props.error,\n hasLabel,\n helper: props.helper,\n label: props.label,\n labelPlacement: props['label-placement'],\n maxLength: props.maxlength,\n onChange: (_event: Event, value: string) => {\n setFieldValue(el, value);\n dispatchNativeFieldEvent(el, 'change');\n },\n onInput: (_event: Event, value: string) => {\n setFieldValue(el, value);\n dispatchNativeFieldEvent(el, 'input');\n },\n prefix: 'input',\n readonly: props.readonly,\n required: props.required,\n signal: abortSignal,\n value: props.value,\n });\n\n defineFieldValue(\n el,\n () => tf.value.value,\n (value) => {\n tf.value.value = value;\n },\n );\n\n tf.attachFormField(\n useField<string>({\n disabled: tf.disabled,\n onReset: tf.reset,\n toFormValue: (v) => v,\n validationMessage: tf.validationMessage,\n validity: tf.validity,\n value: tf.value,\n }),\n );\n\n const {\n ariaDescribedBy,\n ariaErrorMessage,\n ariaInvalid,\n ariaLabelledBy,\n assistiveId,\n clear: clearValue,\n counter,\n errorId,\n errorText,\n fieldId: inputId,\n helperText,\n labelId,\n labelVisible,\n value: fieldValue,\n wire,\n } = tf;\n\n onElement(inputRef, (el) => {\n wire(el, abortSignal);\n\n const unwireRef = bindRefCallback(props.ref, el);\n\n return () => {\n unwireRef();\n };\n });\n\n const clear = (event?: Event): void => {\n clearValue(event);\n inputRef.value?.focus();\n };\n\n const resolvedInputType = (): string =>\n props.type.value === 'password' && showPassword.value ? 'text' : (props.type.value ?? 'text');\n\n bind({\n attr: {\n error: errorAttr(errorText),\n 'has-value': () => (fieldValue.value ? true : undefined),\n size: props.size,\n // Reflects `success` only once `error` is confirmed empty — keeps the two host\n // attributes mutually exclusive even if a consumer sets both props at once.\n success: () => (props.success.value && !errorText.value ? true : undefined),\n variant: props.variant,\n },\n });\n\n const labelHidden = () => !labelVisible.value;\n const passwordToggleLabel = () => (showPassword.value ? 'Hide password' : 'Show password');\n const passwordTogglePressed = () => String(showPassword.value);\n const passwordToggleIcon = () =>\n showPassword.value\n ? html`\n <ore-icon name=\"eye-off\" size=\"14\" stroke-width=\"2\" aria-hidden=\"true\"></ore-icon>\n `\n : html`\n <ore-icon name=\"eye\" size=\"14\" stroke-width=\"2\" aria-hidden=\"true\"></ore-icon>\n `;\n const helperHidden = () => !!errorText.value || !helperText.value;\n const errorHidden = () => !errorText.value;\n const counterNearLimit = () => (counter?.value.counterNearLimit && !counter?.value.counterAtLimit ? '' : null);\n const counterAtLimit = () => (counter?.value.counterAtLimit ? '' : null);\n const counterHidden = () => !counter;\n const counterText = () => counter?.value.counterText ?? '';\n\n const clearTabIndex = () => (fieldValue.value ? '0' : '-1');\n const pwdToggleTabIndex = () => (props.type.value === 'password' ? '0' : '-1');\n\n const togglePassword = () => {\n showPassword.value = !showPassword.value;\n inputRef.value?.focus();\n };\n\n return html`\n <div class=\"input-wrapper\" part=\"wrapper\">\n <label class=\"label\" for=\"${() => (labelHidden() ? null : inputId)}\" id=\"${labelId}\" part=\"label\" ?hidden=\"${labelHidden}\">\n <slot name=\"label\">${props.label}</slot>\n </label>\n <div class=\"field\" part=\"field\">\n <div class=\"input-row\" part=\"input-row\">\n <slot name=\"prefix\"></slot>\n <input\n part=\"input\"\n id=\"${inputId}\"\n type=\"${resolvedInputType}\"\n name=\"${props.name}\"\n placeholder=\"${props.placeholder}\"\n autocomplete=\"${props.autocomplete}\"\n inputmode=\"${props.inputmode}\"\n maxlength=\"${props.maxlength}\"\n minlength=\"${props.minlength}\"\n pattern=\"${props.pattern}\"\n ?disabled=\"${isDisabled}\"\n ?readonly=\"${props.readonly}\"\n ?required=\"${props.required}\"\n value=\"${live(fieldValue)}\"\n aria-label=\"${props['aria-label']}\"\n aria-labelledby=\"${ariaLabelledBy}\"\n aria-describedby=\"${ariaDescribedBy}\"\n aria-errormessage=\"${ariaErrorMessage}\"\n aria-invalid=\"${ariaInvalid}\"\n aria-busy=\"${() => (props.loading.value ? 'true' : null)}\"\n ref=\"${inputRef}\" />\n <slot name=\"suffix\"></slot>\n ${renderStatusIcon(errorText)}\n <span class=\"field-spinner\" part=\"spinner\" role=\"status\" aria-label=\"Loading\"></span>\n <button\n class=\"pwd-toggle-btn\"\n part=\"pwd-toggle\"\n type=\"button\"\n aria-label=\"${passwordToggleLabel}\"\n aria-pressed=\"${passwordTogglePressed}\"\n tabindex=\"${pwdToggleTabIndex}\"\n @click=\"${togglePassword}\">\n ${passwordToggleIcon}\n </button>\n <button\n aria-label=\"Clear\"\n class=\"clear-btn\"\n part=\"clear\"\n type=\"button\"\n tabindex=\"${clearTabIndex}\"\n @click=\"${clear}\">\n <ore-icon aria-hidden=\"true\" name=\"x\" size=\"12\" stroke-width=\"2.5\"></ore-icon>\n </button>\n </div>\n </div>\n <div class=\"helper-text\" aria-live=\"polite\" id=\"${assistiveId}\" part=\"helper\" ?hidden=\"${helperHidden}\">\n <slot name=\"helper\">${() => helperText.value}</slot>\n </div>\n <div class=\"helper-text\" id=\"${errorId}\" role=\"alert\" part=\"error\" ?hidden=\"${errorHidden}\">\n <slot name=\"error\">${() => errorText.value}</slot>\n </div>\n <div\n class=\"char-counter\"\n part=\"char-counter\"\n data-near-limit=\"${counterNearLimit}\"\n data-at-limit=\"${counterAtLimit}\"\n ?hidden=\"${counterHidden}\">\n ${counterText}\n </div>\n </div>\n `;\n },\n shadow: { delegatesFocus: true },\n styles: [\n colorThemeMixin,\n coarsePointerMixin,\n reducedMotionMixin,\n roundedVariantMixin,\n disabledLoadingMixin,\n sizeVariantMixin(FIELD_SIZE_PRESET),\n forcedColorsFocusMixin('input'),\n componentStyles,\n // Must come after `componentStyles` — `@layer` precedence is fixed by which layer name is\n // *first* referenced across this whole array, and `componentStyles` is what establishes\n // `refine.base` (the unconditional `--_bg`/`--_border-color` defaults this mixin's\n // `refine.variants` rules need to win over). Placed earlier, `refine.variants` would end up\n // registered as the *lower*-priority layer, and every variant would silently render as the\n // base default — exactly the bug this ordering fixes.\n fieldVariantMixin({ container: '.field', text: 'input', tokenPrefix: 'input' }),\n ],\n});\n"],"mappings":"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"}
|
package/dist/input.js
CHANGED
|
@@ -36,6 +36,7 @@ C(I, {
|
|
|
36
36
|
...p,
|
|
37
37
|
...d,
|
|
38
38
|
...f,
|
|
39
|
+
"aria-label": k.string(),
|
|
39
40
|
autocomplete: k.string(),
|
|
40
41
|
clearable: k.bool(!1),
|
|
41
42
|
error: k.string(),
|
|
@@ -90,21 +91,27 @@ C(I, {
|
|
|
90
91
|
value: d.value
|
|
91
92
|
}));
|
|
92
93
|
let { ariaDescribedBy: f, ariaErrorMessage: p, ariaInvalid: m, ariaLabelledBy: h, assistiveId: x, clear: C, counter: k, errorId: F, errorText: I, fieldId: L, helperText: R, labelId: z, labelVisible: B, value: V, wire: H } = d;
|
|
93
|
-
|
|
94
|
+
O(i, (t) => {
|
|
94
95
|
H(t, s);
|
|
95
96
|
let n = l(e.ref, t);
|
|
96
97
|
return () => {
|
|
97
98
|
n();
|
|
98
99
|
};
|
|
99
|
-
})
|
|
100
|
+
});
|
|
101
|
+
let U = (e) => {
|
|
102
|
+
C(e), i.value?.focus();
|
|
103
|
+
}, W = () => e.type.value === "password" && r.value ? "text" : e.type.value ?? "text";
|
|
104
|
+
S({ attr: {
|
|
100
105
|
error: g(I),
|
|
101
106
|
"has-value": () => V.value ? !0 : void 0,
|
|
102
107
|
size: e.size,
|
|
103
108
|
success: () => e.success.value && !I.value ? !0 : void 0,
|
|
104
109
|
variant: e.variant
|
|
105
|
-
} })
|
|
110
|
+
} });
|
|
111
|
+
let G = () => !B.value;
|
|
112
|
+
return T`
|
|
106
113
|
<div class="input-wrapper" part="wrapper">
|
|
107
|
-
<label class="label" for="${L}" id="${z}" part="label" ?hidden="${
|
|
114
|
+
<label class="label" for="${() => G() ? null : L}" id="${z}" part="label" ?hidden="${G}">
|
|
108
115
|
<slot name="label">${e.label}</slot>
|
|
109
116
|
</label>
|
|
110
117
|
<div class="field" part="field">
|
|
@@ -113,7 +120,7 @@ C(I, {
|
|
|
113
120
|
<input
|
|
114
121
|
part="input"
|
|
115
122
|
id="${L}"
|
|
116
|
-
type="${
|
|
123
|
+
type="${W}"
|
|
117
124
|
name="${e.name}"
|
|
118
125
|
placeholder="${e.placeholder}"
|
|
119
126
|
autocomplete="${e.autocomplete}"
|
|
@@ -125,6 +132,7 @@ C(I, {
|
|
|
125
132
|
?readonly="${e.readonly}"
|
|
126
133
|
?required="${e.required}"
|
|
127
134
|
value="${E(V)}"
|
|
135
|
+
aria-label="${e["aria-label"]}"
|
|
128
136
|
aria-labelledby="${h}"
|
|
129
137
|
aria-describedby="${f}"
|
|
130
138
|
aria-errormessage="${p}"
|
|
@@ -156,9 +164,7 @@ C(I, {
|
|
|
156
164
|
part="clear"
|
|
157
165
|
type="button"
|
|
158
166
|
tabindex="${() => V.value ? "0" : "-1"}"
|
|
159
|
-
@click="${
|
|
160
|
-
C(e), i.value?.focus();
|
|
161
|
-
}}">
|
|
167
|
+
@click="${U}">
|
|
162
168
|
<ore-icon aria-hidden="true" name="x" size="12" stroke-width="2.5"></ore-icon>
|
|
163
169
|
</button>
|
|
164
170
|
</div>
|