npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

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-ui

Peers: 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. FilePreview shows 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.

  1. Data shapes.
    • A file: PreviewFile { id, name, size?, category?, categoryCode?, cacheKey?, tags?, pending? }. category is the document type's name and categoryCode its code (the CAD rule reads both). cacheKey defaults to id; pass id plus revision when a file's content can change under one id. pending is a file still uploading (no preview, no Download).
    • A part: PartRef { id, number, name?, disabled?, note? }. id is whatever you link files to; disabled with a note says 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).
  2. 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.
  3. 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.
  4. Text keys. Every visible string has a key in the attachments-ui namespace and an English default (all 87 are listed below).
  5. Page marker. Maximize covers the page from the nearest ancestor marked data-au-maximize-root (default: the panel's parent).
  6. Engine files. The package ships them under /engine/*; your bundler serves them, and you pass their URLs once to configureViewer.

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 |