@cantera/upload-typestypes
Generic upload lifecycle types for cantera components: files moving through queued, uploading, processing, complete, and error, plus rejection reasons, an accept matcher, and locale-neutral byte formatting.
npx shadcn@latest add @cantera/upload-typesThe lifecycle of a file on its way into a project. Construction files are heavy and "uploaded" is not "done" — providers translate a design after the bytes land. Components render these shapes and never upload; adapters drive the phases and report back through them.
import { formatBytes, MODEL_FILE_ACCEPT, type UploadFile } from '@/lib/upload-types'
const files: UploadFile[] = [
{ id: 'v1', name: 'summit-tower.rvt', size: 248_000_000, phase: 'complete' },
{ id: 'v2', name: 'cedar-mill-site.nwd', size: 612_000_000, phase: 'uploading', progress: 0.42 },
{
id: 'v3',
name: 'dockside-mep.ifc',
phase: 'processing',
processingLabel: 'Translating model',
},
]
const caption = `${MODEL_FILE_ACCEPT} · up to ${formatBytes(800_000_000)}`| Export | Type | Description |
|---|---|---|
| UploadPhase | type | 'queued' | 'uploading' | 'processing' | 'complete' | 'error'. Processing is the provider working after the bytes arrived — translation, extraction — usually without a progress signal. |
| UploadFile | interface | One tracked file: stable id, name, optional byte size, phase, 0–1 progress while uploading, a processingLabel, and error text with a retryable flag that decides warning versus danger. |
| UploadRejection / UploadRejectionReason | interface / type | A refused browser File plus the rule it broke: 'file-type', 'file-size', or 'file-count'. |
| MODEL_FILE_ACCEPT | string | Accept preset for the design formats APS translates most often: .rvt, .ifc, .dwg, .dxf, .nwd, .nwc, .pdf. |
| matchesAccept | (file: File, accept?: string) => boolean | Whether a File satisfies an accept string — extensions, exact MIME types, and type/* wildcards, the native file-input grammar. |
| formatBytes | (bytes: number, locale?: string) => string | Bytes as a localized short unit string, e.g. 248 MB. Locale-neutral Intl by default; decimal units, matching what storage providers report. |
This is the exact code the CLI installs into your project — you own it from there.
/** `processing` is the provider working after the bytes arrived — translation,
* extraction — usually with no reliable progress signal. */
export type UploadPhase = 'queued' | 'uploading' | 'processing' | 'complete' | 'error'
export interface UploadFile {
/** Stable identifier for reconciliation — not the name, which can repeat. */
id: string
name: string
size?: number
phase: UploadPhase
/** Upload progress as a 0–1 fraction while `uploading`. */
progress?: number
processingLabel?: string
error?: string
/** A retryable error is a warning — a retry away; a terminal one is danger. */
retryable?: boolean
}
export type UploadRejectionReason = 'file-type' | 'file-size' | 'file-count'
export interface UploadRejection {
file: File
reason: UploadRejectionReason
}
/** The design formats APS translates most often — a preset, not a limit. */
export const MODEL_FILE_ACCEPT = '.rvt,.ifc,.dwg,.dxf,.nwd,.nwc,.pdf'
export function matchesAccept(file: File, accept?: string): boolean {
if (!accept) return true
const name = file.name.toLowerCase()
const type = file.type.toLowerCase()
return accept.split(',').some((entry) => {
const rule = entry.trim().toLowerCase()
if (!rule) return false
if (rule.startsWith('.')) return name.endsWith(rule)
if (rule.endsWith('/*')) return type.startsWith(rule.slice(0, -1))
return type === rule
})
}
const BYTE_UNITS = ['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte'] as const
// Decimal units, matching what storage providers and file managers report.
export function formatBytes(bytes: number, locale?: string): string {
let value = Math.max(0, bytes)
let unit: (typeof BYTE_UNITS)[number] = 'byte'
for (const next of BYTE_UNITS.slice(1)) {
if (value < 1000) break
value /= 1000
unit = next
}
return new Intl.NumberFormat(locale, {
style: 'unit',
unit,
unitDisplay: 'short',
maximumFractionDigits: unit !== 'byte' && value < 10 ? 1 : 0,
}).format(value)
}