Plugin Studio names each row from the plugin's own meta label, never a bare profile id

The rail showed Gospel x Internet as "board": a row's name fell back to its
profile id unless a hand-kept DISPLAY_NAME table had an entry. The manifest now
carries each profile's .meta.json label (or title / displayName), and the rail
shows it without a trailing aside in parentheses; with no label it shows a name
made from the ids ("Gospel Resources · Charts"). DISPLAY_NAME is an override
only and is empty. check:studio-rail fails if any row would show a bare id.
This commit is contained in:
Doxa Maps 2026-09-28 01:24:24 +00:00
parent b7333ef970
commit cdb9efc9d4
3 changed files with 24 additions and 8 deletions

View file

@ -64,6 +64,8 @@ try {
}
}
for (const b of byBundle.keys()) if (!seen.has(b)) fail(`bundle ${b} is missing from the rail`)
// A row is NAMED, never shown as a bare id (the Driver: Gospel x Internet read just "board").
for (const r of ctx.rows) if (r.label === r.profile || r.label === r.bundle) fail(`${r.key} shows a bare id ("${r.label}") — give its <profile>.meta.json a "label"`)
const canon = secs.flatMap((s) => s.nodes.filter((n) => n.canonical))
for (const r of ctx.rows) { const c = canon.filter((n) => n.target === r.key).length; if (c !== 1) fail(`${r.key} has ${c} canonical nodes (want 1)`) }
const nProd = [...stageOf.values()].filter((v) => v === 'production').length

View file

@ -17,18 +17,21 @@ export function readProfileMeta(dir, profileName) {
try {
const meta = JSON.parse(readFileSync(join(dir, `${profileName}.meta.json`), 'utf8'))
const corner = meta?.widget?.corner
// The plugin's own display name (label, or title / displayName): what the Plugin Studio rail calls it.
const name = [meta?.label, meta?.title, meta?.displayName].find((v) => typeof v === 'string' && v.trim())
return {
stage: meta?.stage === 'production' ? 'production' : 'experimental',
heavy: meta?.heavy === true,
widget: CORNERS.includes(corner) ? { corner } : null,
label: name ? name.trim() : '',
}
} catch { return { stage: 'experimental', heavy: false, widget: null } }
} catch { return { stage: 'experimental', heavy: false, widget: null, label: '' } }
}
/** The optional manifest keys for one profile (stage always; heavy / widget only when set). */
export function profileMetaFields(dir, profileName) {
const m = readProfileMeta(dir, profileName)
return { stage: m.stage, ...(m.heavy ? { heavy: true } : {}), ...(m.widget ? { widget: m.widget } : {}) }
return { stage: m.stage, ...(m.label ? { label: m.label } : {}), ...(m.heavy ? { heavy: true } : {}), ...(m.widget ? { widget: m.widget } : {}) }
}
/** A bundle is production if ANY of its profiles says so. */

View file

@ -411,11 +411,22 @@ for (const [path, mod] of Object.entries(import.meta.glob('../../library/i18n/lo
if (m) STUDIO_STRINGS[m[1]] = mod
}
// DISPLAY NAMES — what a row is CALLED, keyed by "<bundle>/<profile>". Display only: the row key, the
// URL hash, the stored last-tab and the <tag> under the name are all unchanged, so links and memory
// keep working. The Driver's name for the cap-5 shell is "Research Gallery" (its meta.json label);
// the manifest does not publish meta labels, so the studio carries the one it needs here.
const DISPLAY_NAME = { 'doxa-maps-spa/doxa-maps-spa': 'Research Gallery' }
// DISPLAY NAMES — what a row is CALLED. Display only: the row key, the URL hash, the stored last-tab and
// the <tag> under the name are unchanged, so links and memory keep working. AUTO-DETECTED (the Driver:
// never hardcode): the plugin's own <profile>.meta.json label, carried by the manifest; failing that, a
// readable name made from its ids ("Gospel Internet · Board"), never a bare profile id (a row once read
// just "board"). DISPLAY_NAME is an OVERRIDE only, and empty unless a label must differ from the meta.
const DISPLAY_NAME = {}
const humanize = (id) => String(id).split(/[-_]+/).filter(Boolean).map(w => w[0].toUpperCase() + w.slice(1)).join(' ')
function rowLabel(bundle, p) {
const key = bundle + '/' + p.profile
if (DISPLAY_NAME[key]) return DISPLAY_NAME[key]
// A meta label often ends in an aside for the reader of the JSON ("Research Gallery (vertical-tab app
// shell: …)"); the rail shows the name before it.
const own = typeof p.label === 'string' ? p.label.replace(/\s*\([^()]*\)\s*$/, '').trim() : ''
if (own) return own
return p.profile === bundle ? humanize(bundle) : humanize(bundle) + ' · ' + humanize(p.profile)
}
// Load a bundle exactly once per URL; resolves after its define() has run.
//
@ -988,7 +999,7 @@ export default defineComponent({
bundle: b.bundle,
// production | experimental, from the bundle's .meta.json via the manifest (never a list here)
stage: b.stage === 'production' ? 'production' : 'experimental',
label: DISPLAY_NAME[b.bundle + '/' + p.profile] || p.profile,
label: rowLabel(b.bundle, p),
// The REAL profile id, handed to the element in profile-config. Never the label: a display
// name would mount <doxa-maps-spa> with profile "Research Gallery", which it does not have.
profile: p.profile,