Add India Posters review plugin (preview-only)
A review gallery for the whole-India poster challenge — fitting every India people group on one printable sheet. It gathers each design attempt with what was tried and the measured result, and lets a reviewer label it, score it 1-10, and tag it a best practice or one to avoid (saved locally, exportable as JSON). Withheld from every public build: the attempts, their descriptions, and the poster documents load only under the dev server, in a dev-only gallery module that compiles away. A production build shows only the "solution in progress" notice and emits zero whole-country-poster bytes. Element <india-posters>; stage experimental, visibility team.
This commit is contained in:
parent
c7b77b0a12
commit
40cfb1da68
23 changed files with 3389 additions and 6 deletions
|
|
@ -13,7 +13,7 @@
|
|||
> bundle `parameterized` (cap-2) no matter how light it looks, and a `meta.json` `type`
|
||||
> declares tiers 3/4/5.
|
||||
|
||||
Generated from 28 bundle(s).
|
||||
Generated from 29 bundle(s).
|
||||
|
||||
### One canonical exemplar per tier (generated — quote these)
|
||||
|
||||
|
|
@ -51,6 +51,7 @@ Generated from 28 bundle(s).
|
|||
| `gospel-resources` | `data-tables` | `single` |
|
||||
| `gospel-resources` | `resources-list` | `single` |
|
||||
| `gospel-resources` | `resources-map` | `single` |
|
||||
| `india-posters` | `india-posters` | `single` |
|
||||
| `my-upg-100-list` | `poster-wall` | `single` |
|
||||
| `my-upg-100-list` | `upg-100-list` | `single` |
|
||||
| `poster-design` | `poster-sizes` | `single` |
|
||||
|
|
|
|||
24
plugin-designs/india-posters/README.md
Normal file
24
plugin-designs/india-posters/README.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# india-posters — whole-India poster design attempts (review + grading)
|
||||
|
||||
A review surface, **not a map**. It gathers every attempt at putting all ~855 India people groups
|
||||
on one poster — the leadered sheets, the by-state sheets, and the classic4 numbered family
|
||||
(numbered pins, range badges, and the forked-leader experiments) — each with **what we tried** and
|
||||
the **measured result**. Open any attempt full, then record a **label**, a **1–10 score**, a
|
||||
**Best practice / To avoid** tag, and notes. The record is saved in the browser (localStorage) and
|
||||
can be **exported/imported as JSON** so the catalogue of best practices + pitfalls travels with the repo.
|
||||
|
||||
## B25 — withheld from every public build
|
||||
The whole-India poster is not shipped until it prints legibly. So the attempts render **only** under
|
||||
the Vite dev server (the pilot), gated by `import.meta.env.DEV && globalThis.DXM_PREVIEW_UNRELEASED`.
|
||||
A production build (the research landing and DX Life's `/dx-maps`) compiles that branch — and every
|
||||
poster import inside it — away, leaving **only the withheld sentence** and **zero India-poster bytes**
|
||||
in `public/`. Poster documents are loaded with DEV-guarded **static dynamic `?raw` imports** (never
|
||||
`import.meta.glob` or `?url`, which emit assets) and shown in a `srcdoc` iframe. Enforced by
|
||||
`check:preview-flag-dev-only` and the DX Life drop-in guard.
|
||||
|
||||
## Files
|
||||
- `index.js` — registers `<india-posters>`; sets the dev-only preview flag (guarded).
|
||||
- `india-posters.vue` — the gallery + grading UI.
|
||||
- `data/attempts.json` — the attempts, each with `tried` + measured `result`.
|
||||
- `data/posters/*.html` — the poster documents (source only; dev-loaded, never shipped).
|
||||
- `data/grades.seed.json` — optional committed grades to seed the UI (localStorage overrides).
|
||||
86
plugin-designs/india-posters/data/attempts.json
vendored
Normal file
86
plugin-designs/india-posters/data/attempts.json
vendored
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
{
|
||||
"_comment": "The whole-India poster design attempts, oldest → newest, for review + grading. 'file' matches a .html in ./posters/ (imported ?url, lazy). 'result' is the MEASURED outcome. Grades are entered live in the UI and persisted separately (localStorage + export); nothing here is a grade.",
|
||||
"attempts": [
|
||||
{
|
||||
"id": "leadered-60x96",
|
||||
"title": "Leadered · 60×96",
|
||||
"family": "Leadered (a line to every name)",
|
||||
"size": "60×96 in (5×8 ft)",
|
||||
"file": "leadered-60x96.html",
|
||||
"tried": "Every group is a pin joined by a thin leader line to its name in the four margins, names on 45° rows.",
|
||||
"result": "All 855 at 22.4pt WITH photos, 0 crossings — but a grey web of leaders crosses central India and two dark cones funnel over Kashmir and under Sri Lanka, muddying the map."
|
||||
},
|
||||
{
|
||||
"id": "leadered-66x66",
|
||||
"title": "Leadered · 66×66",
|
||||
"family": "Leadered (a line to every name)",
|
||||
"size": "66×66 in (5.5×5.5 ft)",
|
||||
"file": "leadered-66x66.html",
|
||||
"tried": "Same leadered design on the square sheet.",
|
||||
"result": "855 at 19.7pt, 7.3mm photos, but 2 crossings and the same leader web + cones."
|
||||
},
|
||||
{
|
||||
"id": "bystate-60x96",
|
||||
"title": "By-state · 60×96",
|
||||
"family": "By-state (grouped, leaderless)",
|
||||
"size": "60×96 in (5×8 ft)",
|
||||
"file": "bystate-60x96.html",
|
||||
"tried": "Names grouped under state headings, state-coloured dots, NO per-name leaders — locate a group by its state band + colour.",
|
||||
"result": "Clean and calm, no web — but on the tall 60×96 the map shrinks to a weak island. By-state wants the square sheet."
|
||||
},
|
||||
{
|
||||
"id": "bystate-66x66",
|
||||
"title": "By-state · 66×66",
|
||||
"family": "By-state (grouped, leaderless)",
|
||||
"size": "66×66 in (5.5×5.5 ft)",
|
||||
"file": "bystate-66x66.html",
|
||||
"tried": "The leaderless by-state design on the square sheet, every group an equal photo/placeholder slot.",
|
||||
"result": "855 at 17.5pt, 11.6mm photos, clean calm map, no web. (Set aside in favour of the classic4 numbered approach.)"
|
||||
},
|
||||
{
|
||||
"id": "classic4-a-numbered-60x96",
|
||||
"title": "Classic4 (a) numbered pins · 60×96",
|
||||
"family": "Classic4 (numbered, imitates the library sheet)",
|
||||
"size": "60×96 in (5×8 ft)",
|
||||
"file": "classic4-a-numbered-60x96.html",
|
||||
"tried": "Numbered pins (the number in the pin AND before the name), one column left + one right + a 45° row top and bottom, map framed to India's own bounds. No leaders.",
|
||||
"result": "0 leader crossings; 164 pin overlaps where the coasts are dense. Every name legible and listed."
|
||||
},
|
||||
{
|
||||
"id": "classic4-b-rangebadge-60x96",
|
||||
"title": "Classic4 (b) range badges · 60×96 ★ recommended",
|
||||
"family": "Classic4 (numbered, imitates the library sheet)",
|
||||
"size": "60×96 in (5×8 ft)",
|
||||
"file": "classic4-b-rangebadge-60x96.html",
|
||||
"tried": "Same as (a), but co-located groups share one '12–18' range badge so dense coasts stop piling up.",
|
||||
"result": "0 crossings AND 0 pin overlaps, 168 range badges, all 855 at 11pt, India edge-to-edge (13.9% of the page — the honest ceiling with 855 four-side names). The recommended design."
|
||||
},
|
||||
{
|
||||
"id": "classic4-c1-fork-row-60x96",
|
||||
"title": "Classic4 (c1) forked leaders — row · 60×96",
|
||||
"family": "Classic4 forked leaders (bringing the lines back)",
|
||||
"size": "60×96 in (5×8 ft)",
|
||||
"file": "classic4-c1-fork-row-60x96.html",
|
||||
"tried": "Bring the lines back: paired, forked leaders that split at the row end to reach two names at once.",
|
||||
"result": "2062 leader crossings — a web across the whole map. Confirms leaders cannot stay untangled at 855-density."
|
||||
},
|
||||
{
|
||||
"id": "classic4-c2-fork-trunk-60x96",
|
||||
"title": "Classic4 (c2) forked leaders — trunk · 60×96",
|
||||
"family": "Classic4 forked leaders (bringing the lines back)",
|
||||
"size": "60×96 in (5×8 ft)",
|
||||
"file": "classic4-c2-fork-trunk-60x96.html",
|
||||
"tried": "Forked leaders with a shared trunk before the fork.",
|
||||
"result": "6235 crossings — worse still; near-vertical trunks fill the north and a solid block of leaders buries India's outline."
|
||||
},
|
||||
{
|
||||
"id": "classic4-b-rangebadge-66x66",
|
||||
"title": "Classic4 (b) range badges · 66×66 (comparison)",
|
||||
"family": "Classic4 (numbered, imitates the library sheet)",
|
||||
"size": "66×66 in (5.5×5.5 ft)",
|
||||
"file": "classic4-b-rangebadge-66x66.html",
|
||||
"tried": "The recommended (b) design on the square sheet, to compare India's prominence.",
|
||||
"result": "India only 10.4% of the page vs 13.9% at 60×96 — the tall sheet actually suits India better (India is taller than wide)."
|
||||
}
|
||||
]
|
||||
}
|
||||
1
plugin-designs/india-posters/data/grades.seed.json
vendored
Normal file
1
plugin-designs/india-posters/data/grades.seed.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
1
plugin-designs/india-posters/data/posters/bystate-60x96.html
vendored
Normal file
1
plugin-designs/india-posters/data/posters/bystate-60x96.html
vendored
Normal file
File diff suppressed because one or more lines are too long
1
plugin-designs/india-posters/data/posters/bystate-66x66.html
vendored
Normal file
1
plugin-designs/india-posters/data/posters/bystate-66x66.html
vendored
Normal file
File diff suppressed because one or more lines are too long
363
plugin-designs/india-posters/data/posters/classic4-a-numbered-60x96.html
vendored
Normal file
363
plugin-designs/india-posters/data/posters/classic4-a-numbered-60x96.html
vendored
Normal file
File diff suppressed because one or more lines are too long
363
plugin-designs/india-posters/data/posters/classic4-b-rangebadge-60x96.html
vendored
Normal file
363
plugin-designs/india-posters/data/posters/classic4-b-rangebadge-60x96.html
vendored
Normal file
File diff suppressed because one or more lines are too long
363
plugin-designs/india-posters/data/posters/classic4-b-rangebadge-66x66.html
vendored
Normal file
363
plugin-designs/india-posters/data/posters/classic4-b-rangebadge-66x66.html
vendored
Normal file
File diff suppressed because one or more lines are too long
363
plugin-designs/india-posters/data/posters/classic4-c1-fork-row-60x96.html
vendored
Normal file
363
plugin-designs/india-posters/data/posters/classic4-c1-fork-row-60x96.html
vendored
Normal file
File diff suppressed because one or more lines are too long
363
plugin-designs/india-posters/data/posters/classic4-c2-fork-trunk-60x96.html
vendored
Normal file
363
plugin-designs/india-posters/data/posters/classic4-c2-fork-trunk-60x96.html
vendored
Normal file
File diff suppressed because one or more lines are too long
369
plugin-designs/india-posters/data/posters/leadered-60x96.html
vendored
Normal file
369
plugin-designs/india-posters/data/posters/leadered-60x96.html
vendored
Normal file
File diff suppressed because one or more lines are too long
369
plugin-designs/india-posters/data/posters/leadered-66x66.html
vendored
Normal file
369
plugin-designs/india-posters/data/posters/leadered-66x66.html
vendored
Normal file
File diff suppressed because one or more lines are too long
20
plugin-designs/india-posters/index.html
Normal file
20
plugin-designs/india-posters/index.html
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>doxa-maps · India Posters staging</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body { background: #f7f4ec; min-height: 100%; font-family: system-ui, sans-serif; }
|
||||
india-posters { display: block; width: 100%; min-height: 100vh; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- In dev (the pilot) the preview flag is on and the attempts render; a production build shows
|
||||
only the withheld sentence. The element registers itself from ./index.js. -->
|
||||
<india-posters></india-posters>
|
||||
<script type="module" src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
38
plugin-designs/india-posters/index.js
Normal file
38
plugin-designs/india-posters/index.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* india-posters / index.js — Bundle entry.
|
||||
*
|
||||
* ONE folder → ONE bundle → ONE custom element: <india-posters>.
|
||||
*
|
||||
* A REVIEW gallery, not a map: it shows every whole-India poster design attempt we tried
|
||||
* (leadered, by-state, and the classic4 numbered family), lets the reviewer open each full,
|
||||
* and record a label + a 1–10 score + a "Best practice / To avoid" tag + notes per attempt.
|
||||
* The record persists in the browser (localStorage) and can be exported/imported as JSON so
|
||||
* the catalogue of best practices and pitfalls travels with the repo.
|
||||
*
|
||||
* ★ WHY A PLAIN ELEMENT (not defineMapElement): this surface draws no map and needs no
|
||||
* pinia/i18n/token machinery — it only iframes self-contained poster documents and keeps a
|
||||
* small grading record. So it registers as a lean Vue custom element, exactly like
|
||||
* plugin-studio's index.js, and can be nested/placed by any of the five deploy methods.
|
||||
*
|
||||
* Usage from a host page:
|
||||
* <script src="/dx-maps/india-posters/india-posters.js"></script>
|
||||
* <india-posters></india-posters>
|
||||
*/
|
||||
|
||||
import { defineCustomElement } from 'vue'
|
||||
import IndiaPosters from './india-posters.vue'
|
||||
|
||||
// ★ PREVIEW-UNRELEASED FLAG (B25). The whole-India poster is withheld from every public build until it
|
||||
// prints legibly. This surface shows the attempts ONLY under the Vite dev server (the pilot), where the
|
||||
// flag is on; a production build (research landing + DX Life's /dx-maps) compiles this line away, so the
|
||||
// built tree never names the flag and shows only the withheld sentence. Same guarded-setter pattern as
|
||||
// library/elements/defineMapElement.js, enforced by check:preview-flag-dev-only.
|
||||
if (import.meta.env.DEV && typeof globalThis !== 'undefined') globalThis.DXM_PREVIEW_UNRELEASED = true
|
||||
|
||||
const IndiaPostersElement = defineCustomElement(IndiaPosters)
|
||||
|
||||
if (typeof customElements !== 'undefined' && !customElements.get('india-posters')) {
|
||||
customElements.define('india-posters', IndiaPostersElement)
|
||||
}
|
||||
|
||||
export default IndiaPostersElement
|
||||
6
plugin-designs/india-posters/india-posters.meta.json
Normal file
6
plugin-designs/india-posters/india-posters.meta.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"label": "India Posters (design attempts — preview only)",
|
||||
"stage": "experimental",
|
||||
"visibility": "team",
|
||||
"_comment": "B25: the whole-India poster is withheld from every public build until it prints legibly. This review surface renders the attempts ONLY under the dev server (import.meta.env.DEV && globalThis.DXM_PREVIEW_UNRELEASED); a production build shows only the withheld sentence and emits ZERO India-poster bytes to public/ (enforced by check:preview-flag-dev-only + the DX Life drop-in guard). stage=experimental and visibility=team declared explicitly though both are the defaults."
|
||||
}
|
||||
112
plugin-designs/india-posters/india-posters.vue
Normal file
112
plugin-designs/india-posters/india-posters.vue
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<!--
|
||||
india-posters.vue — the <india-posters> element's root.
|
||||
|
||||
@description A review surface (not a map) for every whole-India poster design attempt. In a PUBLIC
|
||||
build it renders only the withheld sentence. Under the Vite dev server (the pilot), with the
|
||||
preview flag on, it mounts the full review gallery (india-gallery.vue) where each attempt can be
|
||||
opened, labelled, scored 1–10, tagged Best practice / To avoid, and noted.
|
||||
|
||||
★ B25 — WITHHELD FROM EVERY PUBLIC BUILD. The whole-India poster is not shipped until it prints
|
||||
legibly. The gallery — and therefore attempts.json, the grades seed, and every poster document —
|
||||
lives in india-gallery.vue, imported ONLY behind the import.meta.env.DEV && globalThis.DXM_PREVIEW_UNRELEASED guard.
|
||||
A production build (research landing + DX Life's /dx-maps) compiles that import away, so the module
|
||||
never enters the bundle: the built tree carries ONLY this withheld sentence and ZERO India-poster
|
||||
bytes. Enforced by check:preview-flag-dev-only + the DX Life drop-in guard.
|
||||
|
||||
Styles are declared here (non-scoped) so the custom element injects them once into its shadow root;
|
||||
the shadow root encapsulates them, and they also style the async-loaded gallery child.
|
||||
-->
|
||||
<template>
|
||||
<div class="ip-root">
|
||||
<!-- PUBLIC BUILD: the only thing that ships + the only thing shown off the dev server. -->
|
||||
<div v-if="!preview" class="ip-withheld">
|
||||
<h1>India Posters</h1>
|
||||
<p>Too many people groups to fit on one poster legibly — a solution is in progress.</p>
|
||||
</div>
|
||||
<!-- DEV PREVIEW (pilot only): the full review gallery, loaded dev-only. -->
|
||||
<component v-else-if="Gallery" :is="Gallery" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* india-posters.vue — the <india-posters> element.
|
||||
*
|
||||
* @element india-posters
|
||||
* @description A preview-only review board for the whole-India poster problem — fitting every one of
|
||||
* India's people groups onto a single printable sheet. It gathers the design attempts, each with
|
||||
* what was tried and how it measured, and lets a reviewer label, score 1 to 10, and tag each as a
|
||||
* best practice or one to avoid. Public builds show only a short notice; the poster is withheld
|
||||
* until it prints legibly.
|
||||
*/
|
||||
import { shallowRef, defineAsyncComponent } from 'vue'
|
||||
|
||||
const preview = import.meta.env.DEV && globalThis.DXM_PREVIEW_UNRELEASED === true
|
||||
|
||||
// Dev-only: the gallery module (with attempts.json + the posters) is pulled in ONLY here, behind the
|
||||
// compile-time-false guard, so it never enters a production bundle.
|
||||
const Gallery = shallowRef(null)
|
||||
if (import.meta.env.DEV) Gallery.value = defineAsyncComponent(() => import('./src/india-gallery.vue'))
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.ip-root { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; color: #1c1c17; background: #f7f4ec; padding: 20px; box-sizing: border-box; }
|
||||
.ip-withheld { max-width: 48ch; margin: 40px auto; text-align: center; }
|
||||
.ip-withheld h1 { font-size: 22px; margin: 0 0 8px; }
|
||||
.ip-withheld p { color: #55503f; }
|
||||
|
||||
.ip-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; flex-wrap: wrap; border-bottom: 2px solid #d9d2c2; padding-bottom: 14px; margin-bottom: 18px; }
|
||||
.ip-gallery h1 { font-size: 22px; margin: 0 0 4px; }
|
||||
.ip-sub { margin: 0; max-width: 64ch; font-size: 13px; color: #55503f; }
|
||||
.ip-sub em { color: #b23b2e; font-style: italic; }
|
||||
.ip-actions { display: flex; align-items: center; gap: 8px; }
|
||||
.ip-btn { background: #1c1c17; color: #f7f4ec; border: 0; border-radius: 6px; padding: 8px 12px; font-size: 13px; cursor: pointer; }
|
||||
.ip-btn:hover { background: #3a3a2e; }
|
||||
.ip-file { display: inline-flex; align-items: center; }
|
||||
.ip-saved { color: #2e7d32; font-size: 13px; font-weight: 600; }
|
||||
|
||||
.ip-summary { display: flex; gap: 28px; flex-wrap: wrap; background: #fffdf7; border: 1px solid #e4ddcc; border-radius: 10px; padding: 12px 18px; margin-bottom: 20px; }
|
||||
.ip-col { min-width: 220px; }
|
||||
.ip-col h2 { font-size: 14px; margin: 4px 0 6px; }
|
||||
.ip-h-best { color: #2e7d32; }
|
||||
.ip-h-avoid { color: #b23b2e; }
|
||||
.ip-col ol { margin: 0; padding-left: 20px; font-size: 13px; }
|
||||
.ip-col li { margin: 2px 0; }
|
||||
.ip-score { display: inline-block; min-width: 22px; text-align: center; font-weight: 700; border-radius: 4px; padding: 0 4px; margin-right: 4px; font-size: 12px; }
|
||||
.ip-score-best { background: #dcefdc; color: #2e7d32; }
|
||||
.ip-score-avoid { background: #f6dcd8; color: #b23b2e; }
|
||||
|
||||
.ip-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 18px; }
|
||||
.ip-card { background: #fffdf7; border: 1px solid #e4ddcc; border-radius: 12px; padding: 14px; display: flex; flex-direction: column; }
|
||||
.ip-card.is-best { border-color: #2e7d32; box-shadow: 0 0 0 2px #2e7d3233; }
|
||||
.ip-card.is-avoid { border-color: #b23b2e; box-shadow: 0 0 0 2px #b23b2e33; }
|
||||
.ip-card-head { display: flex; justify-content: space-between; font-size: 11px; color: #7a7360; text-transform: uppercase; letter-spacing: .04em; }
|
||||
.ip-title { font-size: 15px; margin: 6px 0 10px; }
|
||||
|
||||
.ip-preview { position: relative; height: 260px; background: #efe9db; border: 1px solid #e4ddcc; border-radius: 8px; overflow: hidden; display: flex; align-items: center; justify-content: center; cursor: pointer; }
|
||||
.ip-thumb-frame { width: 250%; height: 250%; border: 0; transform: scale(0.4); transform-origin: top left; pointer-events: none; }
|
||||
.ip-load { background: #1c1c17; color: #f7f4ec; border: 0; border-radius: 6px; padding: 8px 14px; cursor: pointer; font-size: 13px; }
|
||||
.ip-open-hint { position: absolute; bottom: 6px; right: 8px; font-size: 11px; background: #1c1c17cc; color: #f7f4ec; padding: 2px 6px; border-radius: 4px; }
|
||||
|
||||
.ip-facts { margin: 12px 0; font-size: 12.5px; }
|
||||
.ip-facts dt { font-weight: 700; color: #55503f; margin-top: 6px; }
|
||||
.ip-facts dd { margin: 2px 0 0; color: #333; }
|
||||
|
||||
.ip-grade { margin-top: auto; border-top: 1px dashed #d9d2c2; padding-top: 10px; display: flex; flex-direction: column; gap: 8px; }
|
||||
.ip-label-row, .ip-notes-row { display: flex; flex-direction: column; gap: 3px; font-size: 12px; font-weight: 600; color: #55503f; }
|
||||
.ip-label-row input, .ip-notes-row textarea { font: inherit; font-weight: 400; color: #1c1c17; border: 1px solid #d9d2c2; border-radius: 6px; padding: 6px 8px; }
|
||||
.ip-score-row { display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 600; color: #55503f; }
|
||||
.ip-score-row input { flex: 1; }
|
||||
.ip-score-row output { min-width: 42px; text-align: right; font-weight: 700; color: #1c1c17; }
|
||||
.ip-cat-row { display: flex; gap: 6px; }
|
||||
.ip-cat { flex: 1; border: 1px solid #d9d2c2; background: #fff; border-radius: 6px; padding: 6px 4px; font-size: 12px; cursor: pointer; }
|
||||
.ip-cat-best.on { background: #2e7d32; color: #fff; border-color: #2e7d32; }
|
||||
.ip-cat-avoid.on { background: #b23b2e; color: #fff; border-color: #b23b2e; }
|
||||
.ip-cat-clear.on { background: #d9d2c2; }
|
||||
|
||||
.ip-dialog { border: 0; padding: 0; border-radius: 12px; width: min(92vw, 1100px); height: 90vh; max-width: 92vw; max-height: 90vh; background: #f7f4ec; overflow: hidden; display: flex; flex-direction: column; }
|
||||
.ip-dialog:not([open]) { display: none; }
|
||||
.ip-dialog::backdrop { background: #000000cc; }
|
||||
.ip-modal-bar { display: flex; justify-content: space-between; align-items: center; padding: 10px 14px; border-bottom: 1px solid #d9d2c2; flex: 0 0 auto; }
|
||||
.ip-full-frame { flex: 1 1 auto; width: 100%; border: 0; background: #fff; }
|
||||
</style>
|
||||
214
plugin-designs/india-posters/src/india-gallery.vue
Normal file
214
plugin-designs/india-posters/src/india-gallery.vue
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
<!--
|
||||
india-gallery.vue — the review gallery itself (header, summary, cards, grading, modal).
|
||||
|
||||
★ DEV-ONLY MODULE. This component is imported ONLY from india-posters.vue behind
|
||||
import.meta.env.DEV (a compile-time false in every build), so in a production build Rollup never
|
||||
pulls this module into the graph — and with it, attempts.json, the grades seed, and every poster
|
||||
document stay out of public/ entirely. That is why this file may use ordinary static imports and
|
||||
import.meta.glob freely: none of it is reachable from a shipped bundle. See india-posters.vue +
|
||||
README.md for the B25 rationale. Styling lives in india-posters.vue (the custom element's root),
|
||||
injected once into the shadow root, so this child needs no <style> of its own.
|
||||
-->
|
||||
<template>
|
||||
<div class="ip-gallery">
|
||||
<header class="ip-top">
|
||||
<div>
|
||||
<h1>India Posters — design attempts</h1>
|
||||
<p class="ip-sub">
|
||||
Every attempt at putting all ~855 India people groups on one poster. Open each, then
|
||||
<b>label</b> it, score it <b>1–10</b>, and tag it <b>Best practice</b> or <b>To avoid</b>
|
||||
so we keep a record of what to repeat and what to steer clear of.
|
||||
<em>Preview only — withheld from every public build.</em>
|
||||
</p>
|
||||
</div>
|
||||
<div class="ip-actions">
|
||||
<button class="ip-btn" @click="exportJson">Export record (JSON)</button>
|
||||
<label class="ip-btn ip-file">
|
||||
Import record
|
||||
<input type="file" accept="application/json" @change="importJson" hidden />
|
||||
</label>
|
||||
<span class="ip-saved" v-if="savedFlash">saved ✓</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="ip-summary" v-if="graded.best.length || graded.avoid.length">
|
||||
<div class="ip-col">
|
||||
<h2 class="ip-h-best">Best practice ({{ graded.best.length }})</h2>
|
||||
<ol>
|
||||
<li v-for="a in graded.best" :key="a.id">
|
||||
<span class="ip-score ip-score-best">{{ rec[a.id].score }}</span>
|
||||
{{ rec[a.id].label || a.title }}
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="ip-col">
|
||||
<h2 class="ip-h-avoid">To avoid ({{ graded.avoid.length }})</h2>
|
||||
<ol>
|
||||
<li v-for="a in graded.avoid" :key="a.id">
|
||||
<span class="ip-score ip-score-avoid">{{ rec[a.id].score }}</span>
|
||||
{{ rec[a.id].label || a.title }}
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="ip-grid">
|
||||
<article
|
||||
v-for="a in attempts"
|
||||
:key="a.id"
|
||||
class="ip-card"
|
||||
:class="{ 'is-best': rec[a.id].category === 'best', 'is-avoid': rec[a.id].category === 'avoid' }"
|
||||
>
|
||||
<div class="ip-card-head">
|
||||
<span class="ip-family">{{ a.family }}</span>
|
||||
<span class="ip-size">{{ a.size }}</span>
|
||||
</div>
|
||||
<h3 class="ip-title">{{ a.title }}</h3>
|
||||
|
||||
<div class="ip-preview" @click="open(a)">
|
||||
<iframe
|
||||
v-if="srcdoc[a.id]"
|
||||
:srcdoc="srcdoc[a.id]"
|
||||
class="ip-thumb-frame"
|
||||
title="poster preview"
|
||||
></iframe>
|
||||
<button v-else class="ip-load" @click.stop="reveal(a)">Load preview</button>
|
||||
<span class="ip-open-hint" v-if="srcdoc[a.id]">click to open full →</span>
|
||||
</div>
|
||||
|
||||
<dl class="ip-facts">
|
||||
<dt>What we tried</dt>
|
||||
<dd>{{ a.tried }}</dd>
|
||||
<dt>Measured result</dt>
|
||||
<dd>{{ a.result }}</dd>
|
||||
</dl>
|
||||
|
||||
<div class="ip-grade">
|
||||
<label class="ip-label-row">
|
||||
<span>Label</span>
|
||||
<input type="text" v-model="rec[a.id].label" :placeholder="a.title" @input="save" />
|
||||
</label>
|
||||
|
||||
<div class="ip-score-row">
|
||||
<span>Score</span>
|
||||
<input type="range" min="0" max="10" step="1" v-model.number="rec[a.id].score" @input="save" />
|
||||
<output>{{ rec[a.id].score === 0 ? '–' : rec[a.id].score }}/10</output>
|
||||
</div>
|
||||
|
||||
<div class="ip-cat-row">
|
||||
<button class="ip-cat ip-cat-best" :class="{ on: rec[a.id].category === 'best' }" @click="setCat(a.id, 'best')">Best practice</button>
|
||||
<button class="ip-cat ip-cat-avoid" :class="{ on: rec[a.id].category === 'avoid' }" @click="setCat(a.id, 'avoid')">To avoid</button>
|
||||
<button class="ip-cat ip-cat-clear" :class="{ on: !rec[a.id].category }" @click="setCat(a.id, '')">Ungraded</button>
|
||||
</div>
|
||||
|
||||
<label class="ip-notes-row">
|
||||
<span>Notes</span>
|
||||
<textarea v-model="rec[a.id].notes" rows="2" @input="save" placeholder="What to repeat / what to steer clear of…"></textarea>
|
||||
</label>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<!-- Native <dialog> + showModal(): its top-layer promotion escapes the shell pane's
|
||||
contain:layout (a position:fixed overlay would be trapped in the pane), and Escape-to-close
|
||||
comes for free. @click.self closes on a backdrop click. -->
|
||||
<dialog ref="dlg" class="ip-dialog" @close="openId = ''" @click.self="closeDialog">
|
||||
<div class="ip-modal-bar">
|
||||
<strong>{{ current.title }}</strong>
|
||||
<button class="ip-btn" @click="closeDialog">Close ✕</button>
|
||||
</div>
|
||||
<iframe v-if="openId" :srcdoc="srcdoc[openId]" class="ip-full-frame" title="poster full"></iframe>
|
||||
</dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, computed, onMounted, nextTick } from 'vue'
|
||||
import data from '../data/attempts.json'
|
||||
import seed from '../data/grades.seed.json'
|
||||
|
||||
const STORE_KEY = 'india-posters.grades.v1'
|
||||
// Lazy per-poster loaders: each resolves the poster document as a string, on demand. Fine here —
|
||||
// this whole module is dev-only, so none of these assets reach a production build.
|
||||
const posters = import.meta.glob('../data/posters/*.html', { query: '?raw', import: 'default' })
|
||||
|
||||
const attempts = data.attempts
|
||||
const openId = ref('')
|
||||
const dlg = ref(null)
|
||||
const current = computed(() => attempts.find((a) => a.id === openId.value) || {})
|
||||
const srcdoc = reactive({}) // id → loaded poster HTML string
|
||||
const savedFlash = ref(false)
|
||||
|
||||
const blank = (a) => ({ label: a.title, score: 0, category: '', notes: '' })
|
||||
const rec = reactive({})
|
||||
for (const a of attempts) rec[a.id] = blank(a)
|
||||
|
||||
function load() {
|
||||
const merged = { ...(seed || {}) }
|
||||
try {
|
||||
const raw = window.localStorage.getItem(STORE_KEY)
|
||||
if (raw) Object.assign(merged, JSON.parse(raw))
|
||||
} catch {}
|
||||
for (const a of attempts) if (merged[a.id]) Object.assign(rec[a.id], merged[a.id])
|
||||
}
|
||||
let flashT
|
||||
function save() {
|
||||
try {
|
||||
window.localStorage.setItem(STORE_KEY, JSON.stringify(rec))
|
||||
savedFlash.value = true
|
||||
clearTimeout(flashT)
|
||||
flashT = setTimeout(() => (savedFlash.value = false), 1200)
|
||||
} catch {}
|
||||
}
|
||||
function setCat(id, cat) {
|
||||
rec[id].category = rec[id].category === cat ? '' : cat
|
||||
save()
|
||||
}
|
||||
async function reveal(a) {
|
||||
if (srcdoc[a.id]) return
|
||||
const loader = posters[`../data/posters/${a.file}`]
|
||||
if (!loader) return
|
||||
try { srcdoc[a.id] = await loader() } catch {}
|
||||
}
|
||||
async function open(a) {
|
||||
await reveal(a)
|
||||
openId.value = a.id
|
||||
await nextTick()
|
||||
if (dlg.value && !dlg.value.open) dlg.value.showModal()
|
||||
}
|
||||
function closeDialog() {
|
||||
if (dlg.value && dlg.value.open) dlg.value.close()
|
||||
else openId.value = ''
|
||||
}
|
||||
|
||||
const graded = computed(() => {
|
||||
const best = attempts.filter((a) => rec[a.id].category === 'best').sort((x, y) => rec[y.id].score - rec[x.id].score)
|
||||
const avoid = attempts.filter((a) => rec[a.id].category === 'avoid').sort((x, y) => rec[y.id].score - rec[x.id].score)
|
||||
return { best, avoid }
|
||||
})
|
||||
|
||||
function exportJson() {
|
||||
const blob = new Blob([JSON.stringify(rec, null, 2)], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = 'india-posters.grades.json'
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
function importJson(e) {
|
||||
const file = e.target.files && e.target.files[0]
|
||||
if (!file) return
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
try {
|
||||
const obj = JSON.parse(String(reader.result))
|
||||
for (const a of attempts) if (obj[a.id]) Object.assign(rec[a.id], obj[a.id])
|
||||
save()
|
||||
} catch { /* ignore malformed */ }
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
This folder is GENERATED by `bun run build`; do not edit it by hand. It holds exactly one thing:
|
||||
|
||||
- **`dx-maps/`** — the output folder: 28 plugin bundles and 43 pages, self-contained.
|
||||
- **`dx-maps/`** — the output folder: 29 plugin bundles and 44 pages, self-contained.
|
||||
Copy this one folder to any web host (or open it from disk) and it works.
|
||||
- `dx-maps/index.html` — the front page: the Plugin Studio, every plugin in one place.
|
||||
- `dx-maps/<plugin>/<plugin>.js` — a plugin's bundle (a custom element; load it with a `<script>` tag).
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
"schema": "doxa-maps-manifest@1",
|
||||
"generatedFrom": "plugin-designs/*/*.vue",
|
||||
"embedBase": ".",
|
||||
"bundleCount": 28,
|
||||
"profileCount": 43,
|
||||
"bundleCount": 29,
|
||||
"profileCount": 44,
|
||||
"runtime": {
|
||||
"apiBaseUrl": "https://pray.doxa.life"
|
||||
},
|
||||
|
|
@ -925,6 +925,34 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"bundle": "india-posters",
|
||||
"scriptUrl": "./india-posters/india-posters.js",
|
||||
"output": "dx-maps/india-posters/india-posters.js",
|
||||
"integrity": "sha384-AZTLfyEPBd7yrap4tKeJX5SeauB35KtoqOFf82B1Yan6FbgN+p0B+Y3bl1cqhvYr",
|
||||
"elements": [
|
||||
"india-posters"
|
||||
],
|
||||
"stage": "experimental",
|
||||
"profiles": [
|
||||
{
|
||||
"profile": "india-posters",
|
||||
"stage": "experimental",
|
||||
"label": "India Posters (design attempts — preview only)",
|
||||
"file": "india-posters.vue",
|
||||
"description": "A preview-only review board for the whole-India poster problem — fitting every one of",
|
||||
"element": "india-posters",
|
||||
"type": "single",
|
||||
"isMap": false,
|
||||
"iframeUrl": "./india-posters/india-posters/index.html",
|
||||
"scriptUrl": "./india-posters/india-posters.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./india-posters/india-posters/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./india-posters/india-posters.js\" integrity=\"sha384-AZTLfyEPBd7yrap4tKeJX5SeauB35KtoqOFf82B1Yan6FbgN+p0B+Y3bl1cqhvYr\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<india-posters profile-config='{\"profile\":\"india-posters\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></india-posters>"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"bundle": "my-upg-100-list",
|
||||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
|
|
|
|||
20
public/dx-maps/india-posters/india-posters.js
Normal file
20
public/dx-maps/india-posters/india-posters.js
Normal file
File diff suppressed because one or more lines are too long
250
public/dx-maps/india-posters/india-posters/index.html
Normal file
250
public/dx-maps/india-posters/india-posters/index.html
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<!-- THIS PAGE IS OURS. The only thing that knows a document belongs to the plugin is the page
|
||||
the generator writes, so it says so here. library/i18n reads it before reflecting an RTL
|
||||
locale on <html dir>; nothing else sets it, so a PARTNER page defaults to "not ours" and
|
||||
is never touched. Absence must mean restraint — an embedded plugin flipping a partner's
|
||||
whole <html> because a reader followed a ?lang=ar link is the bug this prevents. -->
|
||||
<script>window.MAP_OWNS_PAGE = true;</script>
|
||||
<title>india-posters · india-posters</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<!-- NON-MAP leaf (app-shell / composite / grid / card / chart-table): NO map peer.
|
||||
This element draws no map tiles, so it needs no Mapbox/MapLibre engine and MUST NOT
|
||||
gate on a token. Any nested MAP (in a composite/shell) renders in its own iframe leaf,
|
||||
which loads its own map peer + token. A peer here would be dead weight + imply a gate. -->
|
||||
<style>
|
||||
/* BARE full-page map — no chrome. The custom element IS the page, so the
|
||||
browser URL shares the map itself.
|
||||
HEIGHT IN svh, NOT inset:0. A fixed box with inset:0 follows the phone's DYNAMIC viewport, so the map
|
||||
(and a legend sheet sized from it) changes height every time the address bar hides or shows. 100svh is
|
||||
the viewport WITH the bar shown and never changes; when the bar hides, the page background shows below
|
||||
instead of the map resizing. On a desktop svh = the viewport, so nothing moves there. The 100% before it
|
||||
is the fallback for a browser without svh, which would otherwise size the box to its content.
|
||||
--profile-min-h:0 opts out of ProfileLoader's 100dvh floor, which would grow the element again. */
|
||||
html, body { height: 100%; margin: 0; background: #0a0e15; }
|
||||
#slot, india-posters { position: fixed; inset: 0 auto auto 0; width: 100%; height: 100%; height: 100svh; --profile-min-h: 0; display: block; }
|
||||
#status { position: fixed; inset: 0; z-index: 2; display: grid; place-items: center;
|
||||
padding: 24px; text-align: center; color: #9aa7b8;
|
||||
font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace; }
|
||||
#status.err { color: #f87171; }
|
||||
|
||||
/* ── PRINT, BOTH LEAF SHAPES ────────────────────────────────────────────────────────────
|
||||
Two things every generated leaf got wrong on paper, because no generated page has ever
|
||||
carried an @media print rule (measured: 0 across the tree).
|
||||
|
||||
The background is #0a0e15 — a dark page, printed. Browsers drop background colours by
|
||||
default, but a reader who ticks "Background graphics" to get the MAP also gets the page
|
||||
flooded black, and that is the reader who most wants the printout.
|
||||
|
||||
#status is the LOADING OVERLAY: position:fixed, inset:0, over everything. On paper it is
|
||||
a full sheet reading "loading…" above the content that did in fact load. It is hidden
|
||||
only while it is NOT an error — an error still prints, because a blank sheet with the
|
||||
explanation suppressed is worse than a sheet that says what went wrong. */
|
||||
@media print {
|
||||
html, body { height: auto; background: #fff; }
|
||||
#status:not(.err) { display: none; }
|
||||
/* ★ RELEASE THE FIXED HOST, MEASURED (a print check on a035408e2). The leaf pins both the
|
||||
slot and the element with position:fixed; inset:0. A fixed element is painted once, on the
|
||||
first page, so the printed document was exactly ONE viewport: 900px tall, while the content
|
||||
continued to y=1753. Every plugin whose leaf is taller than a screen has been printing its
|
||||
top screenful and silently dropping the rest — the dashboard printed its stats and its map,
|
||||
then blank to the bottom of the sheet.
|
||||
min-height, not a released height alone: a full-bleed map leaf takes its SIZE from this rule,
|
||||
so height:auto with nothing else can leave the element a zero-height box and print an empty
|
||||
page — a worse failure than the one being fixed, and the discriminator is not isMap
|
||||
(region-dashboards is isMap:true and is a scrolling dashboard). The floor keeps a definite
|
||||
box for the map case while letting a taller document grow onto page two. */
|
||||
#slot, india-posters { position: static; inset: auto; width: 100%; height: auto; min-height: 14cm; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="slot"></div>
|
||||
<div id="status">loading…</div>
|
||||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <india-posters> custom element. -->
|
||||
<script src="../india-posters.js" integrity="sha384-AZTLfyEPBd7yrap4tKeJX5SeauB35KtoqOFf82B1Yan6FbgN+p0B+Y3bl1cqhvYr" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
// 1. prop — a token hardcoded into this page's profile-config (config.tk)
|
||||
// 2. ?tk= URL param — token carried in the shared link
|
||||
// 3. baked token — opt-in literal token inlined at build time (build-config.token)
|
||||
// A token from rungs 1-3 is used DIRECTLY (no network). Otherwise a token
|
||||
// *endpoint* is resolved and fetched:
|
||||
// 4. window.MAP_TOKEN_URL override 5. configured TOKEN_SRC URL 6. same-origin /api/maps/token
|
||||
// TOKEN_SRC is treated as DATA, never eval'd: the literal "window.MAP_TOKEN_URL"
|
||||
// means "read that global"; anything else is used as a URL — so both a global
|
||||
// name and a plain URL are safe (no SyntaxError footgun).
|
||||
(function () {
|
||||
var slot = document.getElementById('slot');
|
||||
var status = document.getElementById('status');
|
||||
var BASE_CONFIG = {"profile":"india-posters","dataSource":"pray-tools","instanceId":"india-posters-india-posters","assetsBase":"../../assets"};
|
||||
// MapLibre map pages AND shell pages (app-shell / nested-mfe-spa) need NO token:
|
||||
// the ladder below is skipped entirely and the element mounts immediately.
|
||||
// (A leaf whose engine is MapLibre at RUNTIME joins them — see PROVIDER below.)
|
||||
var TOKEN_FREE = true;
|
||||
// A token-free COMPOSITE still nests plugins, and a nested Mapbox map reads window.MAP_TOKEN
|
||||
// rather than this page's profile-config. With no baked token (the public copy), it must get
|
||||
// one from ?tk= or a pasted token, or be asked for one — see the rungs below.
|
||||
var NESTS_MAPS = false;
|
||||
var TOKEN_SRC = "window.MAP_TOKEN_URL";
|
||||
var BAKED_TOKEN = "";
|
||||
// Expose the baked token as window.MAP_TOKEN so a BAKED composite (a compiled element that mounts
|
||||
// a map component directly, with no ProfileLoader/profile-config wrapper) still finds a token —
|
||||
// useMapInstance falls back to window.MAP_TOKEN. No effect when BAKED_TOKEN is empty.
|
||||
// A token the HOST set before this page ran outranks the baked one (measured: it used to lose).
|
||||
var HOST_TOKEN = (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN) || '';
|
||||
if (BAKED_TOKEN && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = BAKED_TOKEN; }
|
||||
var DEFAULT_TOKEN_URL = '/api/maps/token';
|
||||
|
||||
function fail(msg) { if (status) { status.className = 'err'; status.textContent = '⚠ ' + msg; } }
|
||||
// No token anywhere on the ladder → ask ONCE, store locally, remount. No dead ends.
|
||||
function askToken() {
|
||||
if (!status) return;
|
||||
status.className = 'err';
|
||||
status.innerHTML = 'map needs a Mapbox token — ' +
|
||||
'<input id="tkIn" type="password" placeholder="paste pk.… (stays in this browser)" ' +
|
||||
'style="font:12px ui-monospace,monospace;padding:6px 8px;border-radius:6px;border:1px solid #223047;background:#0e1623;color:#eef2f7;width:min(320px,70vw)"> ' +
|
||||
'<button id="tkGo" style="font:700 11px ui-monospace,monospace;padding:7px 12px;border-radius:999px;border:0;cursor:pointer;background:linear-gradient(90deg,#46d4ff,#b9a6f5);color:#0a0e15">use it</button>';
|
||||
var go = document.getElementById('tkGo');
|
||||
if (go) go.addEventListener('click', function () {
|
||||
var v = (document.getElementById('tkIn').value || '').trim();
|
||||
if (!v) return;
|
||||
try { localStorage.setItem('MAPS_STAGING_TK', v); } catch (e) {}
|
||||
mount(v);
|
||||
});
|
||||
}
|
||||
/* ★ RELEASE THE LOAD BACKDROP WHEN THE ELEMENT MOUNTS (BUG-GOSPEL-MAP-BLACK-RECTANGLE).
|
||||
html/body carry background:#0a0e15 so the light #status text (#9aa7b8) is readable while the
|
||||
bundle loads. That backdrop is LOAD-STATE CHROME, but it outlived its moment: any gap the
|
||||
mounted element does not cover shows it through, because every layer above body is
|
||||
transparent. On gospel-resources/resources-map that produced a 352x126 DARK RECTANGLE between
|
||||
the legend card and the detail panel — MEASURED rgb(8,12,18), i.e. the page background, NOT an
|
||||
unpainted canvas as first supposed. Clearing it here (the one place we already know the
|
||||
element is up) is a one-shot style write — no resize, no repaint loop. Keep it a one-shot
|
||||
write: do NOT turn this into a per-frame or per-event handler (a resize loop strobes the screen). */
|
||||
function done() {
|
||||
if (status && status.parentNode) { status.parentNode.removeChild(status); }
|
||||
document.documentElement.style.background = '#f3f3f1';
|
||||
document.body.style.background = '#f3f3f1';
|
||||
}
|
||||
|
||||
function mount(token) {
|
||||
if (!token && !TOKEN_FREE) { return askToken(); }
|
||||
// Nested maps (a composite's children) read window.MAP_TOKEN, not this element's config.
|
||||
if (token && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = token; }
|
||||
try {
|
||||
var cfg = token ? Object.assign({}, BASE_CONFIG, { tk: token }) : BASE_CONFIG;
|
||||
var el = document.createElement("india-posters");
|
||||
el.setAttribute('profile-config', JSON.stringify(cfg));
|
||||
slot.innerHTML = '';
|
||||
slot.appendChild(el);
|
||||
done();
|
||||
} catch (e) { fail(e && e.message ? e.message : String(e)); }
|
||||
}
|
||||
|
||||
function qp(name) {
|
||||
try { return new URLSearchParams(location.search).get(name) || ''; }
|
||||
catch (e) { return ''; }
|
||||
}
|
||||
|
||||
function extractToken(data) {
|
||||
if (typeof data === 'string') return data.trim();
|
||||
if (data && typeof data === 'object') return data.token || data.tk || data.accessToken || '';
|
||||
return '';
|
||||
}
|
||||
|
||||
// Rungs 1-3b: an actual token → mount directly, no fetch.
|
||||
// 2b = localStorage: the paste-once store shared with the staging pages
|
||||
// (MAPS_STAGING_TK) — paste a token ONCE anywhere in the tree, every page uses it.
|
||||
var stored = '';
|
||||
try { stored = localStorage.getItem('MAPS_STAGING_TK') || ''; } catch (e) {}
|
||||
var urlTk = qp('tk');
|
||||
if (urlTk) { try { localStorage.setItem('MAPS_STAGING_TK', urlTk); } catch (e) {} }
|
||||
var directToken = (BASE_CONFIG && BASE_CONFIG.tk) || urlTk || stored || HOST_TOKEN || BAKED_TOKEN;
|
||||
// PROVIDER — the same ladder as resolveMapProvider (library/composables/useMapProvider.js):
|
||||
// profile-config.provider → ?provider= → window.MAP_PROVIDER → 'auto'. An explicit engine wins,
|
||||
// and 'auto' draws with MapLibre whether or not a token is available (the element decides that).
|
||||
// This page only decides whether to FIND a token first: when one is on offer (a rung above, a
|
||||
// host-set window.MAP_TOKEN, or a token endpoint) it resolves it, since place search uses it on
|
||||
// either engine; with none, it mounts at once, keyless: no token prompt, no request to Mapbox.
|
||||
var ASKED = String((BASE_CONFIG && BASE_CONFIG.provider) || qp('provider') || window.MAP_PROVIDER || '').toLowerCase().trim();
|
||||
var EXPLICIT = (ASKED === 'mapbox' || ASKED === 'maplibre') ? ASKED : '';
|
||||
var HAS_TOKEN = !!(directToken || (typeof window.MAP_TOKEN === 'string' && window.MAP_TOKEN));
|
||||
var HAS_ENDPOINT = !!(window.MAP_TOKEN_URL || (TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL'));
|
||||
if (EXPLICIT === 'maplibre' || (!EXPLICIT && !HAS_TOKEN && !HAS_ENDPOINT)) { TOKEN_FREE = true; }
|
||||
if (TOKEN_FREE) {
|
||||
if (directToken && typeof window !== 'undefined' && !window.MAP_TOKEN) { window.MAP_TOKEN = directToken; }
|
||||
mount('');
|
||||
// A token-free composite with no token anywhere: its content renders, but its nested maps
|
||||
// cannot. Ask once; the token is stored for the whole tree and the page reloads with it.
|
||||
// Only when Mapbox was ASKED for: under 'auto' the nested maps draw with MapLibre instead.
|
||||
if (NESTS_MAPS && !directToken && EXPLICIT === 'mapbox') {
|
||||
var bar = document.createElement('div');
|
||||
bar.id = 'status';
|
||||
bar.setAttribute('style', 'position:fixed;left:12px;right:12px;bottom:12px;z-index:2147483000;padding:10px 12px;border-radius:10px;background:#0e1623;color:#eef2f7;font:13px system-ui,sans-serif;box-shadow:0 6px 24px rgba(0,0,0,.25)');
|
||||
document.body.appendChild(bar);
|
||||
status = bar;
|
||||
askToken();
|
||||
var goBtn = document.getElementById('tkGo');
|
||||
if (goBtn) goBtn.addEventListener('click', function () {
|
||||
var v = (document.getElementById('tkIn').value || '').trim();
|
||||
if (v) { location.reload(); }
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (directToken) { return mount(directToken); }
|
||||
|
||||
// Rungs 4-6: resolve the token ENDPOINT candidates and try them IN ORDER — the first
|
||||
// one that answers with a token wins. Order: window.MAP_TOKEN_URL override → the
|
||||
// configured TOKEN_SRC (an ABSOLUTE url, so a tree hosted on a CDN away from the API
|
||||
// still finds it) → same-origin /api/maps/token (dev, or the site hosting both).
|
||||
// A single hard-coded pick used to kill the page when its one url 404'd or was
|
||||
// blocked by CORS; a shared link must work wherever the tree is hosted.
|
||||
var candidates = [];
|
||||
function addCandidate(u) { if (u && candidates.indexOf(u) === -1) candidates.push(u); }
|
||||
addCandidate(window.MAP_TOKEN_URL);
|
||||
addCandidate(TOKEN_SRC && TOKEN_SRC !== 'window.MAP_TOKEN_URL' ? TOKEN_SRC : '');
|
||||
addCandidate(DEFAULT_TOKEN_URL);
|
||||
|
||||
function fetchToken(i, errors) {
|
||||
if (i >= candidates.length) {
|
||||
// Every endpoint failed. Under 'auto' that means no token: draw with MapLibre, keyless.
|
||||
if (!EXPLICIT) { TOKEN_FREE = true; return mount(''); }
|
||||
// Mapbox was asked for: say which endpoints failed, and still offer the paste-once box.
|
||||
askToken();
|
||||
if (status) {
|
||||
var d = document.createElement('div');
|
||||
d.textContent = '⚠ Token fetch failed from ' + candidates.join(', ') + ' — set window.MAP_TOKEN_URL or pass ?tk=. (' + errors.join(' | ') + ')';
|
||||
status.insertBefore(d, status.firstChild);
|
||||
}
|
||||
return;
|
||||
}
|
||||
var url = candidates[i];
|
||||
return fetch(url)
|
||||
.then(function (r) {
|
||||
if (!r.ok) throw new Error(url + ' → HTTP ' + r.status);
|
||||
var ct = r.headers.get('content-type') || '';
|
||||
return ct.indexOf('json') !== -1 ? r.json() : r.text();
|
||||
})
|
||||
.then(function (data) {
|
||||
var tk = extractToken(data);
|
||||
if (!tk) throw new Error(url + ' → no token in response');
|
||||
mount(tk);
|
||||
})
|
||||
.catch(function (e) {
|
||||
errors.push(e && e.message ? e.message : String(e));
|
||||
return fetchToken(i + 1, errors);
|
||||
});
|
||||
}
|
||||
fetchToken(0, []);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -2,8 +2,8 @@
|
|||
"schema": "doxa-maps-manifest@1",
|
||||
"generatedFrom": "plugin-designs/*/*.vue",
|
||||
"embedBase": ".",
|
||||
"bundleCount": 28,
|
||||
"profileCount": 43,
|
||||
"bundleCount": 29,
|
||||
"profileCount": 44,
|
||||
"runtime": {
|
||||
"apiBaseUrl": "https://pray.doxa.life"
|
||||
},
|
||||
|
|
@ -925,6 +925,34 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"bundle": "india-posters",
|
||||
"scriptUrl": "./india-posters/india-posters.js",
|
||||
"output": "dx-maps/india-posters/india-posters.js",
|
||||
"integrity": "sha384-AZTLfyEPBd7yrap4tKeJX5SeauB35KtoqOFf82B1Yan6FbgN+p0B+Y3bl1cqhvYr",
|
||||
"elements": [
|
||||
"india-posters"
|
||||
],
|
||||
"stage": "experimental",
|
||||
"profiles": [
|
||||
{
|
||||
"profile": "india-posters",
|
||||
"stage": "experimental",
|
||||
"label": "India Posters (design attempts — preview only)",
|
||||
"file": "india-posters.vue",
|
||||
"description": "A preview-only review board for the whole-India poster problem — fitting every one of",
|
||||
"element": "india-posters",
|
||||
"type": "single",
|
||||
"isMap": false,
|
||||
"iframeUrl": "./india-posters/india-posters/index.html",
|
||||
"scriptUrl": "./india-posters/india-posters.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./india-posters/india-posters/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./india-posters/india-posters.js\" integrity=\"sha384-AZTLfyEPBd7yrap4tKeJX5SeauB35KtoqOFf82B1Yan6FbgN+p0B+Y3bl1cqhvYr\" crossorigin=\"anonymous\"></script>\n<!-- provider: \"maplibre\" is the default; change it to \"mapbox\" and fill tk to use Mapbox. -->\n<india-posters profile-config='{\"profile\":\"india-posters\",\"provider\":\"maplibre\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></india-posters>"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"bundle": "my-upg-100-list",
|
||||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
|
|
|
|||
Loading…
Reference in a new issue