@viax.io/uxm 4.36.0 → 4.37.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/CHANGELOG.md +7 -0
- package/README.md +74 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js.map +1 -1
- package/dist/previews/index.cjs +3 -0
- package/dist/previews/index.cjs.map +1 -1
- package/dist/previews/index.d.ts +3 -2
- package/dist/previews/index.d.ts.map +1 -1
- package/dist/previews/index.js +2 -0
- package/dist/previews/index.js.map +1 -1
- package/dist/studio/index.cjs.map +1 -1
- package/dist/studio/index.d.ts +2 -2
- package/dist/studio/index.js.map +1 -1
- package/dist/studio/lib/registry/app.cjs +52 -0
- package/dist/studio/lib/registry/app.cjs.map +1 -1
- package/dist/studio/lib/registry/app.d.ts.map +1 -1
- package/dist/studio/lib/registry/app.js +52 -0
- package/dist/studio/lib/registry/app.js.map +1 -1
- package/dist/studio/lib/registry/buttons.cjs +2 -2
- package/dist/studio/lib/registry/buttons.cjs.map +1 -1
- package/dist/studio/lib/registry/buttons.js +2 -2
- package/dist/studio/lib/registry/buttons.js.map +1 -1
- package/dist/studio/lib/registry/composite.cjs +1 -1
- package/dist/studio/lib/registry/composite.cjs.map +1 -1
- package/dist/studio/lib/registry/composite.js +1 -1
- package/dist/studio/lib/registry/composite.js.map +1 -1
- package/dist/studio/lib/registry/diagram.cjs +1 -1
- package/dist/studio/lib/registry/diagram.cjs.map +1 -1
- package/dist/studio/lib/registry/diagram.js +1 -1
- package/dist/studio/lib/registry/diagram.js.map +1 -1
- package/dist/studio/lib/registry/display.cjs +2 -2
- package/dist/studio/lib/registry/display.cjs.map +1 -1
- package/dist/studio/lib/registry/display.js +2 -2
- package/dist/studio/lib/registry/display.js.map +1 -1
- package/dist/studio/lib/registry/feedback.cjs +3 -3
- package/dist/studio/lib/registry/feedback.cjs.map +1 -1
- package/dist/studio/lib/registry/feedback.js +3 -3
- package/dist/studio/lib/registry/feedback.js.map +1 -1
- package/dist/studio/lib/registry/inputs.cjs +12 -12
- package/dist/studio/lib/registry/inputs.cjs.map +1 -1
- package/dist/studio/lib/registry/inputs.js +12 -12
- package/dist/studio/lib/registry/inputs.js.map +1 -1
- package/dist/studio/lib/types.cjs.map +1 -1
- package/dist/studio/persistence/generate-css.cjs +15 -0
- package/dist/studio/persistence/generate-css.cjs.map +1 -1
- package/dist/studio/persistence/generate-css.d.ts.map +1 -1
- package/dist/studio/persistence/generate-css.js +15 -0
- package/dist/studio/persistence/generate-css.js.map +1 -1
- package/dist/studio/shell/canvas.cjs +1 -0
- package/dist/studio/shell/canvas.cjs.map +1 -1
- package/dist/studio/shell/canvas.d.ts.map +1 -1
- package/dist/studio/shell/canvas.js +2 -0
- package/dist/studio/shell/canvas.js.map +1 -1
- package/dist/ui/calendar/calendar.cjs +3 -1
- package/dist/ui/calendar/calendar.cjs.map +1 -1
- package/dist/ui/calendar/calendar.d.ts +6 -2
- package/dist/ui/calendar/calendar.d.ts.map +1 -1
- package/dist/ui/calendar/calendar.js +3 -1
- package/dist/ui/calendar/calendar.js.map +1 -1
- package/dist/ui/currency-input/currency-input.cjs +3 -1
- package/dist/ui/currency-input/currency-input.cjs.map +1 -1
- package/dist/ui/currency-input/currency-input.d.ts +4 -2
- package/dist/ui/currency-input/currency-input.d.ts.map +1 -1
- package/dist/ui/currency-input/currency-input.js +3 -1
- package/dist/ui/currency-input/currency-input.js.map +1 -1
- package/dist/ui/date-input/date-input.cjs +2 -1
- package/dist/ui/date-input/date-input.cjs.map +1 -1
- package/dist/ui/date-input/date-input.d.ts +11 -1
- package/dist/ui/date-input/date-input.d.ts.map +1 -1
- package/dist/ui/date-input/date-input.js +2 -1
- package/dist/ui/date-input/date-input.js.map +1 -1
- package/dist/ui/editable-cell/editable-cell.cjs +20 -6
- package/dist/ui/editable-cell/editable-cell.cjs.map +1 -1
- package/dist/ui/editable-cell/editable-cell.d.ts +24 -1
- package/dist/ui/editable-cell/editable-cell.d.ts.map +1 -1
- package/dist/ui/editable-cell/editable-cell.js +20 -6
- package/dist/ui/editable-cell/editable-cell.js.map +1 -1
- package/dist/ui/file-upload/file-upload.cjs +36 -9
- package/dist/ui/file-upload/file-upload.cjs.map +1 -1
- package/dist/ui/file-upload/file-upload.d.ts +46 -4
- package/dist/ui/file-upload/file-upload.d.ts.map +1 -1
- package/dist/ui/file-upload/file-upload.js +36 -9
- package/dist/ui/file-upload/file-upload.js.map +1 -1
- package/dist/ui/index.cjs +12 -2
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.ts +5 -1
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +7 -1
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/language-switcher/index.cjs +29 -0
- package/dist/ui/language-switcher/index.cjs.map +1 -0
- package/dist/ui/language-switcher/index.d.ts +3 -0
- package/dist/ui/language-switcher/index.d.ts.map +1 -0
- package/dist/ui/language-switcher/index.js +5 -0
- package/dist/ui/language-switcher/index.js.map +1 -0
- package/dist/ui/language-switcher/language-switcher-preview.cjs +137 -0
- package/dist/ui/language-switcher/language-switcher-preview.cjs.map +1 -0
- package/dist/ui/language-switcher/language-switcher-preview.d.ts +3 -0
- package/dist/ui/language-switcher/language-switcher-preview.d.ts.map +1 -0
- package/dist/ui/language-switcher/language-switcher-preview.js +113 -0
- package/dist/ui/language-switcher/language-switcher-preview.js.map +1 -0
- package/dist/ui/language-switcher/language-switcher.cjs +127 -0
- package/dist/ui/language-switcher/language-switcher.cjs.map +1 -0
- package/dist/ui/language-switcher/language-switcher.css +95 -0
- package/dist/ui/language-switcher/language-switcher.d.ts +73 -0
- package/dist/ui/language-switcher/language-switcher.d.ts.map +1 -0
- package/dist/ui/language-switcher/language-switcher.js +103 -0
- package/dist/ui/language-switcher/language-switcher.js.map +1 -0
- package/dist/ui/locale/index.cjs +33 -0
- package/dist/ui/locale/index.cjs.map +1 -0
- package/dist/ui/locale/index.d.ts +3 -0
- package/dist/ui/locale/index.d.ts.map +1 -0
- package/dist/ui/locale/index.js +7 -0
- package/dist/ui/locale/index.js.map +1 -0
- package/dist/ui/locale/locale.cjs +43 -0
- package/dist/ui/locale/locale.cjs.map +1 -0
- package/dist/ui/locale/locale.d.ts +49 -0
- package/dist/ui/locale/locale.d.ts.map +1 -0
- package/dist/ui/locale/locale.js +17 -0
- package/dist/ui/locale/locale.js.map +1 -0
- package/dist/ui/page-shell/page-shell.cjs.map +1 -1
- package/dist/ui/page-shell/page-shell.d.ts +1 -1
- package/dist/ui/page-shell/page-shell.d.ts.map +1 -1
- package/dist/ui/page-shell/page-shell.js.map +1 -1
- package/dist/ui/range-slider/range-slider.cjs +7 -3
- package/dist/ui/range-slider/range-slider.cjs.map +1 -1
- package/dist/ui/range-slider/range-slider.d.ts +17 -1
- package/dist/ui/range-slider/range-slider.d.ts.map +1 -1
- package/dist/ui/range-slider/range-slider.js +7 -3
- package/dist/ui/range-slider/range-slider.js.map +1 -1
- package/dist/ui/styles.css +2 -1
- package/package.json +4 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/ui/file-upload/file-upload.tsx"],"sourcesContent":["import {\n useCallback,\n useId,\n useState,\n type ChangeEvent,\n type CSSProperties,\n type DragEvent,\n type HTMLAttributes,\n type ReactNode,\n} from 'react';\n\nimport { cn } from '@/helpers';\n\nimport { Icon } from '../icon';\n\n/**\n * Visual states the FileUpload **drop area** can be forced into via the\n * `state` prop. These describe the drop area surface only — not per-file\n * lifecycle. Per-file states (queued / uploading / done / error) live on\n * each `FileUploadFileMeta.status` and render row-by-row independently.\n *\n * `uploading` is **caller-controlled** — the atom never sets it from per-\n * file statuses. Use it for page-level signals: a batch is being prepared\n * before per-file rows exist, a chunked upload with no per-file progress,\n * or any \"the system is busy with this dropzone\" state the caller wants\n * to surface. Per-row uploading rows remain the *primary* progress signal.\n *\n * `error` means a *page-level* error (e.g. \"Connection lost\", \"Server\n * rejected the batch\") — surfaced via the `errorMessage` prop and shown\n * below the drop area. Per-file errors don't change the drop area's state.\n */\nexport type FileUploadState = 'default' | 'drag-over' | 'uploading' | 'error';\n\n/**\n * Per-file lifecycle status. The atom paints each row based on its own\n * status independently of other rows.\n *\n * - `queued`: file is added but not yet sent. Neutral row appearance.\n * - `uploading`: file is in flight. Row shows a progress strip and a\n * verbose meta line (\"Uploading · 61% of 471.0 KB\"). Caller drives\n * `progress` (0–1).\n * - `done`: file completed. Document icon swaps to a success check.\n * - `error`: file failed. Border + icon + meta switch to danger styling;\n * the per-file `errorMessage` replaces the size meta.\n */\nexport type FileStatus = 'queued' | 'uploading' | 'done' | 'error';\n\nexport interface FileUploadFileMeta {\n /** Stable id for keying + removal callbacks. */\n id: string;\n /** Filename to display in the row. */\n name: string;\n /** Size in bytes. Formatted for display by the atom. */\n size: number;\n /**\n * Per-file lifecycle status. Defaults to `\"queued\"` if omitted. Drives\n * the row's icon, meta line, border, and progress strip.\n */\n status?: FileStatus;\n /**\n * 0–1 progress fraction. Used only when `status === \"uploading\"`.\n * If omitted during uploading, the row shows an indeterminate state\n * (\"Uploading…\") and no fraction.\n */\n progress?: number;\n /**\n * Per-file error message. Used only when `status === \"error\"`.\n * Replaces the size meta on the row in danger styling.\n */\n errorMessage?: string;\n /**\n * Direct URL for a stored file. On a `done` row this renders a trailing\n * control as a real `<a href download>` — which is why it exists alongside\n * `onOpenFile` rather than being replaced by it: only an anchor gives the\n * browser's own affordances (⌘/middle-click to a new tab, right-click →\n * \"Save link as\", the native download UI).\n *\n * ⚠️ Use this ONLY when the browser can fetch the URL unauthenticated. A\n * plain navigation carries no `Authorization` header, so a token-protected\n * URL returns 401 — reach for `onOpenFile` there.\n *\n * ⚠️ The `download` attribute is ignored CROSS-ORIGIN: if the file is\n * served from another origin the browser navigates to it instead of saving\n * it. Pass `downloadGlyph=\"arrow-up-right\"` so the control does not promise\n * a save it cannot perform, or route through `onOpenFile` and a blob.\n */\n href?: string;\n}\n\nexport interface FileUploadProps\n extends Omit<HTMLAttributes<HTMLDivElement>, 'onError' | 'onDragEnter' | 'onDragLeave'> {\n /**\n * Forced drop-area visual state — overrides internal drag detection.\n * The workbench uses this to paint each state without real interaction.\n * Omit in production: the atom flips to `drag-over` itself while\n * dragging, and to `error` when `errorMessage` is set.\n *\n * Per-file states (uploading / done / per-file errors) are NOT atom-\n * level — they live on each `FileUploadFileMeta.status`.\n */\n state?: FileUploadState;\n /** Allow selecting more than one file. Defaults to `true`. */\n multiple?: boolean;\n /** Standard `<input type=\"file\" accept>` filter, e.g. `\"image/*\"` or `\".pdf,.docx\"`. */\n accept?: string;\n /** Disable interaction. Also disables drop and all per-row remove buttons. */\n disabled?: boolean;\n /** Heading text inside the drop area. */\n titleText?: string;\n /**\n * Hint line below the title. Falls back to a `multiple`-aware default\n * (\"Drop files here, or click to browse\" vs \"Drop a file here, or click\n * to browse\"). When the caller sets `state=\"uploading\"`, the atom\n * temporarily replaces this with \"Uploading…\" — caller's `helpText`\n * value is restored once they flip back to default.\n *\n * Per-file uploading rows do NOT trigger this swap on their own — the\n * drop-area copy reflects the caller's declared state, not aggregate\n * row status. Rows carry their own \"Uploading · 60% of …\" indicators.\n */\n helpText?: string;\n /**\n * Optional constraint line below the help text — e.g. \"PDF, DOCX · up to\n * 10 MB\". Caller-controlled copy describing what the page accepts, so it\n * stays in sync with whatever `accept` filter and validation the page\n * enforces in `onFiles`. The atom never invents this string — when\n * omitted, no slot renders. The atom does not enforce anything in it.\n */\n allowedTypesText?: string;\n /**\n * Page-level error message to render below the drop area (e.g. \"Connection\n * lost\", \"Server rejected the batch\"). When set, the atom paints the drop\n * area in the `error` state automatically. Named `error` to match the rest\n * of the input family's `error?: string`.\n *\n * Distinct from per-file errors — set `status: \"error\"` + `errorMessage`\n * on a `FileUploadFileMeta` to surface a single-file failure inline on\n * its row instead.\n */\n error?: string;\n /**\n * @deprecated Renamed to `error` (input-family convention). Still accepted\n * as an alias for back-compat; `error` wins when both are set.\n */\n errorMessage?: string;\n /**\n * Files to display in the inline list under the drop area. Rendered\n * automatically whenever `files.length > 0`. Each entry's `status`\n * drives its row appearance independently. To render the list elsewhere\n * on the page, omit `files` and manage that state yourself.\n */\n files?: FileUploadFileMeta[];\n /** Fires with the selected `File[]` whether the user picked via dialog or dropped. */\n onFiles?: (files: File[]) => void;\n /**\n * Fires when a row's remove button is clicked. The caller decides what\n * \"remove\" means per status — cancel an in-flight upload, drop a queued\n * file, delete a completed one server-side. The atom doesn't auto-disable\n * the button during `uploading` so the user always has an escape hatch.\n */\n onRemove?: (id: string) => void;\n /**\n * Fires when a row's download control is activated. Set this when the\n * stored file needs an auth header or a signed-URL round trip: the consumer\n * fetches the bytes itself and saves them (typically `fetch` with the\n * bearer, then `URL.createObjectURL` — remember to `revokeObjectURL`, and\n * to pass the filename through, or the file lands named `blob`).\n *\n * Takes PRECEDENCE over `FileUploadFileMeta.href`: a consumer that supplied\n * a handler wants control of what opening means. Set neither and no\n * download control renders, which is how every existing consumer behaves.\n */\n onOpenFile?: (id: string) => void;\n /**\n * Glyph for the download control. Defaults to `\"arrow-down\"`. Override to\n * `\"arrow-up-right\"` when the file opens in a tab rather than saving —\n * cross-origin `href`, or an `onOpenFile` that previews in-app.\n */\n downloadGlyph?: string;\n /**\n * Verb in the download control's accessible name, rendered as\n * `` `${downloadLabel} ${file.name}` ``. Defaults to `\"Download\"`.\n *\n * Change it together with `downloadGlyph` whenever the control opens rather\n * than saves: the glyph alone corrects the promise for sighted users and\n * leaves screen-reader users being told \"Download\" for a control that\n * navigates. `downloadLabel=\"Open\"` keeps the two in step.\n */\n downloadLabel?: string;\n /** Fires when a drag enters the drop area. */\n onDragEnter?: (e: DragEvent<HTMLLabelElement>) => void;\n /** Fires when a drag leaves the drop area. */\n onDragLeave?: (e: DragEvent<HTMLLabelElement>) => void;\n /** Fires when a drop is rejected (e.g. wrong type — for caller-driven validation). */\n onError?: (reason: string) => void;\n /** Optional override for the drop-area icon glyph. */\n iconGlyph?: string;\n /** Optional slot rendered above the file list (e.g. a summary line). */\n children?: ReactNode;\n}\n\nexport function FileUpload({\n state: forcedState,\n multiple = true,\n accept,\n disabled = false,\n titleText,\n helpText,\n allowedTypesText,\n error,\n errorMessage,\n files = [],\n onFiles,\n onRemove,\n onOpenFile,\n downloadGlyph = 'arrow-down',\n downloadLabel = 'Download',\n onDragEnter,\n onDragLeave,\n onError: _onError,\n className,\n style,\n iconGlyph = 'cloud-arrow-up',\n children,\n ...rest\n}: FileUploadProps) {\n const inputId = useId();\n const helpId = useId();\n\n // Whether the list has a download COLUMN at all — a whole-list property, so\n // it is computed once here rather than per row (inside the map it re-scanned\n // `files` for every row, which is O(n²) on exactly the renders a list does\n // most: one per progress tick during an upload).\n //\n // The column exists so the control can be `done`-only without the name column\n // gaining 24px the moment an upload settles — a reflow of the row the user is\n // watching. Rows with nothing to fetch render an inert placeholder instead.\n const listHasDownloads = !!onOpenFile || files.some((f) => !!f.href);\n const [internalDrag, setInternalDrag] = useState(false);\n\n // The drop area is intentionally independent of per-file statuses. It\n // doesn't change copy, busy-state, or cursor automatically based on\n // whether any row is uploading — the rows speak for themselves via\n // their own per-row meta + progress strip. The two state spaces stay\n // orthogonal: caller declares drop-area state explicitly via the\n // `state` prop, and sets per-file lifecycle via each `file.status`.\n //\n // `uploading` here is an explicit drop-area state for cases the rows\n // can't carry: batch lockouts, pre-file validation, indeterminate\n // chunked uploads without per-file callbacks. Caller drives it.\n const isUploading = forcedState === 'uploading';\n\n // Atom-level error: caller set `error` (or the deprecated `errorMessage`\n // alias) OR forced state to \"error\". Per-file errors are NOT reflected here —\n // they're each row's concern.\n const pageError = error ?? errorMessage;\n const isPageError = forcedState === 'error' || (!forcedState && !!pageError);\n\n const dataState: FileUploadState = forcedState\n ? forcedState\n : internalDrag\n ? 'drag-over'\n : isPageError\n ? 'error'\n : 'default';\n\n const handleChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>) => {\n if (disabled) return;\n const list = e.target.files;\n if (!list || list.length === 0) return;\n onFiles?.(Array.from(list));\n // Clear so re-selecting the same file re-fires onChange.\n e.target.value = '';\n },\n [disabled, onFiles],\n );\n\n const handleDragEnter = useCallback(\n (e: DragEvent<HTMLLabelElement>) => {\n if (disabled) return;\n e.preventDefault();\n setInternalDrag(true);\n onDragEnter?.(e);\n },\n [disabled, onDragEnter],\n );\n\n const handleDragOver = useCallback(\n (e: DragEvent<HTMLLabelElement>) => {\n if (disabled) return;\n e.preventDefault();\n },\n [disabled],\n );\n\n const handleDragLeave = useCallback(\n (e: DragEvent<HTMLLabelElement>) => {\n if (disabled) return;\n e.preventDefault();\n setInternalDrag(false);\n onDragLeave?.(e);\n },\n [disabled, onDragLeave],\n );\n\n const handleDrop = useCallback(\n (e: DragEvent<HTMLLabelElement>) => {\n if (disabled) return;\n e.preventDefault();\n setInternalDrag(false);\n const list = e.dataTransfer?.files;\n if (!list || list.length === 0) return;\n onFiles?.(Array.from(list));\n },\n [disabled, onFiles],\n );\n\n const resolvedTitle = titleText ?? 'Upload a file';\n const baseHelp =\n helpText ??\n (multiple\n ? 'Drop files here, or click to browse'\n : 'Drop a file here, or click to browse');\n // The uploading state owns the help slot: it swaps to \"Uploading…\" so\n // the drop area visibly reflects the caller-declared busy phase. Falls\n // back to the normal copy in every other state.\n const resolvedHelp = isUploading ? 'Uploading…' : baseHelp;\n\n return (\n <div className={cn('uxm-file-upload-wrapper', className)} style={style} {...rest}>\n <label\n className=\"uxm-file-upload\"\n htmlFor={inputId}\n data-state={dataState}\n data-disabled={disabled ? 'true' : undefined}\n aria-busy={isUploading || undefined}\n onDragEnter={handleDragEnter}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n onDrop={handleDrop}\n >\n <input\n id={inputId}\n className=\"uxm-file-upload__input\"\n type=\"file\"\n multiple={multiple}\n accept={accept}\n disabled={disabled}\n aria-invalid={isPageError || undefined}\n aria-describedby={helpId}\n onChange={handleChange}\n />\n <span className=\"uxm-file-upload__icon\" aria-hidden=\"true\">\n <Icon glyph={iconGlyph} size={40} />\n </span>\n <span className=\"uxm-file-upload__title\">{resolvedTitle}</span>\n <span id={helpId} className=\"uxm-file-upload__help\">\n {resolvedHelp}\n </span>\n {allowedTypesText && !isUploading && (\n <span className=\"uxm-file-upload__allowed-types\">{allowedTypesText}</span>\n )}\n </label>\n {pageError && <span className=\"uxm-file-upload__error\">{pageError}</span>}\n {children}\n {files.length > 0 && (\n <ul className=\"uxm-file-upload__list\">\n {files.map((file) => {\n const status: FileStatus = file.status ?? 'queued';\n const isUploading = status === 'uploading';\n const isDone = status === 'done';\n const isFileError = status === 'error';\n const pctFraction =\n file.progress != null\n ? Math.max(0, Math.min(1, file.progress))\n : undefined;\n const pctLabel =\n pctFraction != null ? `${Math.round(pctFraction * 100)}%` : undefined;\n const pctWidth = pctFraction != null ? `${pctFraction * 100}%` : '0%';\n\n // Icon glyph + color come from the row's status. Default knobs\n // (rowIconColor etc.) cover queued/uploading; done + error\n // route through dedicated vars the CSS reads via [data-status].\n const iconForRow = isDone\n ? 'check-circle'\n : isFileError\n ? 'exclamation-circle'\n : 'document';\n\n // Meta line content swaps with status:\n // queued / done → file size\n // uploading → \"Uploading · 61% of 471.0 KB\" (or \"Uploading…\" if no progress)\n // error → per-file errorMessage (or generic fallback)\n const sizeLabel = formatBytes(file.size);\n const metaText = isUploading\n ? pctLabel != null\n ? `Uploading · ${pctLabel} of ${sizeLabel}`\n : 'Uploading…'\n : isFileError\n ? file.errorMessage ?? 'Upload failed'\n : sizeLabel;\n\n return (\n <li\n key={file.id}\n className=\"uxm-file-upload__row\"\n data-status={status}\n >\n <span className=\"uxm-file-upload__row-icon\" aria-hidden=\"true\">\n <Icon glyph={iconForRow} size={20} />\n </span>\n <span className=\"uxm-file-upload__row-body\">\n <span className=\"uxm-file-upload__row-name\">{file.name}</span>\n <span className=\"uxm-file-upload__row-meta\">{metaText}</span>\n </span>\n {/* Download / open. `done` only — there is nothing stored to\n fetch until the upload settles. `onOpenFile` wins over\n `href` so a consumer that needs an auth header or a signed\n URL is never silently downgraded to a bare navigation. */}\n {listHasDownloads &&\n (isDone && (onOpenFile || file.href) ? (\n onOpenFile ? (\n <button\n type=\"button\"\n className=\"uxm-file-upload__row-download\"\n aria-label={`${downloadLabel} ${file.name}`}\n disabled={disabled}\n onClick={() => onOpenFile(file.id)}\n >\n <Icon glyph={downloadGlyph} size={16} />\n </button>\n ) : (\n // eslint-disable-next-line jsx-a11y/anchor-is-valid -- `href` is dropped ONLY while `disabled`, which is the point: an anchor that keeps its href stays focusable and Enter still follows it, so a disabled control would remain usable from the keyboard. The rule cannot model a conditionally-inert anchor; when enabled this always has a valid href.\n <a\n className=\"uxm-file-upload__row-download\"\n // DROPPED while disabled, not merely styled out. An\n // anchor keeps `href` in the tab order and Enter still\n // follows it, so `pointer-events: none` disables only\n // the mouse — a keyboard user could still download from\n // a form the consumer switched off. Without `href` the\n // element is neither focusable nor a link.\n href={disabled ? undefined : file.href}\n // Names the saved file: without this the browser uses\n // the URL's last segment, so a storage key lands on\n // disk as a UUID. Ignored cross-origin, along with the\n // download behaviour itself — see `href`'s docs.\n download={file.name}\n // Cross-origin, `download` is ignored and this becomes a\n // real navigation — into a NEW tab, so the user does not\n // lose the app and whatever is unsaved beside the list.\n // Same-origin the `download` attribute wins and no tab\n // opens, so this costs nothing there. It is also what\n // makes `rel=\"noopener\"` mean anything.\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n aria-label={`${downloadLabel} ${file.name}`}\n aria-disabled={disabled || undefined}\n >\n <Icon glyph={downloadGlyph} size={16} />\n </a>\n )\n ) : (\n // Inert placeholder keeping the column width stable across\n // the uploading → done transition.\n <span className=\"uxm-file-upload__row-download-slot\" aria-hidden=\"true\" />\n ))}\n <button\n type=\"button\"\n className=\"uxm-file-upload__row-remove\"\n aria-label={\n isUploading ? `Cancel ${file.name}` : `Remove ${file.name}`\n }\n disabled={disabled}\n onClick={() => onRemove?.(file.id)}\n >\n <Icon glyph=\"trash\" size={16} />\n </button>\n {isUploading && (\n <span\n className=\"uxm-file-upload__row-progress\"\n style={\n { '--uxm-file-upload-row-progress': pctWidth } as CSSProperties\n }\n role=\"progressbar\"\n aria-valuenow={pctFraction != null ? Math.round(pctFraction * 100) : undefined}\n aria-valuemin={0}\n aria-valuemax={100}\n />\n )}\n </li>\n );\n })}\n </ul>\n )}\n </div>\n );\n}\n\nfunction formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;\n return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;\n}\n"],"mappings":"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","names":["isUploading"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/ui/file-upload/file-upload.tsx"],"sourcesContent":["import {\n useCallback,\n useId,\n useState,\n type ChangeEvent,\n type CSSProperties,\n type DragEvent,\n type HTMLAttributes,\n type ReactNode,\n} from 'react';\n\nimport { cn } from '@/helpers';\n\nimport { Icon } from '../icon';\nimport { useUxmLocale } from '../locale';\n\n/**\n * Visual states the FileUpload **drop area** can be forced into via the\n * `state` prop. These describe the drop area surface only — not per-file\n * lifecycle. Per-file states (queued / uploading / done / error) live on\n * each `FileUploadFileMeta.status` and render row-by-row independently.\n *\n * `uploading` is **caller-controlled** — the atom never sets it from per-\n * file statuses. Use it for page-level signals: a batch is being prepared\n * before per-file rows exist, a chunked upload with no per-file progress,\n * or any \"the system is busy with this dropzone\" state the caller wants\n * to surface. Per-row uploading rows remain the *primary* progress signal.\n *\n * `error` means a *page-level* error (e.g. \"Connection lost\", \"Server\n * rejected the batch\") — surfaced via the `errorMessage` prop and shown\n * below the drop area. Per-file errors don't change the drop area's state.\n */\nexport type FileUploadState = 'default' | 'drag-over' | 'uploading' | 'error';\n\n/**\n * Per-file lifecycle status. The atom paints each row based on its own\n * status independently of other rows.\n *\n * - `queued`: file is added but not yet sent. Neutral row appearance.\n * - `uploading`: file is in flight. Row shows a progress strip and a\n * verbose meta line (`labels.uploadProgress`). Caller drives\n * `progress` (0–1).\n * - `done`: file completed. Document icon swaps to a success check.\n * - `error`: file failed. Border + icon + meta switch to danger styling;\n * the per-file `errorMessage` replaces the size meta.\n */\nexport type FileStatus = 'queued' | 'uploading' | 'done' | 'error';\n\nexport interface FileUploadFileMeta {\n /** Stable id for keying + removal callbacks. */\n id: string;\n /** Filename to display in the row. */\n name: string;\n /** Size in bytes. Formatted for display by the atom. */\n size: number;\n /**\n * Per-file lifecycle status. Defaults to `\"queued\"` if omitted. Drives\n * the row's icon, meta line, border, and progress strip.\n */\n status?: FileStatus;\n /**\n * 0–1 progress fraction. Used only when `status === \"uploading\"`.\n * If omitted during uploading, the row shows an indeterminate state\n * (`labels.uploading`) and no fraction.\n */\n progress?: number;\n /**\n * Per-file error message. Used only when `status === \"error\"`.\n * Replaces the size meta on the row in danger styling.\n */\n errorMessage?: string;\n /**\n * Direct URL for a stored file. On a `done` row this renders a trailing\n * control as a real `<a href download>` — which is why it exists alongside\n * `onOpenFile` rather than being replaced by it: only an anchor gives the\n * browser's own affordances (⌘/middle-click to a new tab, right-click →\n * \"Save link as\", the native download UI).\n *\n * ⚠️ Use this ONLY when the browser can fetch the URL unauthenticated. A\n * plain navigation carries no `Authorization` header, so a token-protected\n * URL returns 401 — reach for `onOpenFile` there.\n *\n * ⚠️ The `download` attribute is ignored CROSS-ORIGIN: if the file is\n * served from another origin the browser navigates to it instead of saving\n * it. Pass `downloadGlyph=\"arrow-up-right\"` so the control does not promise\n * a save it cannot perform, or route through `onOpenFile` and a blob.\n */\n href?: string;\n}\n\n/**\n * Copy the atom generates on its own — the strings that have no dedicated\n * prop because they are per-row or per-state rather than per-instance.\n * Merged over the English defaults, so a consumer overrides only what it\n * needs to translate.\n *\n * The two composed names take the filename as an argument rather than a\n * prefix string: word order around a filename differs by language, and a\n * `\"Remove\" + name` concatenation cannot express that.\n */\nexport interface FileUploadLabels {\n /** Busy copy — replaces the help line and any progressless row. Default `\"Uploading…\"`. */\n uploading?: string;\n /** Row meta fallback when a failed file carries no `errorMessage`. Default `\"Upload failed\"`. */\n uploadFailed?: string;\n /** Row meta while uploading with known progress. Default `` `Uploading · ${percent} of ${size}` ``. */\n uploadProgress?: (percent: string, size: string) => string;\n /** Row trash button, idle file. Default `` `Remove ${name}` ``. */\n removeFile?: (name: string) => string;\n /** Row trash button, in-flight file. Default `` `Cancel ${name}` ``. */\n cancelFile?: (name: string) => string;\n}\n\nconst DEFAULT_LABELS = {\n uploading: 'Uploading…',\n uploadFailed: 'Upload failed',\n uploadProgress: (percent: string, size: string) => `Uploading · ${percent} of ${size}`,\n removeFile: (name: string) => `Remove ${name}`,\n cancelFile: (name: string) => `Cancel ${name}`,\n} satisfies Required<FileUploadLabels>;\n\nexport interface FileUploadProps\n extends Omit<HTMLAttributes<HTMLDivElement>, 'onError' | 'onDragEnter' | 'onDragLeave'> {\n /**\n * Forced drop-area visual state — overrides internal drag detection.\n * The workbench uses this to paint each state without real interaction.\n * Omit in production: the atom flips to `drag-over` itself while\n * dragging, and to `error` when `errorMessage` is set.\n *\n * Per-file states (uploading / done / per-file errors) are NOT atom-\n * level — they live on each `FileUploadFileMeta.status`.\n */\n state?: FileUploadState;\n /** Allow selecting more than one file. Defaults to `true`. */\n multiple?: boolean;\n /** Standard `<input type=\"file\" accept>` filter, e.g. `\"image/*\"` or `\".pdf,.docx\"`. */\n accept?: string;\n /** Disable interaction. Also disables drop and all per-row remove buttons. */\n disabled?: boolean;\n /** Heading text inside the drop area. */\n titleText?: string;\n /**\n * Hint line below the title. Falls back to a `multiple`-aware default\n * (\"Drop files here, or click to browse\" vs \"Drop a file here, or click\n * to browse\"). When the caller sets `state=\"uploading\"`, the atom\n * temporarily replaces this with `labels.uploading` — caller's `helpText`\n * value is restored once they flip back to default.\n *\n * Per-file uploading rows do NOT trigger this swap on their own — the\n * drop-area copy reflects the caller's declared state, not aggregate\n * row status. Rows carry their own \"Uploading · 60% of …\" indicators.\n */\n helpText?: string;\n /**\n * Optional constraint line below the help text — e.g. \"PDF, DOCX · up to\n * 10 MB\". Caller-controlled copy describing what the page accepts, so it\n * stays in sync with whatever `accept` filter and validation the page\n * enforces in `onFiles`. The atom never invents this string — when\n * omitted, no slot renders. The atom does not enforce anything in it.\n */\n allowedTypesText?: string;\n /**\n * Page-level error message to render below the drop area (e.g. \"Connection\n * lost\", \"Server rejected the batch\"). When set, the atom paints the drop\n * area in the `error` state automatically. Named `error` to match the rest\n * of the input family's `error?: string`.\n *\n * Distinct from per-file errors — set `status: \"error\"` + `errorMessage`\n * on a `FileUploadFileMeta` to surface a single-file failure inline on\n * its row instead.\n */\n error?: string;\n /**\n * @deprecated Renamed to `error` (input-family convention). Still accepted\n * as an alias for back-compat; `error` wins when both are set.\n */\n errorMessage?: string;\n /**\n * Files to display in the inline list under the drop area. Rendered\n * automatically whenever `files.length > 0`. Each entry's `status`\n * drives its row appearance independently. To render the list elsewhere\n * on the page, omit `files` and manage that state yourself.\n */\n files?: FileUploadFileMeta[];\n /** Fires with the selected `File[]` whether the user picked via dialog or dropped. */\n onFiles?: (files: File[]) => void;\n /**\n * Fires when a row's remove button is clicked. The caller decides what\n * \"remove\" means per status — cancel an in-flight upload, drop a queued\n * file, delete a completed one server-side. The atom doesn't auto-disable\n * the button during `uploading` so the user always has an escape hatch.\n */\n onRemove?: (id: string) => void;\n /**\n * Fires when a row's download control is activated. Set this when the\n * stored file needs an auth header or a signed-URL round trip: the consumer\n * fetches the bytes itself and saves them (typically `fetch` with the\n * bearer, then `URL.createObjectURL` — remember to `revokeObjectURL`, and\n * to pass the filename through, or the file lands named `blob`).\n *\n * Takes PRECEDENCE over `FileUploadFileMeta.href`: a consumer that supplied\n * a handler wants control of what opening means. Set neither and no\n * download control renders, which is how every existing consumer behaves.\n */\n onOpenFile?: (id: string) => void;\n /**\n * Glyph for the download control. Defaults to `\"arrow-down\"`. Override to\n * `\"arrow-up-right\"` when the file opens in a tab rather than saving —\n * cross-origin `href`, or an `onOpenFile` that previews in-app.\n */\n downloadGlyph?: string;\n /**\n * Verb in the download control's accessible name, rendered as\n * `` `${downloadLabel} ${file.name}` ``. Defaults to `\"Download\"`.\n *\n * Change it together with `downloadGlyph` whenever the control opens rather\n * than saves: the glyph alone corrects the promise for sighted users and\n * leaves screen-reader users being told \"Download\" for a control that\n * navigates. `downloadLabel=\"Open\"` keeps the two in step.\n */\n downloadLabel?: string;\n /** Fires when a drag enters the drop area. */\n onDragEnter?: (e: DragEvent<HTMLLabelElement>) => void;\n /** Fires when a drag leaves the drop area. */\n onDragLeave?: (e: DragEvent<HTMLLabelElement>) => void;\n /** Fires when a drop is rejected (e.g. wrong type — for caller-driven validation). */\n onError?: (reason: string) => void;\n /**\n * Accessible names and row copy the atom generates itself. Merged over the\n * English defaults. `titleText` / `helpText` / `allowedTypesText` stay\n * separate props — they are per-instance copy, not per-row.\n */\n labels?: FileUploadLabels;\n /**\n * BCP-47 locale for the file-size formatter. Defaults to the nearest\n * `UxmLocaleProvider`, then to `\"en-US\"`. Drives the decimal separator and\n * the localised byte unit (`\"471.0 kB\"` vs `\"471,0 кБ\"`). Ignored when\n * `formatSize` is supplied.\n */\n locale?: string;\n /**\n * Replace the built-in file-size formatter wholesale. Receives the raw byte\n * count from `FileUploadFileMeta.size`. Use this when the default\n * `Intl`-formatted SI units don't match the product's house style (e.g. an\n * app that insists on binary `KiB` / `MiB`).\n */\n formatSize?: (bytes: number) => string;\n /** Optional override for the drop-area icon glyph. */\n iconGlyph?: string;\n /** Optional slot rendered above the file list (e.g. a summary line). */\n children?: ReactNode;\n}\n\nexport function FileUpload({\n state: forcedState,\n multiple = true,\n accept,\n disabled = false,\n titleText,\n helpText,\n allowedTypesText,\n error,\n errorMessage,\n files = [],\n onFiles,\n onRemove,\n onOpenFile,\n downloadGlyph = 'arrow-down',\n downloadLabel = 'Download',\n onDragEnter,\n onDragLeave,\n onError: _onError,\n labels,\n locale: localeProp,\n formatSize,\n className,\n style,\n iconGlyph = 'cloud-arrow-up',\n children,\n ...rest\n}: FileUploadProps) {\n const l = { ...DEFAULT_LABELS, ...labels };\n const locale = useUxmLocale(localeProp);\n const sizeFormatter = formatSize ?? ((bytes: number) => formatBytes(bytes, locale));\n const inputId = useId();\n const helpId = useId();\n\n // Whether the list has a download COLUMN at all — a whole-list property, so\n // it is computed once here rather than per row (inside the map it re-scanned\n // `files` for every row, which is O(n²) on exactly the renders a list does\n // most: one per progress tick during an upload).\n //\n // The column exists so the control can be `done`-only without the name column\n // gaining 24px the moment an upload settles — a reflow of the row the user is\n // watching. Rows with nothing to fetch render an inert placeholder instead.\n const listHasDownloads = !!onOpenFile || files.some((f) => !!f.href);\n const [internalDrag, setInternalDrag] = useState(false);\n\n // The drop area is intentionally independent of per-file statuses. It\n // doesn't change copy, busy-state, or cursor automatically based on\n // whether any row is uploading — the rows speak for themselves via\n // their own per-row meta + progress strip. The two state spaces stay\n // orthogonal: caller declares drop-area state explicitly via the\n // `state` prop, and sets per-file lifecycle via each `file.status`.\n //\n // `uploading` here is an explicit drop-area state for cases the rows\n // can't carry: batch lockouts, pre-file validation, indeterminate\n // chunked uploads without per-file callbacks. Caller drives it.\n const isUploading = forcedState === 'uploading';\n\n // Atom-level error: caller set `error` (or the deprecated `errorMessage`\n // alias) OR forced state to \"error\". Per-file errors are NOT reflected here —\n // they're each row's concern.\n const pageError = error ?? errorMessage;\n const isPageError = forcedState === 'error' || (!forcedState && !!pageError);\n\n const dataState: FileUploadState = forcedState\n ? forcedState\n : internalDrag\n ? 'drag-over'\n : isPageError\n ? 'error'\n : 'default';\n\n const handleChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>) => {\n if (disabled) return;\n const list = e.target.files;\n if (!list || list.length === 0) return;\n onFiles?.(Array.from(list));\n // Clear so re-selecting the same file re-fires onChange.\n e.target.value = '';\n },\n [disabled, onFiles],\n );\n\n const handleDragEnter = useCallback(\n (e: DragEvent<HTMLLabelElement>) => {\n if (disabled) return;\n e.preventDefault();\n setInternalDrag(true);\n onDragEnter?.(e);\n },\n [disabled, onDragEnter],\n );\n\n const handleDragOver = useCallback(\n (e: DragEvent<HTMLLabelElement>) => {\n if (disabled) return;\n e.preventDefault();\n },\n [disabled],\n );\n\n const handleDragLeave = useCallback(\n (e: DragEvent<HTMLLabelElement>) => {\n if (disabled) return;\n e.preventDefault();\n setInternalDrag(false);\n onDragLeave?.(e);\n },\n [disabled, onDragLeave],\n );\n\n const handleDrop = useCallback(\n (e: DragEvent<HTMLLabelElement>) => {\n if (disabled) return;\n e.preventDefault();\n setInternalDrag(false);\n const list = e.dataTransfer?.files;\n if (!list || list.length === 0) return;\n onFiles?.(Array.from(list));\n },\n [disabled, onFiles],\n );\n\n const resolvedTitle = titleText ?? 'Upload a file';\n const baseHelp =\n helpText ??\n (multiple\n ? 'Drop files here, or click to browse'\n : 'Drop a file here, or click to browse');\n // The uploading state owns the help slot: it swaps to `labels.uploading` so\n // the drop area visibly reflects the caller-declared busy phase. Falls\n // back to the normal copy in every other state.\n const resolvedHelp = isUploading ? l.uploading : baseHelp;\n\n return (\n <div className={cn('uxm-file-upload-wrapper', className)} style={style} {...rest}>\n <label\n className=\"uxm-file-upload\"\n htmlFor={inputId}\n data-state={dataState}\n data-disabled={disabled ? 'true' : undefined}\n aria-busy={isUploading || undefined}\n onDragEnter={handleDragEnter}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n onDrop={handleDrop}\n >\n <input\n id={inputId}\n className=\"uxm-file-upload__input\"\n type=\"file\"\n multiple={multiple}\n accept={accept}\n disabled={disabled}\n aria-invalid={isPageError || undefined}\n aria-describedby={helpId}\n onChange={handleChange}\n />\n <span className=\"uxm-file-upload__icon\" aria-hidden=\"true\">\n <Icon glyph={iconGlyph} size={40} />\n </span>\n <span className=\"uxm-file-upload__title\">{resolvedTitle}</span>\n <span id={helpId} className=\"uxm-file-upload__help\">\n {resolvedHelp}\n </span>\n {allowedTypesText && !isUploading && (\n <span className=\"uxm-file-upload__allowed-types\">{allowedTypesText}</span>\n )}\n </label>\n {pageError && <span className=\"uxm-file-upload__error\">{pageError}</span>}\n {children}\n {files.length > 0 && (\n <ul className=\"uxm-file-upload__list\">\n {files.map((file) => {\n const status: FileStatus = file.status ?? 'queued';\n const isUploading = status === 'uploading';\n const isDone = status === 'done';\n const isFileError = status === 'error';\n const pctFraction =\n file.progress != null\n ? Math.max(0, Math.min(1, file.progress))\n : undefined;\n const pctLabel =\n pctFraction != null ? `${Math.round(pctFraction * 100)}%` : undefined;\n const pctWidth = pctFraction != null ? `${pctFraction * 100}%` : '0%';\n\n // Icon glyph + color come from the row's status. Default knobs\n // (rowIconColor etc.) cover queued/uploading; done + error\n // route through dedicated vars the CSS reads via [data-status].\n const iconForRow = isDone\n ? 'check-circle'\n : isFileError\n ? 'exclamation-circle'\n : 'document';\n\n // Meta line content swaps with status:\n // queued / done → file size\n // uploading → labels.uploadProgress (or labels.uploading if no progress)\n // error → per-file errorMessage (or generic fallback)\n const sizeLabel = sizeFormatter(file.size);\n const metaText = isUploading\n ? pctLabel != null\n ? l.uploadProgress(pctLabel, sizeLabel)\n : l.uploading\n : isFileError\n ? file.errorMessage ?? l.uploadFailed\n : sizeLabel;\n\n return (\n <li\n key={file.id}\n className=\"uxm-file-upload__row\"\n data-status={status}\n >\n <span className=\"uxm-file-upload__row-icon\" aria-hidden=\"true\">\n <Icon glyph={iconForRow} size={20} />\n </span>\n <span className=\"uxm-file-upload__row-body\">\n <span className=\"uxm-file-upload__row-name\">{file.name}</span>\n <span className=\"uxm-file-upload__row-meta\">{metaText}</span>\n </span>\n {/* Download / open. `done` only — there is nothing stored to\n fetch until the upload settles. `onOpenFile` wins over\n `href` so a consumer that needs an auth header or a signed\n URL is never silently downgraded to a bare navigation. */}\n {listHasDownloads &&\n (isDone && (onOpenFile || file.href) ? (\n onOpenFile ? (\n <button\n type=\"button\"\n className=\"uxm-file-upload__row-download\"\n aria-label={`${downloadLabel} ${file.name}`}\n disabled={disabled}\n onClick={() => onOpenFile(file.id)}\n >\n <Icon glyph={downloadGlyph} size={16} />\n </button>\n ) : (\n // eslint-disable-next-line jsx-a11y/anchor-is-valid -- `href` is dropped ONLY while `disabled`, which is the point: an anchor that keeps its href stays focusable and Enter still follows it, so a disabled control would remain usable from the keyboard. The rule cannot model a conditionally-inert anchor; when enabled this always has a valid href.\n <a\n className=\"uxm-file-upload__row-download\"\n // DROPPED while disabled, not merely styled out. An\n // anchor keeps `href` in the tab order and Enter still\n // follows it, so `pointer-events: none` disables only\n // the mouse — a keyboard user could still download from\n // a form the consumer switched off. Without `href` the\n // element is neither focusable nor a link.\n href={disabled ? undefined : file.href}\n // Names the saved file: without this the browser uses\n // the URL's last segment, so a storage key lands on\n // disk as a UUID. Ignored cross-origin, along with the\n // download behaviour itself — see `href`'s docs.\n download={file.name}\n // Cross-origin, `download` is ignored and this becomes a\n // real navigation — into a NEW tab, so the user does not\n // lose the app and whatever is unsaved beside the list.\n // Same-origin the `download` attribute wins and no tab\n // opens, so this costs nothing there. It is also what\n // makes `rel=\"noopener\"` mean anything.\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n aria-label={`${downloadLabel} ${file.name}`}\n aria-disabled={disabled || undefined}\n >\n <Icon glyph={downloadGlyph} size={16} />\n </a>\n )\n ) : (\n // Inert placeholder keeping the column width stable across\n // the uploading → done transition.\n <span className=\"uxm-file-upload__row-download-slot\" aria-hidden=\"true\" />\n ))}\n <button\n type=\"button\"\n className=\"uxm-file-upload__row-remove\"\n aria-label={\n isUploading ? l.cancelFile(file.name) : l.removeFile(file.name)\n }\n disabled={disabled}\n onClick={() => onRemove?.(file.id)}\n >\n <Icon glyph=\"trash\" size={16} />\n </button>\n {isUploading && (\n <span\n className=\"uxm-file-upload__row-progress\"\n style={\n { '--uxm-file-upload-row-progress': pctWidth } as CSSProperties\n }\n role=\"progressbar\"\n aria-valuenow={pctFraction != null ? Math.round(pctFraction * 100) : undefined}\n aria-valuemin={0}\n aria-valuemax={100}\n />\n )}\n </li>\n );\n })}\n </ul>\n )}\n </div>\n );\n}\n\n// SI, not binary, and deliberately so: the unit names below come from `Intl`\n// (`kilobyte`, `megabyte`, `gigabyte`), and those are defined as powers of\n// 1000. Dividing by 1024 while printing `kB` would misstate the size in every\n// locale, and `Intl` has no binary unit to switch to — the sanctioned list has\n// no `kibibyte`. Consumers who want binary units own the whole formatter via\n// `formatSize` (`KiB` / `MiB`), which is why that prop exists.\nconst KB = 1000;\nconst MB = KB * 1000;\nconst GB = MB * 1000;\n\n/**\n * Locale-aware size formatter. `Intl`'s `style: \"unit\"` supplies both the\n * decimal separator and the translated unit, which a hand-rolled\n * `` `${n.toFixed(1)} KB` `` cannot: `de-DE` needs a comma, `uk-UA` needs\n * `кБ`, `fr-FR` counts in octets. Bytes render whole; everything above keeps\n * one fraction digit, as before.\n *\n * The byte tier uses `unitDisplay: \"long\"` while the rest use `\"short\"`, and\n * that asymmetry is deliberate. CLDR's *short* byte form is neither short nor\n * pluralised in English — `Intl` renders `\"512 byte\"` — whereas the long form\n * pluralises correctly in every locale (`512 bytes`, `512 байтів`,\n * `512 bajtów`). It is also the one tier that can afford the extra characters,\n * since the number itself is at most three digits.\n *\n * Older `Intl` implementations reject `style: \"unit\"` with a RangeError, so\n * the English SI form stays as a fallback rather than crashing the row.\n */\nfunction formatBytes(bytes: number, locale: string): string {\n const [value, unit, fallbackUnit] =\n bytes < KB\n ? ([bytes, 'byte', 'B'] as const)\n : bytes < MB\n ? ([bytes / KB, 'kilobyte', 'kB'] as const)\n : bytes < GB\n ? ([bytes / MB, 'megabyte', 'MB'] as const)\n : ([bytes / GB, 'gigabyte', 'GB'] as const);\n const isBytes = unit === 'byte';\n const fractionDigits = isBytes ? 0 : 1;\n try {\n return new Intl.NumberFormat(locale, {\n style: 'unit',\n unit,\n unitDisplay: isBytes ? 'long' : 'short',\n minimumFractionDigits: fractionDigits,\n maximumFractionDigits: fractionDigits,\n }).format(value);\n } catch {\n return `${value.toFixed(fractionDigits)} ${fallbackUnit}`;\n }\n}\n"],"mappings":"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","names":["isUploading"]}
|
|
@@ -22,7 +22,7 @@ export type FileUploadState = 'default' | 'drag-over' | 'uploading' | 'error';
|
|
|
22
22
|
*
|
|
23
23
|
* - `queued`: file is added but not yet sent. Neutral row appearance.
|
|
24
24
|
* - `uploading`: file is in flight. Row shows a progress strip and a
|
|
25
|
-
* verbose meta line (
|
|
25
|
+
* verbose meta line (`labels.uploadProgress`). Caller drives
|
|
26
26
|
* `progress` (0–1).
|
|
27
27
|
* - `done`: file completed. Document icon swaps to a success check.
|
|
28
28
|
* - `error`: file failed. Border + icon + meta switch to danger styling;
|
|
@@ -44,7 +44,7 @@ export interface FileUploadFileMeta {
|
|
|
44
44
|
/**
|
|
45
45
|
* 0–1 progress fraction. Used only when `status === "uploading"`.
|
|
46
46
|
* If omitted during uploading, the row shows an indeterminate state
|
|
47
|
-
* (
|
|
47
|
+
* (`labels.uploading`) and no fraction.
|
|
48
48
|
*/
|
|
49
49
|
progress?: number;
|
|
50
50
|
/**
|
|
@@ -70,6 +70,28 @@ export interface FileUploadFileMeta {
|
|
|
70
70
|
*/
|
|
71
71
|
href?: string;
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Copy the atom generates on its own — the strings that have no dedicated
|
|
75
|
+
* prop because they are per-row or per-state rather than per-instance.
|
|
76
|
+
* Merged over the English defaults, so a consumer overrides only what it
|
|
77
|
+
* needs to translate.
|
|
78
|
+
*
|
|
79
|
+
* The two composed names take the filename as an argument rather than a
|
|
80
|
+
* prefix string: word order around a filename differs by language, and a
|
|
81
|
+
* `"Remove" + name` concatenation cannot express that.
|
|
82
|
+
*/
|
|
83
|
+
export interface FileUploadLabels {
|
|
84
|
+
/** Busy copy — replaces the help line and any progressless row. Default `"Uploading…"`. */
|
|
85
|
+
uploading?: string;
|
|
86
|
+
/** Row meta fallback when a failed file carries no `errorMessage`. Default `"Upload failed"`. */
|
|
87
|
+
uploadFailed?: string;
|
|
88
|
+
/** Row meta while uploading with known progress. Default `` `Uploading · ${percent} of ${size}` ``. */
|
|
89
|
+
uploadProgress?: (percent: string, size: string) => string;
|
|
90
|
+
/** Row trash button, idle file. Default `` `Remove ${name}` ``. */
|
|
91
|
+
removeFile?: (name: string) => string;
|
|
92
|
+
/** Row trash button, in-flight file. Default `` `Cancel ${name}` ``. */
|
|
93
|
+
cancelFile?: (name: string) => string;
|
|
94
|
+
}
|
|
73
95
|
export interface FileUploadProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onError' | 'onDragEnter' | 'onDragLeave'> {
|
|
74
96
|
/**
|
|
75
97
|
* Forced drop-area visual state — overrides internal drag detection.
|
|
@@ -93,7 +115,7 @@ export interface FileUploadProps extends Omit<HTMLAttributes<HTMLDivElement>, 'o
|
|
|
93
115
|
* Hint line below the title. Falls back to a `multiple`-aware default
|
|
94
116
|
* ("Drop files here, or click to browse" vs "Drop a file here, or click
|
|
95
117
|
* to browse"). When the caller sets `state="uploading"`, the atom
|
|
96
|
-
* temporarily replaces this with
|
|
118
|
+
* temporarily replaces this with `labels.uploading` — caller's `helpText`
|
|
97
119
|
* value is restored once they flip back to default.
|
|
98
120
|
*
|
|
99
121
|
* Per-file uploading rows do NOT trigger this swap on their own — the
|
|
@@ -175,10 +197,30 @@ export interface FileUploadProps extends Omit<HTMLAttributes<HTMLDivElement>, 'o
|
|
|
175
197
|
onDragLeave?: (e: DragEvent<HTMLLabelElement>) => void;
|
|
176
198
|
/** Fires when a drop is rejected (e.g. wrong type — for caller-driven validation). */
|
|
177
199
|
onError?: (reason: string) => void;
|
|
200
|
+
/**
|
|
201
|
+
* Accessible names and row copy the atom generates itself. Merged over the
|
|
202
|
+
* English defaults. `titleText` / `helpText` / `allowedTypesText` stay
|
|
203
|
+
* separate props — they are per-instance copy, not per-row.
|
|
204
|
+
*/
|
|
205
|
+
labels?: FileUploadLabels;
|
|
206
|
+
/**
|
|
207
|
+
* BCP-47 locale for the file-size formatter. Defaults to the nearest
|
|
208
|
+
* `UxmLocaleProvider`, then to `"en-US"`. Drives the decimal separator and
|
|
209
|
+
* the localised byte unit (`"471.0 kB"` vs `"471,0 кБ"`). Ignored when
|
|
210
|
+
* `formatSize` is supplied.
|
|
211
|
+
*/
|
|
212
|
+
locale?: string;
|
|
213
|
+
/**
|
|
214
|
+
* Replace the built-in file-size formatter wholesale. Receives the raw byte
|
|
215
|
+
* count from `FileUploadFileMeta.size`. Use this when the default
|
|
216
|
+
* `Intl`-formatted SI units don't match the product's house style (e.g. an
|
|
217
|
+
* app that insists on binary `KiB` / `MiB`).
|
|
218
|
+
*/
|
|
219
|
+
formatSize?: (bytes: number) => string;
|
|
178
220
|
/** Optional override for the drop-area icon glyph. */
|
|
179
221
|
iconGlyph?: string;
|
|
180
222
|
/** Optional slot rendered above the file list (e.g. a summary line). */
|
|
181
223
|
children?: ReactNode;
|
|
182
224
|
}
|
|
183
|
-
export declare function FileUpload({ state: forcedState, multiple, accept, disabled, titleText, helpText, allowedTypesText, error, errorMessage, files, onFiles, onRemove, onOpenFile, downloadGlyph, downloadLabel, onDragEnter, onDragLeave, onError: _onError, className, style, iconGlyph, children, ...rest }: FileUploadProps): import("react/jsx-runtime").JSX.Element;
|
|
225
|
+
export declare function FileUpload({ state: forcedState, multiple, accept, disabled, titleText, helpText, allowedTypesText, error, errorMessage, files, onFiles, onRemove, onOpenFile, downloadGlyph, downloadLabel, onDragEnter, onDragLeave, onError: _onError, labels, locale: localeProp, formatSize, className, style, iconGlyph, children, ...rest }: FileUploadProps): import("react/jsx-runtime").JSX.Element;
|
|
184
226
|
//# sourceMappingURL=file-upload.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-upload.d.ts","sourceRoot":"","sources":["../../../src/ui/file-upload/file-upload.tsx"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"file-upload.d.ts","sourceRoot":"","sources":["../../../src/ui/file-upload/file-upload.tsx"],"names":[],"mappings":"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"}
|
|
@@ -6,6 +6,14 @@ import {
|
|
|
6
6
|
} from "react";
|
|
7
7
|
import { cn } from "../../helpers/index.js";
|
|
8
8
|
import { Icon } from "../icon/index.js";
|
|
9
|
+
import { useUxmLocale } from "../locale/index.js";
|
|
10
|
+
const DEFAULT_LABELS = {
|
|
11
|
+
uploading: "Uploading\u2026",
|
|
12
|
+
uploadFailed: "Upload failed",
|
|
13
|
+
uploadProgress: (percent, size) => `Uploading \xB7 ${percent} of ${size}`,
|
|
14
|
+
removeFile: (name) => `Remove ${name}`,
|
|
15
|
+
cancelFile: (name) => `Cancel ${name}`
|
|
16
|
+
};
|
|
9
17
|
function FileUpload({
|
|
10
18
|
state: forcedState,
|
|
11
19
|
multiple = true,
|
|
@@ -25,12 +33,18 @@ function FileUpload({
|
|
|
25
33
|
onDragEnter,
|
|
26
34
|
onDragLeave,
|
|
27
35
|
onError: _onError,
|
|
36
|
+
labels,
|
|
37
|
+
locale: localeProp,
|
|
38
|
+
formatSize,
|
|
28
39
|
className,
|
|
29
40
|
style,
|
|
30
41
|
iconGlyph = "cloud-arrow-up",
|
|
31
42
|
children,
|
|
32
43
|
...rest
|
|
33
44
|
}) {
|
|
45
|
+
const l = { ...DEFAULT_LABELS, ...labels };
|
|
46
|
+
const locale = useUxmLocale(localeProp);
|
|
47
|
+
const sizeFormatter = formatSize ?? ((bytes) => formatBytes(bytes, locale));
|
|
34
48
|
const inputId = useId();
|
|
35
49
|
const helpId = useId();
|
|
36
50
|
const listHasDownloads = !!onOpenFile || files.some((f) => !!f.href);
|
|
@@ -87,7 +101,7 @@ function FileUpload({
|
|
|
87
101
|
);
|
|
88
102
|
const resolvedTitle = titleText ?? "Upload a file";
|
|
89
103
|
const baseHelp = helpText ?? (multiple ? "Drop files here, or click to browse" : "Drop a file here, or click to browse");
|
|
90
|
-
const resolvedHelp = isUploading ?
|
|
104
|
+
const resolvedHelp = isUploading ? l.uploading : baseHelp;
|
|
91
105
|
return /* @__PURE__ */ jsxs("div", { className: cn("uxm-file-upload-wrapper", className), style, ...rest, children: [
|
|
92
106
|
/* @__PURE__ */ jsxs(
|
|
93
107
|
"label",
|
|
@@ -134,8 +148,8 @@ function FileUpload({
|
|
|
134
148
|
const pctLabel = pctFraction != null ? `${Math.round(pctFraction * 100)}%` : void 0;
|
|
135
149
|
const pctWidth = pctFraction != null ? `${pctFraction * 100}%` : "0%";
|
|
136
150
|
const iconForRow = isDone ? "check-circle" : isFileError ? "exclamation-circle" : "document";
|
|
137
|
-
const sizeLabel =
|
|
138
|
-
const metaText = isUploading2 ? pctLabel != null ?
|
|
151
|
+
const sizeLabel = sizeFormatter(file.size);
|
|
152
|
+
const metaText = isUploading2 ? pctLabel != null ? l.uploadProgress(pctLabel, sizeLabel) : l.uploading : isFileError ? file.errorMessage ?? l.uploadFailed : sizeLabel;
|
|
139
153
|
return /* @__PURE__ */ jsxs(
|
|
140
154
|
"li",
|
|
141
155
|
{
|
|
@@ -182,7 +196,7 @@ function FileUpload({
|
|
|
182
196
|
{
|
|
183
197
|
type: "button",
|
|
184
198
|
className: "uxm-file-upload__row-remove",
|
|
185
|
-
"aria-label": isUploading2 ?
|
|
199
|
+
"aria-label": isUploading2 ? l.cancelFile(file.name) : l.removeFile(file.name),
|
|
186
200
|
disabled,
|
|
187
201
|
onClick: () => onRemove?.(file.id),
|
|
188
202
|
children: /* @__PURE__ */ jsx(Icon, { glyph: "trash", size: 16 })
|
|
@@ -206,11 +220,24 @@ function FileUpload({
|
|
|
206
220
|
}) })
|
|
207
221
|
] });
|
|
208
222
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
223
|
+
const KB = 1e3;
|
|
224
|
+
const MB = KB * 1e3;
|
|
225
|
+
const GB = MB * 1e3;
|
|
226
|
+
function formatBytes(bytes, locale) {
|
|
227
|
+
const [value, unit, fallbackUnit] = bytes < KB ? [bytes, "byte", "B"] : bytes < MB ? [bytes / KB, "kilobyte", "kB"] : bytes < GB ? [bytes / MB, "megabyte", "MB"] : [bytes / GB, "gigabyte", "GB"];
|
|
228
|
+
const isBytes = unit === "byte";
|
|
229
|
+
const fractionDigits = isBytes ? 0 : 1;
|
|
230
|
+
try {
|
|
231
|
+
return new Intl.NumberFormat(locale, {
|
|
232
|
+
style: "unit",
|
|
233
|
+
unit,
|
|
234
|
+
unitDisplay: isBytes ? "long" : "short",
|
|
235
|
+
minimumFractionDigits: fractionDigits,
|
|
236
|
+
maximumFractionDigits: fractionDigits
|
|
237
|
+
}).format(value);
|
|
238
|
+
} catch {
|
|
239
|
+
return `${value.toFixed(fractionDigits)} ${fallbackUnit}`;
|
|
240
|
+
}
|
|
214
241
|
}
|
|
215
242
|
export {
|
|
216
243
|
FileUpload
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/ui/file-upload/file-upload.tsx"],"sourcesContent":["import {\n useCallback,\n useId,\n useState,\n type ChangeEvent,\n type CSSProperties,\n type DragEvent,\n type HTMLAttributes,\n type ReactNode,\n} from 'react';\n\nimport { cn } from '@/helpers';\n\nimport { Icon } from '../icon';\n\n/**\n * Visual states the FileUpload **drop area** can be forced into via the\n * `state` prop. These describe the drop area surface only — not per-file\n * lifecycle. Per-file states (queued / uploading / done / error) live on\n * each `FileUploadFileMeta.status` and render row-by-row independently.\n *\n * `uploading` is **caller-controlled** — the atom never sets it from per-\n * file statuses. Use it for page-level signals: a batch is being prepared\n * before per-file rows exist, a chunked upload with no per-file progress,\n * or any \"the system is busy with this dropzone\" state the caller wants\n * to surface. Per-row uploading rows remain the *primary* progress signal.\n *\n * `error` means a *page-level* error (e.g. \"Connection lost\", \"Server\n * rejected the batch\") — surfaced via the `errorMessage` prop and shown\n * below the drop area. Per-file errors don't change the drop area's state.\n */\nexport type FileUploadState = 'default' | 'drag-over' | 'uploading' | 'error';\n\n/**\n * Per-file lifecycle status. The atom paints each row based on its own\n * status independently of other rows.\n *\n * - `queued`: file is added but not yet sent. Neutral row appearance.\n * - `uploading`: file is in flight. Row shows a progress strip and a\n * verbose meta line (\"Uploading · 61% of 471.0 KB\"). Caller drives\n * `progress` (0–1).\n * - `done`: file completed. Document icon swaps to a success check.\n * - `error`: file failed. Border + icon + meta switch to danger styling;\n * the per-file `errorMessage` replaces the size meta.\n */\nexport type FileStatus = 'queued' | 'uploading' | 'done' | 'error';\n\nexport interface FileUploadFileMeta {\n /** Stable id for keying + removal callbacks. */\n id: string;\n /** Filename to display in the row. */\n name: string;\n /** Size in bytes. Formatted for display by the atom. */\n size: number;\n /**\n * Per-file lifecycle status. Defaults to `\"queued\"` if omitted. Drives\n * the row's icon, meta line, border, and progress strip.\n */\n status?: FileStatus;\n /**\n * 0–1 progress fraction. Used only when `status === \"uploading\"`.\n * If omitted during uploading, the row shows an indeterminate state\n * (\"Uploading…\") and no fraction.\n */\n progress?: number;\n /**\n * Per-file error message. Used only when `status === \"error\"`.\n * Replaces the size meta on the row in danger styling.\n */\n errorMessage?: string;\n /**\n * Direct URL for a stored file. On a `done` row this renders a trailing\n * control as a real `<a href download>` — which is why it exists alongside\n * `onOpenFile` rather than being replaced by it: only an anchor gives the\n * browser's own affordances (⌘/middle-click to a new tab, right-click →\n * \"Save link as\", the native download UI).\n *\n * ⚠️ Use this ONLY when the browser can fetch the URL unauthenticated. A\n * plain navigation carries no `Authorization` header, so a token-protected\n * URL returns 401 — reach for `onOpenFile` there.\n *\n * ⚠️ The `download` attribute is ignored CROSS-ORIGIN: if the file is\n * served from another origin the browser navigates to it instead of saving\n * it. Pass `downloadGlyph=\"arrow-up-right\"` so the control does not promise\n * a save it cannot perform, or route through `onOpenFile` and a blob.\n */\n href?: string;\n}\n\nexport interface FileUploadProps\n extends Omit<HTMLAttributes<HTMLDivElement>, 'onError' | 'onDragEnter' | 'onDragLeave'> {\n /**\n * Forced drop-area visual state — overrides internal drag detection.\n * The workbench uses this to paint each state without real interaction.\n * Omit in production: the atom flips to `drag-over` itself while\n * dragging, and to `error` when `errorMessage` is set.\n *\n * Per-file states (uploading / done / per-file errors) are NOT atom-\n * level — they live on each `FileUploadFileMeta.status`.\n */\n state?: FileUploadState;\n /** Allow selecting more than one file. Defaults to `true`. */\n multiple?: boolean;\n /** Standard `<input type=\"file\" accept>` filter, e.g. `\"image/*\"` or `\".pdf,.docx\"`. */\n accept?: string;\n /** Disable interaction. Also disables drop and all per-row remove buttons. */\n disabled?: boolean;\n /** Heading text inside the drop area. */\n titleText?: string;\n /**\n * Hint line below the title. Falls back to a `multiple`-aware default\n * (\"Drop files here, or click to browse\" vs \"Drop a file here, or click\n * to browse\"). When the caller sets `state=\"uploading\"`, the atom\n * temporarily replaces this with \"Uploading…\" — caller's `helpText`\n * value is restored once they flip back to default.\n *\n * Per-file uploading rows do NOT trigger this swap on their own — the\n * drop-area copy reflects the caller's declared state, not aggregate\n * row status. Rows carry their own \"Uploading · 60% of …\" indicators.\n */\n helpText?: string;\n /**\n * Optional constraint line below the help text — e.g. \"PDF, DOCX · up to\n * 10 MB\". Caller-controlled copy describing what the page accepts, so it\n * stays in sync with whatever `accept` filter and validation the page\n * enforces in `onFiles`. The atom never invents this string — when\n * omitted, no slot renders. The atom does not enforce anything in it.\n */\n allowedTypesText?: string;\n /**\n * Page-level error message to render below the drop area (e.g. \"Connection\n * lost\", \"Server rejected the batch\"). When set, the atom paints the drop\n * area in the `error` state automatically. Named `error` to match the rest\n * of the input family's `error?: string`.\n *\n * Distinct from per-file errors — set `status: \"error\"` + `errorMessage`\n * on a `FileUploadFileMeta` to surface a single-file failure inline on\n * its row instead.\n */\n error?: string;\n /**\n * @deprecated Renamed to `error` (input-family convention). Still accepted\n * as an alias for back-compat; `error` wins when both are set.\n */\n errorMessage?: string;\n /**\n * Files to display in the inline list under the drop area. Rendered\n * automatically whenever `files.length > 0`. Each entry's `status`\n * drives its row appearance independently. To render the list elsewhere\n * on the page, omit `files` and manage that state yourself.\n */\n files?: FileUploadFileMeta[];\n /** Fires with the selected `File[]` whether the user picked via dialog or dropped. */\n onFiles?: (files: File[]) => void;\n /**\n * Fires when a row's remove button is clicked. The caller decides what\n * \"remove\" means per status — cancel an in-flight upload, drop a queued\n * file, delete a completed one server-side. The atom doesn't auto-disable\n * the button during `uploading` so the user always has an escape hatch.\n */\n onRemove?: (id: string) => void;\n /**\n * Fires when a row's download control is activated. Set this when the\n * stored file needs an auth header or a signed-URL round trip: the consumer\n * fetches the bytes itself and saves them (typically `fetch` with the\n * bearer, then `URL.createObjectURL` — remember to `revokeObjectURL`, and\n * to pass the filename through, or the file lands named `blob`).\n *\n * Takes PRECEDENCE over `FileUploadFileMeta.href`: a consumer that supplied\n * a handler wants control of what opening means. Set neither and no\n * download control renders, which is how every existing consumer behaves.\n */\n onOpenFile?: (id: string) => void;\n /**\n * Glyph for the download control. Defaults to `\"arrow-down\"`. Override to\n * `\"arrow-up-right\"` when the file opens in a tab rather than saving —\n * cross-origin `href`, or an `onOpenFile` that previews in-app.\n */\n downloadGlyph?: string;\n /**\n * Verb in the download control's accessible name, rendered as\n * `` `${downloadLabel} ${file.name}` ``. Defaults to `\"Download\"`.\n *\n * Change it together with `downloadGlyph` whenever the control opens rather\n * than saves: the glyph alone corrects the promise for sighted users and\n * leaves screen-reader users being told \"Download\" for a control that\n * navigates. `downloadLabel=\"Open\"` keeps the two in step.\n */\n downloadLabel?: string;\n /** Fires when a drag enters the drop area. */\n onDragEnter?: (e: DragEvent<HTMLLabelElement>) => void;\n /** Fires when a drag leaves the drop area. */\n onDragLeave?: (e: DragEvent<HTMLLabelElement>) => void;\n /** Fires when a drop is rejected (e.g. wrong type — for caller-driven validation). */\n onError?: (reason: string) => void;\n /** Optional override for the drop-area icon glyph. */\n iconGlyph?: string;\n /** Optional slot rendered above the file list (e.g. a summary line). */\n children?: ReactNode;\n}\n\nexport function FileUpload({\n state: forcedState,\n multiple = true,\n accept,\n disabled = false,\n titleText,\n helpText,\n allowedTypesText,\n error,\n errorMessage,\n files = [],\n onFiles,\n onRemove,\n onOpenFile,\n downloadGlyph = 'arrow-down',\n downloadLabel = 'Download',\n onDragEnter,\n onDragLeave,\n onError: _onError,\n className,\n style,\n iconGlyph = 'cloud-arrow-up',\n children,\n ...rest\n}: FileUploadProps) {\n const inputId = useId();\n const helpId = useId();\n\n // Whether the list has a download COLUMN at all — a whole-list property, so\n // it is computed once here rather than per row (inside the map it re-scanned\n // `files` for every row, which is O(n²) on exactly the renders a list does\n // most: one per progress tick during an upload).\n //\n // The column exists so the control can be `done`-only without the name column\n // gaining 24px the moment an upload settles — a reflow of the row the user is\n // watching. Rows with nothing to fetch render an inert placeholder instead.\n const listHasDownloads = !!onOpenFile || files.some((f) => !!f.href);\n const [internalDrag, setInternalDrag] = useState(false);\n\n // The drop area is intentionally independent of per-file statuses. It\n // doesn't change copy, busy-state, or cursor automatically based on\n // whether any row is uploading — the rows speak for themselves via\n // their own per-row meta + progress strip. The two state spaces stay\n // orthogonal: caller declares drop-area state explicitly via the\n // `state` prop, and sets per-file lifecycle via each `file.status`.\n //\n // `uploading` here is an explicit drop-area state for cases the rows\n // can't carry: batch lockouts, pre-file validation, indeterminate\n // chunked uploads without per-file callbacks. Caller drives it.\n const isUploading = forcedState === 'uploading';\n\n // Atom-level error: caller set `error` (or the deprecated `errorMessage`\n // alias) OR forced state to \"error\". Per-file errors are NOT reflected here —\n // they're each row's concern.\n const pageError = error ?? errorMessage;\n const isPageError = forcedState === 'error' || (!forcedState && !!pageError);\n\n const dataState: FileUploadState = forcedState\n ? forcedState\n : internalDrag\n ? 'drag-over'\n : isPageError\n ? 'error'\n : 'default';\n\n const handleChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>) => {\n if (disabled) return;\n const list = e.target.files;\n if (!list || list.length === 0) return;\n onFiles?.(Array.from(list));\n // Clear so re-selecting the same file re-fires onChange.\n e.target.value = '';\n },\n [disabled, onFiles],\n );\n\n const handleDragEnter = useCallback(\n (e: DragEvent<HTMLLabelElement>) => {\n if (disabled) return;\n e.preventDefault();\n setInternalDrag(true);\n onDragEnter?.(e);\n },\n [disabled, onDragEnter],\n );\n\n const handleDragOver = useCallback(\n (e: DragEvent<HTMLLabelElement>) => {\n if (disabled) return;\n e.preventDefault();\n },\n [disabled],\n );\n\n const handleDragLeave = useCallback(\n (e: DragEvent<HTMLLabelElement>) => {\n if (disabled) return;\n e.preventDefault();\n setInternalDrag(false);\n onDragLeave?.(e);\n },\n [disabled, onDragLeave],\n );\n\n const handleDrop = useCallback(\n (e: DragEvent<HTMLLabelElement>) => {\n if (disabled) return;\n e.preventDefault();\n setInternalDrag(false);\n const list = e.dataTransfer?.files;\n if (!list || list.length === 0) return;\n onFiles?.(Array.from(list));\n },\n [disabled, onFiles],\n );\n\n const resolvedTitle = titleText ?? 'Upload a file';\n const baseHelp =\n helpText ??\n (multiple\n ? 'Drop files here, or click to browse'\n : 'Drop a file here, or click to browse');\n // The uploading state owns the help slot: it swaps to \"Uploading…\" so\n // the drop area visibly reflects the caller-declared busy phase. Falls\n // back to the normal copy in every other state.\n const resolvedHelp = isUploading ? 'Uploading…' : baseHelp;\n\n return (\n <div className={cn('uxm-file-upload-wrapper', className)} style={style} {...rest}>\n <label\n className=\"uxm-file-upload\"\n htmlFor={inputId}\n data-state={dataState}\n data-disabled={disabled ? 'true' : undefined}\n aria-busy={isUploading || undefined}\n onDragEnter={handleDragEnter}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n onDrop={handleDrop}\n >\n <input\n id={inputId}\n className=\"uxm-file-upload__input\"\n type=\"file\"\n multiple={multiple}\n accept={accept}\n disabled={disabled}\n aria-invalid={isPageError || undefined}\n aria-describedby={helpId}\n onChange={handleChange}\n />\n <span className=\"uxm-file-upload__icon\" aria-hidden=\"true\">\n <Icon glyph={iconGlyph} size={40} />\n </span>\n <span className=\"uxm-file-upload__title\">{resolvedTitle}</span>\n <span id={helpId} className=\"uxm-file-upload__help\">\n {resolvedHelp}\n </span>\n {allowedTypesText && !isUploading && (\n <span className=\"uxm-file-upload__allowed-types\">{allowedTypesText}</span>\n )}\n </label>\n {pageError && <span className=\"uxm-file-upload__error\">{pageError}</span>}\n {children}\n {files.length > 0 && (\n <ul className=\"uxm-file-upload__list\">\n {files.map((file) => {\n const status: FileStatus = file.status ?? 'queued';\n const isUploading = status === 'uploading';\n const isDone = status === 'done';\n const isFileError = status === 'error';\n const pctFraction =\n file.progress != null\n ? Math.max(0, Math.min(1, file.progress))\n : undefined;\n const pctLabel =\n pctFraction != null ? `${Math.round(pctFraction * 100)}%` : undefined;\n const pctWidth = pctFraction != null ? `${pctFraction * 100}%` : '0%';\n\n // Icon glyph + color come from the row's status. Default knobs\n // (rowIconColor etc.) cover queued/uploading; done + error\n // route through dedicated vars the CSS reads via [data-status].\n const iconForRow = isDone\n ? 'check-circle'\n : isFileError\n ? 'exclamation-circle'\n : 'document';\n\n // Meta line content swaps with status:\n // queued / done → file size\n // uploading → \"Uploading · 61% of 471.0 KB\" (or \"Uploading…\" if no progress)\n // error → per-file errorMessage (or generic fallback)\n const sizeLabel = formatBytes(file.size);\n const metaText = isUploading\n ? pctLabel != null\n ? `Uploading · ${pctLabel} of ${sizeLabel}`\n : 'Uploading…'\n : isFileError\n ? file.errorMessage ?? 'Upload failed'\n : sizeLabel;\n\n return (\n <li\n key={file.id}\n className=\"uxm-file-upload__row\"\n data-status={status}\n >\n <span className=\"uxm-file-upload__row-icon\" aria-hidden=\"true\">\n <Icon glyph={iconForRow} size={20} />\n </span>\n <span className=\"uxm-file-upload__row-body\">\n <span className=\"uxm-file-upload__row-name\">{file.name}</span>\n <span className=\"uxm-file-upload__row-meta\">{metaText}</span>\n </span>\n {/* Download / open. `done` only — there is nothing stored to\n fetch until the upload settles. `onOpenFile` wins over\n `href` so a consumer that needs an auth header or a signed\n URL is never silently downgraded to a bare navigation. */}\n {listHasDownloads &&\n (isDone && (onOpenFile || file.href) ? (\n onOpenFile ? (\n <button\n type=\"button\"\n className=\"uxm-file-upload__row-download\"\n aria-label={`${downloadLabel} ${file.name}`}\n disabled={disabled}\n onClick={() => onOpenFile(file.id)}\n >\n <Icon glyph={downloadGlyph} size={16} />\n </button>\n ) : (\n // eslint-disable-next-line jsx-a11y/anchor-is-valid -- `href` is dropped ONLY while `disabled`, which is the point: an anchor that keeps its href stays focusable and Enter still follows it, so a disabled control would remain usable from the keyboard. The rule cannot model a conditionally-inert anchor; when enabled this always has a valid href.\n <a\n className=\"uxm-file-upload__row-download\"\n // DROPPED while disabled, not merely styled out. An\n // anchor keeps `href` in the tab order and Enter still\n // follows it, so `pointer-events: none` disables only\n // the mouse — a keyboard user could still download from\n // a form the consumer switched off. Without `href` the\n // element is neither focusable nor a link.\n href={disabled ? undefined : file.href}\n // Names the saved file: without this the browser uses\n // the URL's last segment, so a storage key lands on\n // disk as a UUID. Ignored cross-origin, along with the\n // download behaviour itself — see `href`'s docs.\n download={file.name}\n // Cross-origin, `download` is ignored and this becomes a\n // real navigation — into a NEW tab, so the user does not\n // lose the app and whatever is unsaved beside the list.\n // Same-origin the `download` attribute wins and no tab\n // opens, so this costs nothing there. It is also what\n // makes `rel=\"noopener\"` mean anything.\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n aria-label={`${downloadLabel} ${file.name}`}\n aria-disabled={disabled || undefined}\n >\n <Icon glyph={downloadGlyph} size={16} />\n </a>\n )\n ) : (\n // Inert placeholder keeping the column width stable across\n // the uploading → done transition.\n <span className=\"uxm-file-upload__row-download-slot\" aria-hidden=\"true\" />\n ))}\n <button\n type=\"button\"\n className=\"uxm-file-upload__row-remove\"\n aria-label={\n isUploading ? `Cancel ${file.name}` : `Remove ${file.name}`\n }\n disabled={disabled}\n onClick={() => onRemove?.(file.id)}\n >\n <Icon glyph=\"trash\" size={16} />\n </button>\n {isUploading && (\n <span\n className=\"uxm-file-upload__row-progress\"\n style={\n { '--uxm-file-upload-row-progress': pctWidth } as CSSProperties\n }\n role=\"progressbar\"\n aria-valuenow={pctFraction != null ? Math.round(pctFraction * 100) : undefined}\n aria-valuemin={0}\n aria-valuemax={100}\n />\n )}\n </li>\n );\n })}\n </ul>\n )}\n </div>\n );\n}\n\nfunction formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;\n return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;\n}\n"],"mappings":"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","names":["isUploading"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/ui/file-upload/file-upload.tsx"],"sourcesContent":["import {\n useCallback,\n useId,\n useState,\n type ChangeEvent,\n type CSSProperties,\n type DragEvent,\n type HTMLAttributes,\n type ReactNode,\n} from 'react';\n\nimport { cn } from '@/helpers';\n\nimport { Icon } from '../icon';\nimport { useUxmLocale } from '../locale';\n\n/**\n * Visual states the FileUpload **drop area** can be forced into via the\n * `state` prop. These describe the drop area surface only — not per-file\n * lifecycle. Per-file states (queued / uploading / done / error) live on\n * each `FileUploadFileMeta.status` and render row-by-row independently.\n *\n * `uploading` is **caller-controlled** — the atom never sets it from per-\n * file statuses. Use it for page-level signals: a batch is being prepared\n * before per-file rows exist, a chunked upload with no per-file progress,\n * or any \"the system is busy with this dropzone\" state the caller wants\n * to surface. Per-row uploading rows remain the *primary* progress signal.\n *\n * `error` means a *page-level* error (e.g. \"Connection lost\", \"Server\n * rejected the batch\") — surfaced via the `errorMessage` prop and shown\n * below the drop area. Per-file errors don't change the drop area's state.\n */\nexport type FileUploadState = 'default' | 'drag-over' | 'uploading' | 'error';\n\n/**\n * Per-file lifecycle status. The atom paints each row based on its own\n * status independently of other rows.\n *\n * - `queued`: file is added but not yet sent. Neutral row appearance.\n * - `uploading`: file is in flight. Row shows a progress strip and a\n * verbose meta line (`labels.uploadProgress`). Caller drives\n * `progress` (0–1).\n * - `done`: file completed. Document icon swaps to a success check.\n * - `error`: file failed. Border + icon + meta switch to danger styling;\n * the per-file `errorMessage` replaces the size meta.\n */\nexport type FileStatus = 'queued' | 'uploading' | 'done' | 'error';\n\nexport interface FileUploadFileMeta {\n /** Stable id for keying + removal callbacks. */\n id: string;\n /** Filename to display in the row. */\n name: string;\n /** Size in bytes. Formatted for display by the atom. */\n size: number;\n /**\n * Per-file lifecycle status. Defaults to `\"queued\"` if omitted. Drives\n * the row's icon, meta line, border, and progress strip.\n */\n status?: FileStatus;\n /**\n * 0–1 progress fraction. Used only when `status === \"uploading\"`.\n * If omitted during uploading, the row shows an indeterminate state\n * (`labels.uploading`) and no fraction.\n */\n progress?: number;\n /**\n * Per-file error message. Used only when `status === \"error\"`.\n * Replaces the size meta on the row in danger styling.\n */\n errorMessage?: string;\n /**\n * Direct URL for a stored file. On a `done` row this renders a trailing\n * control as a real `<a href download>` — which is why it exists alongside\n * `onOpenFile` rather than being replaced by it: only an anchor gives the\n * browser's own affordances (⌘/middle-click to a new tab, right-click →\n * \"Save link as\", the native download UI).\n *\n * ⚠️ Use this ONLY when the browser can fetch the URL unauthenticated. A\n * plain navigation carries no `Authorization` header, so a token-protected\n * URL returns 401 — reach for `onOpenFile` there.\n *\n * ⚠️ The `download` attribute is ignored CROSS-ORIGIN: if the file is\n * served from another origin the browser navigates to it instead of saving\n * it. Pass `downloadGlyph=\"arrow-up-right\"` so the control does not promise\n * a save it cannot perform, or route through `onOpenFile` and a blob.\n */\n href?: string;\n}\n\n/**\n * Copy the atom generates on its own — the strings that have no dedicated\n * prop because they are per-row or per-state rather than per-instance.\n * Merged over the English defaults, so a consumer overrides only what it\n * needs to translate.\n *\n * The two composed names take the filename as an argument rather than a\n * prefix string: word order around a filename differs by language, and a\n * `\"Remove\" + name` concatenation cannot express that.\n */\nexport interface FileUploadLabels {\n /** Busy copy — replaces the help line and any progressless row. Default `\"Uploading…\"`. */\n uploading?: string;\n /** Row meta fallback when a failed file carries no `errorMessage`. Default `\"Upload failed\"`. */\n uploadFailed?: string;\n /** Row meta while uploading with known progress. Default `` `Uploading · ${percent} of ${size}` ``. */\n uploadProgress?: (percent: string, size: string) => string;\n /** Row trash button, idle file. Default `` `Remove ${name}` ``. */\n removeFile?: (name: string) => string;\n /** Row trash button, in-flight file. Default `` `Cancel ${name}` ``. */\n cancelFile?: (name: string) => string;\n}\n\nconst DEFAULT_LABELS = {\n uploading: 'Uploading…',\n uploadFailed: 'Upload failed',\n uploadProgress: (percent: string, size: string) => `Uploading · ${percent} of ${size}`,\n removeFile: (name: string) => `Remove ${name}`,\n cancelFile: (name: string) => `Cancel ${name}`,\n} satisfies Required<FileUploadLabels>;\n\nexport interface FileUploadProps\n extends Omit<HTMLAttributes<HTMLDivElement>, 'onError' | 'onDragEnter' | 'onDragLeave'> {\n /**\n * Forced drop-area visual state — overrides internal drag detection.\n * The workbench uses this to paint each state without real interaction.\n * Omit in production: the atom flips to `drag-over` itself while\n * dragging, and to `error` when `errorMessage` is set.\n *\n * Per-file states (uploading / done / per-file errors) are NOT atom-\n * level — they live on each `FileUploadFileMeta.status`.\n */\n state?: FileUploadState;\n /** Allow selecting more than one file. Defaults to `true`. */\n multiple?: boolean;\n /** Standard `<input type=\"file\" accept>` filter, e.g. `\"image/*\"` or `\".pdf,.docx\"`. */\n accept?: string;\n /** Disable interaction. Also disables drop and all per-row remove buttons. */\n disabled?: boolean;\n /** Heading text inside the drop area. */\n titleText?: string;\n /**\n * Hint line below the title. Falls back to a `multiple`-aware default\n * (\"Drop files here, or click to browse\" vs \"Drop a file here, or click\n * to browse\"). When the caller sets `state=\"uploading\"`, the atom\n * temporarily replaces this with `labels.uploading` — caller's `helpText`\n * value is restored once they flip back to default.\n *\n * Per-file uploading rows do NOT trigger this swap on their own — the\n * drop-area copy reflects the caller's declared state, not aggregate\n * row status. Rows carry their own \"Uploading · 60% of …\" indicators.\n */\n helpText?: string;\n /**\n * Optional constraint line below the help text — e.g. \"PDF, DOCX · up to\n * 10 MB\". Caller-controlled copy describing what the page accepts, so it\n * stays in sync with whatever `accept` filter and validation the page\n * enforces in `onFiles`. The atom never invents this string — when\n * omitted, no slot renders. The atom does not enforce anything in it.\n */\n allowedTypesText?: string;\n /**\n * Page-level error message to render below the drop area (e.g. \"Connection\n * lost\", \"Server rejected the batch\"). When set, the atom paints the drop\n * area in the `error` state automatically. Named `error` to match the rest\n * of the input family's `error?: string`.\n *\n * Distinct from per-file errors — set `status: \"error\"` + `errorMessage`\n * on a `FileUploadFileMeta` to surface a single-file failure inline on\n * its row instead.\n */\n error?: string;\n /**\n * @deprecated Renamed to `error` (input-family convention). Still accepted\n * as an alias for back-compat; `error` wins when both are set.\n */\n errorMessage?: string;\n /**\n * Files to display in the inline list under the drop area. Rendered\n * automatically whenever `files.length > 0`. Each entry's `status`\n * drives its row appearance independently. To render the list elsewhere\n * on the page, omit `files` and manage that state yourself.\n */\n files?: FileUploadFileMeta[];\n /** Fires with the selected `File[]` whether the user picked via dialog or dropped. */\n onFiles?: (files: File[]) => void;\n /**\n * Fires when a row's remove button is clicked. The caller decides what\n * \"remove\" means per status — cancel an in-flight upload, drop a queued\n * file, delete a completed one server-side. The atom doesn't auto-disable\n * the button during `uploading` so the user always has an escape hatch.\n */\n onRemove?: (id: string) => void;\n /**\n * Fires when a row's download control is activated. Set this when the\n * stored file needs an auth header or a signed-URL round trip: the consumer\n * fetches the bytes itself and saves them (typically `fetch` with the\n * bearer, then `URL.createObjectURL` — remember to `revokeObjectURL`, and\n * to pass the filename through, or the file lands named `blob`).\n *\n * Takes PRECEDENCE over `FileUploadFileMeta.href`: a consumer that supplied\n * a handler wants control of what opening means. Set neither and no\n * download control renders, which is how every existing consumer behaves.\n */\n onOpenFile?: (id: string) => void;\n /**\n * Glyph for the download control. Defaults to `\"arrow-down\"`. Override to\n * `\"arrow-up-right\"` when the file opens in a tab rather than saving —\n * cross-origin `href`, or an `onOpenFile` that previews in-app.\n */\n downloadGlyph?: string;\n /**\n * Verb in the download control's accessible name, rendered as\n * `` `${downloadLabel} ${file.name}` ``. Defaults to `\"Download\"`.\n *\n * Change it together with `downloadGlyph` whenever the control opens rather\n * than saves: the glyph alone corrects the promise for sighted users and\n * leaves screen-reader users being told \"Download\" for a control that\n * navigates. `downloadLabel=\"Open\"` keeps the two in step.\n */\n downloadLabel?: string;\n /** Fires when a drag enters the drop area. */\n onDragEnter?: (e: DragEvent<HTMLLabelElement>) => void;\n /** Fires when a drag leaves the drop area. */\n onDragLeave?: (e: DragEvent<HTMLLabelElement>) => void;\n /** Fires when a drop is rejected (e.g. wrong type — for caller-driven validation). */\n onError?: (reason: string) => void;\n /**\n * Accessible names and row copy the atom generates itself. Merged over the\n * English defaults. `titleText` / `helpText` / `allowedTypesText` stay\n * separate props — they are per-instance copy, not per-row.\n */\n labels?: FileUploadLabels;\n /**\n * BCP-47 locale for the file-size formatter. Defaults to the nearest\n * `UxmLocaleProvider`, then to `\"en-US\"`. Drives the decimal separator and\n * the localised byte unit (`\"471.0 kB\"` vs `\"471,0 кБ\"`). Ignored when\n * `formatSize` is supplied.\n */\n locale?: string;\n /**\n * Replace the built-in file-size formatter wholesale. Receives the raw byte\n * count from `FileUploadFileMeta.size`. Use this when the default\n * `Intl`-formatted SI units don't match the product's house style (e.g. an\n * app that insists on binary `KiB` / `MiB`).\n */\n formatSize?: (bytes: number) => string;\n /** Optional override for the drop-area icon glyph. */\n iconGlyph?: string;\n /** Optional slot rendered above the file list (e.g. a summary line). */\n children?: ReactNode;\n}\n\nexport function FileUpload({\n state: forcedState,\n multiple = true,\n accept,\n disabled = false,\n titleText,\n helpText,\n allowedTypesText,\n error,\n errorMessage,\n files = [],\n onFiles,\n onRemove,\n onOpenFile,\n downloadGlyph = 'arrow-down',\n downloadLabel = 'Download',\n onDragEnter,\n onDragLeave,\n onError: _onError,\n labels,\n locale: localeProp,\n formatSize,\n className,\n style,\n iconGlyph = 'cloud-arrow-up',\n children,\n ...rest\n}: FileUploadProps) {\n const l = { ...DEFAULT_LABELS, ...labels };\n const locale = useUxmLocale(localeProp);\n const sizeFormatter = formatSize ?? ((bytes: number) => formatBytes(bytes, locale));\n const inputId = useId();\n const helpId = useId();\n\n // Whether the list has a download COLUMN at all — a whole-list property, so\n // it is computed once here rather than per row (inside the map it re-scanned\n // `files` for every row, which is O(n²) on exactly the renders a list does\n // most: one per progress tick during an upload).\n //\n // The column exists so the control can be `done`-only without the name column\n // gaining 24px the moment an upload settles — a reflow of the row the user is\n // watching. Rows with nothing to fetch render an inert placeholder instead.\n const listHasDownloads = !!onOpenFile || files.some((f) => !!f.href);\n const [internalDrag, setInternalDrag] = useState(false);\n\n // The drop area is intentionally independent of per-file statuses. It\n // doesn't change copy, busy-state, or cursor automatically based on\n // whether any row is uploading — the rows speak for themselves via\n // their own per-row meta + progress strip. The two state spaces stay\n // orthogonal: caller declares drop-area state explicitly via the\n // `state` prop, and sets per-file lifecycle via each `file.status`.\n //\n // `uploading` here is an explicit drop-area state for cases the rows\n // can't carry: batch lockouts, pre-file validation, indeterminate\n // chunked uploads without per-file callbacks. Caller drives it.\n const isUploading = forcedState === 'uploading';\n\n // Atom-level error: caller set `error` (or the deprecated `errorMessage`\n // alias) OR forced state to \"error\". Per-file errors are NOT reflected here —\n // they're each row's concern.\n const pageError = error ?? errorMessage;\n const isPageError = forcedState === 'error' || (!forcedState && !!pageError);\n\n const dataState: FileUploadState = forcedState\n ? forcedState\n : internalDrag\n ? 'drag-over'\n : isPageError\n ? 'error'\n : 'default';\n\n const handleChange = useCallback(\n (e: ChangeEvent<HTMLInputElement>) => {\n if (disabled) return;\n const list = e.target.files;\n if (!list || list.length === 0) return;\n onFiles?.(Array.from(list));\n // Clear so re-selecting the same file re-fires onChange.\n e.target.value = '';\n },\n [disabled, onFiles],\n );\n\n const handleDragEnter = useCallback(\n (e: DragEvent<HTMLLabelElement>) => {\n if (disabled) return;\n e.preventDefault();\n setInternalDrag(true);\n onDragEnter?.(e);\n },\n [disabled, onDragEnter],\n );\n\n const handleDragOver = useCallback(\n (e: DragEvent<HTMLLabelElement>) => {\n if (disabled) return;\n e.preventDefault();\n },\n [disabled],\n );\n\n const handleDragLeave = useCallback(\n (e: DragEvent<HTMLLabelElement>) => {\n if (disabled) return;\n e.preventDefault();\n setInternalDrag(false);\n onDragLeave?.(e);\n },\n [disabled, onDragLeave],\n );\n\n const handleDrop = useCallback(\n (e: DragEvent<HTMLLabelElement>) => {\n if (disabled) return;\n e.preventDefault();\n setInternalDrag(false);\n const list = e.dataTransfer?.files;\n if (!list || list.length === 0) return;\n onFiles?.(Array.from(list));\n },\n [disabled, onFiles],\n );\n\n const resolvedTitle = titleText ?? 'Upload a file';\n const baseHelp =\n helpText ??\n (multiple\n ? 'Drop files here, or click to browse'\n : 'Drop a file here, or click to browse');\n // The uploading state owns the help slot: it swaps to `labels.uploading` so\n // the drop area visibly reflects the caller-declared busy phase. Falls\n // back to the normal copy in every other state.\n const resolvedHelp = isUploading ? l.uploading : baseHelp;\n\n return (\n <div className={cn('uxm-file-upload-wrapper', className)} style={style} {...rest}>\n <label\n className=\"uxm-file-upload\"\n htmlFor={inputId}\n data-state={dataState}\n data-disabled={disabled ? 'true' : undefined}\n aria-busy={isUploading || undefined}\n onDragEnter={handleDragEnter}\n onDragOver={handleDragOver}\n onDragLeave={handleDragLeave}\n onDrop={handleDrop}\n >\n <input\n id={inputId}\n className=\"uxm-file-upload__input\"\n type=\"file\"\n multiple={multiple}\n accept={accept}\n disabled={disabled}\n aria-invalid={isPageError || undefined}\n aria-describedby={helpId}\n onChange={handleChange}\n />\n <span className=\"uxm-file-upload__icon\" aria-hidden=\"true\">\n <Icon glyph={iconGlyph} size={40} />\n </span>\n <span className=\"uxm-file-upload__title\">{resolvedTitle}</span>\n <span id={helpId} className=\"uxm-file-upload__help\">\n {resolvedHelp}\n </span>\n {allowedTypesText && !isUploading && (\n <span className=\"uxm-file-upload__allowed-types\">{allowedTypesText}</span>\n )}\n </label>\n {pageError && <span className=\"uxm-file-upload__error\">{pageError}</span>}\n {children}\n {files.length > 0 && (\n <ul className=\"uxm-file-upload__list\">\n {files.map((file) => {\n const status: FileStatus = file.status ?? 'queued';\n const isUploading = status === 'uploading';\n const isDone = status === 'done';\n const isFileError = status === 'error';\n const pctFraction =\n file.progress != null\n ? Math.max(0, Math.min(1, file.progress))\n : undefined;\n const pctLabel =\n pctFraction != null ? `${Math.round(pctFraction * 100)}%` : undefined;\n const pctWidth = pctFraction != null ? `${pctFraction * 100}%` : '0%';\n\n // Icon glyph + color come from the row's status. Default knobs\n // (rowIconColor etc.) cover queued/uploading; done + error\n // route through dedicated vars the CSS reads via [data-status].\n const iconForRow = isDone\n ? 'check-circle'\n : isFileError\n ? 'exclamation-circle'\n : 'document';\n\n // Meta line content swaps with status:\n // queued / done → file size\n // uploading → labels.uploadProgress (or labels.uploading if no progress)\n // error → per-file errorMessage (or generic fallback)\n const sizeLabel = sizeFormatter(file.size);\n const metaText = isUploading\n ? pctLabel != null\n ? l.uploadProgress(pctLabel, sizeLabel)\n : l.uploading\n : isFileError\n ? file.errorMessage ?? l.uploadFailed\n : sizeLabel;\n\n return (\n <li\n key={file.id}\n className=\"uxm-file-upload__row\"\n data-status={status}\n >\n <span className=\"uxm-file-upload__row-icon\" aria-hidden=\"true\">\n <Icon glyph={iconForRow} size={20} />\n </span>\n <span className=\"uxm-file-upload__row-body\">\n <span className=\"uxm-file-upload__row-name\">{file.name}</span>\n <span className=\"uxm-file-upload__row-meta\">{metaText}</span>\n </span>\n {/* Download / open. `done` only — there is nothing stored to\n fetch until the upload settles. `onOpenFile` wins over\n `href` so a consumer that needs an auth header or a signed\n URL is never silently downgraded to a bare navigation. */}\n {listHasDownloads &&\n (isDone && (onOpenFile || file.href) ? (\n onOpenFile ? (\n <button\n type=\"button\"\n className=\"uxm-file-upload__row-download\"\n aria-label={`${downloadLabel} ${file.name}`}\n disabled={disabled}\n onClick={() => onOpenFile(file.id)}\n >\n <Icon glyph={downloadGlyph} size={16} />\n </button>\n ) : (\n // eslint-disable-next-line jsx-a11y/anchor-is-valid -- `href` is dropped ONLY while `disabled`, which is the point: an anchor that keeps its href stays focusable and Enter still follows it, so a disabled control would remain usable from the keyboard. The rule cannot model a conditionally-inert anchor; when enabled this always has a valid href.\n <a\n className=\"uxm-file-upload__row-download\"\n // DROPPED while disabled, not merely styled out. An\n // anchor keeps `href` in the tab order and Enter still\n // follows it, so `pointer-events: none` disables only\n // the mouse — a keyboard user could still download from\n // a form the consumer switched off. Without `href` the\n // element is neither focusable nor a link.\n href={disabled ? undefined : file.href}\n // Names the saved file: without this the browser uses\n // the URL's last segment, so a storage key lands on\n // disk as a UUID. Ignored cross-origin, along with the\n // download behaviour itself — see `href`'s docs.\n download={file.name}\n // Cross-origin, `download` is ignored and this becomes a\n // real navigation — into a NEW tab, so the user does not\n // lose the app and whatever is unsaved beside the list.\n // Same-origin the `download` attribute wins and no tab\n // opens, so this costs nothing there. It is also what\n // makes `rel=\"noopener\"` mean anything.\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n aria-label={`${downloadLabel} ${file.name}`}\n aria-disabled={disabled || undefined}\n >\n <Icon glyph={downloadGlyph} size={16} />\n </a>\n )\n ) : (\n // Inert placeholder keeping the column width stable across\n // the uploading → done transition.\n <span className=\"uxm-file-upload__row-download-slot\" aria-hidden=\"true\" />\n ))}\n <button\n type=\"button\"\n className=\"uxm-file-upload__row-remove\"\n aria-label={\n isUploading ? l.cancelFile(file.name) : l.removeFile(file.name)\n }\n disabled={disabled}\n onClick={() => onRemove?.(file.id)}\n >\n <Icon glyph=\"trash\" size={16} />\n </button>\n {isUploading && (\n <span\n className=\"uxm-file-upload__row-progress\"\n style={\n { '--uxm-file-upload-row-progress': pctWidth } as CSSProperties\n }\n role=\"progressbar\"\n aria-valuenow={pctFraction != null ? Math.round(pctFraction * 100) : undefined}\n aria-valuemin={0}\n aria-valuemax={100}\n />\n )}\n </li>\n );\n })}\n </ul>\n )}\n </div>\n );\n}\n\n// SI, not binary, and deliberately so: the unit names below come from `Intl`\n// (`kilobyte`, `megabyte`, `gigabyte`), and those are defined as powers of\n// 1000. Dividing by 1024 while printing `kB` would misstate the size in every\n// locale, and `Intl` has no binary unit to switch to — the sanctioned list has\n// no `kibibyte`. Consumers who want binary units own the whole formatter via\n// `formatSize` (`KiB` / `MiB`), which is why that prop exists.\nconst KB = 1000;\nconst MB = KB * 1000;\nconst GB = MB * 1000;\n\n/**\n * Locale-aware size formatter. `Intl`'s `style: \"unit\"` supplies both the\n * decimal separator and the translated unit, which a hand-rolled\n * `` `${n.toFixed(1)} KB` `` cannot: `de-DE` needs a comma, `uk-UA` needs\n * `кБ`, `fr-FR` counts in octets. Bytes render whole; everything above keeps\n * one fraction digit, as before.\n *\n * The byte tier uses `unitDisplay: \"long\"` while the rest use `\"short\"`, and\n * that asymmetry is deliberate. CLDR's *short* byte form is neither short nor\n * pluralised in English — `Intl` renders `\"512 byte\"` — whereas the long form\n * pluralises correctly in every locale (`512 bytes`, `512 байтів`,\n * `512 bajtów`). It is also the one tier that can afford the extra characters,\n * since the number itself is at most three digits.\n *\n * Older `Intl` implementations reject `style: \"unit\"` with a RangeError, so\n * the English SI form stays as a fallback rather than crashing the row.\n */\nfunction formatBytes(bytes: number, locale: string): string {\n const [value, unit, fallbackUnit] =\n bytes < KB\n ? ([bytes, 'byte', 'B'] as const)\n : bytes < MB\n ? ([bytes / KB, 'kilobyte', 'kB'] as const)\n : bytes < GB\n ? ([bytes / MB, 'megabyte', 'MB'] as const)\n : ([bytes / GB, 'gigabyte', 'GB'] as const);\n const isBytes = unit === 'byte';\n const fractionDigits = isBytes ? 0 : 1;\n try {\n return new Intl.NumberFormat(locale, {\n style: 'unit',\n unit,\n unitDisplay: isBytes ? 'long' : 'short',\n minimumFractionDigits: fractionDigits,\n maximumFractionDigits: fractionDigits,\n }).format(value);\n } catch {\n return `${value.toFixed(fractionDigits)} ${fallbackUnit}`;\n }\n}\n"],"mappings":"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","names":["isUploading"]}
|
package/dist/ui/index.cjs
CHANGED
|
@@ -49,6 +49,7 @@ __export(ui_exports, {
|
|
|
49
49
|
ContentTooltip: () => import_tooltip.ContentTooltip,
|
|
50
50
|
CurrencyInput: () => import_currency_input.CurrencyInput,
|
|
51
51
|
DEFAULT_SIDEBAR_ICON_COLORS: () => import_app_sidebar.DEFAULT_SIDEBAR_ICON_COLORS,
|
|
52
|
+
DEFAULT_UXM_LOCALE: () => import_locale.DEFAULT_UXM_LOCALE,
|
|
52
53
|
DataTable: () => import_data_table.DataTable,
|
|
53
54
|
DateInput: () => import_date_input.DateInput,
|
|
54
55
|
DetailSection: () => import_detail_section.DetailSection,
|
|
@@ -73,6 +74,7 @@ __export(ui_exports, {
|
|
|
73
74
|
InlineAction: () => import_inline_action.InlineAction,
|
|
74
75
|
InlineFilter: () => import_inline_filter.InlineFilter,
|
|
75
76
|
InputWithIcon: () => import_input_with_icon.InputWithIcon,
|
|
77
|
+
LanguageSwitcher: () => import_language_switcher.LanguageSwitcher,
|
|
76
78
|
LifecycleConnector: () => import_lifecycle_connector.LifecycleConnector,
|
|
77
79
|
LifecycleDropSlot: () => import_lifecycle_drop_slot.LifecycleDropSlot,
|
|
78
80
|
LifecycleEdgeLabel: () => import_lifecycle_edge_label.LifecycleEdgeLabel,
|
|
@@ -131,9 +133,11 @@ __export(ui_exports, {
|
|
|
131
133
|
ToggleSwitch: () => import_toggle_switch.ToggleSwitch,
|
|
132
134
|
Tooltip: () => import_tooltip.Tooltip,
|
|
133
135
|
TypeOverviewCard: () => import_type_overview_card.TypeOverviewCard,
|
|
136
|
+
UxmLocaleProvider: () => import_locale.UxmLocaleProvider,
|
|
134
137
|
ViewSwitcher: () => import_view_switcher.ViewSwitcher,
|
|
135
138
|
getIcon: () => import_icons.getIcon,
|
|
136
|
-
toast: () => import_toast.toast
|
|
139
|
+
toast: () => import_toast.toast,
|
|
140
|
+
useUxmLocale: () => import_locale.useUxmLocale
|
|
137
141
|
});
|
|
138
142
|
module.exports = __toCommonJS(ui_exports);
|
|
139
143
|
var import_app_sidebar = require("./app-sidebar/index.cjs");
|
|
@@ -189,9 +193,11 @@ var import_inline_filter = require("./inline-filter/index.cjs");
|
|
|
189
193
|
var import_color_input = require("./color-input/index.cjs");
|
|
190
194
|
var import_input_with_icon = require("./input-with-icon/index.cjs");
|
|
191
195
|
var import_input = require("./input/index.cjs");
|
|
196
|
+
var import_language_switcher = require("./language-switcher/index.cjs");
|
|
192
197
|
var import_link = require("./link/index.cjs");
|
|
193
198
|
var import_list = require("./list/index.cjs");
|
|
194
199
|
var import_listbox = require("./listbox/index.cjs");
|
|
200
|
+
var import_locale = require("./locale/index.cjs");
|
|
195
201
|
var import_loader = require("./loader/index.cjs");
|
|
196
202
|
var import_menu = require("./menu/index.cjs");
|
|
197
203
|
var import_number_stepper = require("./number-stepper/index.cjs");
|
|
@@ -266,6 +272,7 @@ var import_icons = require("../lib/icons.cjs");
|
|
|
266
272
|
ContentTooltip,
|
|
267
273
|
CurrencyInput,
|
|
268
274
|
DEFAULT_SIDEBAR_ICON_COLORS,
|
|
275
|
+
DEFAULT_UXM_LOCALE,
|
|
269
276
|
DataTable,
|
|
270
277
|
DateInput,
|
|
271
278
|
DetailSection,
|
|
@@ -290,6 +297,7 @@ var import_icons = require("../lib/icons.cjs");
|
|
|
290
297
|
InlineAction,
|
|
291
298
|
InlineFilter,
|
|
292
299
|
InputWithIcon,
|
|
300
|
+
LanguageSwitcher,
|
|
293
301
|
LifecycleConnector,
|
|
294
302
|
LifecycleDropSlot,
|
|
295
303
|
LifecycleEdgeLabel,
|
|
@@ -348,8 +356,10 @@ var import_icons = require("../lib/icons.cjs");
|
|
|
348
356
|
ToggleSwitch,
|
|
349
357
|
Tooltip,
|
|
350
358
|
TypeOverviewCard,
|
|
359
|
+
UxmLocaleProvider,
|
|
351
360
|
ViewSwitcher,
|
|
352
361
|
getIcon,
|
|
353
|
-
toast
|
|
362
|
+
toast,
|
|
363
|
+
useUxmLocale
|
|
354
364
|
});
|
|
355
365
|
//# sourceMappingURL=index.cjs.map
|