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

@supap-labs/doc-management

v0.2.2

Published

A React document management component library

Downloads

494

Readme

@supap-labs/doc-management

A React document management component library by supap labs.

npm version license

DocumentExplorer — folder tree with an inline file preview


Installation

npm install @supap-labs/doc-management
# or
yarn add @supap-labs/doc-management
# or
pnpm add @supap-labs/doc-management

Peer dependencies: react >= 18 and react-dom >= 18 must already be installed in your project.

Note: The library automatically loads the Inter font from Google Fonts and applies it to all components.


Styling

Every component accepts the same four styling props, applied in this order — later wins:

| Prop | Type | Description | |---|---|---| | theme | Partial<DocumentTheme> | Design-token overrides — colours, radii, font | | styles | Partial<Record<Slot, CSSProperties>> | Inline styles per internal element | | classNames | Partial<Record<Slot, string>> | Extra class names per internal element | | className | string | Class name for the root element | | style | CSSProperties | Inline styles for the root element |

Which one do I want? Use a token when several elements share the value — colorPrimary recolours the selected row, links and buttons in one go. Use a slot when exactly one element is involved, like row padding. If you find yourself repeating the same colour across five slots, it wanted to be a token.

Design tokens

<DocumentExplorer
  nodes={nodes}
  theme={{
    colorPrimary: '#0f766e',
    colorPrimarySoft: '#ccfbf1',
    radius: '4px',
    fontFamily: "Georgia, serif",
  }}
/>

| Token | Default | Used for | |---|---|---| | fontFamily | Inter stack | All text in every component | | fontSize | '0.95rem' | Primary labels | | colorPrimary | #6366f1 | Selection, links, primary buttons | | colorPrimarySoft | #eef2ff | Selected rows, drag-over background | | colorAccent | #22c55e | Uploader "browse" link and upload button | | colorText | #1a1a2e | Body text | | colorTextMuted | #94a3b8 | Secondary text, placeholders | | colorBorder | #e8ecf0 | All borders | | colorSurface | #ffffff | Panel and card backgrounds | | colorSurfaceHover | #f1f5f9 | Row hover background | | colorSurfaceMuted | #f8fafc | Headers and toolbars | | colorDanger | #ef4444 | Delete actions and errors | | radius | '12px' | Panels and cards | | radiusSm | '8px' | Rows, buttons, inner elements | | shadow | 0 1px 4px rgba(0,0,0,0.06) | Card elevation |

App-wide theming

<DocumentThemeProvider /> themes everything beneath it. Nested providers merge, and a component's own theme prop still wins.

import { DocumentThemeProvider } from '@supap-labs/doc-management';

<DocumentThemeProvider theme={{ colorPrimary: '#db2777', radius: '4px' }}>
  <DocumentExplorer nodes={nodes} />
  <DocumentUploader onUpload={handleUpload} />
</DocumentThemeProvider>;

A dark theme is just a different set of tokens:

<DocumentThemeProvider
  theme={{
    colorText: '#e2e8f0',
    colorTextMuted: '#94a3b8',
    colorBorder: '#334155',
    colorSurface: '#0f172a',
    colorSurfaceHover: '#1e293b',
    colorSurfaceMuted: '#1e293b',
    colorPrimary: '#818cf8',
    colorPrimarySoft: '#312e81',
  }}
>
  <DocumentExplorer nodes={nodes} />
</DocumentThemeProvider>;

Slot overrides

When a token isn't enough, target an individual element. Slot names are exported as types (DocumentFolderTreeSlot, DocumentViewerSlot, …).

<DocumentFolderTree
  nodes={nodes}
  styles={{
    root: { border: 'none', boxShadow: '0 8px 24px rgba(15,23,42,0.08)' },
    item: { padding: '3px 6px' },
    itemSelected: { background: '#111827', color: '#fff' },
  }}
  classNames={{ root: 'my-tree', label: 'truncate' }}
/>

| Component | Slots | |---|---| | DocumentCard | root, checkbox, body, nameRow, name, meta, editButton, deleteButton | | DocumentList | root, empty, loading (plus cardStyles / cardClassNames forwarded to each card) | | DocumentUploader | root, dropzone, dropzoneDragging, icon, title, browse, hint, error, fileList, fileItem, fileName, fileSize, removeButton, uploadButton | | DocumentViewer | root, header, titleGroup, titleIcon, title, closeButton, body, preview, pagination, pageButton, pageLabel, message, error, downloadButton | | DocumentFolderTree | root, toolbar, refreshButton, scrollArea, list, group, item, itemSelected, itemHovered, chevron, icon, label, placeholder, empty, loading | | DocumentExplorer | root, treePane, viewerPane, empty, emptyIcon, emptyTitle, emptyDescription (plus treeStyles / treeClassNames / viewerStyles / viewerClassNames) |

Every internal element also carries a stable BEM-style class (doc-mgmt-tree__item, doc-mgmt-viewer__header, …) if you would rather style with a stylesheet. Note that inline styles win over CSS, so use styles for properties the components already set.

What the slot names map to

Taking DocumentFolderTree as the example:

root                    the bordered panel
├── toolbar             the Refresh bar
│   └── refreshButton
└── scrollArea
    └── list            <ul role="tree">
        └── item        one row, folder or file
            ├── chevron
            ├── icon
            └── label   the filename text

Row states layer

item styles every row. itemHovered and itemSelected are merged on top of it when active, so you only describe the difference:

item  →  (+ itemHovered while hovered)  →  (+ itemSelected while selected)

In the example above, the selected row keeps item's 3px 6px padding and only swaps its colours.

Caveat: itemHovered and itemSelected work with styles only. The row's class always comes from the item key, so classNames={{ itemSelected: '…' }} is silently ignored.

Putting it together

Tokens for anything shared, slots for one-off adjustments. DocumentExplorer forwards styling to its two child components through treeStyles / viewerStyles:

<DocumentExplorer
  nodes={nodes}

  // Tokens — repaint both panes at once
  theme={{
    colorPrimary: '#0f766e',      // selected row text, links, download button
    colorPrimarySoft: '#ccfbf1',  // selected row background
    colorBorder: '#d6e4e2',       // every border in both panes
    colorSurfaceMuted: '#f0fdfa', // viewer header bar
    radius: '6px',
  }}

  // Slots — the explorer's own layout
  styles={{ root: { gap: '32px' } }}

  // Slots — forwarded to the tree pane
  treeStyles={{
    root: { border: 'none', boxShadow: '0 8px 24px rgba(15,23,42,0.08)' },
    item: { padding: '3px 6px' },
    itemSelected: { background: '#111827', color: '#fff' },
  }}

  // Slots — forwarded to the previewer
  viewerStyles={{
    header: { padding: '16px 20px' },
    title: { maxWidth: '100%' },   // default truncates at 300px
  }}
/>

Every pattern on this page has a live story in Storybook (npm run storybook):

| Story | Shows | |---|---| | Components/DocumentExplorerBranded Theme | Token overrides, including a custom font | | Components/DocumentExplorerDark Theme | A dark palette built from tokens alone | | Components/DocumentExplorerCustom Slot Styles | styles and treeStyles together | | Components/DocumentExplorerTheme Provider | One provider theming everything below it | | Components/DocumentFolderTreeCompact Styling | indent, iconSize and denser rows | | Components/DocumentFolderTreeCustom Icons | Replacing icons via renderIcon | | Examples/Folder Tree + ViewerSide By Side | The tree and viewer wired together by hand |


Components

All components additionally accept the theme, styles, classNames, className and style props described in Styling.

<DocumentCard />

A single document card with checkbox selection, inline edit and delete actions.

import { DocumentCard } from '@supap-labs/doc-management';

<DocumentCard
  document={doc}
  checked={false}
  onCheck={(id, checked) => console.log(id, checked)}
  onSelect={(doc) => console.log('selected', doc)}
  onEdit={(doc) => console.log('edit', doc)}
  onDelete={(id) => console.log('delete', id)}
/>

Props

| Prop | Type | Default | Description | |---|---|---|---| | document | Document | — | The document to display | | checked | boolean | false | Checkbox checked state | | onCheck | (id, checked) => void | — | Fired when checkbox changes | | onSelect | (doc) => void | — | Fired when the card is clicked | | onEdit | (doc) => void | — | Shows pencil icon when provided | | onDelete | (id) => void | — | Shows trash icon when provided | | className | string | '' | Extra CSS class |


<DocumentList />

Renders a list of DocumentCard components with loading and empty states.

DocumentList — stacked document cards with checkbox selection and delete actions

import { DocumentList } from '@supap-labs/doc-management';

<DocumentList
  documents={docs}
  isLoading={false}
  onSelect={(doc) => console.log('selected', doc)}
  onDelete={(id) => console.log('delete', id)}
  emptyMessage="No documents found."
/>

Props

| Prop | Type | Default | Description | |---|---|---|---| | documents | Document[] | — | Array of documents to render | | isLoading | boolean | false | Shows a loading indicator | | emptyMessage | string | — | Message shown when list is empty | | onSelect | (doc) => void | — | Passed to each DocumentCard | | onDelete | (id) => void | — | Passed to each DocumentCard | | onDownload | (doc) => void | — | Passed to each DocumentCard | | className | string | '' | Extra CSS class |


<DocumentUploader />

Drag-and-drop / click-to-browse file uploader. Selected files are staged in a list with per-file removal before the upload is confirmed.

DocumentUploader — dashed drop zone with a browse link

import { DocumentUploader } from '@supap-labs/doc-management';

<DocumentUploader
  onUpload={async (files) => {
    // files: File[]
    await uploadToServer(files);
  }}
  accept=".pdf,.docx,.xlsx"
  multiple={true}
  maxSize={50 * 1024 * 1024} // 50 MB
/>

Props

| Prop | Type | Default | Description | |---|---|---|---| | onUpload | (files: File[]) => void \| Promise<void> | — | Called when user confirms upload | | accept | string | '*' | Passed to the file <input> (accept attribute) | | multiple | boolean | true | Allow selecting multiple files | | maxSize | number | — | Max file size in bytes; shows error if exceeded | | disabled | boolean | false | Disables the uploader | | className | string | '' | Extra CSS class |


<DocumentViewer />

In-place file preview supporting PDF, images, video, Word/Excel/PowerPoint (via MS Office Online), and plain text.

import { DocumentViewer } from '@supap-labs/doc-management';

<DocumentViewer
  document={doc}
  onClose={() => setOpen(false)}
/>

Props

| Prop | Type | Default | Description | |---|---|---|---| | document | Document | — | The document to preview | | onClose | () => void | — | Shows a close button when provided | | previewMaxHeight | number \| string | 600 | Height cap for image and video previews |

Supported preview types:

| Type | Preview method | |---|---| | pdf | Native in-browser via react-pdf | | image | <img> tag | | video | <video> tag | | word / excel / powerpoint | Embedded MS Office Online viewer | | text | Sandboxed <iframe> | | other | Download link fallback |


<DocumentFolderTree />

A collapsible folder tree. Each child is either a folder (yellow folder icon, expand arrow) or a file (icon derived from its DocumentType). Clicking a file fires onSelectFile with a plain Document, ready to hand to <DocumentViewer />.

DocumentFolderTree — nested matter folders with PDF files under Discovery

import { DocumentFolderTree, type DocumentTreeNode } from '@supap-labs/doc-management';

const nodes: DocumentTreeNode[] = [
  {
    kind: 'folder',
    id: 'matter-1',
    name: 'Sam, John',
    children: [
      { kind: 'folder', id: 'correspondence', name: 'Correspondence', children: [] },
      {
        kind: 'folder',
        id: 'discovery',
        name: 'Discovery',
        children: [
          {
            kind: 'file',
            id: 'hoja2',
            name: 'hoja2.pdf',
            type: 'pdf',
            size: 102_400,
            status: 'approved',
            url: 'https://example.com/hoja2.pdf',
            createdAt: new Date(),
            updatedAt: new Date(),
          },
        ],
      },
    ],
  },
];

<DocumentFolderTree
  nodes={nodes}
  defaultExpandedIds={['matter-1']}
  onRefresh={() => refetch()}
  onSelectFile={(doc) => setPreview(doc)}
  height={600}
/>;

Props

| Prop | Type | Default | Description | |---|---|---|---| | nodes | DocumentTreeNode[] | — | Root-level folders and files | | selectedId | string \| null | — | Controlled highlight; omit to let the tree manage it | | defaultSelectedId | string | — | Initial highlight (uncontrolled) | | expandedIds | string[] | — | Controlled expansion; omit to let the tree manage it | | defaultExpandedIds | string[] | [] | Folders open on first render (uncontrolled) | | onSelectFile | (doc: Document) => void | — | Fired when a file row is clicked | | onSelectFolder | (folder: DocumentFolderNode) => void | — | Fired when a folder row is clicked | | onToggleFolder | (folder, expanded) => void | — | Fired on expand/collapse — the hook for lazy loading | | onRefresh | () => void | — | Shows the Refresh bar when provided | | refreshLabel | string | 'Refresh' | Label for the refresh button | | isLoading | boolean | false | Replaces the tree with a loading indicator | | emptyMessage | string | 'No folders or files to show.' | Shown when nodes is empty | | height | number \| string | — | Fixed height; the tree scrolls internally | | indent | number | 18 | Left padding added per nesting level, in pixels | | iconSize | number | 20 | Pixel size of the folder / file-type icons | | renderIcon | (node, { expanded, selected }) => ReactNode | — | Replaces the icon for any row |

Lazy loading: omit children on a folder and it still renders an expand arrow. On expand, onToggleFolder fires — set isLoading: true on that folder while fetching, then supply children. Use hasChildren: false to force a folder to render as a leaf.

Untyped data: kind may be omitted. Any node carrying a children array (or lacking a document type) is treated as a folder — see isFolderNode().


<DocumentExplorer />

The folder tree and the file previewer wired together: tree on the left, <DocumentViewer /> on the right. Before anything is picked, the right pane shows an empty state.

DocumentExplorer — folder tree beside an image preview

import { DocumentExplorer } from '@supap-labs/doc-management';

<DocumentExplorer
  nodes={nodes}
  defaultExpandedIds={['matter-1', 'discovery']}
  onRefresh={() => refetch()}
  onToggleFolder={(folder, expanded) => expanded && loadChildren(folder.id)}
  height={700}
/>;

Selection is uncontrolled by default — click a file and it opens in the previewer. Pass selectedFile to drive it from outside instead.

Props

Accepts every DocumentFolderTree prop except selectedId / defaultSelectedId, plus:

| Prop | Type | Default | Description | |---|---|---|---| | selectedFile | Document \| null | — | Controlled preview; omit for uncontrolled | | defaultSelectedFileId | string | — | File open on first render (uncontrolled) | | onCloseViewer | () => void | — | Fired when the viewer's close button is clicked | | treeWidth | number \| string | 320 | Width of the tree pane | | height | number \| string | 600 | Height of the whole explorer | | emptyTitle | string | 'Option Not Selected' | Heading of the empty preview pane | | emptyDescription | string | 'Select item with indicator from the folder tree to begin.' | Sub-text of the empty preview pane | | emptyIcon | ReactNode | folder glyph | Icon of the empty preview pane | | treeStyles / treeClassNames | slot maps | — | Styling forwarded to the tree pane | | viewerStyles / viewerClassNames | slot maps | — | Styling forwarded to the previewer |


Types

import type {
  Document,
  DocumentStatus,
  DocumentType,
  DocumentCardProps,
  DocumentExplorerProps,
  DocumentFileNode,
  DocumentFolderNode,
  DocumentFolderTreeProps,
  DocumentListProps,
  DocumentTreeNode,
  DocumentUploaderProps,
  DocumentViewerProps,
} from '@supap-labs/doc-management';

Document

interface Document {
  id: string;
  name: string;
  type: DocumentType;        // 'pdf' | 'word' | 'excel' | 'image' | 'text' | 'video' | 'powerpoint' | 'other'
  size: number;              // bytes
  status: DocumentStatus;    // 'draft' | 'pending' | 'approved' | 'rejected' | 'archived'
  url?: string;
  createdAt: Date | string;
  updatedAt: Date | string;
  uploadedBy?: string;
  tags?: string[];
  metadata?: Record<string, unknown>;
}

DocumentTreeNode

interface DocumentFolderNode {
  kind: 'folder';
  id: string;
  name: string;
  children?: DocumentTreeNode[];  // omit for lazily-loaded folders
  hasChildren?: boolean;          // force the expand arrow on/off
  isLoading?: boolean;            // show a spinner row while fetching children
  disabled?: boolean;
  metadata?: Record<string, unknown>;
}

type DocumentFileNode = Document & { kind: 'file' };

type DocumentTreeNode = DocumentFolderNode | DocumentFileNode;

DocumentTheme

import { defaultDocumentTheme, type DocumentTheme } from '@supap-labs/doc-management';

const myTheme: Partial<DocumentTheme> = { colorPrimary: '#0f766e' };

See Styling for the full token list.


Utilities

| Function | Description | |---|---| | formatFileSize(bytes) | Converts bytes to a human-readable string — e.g. "1.2 MB" | | getDocumentTypeFromFile(file) | Infers DocumentType from a File object | | getDocumentTypeIcon(type, size?) | Returns an SVG React.ReactElement icon for a given DocumentType | | isFolderNode(node) | Type guard narrowing a DocumentTreeNode to a DocumentFolderNode | | resolveTheme(partial?) | Merges token overrides onto defaultDocumentTheme | | useDocumentTheme(override?) | Hook returning the resolved theme from the nearest provider |


Development

# Install dependencies
npm install

# Start Storybook (component explorer)
npm run storybook

# Build the library (ESM + CJS + type declarations)
npm run build

# Build in watch mode
npm run dev

# Type-check without emitting
npm run type-check

Contributing

Contributions are welcome! Here's how to get started:

  1. Fork the repository and clone it locally
  2. Install dependencies
    npm install
  3. Start Storybook to develop and preview components interactively
    npm run storybook
  4. Make your changes — please keep them focused and minimal
  5. Type-check before committing
    npm run type-check
  6. Build to verify the output is correct
    npm run build
  7. Open a pull request with a clear description of what was changed and why

Guidelines

  • Follow the existing code style (TypeScript strict mode, inline styles, no external CSS dependencies)
  • New components should include a Storybook story
  • Keep peer dependencies out of dependencies — use peerDependencies instead
  • Do not add runtime dependencies without discussion

Commit Convention

This project follows Conventional Commits. Please use the format:

<type>(<scope>): <short description>

Types:

| Type | When to use | |---|---| | feat | New feature or component | | fix | Bug fix | | chore | Config, tooling, or dependency changes | | docs | Documentation only | | refactor | Code restructure with no behaviour change | | style | Formatting, whitespace, or UI tweaks | | test | Adding or updating tests | | perf | Performance improvement | | revert | Reverting a previous commit |

Examples:

feat(document-card): add checkbox and edit/delete actions
fix(document-uploader): reset input after file selection
style(document-card): switch font to Inter
chore(storybook): add Vite builder and addons
docs: update README with component props

Reporting Issues

Please open an issue on GitHub with steps to reproduce, expected behaviour, and actual behaviour.


License

MIT © supap lab