@supap-labs/doc-management
v0.2.2
Published
A React document management component library
Downloads
494
Maintainers
Readme
@supap-labs/doc-management
A React document management component library by supap labs.

Installation
npm install @supap-labs/doc-management
# or
yarn add @supap-labs/doc-management
# or
pnpm add @supap-labs/doc-managementPeer dependencies:
react >= 18andreact-dom >= 18must 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 textRow 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:
itemHoveredanditemSelectedwork withstylesonly. The row's class always comes from theitemkey, soclassNames={{ 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/DocumentExplorer → Branded Theme | Token overrides, including a custom font |
| Components/DocumentExplorer → Dark Theme | A dark palette built from tokens alone |
| Components/DocumentExplorer → Custom Slot Styles | styles and treeStyles together |
| Components/DocumentExplorer → Theme Provider | One provider theming everything below it |
| Components/DocumentFolderTree → Compact Styling | indent, iconSize and denser rows |
| Components/DocumentFolderTree → Custom Icons | Replacing icons via renderIcon |
| Examples/Folder Tree + Viewer → Side By Side | The tree and viewer wired together by hand |
Components
All components additionally accept the
theme,styles,classNames,classNameandstyleprops 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.

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.

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 />.

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.

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-checkContributing
Contributions are welcome! Here's how to get started:
- Fork the repository and clone it locally
- Install dependencies
npm install - Start Storybook to develop and preview components interactively
npm run storybook - Make your changes — please keep them focused and minimal
- Type-check before committing
npm run type-check - Build to verify the output is correct
npm run build - 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— usepeerDependenciesinstead - 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 propsReporting Issues
Please open an issue on GitHub with steps to reproduce, expected behaviour, and actual behaviour.
License
MIT © supap lab
