From 605f4ab3e02b6b06329d6681519ca250d7ebc1ee Mon Sep 17 00:00:00 2001 From: ZDDC Date: Sat, 13 Jun 2026 12:17:16 -0500 Subject: [PATCH] chore(embedded): cut v0.0.27-beta --- zddc/internal/apps/embedded/archive.html | 59 +++++- zddc/internal/apps/embedded/browse.html | 101 +++++++++- zddc/internal/apps/embedded/classifier.html | 188 +++++++++++++------ zddc/internal/apps/embedded/index.html | 83 +++++++- zddc/internal/apps/embedded/transmittal.html | 59 +++++- zddc/internal/apps/embedded/versions.txt | 14 +- zddc/internal/handler/tables.html | 63 ++++++- 7 files changed, 477 insertions(+), 90 deletions(-) diff --git a/zddc/internal/apps/embedded/archive.html b/zddc/internal/apps/embedded/archive.html index da46869..3e895b0 100644 --- a/zddc/internal/apps/embedded/archive.html +++ b/zddc/internal/apps/embedded/archive.html @@ -2717,7 +2717,7 @@ td[data-field="trackingNumber"] {
ZDDC Archive - v0.0.27-beta · 2026-06-12 16:07:31 · f66b9c5 + v0.0.27-beta · 2026-06-13 17:17:09 · 51f5947
@@ -11523,6 +11523,48 @@ window.app.modules.filtering = { a: 'edit access rules' }; + // ── "who can?" — tell a denied user who to ask, subtly ────────────────── + // The PATTERN: gate an affordance on cap.has(node, verb) / a path view's + // path_verbs; when the verb is ABSENT, don't just disable — render + // cap.denyHint(view, verb) as the disabled control's tooltip/subtitle (or + // a small inline note) so the user learns who can do it instead of acting + // and bouncing off a 403. handleForbidden() does the same on a denial that + // slips past a pre-check. + + function humanizeRole(name) { return String(name || '').replace(/[_-]+/g, ' ').trim(); } + + // whoCan(view, verb) → the Authority {roles, people} the server computed for + // a verb the caller lacks at view's path, or null. `view` is a /.profile/ + // access?path= result (from cap.at) OR a 403 body carrying who_can. + function whoCan(view, verb) { + if (!view) return null; + var map = view.path_who_can; + if (map && map[verb]) return map[verb]; + if (view.who_can && view.missing_verb === verb) return view.who_can; + return null; + } + + // denyHint(view, verb) → { text, title } for a subtle "who can" line. + // Role-first: "Only the document controller can create here", with the + // specific people (admins / role members) as the tooltip detail. Falls back + // to naming people, then to "Ask an administrator". Returns null when the + // verb is actually granted (nothing to hint) and a generic hint when no + // authority is known. + function denyHint(view, verb) { + var a = whoCan(view, verb); + var doing = VERB_LABELS[verb] || verb || 'do that'; + if (!a || (!(a.roles && a.roles.length) && !(a.people && a.people.length))) { + return { text: 'Ask an administrator to ' + doing + ' here.', title: '' }; + } + var people = (a.people || []).slice(); + var detail = people.length ? people.join(', ') : ''; + if (a.roles && a.roles.length) { + return { text: 'Only the ' + humanizeRole(a.roles[0]) + ' can ' + doing + ' here.', title: detail }; + } + var shown = people.slice(0, 2).join(', ') + (people.length > 2 ? ', …' : ''); + return { text: 'Ask ' + shown + ' to ' + doing + ' here.', title: detail }; + } + // handleForbidden(resp, opts) — render a 403 toast naming the // missing verb. opts.path (optional) is the URL the failed request // hit; when provided, the helper consults /.profile/access?path= to @@ -11536,8 +11578,9 @@ window.app.modules.filtering = { async function handleForbidden(resp, opts) { opts = opts || {}; var missing = ''; + var body = null; try { - var body = await resp.clone().json(); + body = await resp.clone().json(); if (body && typeof body.missing_verb === 'string') { missing = body.missing_verb; } @@ -11552,6 +11595,16 @@ window.app.modules.filtering = { msg = prefix + 'Forbidden.'; } + // Append the subtle "who can" hint: prefer who_can from the 403 body, + // else fall back to the path-scoped access view. So even a denial the + // UI didn't pre-check still tells the user who to ask. + if (missing) { + var src = (body && body.who_can) ? { who_can: body.who_can, missing_verb: missing } : null; + if (!src && opts.path) src = await at(opts.path); + var hint = src ? denyHint(src, missing) : null; + if (hint && hint.text) msg += ' ' + hint.text; + } + // Optional elevate offer: only when the caller supplied a // path AND the path-scoped access view reports an elevation // grant covering the missing verb. Render as a clickable @@ -11584,7 +11637,7 @@ window.app.modules.filtering = { } } - window.zddc.cap = { at: at, has: has, handleForbidden: handleForbidden }; + window.zddc.cap = { at: at, has: has, handleForbidden: handleForbidden, whoCan: whoCan, denyHint: denyHint }; })(); diff --git a/zddc/internal/apps/embedded/browse.html b/zddc/internal/apps/embedded/browse.html index 7c42f59..73cd8bd 100644 --- a/zddc/internal/apps/embedded/browse.html +++ b/zddc/internal/apps/embedded/browse.html @@ -2824,7 +2824,7 @@ li.CodeMirror-hint-active {
ZDDC Browse - v0.0.27-beta · 2026-06-12 16:07:31 · f66b9c5 + v0.0.27-beta · 2026-06-13 17:17:09 · 51f5947
@@ -7022,6 +7022,48 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr a: 'edit access rules' }; + // ── "who can?" — tell a denied user who to ask, subtly ────────────────── + // The PATTERN: gate an affordance on cap.has(node, verb) / a path view's + // path_verbs; when the verb is ABSENT, don't just disable — render + // cap.denyHint(view, verb) as the disabled control's tooltip/subtitle (or + // a small inline note) so the user learns who can do it instead of acting + // and bouncing off a 403. handleForbidden() does the same on a denial that + // slips past a pre-check. + + function humanizeRole(name) { return String(name || '').replace(/[_-]+/g, ' ').trim(); } + + // whoCan(view, verb) → the Authority {roles, people} the server computed for + // a verb the caller lacks at view's path, or null. `view` is a /.profile/ + // access?path= result (from cap.at) OR a 403 body carrying who_can. + function whoCan(view, verb) { + if (!view) return null; + var map = view.path_who_can; + if (map && map[verb]) return map[verb]; + if (view.who_can && view.missing_verb === verb) return view.who_can; + return null; + } + + // denyHint(view, verb) → { text, title } for a subtle "who can" line. + // Role-first: "Only the document controller can create here", with the + // specific people (admins / role members) as the tooltip detail. Falls back + // to naming people, then to "Ask an administrator". Returns null when the + // verb is actually granted (nothing to hint) and a generic hint when no + // authority is known. + function denyHint(view, verb) { + var a = whoCan(view, verb); + var doing = VERB_LABELS[verb] || verb || 'do that'; + if (!a || (!(a.roles && a.roles.length) && !(a.people && a.people.length))) { + return { text: 'Ask an administrator to ' + doing + ' here.', title: '' }; + } + var people = (a.people || []).slice(); + var detail = people.length ? people.join(', ') : ''; + if (a.roles && a.roles.length) { + return { text: 'Only the ' + humanizeRole(a.roles[0]) + ' can ' + doing + ' here.', title: detail }; + } + var shown = people.slice(0, 2).join(', ') + (people.length > 2 ? ', …' : ''); + return { text: 'Ask ' + shown + ' to ' + doing + ' here.', title: detail }; + } + // handleForbidden(resp, opts) — render a 403 toast naming the // missing verb. opts.path (optional) is the URL the failed request // hit; when provided, the helper consults /.profile/access?path= to @@ -7035,8 +7077,9 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr async function handleForbidden(resp, opts) { opts = opts || {}; var missing = ''; + var body = null; try { - var body = await resp.clone().json(); + body = await resp.clone().json(); if (body && typeof body.missing_verb === 'string') { missing = body.missing_verb; } @@ -7051,6 +7094,16 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr msg = prefix + 'Forbidden.'; } + // Append the subtle "who can" hint: prefer who_can from the 403 body, + // else fall back to the path-scoped access view. So even a denial the + // UI didn't pre-check still tells the user who to ask. + if (missing) { + var src = (body && body.who_can) ? { who_can: body.who_can, missing_verb: missing } : null; + if (!src && opts.path) src = await at(opts.path); + var hint = src ? denyHint(src, missing) : null; + if (hint && hint.text) msg += ' ' + hint.text; + } + // Optional elevate offer: only when the caller supplied a // path AND the path-scoped access view reports an elevation // grant covering the missing verb. Render as a clickable @@ -7083,7 +7136,7 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr } } - window.zddc.cap = { at: at, has: has, handleForbidden: handleForbidden }; + window.zddc.cap = { at: at, has: has, handleForbidden: handleForbidden, whoCan: whoCan, denyHint: denyHint }; })(); // shared/icons.js — minimal outline SVG sprite for ZDDC tools. @@ -16209,6 +16262,13 @@ window.__ZDDC_SCHEMA__ = { function openPartyPicker(opts) { return new Promise(function (resolve) { var kindWord = opts.kind === 'folder' ? 'folder' : 'file'; + // The "+ New party" affordance is gated on create authority over ssr/ + // (pre-checked in createInAggregator). When denied, disable it and say + // who can — role-first text inline, the specific people in the tooltip. + var newPartyAllowed = opts.canNewParty !== false; + var newPartyNote = newPartyAllowed ? '(registers a new party)' + : (opts.newPartyHint && opts.newPartyHint.text) || 'You can’t register a new party here.'; + var newPartyTitle = (!newPartyAllowed && opts.newPartyHint && opts.newPartyHint.title) || ''; var overlay = document.createElement('div'); overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.4);display:flex;align-items:center;justify-content:center;z-index:9000;'; var box = document.createElement('div'); @@ -16227,10 +16287,10 @@ window.__ZDDC_SCHEMA__ = { 'Pick the party this ' + kindWord + ' belongs to — it lands under ' + escapeHtml(opts.slot) + '/<party>/.' + '

' + '
' + - (partyList || 'No parties yet — create one below.') + - '' + + (partyList || 'No parties yet.') + + '' + '
' + '
@@ -1951,6 +1966,15 @@ body {
+ + +