@campfire-interactive/attachments-ui
v0.1.0
Published
A record's attachments for Campfire Suite apps: one file viewer (PDF, image, STEP/IGES and mesh 3D) and the file rules apps share (preview order, what counts as CAD, file-name matching, upload queue, list filters). Draws a file it is given; where files co
Keywords
Readme
@campfire-interactive/attachments-ui
The platform's standard for a record's files in Campfire Suite apps: a viewer for PDF, image and 3D (STEP/IGES and meshes), the dialogs to upload, attach and edit files, maximize and a pop-out window, and the file rules every app shares (what counts as CAD, what previews first, how a file name finds its part).
It draws what it is given. Where files come from, how they are listed and how a change is saved stay in your app: you map your records to the package's shapes with a small adapter, and every save reaches you as a callback. The package imports no service client.
| Entry | What it holds |
|---|---|
| /viewer | The viewers, full-size preview, the six dialogs and part picker, useMaximize, usePopout / usePopoutWindow. Re-exports /rules. |
| /rules | Plain functions, no UI. Importing it never loads three.js or pdf.js. |
| /engine/* | The files the viewer loads at runtime, for your bundler to serve. |
A live catalog of every piece, with a step-by-step "How to use", is the
design-system showcase's Attachments page (npm run showcase, then
#attachments).
Install
npm install @campfire-interactive/attachments-uiPeers: react and react-dom (18 or 19), lucide-react (>= 0.460). No CSS
import: the stylesheet (cfi-au-* classes) is injected by the bundle.
This is a 0.x package, so ^0.1.0 does not reach 0.2.0. A change to the
standard ships as a minor; to take it, raise your caret by hand. Nothing errors
when you do not.
Set up once
At start-up, before anything renders (main.tsx). The package ships the
engine files; your bundler serves them from your own site (never a CDN) and
you pass their URLs once. Coming from the package's own paths, they always
match the versions it runs.
import { configureViewer } from '@campfire-interactive/attachments-ui/viewer';
// pdf.js's worker (1.4 MB): reads PDF pages off the main thread. Loaded on the first PDF.
import pdfWorkerUrl from '@campfire-interactive/attachments-ui/engine/pdf.worker.min.mjs?url';
// The STEP/IGES worker (2 KB): turns a CAD file into a 3D mesh in the background.
import stepWorkerUrl from '@campfire-interactive/attachments-ui/engine/occtWorker.js?url';
// occt-import-js's loader (95 KB): the script the STEP worker loads to start the CAD kernel.
import occtJsUrl from '@campfire-interactive/attachments-ui/engine/occt-import-js.js?url';
// The OpenCascade CAD kernel in WebAssembly (7.6 MB): reads STEP and IGES. Only the worker fetches it.
import occtWasmUrl from '@campfire-interactive/attachments-ui/engine/occt-import-js.wasm?url';
configureViewer({
pdfWorkerUrl,
stepWorkerUrl,
occtJsUrl,
occtWasmUrl,
// Optional: false shows STEP, IGES and meshes as a tile with Download.
modelViewer: true,
});Images and meshes (STL, OBJ, glTF/GLB, 3MF, PLY) need no engine file. A missing
URL fails only when a file of that kind opens, with an error naming the
configureViewer field.
?url is Vite's; the package cannot resolve it itself (it is built with
tsup), which is why these four lines live in your app.
Show one file
The smallest use: no list, no dialogs, no rules.
import { useCallback } from 'react';
import { FilePreview, type PreviewFile } from '@campfire-interactive/attachments-ui/viewer';
export function OneFileViewer({ doc }: { doc: MyDocument }) {
const file: PreviewFile = { id: doc.id, name: doc.name, size: doc.size };
const getUrl = useCallback(() => api.presignedUrl(doc.id), [doc.id]);
const download = () => api.download(doc.id);
return (
<div style={{ display: 'flex', flexDirection: 'column', height: 480 }}>
<div>
{doc.name} <button onClick={download}>Download</button>
</div>
<div style={{ flex: 1, minHeight: 0 }}>
<FilePreview file={file} getUrl={getUrl} onDownload={download} />
</div>
</div>
);
}Three things to get right:
- Pass
size. Without it there is no "Preview anyway" and no too-large check: a huge file is read straight away. - Add your own Download.
FilePreviewshows one only when it cannot draw the file. - Give the parent a height. The preview fills it.
What FilePreview shows on its own:
| The file | What it shows |
|---|---|
| PDF up to 50 MB | Page by page, with Previous and Next page |
| Image up to 25 MB | Fitted; click to zoom, click to fit |
| STEP / IGES up to 20 MB, meshes up to 50 MB | In 3D: Iso, Front, Top, Wireframe, Fit |
| STEP / IGES over 20 MB, meshes over 50 MB | "This is a large 3D file" with Preview anyway and Download |
| PDF over 50 MB, image over 25 MB | "Too large to preview" with Download |
| JT, CATIA, SolidWorks, NX/Creo, DWG, DXF | "Preview not available for … files yet" with Download |
| Anything else | "No preview available for this file type" with Download |
| Still uploading (pending), or a URL that fails | The reason, with Download (disabled while uploading) |
The standard
What your app meets, and what it can rely on. The package's tests prove it.
- Data shapes.
- A file:
PreviewFile { id, name, size?, category?, categoryCode?, cacheKey?, tags?, pending? }.categoryis the document type's name andcategoryCodeits code (the CAD rule reads both).cacheKeydefaults toid; pass id plus revision when a file's content can change under one id.pendingis a file still uploading (no preview, no Download). - A part:
PartRef { id, number, name?, disabled?, note? }.idis whatever you link files to;disabledwith anotesays why a part cannot take a file ("This part"). - A document type:
DocTypeLike { id, code, name }. - The file's bytes: a
getUrl(id)the browser can GET (a presigned URL, say).
- A file:
- Rules are the platform's, not options. What counts as CAD: native CAD and 3D formats always; PDF, TIFF and images by their category (a name or code saying drawing, print, CAD, 3D or model counts); an uncategorized PDF or TIFF counts, an uncategorized image does not. Preview order: the primary first, else CAD files, then the rest, in list order. An app that needs a different rule changes the package, in a minor release.
- Every save is your callback, returning a promise. The dialogs draw the
UI and keep its rules, and ask you for the parts to offer, what is already
linked, and every save (
uploadOne,onToggle,onApply,onSave,onUnlink,uploadTo). A rejected promise shows as that file's or part's failure. A change that half succeeds is yours to report: resolve, and show your own message. - Text keys. Every visible string has a key in the
attachments-uinamespace and an English default (all 87 are listed below). - Page marker. Maximize covers the page from the nearest ancestor marked
data-au-maximize-root(default: the panel's parent). - Engine files. The package ships them under
/engine/*; your bundler serves them, and you pass their URLs once toconfigureViewer.
A full file panel
The list, the card or panel, and the data calls are yours; everything else comes from here. In outline:
import {
FilePreview, UploadQueueDialog, LinkToPartsDialog, LinkManyToPartsDialog,
AttachByNameDialog, EditDetailsDialog, BulkCategoryDialog, ShortcutsHelp,
useMaximize, usePopout, previewOrder, stepPreview, filterFiles,
groupByCategory, namePastedFiles, shortcutFor, ignoresShortcuts,
} from '@campfire-interactive/attachments-ui/viewer';
const files = docs.map(toPreviewFile); // your adapter
const order = previewOrder(files, primaryId); // ids, primary or CAD first
const groups = groupByCategory(filterFiles(files, query, category), 'Uncategorized');
<UploadQueueDialog
files={queue} // null closes it
onClose={() => setQueue(null)}
target={part.number}
existingNames={docs.map((d) => d.name)}
docTypes={docTypes}
uploadOne={(file, docTypeId) => api.upload(part.id, file, docTypeId)}
onUploaded={reload}
/>The showcase's How to use walks through all of it, step by step: the adapter, preview and paging, the list, upload, the attach dialogs, edit details, maximize and shortcuts, the pop-out, and translations.
Licenses
The package redistributes the pdf.js worker and occt-import-js, and runs
three.js, so publish the license notice with your site: copy this
package's third-party-licenses/ folder (a NOTICE.txt and one license text
per component) into your app's public/third-party-licenses/.
| Component | Version | License |
|---|---|---|
| three.js | 0.176.0 | MIT |
| pdf.js (pdfjs-dist) | 4.10.38 | Apache 2.0 |
| occt-import-js | 0.0.23 | LGPL 2.1 |
| Open CASCADE Technology (inside occt-import-js) | | LGPL 2.1 |
occt-import-js is used unmodified and is loaded as its own file, never
combined into this package's code; NOTICE.txt says how.
Text keys
Every visible string has a key in the package's namespace, attachments-ui,
and an English default. Pass a translate function with i18next's shape,
t(key, defaultText, values), bound to that namespace — for example
i18n.getFixedT(null, 'attachments-ui'). Without one, the English defaults
show with their values filled in. Values are written {{name}}; a
translation places them, never concatenation.
| Key | English default |
|---|---|
| bulkCategory.apply | Apply |
| bulkCategory.title | Change the category of {{count}} files |
| byName.ambiguous | Ambiguous |
| byName.attachN | Attach {{count}} |
| byName.choose | Choose files… |
| byName.colFile | File |
| byName.colPart | Attach to |
| byName.colStatus | Status |
| byName.intro | Choose a folder of drawings. Files named after a part number are matched automatically; you confirm every row before anything is attached. |
| byName.matched | Matched |
| byName.skip | Don't attach |
| byName.summary | {{assigned}} of {{total}} will be attached |
| byName.title | Attach files to parts by file name |
| byName.tooLarge | {{name}} is {{size}} - over the {{limit}} upload limit. |
| byName.unmatched | No match |
| common.cancel | Cancel |
| common.category | Category |
| common.categoryEverywhere | The category applies on every part this file is on. |
| common.close | Close |
| common.download | Download |
| common.uncategorized | Uncategorized |
| details.addTag | Add a tag and press Enter |
| details.attachMore | Attach to other parts |
| details.linkedNote | Linked, never copied: every part shows this same file. |
| details.noTags | No tags |
| details.removeTag | Remove tag |
| details.saveDetails | Save |
| details.saveFailedDetails | Could not save the details: {{message}} |
| details.tags | Tags |
| details.thisPart | This part |
| details.type | Type |
| details.unlink | Remove from {{part}} (keeps the file) |
| details.usedOn | Used on {{count}} parts |
| link.help | One file, many parts. This links the same document rather than uploading copies, so a change to it shows on every part. |
| link.title | Attach “{{name}}” to other parts |
| linkMany.apply | Attach to {{count}} parts |
| linkMany.title | Attach {{count}} files to other parts |
| model.failed | This model could not be shown in 3D. |
| model.fit | Fit to view |
| model.front | Front |
| model.iso | Iso |
| model.loading | Loading the model… |
| model.parsing | Reading the 3D model… |
| model.shaded | Shaded |
| model.stats | {{parts}} parts · {{triangles}} triangles · read in {{seconds}} s |
| model.top | Top |
| model.wireframe | Wireframe |
| picker.none | No parts match. |
| picker.search | Filter parts… |
| preview.actualSize | Click to zoom |
| preview.closeFull | Close full size (Esc) |
| preview.fitToWindow | Click to fit |
| preview.imageFailed | This image could not be displayed. |
| preview.large3d | This is a large 3D file ({{size}}). Showing it in 3D can take up to a minute. |
| preview.nextPage | Next page |
| preview.noCadViewer | Preview not available for {{label}} files yet. Download to open it in your CAD tool. |
| preview.noViewer | No preview available for this file type. Download to open it. |
| preview.pageOf | Page {{page}} of {{total}} |
| preview.pdfFailed | This PDF could not be displayed. Download it to open in your PDF reader. |
| preview.pending | This file is still uploading. |
| preview.previewAnyway | Preview anyway |
| preview.prevPage | Previous page |
| preview.tooLarge | This file is too large to preview ({{size}}, limit {{limit}}). Download to open it. |
| preview.urlFailed | This file could not be opened for preview. |
| queue.count | {{count}} files queued |
| queue.done | attached |
| queue.drop | Leave this file out |
| queue.dropFirst | Drop files here, or browse |
| queue.dropMore | Drop more files, or browse |
| queue.duplicate | A file with this name is already here — this adds a separate file |
| queue.failed | Failed: {{message}} |
| queue.footnote | Files are stored and attached here. |
| queue.hint | Up to {{limit}} each · category is set from the file type |
| queue.hintPreset | Up to {{limit}} each · category set to {{name}} |
| queue.ready | ready |
| queue.titleTo | Upload to |
| queue.tooLarge | Over the {{limit}} limit — skipped |
| queue.uploading | uploading… |
| queue.uploadN | Upload {{count}} files |
| shortcuts.edit | Edit details of the file in the preview |
| shortcuts.escape | Close the preview, restore the panel, or clear the highlight |
| shortcuts.full | Full-size preview of the file in the preview |
| shortcuts.help | Show or hide this list |
| shortcuts.move | Move through the files |
| shortcuts.note | Shortcuts work while it has focus — click in it first. They never fire while you type. |
| shortcuts.title | Keyboard shortcuts |
| shortcuts.upload | Attach files |
