doxa-maps/plugin-designs/golden-path-check/golden-path-check.vue
Doxa Maps e19697673d
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Doxa Maps — the map bundler, 7 production plugins and the Experimental shelf
2026-09-25 20:07:59 +00:00

169 lines
6.1 KiB
Vue

<script setup>
/**
* golden-path-check.vue — the capability-1 single-element SELF-TEST of the golden path.
*
* @element golden-path-check
* @description A live self-test of the develop-a-plugin-and-see-it-live path: it reads the built
* manifest.json it was published beside and reports what the catalog actually contains — bundle
* and profile counts, how many profiles describe themselves, and whether this very plugin is
* listed. If you can see it answering, the whole path worked.
*
* WHY THIS EXISTS. The north star is that an admin agent can author a plugin, build, publish and
* SEE IT LIVE. That path was documented and its parts were individually verified, but never walked
* end-to-end in one pass by following the doc as written. This plugin IS that walk: authoring it
* from the documented 3-file minimum, building it, and finding it in the live catalog exercises
* every step. It then stays useful, because it reports the catalog's real state to anyone who
* opens it — the first thing an inheritor can click to confirm their own build reached the world.
*
* DELIBERATELY DEPENDENCY-FREE. No map engine, no Mapbox token, no data API, no shared library
* component. A cap-1 element is the right tier for a self-test: it must be able to mount when
* everything else is broken, which is exactly when someone reaches for it. Material light only
* (#f3f3f1 / #fff / #3b463d / #92b195 / #d1d4cd / #6d766e), per the standing palette rule.
*
* TREE-RELATIVE BY CONSTRUCTION. It resolves manifest.json from its own document URL rather than
* any baked host, so it answers correctly wherever the tree is dropped — CDN, a foreign subpath,
* or file://. That is the same position-independence check:drop enforces for every other bundle.
*/
import { ref, computed, onMounted } from 'vue'
/**
* `tree-base` is a plain ATTRIBUTE, not an injection. This element mounts its own SFC (no
* ProfileLoader — see index.js), so there is no provide('profileConfig') above it. Taking a prop
* instead keeps the element self-sufficient: it works when embedded directly by a host page, and
* it cannot break because something upstream failed to provide a key.
*/
const props = defineProps({
treeBase: { type: String, default: '' },
})
const state = ref('loading') // loading | ok | error
const error = ref('')
const manifest = ref(null)
/**
* Where is the tree root? The leaf page for any profile is served at
* <tree>/<bundle>/<profile>/index.html
* so the manifest sits three levels up. A host embedding the element directly can override with
* the `tree-base` ATTRIBUTE (<golden-path-check tree-base="https://host/doxa-maps">). Falls back
* to deriving it from location.pathname so a plain drop still resolves.
*/
const treeBase = computed(() => {
if (props.treeBase) return props.treeBase.replace(/\/+$/, '')
if (typeof location === 'undefined') return '.'
const parts = location.pathname.split('/').filter(Boolean)
// drop index.html + <profile> + <bundle> when they are present
const trimmed = parts.slice(0, Math.max(0, parts.length - 3))
return `${location.origin}/${trimmed.join('/')}`.replace(/\/+$/, '')
})
onMounted(async () => {
try {
const res = await fetch(`${treeBase.value}/manifest.json`, { cache: 'no-store' })
if (!res.ok) throw new Error(`manifest.json → HTTP ${res.status}`)
manifest.value = await res.json()
state.value = 'ok'
} catch (e) {
error.value = String(e && e.message ? e.message : e)
state.value = 'error'
}
})
const bundles = computed(() => (manifest.value?.bundles || []))
const profiles = computed(() => bundles.value.flatMap(b => b.profiles || []))
const described = computed(() =>
profiles.value.filter(p => String(p.description || '').trim().length > 0).length)
const selfListed = computed(() =>
bundles.value.some(b => (b.name || b.bundle) === 'golden-path-check'))
const rows = computed(() => [
{ label: 'bundles in the catalog', value: String(bundles.value.length) },
{ label: 'profiles in the catalog', value: String(profiles.value.length) },
{ label: 'profiles that describe themselves',
value: `${described.value} / ${profiles.value.length}` },
{ label: 'this plugin is listed', value: selfListed.value ? 'yes' : 'no' },
])
</script>
<template>
<div class="gpc">
<h2 class="gpc-h">Golden path — live self-test</h2>
<p v-if="state === 'loading'" class="gpc-note">Reading the catalog manifest…</p>
<p v-else-if="state === 'error'" class="gpc-note gpc-err">
Could not read <code>manifest.json</code> beside this page.<br>
<small>{{ error }}</small>
</p>
<template v-else>
<dl class="gpc-rows">
<div v-for="r in rows" :key="r.label" class="gpc-row">
<dt>{{ r.label }}</dt>
<dd>{{ r.value }}</dd>
</div>
</dl>
<p class="gpc-note">
This element was authored in <code>plugin-designs/</code>, built with
<code>bun run build</code>, and published to the catalog. It is reading the manifest it
shipped beside — so if these numbers are here, the whole path worked.
</p>
</template>
</div>
</template>
<style>
/* Material LIGHT only — no dark mode, no prefers-color-scheme:dark (standing palette rule). */
:host { display: block; }
.gpc {
background: #f3f3f1;
color: #3b463d;
border: 1px solid #d1d4cd;
border-radius: 10px;
padding: 18px 20px;
font: 14px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
}
.gpc-h {
margin: 0 0 14px;
font-size: 16px;
font-weight: 700;
letter-spacing: -0.01em;
}
.gpc-rows { margin: 0; }
.gpc-row {
display: flex;
justify-content: space-between;
gap: 16px;
padding: 9px 0;
border-bottom: 1px solid #d1d4cd;
}
.gpc-row:last-child { border-bottom: 0; }
.gpc-row dt { color: #6d766e; }
.gpc-row dd {
margin: 0;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.gpc-note {
margin: 14px 0 0;
color: #6d766e;
font-size: 12.5px;
}
.gpc-err { color: #3b463d; }
.gpc code {
background: #fff;
border: 1px solid #d1d4cd;
border-radius: 4px;
padding: 1px 5px;
font: 12px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
}
</style>