Reworks the browse menu/tree interaction into a declarative, contextually honest model and moves view settings onto a toolbar — the menu is the UI to the system, so it should be familiar, inviting, and only ever offer what applies. New declarative menu model (browse/js/menu-model.js): - Every action is one descriptor with a TYPE predicate (appliesTo) and a CAPABILITY predicate (enabled)+tooltip. Row/pane menus are projections over it; separators are derived from group changes. Designed data-shaped so a future server-sourced manifest (zddc.zip) can supply/extend it. - Hybrid visibility: type-inapplicable actions are OMITTED (New folder on a file, Expand on a file); permission/role/tier-gated actions are SHOWN DISABLED with a reason — so a lower tier sees what a higher role unlocks. - Roles are NOT hardcoded: ordinary actions gate on the verbs the server returns (node.verbs / path_verbs), so any operator-defined role works. Only the two intrinsically-special tiers are recognised by name — site admin (is_super_admin) and project/subtree admin (path_is_admin), surfaced as the "Edit access rules…" item; both come from the existing /.profile/access. - The headline fix: New folder / New markdown file no longer appear on file rows (they target a folder or the current dir). events.js: deletes the ~350-line inline buildTreeRowMenu/buildPaneMenu/ SORT_BY_ITEMS; opens menus via menuModel projections through one openRowMenuFor /openPaneMenu path shared by right-click, the hover kebab, and the keyboard menu key (ContextMenu / Shift+F10). Injects action impls via menuModel.configure to avoid a circular dep. Prefetches the scope /.profile/access (memoised) on load/rescope/refresh/popstate so menus never fetch at open time. Discoverability + a11y: a per-row ⋯ kebab (tree.js + new icon-ellipsis sprite, revealed on hover/selection/focus) opens the same menu; keyboard menu key supported. Toolbar: Sort + Show-hidden moved OUT of per-row right-click menus into the tree-pane toolbar, plus New folder / New file buttons (act on the current dir, greyed with a reason when create access is lacking). Help copy updated. Icons: dropped the 3 stray emoji from menu items (consistent, VS Code/Finder style); only new sprite is the kebab's icon-ellipsis. Tests: +5 browse specs (file row omits New-folder; folder row shows it; a read-only server node greys Rename with a "write access" tooltip via a pure menuModel unit; toolbar Sort/Show-hidden drive state + New buttons present; kebab and Shift+F10 both open the menu). All 23 browse+conflict+diff green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
168 lines
9.1 KiB
JavaScript
168 lines
9.1 KiB
JavaScript
// shared/icons.js — minimal outline SVG sprite for ZDDC tools.
|
||
//
|
||
// Vendored from Lucide (https://lucide.dev, ISC). Only the 16
|
||
// file-type glyphs the browse tree maps to are bundled; total weight
|
||
// is ~4.5 KB of SVG path data. Each symbol viewBox is 0 0 24 24 with
|
||
// no stroke/fill attributes — those are applied at the call site via
|
||
// CSS so the icons inherit `currentColor` and tint with the theme.
|
||
//
|
||
// API:
|
||
// window.zddc.icons.inject() // mount sprite into <body> once
|
||
// window.zddc.icons.html('icon-foo') // → '<svg viewBox="0 0 24 24"><use href="#icon-foo"/></svg>'
|
||
// window.zddc.icons.ID // string set of valid symbol ids
|
||
//
|
||
// Callers concat html() output into innerHTML the same way they
|
||
// previously concat'd emoji glyphs. The injected sprite is hidden
|
||
// (`display:none` on the outer <svg>) so it costs zero layout.
|
||
//
|
||
// Why a sprite (rather than per-row inline paths): a hundred tree
|
||
// rows × 300 bytes of duplicated path data is 30 KB of churn on
|
||
// every re-render. With <use>, each row carries only a ~60-byte
|
||
// reference. The sprite is parsed once.
|
||
(function () {
|
||
'use strict';
|
||
|
||
if (!window.zddc) window.zddc = {};
|
||
if (window.zddc.icons) return;
|
||
|
||
// ── Sprite (Lucide outline glyphs, viewBox 24×24) ──────────────────────
|
||
// Concatenated from upstream lucide-static@1.16.0 SVGs; class/style
|
||
// attributes stripped. Order matches the icons-mapped block below
|
||
// so a diff against Lucide's source stays readable.
|
||
var SYMBOLS = ''
|
||
+ '<symbol id="icon-folder" viewBox="0 0 24 24">'
|
||
+ '<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/>'
|
||
+ '</symbol>'
|
||
+ '<symbol id="icon-folder-archive" viewBox="0 0 24 24">'
|
||
+ '<circle cx="15" cy="19" r="2"/>'
|
||
+ '<path d="M20.9 19.8A2 2 0 0 0 22 18V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h5.1"/>'
|
||
+ '<path d="M15 11v-1"/>'
|
||
+ '<path d="M15 17v-2"/>'
|
||
+ '</symbol>'
|
||
+ '<symbol id="icon-file" viewBox="0 0 24 24">'
|
||
+ '<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/>'
|
||
+ '<path d="M14 2v5a1 1 0 0 0 1 1h5"/>'
|
||
+ '</symbol>'
|
||
+ '<symbol id="icon-file-text" viewBox="0 0 24 24">'
|
||
+ '<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/>'
|
||
+ '<path d="M14 2v5a1 1 0 0 0 1 1h5"/>'
|
||
+ '<path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/>'
|
||
+ '</symbol>'
|
||
+ '<symbol id="icon-file-image" viewBox="0 0 24 24">'
|
||
+ '<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/>'
|
||
+ '<path d="M14 2v5a1 1 0 0 0 1 1h5"/>'
|
||
+ '<circle cx="10" cy="12" r="2"/>'
|
||
+ '<path d="m20 17-1.296-1.296a2.41 2.41 0 0 0-3.408 0L9 22"/>'
|
||
+ '</symbol>'
|
||
+ '<symbol id="icon-file-video" viewBox="0 0 24 24">'
|
||
+ '<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/>'
|
||
+ '<path d="M14 2v5a1 1 0 0 0 1 1h5"/>'
|
||
+ '<path d="M15.033 13.44a.647.647 0 0 1 0 1.12l-4.065 2.352a.645.645 0 0 1-.968-.56v-4.704a.645.645 0 0 1 .967-.56z"/>'
|
||
+ '</symbol>'
|
||
+ '<symbol id="icon-file-audio" viewBox="0 0 24 24">'
|
||
+ '<path d="M4 6.835V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2h-.343"/>'
|
||
+ '<path d="M14 2v5a1 1 0 0 0 1 1h5"/>'
|
||
+ '<path d="M2 19a2 2 0 0 1 4 0v1a2 2 0 0 1-4 0v-4a6 6 0 0 1 12 0v4a2 2 0 0 1-4 0v-1a2 2 0 0 1 4 0"/>'
|
||
+ '</symbol>'
|
||
+ '<symbol id="icon-file-archive" viewBox="0 0 24 24">'
|
||
+ '<path d="M13.659 22H18a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v11.5"/>'
|
||
+ '<path d="M14 2v5a1 1 0 0 0 1 1h5"/>'
|
||
+ '<path d="M8 12v-1"/><path d="M8 18v-2"/><path d="M8 7V6"/>'
|
||
+ '<circle cx="8" cy="20" r="2"/>'
|
||
+ '</symbol>'
|
||
+ '<symbol id="icon-file-spreadsheet" viewBox="0 0 24 24">'
|
||
+ '<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/>'
|
||
+ '<path d="M14 2v5a1 1 0 0 0 1 1h5"/>'
|
||
+ '<path d="M8 13h2"/><path d="M14 13h2"/><path d="M8 17h2"/><path d="M14 17h2"/>'
|
||
+ '</symbol>'
|
||
+ '<symbol id="icon-file-code" viewBox="0 0 24 24">'
|
||
+ '<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/>'
|
||
+ '<path d="M14 2v5a1 1 0 0 0 1 1h5"/>'
|
||
+ '<path d="M10 12.5 8 15l2 2.5"/>'
|
||
+ '<path d="m14 12.5 2 2.5-2 2.5"/>'
|
||
+ '</symbol>'
|
||
+ '<symbol id="icon-file-cog" viewBox="0 0 24 24">'
|
||
+ '<path d="M15 8a1 1 0 0 1-1-1V2a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8z"/>'
|
||
+ '<path d="M20 8v12a2 2 0 0 1-2 2h-4.182"/>'
|
||
+ '<path d="m3.305 19.53.923-.382"/>'
|
||
+ '<path d="M4 10.592V4a2 2 0 0 1 2-2h8"/>'
|
||
+ '<path d="m4.228 16.852-.924-.383"/>'
|
||
+ '<path d="m5.852 15.228-.383-.923"/>'
|
||
+ '<path d="m5.852 20.772-.383.924"/>'
|
||
+ '<path d="m8.148 15.228.383-.923"/>'
|
||
+ '<path d="m8.53 21.696-.382-.924"/>'
|
||
+ '<path d="m9.773 16.852.922-.383"/>'
|
||
+ '<path d="m9.773 19.148.922.383"/>'
|
||
+ '<circle cx="7" cy="18" r="3"/>'
|
||
+ '</symbol>'
|
||
+ '<symbol id="icon-file-pen" viewBox="0 0 24 24">'
|
||
+ '<path d="M12.659 22H18a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v9.34"/>'
|
||
+ '<path d="M14 2v5a1 1 0 0 0 1 1h5"/>'
|
||
+ '<path d="M10.378 12.622a1 1 0 0 1 3 3.003L8.36 20.637a2 2 0 0 1-.854.506l-2.867.837a.5.5 0 0 1-.62-.62l.836-2.869a2 2 0 0 1 .506-.853z"/>'
|
||
+ '</symbol>'
|
||
+ '<symbol id="icon-book-marked" viewBox="0 0 24 24">'
|
||
+ '<path d="M10 2v8l3-3 3 3V2"/>'
|
||
+ '<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"/>'
|
||
+ '</symbol>'
|
||
+ '<symbol id="icon-presentation" viewBox="0 0 24 24">'
|
||
+ '<path d="M2 3h20"/>'
|
||
+ '<path d="M21 3v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V3"/>'
|
||
+ '<path d="m7 21 5-5 5 5"/>'
|
||
+ '</symbol>'
|
||
+ '<symbol id="icon-ruler" viewBox="0 0 24 24">'
|
||
+ '<path d="M21.3 15.3a2.4 2.4 0 0 1 0 3.4l-2.6 2.6a2.4 2.4 0 0 1-3.4 0L2.7 8.7a2.41 2.41 0 0 1 0-3.4l2.6-2.6a2.41 2.41 0 0 1 3.4 0Z"/>'
|
||
+ '<path d="m14.5 12.5 2-2"/>'
|
||
+ '<path d="m11.5 9.5 2-2"/>'
|
||
+ '<path d="m8.5 6.5 2-2"/>'
|
||
+ '<path d="m17.5 15.5 2-2"/>'
|
||
+ '</symbol>'
|
||
+ '<symbol id="icon-globe" viewBox="0 0 24 24">'
|
||
+ '<circle cx="12" cy="12" r="10"/>'
|
||
+ '<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/>'
|
||
+ '<path d="M2 12h20"/>'
|
||
+ '</symbol>'
|
||
// Lightweight outline chevron — used by the tree as the
|
||
// expand/collapse affordance. The single glyph rotates 90°
|
||
// via CSS to indicate the expanded state, so we only ship
|
||
// one path instead of two.
|
||
+ '<symbol id="icon-chevron-right" viewBox="0 0 24 24">'
|
||
+ '<path d="m9 18 6-6-6-6"/>'
|
||
+ '</symbol>'
|
||
// Horizontal three-dot "kebab" — the per-row actions affordance.
|
||
+ '<symbol id="icon-ellipsis" viewBox="0 0 24 24">'
|
||
+ '<circle cx="12" cy="12" r="1"/>'
|
||
+ '<circle cx="19" cy="12" r="1"/>'
|
||
+ '<circle cx="5" cy="12" r="1"/>'
|
||
+ '</symbol>';
|
||
|
||
var injected = false;
|
||
|
||
function inject() {
|
||
if (injected) return;
|
||
// insertAdjacentHTML on body parses the SVG namespace correctly
|
||
// across all modern browsers (innerHTML on a <div> wrapper has
|
||
// historically tripped over <symbol> in some engines).
|
||
var sprite = '<svg xmlns="http://www.w3.org/2000/svg" '
|
||
+ 'aria-hidden="true" style="position:absolute;width:0;height:0;'
|
||
+ 'overflow:hidden" focusable="false">'
|
||
+ SYMBOLS
|
||
+ '</svg>';
|
||
if (document.body) {
|
||
document.body.insertAdjacentHTML('afterbegin', sprite);
|
||
injected = true;
|
||
} else {
|
||
document.addEventListener('DOMContentLoaded', inject, { once: true });
|
||
}
|
||
}
|
||
|
||
// Produces the per-row markup callers concat into innerHTML.
|
||
// Bundles the size + stroke defaults inline so the SVG renders
|
||
// correctly even before the page CSS runs (e.g. mid-paint).
|
||
function html(symbolId) {
|
||
return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" '
|
||
+ 'stroke-width="2" stroke-linecap="round" stroke-linejoin="round" '
|
||
+ 'aria-hidden="true"><use href="#' + symbolId + '"/></svg>';
|
||
}
|
||
|
||
window.zddc.icons = { inject: inject, html: html };
|
||
})();
|