A map says how it behaves on a narrow screen once, in its <profile>.meta.json: "mobile": "desktop-only" (blocked on a phone) or "larger-screens" (opens, under a dismissible notice). Absent means phone-ready. Until now the rule lived only in the Research Gallery, as two hard-coded id sets, so a map opened standalone on its own leaf page on a phone got neither the block nor the notice. - profile-meta.mjs knows the vocabulary; generate-tree and the dev manifest carry the flag into each profile's manifest entry, and generate-tree into the leaf page's BASE_CONFIG (with the map's name for the desktop-only sentence). - ProfileLoader mounts library/components/LargerScreenNotice.vue on a screen under 640px: for desktop-only the profile is never mounted and a centred message says why; for larger-screens a slim in-flow bar sits above the map (it cannot cover the map's controls), dismissible per profile for the session, in the element's own locale and direction. Inside the Gallery it stays quiet: every Gallery pane now carries data-doxa-gallery. - One text: the Gallery's two sentences moved into the library catalogue as messages.largerScreenNotice / desktopOnlyNotice (+ noticeDismiss), all 11 locales moved as they were, and the Gallery reads them from there. - The Gallery derives its rules from the manifest's flags. On a phone it mounts nothing until the manifest has landed: its baked first tab (Big Picture) would otherwise mount there before the flags say it is desktop-only. - dashboard-plus-grid is desktop-only and gospel-internet/board is larger-screens, the two sets the Gallery held; nothing changes for either. - check:mobile-gating now asserts the vocabulary, that each flagged leaf page carries its flag, the Gallery's derivation and its closed-before-manifest rule, that Big Picture stays desktop-only, the 11 locales, and the notice's mounting. Its self-test plants one failure per rule.
93 lines
5.6 KiB
JavaScript
93 lines
5.6 KiB
JavaScript
/**
|
|
* profile-meta.mjs — the per-profile fields a host reads from <profile>.meta.json, in ONE place.
|
|
*
|
|
* generate-tree (the built manifest.json) and the dev server's /__plugin-gallery/manifest.json both call
|
|
* this, so the dev Studio and the built Studio cannot disagree (rega: the corner widget never mounted in
|
|
* dev because only the built manifest carried "widget").
|
|
* stage "production" | "experimental" default experimental
|
|
* heavy true when the element holds live WebGL contexts
|
|
* widget { corner } for a corner widget the Plugin Studio mounts once, fixed
|
|
* visibility "team" | "public" default team — see VISIBILITY below
|
|
* mobile "desktop-only" | "larger-screens" absent = phone-ready — see MOBILE below
|
|
*/
|
|
import { readFileSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
|
|
const CORNERS = ['bottom-right', 'bottom-left', 'top-right', 'top-left']
|
|
|
|
/**
|
|
* ★ WHO ELSE MAY LIST THIS PLUGIN (dx-base, 2026-09-28). The platform dashboard reads each team's
|
|
* own /cdn/dx-maps/manifest.json instead of one shared index, so this field is what decides whether
|
|
* a plugin appears in ANOTHER team's library (loaded read-only from the owner's CDN).
|
|
*
|
|
* "team" listed only in the owning team's library
|
|
* "public" also listed in every other team's library
|
|
* ABSENT team — every manifest that exists today is private until a plugin opts in
|
|
*
|
|
* ★ DO NOT REACH FOR `stage` TO EXPRESS THIS. It sits right next to it in this file and reads like
|
|
* a fit: it is maturity, not audience. Using it would hide every experimental plugin from its OWN
|
|
* team and publish every production one to everybody — wrong in both directions at once.
|
|
*
|
|
* ★ AN UNKNOWN VALUE IS REFUSED, NOT DEFAULTED. Defaulting "Public" or "pubic" to team is the safe
|
|
* direction and that is exactly what makes it dangerous: the author reads their opt-in as done, the
|
|
* field says private, and nobody finds out until someone asks why the plugin never appeared. The
|
|
* generator exits on it (generate-tree, "visibility" block) so a typo is a build failure, loud, at
|
|
* the moment it is introduced.
|
|
*/
|
|
export const VISIBILITY = ['team', 'public']
|
|
|
|
/**
|
|
* ★ HOW A MAP BEHAVES ON A PHONE, declared once per profile (SPEC-MOBILE-FLAG). The Research Gallery
|
|
* and the standalone leaf page both read it from here, through the manifest and the leaf's config:
|
|
*
|
|
* "desktop-only" BLOCKS on a narrow screen: the map is not mounted, a message says why
|
|
* "larger-screens" ADVISES: the map opens, with a dismissible notice at its top
|
|
* ABSENT phone-ready: nothing is shown
|
|
*
|
|
* Any other value is not coerced: check:mobile-gating fails on it, by profile, so a typo cannot
|
|
* quietly turn a desktop-only map into a phone-ready one.
|
|
*/
|
|
export const MOBILE = ['desktop-only', 'larger-screens']
|
|
|
|
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() : '',
|
|
// Where the plugin runs ("web", "desktop"…); absent = web. A desktop-only plugin is never a gallery tab.
|
|
targets: Array.isArray(meta?.targets) ? meta.targets.filter((t) => typeof t === 'string') : null,
|
|
visibility: VISIBILITY.includes(meta?.visibility) ? meta.visibility : 'team',
|
|
// The declared-but-unrecognised value, kept so the generator can name it in the failure.
|
|
// null when the key is absent (the supported way to say "team") or already valid.
|
|
visibilityInvalid: meta?.visibility === undefined || VISIBILITY.includes(meta?.visibility)
|
|
? null
|
|
: String(meta.visibility),
|
|
mobile: MOBILE.includes(meta?.mobile) ? meta.mobile : null,
|
|
}
|
|
} catch { return { stage: 'experimental', heavy: false, widget: null, label: '', targets: null, visibility: 'team', visibilityInvalid: null, mobile: null } }
|
|
}
|
|
|
|
/** The optional manifest keys for one profile (stage always; heavy / widget only when set). */
|
|
export function profileMetaFields(dir, profileName) {
|
|
const m = readProfileMeta(dir, profileName)
|
|
// visibility is emitted ONLY when it is "public": absent means team, so every manifest in the
|
|
// tree today keeps the bytes it has and nothing becomes visible by upgrading the generator.
|
|
return { stage: m.stage, ...(m.visibility === 'public' ? { visibility: 'public' } : {}), ...(m.label ? { label: m.label } : {}), ...(m.targets ? { targets: m.targets } : {}), ...(m.heavy ? { heavy: true } : {}), ...(m.widget ? { widget: m.widget } : {}), ...(m.mobile ? { mobile: m.mobile } : {}) }
|
|
}
|
|
|
|
/** A bundle is production if ANY of its profiles says so. */
|
|
export const bundleStage = (profiles) => (profiles.some((p) => p.stage === 'production') ? 'production' : 'experimental')
|
|
|
|
/**
|
|
* A bundle is public if ANY of its profiles is — because a bundle is ONE IIFE. The profiles in it
|
|
* share a single .js, so publishing one profile publishes the file the others are compiled into;
|
|
* there is no shape of this field that makes half a script fetchable. The per-profile key is still
|
|
* emitted, so a reader can list only the profiles that opted in while loading the one bundle.
|
|
*/
|
|
export const bundleVisibility = (profiles) => (profiles.some((p) => p.visibility === 'public') ? 'public' : 'team')
|