Merge branch 'main' of https://git.nuxtbase.app/doxa-life/doxa-maps into bundler-1/work
Some checks are pending
build-gate / build (push) Waiting to run
Some checks are pending
build-gate / build (push) Waiting to run
This commit is contained in:
commit
8bb7f47e53
2 changed files with 72 additions and 15 deletions
|
|
@ -29,6 +29,9 @@
|
|||
selects : the `key` of the canonical row this one stands for. An ALIAS row has its own unique
|
||||
`key` (for v-for) but clicking it selects `selects`, it highlights whenever that key is
|
||||
active, and it is skipped by keyboard nav so arrow keys visit each canonical row once.
|
||||
branch : an id that makes this row FOLDABLE — a chevron beside it shows/hides the rows below.
|
||||
Closed by default; picking the row also opens it.
|
||||
ancestors : the `branch` ids above this row; it shows only while ALL of them are open.
|
||||
Selection is controlled via v-model:active (the active item's `key`). The host owns pane content
|
||||
and mount logic — this component only paints the SHELL (top bar + rail + a content frame) and
|
||||
exposes slots so each app keeps its unique chrome + its own mount machinery:
|
||||
|
|
@ -161,14 +164,24 @@
|
|||
</div>
|
||||
|
||||
<div class="vtl-rows" v-show="!grp.folder || !isFolderClosed(grp)">
|
||||
<div class="vtl-rowwrap" role="presentation" v-for="row in grp.rows" :key="row.key"
|
||||
v-show="rowShown(row)">
|
||||
<!-- BRANCH toggle: folds this row's sub-rows (see `branch` / `ancestors`). A sibling of the
|
||||
tab, never inside it — a button may not contain a button. -->
|
||||
<button v-if="row.branch" type="button" class="vtl-twisty"
|
||||
:aria-expanded="String(isBranchOpen(row.branch))"
|
||||
:aria-label="(isBranchOpen(row.branch) ? 'Hide' : 'Show') + ' what ' + row.label + ' is made of'"
|
||||
@click="toggleBranch(row.branch)">
|
||||
<span class="vtl-chev" :class="{ open: isBranchOpen(row.branch) }" aria-hidden="true" v-html="iconSvg('chevron')"></span>
|
||||
</button>
|
||||
<button
|
||||
v-for="row in grp.rows" :key="row.key" type="button" role="tab"
|
||||
class="vtl-tab" :class="{ active: target(row) === active, alias: !!row.selects }"
|
||||
type="button" role="tab"
|
||||
class="vtl-tab" :class="{ active: target(row) === active, alias: !!row.selects, 'has-branch': !!row.branch }"
|
||||
:data-depth="row.depth || null" :style="row.depth ? { '--vtl-depth': row.depth } : null"
|
||||
:aria-selected="String(target(row) === active)"
|
||||
:tabindex="!row.selects && row.key === active ? 0 : -1"
|
||||
:ref="el => { if (!row.selects) registerTab(row.key, el) }"
|
||||
@click="pick(target(row))" @keydown="onKey($event)">
|
||||
@click="pick(target(row), row)" @keydown="onKey($event)">
|
||||
<span class="vtl-tab-ic" aria-hidden="true" v-if="row.icon" v-html="iconSvg(row.icon)"></span>
|
||||
<span class="vtl-tab-emoji" aria-hidden="true" v-else-if="row.emoji">{{ row.emoji }}</span>
|
||||
<!-- Slim-rail fallback (desktop collapsed only): a row with NEITHER icon NOR emoji would
|
||||
|
|
@ -180,6 +193,7 @@
|
|||
<span class="vtl-tab-sub" v-if="row.sub"><span v-html="row.sub"></span></span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -368,6 +382,11 @@ const VTL_SHADOW_CSS = `
|
|||
.vtl-tab { display:flex; align-items:center; gap:11px; width:100%; min-height:48px; text-align:left; border:0; background:none; color:var(--vtl-muted); padding:8px 12px; cursor:pointer; border-radius:var(--vtl-radius,8px); font:inherit; margin:0; transition:background .13s ease,color .13s ease; }
|
||||
.vtl-tab[data-depth] { padding-left:calc(12px + var(--vtl-depth) * 16px); }
|
||||
.vtl-tab[data-depth]::before { content:''; flex:0 0 auto; align-self:stretch; width:0; margin-right:-3px; border-left:2px solid var(--vtl-accent); opacity:.45; }
|
||||
.vtl-rowwrap { position:relative; }
|
||||
.vtl-twisty { position:absolute; z-index:1; right:4px; top:50%; transform:translateY(-50%); width:28px; height:28px; display:inline-flex; align-items:center; justify-content:center; padding:0; border:0; border-radius:8px; background:none; color:var(--vtl-muted); cursor:pointer; }
|
||||
.vtl-twisty:hover { background:var(--vtl-accented); color:#3b463d; }
|
||||
.vtl-twisty:focus-visible { outline:2px solid var(--vtl-accent); outline-offset:-2px; }
|
||||
.vtl-tab.has-branch { padding-right:36px; }
|
||||
.vtl-tab:hover { background:var(--vtl-accented); color:#3b463d; }
|
||||
.vtl-tab:focus-visible { outline:2px solid var(--vtl-accent); outline-offset:-2px; }
|
||||
.vtl-tab.active { background:var(--vtl-accented); color:#3b463d; font-weight:500; }
|
||||
|
|
@ -463,6 +482,7 @@ const VTL_SHADOW_CSS = `
|
|||
.vtl.rail-collapsed .vtl-tab-emoji { font-size:19px; }
|
||||
.vtl.rail-collapsed .vtl-tab-fallback { display:inline-flex; }
|
||||
.vtl.rail-collapsed .vtl-tab[data-depth]::before { display:none; }
|
||||
.vtl.rail-collapsed .vtl-twisty { display:none; }
|
||||
.vtl.rail-collapsed .vtl-tier { padding-left:196px; }
|
||||
.vtl.rail-collapsed .vtl-tl, .vtl.rail-collapsed .vtl-td, .vtl.rail-collapsed .vtl-cap, .vtl.rail-collapsed .vtl-chev { display:none; }
|
||||
.vtl.rail-collapsed .vtl-grp + .vtl-grp { margin-top:6px; padding-top:6px; }
|
||||
|
|
@ -489,6 +509,7 @@ const VTL_SHADOW_CSS = `
|
|||
.vtl.rail-collapsed .vtl-tab { padding-left:10px; gap:11px; }
|
||||
.vtl.rail-collapsed .vtl-tab[data-depth] { padding-left:calc(12px + var(--vtl-depth) * 16px); }
|
||||
.vtl.rail-collapsed .vtl-tab[data-depth]::before { display:block; }
|
||||
.vtl.rail-collapsed .vtl-twisty { display:inline-flex; }
|
||||
.vtl.rail-collapsed .vtl-tab-text { display:flex; }
|
||||
.vtl.rail-collapsed .vtl-tab-fallback { display:none; }
|
||||
.vtl.rail-collapsed .vtl-tier { padding-left:8px; }
|
||||
|
|
@ -529,7 +550,7 @@ export default defineComponent({
|
|||
// tab (so their pane is rendered + element mounted); hostState/hostError track each pane's load.
|
||||
// A row not in activatedKeys is never rendered — the lazy contract (no visit → no cost).
|
||||
return {
|
||||
navOpen: this.initialNavOpen, railCollapsed: this.initialRailCollapsed, _tabEls: {}, folderOverride: {},
|
||||
navOpen: this.initialNavOpen, railCollapsed: this.initialRailCollapsed, _tabEls: {}, folderOverride: {}, branchOpen: {},
|
||||
// `narrow` mirrors the (max-width:760px) media query as REACTIVE state, kept current by a resize
|
||||
// listener. It lets us reconcile the two boolean state machines (navOpen = mobile drawer,
|
||||
// railCollapsed = desktop slim rail) at the breakpoint and drive rail visibility correctly.
|
||||
|
|
@ -690,7 +711,15 @@ export default defineComponent({
|
|||
},
|
||||
// The key a row selects: its own, or — for an ALIAS row — the canonical row it points at.
|
||||
target(row) { return row.selects || row.key },
|
||||
pick(key) {
|
||||
// BRANCHES — a row with `branch` folds the rows whose `ancestors` include it. Closed by default,
|
||||
// so a deep composition tree costs one line until someone asks to see inside it.
|
||||
isBranchOpen(id) { return !!this.branchOpen[id] },
|
||||
toggleBranch(id) { this.branchOpen = { ...this.branchOpen, [id]: !this.branchOpen[id] } },
|
||||
rowShown(row) { return !row.ancestors || row.ancestors.every(id => this.branchOpen[id]) },
|
||||
pick(key, row) {
|
||||
// Choosing a row that owns a branch also OPENS it (never closes it): you asked for that plugin,
|
||||
// so show what it is made of. Only the chevron closes a branch.
|
||||
if (row && row.branch && !this.branchOpen[row.branch]) this.toggleBranch(row.branch)
|
||||
this.$emit('update:active', key)
|
||||
// On mobile, picking a tab closes the off-canvas drawer. On desktop navOpen must stay true (it gates
|
||||
// the desktop rail's visibility via railShown), so we only touch it when narrow.
|
||||
|
|
@ -820,6 +849,11 @@ export default defineComponent({
|
|||
.vtl-tab { display:flex; align-items:center; gap:11px; width:100%; min-height:48px; text-align:left; border:0; background:none; color:var(--vtl-muted); padding:8px 12px; cursor:pointer; border-radius:var(--vtl-radius,8px); font:inherit; margin:0; transition:background .13s ease,color .13s ease; }
|
||||
.vtl-tab[data-depth] { padding-left:calc(12px + var(--vtl-depth) * 16px); }
|
||||
.vtl-tab[data-depth]::before { content:''; flex:0 0 auto; align-self:stretch; width:0; margin-right:-3px; border-left:2px solid var(--vtl-accent); opacity:.45; }
|
||||
.vtl-rowwrap { position:relative; }
|
||||
.vtl-twisty { position:absolute; z-index:1; right:4px; top:50%; transform:translateY(-50%); width:28px; height:28px; display:inline-flex; align-items:center; justify-content:center; padding:0; border:0; border-radius:8px; background:none; color:var(--vtl-muted); cursor:pointer; }
|
||||
.vtl-twisty:hover { background:var(--vtl-accented); color:#3b463d; }
|
||||
.vtl-twisty:focus-visible { outline:2px solid var(--vtl-accent); outline-offset:-2px; }
|
||||
.vtl-tab.has-branch { padding-right:36px; }
|
||||
.vtl-tab:hover { background:var(--vtl-accented); color:#3b463d; }
|
||||
.vtl-tab:focus-visible { outline:2px solid var(--vtl-accent); outline-offset:-2px; }
|
||||
.vtl-tab.active { background:var(--vtl-accented); color:#3b463d; font-weight:500; }
|
||||
|
|
@ -915,6 +949,7 @@ export default defineComponent({
|
|||
.vtl.rail-collapsed .vtl-tab-emoji { font-size:19px; }
|
||||
.vtl.rail-collapsed .vtl-tab-fallback { display:inline-flex; }
|
||||
.vtl.rail-collapsed .vtl-tab[data-depth]::before { display:none; }
|
||||
.vtl.rail-collapsed .vtl-twisty { display:none; }
|
||||
.vtl.rail-collapsed .vtl-tier { padding-left:196px; }
|
||||
.vtl.rail-collapsed .vtl-tl, .vtl.rail-collapsed .vtl-td, .vtl.rail-collapsed .vtl-cap, .vtl.rail-collapsed .vtl-chev { display:none; }
|
||||
.vtl.rail-collapsed .vtl-grp + .vtl-grp { margin-top:6px; padding-top:6px; }
|
||||
|
|
@ -941,6 +976,7 @@ export default defineComponent({
|
|||
.vtl.rail-collapsed .vtl-tab { padding-left:10px; gap:11px; }
|
||||
.vtl.rail-collapsed .vtl-tab[data-depth] { padding-left:calc(12px + var(--vtl-depth) * 16px); }
|
||||
.vtl.rail-collapsed .vtl-tab[data-depth]::before { display:block; }
|
||||
.vtl.rail-collapsed .vtl-twisty { display:inline-flex; }
|
||||
.vtl.rail-collapsed .vtl-tab-text { display:flex; }
|
||||
.vtl.rail-collapsed .vtl-tab-fallback { display:none; }
|
||||
.vtl.rail-collapsed .vtl-tier { padding-left:8px; }
|
||||
|
|
|
|||
|
|
@ -422,17 +422,32 @@ export default defineComponent({
|
|||
navGroups() {
|
||||
const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
const treeOf = new Map(this.compositionTrees.map(t => [t.nodes[0].selects, t]))
|
||||
// ★ FOLDED BY DEFAULT (Driver, 2026-09-24: "it needs to be collapsible, otherwise it is too
|
||||
// hard to know how to navigate so many nested plugins"). Every node with parts is a BRANCH with
|
||||
// its own chevron, closed until opened — so the rail first reads as a plain list of plugins, and
|
||||
// you drill into one composite, then one level of it, at a time. Picking a composite opens it.
|
||||
// Branch ids are namespaced by the composite's row key: the same child (dashboard-plus-grid)
|
||||
// folds independently under doxa-maps-spa and in its own CAP 4 entry.
|
||||
const parts = (n) => n + (n === 1 ? ' part' : ' parts')
|
||||
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)
|
||||
const bid = (id) => 'b:' + r.key + ':' + id
|
||||
const out = [{
|
||||
key: r.key, label: r.label, icon: r.icon,
|
||||
branch: t ? bid(0) : undefined,
|
||||
sub: '<code><' + esc(r.tag) + '></code>' + (t ? ' · ' + parts(t.nodes[0].kids) : '')
|
||||
}]
|
||||
if (t) {
|
||||
t.nodes.slice(1).forEach((n, i) => out.push({
|
||||
key: 'tree-' + r.key + '-' + i,
|
||||
t.nodes.slice(1).forEach((n) => out.push({
|
||||
key: 'tree-' + r.key + '-' + n.id,
|
||||
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>'
|
||||
branch: n.kids ? bid(n.id) : undefined,
|
||||
ancestors: n.ancestors.map(bid),
|
||||
sub: n.part ? 'no standalone page'
|
||||
: 'CAP ' + n.cap + ' · <code><' + esc(n.tag) + '></code>' + (n.kids ? ' · ' + parts(n.kids) : '')
|
||||
}))
|
||||
}
|
||||
return out
|
||||
|
|
@ -464,7 +479,9 @@ export default defineComponent({
|
|||
// own page because it has none, so it opens the composite it lives in.
|
||||
compositionTrees() {
|
||||
const out = []
|
||||
const walk = (row, depth, path, nodes) => {
|
||||
// Every node records `ancestors` (the ids of the nodes above it) and, if anything was nested
|
||||
// under it, `kids` — which is what lets the rail fold each level independently.
|
||||
const walk = (row, depth, path, nodes, ancestors) => {
|
||||
const b = this.manifestBundles.find(x => x.bundle === row.bundle)
|
||||
const refs = (row.cap >= 4 && b && b.composes) || []
|
||||
const kids = []
|
||||
|
|
@ -479,12 +496,15 @@ export default defineComponent({
|
|||
}
|
||||
kids.sort((a, b2) => b2.cap - a.cap || a.label.localeCompare(b2.label))
|
||||
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, depth + 1, new Set([...path, k.key]), nodes)
|
||||
const node = { id: nodes.length, label: k.label, tag: k.tag, cap: k.cap, selects: k.key, depth: depth + 1, ancestors, kids: 0 }
|
||||
nodes.push(node)
|
||||
const before = nodes.length
|
||||
if (!path.has(k.key)) walk(k, depth + 1, new Set([...path, k.key]), nodes, [...ancestors, node.id])
|
||||
node.kids = nodes.filter((n, i) => i >= before && n.depth === depth + 2).length
|
||||
}
|
||||
// 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 ref of parts) nodes.push({ id: nodes.length, label: ref, part: true, selects: row.key, depth: depth + 1, ancestors, kids: 0 })
|
||||
}
|
||||
for (const b of this.manifestBundles) {
|
||||
if (!b.composes || !b.composes.length || STUDIO_HIDDEN.has(b.bundle)) continue
|
||||
|
|
@ -493,8 +513,9 @@ export default defineComponent({
|
|||
// this rail already IS its composition.
|
||||
const own = this.rows.find(r => r.bundle === b.bundle)
|
||||
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)
|
||||
const nodes = [{ id: 0, label: own.label, tag: own.tag, cap: own.cap, selects: own.key, depth: 0, ancestors: [] }]
|
||||
walk(own, 0, new Set([own.key]), nodes, [0])
|
||||
nodes[0].kids = nodes.filter(n => n.depth === 1).length
|
||||
if (nodes.length > 1) out.push({ bundle: b.bundle, nodes })
|
||||
}
|
||||
return out
|
||||
|
|
|
|||
Loading…
Reference in a new issue