All checks were successful
Notify chart dev on beta cut / notify-chart-dev (push) Successful in 5s
A new HTML tool — browse — that lists the contents of any directory.
Designed for ZDDC archives but no ZDDC-specific filtering; just a
straight folder browser with expand/collapse, sort, and name filter.
Modes (auto-detected at page load):
- Online: when served by zddc-server at a folder URL, queries
the same URL with Accept: application/json to load the listing
and renders it. Auto-served as the default at any directory
under ZDDC_ROOT without an index.html (replacing the previous
minimal-HTML stub from directory.go).
- Local: 'Select Directory' button uses FileSystemAccessAPI to
pick any folder on disk; works in Chromium-based browsers.
Features (Phase 1 — what's in this commit):
- Tree view with lazy-loaded folders (children fetched on first
expand).
- Sort by name / size / extension / date (column header click).
- Filter by name substring (toolbar input).
- File click opens in a new tab — for server-backed pages,
routes through zddc-server's normal handler so .archive
redirects + apps cascade overrides + ACL all apply.
Phase 2 deferred:
- ZIP files inline expansion (treat archive entries as virtual
children).
- File preview popup (reuse shared/preview-lib.js).
- Extension multi-select filter.
Wiring:
- browse/ added to top-level ./build's per-tool list, embed
block, versions.txt, and the lockstep release commit + tag set.
All seven tools (archive, transmittal, classifier, mdedit,
landing, form, browse) advance together on stable cuts.
- shared/build-lib.sh: browse added to ZDDC_RELEASE_TOOLS and
verify_channel_links's per-tool loop.
- zddc/internal/apps/embed.go: //go:embed browse.html +
EmbeddedBytes("browse") case.
- zddc/internal/apps/availability.go: browse available at every
directory (same as archive).
- zddc/internal/apps/handler.go: MatchAppHTML routes
/<dir>/browse.html → 'browse'.
- zddc/internal/handler/directory.go: when a directory request
arrives with Accept: text/html and no index.html exists,
serve the embedded browse.html bytes (with a JSON-fallback
if the embedded slot is empty during bootstrap).
181 lines
3.5 KiB
CSS
181 lines
3.5 KiB
CSS
/* Toolbar above the listing */
|
|
|
|
.browse-root {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 0;
|
|
}
|
|
|
|
.toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
padding: 0.6rem 1rem;
|
|
background: var(--bg-secondary);
|
|
border-bottom: 1px solid var(--border);
|
|
flex-shrink: 0;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.toolbar__path {
|
|
font-family: Consolas, Monaco, monospace;
|
|
font-size: 0.9rem;
|
|
color: var(--text-muted);
|
|
flex: 1;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.toolbar__filter {
|
|
width: 22rem;
|
|
max-width: 100%;
|
|
padding: 0.3rem 0.6rem;
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.toolbar__count {
|
|
font-size: 0.8rem;
|
|
color: var(--text-muted);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Table — folders + files in a tree */
|
|
|
|
.browse-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 0.9rem;
|
|
background: var(--bg);
|
|
flex: 1;
|
|
}
|
|
|
|
.browse-table thead th {
|
|
position: sticky;
|
|
top: 0;
|
|
background: var(--bg-secondary);
|
|
border-bottom: 1px solid var(--border);
|
|
text-align: left;
|
|
padding: 0.5rem 0.75rem;
|
|
font-weight: 600;
|
|
color: var(--text);
|
|
user-select: none;
|
|
z-index: 1;
|
|
}
|
|
|
|
.browse-table th.sortable {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.browse-table th.sortable:hover {
|
|
background: var(--bg-hover, #e8e8e8);
|
|
}
|
|
|
|
.sort-arrow {
|
|
display: inline-block;
|
|
width: 0.7rem;
|
|
color: var(--text-muted);
|
|
font-size: 0.7rem;
|
|
margin-left: 0.2rem;
|
|
}
|
|
|
|
.browse-table th.sort-asc .sort-arrow::after { content: "▲"; color: var(--text); }
|
|
.browse-table th.sort-desc .sort-arrow::after { content: "▼"; color: var(--text); }
|
|
|
|
.browse-table tbody td {
|
|
padding: 0.3rem 0.75rem;
|
|
border-bottom: 1px solid var(--border);
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.browse-table tbody tr:hover {
|
|
background: var(--bg-hover, #f6faff);
|
|
}
|
|
|
|
/* Tree-row — name cell with indent + chevron */
|
|
|
|
.tree-name {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
min-width: 0;
|
|
}
|
|
|
|
.tree-name__indent {
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
.tree-name__chevron {
|
|
width: 1rem;
|
|
text-align: center;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
flex: 0 0 1rem;
|
|
line-height: 1;
|
|
}
|
|
|
|
.tree-name__chevron--leaf { visibility: hidden; }
|
|
.tree-name__chevron::before { content: "▶"; font-size: 0.65rem; }
|
|
.tree-row.expanded > td .tree-name__chevron::before { content: "▼"; }
|
|
|
|
.tree-name__icon {
|
|
flex: 0 0 1.1rem;
|
|
text-align: center;
|
|
color: var(--text-muted);
|
|
font-size: 1rem;
|
|
line-height: 1;
|
|
}
|
|
|
|
.tree-name__label {
|
|
flex: 1;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
color: var(--text);
|
|
}
|
|
|
|
.tree-name__label.is-folder {
|
|
font-weight: 500;
|
|
}
|
|
|
|
.tree-name__label.is-file {
|
|
cursor: pointer;
|
|
color: var(--primary);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.tree-name__label.is-file:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
/* Numeric columns right-aligned */
|
|
.col-size, .col-date {
|
|
text-align: right;
|
|
font-variant-numeric: tabular-nums;
|
|
white-space: nowrap;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.col-ext {
|
|
color: var(--text-muted);
|
|
font-family: Consolas, Monaco, monospace;
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
/* Loading row */
|
|
.tree-row--loading td {
|
|
color: var(--text-muted);
|
|
font-style: italic;
|
|
padding: 0.5rem 1rem 0.5rem calc(0.75rem + 2.4rem);
|
|
}
|
|
|
|
/* When filter hides a row */
|
|
.tree-row--filtered { display: none; }
|