Plugin studio: one rail, highest capability to lowest, trees under their plugin
Some checks are pending
build-gate / build (push) Waiting to run
Some checks are pending
build-gate / build (push) Waiting to run
The separate "What each composite is made of" part is gone. It started over at CAP 5 below the CAP 1 singles, so reading down the rail jumped back up the ladder. Each composite's composition tree now sits directly under that composite's own row, inside its capability section, so the whole rail reads CAP 5 -> CAP 1 once: doxa-maps-spa with its six tabs and what they are made of, then each CAP 4 composite with its parts, then the multi-view, preset and single elements. Tree rows are still aliases of each plugin's one canonical row, arrow keys still visit each plugin once, and section counts count plugins, not repeats. An unpublished part now sorts after every published child.
This commit is contained in:
parent
edb70fccd9
commit
2693c6ffa3
1 changed files with 45 additions and 69 deletions
|
|
@ -8,9 +8,9 @@
|
|||
|
||||
ONE element, <plugin-studio>, renders a VERTICAL-TAB layout whose sidebar is the composability
|
||||
ladder grouped by CAPABILITY (5 → 1). Under each capability heading it lists every element that
|
||||
tier holds; clicking a row nests that element in the main slot. Below the tiers, one collapsible
|
||||
"Inside <composite>" section per composite draws its full composition tree, so a defect can be
|
||||
pinned on the exact nested element responsible. This is the DX viewing surface:
|
||||
tier holds; clicking a row nests that element in the main slot. Directly under each composite's
|
||||
row, its full composition tree is drawn indented, so a defect can be pinned on the exact nested
|
||||
element responsible. This is the DX viewing surface:
|
||||
the spot to SEE every plugin element work, in one place, ordered by capability, with the embed
|
||||
snippet for each — so a developer (or the Driver) can review each element and give feedback.
|
||||
|
||||
|
|
@ -97,7 +97,7 @@
|
|||
<!-- Toolbar strip: the active element's identity and copy-embed. Kept to ONE line on purpose —
|
||||
the plugin is what the studio is for, so it gets the rest of the main area. (A "Composed of"
|
||||
panel used to sit here; the Driver removed it 2026-09-24 because it took the plugin's space.
|
||||
Composition lives in the rail's "Inside <composite>" trees instead.) -->
|
||||
Composition lives in the rail, indented under each composite.) -->
|
||||
<template #toolbar>
|
||||
<div class="ps-toolbar" v-if="active">
|
||||
<div class="ps-bar">
|
||||
|
|
@ -399,67 +399,49 @@ export default defineComponent({
|
|||
.filter(c => byCap[c] && byCap[c].length)
|
||||
.map(c => ({ cap: c, label: CAP_LABEL[c] || ('capability ' + c), rows: byCap[c] }))
|
||||
},
|
||||
// The GROUPED rail model the shared VerticalTabsLayout paints, in two parts (Driver, 2026-09-24:
|
||||
// "clear capability sections 5→1, every plugin reachable, composition tree legible"):
|
||||
// The GROUPED rail model the shared VerticalTabsLayout paints: ONE list, highest capability to
|
||||
// lowest (Driver, 2026-09-24: "everything in studio organized by highest capability to lowest";
|
||||
// "the composability in the capability sidebar vertical tabs … to understand the interoperability
|
||||
// and the various elements that go into each plugin").
|
||||
//
|
||||
// 1. CAPABILITY SECTIONS, CAP 5 → CAP 1 — where each plugin LIVES. Exactly one node per plugin,
|
||||
// so every plugin has one name to report it by. Collapsible folders, open by default.
|
||||
// 2. COMPOSITION TREES — what each composite is MADE OF. One collapsible section per composite
|
||||
// (closed by default so the rail stays short), headed by the composite itself, with every
|
||||
// element nested into it drawn as an indented tree, ordered by capability at each level.
|
||||
// A child repeats in every section that nests it — the Driver's spec: "duplication across
|
||||
// sections is intended", because the question here is "what composes THIS plugin".
|
||||
// CAP 5 → CAP 1 sections, collapsible, open by default. Every plugin has exactly ONE canonical
|
||||
// row, in its own tier. Directly under a COMPOSITE's row sits its composition tree: every element
|
||||
// nested into it, indented by depth, ordered by capability at each level. So reading down the
|
||||
// rail never jumps back up the ladder — the tree lives with the plugin it explains.
|
||||
//
|
||||
// ★ HOW THE REPEATS STAY ONE IDENTITY. Every tree row is an ALIAS (VerticalTabsLayout `selects`):
|
||||
// clicking people-groups-grid under big-picture selects the SAME canonical row as clicking it in
|
||||
// CAP 2 and highlights both. afe6abe removed the earlier rail
|
||||
// folders because six independent nodes for one plugin meant there was no single name to report
|
||||
// it by; an alias has no separate identity, so the tree and the one-name rule now coexist.
|
||||
// people-groups-grid under dashboard-plus-grid selects the SAME canonical row as its own CAP 2
|
||||
// entry and highlights both, and arrow keys skip aliases. afe6abe removed the first rail folders
|
||||
// because six independent nodes for one plugin left no single name to report it by; an alias has
|
||||
// no separate identity, so nesting and the one-name rule coexist. The section count counts
|
||||
// canonical plugins only.
|
||||
navGroups() {
|
||||
const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
const rowModel = (r) => ({
|
||||
key: r.key,
|
||||
label: r.label,
|
||||
icon: r.icon,
|
||||
sub: '<code><' + esc(r.tag) + '></code>'
|
||||
})
|
||||
const tiers = this.groups.map(g => ({
|
||||
const treeOf = new Map(this.compositionTrees.map(t => [t.nodes[0].selects, t]))
|
||||
const rowsFor = (r) => {
|
||||
const out = [{ key: r.key, label: r.label, icon: r.icon, sub: '<code><' + esc(r.tag) + '></code>' }]
|
||||
const t = treeOf.get(r.key)
|
||||
if (t) {
|
||||
t.nodes.slice(1).forEach((n, i) => out.push({
|
||||
key: 'tree-' + r.key + '-' + i,
|
||||
selects: n.selects || ('none:' + r.key),
|
||||
depth: n.depth,
|
||||
label: n.label,
|
||||
badge: n.part ? 'part' : '',
|
||||
sub: n.part ? 'no standalone page' : 'CAP ' + n.cap + ' · <code><' + esc(n.tag) + '></code>'
|
||||
}))
|
||||
}
|
||||
return out
|
||||
}
|
||||
return this.groups.map(g => ({
|
||||
key: 'cap-' + g.cap,
|
||||
badge: 'CAP ' + g.cap,
|
||||
label: g.label,
|
||||
count: g.rows.length,
|
||||
folder: true,
|
||||
open: true,
|
||||
rows: g.rows.map(rowModel)
|
||||
rows: g.rows.flatMap(rowsFor)
|
||||
}))
|
||||
// A composite is named by its BUNDLE here, not its profile: "Inside my-upg-100-list" says which
|
||||
// plugin you are looking into; "Inside upg-100-mfe" made you already know the answer.
|
||||
const trees = this.compositionTrees.map(t => ({
|
||||
key: 'tree-' + t.bundle,
|
||||
label: 'Inside ' + t.bundle,
|
||||
count: t.nodes.length - 1,
|
||||
folder: true,
|
||||
open: false,
|
||||
rows: t.nodes.map((n, i) => ({
|
||||
key: 'tree-' + t.bundle + '-' + i,
|
||||
// An empty selects (the studio's own head, or a part inside it) still gets a key no row
|
||||
// has, so it stays an alias: click is a no-op and arrow keys skip it.
|
||||
selects: n.selects || ('none:' + t.bundle),
|
||||
depth: n.depth || null,
|
||||
label: n.label,
|
||||
badge: n.part ? 'part' : '',
|
||||
sub: n.part
|
||||
? 'no standalone page'
|
||||
: 'CAP ' + n.cap + ' · <code><' + esc(n.tag) + '></code>'
|
||||
}))
|
||||
}))
|
||||
// Two row-less plain groups act as part titles, so the eye can tell "where each plugin lives"
|
||||
// from "what each composite is made of" — the two halves otherwise share one heading style.
|
||||
const title = (key, label) => ({ key, label, rows: [] })
|
||||
return [
|
||||
title('part-caps', 'Every plugin, by capability'), ...tiers,
|
||||
...(trees.length ? [title('part-trees', 'What each composite is made of'), ...trees] : [])
|
||||
]
|
||||
},
|
||||
// ★ ONE TREE PER COMPOSITE, built ENTIRELY from the manifest's `composes` edges — no bundle names
|
||||
// or child lists live in this file, so a new composite appears here with zero edits.
|
||||
|
|
@ -492,31 +474,25 @@ export default defineComponent({
|
|||
kids.push(child)
|
||||
}
|
||||
kids.sort((a, b2) => b2.cap - a.cap || a.label.localeCompare(b2.label))
|
||||
for (const ref of parts) {
|
||||
nodes.push({ label: ref, part: true, selects: row.real ? row.key : '', depth: depth + 1 })
|
||||
}
|
||||
for (const k of kids) {
|
||||
nodes.push({ label: k.label, tag: k.tag, cap: k.cap, selects: k.key, depth: depth + 1 })
|
||||
if (!path.has(k.key)) walk({ ...k, real: true }, depth + 1, new Set([...path, k.key]), nodes)
|
||||
if (!path.has(k.key)) walk(k, depth + 1, new Set([...path, k.key]), nodes)
|
||||
}
|
||||
// Unpublished parts have no tier, so they sort LAST — below every capability — and open the
|
||||
// composite they live in.
|
||||
for (const ref of parts) nodes.push({ label: ref, part: true, selects: row.key, depth: depth + 1 })
|
||||
}
|
||||
for (const b of this.manifestBundles) {
|
||||
if (!b.composes || !b.composes.length || STUDIO_HIDDEN.has(b.bundle)) continue
|
||||
// The composite's own row: its highest-capability profile (rows are sorted cap desc).
|
||||
// ★ plugin-studio HAS NO ROW — it never lists itself as its own tab (mounting <plugin-studio>
|
||||
// inside itself is infinite regress). Its tree is still the one a developer most wants, so it
|
||||
// gets a head built from the manifest entry with an empty `selects`: select()'s dead-key guard
|
||||
// turns a click on it into a no-op rather than a blank pane.
|
||||
// plugin-studio has no row (it never lists itself), so it has no tree either: every row of
|
||||
// this rail already IS its composition.
|
||||
const own = this.rows.find(r => r.bundle === b.bundle)
|
||||
const p0 = (b.profiles && b.profiles[0]) || {}
|
||||
const head = own
|
||||
? { ...own, real: true }
|
||||
: { key: '', bundle: b.bundle, label: p0.profile || b.bundle, tag: p0.element || b.bundle, cap: capFor(p0.type || 'single'), real: false }
|
||||
const nodes = [{ label: head.label, tag: head.tag, cap: head.cap, selects: head.key, depth: 0 }]
|
||||
walk(head, 0, new Set([head.key]), nodes)
|
||||
if (nodes.length > 1) out.push({ bundle: b.bundle, label: head.label, cap: head.cap, nodes })
|
||||
if (!own) continue
|
||||
const nodes = [{ label: own.label, tag: own.tag, cap: own.cap, selects: own.key, depth: 0 }]
|
||||
walk(own, 0, new Set([own.key]), nodes)
|
||||
if (nodes.length > 1) out.push({ bundle: b.bundle, nodes })
|
||||
}
|
||||
out.sort((a, b) => b.cap - a.cap || a.bundle.localeCompare(b.bundle))
|
||||
return out
|
||||
},
|
||||
active() { return this.rows.find(r => r.key === this.activeKey) || null },
|
||||
|
|
|
|||
Loading…
Reference in a new issue