From e1df915adde0dd371255692ee62a3f4029eff4d8 Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 15 May 2026 03:05:12 +0200 Subject: [PATCH] Consolidate Soma file upload runtime --- .../file-upload-provider.svelte.ts | 143 ++++++++++-------- 1 file changed, 80 insertions(+), 63 deletions(-) diff --git a/src/uix/soma/components/file-upload/file-upload-provider.svelte.ts b/src/uix/soma/components/file-upload/file-upload-provider.svelte.ts index 5f540e13d..043d5b7cb 100644 --- a/src/uix/soma/components/file-upload/file-upload-provider.svelte.ts +++ b/src/uix/soma/components/file-upload/file-upload-provider.svelte.ts @@ -9,7 +9,7 @@ import { FieldProvider } from '../field/field-provider.svelte'; import type { FileRejection, FileRejectionReason } from './types'; import { fileUploadMorfo } from '../../../morfo/components/file-upload'; -import type { SomaRuntimePart } from '../../runtime.svelte'; +import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte'; // ── Helpers ────────────────────────────────────────────────────────────────── @@ -65,6 +65,8 @@ interface FileUploadOpts export class FileUploadProvider { readonly opts: FileUploadOpts; readonly runtimePart: SomaRuntimePart; + readonly soma: Soma; + readonly runtime: SomaRuntime; static readonly ctx = context('FileUpload'); static get(): FileUploadProvider | undefined { return this.ctx.getOr(undefined) as FileUploadProvider | undefined; @@ -77,7 +79,6 @@ export class FileUploadProvider { return new FileUploadProvider(opts); } - readonly soma = Soma.get(); readonly field = FieldProvider.get(); /** Set by HiddenInput so Dropzone/Trigger/Label can target it. */ @@ -87,14 +88,14 @@ export class FileUploadProvider { private constructor(opts: FileUploadOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(fileUploadMorfo, {}) - .part('provider', { - id: opts.id, - ref: opts.ref, - owner: this, - context: FileUploadProvider.ctx - }); + this.soma = Soma.require(); + this.runtime = this.soma.runtime(fileUploadMorfo, {}); + this.runtimePart = this.runtime.part('provider', { + id: opts.id, + ref: opts.ref, + owner: this, + context: FileUploadProvider.ctx + }); // When embedded inside a Field, register the hidden input id so // Field.Label `for=` targets the file input and Field flags propagate. @@ -238,10 +239,12 @@ export class FileUploadLabelProvider { private constructor(opts: FileUploadLabelOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(fileUploadMorfo, {}) - .part('label', { id: opts.id, ref: opts.ref, owner: this }); this.provider = FileUploadProvider.require(); + this.runtimePart = this.provider.runtime.part('label', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly props = $derived.by(() => @@ -269,10 +272,12 @@ export class FileUploadDropzoneProvider { private constructor(opts: FileUploadDropzoneOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(fileUploadMorfo, {}) - .part('dropzone', { id: opts.id, ref: opts.ref, owner: this }); this.provider = FileUploadProvider.require(); + this.runtimePart = this.provider.runtime.part('dropzone', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly onclick = () => { @@ -325,7 +330,7 @@ export class FileUploadDropzoneProvider { ...this.runtimePart.props, role: 'button' as const, tabindex: this.provider.isDisabled ? -1 : 0, - 'aria-label': this.provider.soma?.langs.ts(FILE_UPLOAD_LANGS.DROPZONE), + 'aria-label': this.provider.soma.langs.ts(FILE_UPLOAD_LANGS.DROPZONE), 'aria-disabled': boolToStr(this.provider.isDisabled), 'data-disabled': boolToEmptyStrOrUndef(this.provider.isDisabled), 'data-dragging': boolToEmptyStrOrUndef(this.dragging), @@ -355,10 +360,12 @@ export class FileUploadTriggerProvider { private constructor(opts: FileUploadTriggerOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(fileUploadMorfo, {}) - .part('trigger', { id: opts.id, ref: opts.ref, owner: this }); this.provider = FileUploadProvider.require(); + this.runtimePart = this.provider.runtime.part('trigger', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly onclick = (e: SomaMouseEvent) => { @@ -372,7 +379,7 @@ export class FileUploadTriggerProvider { this.runtimePart.assert({ ...this.runtimePart.props, type: 'button' as const, - 'aria-label': this.provider.soma?.langs.ts(FILE_UPLOAD_LANGS.TRIGGER), + 'aria-label': this.provider.soma.langs.ts(FILE_UPLOAD_LANGS.TRIGGER), 'aria-disabled': boolToStr(this.provider.isDisabled), disabled: this.provider.isDisabled || undefined, 'data-disabled': boolToEmptyStrOrUndef(this.provider.isDisabled), @@ -400,17 +407,15 @@ export class FileUploadHiddenInputProvider { // child→parent reactive edge the old `$effect` created was pure // overhead. this.opts = opts; - this.runtimePart = Soma.require() - .runtime(fileUploadMorfo, {}) - .part('hidden-input', { - id: opts.id, - ref: opts.ref, - owner: this, - onRefChange: (el) => { - this.provider.hiddenInputRef.current = el as HTMLInputElement | null; - } - }); this.provider = FileUploadProvider.require(); + this.runtimePart = this.provider.runtime.part('hidden-input', { + id: opts.id, + ref: opts.ref, + owner: this, + onRefChange: (el) => { + this.provider.hiddenInputRef.current = el as HTMLInputElement | null; + } + }); this.provider.hiddenInputId.current = opts.id.current; } @@ -454,17 +459,19 @@ export class FileUploadFileListProvider { private constructor(opts: FileUploadFileListOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(fileUploadMorfo, {}) - .part('file-list', { id: opts.id, ref: opts.ref, owner: this }); this.provider = FileUploadProvider.require(); + this.runtimePart = this.provider.runtime.part('file-list', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly props = $derived.by(() => this.runtimePart.assert({ ...this.runtimePart.props, role: 'list' as const, - 'aria-label': this.provider.soma?.langs.ts(FILE_UPLOAD_LANGS.LIST) + 'aria-label': this.provider.soma.langs.ts(FILE_UPLOAD_LANGS.LIST) } as const) ); } @@ -492,15 +499,13 @@ export class FileUploadItemProvider { private constructor(opts: FileUploadItemOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(fileUploadMorfo, {}) - .part('item', { - id: opts.id, - ref: opts.ref, - owner: this, - context: FileUploadItemProvider.ctx - }); this.provider = FileUploadProvider.require(); + this.runtimePart = this.provider.runtime.part('item', { + id: opts.id, + ref: opts.ref, + owner: this, + context: FileUploadItemProvider.ctx + }); } readonly snippetProps = $derived.by(() => ({ @@ -533,10 +538,12 @@ export class FileUploadItemNameProvider { private constructor(opts: FileUploadItemNameOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(fileUploadMorfo, {}) - .part('item-name', { id: opts.id, ref: opts.ref, owner: this }); this.item = FileUploadItemProvider.require(); + this.runtimePart = this.item.provider.runtime.part('item-name', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly defaultContent = $derived.by(() => this.item.opts.file.current.name); @@ -561,10 +568,12 @@ export class FileUploadItemSizeProvider { private constructor(opts: FileUploadItemSizeOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(fileUploadMorfo, {}) - .part('item-size', { id: opts.id, ref: opts.ref, owner: this }); this.item = FileUploadItemProvider.require(); + this.runtimePart = this.item.provider.runtime.part('item-size', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly defaultContent = $derived.by(() => formatBytes(this.item.opts.file.current.size)); @@ -592,10 +601,12 @@ export class FileUploadItemPreviewProvider { private constructor(opts: FileUploadItemPreviewOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(fileUploadMorfo, {}) - .part('item-preview', { id: opts.id, ref: opts.ref, owner: this }); this.item = FileUploadItemProvider.require(); + this.runtimePart = this.item.provider.runtime.part('item-preview', { + id: opts.id, + ref: opts.ref, + owner: this + }); $effect(() => { if (!this.matches) { @@ -642,10 +653,12 @@ export class FileUploadItemRemoveProvider { private constructor(opts: FileUploadItemRemoveOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(fileUploadMorfo, {}) - .part('item-remove', { id: opts.id, ref: opts.ref, owner: this }); this.item = FileUploadItemProvider.require(); + this.runtimePart = this.item.provider.runtime.part('item-remove', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly onclick = (_e: SomaMouseEvent) => { @@ -659,7 +672,7 @@ export class FileUploadItemRemoveProvider { type: 'button' as const, disabled: this.item.provider.isDisabled || undefined, 'aria-label': [ - this.item.provider.soma?.langs.ts(FILE_UPLOAD_LANGS.REMOVE), + this.item.provider.soma.langs.ts(FILE_UPLOAD_LANGS.REMOVE), this.item.opts.file.current.name ] .filter(Boolean) @@ -685,10 +698,12 @@ export class FileUploadItemProgressProvider { private constructor(opts: FileUploadItemProgressOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(fileUploadMorfo, {}) - .part('item-progress', { id: opts.id, ref: opts.ref, owner: this }); this.item = FileUploadItemProvider.require(); + this.runtimePart = this.item.provider.runtime.part('item-progress', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly percent = $derived.by(() => Math.min(100, Math.max(0, this.opts.value.current ?? 0))); @@ -721,10 +736,12 @@ export class FileUploadClearTriggerProvider { private constructor(opts: FileUploadClearTriggerOpts) { this.opts = opts; - this.runtimePart = Soma.require() - .runtime(fileUploadMorfo, {}) - .part('clear-trigger', { id: opts.id, ref: opts.ref, owner: this }); this.provider = FileUploadProvider.require(); + this.runtimePart = this.provider.runtime.part('clear-trigger', { + id: opts.id, + ref: opts.ref, + owner: this + }); } readonly isDisabled = $derived.by(() => this.provider.isDisabled || this.provider.isEmpty); @@ -739,7 +756,7 @@ export class FileUploadClearTriggerProvider { ...this.runtimePart.props, type: 'button' as const, disabled: this.isDisabled || undefined, - 'aria-label': this.provider.soma?.langs.ts(FILE_UPLOAD_LANGS.CLEAR), + 'aria-label': this.provider.soma.langs.ts(FILE_UPLOAD_LANGS.CLEAR), 'data-disabled': boolToEmptyStrOrUndef(this.isDisabled), onclick: this.onclick } as const)