import type { Morfo } from '../types'; import { v } from '../types'; export const fileUploadMorfo = { name: 'FileUpload', kebab: 'file-upload', scope: ['soma', 'sema', 'eidos'], texts: { label: '#?components.file-upload.label|File Upload', dropzone: '#?components.file-upload.dropzone|Upload files', trigger: '#?components.file-upload.trigger|Select files', list: '#?components.file-upload.list|Selected files', remove: '#?components.file-upload.remove|Remove file' }, events: [ { name: 'trigger-picker', semantic: { family: 'contact', verb: 'trigger', target: v.partRef('trigger'), sequence: 'coincident' } }, { name: 'commit-set-add', semantic: { family: 'commit', verb: 'set', target: v.partRef('provider'), intent: 'affirm', sequence: 'post' } }, { // Persistence: book §6.2 — signal.risk = untilFix. A rejected // file (too big, wrong type, too many) must keep its warning // visible until the user removes the offending file or chooses // a valid one. Provider clears via `runtime.clearTarget(provider)` // when the file list becomes valid. name: 'signal-warn-reject', semantic: { family: 'signal', verb: 'warn', target: v.partRef('provider'), intent: 'risk', sequence: 'post', persistence: 'untilFix' }, // Book §9.2: signal.warn + risk ⇒ persistentTrace + // reducedMotionFallback='text'. Caller passes the rejection // reason as `message` so the live region announces e.g. // "File too large — choose under 5MB." a11ySemantic: { requiresPersistentTrace: true, reducedMotionFallback: 'text' } }, { name: 'commit-remove', semantic: { family: 'commit', verb: 'remove', target: v.partRef('item'), intent: 'neutral', sequence: 'post' } }, { name: 'commit-reset', semantic: { family: 'commit', verb: 'reset', target: v.partRef('provider'), intent: 'neutral', sequence: 'post' } } ], parts: [ { name: 'Provider', kebab: 'provider', archetype: 'provider', kind: 'public', defaultElement: 'div', optional: false, data: [ { attr: 'data-disabled', severity: 'optional' }, { attr: 'data-invalid', severity: 'optional' }, { attr: 'data-required', severity: 'optional' }, { attr: 'data-empty', severity: 'optional' } ], aria: [] }, { name: 'Label', kebab: 'label', archetype: 'label', kind: 'public', defaultElement: 'label', optional: true, data: [{ attr: 'data-disabled', severity: 'optional' }], aria: [] }, { name: 'Dropzone', kebab: 'dropzone', kind: 'public', defaultElement: 'div', role: 'button', optional: true, data: [ { attr: 'data-disabled', severity: 'optional' }, { attr: 'data-dragging', severity: 'optional' } ], aria: [ { attr: 'aria-label', value: v.translationRef('#?components.file-upload.dropzone|Upload files'), severity: 'recommended' }, // Provider emitted `boolToStr(disabled)` (always "true"/"false"), so // `propRef` + `ariaBoolean` lets renderProps() emit the same string. { attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional', ariaBoolean: true } ], keyboard: [ { key: 'Enter', action: 'open-file-picker' }, { key: ' ', action: 'open-file-picker' } ] }, { name: 'Trigger', kebab: 'trigger', archetype: 'trigger', kind: 'public', defaultElement: 'button', role: 'button', optional: true, data: [{ attr: 'data-disabled', severity: 'optional' }], aria: [ { attr: 'type', value: v.literal('button') }, { attr: 'aria-label', value: v.translationRef('#?components.file-upload.trigger|Select files'), severity: 'recommended' }, // Provider emitted `boolToStr(disabled)` (always "true"/"false"), so // `propRef` + `ariaBoolean` lets renderProps() emit the same string. { attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional', ariaBoolean: true } ] }, { name: 'HiddenInput', kebab: 'hidden-input', archetype: 'input', kind: 'public', defaultElement: 'input', optional: true, data: [], aria: [{ attr: 'aria-hidden', value: v.literal('true') }] }, { name: 'FileList', kebab: 'file-list', archetype: 'group', kind: 'public', defaultElement: 'ul', role: 'list', optional: true, data: [], aria: [ { attr: 'aria-label', value: v.translationRef('#?components.file-upload.list|Selected files'), severity: 'recommended' } ] }, { name: 'Item', kebab: 'item', archetype: 'item', kind: 'public', defaultElement: 'li', role: 'listitem', optional: true, data: [ { attr: 'data-file-name' }, { attr: 'data-file-type', severity: 'optional' } ], aria: [] }, { name: 'ItemName', kebab: 'item-name', archetype: 'description', kind: 'public', defaultElement: 'span', optional: true, data: [], aria: [] }, { name: 'ItemSize', kebab: 'item-size', archetype: 'description', kind: 'public', defaultElement: 'span', optional: true, data: [], aria: [] }, { name: 'ItemPreview', kebab: 'item-preview', archetype: 'preview', kind: 'public', defaultElement: 'div', optional: true, data: [{ attr: 'data-type', severity: 'optional' }], aria: [{ attr: 'aria-label', value: v.propRef('fileName'), severity: 'recommended' }] }, { name: 'ItemRemove', kebab: 'item-remove', archetype: 'action', kind: 'public', defaultElement: 'button', role: 'button', optional: true, data: [{ attr: 'data-disabled', severity: 'optional' }], aria: [ { attr: 'type', value: v.literal('button') }, { attr: 'aria-label', value: v.translationRef('#?components.file-upload.remove|Remove file'), severity: 'recommended' } ] }, { name: 'ItemProgress', kebab: 'item-progress', archetype: 'indicator', kind: 'public', defaultElement: 'div', role: 'progressbar', optional: true, data: [{ attr: 'data-value' }], aria: [ { attr: 'aria-valuenow', value: v.propRef('percent') }, { attr: 'aria-valuemin', value: v.literal('0') }, { attr: 'aria-valuemax', value: v.literal('100') }, { attr: 'aria-label', value: v.propRef('fileName'), severity: 'recommended' } ] }, { name: 'ClearTrigger', kebab: 'clear-trigger', archetype: 'action', kind: 'public', defaultElement: 'button', role: 'button', optional: true, data: [{ attr: 'data-disabled', severity: 'optional' }], aria: [ { attr: 'type', value: v.literal('button') }, { attr: 'aria-label', value: v.commonRef('buttons.clear', 'Clear all'), severity: 'recommended' } ] } ] } as const satisfies Morfo;