@scaleflex/folder-picker
v0.1.0
Published
Standalone folder picker for Scaleflex DAM — lazy folder tree with search, inline folder creation, single/multi selection, and per-folder disable rules. Lit web component built on @scaleflex/dam-core; embeddable inline or as a modal, in any framework via
Keywords
Readme
@scaleflex/folder-picker
A folder picker web component for Scaleflex DAM — a lazy folder tree with search, inline folder creation, single or multi selection, and per-folder disable rules.
Built as a Lit 3 custom element on @scaleflex/dam-core + @scaleflex/dam-ui,
so it drops into React, Vue, Angular, or plain HTML. Ships to npm only — there is no
CDN bundle.
Part of the
scaleflex-dam-toolsmonorepo (packages/folder-picker/).
Install
pnpm add @scaleflex/folder-pickerImport surfaces
| Import | Use |
| --- | --- |
| @scaleflex/folder-picker | Element class + types (no auto-register) |
| @scaleflex/folder-picker/define | Registers <sfx-folder-picker> |
| @scaleflex/folder-picker/react | <FolderPicker> React wrapper |
Web component
<sfx-folder-picker id="picker"></sfx-folder-picker>
<script type="module">
import '@scaleflex/folder-picker/define';
const picker = document.getElementById('picker');
picker.config = { auth: { mode: 'sass-key', container: 'my-container', sassKey: '…' } };
picker.addEventListener('sfx-folder-select', (e) => console.log(e.detail.selection.path));
</script>React
import { FolderPicker } from '@scaleflex/folder-picker/react';
<FolderPicker
config={{ auth: { mode: 'sass-key', container, sassKey } }}
presentation="modal"
open={open}
features={{ createFolder: true }}
onConfirm={({ selection }) => save(selection?.path)}
onCancel={() => setOpen(false)}
/>;Next.js: this module registers a custom element at import time, so pull it in with
dynamic(() => import('...'), { ssr: false }).
Configuration
interface FolderPickerConfig {
auth?: AnyAuthConfig; // canonical or the asset-picker shape — both accepted
apiDomain?: string;
client?: DamClient; // reuse a client you already built
locale?: string; // BCP-47
}| Property | Type | Default | |
| --- | --- | --- | --- |
| mode | 'single' \| 'multi' | 'single' | Multi adds checkboxes and removable chips |
| presentation | 'inline' \| 'modal' | 'inline' | Modal wraps the panel in sfx-dialog |
| open | boolean | false | Modal only |
| value | string[] | — | Selected paths. Setting it makes the picker controlled |
| rootPath / rootLabel | string | '/' / "Root" | Scope the tree |
| revealPath | string | — | Expand down to this path |
| disabled | DisabledRule | {} | See below |
| permissions | PermissionCheck | — | Checked on select only |
| source | FolderSource | — | Bring your own data layer |
| sort | SortSpec \| comparator | name asc | Presentation order; re-orders without refetching |
| filter | (folder) => boolean | — | Hides folders outright (disabled only greys them out) |
| query | string | '' | Search text — set it to drive the tree from your own field |
| variant | 'hub' \| 'portals' | 'hub' | Which shipped look to use — see Theming |
| loading / errorMessage | boolean / string | — | Host-driven overlay |
Features
picker.features = {
search: true, // debounced search across the subtree
createFolder: false, // opt-in — only you know the user's permissions
root: true, // show a selectable Root row
confirm: true, // two-step select; defaults to true for modal, false for inline
chips: true, // removable pills in multi mode
};Disable rules
picker.disabled = {
pathPrefixes: ['/Campaigns'], // the folder and everything under it — the move-into-self guard
uuids: ['…'],
paths: ['/Reports'], // exact match only
predicate: (folder) => folder.name.startsWith('.'),
hideSubtree: false, // greyed out by default; true hides descendants entirely
};Events
All events bubble and are composed.
| Event | detail |
| --- | --- |
| sfx-folder-select | { mode, selection, selected } — fires on every click/toggle, including deselect |
| sfx-folder-confirm | { selection, selected } — only when features.confirm |
| sfx-folder-cancel | {} |
| sfx-folder-open-change | { open } |
| sfx-folder-expand | { path, expanded } |
| sfx-folder-create | { folder, parentPath } |
| sfx-folder-search | { query, resultCount, truncated } |
| sfx-folder-error | { code, message, path? } — code is load \| search \| create \| permission |
A FolderSelection is { path, uuid?, name, folder? }. path is the identity — uuid
is absent for the synthetic Root row and is not stable across a move.
The picker never reaches into the host's UI. It emits; you decide what to enable.
Composing your own chrome
The panel is search + tree. Anything around it — a title, a sort control, a "collapse all" button — is yours, through two slots and a couple of hooks:
<sfx-folder-picker id="picker" features='{"search":false}'>
<div slot="header">Folders · your sort dropdown · your collapse button</div>
<div slot="footer">…</div>
</sfx-folder-picker>picker.query = q // drive search from a field you rendered elsewhere
picker.sort = { by: 'modified', order: 'desc' }
picker.collapseAll() // fold the tree; loaded children stay cachedsort also takes a comparator ((a, b) => number) when a named field is not enough. Both
sort and filter apply at render time over the cache, so switching either is instant and
issues no requests.
Bringing your own data
Hosts that already hold a folder tree (Redux, react-query, …) should implement
FolderSource instead of letting the element fetch, so nothing is requested twice:
picker.source = {
async listChildren(path, { offset, limit }) {
const folders = await myCache.children(path, offset, limit);
return { folders, hasMore: folders.length === limit };
},
async search(query, { rootPath, limit }) { … },
async create(leafName, parentPath) { … },
async checkPermissions(uuid, required) { … },
};createStaticFolderSource(folders) wraps a flat list — handy for tests and demos.
Folders are paged at 200 with up to 10 pages fetched automatically; beyond that the node is marked partial and a Show more row appears. (The API returns no total, so "more exist" is inferred from a full page.)
Theming
Pick a shape, then wire your palette
Two shapes ship with the package, so a host does not have to reconstruct one from a pile of metrics:
| variant | Reproduces | Rows |
| --- | --- | --- |
| hub (default) | Hub's Airbox picker | chevron only, 20px indent, 11px chevron, 220px list |
| portals | Portals' folders-selector | folder icon per row, 16px indent, 20px chevron that swaps glyph on expand, 320px list |
<FolderPicker variant='portals' … />Colours are not part of a variant — every product has its own palette — so map those with custom properties:
sfx-folder-picker {
--sfx-fp-fg: var(--foreground);
--sfx-fp-accent: var(--accent-foreground);
--sfx-fp-accent-soft: var(--accent);
--sfx-fp-border: var(--border);
--sfx-fp-hover: var(--muted);
}Overriding one value
Any --sfx-fp-* property wins over the variant, so a one-off tweak does not mean leaving
the shape behind:
sfx-folder-picker { --sfx-fp-body-max-height: 420px; }Metrics available: --sfx-fp-indent, --sfx-fp-row-padding-{left,right,y},
--sfx-fp-row-{gap,radius,font-size,line-height}, --sfx-fp-chevron-{box,size},
--sfx-fp-icon-chevron[-open], --sfx-fp-folder-icon-display, --sfx-fp-list-padding,
--sfx-fp-body-max-height, --sfx-fp-search-{height,font-size,padding-x,gap,bg,border,color,shadow},
--sfx-fp-placeholder, --sfx-fp-focus-ring{,-width,-offset,-offset-color}.
The chevron is a CSS mask, so --sfx-fp-icon-chevron also accepts a different glyph as a
data: URI.
Embedding inside a Radix / headless overlay
Overlay libraries treat a click inside a shadow-DOM element as an outside click and close themselves. Guard with the exported helper:
import { isInsideFolderPicker } from '@scaleflex/folder-picker';
<DialogContent
onInteractOutside={(e) => isInsideFolderPicker(e.target) && e.preventDefault()}
onEscapeKeyDown={(e) => isInsideFolderPicker(e.target) && e.preventDefault()}
/>;Accessibility
role="tree" with a single tab stop (roving tabindex). Arrow keys move and expand/collapse,
Home/End jump, Enter/Space select, Escape cancels.
Development
pnpm --filter @scaleflex/folder-picker dev # dev harness, offline scenarios included
pnpm --filter @scaleflex/folder-picker test
pnpm --filter @scaleflex/folder-picker typecheck
pnpm --filter @scaleflex/folder-picker buildThe harness ships offline scenarios (small tree, 2500 children, stalled, failing, scoped) so it works without credentials. For live data, paste a container + sass key into the bar.
